CherieLi Student

Big five

2019-10-14

简介

在C++中声明自定义的类,编译器会默认帮助程序员生成一些未自定义的成员函数。这样的函数被称为“默认函数”。

Big-Five : 默认构造函数,析构函数, 拷贝构造函数, 拷贝赋值运算符、移动构造函数和移动赋值运算符。

拷贝操作:拷贝构造函数、拷贝赋值运算符。

移动操作:移动构造函数、移动赋值运算符

一旦程序员实现了这些函数的自定义版本,则编译器不会再为这类自动生成默认版本。(又称特种成员函数)

类默认函数的控制:“=default” 和“=delete” 函数

“=default”, 显式地指示编译器生成该函数的默认版本。

“=delete”,会指示编译器不生成函数的缺省版本。一旦缺省版本被删除了,重载该函数也是非法的。

默认构造函数(Constructor)

默认构造函数是可以无实参调用的构造函数(以空参数列表定义,或为每个形参提供默认实参而定义)。拥有公开默认构造函数的类型,是可默认构造的。

仅当类中不包含用户声明的构造函数时,才生成。

示例一:没有定义默认构造函数,创建对象时,隐式生成一个默认构造函数

class X {
private:
  int a;
};

X x; //可以编译通过

自动生成的默认构造函数没有参数,包含一个空的函数体,即 X::X(){ }

示例二:没有定义默认构造函数,自定义了非默认构造函数,出现编译错误

class X {
public:
  X(int i) { a = i; } //用户自定义的构造函数

private:
  int a;
};

X x; // 错误 , 默认构造函数 X::X() 不存在

不再隐式的生成默认构造函数,如果需要用默认构造函数来创建类的对象,必须显式定义。

示例三:手动定义默认构造函数

class X {
public:
  X(){}; // 手动定义默认构造函数
  X(int i) { a = i; }

private:
  int a;
};

X x; // 正确,默认构造函数 X::X() 存在

问题:

  1. 自动生成->手动编写, 工作量加大
  2. 手动编写的默认构造函数的代码执行效率比编译器自动生成的默认构造函数低。

示例四:defaulted函数,编译器自动生成默认构造函数

class X {
public:
  X() = default;
  X(int i) { a = i; }

private:
  int a;
};

X x;

在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上=default来要求编译器生构造函数

示例五:使类型恢复为POD类型

POD (Plain Old Data) 用于说明一个类型的属性,尤其是用户自定义类型的属性。

Plain 表示POD是个普通的类型,C++中常见的类型都有这样的属性。

Old 体现与C的兼容性。

#include <iostream>
#include <type_traits>
using namespace std;
class TwoCstor {
public:
//提供了带参数版本的构造函数,则必须自行提供不带参数版本,且TwoCstor不再是POD类型
TwoCstor() {}
TwoCstor(int i) : data(i) {}

private:
int data;
};

int main() { 
    cout << is_pod<TwoCstor>::value << endl;  //0
}   
#include <iostream>
#include <type_traits>
using namespace std;
class TwoCstor {
public:
//提供了带参数版本的构造函数,再指示编译器提供默认版本,则自定义类型依然是POD类型
TwoCstor() = default;
TwoCstor(int i) : data(i) {}

private:
int data;
};
int main() { 
    cout << is_pod<TwoCstor>::value << endl;  //1
}   

C++11,在函数的定义或者声明加上”=delete”,避免隐式数据类型转换。

示例六:避免隐式数据类型转换

class ConvType {
public:
  ConvType(int i){};
  ConvType(char c) = delete; //删除char 版本
};
void Func(ConvType ct) {}
int main() {
  Func(3);
  Func('a'); //无法通过编译
  ConvType ci(3);
  ConvType cc('a'); //无法通过编译
}

// 编译结果 error: use of deleted function ‘ConvType::ConvType(char)’

如果没有ConvType(char c) = delete; 编译器会隐式将’a’转化成int型,并调用int型的构造函数。

explicit关键字防止隐式转换。

示例七: 不建议将“=delete”与explicit合用

