当对象在创建时获得了一个特定的值,我们说这个对象被初始化。初始化不是赋值,初始化的含义是创建变量赋予其一个初始值,而赋值的含义是把当前值擦除,而以一个新值来替代。对象初始化可以分为默认初始化、直接初始化、拷贝初始化以及值初始化。
// new edit on 2020.7.23 #pragma once #include <iostream> using namespace std; class ClassTest { public: //定义默认构造函数 ClassTest() { c[0] = '\0'; cout << "1) ClassTest()" << endl; } // 直接初始化 ClassTest(const char* pc) { strcpy_s(c, pc); cout << "2) ClassTest (const char *pc)" << endl; } //复制/拷贝构造函数 ClassTest(const ClassTest& ct) { strcpy_s(c, ct.c); cout << "3) ClassTest(const ClassTest& ct)" << endl; } //重载赋值操作符 ClassTest& operator=(const ClassTest& ct) { strcpy_s(c, ct.c); cout << "4) ClassTest& operator=(const ClassTest &ct)" << endl; return *this; } private: char c[256]; }; ClassTest func(ClassTest temp) { return temp; } int demo_test() { cout << "ct1: "; ClassTest ct1("ab"); // 直接初始化 cout << "ct2: "; ClassTest ct2 = "ab"; // 直接初始化 /* 输出说明:关于编译优化: ClassTest ct2 = "ab"; 它本来是要这样来构造对象的: 首先,调用构造函数ClassTest(const char *pc)函数创建一个临时对象。 然后,调用复制构造函数,把这个临时对象作为参数,构造对象ct2。然而,编译也发现,复制构造函数是 公有的,即你明确地告诉了编译器,你允许对象之间的复制,而且此时它发现可以通过直接调用重载的 构造函数ClassTest(const char *pc)来直接初始化对象,而达到相同的效果,所以就把这条语句优化为 ClassTest ct2("ab")。 */ cout << "ct3: "; ClassTest ct3 = ct1; // 复制初始化 cout << "ct4: "; ClassTest ct4(ct1); // 复制初始化 cout << "ct5: "; ClassTest ct5 = ClassTest(); // 默认构造函数 cout << "\nct6: "; // 依次調用 1)、2)、4),即默认、直接、重载 ClassTest ct6; ct6 = "caoyan is a good boy!"; cout << "\nct7: "; ClassTest ct7; // 依次調用 1)、3)、3)、4) ct7 = func(ct6); return 0; }
old code:

