C++默认的类函数
class A {
A() = default;
~A() = default;
A(const A &) = default;
A &operator=(const A &) = default;
A(A &&) = default;
A &operator=(A &&) = default;
};
会默认生成默认构造函数和析构函数,拷贝、移动对应的构造函数和赋值运算符。
一般来说当这些函数需要被调用,而我们没有声明的时候,编译器会自动生成。其自动生成的是没有虚函数属性,除非他是一个派生类,而其基类对应的函数是虚函数
如果类成员对象有引用或者const成员,不会生成默认的拷贝相关的函数。
struct A {
const int a;
int &b;
A(int c, int &d) : a(c), b(d) {}
};
int main() {
int i = 3;
A a(3, i);
A b(4, i);
// b = a; 报错
}
只能手动定义拷贝赋值运算符
不需要自动生成函数时,使用delete
- C++11 中可以通过关键字delete禁止编译器默认生成
- 老旧版本可以通过继承UnCopyable基类来完成禁止默认生成
class UnCopyable {
public:
UnCopyable() = default;
~UnCopyable() = default;
private:
UnCopyable(const UnCopyable &);
UnCopyable &operator=(const UnCopyable &);
};
class A : private UnCopyable{
// ...
}
这样子后续派生类都不允许自动生成拷贝族函数了
为多态基类声明虚析构函数
- 带多态性质的基类应该声明一个虚析构函数。如果类带有任何虚函数,其析构函数应被声明为虚函数。
- 一个类如果不是被设计为基类或者不带有多态性质,就不应被声明为虚函数
将为多态设计的类的析构函数设置为虚函数
实现动态绑定,使指向基类指针的派生类的资源能被正确析构
struct Base {
virtual ~Base() {
printf("Delete Base\n");
}
};
struct A : Base {
~A() {
printf("Delete A\n");
}
};
struct B : A {
~B() {
printf("Delete B\n");
}
};
int main() {
Base *b = new B;
delete b;
{
Base b;
}
}
/*
Delete B
Delete A
Delete Base
---------
Delete B
*/
如果没有将其设为虚函数
struct Base {
// 设为非虚函数
~Base() {
printf("Delete Base\n");
}
};
int main() {
{
Base *b = new B;
delete b;
}
cout << "---------" << endl;
{
Base b;
}
cout << "---------" << endl;
{
B *b = new B;
delete b;
}
cout << "---------" << endl;
{
B b;
}
}
/*
Delete Base
---------
Delete Base
---------
Delete B
Delete A
Delete Base
---------
Delete B
Delete A
Delete Base
*/
所以这一条针对的是指向基类指针的派生类对象类型的正确析构,即:
Base p = new P();
纯虚函数可以被定义
有时候该纯虚函数的定义代码重复率较高,就可以将其显式定义出来,在派生类中可以显式调用
但是存在问题:纯虚函数一般是希望派生类定义自己的相关代码,定义了纯虚函数可能使使用者忽略之
struct Base {
virtual void f1() = 0;
virtual void f2(const string &) = 0;
};
struct A : Base {
virtual void f1() override {
}
};
struct B : Base {
virtual void f1() override {
Base::f1();
}
virtual void f2(const string &s) override {
Base::f2(s);
}
};
void Base::f1() { printf("F1\n"); }
void Base::f2(const string &s) { printf("F2-> %s\n", s.c_str()); }
int main() {
// A 没有实现所有纯虚函数,不能被实例化
// A a;
B b;
b.f1();
b.f2("Hello");
}
// F1
// F2-> Hello
虚函数会扩大对象的大小
有虚函数的类的对象会有一个虚函数指针,指向虚函数表。会占据一个指针大小(按系统32位还是64位划分)。因此没有设计为多态特性的类没必要有虚函数。会浪费空间。
struct Base {
virtual ~Base() {
printf("Delete Base\n");
}
void func() {
}
};
struct A : Base {
~A() {
printf("Delete A\n");
}
};
struct B : Base {
int i;
};
int main() {
int *ptr = nullptr;
cout << "size of ptr is " << sizeof(ptr) << endl;
cout << "size of Base is " << sizeof(Base) << endl;
cout << "size of A is " << sizeof(A) << endl;
cout << "size of B is " << sizeof(B) << endl;
}
/*
size of ptr is 8
size of Base is 8
size of A is 8
size of B is 16
*/
当类内至少含有一个虚函数时,才将析构函数声明为虚函数
析构函数声明为纯虚函数
当希望一个类为虚基类,但是没有其他任何虚函数的时候,可以将析构函数声明为纯虚函数,以此将这个类定义为虚基类。
但是注意纯虚函数的析构函数需要被定义!!否则连接时会报错
struct Base {
virtual ~Base() = 0;
};
struct A : Base {
~A() {
printf("Delete A\n");
}
};
// 需要定义
Base::~Base() noexcept {}
int main() {
Base *p = new A;
A *a = new A;
}
不要让异常逃离析构函数
意思是:析构函数里的异常要在析构函数里捕获,不要让其抛出异常。在析构函数中抛出异常可能会导致程序提前结束或者未定义行为
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该能够捕捉任何异常,然后吞下它们(不处理)或者提前结束程序
- 如果需要在对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数执行该操作(而非在析构函数中)
在析构函数中处理异常
常见的RAII管理资源手段:
class DB{
public:
static DB create();
void close();
};
class DBConn{
private:
DB db;
public:
// ...
~DBConn{
db.close();
}
};
做到了自动释放数据库连接。但是在析构函数中调用关闭数据库操作(close),该操作可能会导致抛出异常。上诉例子中没有对异常处理,允许其离开析构函数,可能会造成错误。
常见处理办法:
// 方法一,结束程序
~DBConn{
try{
db.close();
}catch(...){
// 记录下异常
// 结束程序
std::abort();
}
};
// 方法二,吞下异常
~DBConn{
try{
db.close();
}catch(...){
// 记录下异常
// ...
}
};
提取出可能抛出异常的操作,提前处理
class DB{
public:
static DB create();
void close();
};
class DBConn{
private:
bool closed = false;
DB db;
public:
// 该函数可以抛出异常,被处理
void close(){
db.close();
closed = true;
}
~DBConn{
// 避免忘了手动释放
if(!closed){
try{
db.close();
}catch(...){
// 记录下异常
// ...
}
}
}
};
不要在构造函数或者析构函数中直接或间接调用虚函数
- 不要在构造函数或者析构函数中调用虚函数,因为构造派生类对象时,其基类部分先于派生类部分构造
假设一个交易日志体系,希望在创建的时候自动记录操作,因此在构造函数内加了log操作。
struct Transaction {
Transaction() {
printf("Construct Base\n");
log();
}
virtual void log() const = 0;
};
struct Buy : Transaction {
Buy() {
printf("Construct Buy\n");
log();
}
void log() const override {
printf("Buy log\n");
}
};
struct Sell : Transaction {
void log() const override {
printf("Sell log\n");
}
};
void Transaction::log() const {
printf("Base log\n");
}
int main() {
Buy b;
}
/*
Construct Base
Base log
Construct Buy
Buy log
*/
在构造派生类对象时,其基类部分先于其他类部分被构造。这时候调用虚函数,是调用的其基类版本的虚函数。会有几个错误:
- 如果该函数是个纯虚函数且没有被定义(很多情况都会这样),那么会发生连接错误
- 调用的是其基类版本的虚函数,不会按照动态绑定原则调用其派生类定义的虚函数,与设计目的不同
- 可能会有未定义行为错误
解决办法 将该函数设计为普通函数,派生类的构造函数向基类的构造函数传递必要信息。
struct Transaction {
Transaction(const string &s) {
log(s);
}
void log(const string &) const;
};
struct Buy : Transaction {
Buy(parameters) : Transaction(createLogString(parameters)) {}
static string createLogString(parameters);
};
这里通过静态函数生成log信息,可以避免指向派生类中还未被构造出来的成员,造成程序错误。
令operator=返回一个*this的引用
这样方便于连续赋值
struct Base {
int v;
Base(int i = 0) : v(i) {}
Base &operator=(const Base &rhs) {
if (this != &rhs) {
this->v = rhs.v;
}
return *this;
}
};
int main() {
Base a(-1);
Base p, q;
p = q = a;
cout << p.v << " " << q.v << endl;
}
//-1 -1
在operator=中处理自赋值情况
- 确保对象自我赋值时有正确的行为。其中技术包括①比较地址 ②设计好语句顺序 ③copy-and-swap
- 确定任何函数如果操作一个以上的对象,而其中多个对象仍是同一个对象时(常见于指向同一个对象的不同的指针、引用),其行为依然正确
不要忽视自赋值情况
并不是只有显式自赋值
并不是所有的自赋值都是形如x = x;
这种情况,当涉及到指针和引用时,会存在潜在的自赋值,比如
Base *p = new Base;
Base *a = p, *b = p;
// 发生了自赋值
*a = *b;
Base bN[3];
int i = 0, j = 0;
// 发生了自赋值
bN[i] = bN[j];
所以不要觉得自己不会写出x = x;
这种代码就忽视自赋值
不正确自赋值的危害
struct Data {
// ...
};
struct User {
private:
Data *data;
public:
User &operator=(const User &user) {
delete data;
data = new Data(*user.data);
return *this;
}
};
正确处理自赋值
比较地址
struct User {
private:
Data *data;
public:
User &operator=(const User &user) {
if(this == &user) return *this;
delete data;
data = new Data(*user.data);
return *this;
}
};
**缺点:**不具备异常安全
如果new Data(*user.data)
抛出异常,那么data将指向一个被释放过的内存,这样的指针是有害的。
具备异常安全性,将自动满足自赋值安全性
正确的赋值顺序
struct User {
private:
Data *data;
public:
User &operator=(const User &user) {
Data *cache = data;
data = new Data(*user.data);
delete cache;
return *this;
}
};
这样当new Data(*user.data)
抛出异常时,data将依旧指向原来的内存。
copy and swap
自动具备异常安全
struct User {
private:
Data *data;
public:
// 注意这里的参数是非引用
User &operator=(const User user) {
// 使用了std::swap();
swap(*this, user);
return *this;
}
};
或者
struct User {
private:
Data *data;
public:
User &operator=(const User& user) {
User temp(user);
swap(*this, temp);
return *this;
}
};
这里注意,如果是内置类型的话,一般会自定义swap函数,来完成高效的交换
struct User{
void swap(User &rhs){
// ...
}
}
复制对象时不要遗忘其每个成分
- 拷贝函数应确保复制对象内的每个成员变量以及其基类的成员
- 不要尝试使用某个拷贝函数来实现另一个拷贝函数,共同代码可以抽象为公共函数
派生类的拷贝函数不要遗漏了其基类成员
该派生类的拷贝函数看上去没有什么问题,但是:
struct Base {
int val;
string s;
Base(int v = -1, string ss = "default") : val(v), s(ss) {}
Base(const Base &rhs) : val(rhs.val), s(rhs.s) {}
Base &operator=(const Base &rhs) {
val = rhs.val;
s = rhs.s;
return *this;
}
virtual void show() const { printf("%s %d\n", s.c_str(), val); }
};
struct P : Base {
int k;
P(int j = -1) : k(j) {}
P(const P &rhs) : k(rhs.k) {}
P &operator=(const P &rhs) {
k = rhs.k;
return *this;
}
void show() const override { printf("%s %d %d\n", s.c_str(), val, k); }
};
int main() {
P p(6);
p.s = "Hello", p.val = 1;
p.show();
P newP(p);
newP.show();
P newP2(1);
newP2.s = "newP2";
newP2 = p;
newP2.show();
}
/*
Hello 1 6
default -1 6
newP2 -1 6
*/
可见派生类的拷贝构造函数只拷贝了派生类的部分,基类部分调用了其默认构造函数。派生类的拷贝赋值运算符只拷贝了派生类部分,保持基类部分不变。
在派生类中调用基类的复制函数
所以派生类的拷贝函数需要手动拷贝基类的成员,基类的成员一般都是private的,所以一般调用基类的拷贝函数。
struct P : Base {
int k;
P(int j = -1) : k(j) {}
// 调用基类的拷贝构造函数
P(const P &rhs) : Base(rhs), k(rhs.k) {}
P &operator=(const P &rhs) {
// 调用基类的拷贝赋值运算符
Base::operator=(rhs);
k = rhs.k;
return *this;
}
void show() const override { printf("%s %d %d\n", s.c_str(), val, k); }
};
不同的拷贝函数之间不可以互相调用
**拷贝赋值运算符不可以调用拷贝构造函数:**试图构造一个已经存在的对象,显然不合理
拷贝构造函数不可以调用拷贝赋值运算符: 可能某些成员还没有被初始化。