Effective C++笔记之一:声明、定义、初始化与赋值
扫描二维码
随时随地手机看文章
一.声明(Declaration)
声明的作用是指定变量的类型和名称,makes a name known to the program。区分声明和定义可以让C++支持分开编译,比如A.cpp中定义了变量var1,在B.cpp中只需要声明var1这个变量就可以直接使用。因为这样的用法,声明常常见于头文件中。源文件包含头文件之后,就可以使用这个变量,即使没有看到该变量的定义。 声明的语法如下:
extern int i; // object declaration int numDigits(int number); // function declaration class Widget; // class declaration template// template declaration class GraphNode; extern double pi = 3.1416; // definition
二.定义(Definition)
定义是为变量分配存储空间,并可能进行初始化。定义是一种声明,因为定义的同时必然会指定变量的类型和名称,然而声明却不是定义。C++中变量的定义必须有且仅有一次,而变量的声明可以多次。变量一般不能定义在头文件中,除了const变量(local to a file)。
除了变量,类和函数也有定义的说法,总结如下:
1.对于类来说,一般定义在头文件中。因为编译器需要在每个源文件都看到类的定义才能编译成功;
2.对于一般函数来说,函数声明在头文件中,函数定义则在源文件中;
3.对于inline和constexpr function,编译器需要在每个源文件都看到定义,因此通常也定义在头文件中。
int x; // declaration or definition
上面单独的一行,是声明还是定义,判断的原则是看是否占用内存(能否进行初始化)。例如:
class MyClass // 类定义 { int x; // 它是声明,以为C++11之前是不允许在类的定义内部直接初始化数据成员的 float y = 10.0f; // C++11及以后支持这种写法,但它仍然是个声明 static char c; // 这也是个声明,因为如果写成这样 static char c = 'A'; // 编译器会报错,你需要在类外进行定义并初始化,因为类里面的只是声明而已 };
但是如果int x;出现在函数定义内部,它就是一个定义了。例如:
int nurnDigits(int number) // function definition { int x; // object definition,因为此时x是可以被初始化或赋值的 x = number/10; return x; } class Widget // class definition { public: Widget(); // function declaration ~Widget(); private: int x; // object declaration int y; } template// template definition class GraphNode { public: GraphNode(); ~GraphNode(); ...... }
这里有一个令人疑惑的地方,头文件的的类MyClass既然是定义,按照“定义”的解释,它应该占有内存,那为何类中包含的内容反而是声明。
因为类是属于用户自定义的数据类型,与内置类型,比如说int,在使用上类似。类定义只是定义了一种类型,也即说明了一个类,并没有实际定义类的对象,定义的是类,定义类描述的是新的类型,而描述新类型并不会开辟内存空间去存储这样一种新类的对象。
三.初始化(Initialization)
初始化是指变量在创建的同时获得的初始值。虽然C++经常用=来初始化一个变量,但是赋值和初始化是两种不同的操作。赋值是变量定义后的操作,效果是改变变量的值,或者说是用新值来替换旧值;而初始化是在变量创建期获得一个值。两者具有本质的区别。下面分别介绍一下C++常见的初始化方式:
default initialization
当我们定义一个变量时,不提供initializer,那么这个变量就是默认初始化(default initialized)的。默认值由变量的类型和变量的定义位置来决定。
对于built-in type,默认值由变量的定义位置决定。在函数外部定义的全局变量(global variable),函数内部定义的局部静态变量(local static object)全部初始化为0。函数内部定义的局部变量是未初始化的;使用未初始化的变量值的行为是未定义的,会带来巨大的潜在风险。
对于class type,由类里的默认构造函数初始化。如果类定义里没有默认构造函数(显示或隐示),则编译出错。
#includeusing namespace std; int a; int main() { static int b; int c; cout << a << endl; cout << b << endl; cout << c<< endl; system("pause"); return 0; }
在VS执行这段代码,输出变量a的值0,b的值为0,同时VS会报错:Run-Time Check Failure #3 — The variable 'c' is being used without being initialized。 变量a和b被默认初始化为0,变量c未被初始化。
list initialization
C++11中提供了一种新的初始化方式,list initialization,以大括号包围。A tour of c++中写到The = form is traditional and dates back to C, but if in doubt, use the general {}-list form。注意这种初始化方式要求提供的初始值与要初始化的变量类型严格统一,用法如下,
// built-in type initialization double d1{2.3}; //ok: direct-list-initialization double d2 = {2.3}; //ok: copy-list-initialization // class type initialization complexz2{d1,d2}; complexz3 = {1,2}; //ok: the = is optional with {...} vectorvec{1,2,3,4,5,6};//ok: a vector of ints long double pi = 3.1415; int a{pi}, b = {pi}; //error: narrowing conversion required int c(pi), d = pi; //ok: implict conversion.
value initialization
value initialization里,built-in type变量被初始化为0,class type的对象被默认构造(一定要有)初始化。这种方式通常见于STL里的vector和数组,且经常与list initialization结合起来使用,为我们初始化全0数组提供了很大的便利。简单用法如下:
vectorivec(10); //ten elements, each initialized to 0 vectorsvec(10); //ten elmenets, each an empty string vectorv1 = {"a", "an", "the"}; //list initialized int a[10] = {}; //ten elements, each initialized to 0 int a2[] = {1,2,3}; //list initialized int a3[5] = {1,2,3}; //equivalent to a3[] = {1,2,3,0,0}
关于类的初始化比较复杂,整理几点:
1.编译器首先编译类成员的声明,包括函数和变量
2.整个类可见后,才编译函数体(所以不管定义顺序,函数里可以用类里的任何变量和函数)
3.C++11提供了in-class initializers机制,C++ Primer里面讲如果编译器支持,推荐使用in-class initializers机制。注意这种机制只支持=,{}形式,不支持()。Constructor Initializer List对变量进行初始化后,才进入构造函数。Constructor Initializer List里忽略的成员变量(为空则相当于全部忽略),会由in-class initializers初始化,或者采取default initialization,然后进入构造函数体,构造函数体实际是给成员二次赋值
4.对于class type成员,会调用其默认构造函数进行default initialization。
5.对于built-in type成员,要么in-class initialization,要么Constructor initializer list。是否会被default initialization与类定义的位置有关,这点和“default initialization”小节中说的built-int type类似
6.类的静态函数成员可以在类内部或者外部定义,而静态数据成员(const除外)则只能在外部定义以及初始化
#includeusing namespace std; class testA { public: testA() { cout << "A-x:" << x << endl; cout << "A-y:" << y << endl; } private: int x; int y = 10; // in-class initializer }; class testB { public: void printf() const { cout << "B:" << data << endl; } private: int data; testA a; }; testB b1; int main() { b1.printf(); testB b2; b2.printf(); system("pause"); return 0; }
如果是动态初始化的对象,输出结果和上图一样,代码如下:
#includeusing namespace std; class testA { public: testA() { cout << "A-x:" << x << endl; cout << "A-y:" << y << endl; } private: int x; int y = 10; // in-class initializer }; class testB { public: void printf() const { cout << "B:" << data << endl; } private: int data; testA a; }; testB *b1=new testB(); int main() { b1->printf(); testB *b2 = new testB; b2->printf(); system("pause"); return 0; }
但是如果在main函数中,对b2进行value initialization,即将testB *b2 =new testB;改成testB *b2 =new testB();,那么类中的built-in type成员都会被default initialization了。输出结果如下所示:
还需注意的是数组的初始化。
定义数组时,如果没有显示提供初始化列表,则数组元素的默认化初始规则同普通变量一样:函数体外定义的内置类型数组,其元素初始为0;函数体内定义的内置类型数组,其元素无初始化;类类型数组无论在哪里定义,皆调用默认构造函数进行初始化,无默认构造函数则必须提供显示初始化列表。
如果定义数组时,仅提供了部分元素的初始列表,其剩下的数组元素,若是类类型则调用默认构造函数进行初始,若是内置类型则初始为0(不论数组定义位置)。
对于动态分配的数组,如果数组元素是内置类型,其元素无初始化;如果数组元素是类类型,依然调用默认构造函数进行初始化。也可以在使用跟在数组长度后面的一对空圆括号对数组元素做值初始化。
例如: int *ptrA = new int[10];
int *ptrB = new int[10] ();
其中ptrA指向的动态数组其元素未初始化,而ptrB指向的动态数组元素被初始化为0。
四.赋值(Assignment)
赋值的结果是左边的操作元,为左值,也就是说,下面的写法语法正确
int a = 0; (a = 0) = 1; // the final value of a is 1
因为赋值操作符的优先级很低,该带括号的时候不能遗漏。
顺便提一下++i和i++的区别:前者将操作元增加,并且返回改变后的操作元;后者将操作数增加,返回原先值得拷贝作为结果。前置自增返回的结果是左值,后置自增返回的是右值。前置自增操作符做的无用功少,虽然C++编译器对int和指针类型的后置自增操作符作了优化,C++ Primer推荐如无特殊需求,优先使用前置自增操作符。
数组不支持拷贝初始化或者将一个整体赋值给另一个数组。
int a[] = {0,1,2} int a2[] = a; // error: cannot assign one array to another