通常,构造函数具有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详解

C++ 构造函数 explicit 关键字 成员初始化列表的更多相关文章

  1. The Semantics of Constructors——2.4 成员初始化列表

    2.4 成员初始化列表(Member Initialization List) 当你写下一个constructor时,就有机会设定class members的初值.要不是经由member initia ...

  2. C++:用成员初始化列表对数据成员初始化

    1.在声明类时,对数据成员的初始化工作一般在构造函数中用赋值语句进行. 例如: class Complex{ private: double real; double imag; public: Co ...

  3. C++类的成员初始化列表的相关问题

    在以下四中情况下,要想让程序顺利编译,必须使用成员初始化列表(member initialization list): 1,初始化一个引用成员(reference member): 2,初始化一个常量 ...

  4. C++: 类成员初始化列表语法

      类的成员初始化列表的初始化的基本语法,类的构造函数还可以运用此语法为其变量初始化: class Class { private: int a; int b; char ch; public: Cl ...

  5. C++ 成员初始化列表

    1.什么是成员初始化列表 #include<iostream> #include<string> using namespace std; class Weapon { pri ...

  6. C++的成员初始化列表和构造函数体(以前未知)

    成员的初始化列表和构造函数在对成员指定初值方面是不一样的.成员初始化列表是对成员初始化,而构造函数,是对成员赋值 成员初始化列表使用初始化的方式来为数据成员指定初值, 而构造函数的函数体是通过赋值的方 ...

  7. C++中成员初始化列表的使用

    C++在类的构造函数中,可以两种方式初始化成员数据(data member). 1,在构造函数的实现中,初始类的成员数据.诸如: class point{private: int x,y;public ...

  8. (转) C++中成员初始化列表的使用

    C++在类的构造函数中,可以两种方式初始化成员数据(data member). 1,在构造函数的实现中,初始类的成员数据.诸如: class point{private: int x,y;public ...

  9. c++类 用冒号初始化对象(成员初始化列表)

    c++类 用冒号初始化对象(成员初始化列表) 成员初始化的顺序不同于它们在构造函数初始化列表中的顺序,而与它们在类定义中的顺序相同 #include<iostream> ; using n ...

  10. 个人学习记录-Cpp基础-成员初始化列表

    Translator     Translator     参考链接: https://blog.csdn.net/XIONGXING_xx/article/details/115553291http ...

随机推荐

  1. Java 中,byte 数据类型的取值范围为什么是 -128 - 127 ?其它数值类型 都雷同

    byte 的取值范围:-128 - 127 基本的数学计算方法,一个byte占8位,第一位为符号位,还有7位,7位能表示最大为:2^7 - 1 怎么来的呢:看如下数学计算 1111 111 = 2^0 ...

  2. Elasticsearch Web管理工具

    Cerebro是一个开源的elasticsearch web管理工具 首先,下载Elasticsearch https://www.elastic.co/guide/en/elasticsearch/ ...

  3. oracle 游标变量ref cursor详解

    一 介绍      像游标cursor一样,游标变量ref cursor指向指定查询结果集当前行.游标变量显得更加灵活因为其声明并不绑定指定查询. 其主要运用于PLSQL函数或存储过程以及其他编程语言 ...

  4. ORACLE SEQUENCE 详解

    1.    About Sequences(关于序列) 序列是数据库对象一种.多个用户可以通过序列生成连续的数字以此来实现主键字段的自动.唯一增长,并且一个序列可为多列.多表同时使用. 序列消除了串行 ...

  5. Android使用SurfaceView实现签名板

    SurfaceView使用 首先创建一个SurfaceViewSign类,继承SurfaceView类,继承 SurfaceHolder.Callback和Runnable接口,代码如下: impor ...

  6. MySQL写入SQL整个执行流程

    innodb存储引擎中一条sql写入的详细流程     第0步:会先去看缓冲区有没有这条数据,如果有就不进行缓存,直接进入第三步.   第1步:会将要修改的那一行数据所在的一整页加载到缓冲池Buffe ...

  7. RESTful API 介绍,设计

    一:RESTful介绍 在互联网发展过程中,最开始是以html静态网页展示内容,url的表现形式一般为 http://www.example.com/getInfo.html:后来随着需求不断提高以及 ...

  8. 谷歌浏览器vue.js devtools插件安装

    github官网 https://github.com/vuejs/vue-devtools#vue-devtools 插件安装地址(需FQ) https://chrome.google.com/we ...

  9. 【Azure Redis 缓存 Azure Cache For Redis】Redis支持的版本及不同版本迁移风险

    问题描述 1. Azure Redis缓存支持的版本包括4.0以及6.0(预览) 这种情形下,可以使用PaaS服务提供的 Azure Redis 缓存(4.0版本).Azure Redis对6.0的支 ...

  10. Java 多线程------解决 实现Runnabel接口方式线程的线程安全问题 方式二:同步方法 +总结

    方式二:同步方法* 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的 1 package bytezero.threadsynchronization; 2 3 4 5 /** ...