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详解

相关推荐
徐霞客3203 小时前
Qt入门1——认识Qt的几个常用头文件和常用函数
开发语言·c++·笔记·qt
姆路3 小时前
QT Designer内存飙升
qt
Bruce小鬼5 小时前
QT文件基本操作
开发语言·qt
懷淰メ5 小时前
PyQt飞机大战游戏(附下载地址)
开发语言·python·qt·游戏·pyqt·游戏开发·pyqt5
Mr.Q10 小时前
OpenCV和Qt坐标系不一致问题
qt·opencv
重生之我是数学王子13 小时前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
----云烟----1 天前
QT中QString类的各种使用
开发语言·qt
「QT(C++)开发工程师」1 天前
【qt版本概述】
开发语言·qt
一路冰雨1 天前
Qt打开文件对话框选择文件之后弹出两次
开发语言·qt
老赵的博客1 天前
QT 自定义界面布局要诀
开发语言·qt