class ConvType {
public:
  ConvType(int i){};
  explicit ConvType(char c) = delete; 
};
void Func(ConvType ct) {}
int main() {
  Func(3);
  Func('a'); //可以通过编译
  ConvType ci(3);
  ConvType cc('a'); //无法通过编译
}

析构函数(Destructor)

析构函数时对象生存期终结时调用的特殊成员函数。析构函数的目的是释放对象可能在其生存期间获得的资源。

当一个对象超出作用域或执行delete的时候,析构函数就会被调用。

仅当基类的析构函数为虚的,派生类的析构函数才是虚的。

C++11,默认地,所有的析构函数(无论是用户定义的,还是编译器自动生成的)都隐式的具备noexcept性质。

(noexcept表示其修饰的函数不会抛出异常。如果noexcept修饰的函数抛出了异常,编译器可以选择直接调用std::terminate()函数来终止程序的运行)

示例八:自动隐式生成析构函数

class X {
private:
  int x;
};

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

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

在销毁类X的对象的时候,需要调用类X的析构函数。没有为类X定义析构函数,编译器会自动隐式的为该类生成一个析构函数,自动生成的析构函数没有参数,包含一个空的函数体,即X::~X(){ }。

问题:内存泄漏

执行delete x的时候,系统调用类X的析构函数,编译器无法析构派生类的int型变量 y。

C++98:

将基类的析构函数定义为虚函数,利用delete语句删除指向派生类对象的基类指针时,系统会调用相应的派生类析构函数(实现多态性),从而避免内存泄漏。但是编译器隐式自动生成的析构函数都是非虚函数,因此需要手动为基类定义虚构函数。

示例九:手动定义基类虚析构函数

class X {
public:
  virtual ~X(){}; // 手动定义虚析构函数
private:
  int x;
};

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

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

问题:

  1. 自动生成->手动编写, 工作量加大

  2. 手动编写的析构函数的代码执行效率比编译器自动生成的析构函数低。

  3. 阻止了移动操作的生成。(大三律)

示例十:defaulted函数,编译器自动生成函数体。

class X {
public:
  virtual ~X() = default; // 编译器自动生成 defaulted 函数定义体
private:
  int x;
};

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

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

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

示例十一:需要对象在指定内存位置进行内存分配,并且不需要析构函数来完成一些对象级别的清理。

class X {
public:
  ~X() = delete;

private:
  int x;
};

拷贝构造函数(复制构造函数 Copy Constructor)

类T的复制构造函数时非模板构造函数,其首个形参为T&、const T& 、volatile T& 或const volatile T&, 而且要么没有其他形参,要么剩余形参均有默认值。

按成员进行非静态数据成员的拷贝构造。仅当类中不包含用户声明的拷贝构造函数时才生成。如果该类声明了移动操作,则拷贝构造函数将被删除。

拷贝构造函数被调用的场景:

  1. 一个对象需要通过另一个对象进行初始化

  2. 一个对象以值传递的方式作为参数传入函数

  3. 一个对象以值传递的方式作为返回值从函数返回

示例:自动生成默认拷贝构造函数

class X {
public:
  X();
};

int main() {
  X x1;
  X x2 = x1; // 正确,调用编译器隐式生成的默认拷贝构造函数
}

没有显式声明定义对应类的拷贝构造函数,C++编译器会自动生成拷贝构造函数。

C++编译器提供的拷贝构造函数工作方式是浅拷贝

浅拷贝只是单纯拷贝了该成员的内存地址,但所指的空间内容没有被复制,而是由两个对象共用,容易出现double free的问题。

解决办法:1. 手动重载拷贝构造函数,实现深拷贝

	   2. 禁止编译器生成默认的拷贝构造函数, 拒绝不安全的浅拷贝

需求:禁止编译器生成默认的拷贝构造函数, 拒绝不安全的浅拷贝。

C++98,将拷贝构造函数声明为private的成员,并且不提供函数实现。

一旦有人试图(或者无意识)使用拷贝构造函数,编译器就会报错

示例十二:C++98 禁止使用拷贝构造函数

#include <iostream>
#include <type_traits>
using namespace std;
class NoCopyCstor {
public:
  NoCopyCstor() = default;

private:
  NoCopyCstor(const NoCopyCstor &);
  //将拷贝构造函数声明为private成员并不提供实现,可以有效阻止用户错用拷贝构造函数
};

