C++ 构造函数

构造函数 (C++) | Microsoft Learn

若要自定义类初始化其成员的方式,或是在创建类的对象时调用函数,请定义构造函数。 构造函数具有与类相同的名称,没有返回值。 可以定义所需数量的重载构造函数,以各种方式自定义初始化。 通常,构造函数具有公共可访问性,以便类定义或继承层次结构外部的代码可以创建类的对象。 但也可以将构造函数声明为 protected 或 private

构造函数可以选择采用成员初始化表达式列表与在构造函数主体中赋值相比,初始化类成员是更高效的方式。 

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

声明类的实例时编译器基于重载决策选择要调用的构造函数:

int main()
{
    Box b; // Calls Box()

    // Using uniform initialization (preferred):
    Box b2 {5}; // Calls Box(int)
    Box b3 {5, 8, 12}; // Calls Box(int, int, int)

    // Using function-style notation:
    Box b4(2, 4, 6); // Calls Box(int, int, int)
}
  • 构造函数可以声明为 inlineexplicitfriend 或 constexpr
  • 构造函数可以初始化一个已声明为 constvolatile 或 const volatile 的对象。 该对象在构造函数完成之后成为 const

成员初始化表达式列表

构造函数可以选择具有成员初始化表达式列表,该列表会在构造函数主体运行之前初始化类成员。 (成员初始化表达式列表与类型为 std::initializer_list<T> 的初始化表达式列表不同。)

首选成员初始化表达式列表,而不是在构造函数主体中赋值。 成员初始化表达式列表直接初始化成员。

Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}

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

默认构造函数 default constructor

默认构造函数通常没有参数,但它们可以具有带默认值的参数。

class Box {
public:
    Box() { /*perform any required default initialization steps*/}

    // All params have default values
    Box (int w = 1, int l = 1, int h = 1): m_width(w), m_height(h), m_length(l){}
...
}

默认构造函数特殊成员函数之一。 如果类中未声明构造函数,则编译器提供隐式 inline 默认构造函数。

如果你依赖于隐式默认构造函数,请确保在类定义中初始化成员,如下面的示例所示。 如果没有这些初始化表达式,成员会处于未初始化状态,Volume() 调用会生成垃圾值。 一般而言,即使不依赖于隐式默认构造函数,也最好以这种方式初始化成员。

#include <iostream>
using namespace std;

class Box {
public:
    int Volume() {return m_width * m_height * m_length;}
private:
    int m_width { 0 };
    int m_height { 0 };
    int m_length { 0 };
};

int main() {
    Box box1; // Invoke compiler-generated constructor
    cout << "box1.Volume: " << box1.Volume() << endl; // Outputs 0
}

可以通过隐式默认构造函数定义为已删除来阻止复制对象: 

// Default constructor
    Box() = delete;

复制构造函数 copy constructor

复制构造函数通过从相同类型的对象复制成员值来初始化对象。 如果类成员都是简单类型(如标量值),则编译器生成的复制构造函数已够用,你无需定义自己的函数。 如果类需要更复杂的初始化,则需要实现自定义复制构造函数。 例如,如果类成员是指针,则需要定义复制构造函数以分配新内存,并从其他指针指向的对象复制值。 编译器生成的复制构造函数只是复制指针,以便新指针仍指向其他指针的内存位置。

复制构造函数可能具有以下签名之一:

Box(Box& other); // Avoid if possible--allows modification of other.
    Box(const Box& other);
    Box(volatile Box& other);
    Box(volatile const Box& other);

    // Additional parameters OK if they have default values
    Box(Box& other, int i = 42, string label = "Box");

定义复制构造函数时,还应定义复制赋值运算符 (=)。 有关详细信息,请参阅赋值以及复制构造函数和复制赋值运算符

可以通过将复制构造函数定义为已删除来阻止复制对象:

Box (const Box& other) = delete;

移动构造函数 move constructor

移动构造函数是特殊成员函数,它将现有对象数据的所有权移交给新变量,而不复制原始数据。 它采用 rvalue 引用作为其第一个参数,以后的任何参数都必须具有默认值。 移动构造函数在传递大型对象时可以显著提高程序的效率。

Box(Box&& other);

当对象由相同类型的另一个对象初始化时,如果另一对象即将被毁且不再需要其资源,则编译器会选择移动构造函数。 以下示例演示了一种由重载决策选择移动构造函数的情况。

在调用 get_Box() 的构造函数中,返回值是 xvalue(过期值)。 它未分配给任何变量,因此即将超出范围。

为了为此示例提供动力,我们为 Box 提供表示其内容的大型字符串向量。 移动构造函数不会复制该向量及其字符串,而是从过期值“box”中“窃取”它,以便该向量现在属于新对象。 只需调用 std::move 即可,因为 vector 和 string 类都实现自己的移动构造函数。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

