C++中的特种函数

发布时间 2024-01-11 21:25:25作者: 橙皮^-^

一、什么是特种成员函数

特种成员函数是指那些C++会自行生成的成员函数。这些函数仅在需要时才会生成,亦即,在某些代码使用了它们,而在类中并未显式声明的场合。

  • 具体的特种成员函数
    默认构造函数、析构函数、复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符

二、对应的函数签名

class Weight{
public:
  Weight(){} //默认构造函数
  ~Weight(){} //析构函数
  Weight(const Weight& rhs){} //复制构造函数
  Weight& operator= (const Weight& rhs){} //复制赋值运算符
  Weight(Weight&& rhs){} //移动构造函数
  Weight& operator= (Weight && rhs){} //移动赋值运算符
};

三、特种函数生成机制

生成的机制在《Effective Modern C++》Item17中有详细说明,这里采用Linux下objdum工具对二进制文件进行反汇编查看,实验验证生成机制。
测试环境:

  • g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
  • GNU objdump (GNU Binutils for Ubuntu) 2.38
1、默认构造函数

在以下俩个条件其中一个成立时生成默认构造函数

  • 当一个类没有声明任何构造函数,且需要使用构造函数构造实例时,编译器会默认生成构造函数。
  • 使用default关键字声明默认构造函数

当一个类没有声明任何构造函数时、需要使用到默认构造函数情况:
  1、存在非静态成员变量需要调用其默认构造函数进行初始化
  2、继承于基类,且基类存在构造函数需要调用初始化
  3、存在虚基类

1、存在非静态成员变量需要调用其构造函数进行初始化,当不存在任何构造函数时,编译器会生成默认构造函数并调用非静态成员变量的默认构造函数进行初始化。


class MyTestB{
public:
    MyTestB(){}
};

class MyTestA{
public:
    //MyTestA():b(){}  //生成的默认构造函数
    MyTestB b;
};

int main(int argc)
{
    MyTestA a;
    return 0;
}

通过g++ main.cpp -o a.out 和 objdump -d a.out 查看生成的默认构造函数

2、继承于基类,且基类存在默认构造函数时,实例化需要调用基类默认构造函数进行初始化。

class MyTestA{
public:
    MyTestA(){}
};

class MyTestB : public MyTestA
{
//public:
    //MyTestB():MyTestA(){} //生成的默认构造函数,调用基类默认构造函数初始化基类
};

int main(int argc)
{
    MyTestB b;
    return 0;
}

生成的构造函数,调用基类默认构造函数初始化基类

3、存在虚基类

class MyTestA
{

};

class MyTestB : virtual MyTestA
{

};

int main(int argc, char*argv[])
{
   MyTestB b;
   return 0;
}

生成的默认构造函数

2、析构函数

在一个类中没有显示声明虚构函数,且需要用的时候[ord-use]编译器会声明一个inline public访问级别的虚构函数,默认为noexcept。仅当基类的析构函数为虚的,派生类的析构函数才是虚的。若基类存在非noexcept的虚构函数,那么生成的虚构函数也是非noexcept。

当一个类没有声明任析构函数时、需要使用到默认构造函数情况:
  1、存在非静态成员变量销毁时需要调用其析构函数
  2、继承基类存在析构函数时,销毁需要调用其析构函数

1、存在非静态成员变量销毁时需要调用其析构函数

class MyTestA
{
public:
  ~MyTestA(){}
};

class MyTestB
{
private:
  MyTestA a;
};

int main(int argc, char* argv[])
{
  {
    MyTestB b;
  }
  return 0;
}

生成的汇编,可以看到MyTestB类中非静态成员MyTestA存在析构函数,在销毁析构时需要调用MyTestA的析构函数,因此编译器为MyTestB生成一个析构函数,该虚构函数执行调用MyTestA中的虚构函数析构成员a。

2、继承基类存在析构函数时,销毁需要调用其析构函数,当基类MytestA存在析构函数,而派生类未显示声明定义析构函数时,编译器会生成析构函数并在析构函数中调用基类析构函数对基类进行析构。当基类虚构函数为虚时,编译器生成的虚构函数也为虚。

class MyTestA
{
public:
  virtual ~MyTestA(){}
};

class MyTestB : public MyTestA
{
};

int main(int argc, char* argv[])
{
  {
    MyTestB b;
  }
  return 0;
}

生成对应的汇编代码

3、复制构造函数和复制赋值运算符