// (1)默认初始化 int i1;//默认初始化,在函数体之外(初始化为0) int f(void) { int i2;//不被初始化,如果使用此对象则报错 } string empty;//empty非显示的初始化为一个空串,调用的是默认构造函数 // (2)拷贝初始化 string str1(10,'9');//直接初始化 string str2(str1);//直接初始化 string str3 = str1;//拷贝初始化 // (3)值初始化 vector<int> v1(10);//10个元素,每个元素的初始化为0 vector<string> v2(10);//10个元素,每个元素都为空 int *pi = new int;//pi指向一个动态分配的,未初始化的无名对象 string *ps = new string;//初始化为空string int *pi = new int;//pi指向一个未初始化的int int *pi = new int(1024);//pi指向的对象的值为1024 string *ps = new string(10,'9');//*ps为"9999999999" string *ps1 = new string;//默认初始化为空string string *ps2 = new string();//值初始化为空string int *pi1 = new int;//默认初始化 int *pi2 = new int();//值初始化为0
1、C++ Copy初始化
在《inside the c++ object model》一书中谈到copy constructor的构造操作,有三种情况下,会以一个object的内容作为另一个object的初值:
- 第一种情况: XX aa = a;
- 第二种情况: XX aa(a);
- 第三种情况: extern fun(XX aa); fun(a)函数调用
- 第四种情况: XX fun(){...}; XX a = fun();函数返回值的时候
下面我们就上述的四种情况来一一验证
#include <iostream> using namespace std; class ClassTest { public: ClassTest() //定义默认构造函数 { c[0] = '\0'; cout << "ClassTest()" << endl; } ClassTest(const char *pc) // 直接初始化 { strcpy_s(c, pc); cout << "ClassTest (const char *pc)" << endl; } ClassTest(const ClassTest &ct) //复制构造函数 { strcpy_s(c, ct.c); cout << "ClassTest(const ClassTest& ct)" << endl; } ClassTest &operator=(const ClassTest &ct) //重载赋值操作符 { strcpy_s(c, ct.c); cout << "ClassTest& operator=(const ClassTest &ct)" << endl; return *this; } private: char c[256]; }; ClassTest func(ClassTest temp) { return temp; } int main() { cout << "ct1: "; ClassTest ct1("ab"); //直接初始化 cout << "ct2: "; ClassTest ct2 = "ab"; //复制初始化 /*输出说明: ClassTest ct2 = "ab"; 它本来是要这样来构造对象的: 首先,调用构造函数ClassTest(const char *pc)函数创建一个临时对象。 然后,调用复制构造函数,把这个临时对象作为参数,构造对象ct2。然而,编译也发现,复制构造函数是 公有的,即你明确地告诉了编译器,你允许对象之间的复制,而且此时它发现可以通过直接调用重载的 构造函数ClassTest(const char *pc)来直接初始化对象,而达到相同的效果,所以就把这条语句优化为 ClassTest ct2("ab")。 */ cout << "ct3: "; ClassTest ct3 = ct1; //复制初始化 cout << "ct4: "; ClassTest ct4(ct1); //直接初始化 cout << "ct5: "; ClassTest ct5 = ClassTest(); //复制初始化 cout << "ct6: "; ClassTest ct6; //复制初始化 ct6 = "caoyan is a good boy!"; cout << "ct7: "; ClassTest ct7; ct7 = func(ct6); return 0; }
测试结果:
我们可以看到,比较复杂的是ct6和ct7,其中ct6还是比较好理解的,ct7这种情况比较难懂,为什么会有两个拷贝构造函数的调用????
第一次拷贝构造函数的调用:第一次很简单,是因为函数参数的传递,将ct6作为参数传递给temp,用ct6的值初始化temp会调用拷贝构造函数;
第二次拷贝构造函数的调用:因为要返回一个ClassTest对象,我们的编译器怎么做????首先它将temp对象拷贝到func函数的上一级栈帧中,它的上一级栈帧是main函数的栈帧,那么当函数返回时,参数出栈,temp对象的内存空间就会被收回,但是它的值已经被拷贝到main栈帧的一个预留空间中,所以从temp到预留空间的拷贝也是调用拷贝构造函数,最后一步就是给ct7赋值,毫无疑问调用赋值构造函数;对栈帧不同的同学可以看看《程序员的自我修养》一书,里面讲得很详细!
2、初始化列表、构造函数与=赋值之间的区别
总所周知,C++对象在创建之时,会由构造函数进行一系列的初始化工作。以没有继承关系的单个类来看,除了构造函数本身的产生与指定,还涉及到初始化步骤,以及成员初始化方式等一些细节,本篇笔记主要对这些细节进行介绍,弄清C++对象在初始化过程中一些基本运行规则。
class MyCppClass {}
#include <iostream>
using std::cout; using std::endl; class MyCppClass { public: MyCppClass() { std::cout <<"In Default Constructor!" <<std::endl; } MyCppClass(const MyCppClass& rhs) { std::cout <<"In Copy Constructor!" <<std::endl; } MyCppClass& operator= (const MyCppClass& rhs) { std::cout <<"In Copy Assignment Operator!" <<std::endl; return *this; } }; int main() { MyCppClass testClass1; // default constructor
MyCppClass testClass2(testClass1); // copy constructor
testClass1 = testClass2; // copy assignment operator
MyCppClass testClass3 = testClass1; // copy constructor
return 0; }
执行结果:

