最近在看《C++ Primer Plus》的类和动态内存分配,于是自己也敲了相应的代码,但是得到的结果却和书上的不太一样。于是想整理一下相关的内容。内容主要是深浅复制、复制构造函数以及赋值运算符的问题。
先从一段简单的代码开始:
#include
#include
using namespace std;
class Student
{
private:
char *name;
int number;
public:
Student(char *na, int n);
~Student();
setnumber(int n);
setname(char *na);
void Print();
};
Student::Student(char *na, int n)
{
name = new char[strlen(na)+1];
strcpy(name, na);
number = n;
cout << "Student:" << (int)name << " Number:" << number <
细细分析一下上述代码中的 "TOM" 为什么会变成 "Bob" ,可以发现当用s1去初始化s2时,s1的name的地址也复制给了s2。那么自然而然,当修改s2的name时,s1的name也跟着变化。这对Student对象的使用造成了麻烦,而且还会带来别的问题。
这里先给出解决办法:
Student::Student(const Student &st)
{
int len = strlen(st.name);
name = new char[len + 1];
strcpy(name, st.name);
}
至于为什么这么做,我们需要接着看下面的内容。
上面的程序只是在抛砖引玉,接下来的内容是《C++ Primer Plus》中给出的程序代码。通过下面的代码,能让我们发现很多潜在的问题。
#include
#include
#include
using namespace std;
class StringBad
{
private:
char *str;
int len;
static int num_strings;
public:
StringBad(const char *s);
StringBad();
~StringBad();
friend std::ostream & operator << (std::ostream & os, const StringBad & st);
};
int StringBad::num_strings = 0;
StringBad::StringBad(const char *s)
{
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
num_strings++;
cout << num_strings << ": \"" << str << "\" object created\n";
}
StringBad::StringBad()
{
len = 4;
str = new char[4];
strcpy(str, "C++");
num_strings++;
cout << num_strings << ": \"" << str << "\" default object created\n";
}
StringBad::~StringBad()
{
cout << "\"" << str << "\" object deleted, ";
--num_strings;
cout << num_strings << " left\n";
delete []str;
}
ostream & operator << (ostream & os, const StringBad &st)
{
os << st.str;
return os;
}
void callme1(StringBad &);
void callme2(StringBad);
int main()
{
cout << "String an inner block.\n";
StringBad headline1("asd");
StringBad headline2("qwe");
StringBad sports("zxc123");
cout << "headline1: " << headline1 <
这里还要说明的一点是:我用CodeBlocks运行上述代码,没有达到书本分析的预期结果。具体情况是临时对象调用了析构函数释放相应内存后,还可以获取内存中的值。查阅资料后发现原因是:RVO(return value optimization),被C++进行值返回的优化。有的软件可以关闭RVO,因为我不太清楚CodeBlocks如何关闭它。所以我将代码放到了Linux系统下运行。将RVO优化关闭,可以对g++增加选项-fno-elide-constructors,重新编绎之后,执行结果如下:
关闭RVO后,一下子就发现问题了。
这里有一个问题是因为当时忘写knot = headline1;这一条赋值语句
导致"Exiting the block.下面出现的是
"C++" object deleted, 2 left而不是"asd" object deleted, 2 left
程序第一个问题是输出中出现的各种非标准字符随系统而异,另一个问题是对象计数为负。程序开始时还是正常的,但逐渐变得异常,最终导致了灾难性结果。可以看出到headline1传递给callme1()函数,并在调用后重新显示headline1。
这一块代码运行都是正常的:
String passed by reference:
"asd"
headline1: asd
但随后程序将headline2传递给了callme2,出现了一个严重的问题:
String passed by value:
"qwe"
"qwe" object deleted, 2 left
headline2:
首先,将headline2作为函数参数来传递给函数,导致析构函数被调用。其次,虽然按值传递可以防止原始参数被修改,但实际上函数已使原字符串无法识别,导致显示一些非标准字符(显示的内容取决于内存中所包含的内容)。
在为每一个创建的对象自动调用析构函数时,情况更糟糕:
上面的计数异常是一条线索,因为每个对象被构造和析构一次,因此调用构造函数的次数应当与析构函数的调用次数相同。对象计数递减的次数比递增的次数多2,这表明使用了不将num_string递增的构造函数创建了两个对象。此时使用的不是默认的构造函数,也不是参数为const char *的构造函数,而是复制构造函数。
复制构造函数用于将一个对象复制到新创建的对象中。也就是说它用于初始化过程中,而不是常规的赋值过程中。
类的复制构造函数原型通常如下:
Class_name(const Class_name &);
它接受一个指向类对象的常量引用作为参数。例如:
class StringBad
{
private:
char *str;
int len;
static int num_strings;
public:
StringBad(const char *s);
StringBad();
StringBad(const StringBad &); //复制构造函数
~StringBad();
friend std::ostream & operator << (std::ostream & os, const StringBad & st);
};
新建一个对象并将其初始化为同类现有的对象时,复制构造函数都将被调用。假设motto是一个StringBad对象,则下面4种声明将调用复制构造函数:
StringBad ditto(motto);
StringBad metoo = mott;
StringBad also = StringBad (motto);
StringBad * pStringBad = new StringBad(motto);
其中中间的2种声明可能会使用复制构造函数直接创造metoo和also,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋值给metoo和also,这取决于具体实现。每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。
调用复制构造函数总结:
(1)用对象去初始化另一个对象。
(2)函数的参数是类对象(值传递)。
(3)返回值是类对象。
(1)默认的复制构造函数不说明其行为(逐个赋值非静态成员,只是复制成员的值,成员复制也称为浅复制),因为它不指出创建过程,也不增加计数器num_strings的值。但析构函数更新了计数,并且在任何对象过期时都将被调用,而不管对象是如何被创建的。这就导致了程序无法准确地记录对象的个数。
(2)就像开头的小程序一样,程序复制的不是字符串,而是一个指向字符串的指针。也就是说,将sailor初始化为sports后,会有两个指向同一个字符串的指针。当析构函数被调用时,str指针所指向的内存将被释放。此时,另一个对象再用str指针去访问这块区域,必然导致不确定的、可能有害的后果。
(3)最后一个问题是,试图释放内存两次可能导致程序异常终止(不同系统提供的信息不同)。
定义一个显式复制构造函数以解决问题
解决类设计中的这种问题的方法是进行深度复制(deep copy)。也就是说,复制构造函数应当复制字符串并将副本的地址赋值给str成员,而不是仅仅复制字符串地址。
StringBad::StringBad(const StringBad & st)
{
num_strings++;
len = st.len;
str = new char[len + 1];
strcpy(str, st.str);
cout << num_strings << ": \"" << str << "\" object created\n";
}
这时程序可以打印出headline2的值、字符串不乱码以及计数恢复正常。但是最后一条的字符串不应该为空,这又是一个问题。
StringBad的其他问题:赋值运算符
(1)赋值运算符的功能以及何时使用它
StringBad headline1(“asd”);
...
StringBad knot;
knot = headline1;
初始化对象时,不一定会使用赋值运算符
StringBad metoo = knot; //使用复制构造函数
与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
(2)赋值的问题
上述程序中将headline1赋值给了knot:
knot = headline1;
为knot调用析构函数时,knot.str所指向的区域将被释放。那么就会出现和隐式复制构造函数一样的结果:数据受损。以及试图删除已经删除的数据导致的结果是不确定的,因此可能会改变内存中的内容,导致程序异常终止。
(3)解决赋值问题
对于由与默认赋值运算符不合适而导致的问题,解决办法是提供赋值运算符(进行深度复制)定义。
StringBad & StringBad::operator = (const StringBad & st)
{
if (this == &st)
return *this;
delete []str;
len = st.len;
str = new char[len + 1];
strcpy(str, st.str);
return *this;
}
代码首先检查自我复制,这是通过查看赋值运算符右边的地址是否与接受对象的地址相同来完成的。如果相同,程序返回*this,然后结束。如果地址不同,函数将释放str指向的内存,这是因为稍后将把一个新字符串的地址赋给str。赋值操作并不创建新的对象,因此不需要调整静态数据成员num_strings的值。通过修改程序,上述代码存在的3个问题都得到了解决。最终的运行结果如下:
总结:深复制开辟新的空间,而浅复制没有。