对成员进行非静态数据成员的赋值构造。仅当类中不包含用户声明的复制构造函数且使用到时才生成。如果该类声明了移动操作(移动构造或移动赋值运算符),则复制构造函数将被删除。在已经存在复制赋值运算符或析构函数的条件下,仍然生成复制构造函数已经成为了被废弃的行为。

当定义移动操作就禁止生成复制构造和赋值复制的原因:
1、历史上copy constructor的默认生成是为了与C兼容,但大多数情况下默认生成的复制函数会造成问题:如浅拷贝问题,造成指针悬空。
2、C++ 11引进move,这在历史代码中不会出现,也就不会产生兼容问题,因此在C++11后定义移动操作就禁止生成默认复制函数。如果仍需要默认复制函数,可以使用default关键字让编译器生成。
更多解释可以看原文Why user-defined move-constructor disables the implicit copy-constructor?
代码验证:

class Weight{
public:
  Weight()=default;
  Weight(Weight&& rhs){}//移动构造
};

int main(int argc, char*argv[])
{
  Weight w0;
  Weight w1;
  w0 = w1; //赋值复制运算符,编译器报错
  Weight w2(w1); //复制构造
  return 0;
}

可以看到编译器报错提示,当定义移动操作时,默认复制操作被声明为delete。

第二点当存在复制赋值运算符或析构函数时,生成复制构造函数成为被废弃的行为:如果已定义析构或赋值运算符时,意味要涉及到类中资源的管理,这个时候默认生成复制构造函数已经不太合适,因为不知道类中定义的资源管理方式,至于为什么仍保留,这是因为需要兼容历史版本原因。

4、移动构造函数和移动赋值运算符

按成员进行非静态数据成员的移动操作。仅当类中不包含用户声明的复制操作,移动操作和析构函数时才生成。

这样设计的原因:
1、如果声明了一个移动操作(构造或者赋值运算符),那么意味着声明的移动操作和默认按成员移动多少有些不同,因此在已经存在一个移动操作的前提下,编译器会阻止生成另一个移动操作。
2、如果声明复制操作,同样也表明常规的复制操作不适用,编译器判定成员也极有可能不适用于移动操作,因此,一旦显示声明了复制操作,就不会再生成移动操作了。
3、如果显示声明了析构函数,那意味着相对于默认析构函数对各个成员进行释放有些不同,因此对于默认对各个成员进行移动操作也不再适用。因此,一旦显示声明了析构函数,就不会再生成移动操作。

代码验证

#include <memory>
class MyTestA{
public:
  MyTestA()=default;
  MyTestA(MyTestA&& rhs){}
};

//4.移动操作
class MyTestB{
public:
  MyTestB()=default;
  ~MyTestB(){}
  MyTestA a;
};

int main(int argc, char*argv[])
{
  MyTestB b0;
  MyTestB b1(std::move(b0));//不会生成默认移动操作,编译器会报错
}

编译器报错

其他条件组合也是可以通过代码一一验证。

四、三大律

三大律是一个指导原则,指的是:如果你声明了复制构造函数,复制赋值函数或析构函数中的任何一个,你就得同时声明所有这三个函数.

思想根据:
1.在一种复制操作中进行的任何资源管理,也极有可能在另一种复制操作中也需要进行
2.该类的析构函数也会参与到该类资源的管理中

如果存在用户声明的析构函数,则平凡的按成员赋值也不适于该类
可以推导:如果声明了析构函数,则赋值操作就不该被自动生成,因为它们的行为不可能正确.
可以再结合声明了复制操作就会阻止隐式生成移动操作的事实
就推动了C++11中的这样一个规定: 只要用户声明了析构函数,就不会生成移动操作.

简要总结:其原因在于如果显示声明其中一个函数,意味和默认按成员的资源管理方式多少有些不同(如果一样的话,就不需要显示声明,让编译器做就可以了),因此需要实现剩余其他函数,防止默认生成的函数出现错误。

五、总结

  • 特种成员函数是指C++会自行生成的成员函数:默认构造函数,析构函数,复制构造函数,复制赋值运算符,移动构造函数,移动赋值运算符。在需要用到时[odr-use]才会进行生成。
  • 移动操作仅当没有显示声明移动操作,复制操作,析构函数时且用到时才会生成。
  • 复制操作仅当没有显示声明移动操作时才生成,存在析构函数或另一种复制操作,生成复制操作是已被废弃的行为。按照三大律原则,应该自己进行显示声明。

六、参考

What does it mean to "odr-use" something?
Why user-defined move-constructor disables the implicit copy-constructor?
《Effective Modern C++》