一、定义
《C++primer》对构造函数的定义:
每个类都定义了他的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务时初始化类对象的数据成员,无论何时只要是类的对象被创建,就会执行构造函数。
构造函数有以下特点:
- 函数名与类名相同,大小写敏感。
- 无返回值且无返回类型。
- 可以被重载。
- 构造函数中可以有默认参数。
- 必须是public的。
- 构造函数不能被声明为const的,因为在创建const对象的过程完成之前,这个对象都还是可修改的。
二、分类
默认构造函数
- 创建对象的时候不传参,编译器就会调用默认构造函数来对所创建的对象进行默认初始化。
- 若一个类没有定义任何构造函数,编译器会自动生成一个构造函数,C++primer称之为合成的“默认构造函数”;
- 若定义了任意一个构造函数,编译器不会再生成默认构造函数了;
- 默认构造函数也可以用户自己定义,比如下面的代码;
- 自己定义的默认构造函数也可以设置成编译器生成的“合成默认构造函数”。
-
class Dog:public Animal { public: Dog() = default ; //设置=default之后,之前定义的构造函数就与之冲突了。 public: void act () const; void makeSound ()const; }; Dog::Dog() { cout << "Dog 的构造函数被调用了" << endl; }
这时编译器就会报错了blahblah
-
严重性 代码 说明 项目 文件 行 禁止显示状态 错误 C2084 函数“Dog::Dog(void)”已有主体 ConsoleApplication1 \source\repos\consoleapplication1\consoleapplication1\animals.cpp 21
- 有的时候编译器无法自动为对象生成默认构造函数,比如一个类有一个成员对象,这个对象所属的类没有默认构造函数(自定义了一个非默认构造函数,又没有自定义默认构造函数),这个时候编译器不知道应该如何默认初始化这个成员对象。very logical
- 大多数时候,最好自定义默认构造函数,除非想要创建的对象十分简单,简单到像C语言的基本类型组成的结构体一样简单。
带参数的构造函数
- 带参构造函数是最普通的一种构造函数,理解起来也很简单;
-
Dog(int size) { mSize = size; }
- 有点tricky的是初始化列表这个语法糖,写法是在参数列表和函数体之间,加上成员名带括号将对应的构造参数作为这个成员的初始值传入;
-
class Name { public: Name() { cout << "Name 的构造函数被调用了" << "NULL" << endl; } Name(string &s) { mStr = s; cout << "Name 的构造函数被调用了" << s.c_str() << endl; } public: string mStr; }; class Dog:public Animal { public: Dog() ; Dog(int size) { mSize = size; } Dog(int size,string& name):mSize(size),mName(name){//使用列表初始化初始化成员 cout << "Dog 的构造函数被调用了 " << "size " << mSize << " name " << mName.mStr.c_str() << endl; } public: void act () const; void makeSound ()const; private: int mSize; Name mName; }; void test_animal() { /*const Animal* d = new Dog; d->act(); Animal* dd = new Dog(); dd->act();*/ string name("judy"); Dog h(2, name); // 这次创建会做哪些事情呢? }
-
animal 的构造函数被调用了 Name 的构造函数被调用了judy Dog 的构造函数被调用了 size 2 name judy
- 注意到构造函数的执行顺序是 -> 基类构造-> 初始化列表中的成员->当前类构造函数中的代码;
拷贝构造函数
- 拷贝构造函数在发生值拷贝(传值入参,函数返回对象,定义初始化)的时候被调用;
- 拷贝构造函数容易和赋值函数(等号运算符重载)相混淆。要搞清楚二者的区别,需要仔细地从函数执行的角度分析这个过程中到底有没有对象被创建。例如:
-
Dog(const Dog& dog) { cout << "Dog 的拷贝构造函数进入 " << "size " << mSize << " name " << mName << endl; this->mName = dog.mName; // 这里其实会调用Name类的赋值运算符重载函数; this->mSize = dog.mSize; cout << "Dog 的拷贝构造函数完成 " << "size " << mSize << " name " << mName << endl; } Dog& operator=(const Dog& d) { cout << "Dog 的赋值函数进入 " << "size " << mSize << " name " << mName << endl; this->mName = d.mName; // 这里其实会调用Name类的赋值运算符重载函数; this->mSize = d.mSize; cout << "Dog 的赋值函数完成 " << "size " << mSize << " name " << mName << endl; return *this; } void test_animal_001() { string name("judy"); Dog h(2, name); // 创建了h,调用Dog的参数构造函数 cout << "-----------------------" << endl; Dog x1 = h; // 创建x1,调用基类默认构造,调用成员类默认构造,再调用dog的拷贝构造函数--这里的等号不是赋值,而是初始化 cout << "-----------------------" << endl; Dog x2; // 无参创建x2,同上 cout << "-----------------------" << endl; x2 = h; // 使用h对x2赋值,因而会调用dog的赋值重载函数 }
hello bug animal 的构造函数被调用了 Name 的构造函数被调用了judy Dog 的构造函数被调用了 size 2 name judy ----------------------- animal 的构造函数被调用了 Name 的构造函数被调用了NULL Dog 的拷贝构造函数进入 size -858993460 name Dog 的拷贝构造函数完成 size 2 name judy ----------------------- animal 的构造函数被调用了 Name 的构造函数被调用了NULL Dog 的构造函数被调用了 ----------------------- Dog 的赋值函数进入 size -858993460 name Dog 的赋值函数完成 size 2 name judy 请按任意键继续. . .
-
若修改一下赋值重载函数,则在入参的时候会发生对象拷贝,这样就会先调用到拷贝构造函数了
-
//Dog& operator=(const Dog& d) { Dog& operator=(const Dog d) { //去掉引用符 cout << "Dog 的赋值函数进入 " << "size " << mSize << " name " << mName << endl; this->mName = d.mName; // 这里其实会调用Name类的赋值运算符重载函数; this->mSize = d.mSize; cout << "Dog 的赋值函数完成 " << "size " << mSize << " name " << mName << endl; return *this; } // 则在入参的时候会发生对象拷贝,这样就会先调用到拷贝构造函数了 ----------------------- animal 的构造函数被调用了 Name 的构造函数被调用了NULL Dog 的拷贝构造函数进入 size -858993460 name Dog 的拷贝构造函数完成 size 2 name judy Dog 的赋值函数进入 size -858993460 name Dog 的赋值函数完成 size 2 name judy 请按任意键继续. . .
- 若没有定义拷贝构造函数,则编译器会自动生成。当然这个拷贝是所谓浅拷贝(调用每个成员的拷贝构造函数)的;
- 赋值重载函数同理
类型转换构造函数
- 当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转换构造函数。 在发生类型转换的时候被编译器自动调用
-
Dog x = 2; ----------------------- animal 的构造函数被调用了 Name 的构造函数被调用了NULL Dog 的构造函数被调用了 size 12 name 请按任意键继续. . .
-
更常见的场景是对象作为函数参数时,传入参数可以时这个对象的构造函数的参数,那么就会先调用这个对象的类的转换构造函数,创建这个对象,再把这个对象传入函数。
显式构造函数
- 上面的Dog = 2 有点让人感觉莫名其妙,可以通过限制构造函数的行为来组织这种奇怪的类型自动转换;
-
//Dog(int size) { explicit Dog(int size) { mSize = size; cout << "Dog 的构造函数被调用了 " << "size " << mSize << " name " << mName << endl; } 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 C2440 “初始化”: 无法从“int”转换为“Dog” ConsoleApplication1 \consoleapplication1\consoleapplication1\animals.cpp 49
加上explicit之后,这种转换关系就不存在了。
三、构造函数与类的继承关系
上面的测试显示,派生类在构造的时候(若没有显式调用)会先调用基类的默认构造函数。。。。
四、构造函数与C++对象模型
构造函数相关的内容是比较靠近C++语言设计本质的一个课题。等我啃完了c++对象模型再来更新这两节。