• 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 
    
    using namespace std;
    
    int main()
    {
       // vector 类使用移动语义,通过移动矢量元素(而非复制它们)来高效地执行插入操作。
       vector 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 
    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 
    
    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详解

  • 相关阅读:
    汽车制造业安全有效的设计图纸文件外发系统是什么样的?
    Redis原理学习
    【CSDN|每日一练】代写匿名信
    redis学习五redis的持久化RDB,fork,copyonwrite,AOF,RDB&AOF混合使用
    C++ 通过 #include 了解 Compiler 和 Linker
    js事件循环EventLoop
    VMware ESXi 7.0 Update 3e SLIC 2.6 & macOS Unlocker (2022.07 更新)
    包装印刷行业万界星空科技云MES解决方案
    数字信号处理学习笔记(一):离散时间信号与系统
    又一个千亿市场,冰淇淋也成了创新试验田
  • 原文地址:https://www.cnblogs.com/ITnoteforlsy/p/18074235