特殊成员函数是在特定情况下隐式定义为类成员的成员函数。有如下六个:
一、默认构造函数:如果类申明时没有显示定义任何构造函数,则编译器假定该类具有隐式定义的默认构造函数。例如申明如下的类:
class Example {
public:
int total;
void accumulate (int x) { total += x; }
};
编译器会假定有一个默认构造函数:Example() {},因此通过如下语句直接定义一个变量是能编译过的
Example ex;
一旦类有显示申明的构造函数,编译器就不再隐式提供不带参数的默认构造函数了。例如:
class Example2 {
public:
int total;
Example2 (int initial_value) : total(initial_value) { };
void accumulate (int x) { total += x; };
};
就只能带参的对象,申明如下的不带参数的对象编译就会报错:“no matching function for call to ‘Example2::Example2()’”
Example2 ex;
那如果我们还是希望系统提供默认的不带参构造函数,default关键字就可以上场了,如下即可编译通过。
class Example2 {
public:
int total;
Example2() = default;
Example2 (int initial_value) : total(initial_value) { };
void accumulate (int x) { total += x; };
};
二、析构函数
在对象生命周期结束时,负责必要地清理工作。如果没有显示申明自定义的析构函数,系统会默认提供。
三、拷贝构造函数
当一个对象被传递给和它同类型的对象,它的拷贝构造函数被调用以构造一个副本,这时执行的是浅拷贝(shallow copy)。如果类没有定义自定义拷贝构造函数或move构造函数(或赋值),则提供隐式拷贝构造函数。
以Example2为例:
Example2 ex(100);
Example2 ex1(ex); //使用的就是拷贝构造函数
此时系统隐式提供的拷贝构造函数大致如下,仅仅复制成员:
Example2:: Example2(const Example2& x):total(x.tatal){}
如果类成员中有指针成员变量,而且这些指针指向处理其存储的对象,需要注意重载拷贝构造函数,进行深拷贝(deep copy)。
class CopyConstruct{
public:
CopyConstruct(const string &str):ptr(new string(str)){}
~CopyConstruct(){
if(ptr != NULL){
delete ptr;
}
cout << "~CopyConstruct()" << endl;
}
//copy constructor
CopyConstruct(const CopyConstruct& x):ptr(new string(x.content())){}
//access content:
const string& content() const {return *ptr;}
private:
string *ptr;
};
int main(){
CopyConstruct foo("CopyConstruct");
CopyConstruct bar = foo;
cout << "bar's content: " << bar.content() << endl;
return 0;
}
如果上面的没有实现对拷贝构造函数的重载CopyConstruct(const CopyConstruct& x),进行深拷贝,那么在运行时会报double free or corruption错误。通过gdb调试,可以查看到foo和bar对象中的ptr指针的值是一样的,所以会出现double free的问题。
四、拷贝赋值操作
拷贝赋值操作符也是一个特殊函数,如果类没有定义自定义拷贝或移动赋值(或移动构造函数),则拷贝赋值操作符也是隐式定义的。和拷贝构造函数类似,隐式版本执行的是浅拷贝,这适用于许多类,但不适用于具有指针的类,这些指针指向处理其存储的对象。这种情况下,拷贝赋值操作不但存在double free的风险,而且会导致内存泄漏,所以也需要重载拷贝赋值操作,并进行深拷贝。
```cpp
class CopyConstruct{
public:
CopyConstruct(const string &str):ptr(new string(str)){}
~CopyConstruct(){
if(ptr != NULL){
delete ptr;
}
cout << "~CopyConstruct()" << endl;
}
//copy constructor
CopyConstruct(const CopyConstruct& x):ptr(new string(x.content())){
cout << "CopyConstruct is used\n";
}
//access content:
const string& content() const {return *ptr;}
//copy assignment
CopyConstruct& operator= (const CopyConstruct& x){
cout << "CopyConstruct assignment\n";
if(ptr != NULL){
delete ptr;
ptr = NULL;
}
ptr = new string(x.content());
return *this;
}
private:
string *ptr;
};
int main(){
CopyConstruct foo("CopyConstruct");
CopyConstruct test(foo);
CopyConstruct bar = foo;
bar = foo;
cout << "bar's content: " << bar.content() << endl;
return 0;
}
运行结果:
五、移动构造函数和移动赋值函数
与copy类似,move也使用一个对象的值来设置另一个对象的值。 但是,与copy不同的是,内容实际上是从一个对象(源)传输到另一个对象(目标):源丢失了内容,由目标接管。 这种移动只发生在值的源是一个未命名的对象时。未命名对象是指本质上是临时的对象,因此甚至没有名字。未命名对象的典型例子是函数的返回值或类型转换。使用诸如此类的临时对象的值来初始化另一个对象或赋值,实际上不需要副本:对象永远不会被用于其他任何事情,因此,它的值可以被移动到目标对象中。 这些情况触发了move构造函数和move赋值,会更加有效率。move构造函数和move赋值是接受类本身的右值型参的成员。右值引用通过在类型后面跟两个与符号(&&)来指定。作为参数,右值引用匹配这种类型的临时变量的实参。
MyClass (MyClass&&); // move-constructor
MyClass& operator= (MyClass&&); // move-assignment
移动的概念对于管理其使用的存储的对象最有用,例如使用new和delete分配存储的对象。 在这些对象中,复制和移动是真正不同的操作:
—从A复制到B意味着新的内存被分配给B,然后A的全部内容被复制到分配给B的新内存中。
—从A移动到B是指已经分配给A的内存被转移到B,而不需要再分配新的存储空间。 它只涉及到复制指针。
class MoveConstruct{
public:
MoveConstruct(const string& str):ptr(new string(str)){
cout << "construct with string:" << ptr <<": " << *ptr <<" : " << this << endl;
}
~MoveConstruct() {
cout << "~MoveConstruct\n";
delete ptr;
}
//move constructor
MoveConstruct(MoveConstruct&& x):ptr(x.ptr) {
cout << "Move constructor" << ptr <<": " << *ptr << endl;
x.ptr=nullptr;
}
//move assignment
MoveConstruct& operator= (MoveConstruct&& x){
delete ptr;
ptr = x.ptr;
x.ptr = nullptr;
cout << "Move assignment\n";
return *this;
}
//access content
const string& content() const {return *ptr;}
//addition
MoveConstruct operator+(const MoveConstruct& rhs){
return MoveConstruct(content() + rhs.content());
}
private:
string *ptr;
};
int main(){
MoveConstruct foo("Apple");
MoveConstruct bar = MoveConstruct("Banana"); //move construction
cout << "&foo= " << &foo <<" &bar= " << &bar << endl;
MoveConstruct latest = foo + bar; //move construction
foo = latest + bar; //move assignment
cout << "foo's content: " << latest.content() << endl;
return 0;
}
运行结果如下:
预期走到移动构造函数两处实际都没有走到。原来是因为:编译器已经优化了许多在返回值优化中需要移动构造调用的情况。 最值得注意的是,当函数返回的值用于初始化对象时。 在这些情况下,实际上可能永远不会调用move构造函数。注意,尽管右值引用可以用于任何函数形参的类型,但它很少用于move构造函数之外的其他用途。 右值引用是棘手的,不必要的使用可能是错误的来源,很难跟踪。
上文所述class的六个特别成员在某些情况下被隐式地实现:
注意,在相同的情况下,并非所有特殊成员函数都是隐式定义的。 这主要是由于与C结构和早期c++版本的向后兼容性,事实上,有些情况还包括了已弃用的情况。 幸运的是,每个类都可以显式地选择使用默认定义存在哪些成员,或者分别使用关键字default和delete删除哪些成员。 语法是:
function_declaration = default;
function_declaration = delete;
Android的Codec2的代码中有定义如下宏:
禁止使用拷贝构造函数、拷贝赋值函数:
#define C2_DO_NOT_COPY(type) \
type& operator=(const type &) = delete; \
type(const type &) = delete; \
针对C++11之前有如下宏定义实现上述功能:
// A macro to disallow the copy constructor and operator= functions
// This should be used in the priavte:declarations for a class
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
TypeName& operator=(const TypeName&)
使用默认的移动构造函数、移动赋值函数:
#define C2_DEFAULT_MOVE(type) \
type& operator=(type &&) = default; \
type(type &&) = default; \
关键字default用于定义一个如果不删除将被隐式定义的构造函数。一般来说,为了将来的兼容性,如果类显式地定义了一个复制/移动构造函数或一个复制/移动赋值,但不是同时定义了这两个函数,则鼓励对它们没有显式定义的其他特殊成员函数指定delete或default。
参考文档:
https://www.cnblogs.com/xinxue/p/5503836.html
https://www.cplusplus.com/doc/tutorial/classes2/