C++里面设计一个含指针的类比设计一个不含指针的类要复杂很多,因为需要考虑Big Three,也就是三个特殊函数:
拷贝构造函数(copy constructor),
拷贝赋值函数(copy assignment operator)
析构函数(destructor)
为什么不含指针的类不需要考虑Big Three呢?因为C++编译器会生成缺省构造函数, 缺省拷贝构造函数和缺省拷贝赋值函数,这些函数采用memberwise copy,也就是浅拷贝。如果类不含指针的话,这也就够用了。编译器也会生成缺省的析构函数,但该析构函数不做任何操作。如果类不含指针,当对象生命期结束时析构也不用做什么事情,所以也没问题。
但是类里面含有指针的话,C++编译器生成的缺省拷贝构造函数和缺省拷贝赋值函数会采用浅拷贝将一个object的成员一项一项的拷贝给另一个object,这样会导致同一个类的两个object里面的指针指向同一个地址。这样,如果object1的指针值赋值给了object2,那object2的指针原来指向的内存就没人管了,造成内存泄漏。那如果object2的指针之前是NULL行不行呢?也不行。因为如果object 1修改了这个指针的内容,或者删除了这个指针,object 2就躺着中枪了。
所以,类里面有指针的话,我们要自己设计Big Three。其中的 拷贝构造函数和拷贝赋值函数会生成新的指针,然后把指针指向的内容拷贝过来。这就是深拷贝。
以class string为例,代码如下:
class String
{
public:
String(const char* cstr = 0); //构造函数
String(const String& str); //拷贝构造函数
String& operator=(const String& str); //拷贝赋值函数
~String(); //析构函数
char* get_c_str() const {return m_data;}
private:
char* m_data;
}
先讨论拷贝构造函数(copy ctor),代码例子如下:
inline String::String(const String& str)
{
m_data = new char[ strlen(str.m_data)+1];
strcpy(m_data, str.m_data);
}
注意,拷贝构造函数里m_data是新创建的指针,它指向的内容是拷贝过来的。
拷贝构造函数的用法如下:
String s1("hello");
String s2(s1); // line 1
String s2=s1; // line 2
上面的line 1和line 2都会调用拷贝构造函数将s1的字符串赋给s2。注意s2的m_data跟s1的m_data的值不同,但指向的内容是一样的。
下面来谈谈拷贝赋值函数(copy assignment operator),代码例子如下:
inline String& String::operator=(const String& str)
{
if (this==&str)
return *this;
delete[] m_data;
m_data=new char[ strlen(str.m_data)+1];
strcpy(m_data, str.m_data);
return *this;
}
这里有很多地方需要注意:
1) 拷贝赋值函数必须要检查this指针是否已经是准备拷贝的对象里面的指针,换句话说就是不能把obj1赋值给obj1自己。因为如果不检查的话,接下来的delete[]就把这个m_data指针指向的内存给释放了,这个指针就成了野指针。
再细想一下,为什么前面的拷贝构造函数不需要检查this指针呢?这是因为用拷贝构造函数的时候,对象还没有创建,所以this指针为空。
2) 操作符=只能作为类的成员函数重载(也就是拷贝赋值函数),不能像其它操作符==,!=,++,–那样在类外作为一个单独的函数重载。
3) 为什么要delete[] m_data呢? 因为这里s2已经存在,它的m_data已经指向一段内存,里面已经有东西。直接把s1的m_data指向的字符串的内容拷贝给s2的m_data指向的内存是危险的,一是后者长度很可能不一样,再一个可能会覆盖了什么重要东西。所以我们要先把s2里面的m_data指向的内容清除掉, 再重新创建m_data指针。
4) 还有一个问题,为什么要用delete[]而不是delete呢?其实这里是可以用delete的,因为char是基本数据类型,不涉及到析构函数。delete[]和delete都是把m_data指向的那个字符串的内容给释放了,效果是一样的。但是为了规范起见,new[]应该和delete[]对应。
拷贝赋值函数的用法如下:
String s1("hello");
String s2(s1);
s2=s1; //line 3
这里s2会拷贝s1的字符串内容,注意s2和s1的m_data指向不同的地址。这里要特别注意的是:
s2=s1
和上面的line2
String s2=s1;
不一样,后者是调用的拷贝构造函数。在line3里面,s2这个object已经创建,里面的m_data指向的字符串已经有内容,而在line2里面,s2还没有被创建。
下面来谈谈析构函数(dtor)。析构函数在以下三个地方会被用到。
1.object生命周期结束,被销毁时;
2.delete指向object的指针,或delete指向object的基类类型指针并且基类析构函数是虚函数时;
3.object 1是object 2的成员,object 2的析构函数被调用时,object 1的析构函数也被调用。
String类的析构函数的例子如下:
inline String::~String()
{
delete[] m_data;
}
这里析构函数把m_data指向的字符串的内存给释放了。注意这里因为char是基本类型,所以用delete m_data其实是可以的。但是为了规范,new[]和delete[]必须要对应。另外,delete[]或者delete怎么知道要释放多少内存呢?因为new[]的时候编译器已经知道长度了。
下面谈谈new/delete和内存以及构造函数和析构函数的关系:
new: 先分配内存(这里会调用malloc),再调用ctor
delete: 先调用dtor,再释放内存(这里会调用free)
要注意的是,new[]一定要搭配delete[]。这里再强调一下,对于C++的基本数据类型比如int,char的数组,因为不涉及析构操作(内部无指针),delete和delete[]效果是一样的,都是释放内存。但是对于一个class的object数组,delete和delete[]效果就不一样了,举例如下:
String *p=new String[3];
delete[] p;
会调用String的析构函数三次,分别析构p[0],p[1],p[2],然后释放掉p[]的所有内存。
delete p;
只会调用String的析构函数一次,析构p[0],然后释放掉p[]的所有内存。
那么这里问题就来了,p[1],p[2]都清掉了,p[1],p[2]的m_data所指向的内存也就没人管了,造成2处内存泄漏。
关于delete p或delete[] p还要注意的一点是它只释放指针指向的内存,至于指针的值它不会置为NULL。所以有时候为了安全起见,还要把p设置为NULL。
本文详细解释了C++中Big Three(拷贝构造函数、拷贝赋值函数、析构函数)的概念及其重要性,特别是在含有指针的类中如何避免浅拷贝导致的问题,并通过String类实例展示了正确的实现方式。

512

被折叠的 条评论
为什么被折叠?