// 数据成员类型为内置类型
class MyCppClass { public: // 赋值操作进行成员初始化
MyCppClass { counter = 0; } // 初始化列表进行成员初始化
MyCppClass : counter(0) { } private: int counter; }
当类的数据成员类型为内置类型时,上面两种初始化方式的效果一样。当数据成员的类型同样也为一个类时,初始化的过程就会有不一样的地方了,比如:
// 数据成员类型为自定义类型:一个类
class MyCppClass { public: // 赋值操作进行成员初始化
MyCppClass(string name) { counter = 0; theName = name; } // 初始化列表进行成员初始化
MyCppClass : counter(0), theName(name) { } private: int counter; string theName; }
在构造函数体内的theName = name这条语句,theName先会调用string的default constructor进行初始化,之后再调用copy assignment opertor进行拷贝赋值。而对于初始化列表来说,直接通过copy constructor进行初始化。
明显起见,可以通过如下的代码进行测试。
#include <iostream> #include <string>
class SubClass { public: SubClass() { std::cout <<" In SubClass Default Constructor!" <<std::endl; } SubClass(const SubClass& rhs) { std::cout <<" In SubClass Copy Constructor!" <<std::endl; } SubClass& operator= (const SubClass& rhs) { std::cout <<" In SubClass Copy Assignment Operator!" <<std::endl; return *this; } }; class BaseClass { public: BaseClass(const SubClass &rhs) { counter = 0; theBrother = rhs; std::cout <<" In BaseClass Default Constructor!" <<std::endl; } BaseClass(const SubClass &rhs, int cnt):theBrother(rhs),counter(cnt) { std::cout <<" In BaseClass Default Constructor!" <<std::endl; } BaseClass(const BaseClass& rhs) { std::cout <<" In BaseClass Copy Constructor!" <<std::endl; } BaseClass& operator= (const BaseClass& rhs) { std::cout <<" In BaseClass Copy Assignment Operator!" <<std::endl; return *this; } private: int counter; SubClass theBrother; }; int main() { SubClass subClass; std::cout <<"\nNo Member Initialization List: " <<std::endl; BaseClass BaseClass1(SubClass); std::cout <<"\nMember Initialization List: " <<std::endl; BaseClass BaseClass2(SubClass, 1); return 0; }
执行结果:

为了更好的理解它们,先对C++当中的数据类型进行简单划分。在C++里面,数据类型大致可以分为两种:第一种是内置类型,比如float, int, double等;第二种是自定义类型,也就是我们常用的class, struct定义的类。在对这些类型的数据进行初始化时,差别就体现出来了:对于内置类型,在使用之前必须进行显示的初始化,而对于自定义类型,初始化责任则落在了构造函数身上。
int x = 0; // 显示初始化x SubClass subClass; // 依赖SubClass的default constructor进行初始化
上面的名词“缺省初始化”描述的就是当内置类型或者自定义类型的数据没有进行显示初始化时的一种初始化状态。而“隐式初始化”描述的是在该状态下面进行的具体操作方式,比如对于内置类型来说,缺省初始化状态下进行的隐式初始化实际上是未定义的,而自定义类型的隐式初始化则依赖于其constructor。
- 对象被用来初始化一个容器元素
- 为映射表添加一个新元素,对象是这个添加动作的副作用
- 定义一个特定长度的容器,对象为容器的元素
#include <iostream> #include <vector> #include <map> #include <string>
using std::cout; using std::endl; using std::vector; using std::map; using std::string; class NumbericInitTestClass { public: void PrintCounter() { cout <<"counter = " <<counter <<endl; } private: int counter; }; int main() { NumbericInitTestClass tnc; tnc.PrintCounter(); map<string, int> mapTest; cout <<mapTest["me"] <<endl; vector<NumbericInitTestClass> vecNumbericTestClass(1); vecNumbericTestClass[0].PrintCounter(); return 0; }

参考文章