0%

C++11之default

引言:对于default函数,编译器会自动生成函数体定义,从而获得更高的代码执行效率。同时说明了为什么基类的析构函数一定要显式定义成虚析构函数。

1.背景

C++有4类特殊成员函数,分别是:默认构造函数,默认析构函数,拷贝构造函数以及拷贝赋值运算符,这些类的特殊成员函数负责创建、初始化、销毁,或者拷贝类的对象。如果程序员没有显式地为一个类定义某个特殊成员函数,而又需要用到该特殊成员函数时,则编译器会隐式的为这个类生成一个默认的特殊成员函数。例如:

1
2
3
4
5
6
class X {
private:
int a;
}

X x;

在上述程序中,程序员并没有定义类 X 的默认构造函数,但是在创建类 X 的对象 x 的时候,又需要用到类 X 的默认构造函数,此时,编译器会隐式的为类 X 生成一个默认构造函数。该自动生成的默认构造函数没有参数,包含一个空的函数体,即X::X() {}。虽然自动生成的默认构造函数仅有一个空函数体,但是它仍可用来成功创建类 X 的对象 x,上述程序可以编译通过。

但是,如果程序员为类 X 显式的自定义了非默认构造函数,却没有定义默认构造函数的时候,将会出现编译错误:

1
2
3
4
5
6
7
8
class X {
public:
X(int i) {
a = i;
}
private:
int a;
}; // 编译出错

编译出错的原因在于类 X 已经有了用户自定义的构造函数,所以编译器将不再会为它隐式的生成默认构造函数。如果需要用到默认构造函数来创建类的对象时,程序员必须自己显式的定义默认构造函数。例如:

1
2
3
4
5
6
7
8
9
class X {
public:
X() {}; // 手动定义默认构造函数
X(int i) {
a = i;
}
private:
int a;
};

从上述程序可以看出,原本期望编译器自动生成的默认构造函数需要程序员手动编写了,即程序员的工作量加大了。此外,手动编写的默认构造函数的代码执行效率比编译器自动生成的默认构造函数低。类的其它几类特殊成员函数也和默认构造函数一样,当存在用户自定义的特殊成员函数时,编译器将不会隐式的自动生成默认特殊成员函数,而需要程序员手动编写,加大了程序员的工作量。类似的,手动编写的特殊成员函数的代码执行效率比编译器自动生成的特殊成员函数低。

2.default的提出

为了解决上述两个问题:1. 减轻程序员的编程工作量;2. 获得编译器自动生成的默认特殊成员函数的高代码执行效率,C++11 标准引入了一个新特性:default函数。程序员只需在函数声明后加上=default,就可将该函数声明为 default函数,编译器将为显式声明的 default函数自动生成函数体。例如:

1
2
3
4
5
6
7
8
9
10
11
class X {
public:
X()= default;
X(int i) {
a = i;
}
private:
int a;
};

X x;

在上述程序中,编译器会自动生成默认构造函数 X::X() {},该函数可以比用户自己定义的默认构造函数获得更高的代码效率。

3.default的用法及示例

default仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数。例如:

1
2
3
4
5
6
class X {
public:
int f() = default; // 错误, 函数f()非类X的特殊成员函数
X(int) = default; // 错误, 构造函数X(int) 非X的特殊成员函数
X(int = 1) = default; // 错误, 默认构造函数X(int=1)含有默认参数
};

default函数既可以在类体里(inline)定义,也可以在类体外(out-of-line)定义。例如:

1
2
3
4
5
6
7
8
9
10
class X {
public:
X() = default; //inline default默认构造函数
X(const X&);
X& operator = (const X&);
~X() = default; //inline default析构函数
};

X::X(const X&) = default; //out-of-line default拷贝构造函数
X& X::operator = (const X&) = default; //out-of-line default拷贝赋值操作符

在 C++ 代码编译过程中,如果程序员没有为类 X 定义析构函数,但是在销毁类 X 对象的时候又需要调用类 X 的析构函数时,编译器会自动隐式的为该类生成一个析构函数。该自动生成的析构函数没有参数,包含一个空的函数体,即X::~X() {}。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class X {
private:
int x;
};

class Y : public X {
private:
int y;
};

int main() {
X* x = new Y;
delete x;
}

在上述程序中,程序员没有为基类 X 和派生类 Y 定义析构函数,当在主函数内 delete 基类指针 x 的时候,需要调用基类的析构函数。于是,编译器会隐式自动的为类 X 生成一个析构函数,从而可以成功的销毁 x 指向的派生类对象中的基类子对象(即 int 型成员变量 x)。

但是,这段代码存在内存泄露的问题,当利用 delete 语句删除指向派生类对象的指针 x时,系统调用的是基类的析构函数,而非派生类 Y 类的析构函数,因此,编译器无法析构派生类的 int 型成员变量 y。

因此,一般情况下我们需要将基类的析构函数定义为虚函数,当利用 delete 语句删除指向派生类对象的基类指针时,系统会调用相应的派生类的析构函数(实现多态性),从而避免内存泄露。但是编译器隐式自动生成的析构函数都是非虚函数,这就需要由程序员手动的为基类 X 定义虚析构函数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class X {
public:
virtual ~X() {}; // 手动定义虚析构函数

private:
int x;
};

class Y : public X {
private:
int y;
};

int main(){
X* x = new Y;
delete x;
}

在上述程序中,由于程序员手动为基类 X 定义了虚析构函数,当利用 delete 语句删除指向派生类对象的基类指针 x 时,系统会调用相应的派生类 Y 的析构函数(由编译器隐式自动生成)以及基类 X 的析构函数,从而将派生类对象完整的销毁,可以避免内存泄露。

但是,在清单 8 中,程序员需要手动的编写基类的虚构函数的定义(哪怕函数体是空的),增加了程序员的编程工作量。更值得一提的是,手动定义的析构函数的代码执行效率要低于编译器自动生成的析构函数

为了解决上述问题,我们可以将基类的虚析构函数声明为default函数,这样就可以显式的指定编译器为该函数自动生成函数体。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class X {
public:
virtual ~X() = default; // 编译器自动生成default函数定义体

private:
int x;
};

class Y : public X {
private:
int y;
};

int main() {
X* x = new Y;
delete x;
}

在上述程序中,编译器会自动生成虚析构函数virtual X::~X() {},该函数比用户自己定义的虚析构函数具有更高的代码执行效率。

4.总结

1.default只能用于类的特殊成员函数;
2.default解决了两个问题:一是减轻程序员编写函数定义的编程工作量,二是default定义的函数代码执行效率要高于手动编写;
3.default可以在类体内定义,也可在类体外定义;
4.基类的析构函数一定要显式地定义成虚析构函数。