int main() {
  NoCopyCstor a;
  NoCopyCstor b(a); //无法通过编译
}
//编译结果:
//error: ‘NoCopyCstor::NoCopyCstor(const NoCopyCstor&)’ is private

C++11,在函数的定义或者声明加上”=delete”,会指示编译器不生成函数的缺省版本。

示例十三:C++11,禁止使用拷贝构造函数

#include <iostream>
#include <type_traits>
using namespace std;
class NoCopyCstor {
public:
  NoCopyCstor() = default;
  NoCopyCstor(const NoCopyCstor &) = delete;
  //使用“=delete”同样可以有效阻止用户错用拷贝构造函数
};

int main() {
  NoCopyCstor a;
  NoCopyCstor b(a); //无法通过编译
}
//编译结果
// error: use of deleted function ‘NoCopyCstor::NoCopyCstor(const NoCopyCstor&)’

拷贝赋值操作符(复制赋值运算符 copy assignment operator )

类T的复制赋值运算符是名为operator=的非模板非静态成员函数,它接收恰好一个T、T&、const T&、volatile T&或const volatile T &类型的形参。对于可复制赋值类型,它必须有公开的拷贝赋值运算符。

按成员进行非静态数据成员的拷贝赋值。仅当类中不包含用户声明的拷贝赋值运算符时才生成。

如果该类声明了移动操作,则复制构造函数将被删除。在已经存在复制构造函数或析构函数的条件下,仍然生成复制赋值运算符已经成为了被废弃的行为。

默认拷贝赋值操作符:浅拷贝

需要:1. 重载实现深度赋值

       2.禁用拷贝赋值操作符

示例十四:自动生成默认拷贝赋值操作符

class X {
public:
  X();
};

int main() {
  X x1;
  X x2 = x1; // 正确,调用编译器隐式生成的默认拷贝构造函数
  X x3;
  x3 = x1; // 正确,调用编译器隐式生成的默认拷贝赋值操作符
}

没有显式声明定义对应类的拷贝构造函数,C++编译器会自动生成拷贝赋值操作符,X &operator=(const X &){}。

示例十五:禁止类对象之间的赋值。

class X {
public:
  X();
  X(const X &) = delete; // 声明拷贝构造函数为 deleted 函数
  X &operator=(const X &) = delete; // 声明拷贝赋值操作符为 deleted 函数
};

int main() {
  X x1;
  X x2 = x1; // 错误,拷贝构造函数被禁用
  X x3;
  x3 = x1; // 错误,拷贝赋值操作符被禁用
}

拷贝构造函数 VS. 拷贝赋值运算符

拷贝构造函数在创建或初始化对象的时候调用;

拷贝赋值运算符在更新一个对象的值时调用;

两种拷贝操作是彼此独立的。如果声明了一个拷贝构造函数,未声明拷贝赋值运算符,在进行拷贝赋值的时候,编译器会自动生成拷贝赋值运算符。同理,如果声明了一个拷贝赋值运算符,未声明拷贝构造函数,进行拷贝构造的时候,编译器会自动生成拷贝构造函数。

大三律(Rule of Three)

如果你声明了拷贝构造函数,拷贝赋值运算符,或析构函数中的任何一个,你就得同时声明所有的三个。

标准库中用以管理内存的类(如执行动态内存管理的STL容器)都会遵从大三律。

移动构造函数(move constructor)和移动赋值运算符(move assignment operator )

类T的移动构造函数时非模板构造函数,其首个形参是T &&、const T&&、volatile T&& 或 const volatile T&&, 且无其他形参,或剩余形参均有默认值

如果现有的对象是左值(lvalue),那么是拷贝构造函数,如果现有的对象是右值(rvalue)(例如,即将被摧毁的临时对象),那么是移动构造函数。
IntCell B = C;          // 如果C是左值,那么是拷贝构造函数;如果C是右值,那么是移动构造函数
IntCell B { C };        //  如果C是左值,那么是拷贝构造函数;如果C是右值,那么是移动构造函数
class Widget {
public:
  Widget(Widget &&rhs);            //移动构造函数
  Widget &operator=(Widget &&rhs); //移动赋值运算符
};

