C++ 构造函数 explicit 关键字 成员初始化列表
通常,构造函数具有public可访问性,但也可以将构造函数声明为 protected 或 private。构造函数可以选择采用成员初始化表达式列表,该列表会在构造函数主体运行之前初始化类成员。与在构造函数主体中赋值相比,初始化类成员是更高效的方式。首选成员初始化表达式列表,而不是在构造函数主体中赋值。
注意:
- 成员初始化表达式的参数可以是构造函数参数之一、函数调用或 std::initializer_list
。 - const 成员和引用类型的成员必须在成员初始化表达式列表中进行初始化。
- 若要确保在派生构造函数运行之前完全初始化基类,需要在初始化表达式中初始化化基类构造函数。
class Box {
public:
// Default constructor
Box() {}
// Initialize a Box with equal dimensions (i.e. a cube)
explicit Box(int i) : m_width(i), m_length(i), m_height(i) // member init list
{}
// Initialize a Box with custom dimensions
Box(int width, int length, int height)
: m_width(width), m_length(length), m_height(height)
{}
int Volume() { return m_width * m_length * m_height; }
private:
// Will have value of 0 when default constructor is called.
// If we didn't zero-init here, default constructor would
// leave them uninitialized with garbage values.
int m_width{ 0 };
int m_length{ 0 };
int m_height{ 0 };
};
派生构造函数运行之前完全初始化基类
class Box {
public:
Box(int width, int length, int height){
m_width = width;
m_length = length;
m_height = height;
}
private:
int m_width;
int m_length;
int m_height;
};
class StorageBox : public Box {
public:
StorageBox(int width, int length, int height, const string label&) : Box(width, length, height){
m_label = label;
}
private:
string m_label;
};
构造函数可以声明为 inline、explicit、friend 或 constexpr。可以显式设置默认复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符和析构函数。
class Box2
{
public:
Box2() = delete;
Box2(const Box2& other) = default;
Box2& operator=(const Box2& other) = default;
Box2(Box2&& other) = default;
Box2& operator=(Box2&& other) = default;
//...
};
一、默认构造函数
如果类中未声明构造函数,则编译器提供隐式 inline 默认构造函数。编译器提供的默认构造函数没有参数。如果使用隐式默认构造函数,须要在类定义中初始化成员。
class Box {
public:
int Volume() {return m_width * m_height * m_length;}
private:
// 如果没有这些初始化表达式,成员会处于未初始化状态,Volume() 调用会生成垃圾值。
int m_width { 0 };
int m_height { 0 };
int m_length { 0 };
};
如果声明了任何非默认构造函数,编译器不会提供默认构造函数。如果不使用编译器生成的构造函数,可以通过将隐式默认构造函数定义为已删除来阻止编译器生成它。
class Box {
public:
// 只有没声明构造函数时此语句有效
Box() = delete;
Box(int width, int length, int height)
: m_width(width), m_length(length), m_height(height){}
private:
int m_width;
int m_length;
int m_height;
};
int main(){
Box box1(1, 2, 3);
Box box2{ 2, 3, 4 };
Box box3; // 编译错误 C2512: no appropriate default constructor available
Box boxes[3]; // 编译错误 C2512: no appropriate default constructor available
Box boxes[3]{ { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; // 正确
}
二、显式构造函数
如果类的构造函数只有一个参数,或是除了一个参数之外的所有参数都具有默认值,则会发生隐式类型转换。
class Box {
public:
Box(int size): m_width(size), m_length(size), m_height(size){}
private:
int m_width;
int m_length;
int m_height;
};
class ShippingOrder
{
public:
ShippingOrder(Box b, double postage) : m_box(b), m_postage(postage){}
private:
Box m_box;
double m_postage;
}
int main(){
Box b = 42; // 隐式类型转换
ShippingOrder so(42, 10.8); // 隐式类型转换
}
explicit关键字可以防止隐式类型转换的发生。explicit只能用于修饰只有一个参数的类构造函数,表明该构造函数是显示的而非隐式的。
- explicit关键字的作用就是防止类构造函数的隐式自动转换。
- 如果类构造函数参数大于或等于两个时, 不会产生隐式转换的, explicit关键字无效。
- 例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效。
- explicit只能写在在声明中,不能写在定义中。
三、复制构造函数
从 C++11 中开始,支持两类赋值:复制赋值和移动赋值。赋值操作和初始化操作都会导致对象被复制。
赋值:将一个对象的值分配给另一个对象时,第一个对象将复制到第二个对象。
初始化:在声明新对象、按值传递函数参数或从函数返回值时,将发生初始化。
编译器默认会生成复制构造函数。如果类成员都是简单类型(如标量值),则编译器生成的复制构造函数已够用。 如果类需要更复杂的初始化,则需要实现自定义复制构造函数。例如,如果类成员是指针,编译器生成的复制构造函数只是复制指针,以便新指针仍指向原内存位置。
复制构造函数声明方式如下:
Box(Box& other); // 尽量避免这种方式,这种方式允许修改other
Box(const Box& other); // 尽量使用这种方式,它可防止复制构造函数意外更改复制的对象。
Box(volatile Box& other);
Box(volatile const Box& other);
// 后续参数必须要有默认值
Box(Box& other, int i = 42, string label = "Box");
Box& operator=(const Box& x);
定义复制构造函数时,还应定义复制赋值运算符 (=)。如果不声明复制赋值运算符,编译器将自动生成复制赋值运算符。如果只声明复制构造函数,编译器自动生成复制赋值运算符;如果只声明复制赋值运算符,编译器自动生成复制构造函数。 如果未定义显式或隐式移动构造函数,则原本使用移动构造函数的操作会改用复制构造函数。 如果类声明了移动构造函数或移动赋值运算符,则隐式声明的复制构造函数会定义为已删除。
阻止复制对象时,需要将复制构造函数声明为delete。如果要禁止对象复制,应该这样做。
Box (const Box& other) = delete;
三、移动构造函数
当对象由相同类型的另一个对象初始化时,如果另一对象即将被毁且不再需要其资源,则编译器会选择移动构造函数。 移动构造函数在传递大型对象时可以显著提高程序的效率。
#include "MemoryBlock.h"
#include <vector>
using namespace std;
int main()
{
// vector 类使用移动语义,通过移动矢量元素(而非复制它们)来高效地执行插入操作。
vector<MemoryBlock> v;
// 如果 MemoryBlock 没有定义移动构造函数,会按照以下顺序执行
// 1. 创建对象 MemoryBlock(25)
// 2. 复制 MemoryBlock 给push_back
// 3. 删除 MemoryBlock 对象
v.push_back(MemoryBlock(25));
// 如果 MemoryBlock 有移动构造函数,按照以下顺序执行
// 1. 创建对象 MemoryBlock(25)
// 2. 执行push_back时会调用移动构造函数,直接使用MemoryBlock对象而不是复制
v.push_back(MemoryBlock(75));
}
创建移动构造函数
- 定义一个空的构造函数,构造函数的参数类型为右值引用;
- 在移动构造函数中,将源对象中的类数据成员添加到要构造的对象;
- 将源对象的数据成员置空。 这可以防止析构函数多次释放资源(如内存)。
MemoryBlock(MemoryBlock&& other)
: _data(nullptr)
, _length(0)
{
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
}
创建移动赋值运算符
- 定义一个空的赋值运算符,该运算符参数类型为右值引用,返回一个引用类型;
- 防止将对象赋给自身;
- 释放目标对象中所有资源(如内存),将数据成员从源对象转移到要构造的对象;
- 返回对当前对象的引用。
MemoryBlock& operator=(MemoryBlock&& other)
{
if (this != &other)
{
delete[] _data;
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
}
return *this;
}
如果同时提供了移动构造函数和移动赋值运算符,则可以编写移动构造函数来调用移动赋值运算符,从而消除冗余代码。
MemoryBlock(MemoryBlock&& other) noexcept
: _data(nullptr)
, _length(0)
{
*this = std::move(other);
}
四、委托构造函数
委托构造函数就是调用同一类中的其他构造函数,完成部分初始化工作。 可以在一个构造函数中编写主逻辑,并从其他构造函数调用它。委托构造函数可以减少代码重复,使代码更易于了解和维护。
class Box {
public:
// 默认构造函数
Box() {}
// 构造函数
Box(int i) : Box(i, i, i) // 委托构造函数
{}
// 构造函数,主逻辑
Box(int width, int length, int height)
: m_width(width), m_length(length), m_height(height)
{}
};
注意:不能在委托给其他构造函数的构造函数中执行成员初始化
class class_a {
public:
class_a() {}
// 成员初始化,未使用代理
class_a(string str) : m_string{ str } {}
// 使用代理时不能在此初始化成员,否则会出现以下错误
// error C3511: a call to a delegating constructor shall be the only member-initializer
class_a(string str, double dbl) : class_a(str) , m_double{ dbl } {}
// 其它成员正确的初始化方式
class_a(string str, double dbl) : class_a(str) { m_double = dbl; }
double m_double{ 1.0 };
string m_string;
};
注意:构造函数委托语法能循环调用,否则会出现堆栈溢出。
class class_f{
public:
int max;
int min;
// 这样做语法上允许,但是会在运行时出现堆栈溢出
class_f() : class_f(6, 3){ }
class_f(int my_max, int my_min) : class_f() { }
};
五、继承构造函数
派生类可以使用 using 声明从直接基类继承构造函数。一般而言,当派生类未声明新数据成员或构造函数时,最好使用继承构造函数。如果基类的构造函数具有相同签名,则派生类无法从多个基类继承。
#include <iostream>
using namespace std;
class Base
{
public:
Base() { cout << "Base()" << endl; }
Base(const Base& other) { cout << "Base(Base&)" << endl; }
explicit Base(int i) : num(i) { cout << "Base(int)" << endl; }
explicit Base(char c) : letter(c) { cout << "Base(char)" << endl; }
private:
int num;
char letter;
};
class Derived : Base
{
public:
// 从基类 Base 继承全部构造函数
using Base::Base;
private:
// 基类构造函数无法初始化该成员
int newMember{ 0 };
};
int main()
{
cout << "Derived d1(5) calls: ";
Derived d1(5);
cout << "Derived d1('c') calls: ";
Derived d2('c');
cout << "Derived d3 = d2 calls: " ;
Derived d3 = d2;
cout << "Derived d4 calls: ";
Derived d4;
}
/* Output:
Derived d1(5) calls: Base(int)
Derived d1('c') calls: Base(char)
Derived d3 = d2 calls: Base(Base&)
Derived d4 calls: Base()*/
类模板可以从类型参数继承所有构造函数:
template< typename T >
class Derived : T {
using T::T; // declare the constructors from T
// ...
};
构造函数执行顺序
- 按声明顺序调用基类和成员构造函数。
- 如果类派生自虚拟基类,则会将对象的虚拟基指针初始化。
- 如果类具有或继承了虚函数,则会将对象的虚函数指针初始化。 虚函数指针指向类中的虚函数表,确保虚函数正确地调用绑定代码。
- 执行自己函数体中的所有代码。
如果基类没有默认构造函数,则必须在派生类构造函数中提供基类构造函数参数
下面代码,首先,调用基构造函数。 然后,按照在类声明中出现的顺序初始化基类成员。 最后,调用派生构造函数。
#include <iostream>
using namespace std;
class Contained1 {
public:
Contained1() { cout << "Contained1 ctor\n"; }
};
class Contained2 {
public:
Contained2() { cout << "Contained2 ctor\n"; }
};
class Contained3 {
public:
Contained3() { cout << "Contained3 ctor\n"; }
};
class BaseContainer {
public:
BaseContainer() { cout << "BaseContainer ctor\n"; }
private:
Contained1 c1;
Contained2 c2;
};
class DerivedContainer : public BaseContainer {
public:
DerivedContainer() : BaseContainer() { cout << "DerivedContainer ctor\n"; }
private:
Contained3 c3;
};
int main() {
DerivedContainer dc;
}
输出如下:
Contained1 ctor
Contained2 ctor
BaseContainer ctor
Contained3 ctor
DerivedContainer ctor
参考文章:
构造函数 (C++)
QT学习记录(008):explicit 关键字的作用
C++中的explicit详解