C++学习笔记(14)——类的特殊成员函数

本篇笔记将深入浅出地探讨C++类的特殊成员函数相关知识。这些特殊成员函数也许你不经常见到,但是它们会自动生成和定义。这些可能隐式生成的特殊成员函数有:隐式默认构造函数,隐式默认析构函数,隐式默认拷贝构造函数,隐式赋值运算符以及隐式地址运算符。其中,隐式的拷贝构造函数和赋值运算符将可能带来一些问题。为了很好的理解其中的原理要点,特别的从一个实际工程中遇到的例子入手。

一、工程中的一个例子

在具体的一个软件工程中,有这样一段代码:

struct CharArray
{
	CharArray()
	{
		strData = 0;
	}
	CharArray(const char* data)
	{
		if (strData)
		{
			delete[] strData;
			strData = 0;
		}
		if (data)
		{
			//size_t len = std::strlen(data);
			size_t len = strlen(data);
			if (len > 0)
			{
				strData = new char[len+1];
				memcpy(strData, data, len*sizeof(char));
				strData[len] = '\0';
			}
		}
	}
	~CharArray()
	{
		if (strData)
		{
			delete[] strData;
			strData = 0;
		}
	}

	const char* GetData() const 
	{
		return strData; 
	}

	char* strData;
};

这段代码原来不是一个类,而是struct(把上述struct关键字直接换成class即为类的声明),作者的大致意图是写一个字符串结构体(类),至于它为什么不用标准库中的string或着C++11中的array,我也不得而知,因为我并不是这段代码的原作者。在代码被送去做第三方测试时,静态分析问题单中有个问题是这段代码不安全,应编写拷贝构造函数。当时的我甚至没听过“拷贝构造函数”这个名词。这里,为了适应我们研究的主题——类,我姑且把它改造为一个看上去舒服点的类。

CharArray.h

#ifndef CHARARRAY_H

#define CHARARRAY_H

class CharArray

{

public:

   CharArray();//显式声明默认构造函数

   CharArray(const char* data);//带参数的构造函数

   ~CharArray();//析构函数

private:  //私有成员

   char* strData;

public:       //公有接口

   const char* GetData() const ;

};

#endif

CharArray.cpp:

#include "CharArray.h"

#include <cstring>

CharArray::CharArray()

{

   strData = 0;

}

CharArray::CharArray(const char* data)

{

   int len = strlen(data);

   strData = new char[len + 1];

   strcpy(strData, data);

   strData[len] = '\0';

}

CharArray::~CharArray()

{

   if (strData)

   {

       delete[] strData;

       strData = 0;

   }

}

const char* CharArray::GetData() const

{

   return strData;

}

这个类编译没问题,编写测试函数,在main中:

CharArray HelloWord("hello, world!");

CharArray  sss = "ssss";

都可以编译通过,调试还可以发现对象初始化成功,为了能够打印其值,按照上篇笔记的做法,为这个类添加一个重载<<的友元函数:

friend std::ostream & operator<<(std::ostream & os, const CharArray & str);

std::ostream & operator<<(std::ostream &os, const CharArray &str)

{

   if (str.strData)

   {

       os<<str.strData;

   } 

   return os;

}

在main函数中打印该字符串,运行后,可以在终端中看到打印的信息。且程序能够正常退出。

cout<<"Before:"<<HelloWord<<endl;

如果这个类的应用仅限于此,确实是不会出现问题的。这也是为什么我们的软件没有因为这个问题单提出的问题而导致bug。但,问题单中描述的问题确实是存在的,下面慢慢揭开它的面纱。

二、可能会出现的三个问题

1.类的对象当然很可能被当做函数参数传递。假如,在main.cpp中编写一个函数,该函数用HelloWorld作为参数:

void TestStr(CharArray str)//将类的对象作为函数参数按值传递

{

   //do sth

}

在main函数中:

TestStr(HelloWord);

cout<<"After:"<<HelloWord<<endl;

如果在debug下调试,程序将很有可能崩溃,如果只运行不调试,则第二行打印的应该是乱码:

Before:hello, world!

After:葺葺葺葺葺葺葺葺葺葺葺葺!

2.类对象也可以直接赋值,就像两个int型的对象可以互相赋值一样。在main函数中编写如下测试代码:

CharArray Str1("Hi,Str!");

cout<<"Before:"<<Str1<<endl;

{

    CharArray Str2 = Str1;

    cout<<Str2<<endl;

 }

cout<<"After::"<<Str1<<endl;

这里的中括号表示Str2是在中括号内部起作用的临时变量。这意味着在离开中括号后,Str2将被清理。运行程序出现第三行重新打印Str1乱码:

Before:Hi,Str!

Hi,Str!

After::葺葺葺葺葺葺葺葺"

3.在main函数中添加如下代码,实现两个字符串的赋值操作:

{

       CharArray Str3("Hi, Wayne!");

       CharArray str4;

       str4 = Str3;

       cout<<"After::"<<Str3<<"   "<<str4<<endl;

}

这段代码和问题二中的代码的区别是,问题二中类似于初始化的操作,问题三中是单纯的赋值。这在C++的运行机理上是不同的,下面会具体分析。这里运行后,程序可以打印两个字符串,但是你却发现你的程序“跑飞”了,你只能强制关闭它。

三、再谈拷贝构造函数

在《初识类》中涉及到了类的构造函数和类的拷贝构造函数。我们知道如果没有提供显式的构造函数,编译器将添加一个没有任何操作的默认构造函数。同样,如果没有提供显式的拷贝构造函数,编译器也将添加一个默认构造函数,它将实现复制每个非静态成员,复制的成员的值,这被称为浅复制。

那么什么时候程序会调用这个拷贝构造函数呢?新建一个对象并将其初始化为同类现有对象时,都将调用拷贝构造函数。如问题2中的情况,诸如下面的形式都将调用拷贝构造函数:

CharArray S1(“Test”);

CharArray S1(S2);

CharArray S1 = S2;

CharArray S1 = (CharArray)S2;

CharArray *S1 = new CharArray(S2);

再有,每当程序生成了对象副本时,编译器也将使用拷贝构造函数实现创建副本的过程。比如,问题1中的将对象按值传递时,将创建一个HelloWorld的副本,此时编译器将调用拷贝构造函数实现这一过程。

由于默认构造函数是浅复制,按值复制,在问题1中是将HelloWorld实参复制为HelloWorld形参,问题2中将Str1复制到Str2,复制的是strData指针,是个地址值,并没有复制字符串。离开变量的作用域后,则会执行析构函数。也即当退出TestStr函数后,HelloWorld对象执行析构函数,将delete[] StrData,由于实参和形参的StrData地址一样,因此再次打印实参HelloWorld时,出现了乱码。同理,问题2中离开代码块后,临时变量Str2将执行析构函数,而Str2中和Str1中StrData的地址也是一样的,析构后,Str1中的字符串也即被清理。

解决问题的办法很简单,就是提供一个显式的拷贝构造函数。这也就是静态问题单中给出的建议。这个显式的拷贝构造函数实现深度复制,也即不是复制成员的值,而是复制成员的数据。这个函数中,将重新new出一段内存空间,以存放新的对象的成员数据,进而将原有的数据拷贝进这段地址中。CharArray的拷贝构造函数可以如此写:

CharArray(const CharArray &str);//声明拷贝构造函数

CharArray::CharArray(const CharArray &str)

{

   strLen = str.strLen;

   strData = new char[strLen + 1];

   strcpy(strData, str.strData);

   strData[strLen] = '\0';

}

这里将之前的临时变量len升级为类的成员变量,其意义不言自喻。重新编译运行,应该可以看到可以正确打印HelloWorld和Str1的值了。

Before:hello, world!

After:hello, world!

 

Before:Hi,Str!

Hi,Str!

After::Hi,Str!

四、重载赋值运算符

当一个对象赋值给另外一个对象,将使用重载的赋值运算符。这就像我们为CharArray编写的重载<<运算符一样。问题是,我们不是必须显式的提供这个重载接口函数。也即上述代码中并没有编写operator=的函数接口,但我们仍然可以用Str4=Str3的方式来实现赋值。这说明,赋值运算符是可以自动生成的。这种隐式的赋值运算符实现的操作也是浅复制,即复制成员值,而非数据。回到问题3,对象赋值操作后,程序跑飞了,同样是因为数据受损。因为浅复制导致Str4中复制的是Str3的StrData地址,离开程序块时,将依次调用Str4和Str3的析构函数。我们知道,如果对同一地址执行多次delete会带来异常,因此程序跑飞了。

解决办法同样是提供显式的重载赋值运算符的函数,从而进行深度复制。这个函数的声明为:

CharArray & CharArray::operator=(const CharArray & str);

由于被赋值的对象可能之前已经被初始化或赋值,所以首先要用delete来清理这些数据。另外,要避免把对象赋给自身,否则对象重新赋值前,释放内存操作可能删除对象的内容。也即如果判断是Str3=Str3,则没必要重新分配内存,直接返回它自己即可。为了能够实现连续赋值Str4=Str3=Str2,则要将返回值定义为类的引用类型。具体实现如下:

CharArray & CharArray::operator=(const CharArray & str)

{

   if (this == &str)

   {

       return *this;

   }

   delete[] strData;

   strLen = str.strLen;

   strData = new char[strLen + 1];

   strcpy(strData, str.strData);

   return *this;

}

重新编译运行,可以安全无误的运行和输出结果了:

Before:hello, world!

Call Destroy

After:hello, world!

 

Before:Hi,Str!

Hi,Str!

Call Destroy

After::Hi,Str!

 

After::Hi, Wayne!   Hi, Wayne!

Call Destroy

Call Destroy

 

Call Destroy

Call Destroy

请按任意键继续. . .

这里我在析构函数中添加了一条打印消息“Call Destroy”来大概确定何时调用了析构函数。你可以自己对应一下上面的每条“Call Destroy”对应哪个对象的析构函数。

五、总结

至此,我们必须清楚使用new初始化对象的指针成员必须特别小心:

1.前面的《初始类》中总结了在构造函数中new的内存,必须显式地在析构函数中用delete清理。

2.new和delete对应,new[]和delete[]对应。如CharArray类的默认构造函数中对成员变量初始化可以:

   strData = new char[1];

   strData[0] = '\0';

不能够

   strData = new char;

虽然开辟的空间是一样的,但是第二种方法和析构函数中的delete[]不匹配。

这里可以还可以用

strData = 0;//空指针

应为对于空指针,可以同时用delete或delete[]清理。

3.应定义一个显式拷贝构造函数,通过深度复制将一个对象初始化为另一个对象。

4.应定义一个重载赋值运算符函数,通过深度复制将一个对象复制给另外一个对象。

六、上源码

//CharArray.h

#ifndef CHARARRAY_H

#define CHARARRAY_H

#include <iostream>

class CharArray

{

public:

   CharArray();//显式声明默认构造函数

   CharArray(const char* data);//带参数的构造函数

   CharArray(const CharArray &str);//声明拷贝构造函数

   ~CharArray();//析构函数

private:  //私有成员

   char* strData;

   int strLen;

public:       //公有接口

   const char* GetData() const ;

   //重载赋值运算符

   CharArray & CharArray::operator=(const CharArray & str);

   //友元重载<<运算符

   friend std::ostream & operator<<(std::ostream & os, const CharArray & str); 

};

#endif



//CharArray.cpp

#include "CharArray.h"

#include <cstring>

CharArray::CharArray()

{

   strLen = 0;

// strData = new char[1];

// strData[0] = '\0';

   strData = 0;//空指针

}

CharArray::CharArray(const char* data)

{

   strLen = strlen(data);

   strData = new char[strLen + 1];

   strcpy(strData, data);

   strData[strLen] = '\0';

}

CharArray::CharArray(const CharArray &str)

{

   strLen = str.strLen;

   strData = new char[strLen + 1];

   strcpy(strData, str.strData);

   strData[strLen] = '\0';

}

CharArray::~CharArray()

{

   if (strData)

   {

       delete[] strData;

       strData = 0;

   }

   std::cout<<"Call Destroy"<<std::endl;

}

const char* CharArray::GetData() const

{

   return strData;

}

CharArray & CharArray::operator=(const CharArray & str)

{

   if (this == &str)

   {

       return *this;

   }

   delete[] strData;

   strLen = str.strLen;

   strData = new char[strLen + 1];

   strcpy(strData, str.strData);

   return *this;

}

std::ostream & operator<<(std::ostream &os, const CharArray &str)

{

   if (str.strData)

   {

       os<<str.strData;

   } 

   return os;

}



#include "CharArray.h"

//main.cpp

#include <iostream>

using namespace std;

void TestStr(CharArray str);



int main()

{

   //static CharArray  sss = "ssss";

  

   CharArray HelloWord("hello, world!");

   cout<<"Before:"<<HelloWord<<endl;

   TestStr(HelloWord);

   cout<<"After:"<<HelloWord<<endl;

  

   cout<<endl;



   CharArray Str1("Hi,Str!");

   cout<<"Before:"<<Str1<<endl;

    {

       CharArray Str2 = Str1;

       cout<<Str2<<endl;

    }

   cout<<"After::"<<Str1<<endl;



   cout<<endl;

   {

       CharArray Str3("Hi, Wayne!");

       CharArray str4;

       str4 = Str3;

       cout<<"After::"<<Str3<<"   "<<str4<<endl;

   }

   cin.get();

   return 0;

}

void TestStr(CharArray str)//将类的对象作为函数参数按值传递

{

   //do sth

}
发布了76 篇原创文章 · 获赞 63 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/bjtuwayne/article/details/84888284