两种移动操作并不彼此独立,声明了其中一个,就会阻止编译器生成另外一个。

一旦显式声明了拷贝操作,就不会再生成移动操作了。

一旦声明了移动操作,编译器就会废除拷贝操作。(“=delete”)

受到大三律的影响:用户只要声明了析构函数,就不会生成移动操作。

移动操作生成条件(三者同时成立):

  1. 该类未声明任何拷贝操作
  2. 该类未声明任何移动操作
  3. 该类未声明任何析构函数

补充

示例一:defaulted函数既可以在类体内(inline)定义,也可以在类体外(out-of-line)定义。

class X {
public:
  X() = default; // Inline defaulted 默认构造函数
  X(const X &);
  X &operator=(const X &);
  ~X() = default; // Inline defaulted 析构函数
};

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

示例二:defaulted函数的错误用法

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

示例三:deleted函数,禁用某些用户自定义的类new操作符,避免在自由存储区创建类的对象。

#include <cstddef>
using namespace std;

class X {
public:
  void *operator new(size_t) = delete;
  void *operator new[](size_t) = delete;
};

int main() {
  X *pa = new X;     // 错误,new 操作符被禁用
  X *pb = new X[10]; // 错误,new[] 操作符被禁用
}

示例四:deleted函数,用于普通函数禁止类型转换

#include <iostream>
using namespace std;
int add(int, int) = delete;
double add(double a, double b) { return a + b; }
int main() {
  cout << add(1, 3) << endl; // 错误,调用了 deleted 函数 add(int, int)
  cout << add(1.2, 1.3) << endl;
  return 0;
}

delete禁用的仅是函数的定义,即该函数不能被调用,但是函数标识符add仍然有效,在名字查找和函数重载解析时仍会找到该函数标识符。

示例五:成员函数模板在任何情况下都不会抑制特种成员函数的生成。

class Widget {
public:
  template <typename T> 
  Widget(const T &rhs); //以任意型别构造Widget

  template <typename T>
  Widget &operator=(const T &rhs); //以任意型别对Widget赋值
};

总结

“=default” 修饰的函数为显式缺省(explicit defaulted)函数。仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数。

“=delete” 修饰的函数为删除函数。

功能:

  1. 增强对类默认函数的控制,能够更加精细地控制默认版本的函数。
  2. “=default” 减轻编程工作量,获得编译器自动生成的默认特殊成员函数的高代码执行效率。(示例三,四,九,十)
  3. “=default” 使类型恢复为POD类型。(示例五)

                     2. “=default”,不仅可以用于类的定义中修饰成员函数,也可以在类定义之外修饰成员函数。在类定义外显式指定缺省版本所带来的好处是,可以对一个class定义提供多个实现版本。(补充示例一)
                     3. C++11标准并不要求编译器为某些函数提供缺省的实现,但如果将其声明为显式缺省的话,则编译器会按照某些“标准行为”为其生成所需要的版本。比如:“operator ==”。
                     4. "=delete"可以避免用户使用一些不应该使用的类的成员函数(示例十三)。
    
  4. “=delete”在避免编译器做一些不必要的隐式转换上,(不建议将“=delete”与explicit合用,两者合用会引起混乱) (示例六,示例七)。不局限于缺省版本的类成员函数或者全局函数,一些普通函数依然可以通过显式删除来禁止类型转换(补充示例四)。
  5. “=delete”避免在堆上分配该class对象(operator new操作符,补充示例三),限制自定义类型在栈上或者静态的构造。
  6. 移动操作仅当类中未包含用户显式声明的拷贝操作、移动操作和析构函数时候才生成。
  7. 成员函数模板在任何情况下都不会抑制特种成员函数的生成。(补充示例五)

参考链接:

[1]https://www.cnblogs.com/lsgxeva/p/7787438.html

[2]https://blog.csdn.net/tutuxs/article/details/54947395

[3]https://www.cnblogs.com/QG-whz/p/5517643.html


上一篇 google benchmark

下一篇 统一初始化

Comments

Content