C++中的构造函数可以分为6类:默认构造函数、普通构造函数、拷贝构造函数、转换构造函数、移动构造函数、委托构造函数。
1. 默认构造和普通构造
默认构造没有参数,普通构造带有参数。
当没有声明其他构造函数时,编译器会自动生成默认构造函数。
使用=default
可以让编译器显式生成默认构造函数
构造函数调用:括号法、显示法、隐式转换法
注意事项:
- 调用默认构造时,不要加括号。 Person p();这行代码编译器会认为是函数声明,所以不会调用默认构造创建对象。
- 不要使用匿名对象调用拷贝构造。即不要仅仅使用 Person(p2);这种形式
带有参数的构造函数,可以使用初始化列表的方式
初始化列表语法:构造函数(): 属性1(值1), 属性2(值2), ……{}
使用成员初始化列表的效率更高,并且某些时候只能使用初始化列表。因为使用初始化列表是在为类分配内存空间时就设定了默认值,使用=是先把所有变量内存空间分配好了,再设定默认值
class Role
{
int a;
Role():a(b*2){} //此时用初始化列表会出问题,因为还没分配b的空间,不知道b的默认值为10
int b=10;
}
《C++ Primer》中提到在以下三种情况下需要使用初始化成员列表:
-
需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化);
-
需要初始化const修饰的类成员或初始化引用成员数据;
-
子类初始化父类的私有成员;
2. 拷贝构造
拷贝构造函数调用时机:
- 用一个已经创建好的对象初始化一个新对象
- 值传递方式给函数传参
- 以值方式返回局部对象(由于编译器的RVO【返回值优化】,所以不会返回对象时不会调用拷贝构造)
构造函数调用规则:默认情况下,编译器会给类至少添加三个函数:默认构造、拷贝构造、析构函数。如果自定义了有参构造,就不再提供无参构造,但会提供拷贝构造;如果自定义了拷贝构造,就不再提供其他构造函数。
注意:默认拷贝构造是浅拷贝
3. 转换构造
转换构造函数(conversion constructor function) 的作用是将一个其他类型的数据转换成一个类的对象。
当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转换构造函数。转换构造函数是对构造函数的重载。
int a = int(1.23),其作用是将1.23转换为整型1。然而对于用户自定义的类类型,编译系统并不知道如何进行转换,所以需要定义专门的函数来告诉编译系统改如何转换,这就是转换构造函数。
用 转换构造函数可以将一个指定类型的数据转换为类的对象。但是不能反过来将一个类的对象转换为一个其他类型的数据
转换构造函数是隐式转换,使用explicit
可以禁用这种隐式转换,例如
class Complex {
private:
int i;
int j;
public:
// 1. 普通构造函数
Complex(int i, int j) {
this->i = i;
this->j = j;
}
// 2. 转换构造函数
Complex(int i) {
this->i = i;
this->j = 0;
}
};
// 此时会调用转换构造函数
Complex c = 5;
explicit Complex(int i) {
this->i = i;
this->j = 0;
}
// 如果加上explicit后,就会禁用
Complex c = 5; // 此时会报错
4. 移动构造
移动构造是C++11标准中提供的一种新的构造方法。
举个例子,有一本书你不想要了,我想要,有两种方式:
第一,你直接给我书
第二,我拿纸来抄
第一种就是移动构造的方式,在C++中叫完美转发;第二种就是拷贝构造的方式。很明显,第一种方式更高效。
移动构造用于从一个临时对象创建一个新对象,以提高效率和减少内存使用。它们采用右值引用的语法,并将临时对象的资源移动到新对象中,而不是复制它们。参数通常是右值引用类型的对象。
移动构造函数通常在以下情况下被调用:
- 从一个临时对象创建一个新对象
- 将一个临时对象作为参数传递到函数中
- 从函数中返回一个临时对象
移动构造函数适用于具有可移动资源的对象,例如指针、文件句柄、unique_ptr 等,但不适用于具有固定位置内存的对象
5. 委托构造
委托构造也是C++11 引入的。
委托构造函数允许构造函数通过初始化列表调用同一个类的其他构造函数,目的是简化构造函数的书写,提高代码的可维护性,避免代码冗余膨胀。
委托构造函数的成员初始化列表只能包含一个其它构造函数,不能再包含其它成员变量的初始化,且参数列表必须与构造函数匹配。
class A {
int aa,bb;
string cc;
// 非委托构造函数
A (int a, int b, string c):aa(a),bb(b),cc(c){}
//其余构造函数全都委托给另一个构造函数
A():A(0,0,""){}
A(string s):A(0,0,s){}
}
示例
下面给出一个总的小例子:
class Person
{
public:
Person()
{
std::cout << "默认构造" << std::endl;
}
Person(int a)
{
std::cout << "普通构造" << std::endl;
}
Person(const Person& p) //拷贝构造:1.要有const 2.必须是引用类型
{
age = p.age;
std::cout << "拷贝构造" << std::endl;
}
Person& operator=(const Person& p) {
if (this != &p) {
this->age = p.age;
}
std::cout << "赋值运算符" << std::endl;
return *this;
}
Person(float a) {
this->age = (int)a;
std::cout << "转换构造" << std::endl;
}
Person(Person&& p) {
this->age = p.age;
std::cout << "移动构造" << std::endl;
}
private:
int age;
};
Person getPerson() {
return Person(10);
}
int main()
{
//1.括号法
Person p; // 默认构造
Person p2(10); // 普通构造
Person p3(p2); // 拷贝构造
std::cout << std::endl;
//2.显示法
Person p4; // 默认构造
Person p5 = Person(10); // 普通构造
Person p6 = Person(p5); // 拷贝构造
Person(10); // 这是一个匿名对象,当这一行执行完后,系统会立即回收匿名对象。
std::cout << std::endl;
//3.隐式转换法
Person p7 = 10; // 普通构造, 相当于Person p = Person(10);
Person p8 = p7; // 拷贝构造
std::cout << std::endl;
p8 = p6; // 这里调用的是赋值,不是拷贝构造,注意区分
Person p9 = 3.2f; // 转换构造
Person p10 = std::move(p9); // 移动构造
return 0;
}
执行结果如下: