@(C Plus Plus)
真的理解初始化吗?
Debug确实是一个非常好的学习方式,但是作为主要的获取知识的途径,还是挺危险的。
原因有两种:其中之一是Bug的来源,多数是来自初学编程的同学,或者是师弟师妹,多少是因为对编程语言的理解有误差。这里就有一个心智的问题,根据教科书上描述的编程语言理解完成程序的编写,编译器对程序却有另外一个想法,这两个想法真的是一致吗?(一个不幸的消息是我们学校在大三的时候才开编译原理这门课,大多数学校也是类似的),实际上大多数的错误是这两种心智模式的差异造成的。这样的提法是在Andrew Koenig 的《C Traps and Pitfalls》导读里面写到的,我也一直在发掘我的心智与编译器的心智的差异,这对于理解编程语言、理解计算机原理有很大的帮助,学生阶段这种心智间的差异还是比较低层次的,比如很少有同学直接写出这样的代码((ClassA *)NULL)->func(0)
,这样问题的来源导致知识的膨胀收到了限制。
另外一个原因是,很难有较成熟的工程概念,这一点可以用井底之蛙来描述,培养这方面的能力还是需要深入学习和实践的,也就是说需要自己组织知识的来源。
总结,我习惯称作是沉淀,用来主动获取知识(Debug是我被动获取知识的策略,俗称懒),技术博客和EverNote是主要的方式,但是文笔水平有限,公布在技术的博客还是需要认真思考!
笔试中会遇到各种奇葩的问题,比如内存的操作,初始化是怎样完成的。
真的理解初始化吗?
还是先看看定义和声明!
- 内置类型对象声明:
extern int x;
- 类类型对象声明:
extern ClassA A;
- 函数声明:
void func(int a);
- 类的声明:
class ClassA;
- 还有模板的声明,这个我暂时不擅长,先放一下。
声明仅仅是通知编译器,有这个东西,只是一个符号,但是具体的细节,并不知道,要想知道细节(对于变量,就是分配内存;对于函数和类,需要详细的定义,比如函数体,类的定义),必须知道定义:
- 内置类型对象定义:
int x;
- 函数定义:给出函数体
- 类的定义:给出类的定义体
有一点需要注意的是,类的定义,是给出类的定义体,在这个定义内,包含的是成员的声明(内联函数和static const 类型的成员除外),每个成员的实现最好是放在类的实现文件内,比如:1
2
3
4
5
6
7
8
9
10
11class ClassB{
public:
ClassB(); // 构造函数,声明
~ClassB(){}; // 析构函数,内联实现
ClassB(ClassB& other); // 拷贝构造函数,声明
private:
int a; // 普通数据成员,声明
const int b; // 常量数据成员,声明,初始化必须在构造函数初始化列表
static int c; // 静态数据成员,声明,只能在类外初始化
static const int d = 0; // 静态常量数据成员,必须这样初始化
};
还有一点经常迷惑,声明是告诉编译器类型说明符和变量名,定义(未初始化)也是给出类型说明符和变量名,但是声明是不分配空间的,定义是要分配空间的,怎么区别:
在C++里面,添加关键字extern
表示声明,表示这个变量的定义在其他地方,这里只是声明,声明可以多次,但是定义智能有一次。
比如:1
2
3
4int index; // 定义
extern int max; // 声明
int a = 10; // 定义并初始化
extern int c = 11; // 声明,定义,并初始化
理解了声明与定义的区别,声明是简单的描述,是个符号,定义是要分配空间的。但是这里面还没有初始值,在变量的定义的时候我们可以给他们初始化,也可以不给初始化,我喜欢的方式在变量(普通变量,类对象,指针变量等等)定义的时候就要初始化,最小化避免未定义类型的错误!当然,有些变量是必须要在定义的时候就要初始化的,比如:const类型的变量(非类的数据成员),引用类型的变量。
内置类型(build-in type)初始化
在CPP语言中,有两种初始化方式,分别是直接初始化(direct-initialization)和复制初始化(copy-initialization),比如:1
2int a(1); // direct-initialization
int b = 2; // copy-initialization
对于内置类型的变量还是比较容易理解的,前者相当于调用构造函数,后者则是复制运算符(=)的特殊用法,在定义式里,表示的是初始化(在创建变量的同时,给他赋予初始值);在普通的表达式里面,表示赋值操作,是消除原有的数据,赋予新的数据。
两种方式的区别呢?这个对于内置类型的变量,两种初始化的区别不明显。
一个问题,我们没有初始化(在程序中经常是这样的),他的初始值是什么呢?
在《C++ Primer(第4版)》中P44有提到:
在函数体外定义的变量的初始值都是0;
在函数体内定义的变量不进行初始化,是不确定的值。
在读《程序员的自我修养-编译、链接与库》的时候提到了这样的解释:放在函数体外面的变量都是全局变量(类的数据成员除外)了,全局变量分为两种,一种是已经初始化的,这类的全局变量被放在数据段(data segment),还有就是没有初始化的,这一类被放在bss段,被默认认为是0,但是在数据段给这些都是0 的符号,分配空间没必要,因此都放在了BSS,更多关于BSS的说明参考《程序员的自我修养-编译、链接与库》。
另外,全局静态变量和局部静态变量具有生命周期与全局变量相同,因此,也是类似的表述。
类类型(class type)初始化
类的对象的初始化呢?
自定义类型的对象的初始化是通过构造函数完成的,这里没有特殊的情况,主要是写法不同,另外就是有两种特殊的情况!
- 提供了默认构造函数的形式;
- 没有提供默认构造函数的形式;
- 拷贝构造;
- 赋值操作符初始化。
举个栗子!
没有提供默认构造函数
编译器为了编译顺利通过,提供一个默认构造函数,并不负责初始化成员变量,成员变量是个不确定值。1
2
3
4
5
6
7
8
9
10
11
12
13
14class ClassA
{
public:
int getA(){return a;};
private:
int a;
};
int main()
{
ClassA ca;
cout<<ca.getA()<<endl;
return 0;
}
显示提供默认构造函数
自己定义一个空的默认构造函数,只要自己定义了一个构造函数,编译器不再提供默认的构造函数,哪怕自己写的函数什么都不做,因为没有初始化成员变量,所以,内置类型的变量值是不确定的(相当于在函数体内定义一个变量,没有初始化),类类型的对象是调用对应的默认构造函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class ClassB
{
public:
ClassB(){};
int getB(){return b;};
private:
int a;
};
int main()
{
ClassB cb;
cout<<cb.getB()<<endl;
return 0;
}
显示提供默认构造函数(默认形参)
可以提供一个默认形参列表,提供默认,这样比较人性化,我的习惯也是这样。
1 | class ClassC |
下面的形式也是对的,不过我感觉前者更好一些,但是两种形式不能同时存在,否则就会出现二义性(ambiguous),1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class ClassC
{
public:
ClassC():c(0){};
int getC(){return c;};
private:
int c;
};
int main()
{
ClassC cc;
cout<<cc.getC()<<endl;
return 0;
}
关于对象的定义形式,也可以按照下面的方式写:1
2
3
4
5
6ClassC cc;
cout<<cc.getC()<<endl;
ClassC cc1 = ClassC();
cout<<cc1.getC()<<endl;
ClassC cc2 = ClassC(2);
cout<<cc2.getC()<<endl;
提供了构造函数,但是没有提供默认构造函数,怎么办?
1 | class ClassD{ |
需要这样使用:1
2
3
4
5ClassD cd; // error
ClassD cd1(2);
cout<<cd1.getD()<<endl;
ClassD cd2 = ClassD(2);
cout<<cd2.getD()<<endl;
另外两个特殊的情况是拷贝构造和赋值运算符了。
拷贝构造
最好是自定义拷贝构造函数和赋值运算符重载,虽然编译器自提供的也可以,但是好的编程习惯还是要保持(演示方便,全用内联了!)!1
2
3
4
5
6
7
8
9
10class ClassE
{
public:
ClassE(int a= 0){e = a;}
ClassE(ClassE& other){e = other.e;}
ClassE& operator=(const ClassE& other){e = other.e;return *this;}
int getE(){return e;}
private:
int e;
};
拷贝复制:1
2
3
4ClassE ce;
cout<<ce.getE()<<endl;
ClassE ce1(ce);
cout<<ce1.getE()<<endl;
赋值运算符初始化
赋值运算符初始化:1
2
3ClassE ce;
ClassE ce2 = ce;
cout<<ce2.getE()<<endl;
上面部分总结的算是比较正规的操作,也就是在实际工程中常用的,但是在探索C++原理方面,这还是基础,要考虑的内容还有很多,比如:
1 构造函数初始化执行顺序(继承体系中);
2 构造函数的初始化列表;
3 临时对象的产生;
4 构造函数的语义学;
5 默认形参与构造函数;
6 显示构造函数与隐式构造函数;
7 函数签名
8 全局变量和BSS
今天实在太冷了,回忆面试的一个某游戏公司的面试题,有点意思,让我发现一些问题。下面程序的输出是什么?
1 | class Test |
参考信息
- 《程序员的自我修养-编译、链接与库》
- 《C++ Primer(第4版)》
- 《深入探索C++对象模型》
- 《Effective C++》
- 《C指针与陷阱》