前言
C++作为一门非常强大的语言在(Google/Facebook/Microsoft/Baidu/Tencent等)被广泛使用。也是入门成本相对于比较高的一门语言,很多开发过程中的对象的细节,其实可能多年的开发老手都可能注意不到。 本文就是针对C++基础在构造、析构、拷贝、移动等一些部分,针对于实际开发过程中的工程经验。 下面的很多结论都是通过实际功能经验的出来的,并不是说只能这样写,但是这样写可以避免很多问题。
本文档风格约定部分可能跟你的喜好有冲突,请尽量用包容的心态来阅读。
构造函数
构造函数职责
- 单个参数的构造一般需要增加explicit 关键字,防止隐式构造的错误调用。
- 必须使用构造函数初始化列表显式初始化直接基类与所有的基本类型数据成员。
- 没有复制意义的类必须用 DISALLOW_COPY_AND_ASSIGN 宏禁止拷贝构造函数和赋值构造函数。
- 禁止在构造函数中进行可能出错的复杂操作(比如申请资源), 复杂操作用额外的 init() 函数实现。
默认构造函数
显式构造函数
定义:
构造函数职责
- 构造函数:在面向对象编程中通过在对象创建时自动被调用而使对象进入合法状态的一个特别方法。
- 初始化列表:构造函数定义中,参数列表与函数体之间的列表,用于为数据成员赋初值。
默认构造函数(default constructor):
- 形如 MyClass::MyClass() 的构造函数。当程序员没有显式声明/定义任何构造函数时,编译器将自动生成一个默认构造函数。
显示构造函数
- 隐式构造函数(implicit constructor):
形如 MyClass::MyClass(MyArg arg) 的单参数构造函数。 这种函数会定义 MyArg 类型到 MyClass 的隐式转换功能,可能会带来相当大的风险。 - 显式构造函数(explicit constructor):
形如 explicit MyClass::MyClass(MyArg arg) 的单参数构造函数。
解释:
- 显式初始化能够使代码更清晰、不易错误调用,还能避免二次赋值造成的效率问题。
- 当类编写者没有显式声明构造函数时,C++编译器会自动生成默认构造函数与复制构造函数,而其行为往往与程序员的期望不一致。因此,类编写者必须注意,对于没有默认状态的类,应该 private 声明默认构造函数,并不给出实现。
- 使用显式写出的默认构造函数(而非编译器生成的)具有更好的可读性,也可以使用文档化注释向使用者解释默认构造的对象状态。
例子
- 没有名字的人不是合法的人,而且人的名字不应该能够随便变化,所以名字适合在构造函数中传递
#include <iostream>
class Student {
public:
//Student(const std::string& name) : _name(name){} // 尽可能使用初始化列表初始化数据成员
explicit Student(const std::string& name) : _name(name){} // 加上explicit关键字避免隐式构造
Student(const Student& s) {
std::cout << "run Student copy " << std::endl;
_name = s._name;
}
private:
std::string _name;
};
int main(int argc, char* argv[]) {
std::string name = "liutongren";
//如果没有explicit 修饰构造函数,下面这种方式就会成功,会让别人误以为s 是string 类型
//Student s = name;
//上一行注释的代码并不会调用拷贝构造函数, 只有对象一样的对象传递才会调用
//Student s1 = s;
Student s(name);
return 0;
}
- DISALLOW_COPY_AND_ASSIGN 宏,以及其用法:
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
TypeName& operator=(const TypeName&)
class Foo {
public:
explicit Foo(int f);
~Foo();
private:
DISALLOW_COPY_AND_ASSIGN(Foo);
};
- 一个字符串对象,默认值理所当然是空串(“”),所以可以定义默认构造函数。
class String {
public:
String() : // 默认构造函数
_str(_s_empty_c_str) {} // 使用_s_empty_c_str作占位符
~String() {
if (_str != _s_empty_c_str) {
free(_str);
}
}
const char* c_str() const {
return _str;
}
private:
char* _str;
static char _s_empty_c_str[1]; // = ""
};
参考
- Effective C++, item 04: Make sure that objectes are initialized before they’re used
- More Effective C++, Item 04: Avoid gratuitous default constructors 也提到不使用non-trival的构造函数带来的风险和代价
- More Effective C++, item 10: Prevent resource leaks in constructors
- Effective C++, item 06: Explicitly disallow the use of compiler-generated functions you do not want
析构函数
概述
析构函数要么是虚函数OR不能被继承
- 若类定义了虚函数,必须定义虚析构函数。
- 若类设计为可被继承的,应该定义公开的虚析构函数或保护的非虚析构函数。
指针成员必须主动释放
- (不包括结构)含有指针成员,必须显式给出析构函数,并小心指定其行为(是否销毁指针,如何销毁等)。
析构必须处理所有异常
- 绝不允许让异常离开析构函数。
- 析构函数应该用于释放资源,销毁对象,避免执行复杂的操作,尤其***避免执行可能失败***的操作。
定义
析构函数(destructor):形如 MyClass::~MyClass()的函数
解释
- 若基类的析构函数没有声明为虚函数,则 delete pBase; 将不会调用子类析构函数,从而导致错误行为和内存泄漏,因此,避免继承具有公开非虚析构函数的类。
- 编译器默认生成的析构函数不会析构指针成员指向的对象,更不会回收其内存。如果忘记处理的话会导致资源泄漏。
- 程序抛出异常时,会导致栈展开,局部对象依次析构。如果析构过程中再次抛出异常。程序将会立即中止。
###示例 析构函数示例:一个数据库链接的RAII类,能够自动关闭连接,释放资源
class Connection {
public:
void close(); // 关闭数据库连接,具有"无抛掷保证"(定义参见"编程实践->异常安全性"一节)
~Connection() {
if (_connection_state == CONNECTED) {
close(); // 注意:不要让异常离开析构函数,如果调用了会抛出异常的函数,一定要接住异常。
}
}
};
复制构造函数
- 有复制意义的类必须显式给出复制构造函数,并小心指定其行为(浅复制、深复制等)
定义: 复制(拷贝)构造函数(copy constructor):
- 形如 MyClass::MyClass(const MyClass&) 的构造函数。
- 当程序员没有显式声明/定义任何构造函数时,编译器将自动生成一个复制构造函数。
解释
- 托管了资源的类,往往是没有复制意义的。此时应当防止用户错误调用而导致资源泄漏、重复释放的后果。
- 编译器默认生成的复制构造函数,对指针数据成员使用浅复制策略,但这种策略常常不是程序员希望的。
示例
一个可以复制的类
class Point {
public:
Point(int x, int y) : _x(x), _y(y) {}
Point(const Point& other) : // 复制构造函数
_x(other._x), _y(other._y) {}
private:
int _x;
int _y;
}
一个不可以复制的类
RAII类一般都是不可复制
class File {
public:
explicit File(const char* file_name) :
_fp(NULL) {
... // open file, and set _fp
}
~File() {
fclose(_fp);
}
private:
FILE *_fp;
DISALLOW_COPY_AND_ASSIGN(File);
};
备注
RAII 是 resource acquisition is initialization 的缩写,意为“资源获取即初始化”它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。
可移动和可拷贝(c++11以上)
- 可移动&可拷贝
- 如果类型可拷贝, 一定要同时定义拷贝构造函数和赋值构造函数.
- 如果类型可移动, 一定要同时定义移动构造函数和移动赋值函数.
- 如果需要使用默认的拷贝和移动操作, 请使用 = default 定义.
- 如果类型不需要拷贝/移动操作, 请使用 = delete 手段禁用.
- 向容器添加数据时, 优先使用 emplace 开头的接口函数.
- 只在两种情况下使用右值引用, 一种是定义类型的移动操作函数时, 另一种是定义模板函数实现完美转发的时候. 除此之外, 不要使用右值引用
解释
- 可移动类型是C++11专门为优化对象拷贝引入的新特性, 如果类型是可移动的, 很多情况下, 会优先调用类型的移动操作, 可以显著提升性能.
- emplace 这类接口是C++11新增的, 用于原地构造对象, 相比之前的 insert/push_back 接口, 减少一次对象的拷贝/移动开销, 尤其是对于不支持移动的类型来说, 性能开销更小.
- 右值引用的语义比较复杂, 不恰当的使用会造成很难追查的bug
实例
class MyClass {
public:
MyClass(const MyClass& other) = delete; // 禁用拷贝操作
MyClass& operator=(const MyClass& other) = delete;
MyClass(MyClass&& other) = default; // 使用默认的移动操作
MyClass& operator=(MyClass&& other) = default;
};
|