C++ 构造函数 explicit 关键字 成员初始化列表

通常,构造函数具有public可访问性,但也可以将构造函数声明为 protected 或 private。构造函数可以选择采用成员初始化表达式列表,该列表会在构造函数主体运行之前初始化类成员。与在构造函数主体中赋值相比,初始化类成员是更高效的方式。首选成员初始化表达式列表,而不是在构造函数主体中赋值。

注意

  1. 成员初始化表达式的参数可以是构造函数参数之一、函数调用或 std::initializer_list 。

  2. const 成员和引用类型的成员必须在成员初始化表达式列表中进行初始化。

  3. 若要确保在派生构造函数运行之前完全初始化基类,需要在初始化表达式中初始化化基类构造函数。

    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只能用于修饰只有一个参数的类构造函数,表明该构造函数是显示的而非隐式的。

  1. explicit关键字的作用就是防止类构造函数的隐式自动转换。
  2. 如果类构造函数参数大于或等于两个时, 不会产生隐式转换的, explicit关键字无效。
  3. 例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效。
  4. 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));

}

创建移动构造函数

  1. 定义一个空的构造函数,构造函数的参数类型为右值引用;

  2. 在移动构造函数中,将源对象中的类数据成员添加到要构造的对象;

  3. 将源对象的数据成员置空。 这可以防止析构函数多次释放资源(如内存)。

    MemoryBlock(MemoryBlock&& other)
    : _data(nullptr)
    , _length(0)
    {
    _data = other._data;
    _length = other._length;
    other._data = nullptr;
    other._length = 0;
    }

创建移动赋值运算符

  1. 定义一个空的赋值运算符,该运算符参数类型为右值引用,返回一个引用类型;

  2. 防止将对象赋给自身;

  3. 释放目标对象中所有资源(如内存),将数据成员从源对象转移到要构造的对象;

  4. 返回对当前对象的引用。

    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
    // ...
};

构造函数执行顺序

  1. 按声明顺序调用基类和成员构造函数。
  2. 如果类派生自虚拟基类,则会将对象的虚拟基指针初始化。
  3. 如果类具有或继承了虚函数,则会将对象的虚函数指针初始化。 虚函数指针指向类中的虚函数表,确保虚函数正确地调用绑定代码。
  4. 执行自己函数体中的所有代码。

如果基类没有默认构造函数,则必须在派生类构造函数中提供基类构造函数参数

下面代码,首先,调用基构造函数。 然后,按照在类声明中出现的顺序初始化基类成员。 最后,调用派生构造函数。

复制代码
#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详解

相关推荐
用户805533698034 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner4 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz9 天前
QML Hello World 入门示例
qt
xcyxiner12 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner13 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner13 天前
DicomViewer (添加模型类)3
qt
xcyxiner14 天前
DicomViewer (目录调整) 2
qt
xcyxiner14 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
桥田智能16 天前
桥田智能 QT-650S:面向白车身焊装的 800kg 重载快换解决方案
开发语言·qt·系统架构
森G16 天前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt