C++ 构造函数 explicit 关键字 成员初始化列表
通常,构造函数具有public可访问性,但也可以将构造函数声明为 protected 或 private。构造函数可以选择采用成员初始化表达式列表,该列表会在构造函数主体运行之前初始化类成员。与在构造函数主体中赋值相比,初始化类成员是更高效的方式。首选成员初始化表达式列表,而不是在构造函数主体中赋值。
注意:
- 成员初始化表达式的参数可以是构造函数参数之一、函数调用或 std::initializer_list。
- const 成员和引用类型的成员必须在成员初始化表达式列表中进行初始化。
- 若要确保在派生构造函数运行之前完全初始化基类,需要在初始化表达式中初始化化基类构造函数。
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只能用于修饰只有一个参数的类构造函数,表明该构造函数是显示的而非隐式的。
- explicit关键字的作用就是防止类构造函数的隐式自动转换。
- 如果类构造函数参数大于或等于两个时, 不会产生隐式转换的, explicit关键字无效。
- 例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效。
- 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));
}
创建移动构造函数
- 定义一个空的构造函数,构造函数的参数类型为右值引用;
- 在移动构造函数中,将源对象中的类数据成员添加到要构造的对象;
- 将源对象的数据成员置空。 这可以防止析构函数多次释放资源(如内存)。
MemoryBlock(MemoryBlock&& other)
: _data(nullptr)
, _length(0)
{
_data = other._data;
_length = other._length;
other._data = nullptr;
other._length = 0;
}
创建移动赋值运算符
- 定义一个空的赋值运算符,该运算符参数类型为右值引用,返回一个引用类型;
- 防止将对象赋给自身;
- 释放目标对象中所有资源(如内存),将数据成员从源对象转移到要构造的对象;
- 返回对当前对象的引用。
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
// ...
};
构造函数执行顺序
- 按声明顺序调用基类和成员构造函数。
- 如果类派生自虚拟基类,则会将对象的虚拟基指针初始化。
- 如果类具有或继承了虚函数,则会将对象的虚函数指针初始化。 虚函数指针指向类中的虚函数表,确保虚函数正确地调用绑定代码。
- 执行自己函数体中的所有代码。
如果基类没有默认构造函数,则必须在派生类构造函数中提供基类构造函数参数
下面代码,首先,调用基构造函数。 然后,按照在类声明中出现的顺序初始化基类成员。 最后,调用派生构造函数。
#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 关键字 成员初始化列表的更多相关文章
- The Semantics of Constructors——2.4 成员初始化列表
2.4 成员初始化列表(Member Initialization List) 当你写下一个constructor时,就有机会设定class members的初值.要不是经由member initia ...
- C++:用成员初始化列表对数据成员初始化
1.在声明类时,对数据成员的初始化工作一般在构造函数中用赋值语句进行. 例如: class Complex{ private: double real; double imag; public: Co ...
- C++类的成员初始化列表的相关问题
在以下四中情况下,要想让程序顺利编译,必须使用成员初始化列表(member initialization list): 1,初始化一个引用成员(reference member): 2,初始化一个常量 ...
- C++: 类成员初始化列表语法
类的成员初始化列表的初始化的基本语法,类的构造函数还可以运用此语法为其变量初始化: class Class { private: int a; int b; char ch; public: Cl ...
- C++ 成员初始化列表
1.什么是成员初始化列表 #include<iostream> #include<string> using namespace std; class Weapon { pri ...
- C++的成员初始化列表和构造函数体(以前未知)
成员的初始化列表和构造函数在对成员指定初值方面是不一样的.成员初始化列表是对成员初始化,而构造函数,是对成员赋值 成员初始化列表使用初始化的方式来为数据成员指定初值, 而构造函数的函数体是通过赋值的方 ...
- C++中成员初始化列表的使用
C++在类的构造函数中,可以两种方式初始化成员数据(data member). 1,在构造函数的实现中,初始类的成员数据.诸如: class point{private: int x,y;public ...
- (转) C++中成员初始化列表的使用
C++在类的构造函数中,可以两种方式初始化成员数据(data member). 1,在构造函数的实现中,初始类的成员数据.诸如: class point{private: int x,y;public ...
- c++类 用冒号初始化对象(成员初始化列表)
c++类 用冒号初始化对象(成员初始化列表) 成员初始化的顺序不同于它们在构造函数初始化列表中的顺序,而与它们在类定义中的顺序相同 #include<iostream> ; using n ...
- 个人学习记录-Cpp基础-成员初始化列表
Translator Translator 参考链接: https://blog.csdn.net/XIONGXING_xx/article/details/115553291http ...
随机推荐
- JS leetcode 最长公共前缀 题解分析
壹 ❀ 引 今天做的又是一道让我沮丧的题,思路有,但是代码逻辑最后还是没能正确理出来,题名为最长公共前缀,题目如下: 编写一个函数来查找字符串数组中的最长公共前缀. 如果不存在公共前缀,返回空字符串 ...
- NC16498 [NOIP2014]寻找道路
题目链接 题目 题目描述 在有向图G中,每条边的长度均为1,现给定起点和终点,请你在图中找一条从起点到终点的路径,该路径满足以下条件: 1.路径上的所有点的出边所指向的点都直接或间接与终点连通. 2. ...
- NVME(学习笔记八)—Asymmetric Namespace Report
8.20 非对称namespace访问报告 8.20.1 非对称namespace访问报告概况 非对称Namespace访问(ANA)在如下场景下产生,基于访问这个namespace的controll ...
- python3发送需要双向认证的wss请求
python3发送需要双向认证的wss请求 websocket链接python有很多封装好的库:websocket-client.websockets.aiowebsocket 这里用的websoke ...
- NamedTuple技巧用法
PS: 第一眼看到这个代码的时候,就联想到了go中的构造函数,虽然知道go中的构造函数其实就类比于python中的构造函数__init__,但是不得不说,这个太像了 在日常编码中,我们经常需要写一些返 ...
- 面试官:说一下红锁RedLock的实现原理?
RedLock 是一种分布式锁的实现算法,由 Redis 的作者 Salvatore Sanfilippo(也称为 Antirez)提出,主要用于解决在分布式系统中实现可靠锁的问题.在 Redis 单 ...
- 【Azure 事件中心】Azure Event Hub客户端遇见 Expired Heartbeat 错误
问题描述 Azure Event Hub 在消费数端中,经常性遇见 Expired Heartbeat 错误 (consumer-xxxxxxxxxxxxx-c84873c6c828e8df6c843 ...
- 协程的async使用
async与launch一样都是开启一个协程,但是async会返回一个Deferred对象,该Deferred也是一个job async函数类似于 launch函数.它启动了一个单独的协程,这是一个轻 ...
- 千卡利用率超98%,详解JuiceFS在权威AI测试中的实现策略
2023 年 9 月,AI 领域的权威基准评测 MLPerf 推出了 Storage Benchmark.该基准测试通过模拟机器学习 I/O 负载的方法,在不需要 GPU 的情况下就能进行大规模的性能 ...
- Java 多线程---线程优先级
Java 实例 - 线程优先级设置 以下实例演示了如何通过setPriority() 方法来设置线程的优先级: 1 SimplePriorities.java 文件 2 public class Si ...