class Box {
public:
    Box() { std::cout << "default" << std::endl; }
    Box(int width, int height, int length)
       : m_width(width), m_height(height), m_length(length)
    {
        std::cout << "int,int,int" << std::endl;
    }
    Box(Box& other)
       : m_width(other.m_width), m_height(other.m_height), m_length(other.m_length)
    {
        std::cout << "copy" << std::endl;
    }
    Box(Box&& other) : m_width(other.m_width), m_height(other.m_height), m_length(other.m_length)
    {
        m_contents = std::move(other.m_contents);
        std::cout << "move" << std::endl;
    }
    int Volume() { return m_width * m_height * m_length; }
    void Add_Item(string item) { m_contents.push_back(item); }
    void Print_Contents()
    {
        for (const auto& item : m_contents)
        {
            cout << item << " ";
        }
    }
private:
    int m_width{ 0 };
    int m_height{ 0 };
    int m_length{ 0 };
    vector<string> m_contents;
};

Box get_Box()
{
    Box b(5, 10, 18); // "int,int,int"
    b.Add_Item("Toupee");
    b.Add_Item("Megaphone");
    b.Add_Item("Suit");

    return b;
}

int main()
{
    Box b; // "default"
    Box b1(b); // "copy"
    Box b2(get_Box()); // "move"
    cout << "b2 contents: ";
    b2.Print_Contents(); // Prove that we have all the values

    char ch;
    cin >> ch; // keep window open
    return 0;
}

显式设置默认构造函数和已删除构造函数

你可以显式设置默认复制构造函数、设置默认构造函数移动构造函数复制赋值运算符移动赋值运算符析构函数。 你可以显式删除所有特殊成员函数。

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

在 C++ 中,如果某个类型未声明它本身,则编译器将自动为该类型生成默认构造函数、复制构造函数、复制赋值运算符和析构函数。 这些函数称为特殊成员函数,它们使 C++ 中的简单用户定义类型的行为如同 C 中的结构。也就是说,无需任何额外的编码工作就可创建、复制和销毁它们。 C++11 会将移动语义引入语言中,并将移动构造函数和移动赋值运算符添加到编译器可自动生成的特殊成员函数的列表中。编译器生成的实现称为默认特殊成员函数

这对于简单类型非常方便,但是复杂类型通常自己定义一个或多个特殊成员函数,这可以阻止自动生成其他特殊成员函数。 实践操作:

  • 如果显式声明了任何构造函数,则不会自动生成默认构造函数。

  • 如果显式声明了虚拟析构函数,则不会自动生成默认析构函数。

  • 如果显式声明了移动构造函数或移动赋值运算符,则:

    • 不自动生成复制构造函数。

    • 不自动生成复制赋值运算符。

  • 如果显式声明了复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符或析构函数,则:

    • 不自动生成移动构造函数。

    • 不自动生成移动赋值运算符。

此外,C++11 标准指定将以下附加规则:

  • 如果显式声明了复制构造函数或析构函数,则弃用复制赋值运算符的自动生成。
  • 如果显式声明了复制赋值运算符或析构函数,则弃用复制构造函数的自动生成。

可以使用 = default 关键字显式声明默认的特殊成员函数这使得编译器仅在需要时才定义函数,就像根本没有声明函数一样。 

若要显式防止自动生成特殊成员函数,可以使用 = delete 关键字将其声明为已删除。

c++新特性:=delete - 知乎 (zhihu.com)

C++11引入的 =delete 是一种特性,它用于明确禁用或删除类的成员函数特殊成员函数、或者其他成员函数=delete 的主要目的是在编译时捕获潜在的错误,并提供更精确的控制,以确保类的行为符合设计要求

使用 =delete 还可以防止意外的函数重载。当有多个重载版本的函数时,有时会出现参数类型非常相似的情况,可能会导致调用时的二义性。通过使用 =delete 可以明确禁用某些重载,以避免二义性错误。

#include <iostream>
void myFunction(int a) {
    std::cout<<a<<std::endl;
}

void myFunction(double) = delete; // 使用=delete禁止double类型的重载

int main() {
    myFunction(42);      // 调用int版本
    // myFunction(3.14); // 无法编译,double版本已被删除
    return 0;
}

显式构造函数 explicit  (转换构造函数--隐式类型转换)

如果类具有带一个参数的构造函数,或是如果除了一个参数之外的所有参数都具有默认值,则参数类型可以隐式转换为类类型。 例如,如果 Box 类具有一个类似于下面这样的构造函数:

Box(int size): m_width(size), m_length(size), m_height(size){}

可以初始化 Box,如下所示:

Box b = 42;

这类转换可能在某些情况下很有用,但更常见的是,它们可能会导致代码中发生细微但严重的错误。 作为一般规则,应对构造函数(和用户定义的运算符)使用 explicit 关键字以防止出现这种隐式类型转换:

