c++类的六个默认生成函数

  1. 默认缺省构造函数
  2. 默认缺省析构函数
  3. 默认拷贝构造函数
  4. 默认赋值运算符重载
  5. 默认取地址运算符重载函数
  6. 默认const修饰的取地址运算符重载函数
//声明一个什么都没有的空类
class Empty{};


//c++默认会为该类生成以下六个成员
class Empty{
    Empty();
    ~Empty();
    Empty(const Empty&);
    Empty operator=(const Empty&);

    Empty* operator&();
    const Empty* operator&()const;
};

此外,还有两个特例

  1. 移动构造函数
  2. 移动赋值运算符

C++11 标准引入了移动语义的概念,其中就包括移动构造函数和移动赋值运算符。

以下情况会导致编译器自动生成默认的移动构造函数和移动赋值运算符:

  1. 当你没有显式定义任何一个拷贝构造函数、拷贝赋值运算符、移动构造函数或移动赋值运算符时,编译器会自动生成默认的移动构造函数和移动赋值运算符。

  2. 当你显式地声明了拷贝构造函数或拷贝赋值运算符,但没有声明移动构造函数或移动赋值运算符时,编译器会生成默认的移动构造函数和移动赋值运算符。

  3. 当你显式地声明了移动构造函数或移动赋值运算符,但没有声明拷贝构造函数或拷贝赋值运算符时,编译器会生成默认的拷贝构造函数和拷贝赋值运算符。

简单来说,只有在没有显式定义拷贝构造函数、移动构造函数、拷贝赋值运算符或移动赋值运算符时,编译器才会自动生成默认的移动构造函数或移动赋值运算符。

默认构造函数

  • 如果没有显式定义构造函数,编译器会隐式的为一个类生成一个构造函数,该默认构造函数没有实参,并且它将按照以下规则初始化类的数据成员
    • 如果类内存在初始值,则就用它初始化成员
    • 否则,就默认初始化他们,但内置类型或者复合类型(如数组或者指针)的对象被默认初始化时它们的值将是未定义的,如下述代码中m_sold如果类内没有初始化为0,则默认初始的值可能为4399,也可能为23459
class sales_data
{
public:

private:
	//因为已经m_sold和revenue已经提供了初始值,所以默认构造函数会用提供的初始值初始化他们
	int m_sold = 0;
	double revenue = 0.0;

	//默认初始化bookNo为一个空字符串
	string bookNo;
};

  • 一旦我们定义了一些其他构造函数,那么除非我们自己再次定义一个默认构造函数否则编译器将不再为我们生成缺省构造函数

以下示例中,由于我们自己定义了一个带参数的构造函数,则编译器不会再会我们生成一个缺省构造函数,因此如果我们想使用不带参的构造函数定义sales_data类对象,就必须手动创建一个缺省构造函数

手动添加缺省构造后正确

在c++11新标准中,可以通过在参数列表后写上=default要求编译器生成构造函数

class sales_data
{
public:
	sales_data(int sold, double revenue, string bookNo) :
		m_sold(sold), m_revenue(revenue), m_bookNo(bookNo)
	{};
	//等同于sales_data(){};
	sales_data() = default;
	
private:
	int m_sold = 0;
	double m_revenue = 0.0;

	string m_bookNo;
};

默认析构函数

如果没有显式定义析构函数,编译器会为类生成一个默认的析构函数。析构函数在对象被销毁时自动调用,用于进行资源的释放和清理操作。

注意:析构函数不能被重载吗,但可以被声明(定义)为virtual

默认拷贝构造函数

拷贝构造函数的定义

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则该构造函数称为拷贝构造函数

同构造函数一样,如果我们没有显式的定义拷贝构造函数,则编译器会为我们合成一个拷贝构造函数,如上sales_data的例子中,我们没有为它定义拷贝构造函数,于是编译器为它生成了一个拷贝构造函数,具体来说,我们的sales_data类等价于

class sales_data
{
public:
	sales_data(int sold, double revenue, string bookNo) :
		m_sold(sold), m_revenue(revenue), m_bookNo(bookNo)
	{};
	sales_data() = default;
    //这将是编译器为我们生成的拷贝构造函数
	sales_data(const sales_data&);
	
private:
	int m_sold = 0;
	double m_revenue = 0.0;

	string m_bookNo;
};

sales_data::sales_data(const sales_data& sd):
	m_sold(sd.m_sold),
	m_revenue(sd.m_revenue),
	m_bookNo(sd.m_bookNo)
{
}

具体的,拷贝构造函数会根据每个成员的类型决定其拷贝

  • 类类型成员,使用拷贝构造函数拷贝
  • 内置类型成员,直接拷贝,特别的,对于指针类型数据而言,会出现浅拷贝问题8

赋值与初始化

int a(4);//这是初始化
int arr[] = { 1, 2, 3 }; // 直接初始化数组
int b = 4;//这是赋值

//这是初始化
sales_data(int sold, double revenue, string bookNo) :
	m_sold(sold), m_revenue(revenue), m_bookNo(bookNo)
{};

//这是赋值
sales_data(int sold, double revenue, string bookNo) :
{
	m_sold = sold;
	m_revenue = revenue;
	m_bookNo = bookNo;
};

在上述例子中,采用赋值初初始化成员变量的时候,编译器会首先使用默认构造函数为m_sold,m_revenue,m_bookNo赋初值,再对他们赋予新值,这个过程中默认构造函数完全被浪费了,但是使用初始化成员列表进行初始化时完全避免了这个问题,因此一般建议使用初始化成员列表的方式对类的数据成员进行初始化,对于内置类型而言赋值与初始化差别不大

直接初始化与赋值初始化

	string dots(10, '.');//直接初始化
	string s(dots);//拷贝初始化
	string s2 = dots;//拷贝初始化

采用直接初始化使用的是构造函数,采用拷贝初始化使用的是拷贝构造函数

拷贝初始化的发生时间:

  • 将对象作为实参传递给非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 花括号初始化一个数组中的元素

拷贝构造函数的参数必须为引用类型的原因

拷贝构造函数被用来初始化非引用类型参数,如果不使用引用,那么为了调用拷贝构造函数就必须首先拷贝它的实参,但是为了拷贝它的实参,我们又需要调用拷贝构造函数看,如此循环。。。

默认赋值运算符重载

与拷贝构造函数一样,如果一个类没有定义拷贝拷贝赋值运算符,则编译器会自动为其生成一个拷贝赋值运算符

另外需要注意的是,赋值运算符重载函数中通常返回一个指向左侧运算对象的引用,这是为了与标准库进行统一,当然不是必须,但是建议这样做

作为例子,编译器在上述为sales_data类生成的拷贝赋值运算符等价于:

sales_data& sales_data::operator=(const sales_data& rhs)
{
	m_sold = rhs.m_sold;
	m_revenue = rhs.m_revenue;
	m_bookNo = rhs.m_bookNo;
	return *this;
}

参考:
《c++ Primer》

《effective c++》

更多推荐