C++编译器合成的默认函数(第二篇)

版权声明:转载请说明来源 https://blog.csdn.net/weixin_39640298/article/details/84726340

概述

前面我们整理了编译器合成的默认构造函数,也进行了深度的解析,那其他的合成函数是怎样的呢?下面就对其他的合成函数进行逐一整理。

1、析构函数

当对象被销毁的时候,会自动调用的函数。主要用来销毁类中申请的资源。格式跟构造函数差不多,只是在前面增加一个“~”符号。

注意:

  1. 对于未定义析构函数的类,编译器会默认生成一个非虚析构函数,该析构函数会调用成员以及基类的析构函数。
  2. 如果在类中申请了动态资源,那么就需要使用自定义析构函数去释放资源
  3. 对于基类一定要写虚析构函数

对于派生类对象经由一个基类指针被删除,若该基类没有虚析构,其结果是对象的派生类成员被析构。既然这样子,那写代码的时候把所有的类都加上虚析构不就避免错误了?

我们知道增加虚函数之后,编译器会给类增加虚函数表,而类对象中会添加一个指针,来指向这个虚函数表,这样类的大小会变大一个指针的大小。所以不能都给所有加上虚析构。那什么时候应该把析构函数写成虚析构,什么时候不用呢?

1. 带有多态性质的基类应该声明一个虚析构函数。如果类中带有虚函数,它就应该拥有一个虚析构函数。
2. 如果类的设计不是为了作为基类使用,或不是为了具备多态性,就不应该声明虚析构。

标准库中的string,STL容器中的vector、list、set等,都不带任何的虚函数,所以不要试图从它们继承,这样有时会引发错误。

虚析构函数的运作方式是,从深层派生的那个类的析构函数最先被调用,然后是其每一个基类的析构函数被调用

2、拷贝构造函数

以拷贝/复制的方式去初始化一个新的对象,会调用拷贝构造函数。若没有定义拷贝构造,则编译器会给合成一个。拷贝构造也可以定义多个。

有三种情况会调用此函数:

class A{......};
    
A a;

A a1(a);		//第一种,用同类型的对象初始化另一个对象,和下面的写法一样
A a2 = a;

void foo ( A a );	//第二种,作为函数的值传递形参时

A foo_bar()
{
	A a;
	.....
	return a;		//第三种,返回一个对象时
}

通过上面的例子可以总结为:

1. 同用类型的对象去初始化一个新的对象
2. 函数调用时传递对象型参数,函数的形参不能是引用
3. 函数返回一个对象时,以值传递的方式返回,可能拷贝构造函数(和编译器优化有关)

注意:拷贝构造函数必须以引用的形式传递(参数为引用值)。原因是:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环直至栈溢出(Stack Overflow)。

编译器给我们合成出的拷贝拷贝构造是按位拷贝的,没有进行深拷贝,所以有时会出问题。

什么时候编译器会给我们合成拷贝构造函数呢?也分为四种情况:

1. 当类内含一个成员对象,而后者的类声明有一个拷贝构造时(不论是被显式的声明,还是被编译器合成)
2. 当类继承自一个基类,而该基类拥有一个拷贝构造时(不论是被显式的声明,还是被编译器合成)
3. 当类声明了一个或多个虚函数时
4. 当类派生自一个继承串链,其中有一个或者多个虚基类时

第1和2两种情况好说,只是在调用拷贝构造时,调用相应的拷贝构造函数(基类调用基类的,成员变量调用自己的),下面注重看看3和4。

先看下面一段代码:

class ZooAnimal
{
 public:
        	ZooAnimal();
	virtual ~ZooAnimal();	//基类,指明虚析构
	
	virtual void animate();	//虚函数
	virtual void draw();
};

class Bear : public ZooAnimal
{
public:
	Bear();
	void animate(); //虽没有写virtual,但其实它是virtual
	virtual void dance();
}; 

Bear yogi;
Bera winnie = yogi; //没有显式书写拷贝构造,使用编译器合成的

ZooAnimal franny = yogi; //会发生切割行为

我们知道类中有虚函数时,编译器会给类增加虚函数表,并且对象中增加一个指针指向这张虚函数表。所以Bear winnie = yogi的拷贝可以看做下面的这张图:
在这里插入图片描述
在合成的拷贝赋值中,按位拷贝yogi的vptr指向Bear 类的虚函数表。而winnie和yogi的类型相同,所以把yogi的vptr值拷贝给winnie的vptr是安全的。

在看ZooAnimal franny = yogi的按位拷贝,如下图:
在这里插入图片描述如果是按位操作,则franny的vptr指向了Bear类的虚函数表,这样在运行时会发生错误。

对于第四种情况,跟上面的情况差不多,只是由确定对象指针指向的正确性变为确定虚基类位置的正确性的问题,这里就不详细讲述了。

3、拷贝赋值函数(赋值运算符)

以一个对象对已有对象重新赋值时,会调用拷贝赋值函数。拷贝赋值和拷贝构造差不多,也是按位进行赋值。拷贝赋值也可以有多个版本。如果没有显式书写,则编译器会给合成一个。

需要注意的是:

1. 防止自赋值
2. 先释放已有的资源,然后再去从拷贝源中赋值资源
3. 返回自引用,支持连续赋值
4. 如果类中需要动态分配内存,需要自定义拷贝赋值运算符函数,避免重复释放引发错误。

与拷贝构造函数的区别:
1、拷贝构造函数生成新的类对象,而赋值运算符不能
2、由于拷贝构造函数生成新的对象,所以在初始化时不用检测源对象和新建对象是否相同,而赋值运算符需要。

4、总结

通过前面的讲解,我们了解了编译器给我们合成的四个函数(还有两个是取值运算符&和取值运算符const)。尽管编译器能替我们合成这些函数,但是我们也发现了对于某些类来说合成版本是无法正常工作的。特别是当类需要分配类对象之外的资源时,合成的版本常常会失效。

所以最好的方式是我们自己手动的显式进行实现。不想让编译器自动合成函数,就明确的拒绝。

5、String类的实现,看看各种函数实现

class String 
{
public:
    String(const char* str = "");		//构造函数
    String(const String& rhs);		//拷贝构造函数,形参是引用
    ~String();						//析构函数
    String& operator=(const String& rhs);	//拷贝赋值函数,返回值是自引用
    size_t size const();
    const char* c_str const();
private:
    char* data;
};

/* 不简洁版本
String::String(const char* str) 
{
    if (str == NULL) 
    {
        data = new char[1];
        *data = '\0';
    } 
    else 
    {
        int len = strlen(str);
        data = new char[len + 1];
        strcpy(data, str);
    }
}
*/

String::String(const char* str)
 {
    data = new char[strlen(str) + 1];
    strcpy(data, str);
}

String::~String() 
{
    delete[] data;
}

String::String(const String& rhs) 
{
    data = new char[rhs.size() + 1];
    strcpy(data, rhs.c_str());
}

// bad version
String& String::operator=(const String& rhs)
 {
    if (this != &rhs) 
    {
        delete[] data;
        data = new char[rhs.size() + 1];
        strcpy(data, rhs.c_str());
    }
    return *this;
}

size_t String::size() const
{
    return strlen(data);
}

const char* String::c_str const() 
{
    return data;
}

感谢大家,我是假装很努力的YoungYangD(小羊)。

参考资料:
《c++ primer (第五版)》
《深度探索C++对象模型》

猜你喜欢

转载自blog.csdn.net/weixin_39640298/article/details/84726340