explicit Box(int size): m_width(size), m_length(size), m_height(size){}

explicit 关键字会通知编译器指定的转换不能用于执行隐式转换。

委托构造函数

委托构造函数调用同一类中的其他构造函数,以完成部分初始化工作。 在具有多个全都必须执行类似工作的构造函数时,此功能非常有用。 可以在一个构造函数中编写主逻辑,并从其他构造函数调用它。若要添加委托构造函数,请使用 constructor (. . .) : constructor (. . .) 语法。

在以下简单示例中,Box(int) 将其工作委托给 Box(int,int,int):

class Box {
public:
    // Default constructor
    Box() {}

    // Initialize a Box with equal dimensions (i.e. a cube)
    Box(int i) :  Box(i, i, i)  // delegating constructor
    {}

    // Initialize a Box with custom dimensions
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}
    //... rest of class as before
};

继承构造函数(C++11)

派生类可以使用 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:
    // Inherit all constructors from Base
    using Base::Base;

private:
    // Can't initialize newMember from Base constructors.
    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()*/

 一般而言,当派生类未声明新数据成员或构造函数时,最好使用继承构造函数。

复合类以及成员构造

包含类类型成员的类称为“复合类”。 创建复合类的类类型成员时,调用类自己的构造函数之前,先调用构造函数。 当包含的类没有默认构造函数是,必须使用复合类构造函数中的初始化列表。 

将 m_label 成员变量的类型更改为新的 Label 类,则必须调用基类构造函数,并且将 m_label 变量(位于 StorageBox 构造函数中)初始化:

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 Label {
public:
    Label(const string& name, const string& address) { m_name = name; m_address = address; }
    string m_name;
    string m_address;
};

class StorageBox : public Box {
public:
    StorageBox(int width, int length, int height, Label label)
        : Box(width, length, height), m_label(label){}
private:
    Label m_label;
};

int main(){
// passing a named Label
    Label label1{ "some_name", "some_address" };
    StorageBox sb1(1, 2, 3, label1);

    // passing a temporary label
    StorageBox sb2(3, 4, 5, Label{ "another name", "another address" });

    // passing a temporary label as an initializer list
    StorageBox sb3(1, 2, 3, {"myname", "myaddress"});
}

用 constexpr 代替宏

C 和 C++ 中的宏是指编译之前由预处理器处理的标记。 在编译文件之前,宏标记的每个实例都将替换为其定义的值或表达式。 C 样式编程通常使用宏来定义编译时常量值。 但宏容易出错且难以调试。 在现代 C++ 中,应优先使用 constexpr 变量定义编译时常量:

#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

关键字 constexpr 是在 C++11 中引入的,并在 C++14 中进行了改进。 它表示 constant(常数)表达式。 与 const 一样,它可以应用于变量:如果任何代码试图 modify(修改)该值,将引发编译器错误。 与 const 不同,constexpr 也可以应用于函数和类 constructor(构造函数)。 constexpr 指示值或返回值是 constant(常数),如果可能,将在编译时进行计算

每当需要 const 整数时(如在模板自变量和数组声明中),都可以使用 constexpr 整数值。 如果在编译时(而非运行时)计算某个值,它可以使程序运行速度更快、占用内存更少

constexpr 变量

const 与 constexpr 变量之间的主要 difference(区别)是,const 变量的初始化可以推迟到运行时进行。 constexpr 变量必须在编译时进行初始化。 所有的 constexpr 变量都是 const

  • 如果一个变量具有文本类型并且已初始化,则可以使用 constexpr 声明该变量。 如果初始化是由 constructor(构造函数)performed(执行)的,则必须将 constructor(构造函数)声明为 constexpr。
  • 当满足以下两个条件时,引用可以被声明为 constexpr:引用的对象是由 constant(常数)常数表达式初始化的,初始化期间调用的任何隐式转换也是 constant(常数)表达式。
  • constexpr 变量或函数的所有声明都必须具有 constexpr specifier(说明符)。
constexpr float x = 42.0;
constexpr float y{108};
constexpr float z = exp(5, 3);
constexpr int i; // Error! Not initialized
int j = 0;
constexpr int k = j + 1; //Error! j not a constant expression

constexpr 函数

constexpr 函数是在使用需要它的代码时,可在编译时计算其返回值的函数。 使用代码需要编译时的返回值来初始化 constexpr 变量,或者用于提供非类型模板自变量。 当其自变量为 constexpr 值时,函数 constexpr 将生成编译时 constant(常数)。 使用非 constexpr 自变量调用时,或者编译时不需要其值时,它将与正则函数一样,在运行时生成一个值。 (此双重行为使你无需编写同一函数的 constexpr 和非 constexpr 版本。)