============C++ 入门5 ---- 类和动态内存分配===============
为了说明类和动态内存分配,我们先来看一个设计得十分糟糕的类 ---- StringBad
-------------------------------------------StringBad.h---------------------------------
1 /** 2 * 一个不好的内存分配的例子 3 * 定义了类 4 * StringBad.h 5 */ 6 7 #include<iostream> 8 9 #ifndef STRINGBAD_H_ 10 #define STRINGBAD_H_ 11 class StringBad 12 { 13 private: 14 char* str; 15 int len; 16 static int num_strings; 17 public: 18 StringBad(const char *s);//Constructor 19 StringBad();//defaut Constuctor 20 ~StringBad(); //Destructor 21 22 //friend function 23 friend std::ostream& operator << (std::ostream &os, const StringBad &st); 24 }; 25 #endif
-------------------------------------------StringBad.cpp---------------------------------
#include <cstring> #include "StringBad.h" using std::cout; //初始化静态成员变量 int StringBad::num_strings= 0; //Construct StringBad frome C string StringBad::StringBad(const char *s) { //这种方式使得字符串并没有保存在对象当中,而是使得 //字符串保存在对内存当中,对象只是保存了指出在哪里 //去查找字符串的信息 len = std::strlen(s); str = new char[len+1]; std::strcpy(str, s); num_strings++; cout << num_strings << ": \"" << str << " \" object created.\n"; } StringBad::StringBad() { //这种方式使得字符串并没有保存在对象当中,而是使得 //字符串保存在对内存当中,对象只是保存了指出在哪里 //去查找字符串的信息 len = 4; str = new char[len+1]; std::strcpy(str, "C++"); num_strings++; cout << num_strings << ": \"" << str << " \" object created.\n"; } StringBad::~StringBad() { cout << "\" " << str << " \" object deleted, "; --num_strings; cout << num_strings << " left\n"; //删除对象可以释放对象本身所占的内存,但是并不能自动释放 //属于对象成员的指针指向的内存。因此,必须在对象删除的时 //候用 delete 释放由构造函数new 出来的内存空间 delete []str; } std::ostream& operator<< (std::ostream &os, const StringBad &st) { os << st.str; return os; }
-------------------------------------------TestStringBad.cpp---------------------------------
#include "StringBad.cpp" #include "StringBad.h" int main() { //using StringBad; //const char * c = "Fanlielong"; StringBad s = "可以这样初始化对象"; StringBad* pS = new StringBad("初始化"); //这个对象所占的内存没有被释放 delete(pS); //在这里释放pS所占有的内存空间 cout << s << std::endl; return 0; }
TestStringBad的输出结果:
1: "可以这样初始化对象 " object created. 2: "初始化 " object created. " 初始化 " object deleted, 1 left 可以这样初始化对象 " 可以这样初始化对象 " object deleted, 0 left
看上去好像没什么问题,
为了说明这个StringBad真的是bad,我们再测试一次:
-------------------------------------------AnothoerTestStringBad.cpp---------------------------------
#include <iostream> #include "StringBad.cpp" using std::cout; using std::endl; void callme1(StringBad & reb);//pass by reference void callme2(StringBad sb);//pass by value int main() { StringBad headline1 = "第一个字符串"; StringBad headline2("第二个字符串"); StringBad sports("第三个字符串"); StringBad CPP; cout << endl << endl; cout << "Headline_1:" << headline1 << endl; cout << "Headline_2:" << headline2 << endl; cout << "Sports:" << sports << endl; callme1(headline1); //按引用传递没有什么问题 //headline2 按值传递从而导致析构函数被调用,尽管按值传递可以防止原始参数被 //修改,但实际上函数已经使原始字符串无法被访问,可能会导致一些非标准字符的出现 callme2(headline2); //在函数里面第二个字符串的空间就被释放了 //最后的字符串的统计数量居然是负值 //在某些编译器或操作系统上运行本程序的时候通常会显示有关 //的还有-1个对象的信息之前中断,有些这样的机器将报告通过 //通用保护错误(GPF)。 cout << endl << endl; return 0; } void callme1(StringBad & reb) { cout << endl << endl; cout << "字符串按引用传递: \n"; cout << " \"" <<reb << "\"\n"; } void callme2(StringBad sb) { cout << endl << endl; cout << "字符串按值传递: \n"; cout << " \"" <<sb << "\"\n"; }
AnothoerTestStringBad的测试结果:
1: "第一个字符串 " object created. 2: "第二个字符串 " object created. 3: "第三个字符串 " object created. 4: "C++ " object created. Headline_1:第一个字符串 Headline_2:第二个字符串 Sports:第三个字符串 字符串按引用传递: "第一个字符串" 字符串按值传递: "第二个字符串" " 第二个字符串 " object deleted, 3 left " C++ " object deleted, 2 left " 第三个字符串 " object deleted, 1 left " " object deleted, 0 left " 第一个字符串 " object deleted, -1 left
哈哈, 看出说明问题了吧,最后剩下的对象个数居然是-1.
原因在于headline2 按值传递的时候导致了析构函数被调用
StringBad 类的问题是由自动定义的隐式成员函数引起的,这种函数的行为与类的设计不相符。具体来说,C++自动提供了下面这些成员函数:
· 默认构造函数,如果没有定义构。
· 复制构造函数,如果没有定义。
· 赋值操作函数,如果没有定义。
· 默认析构函数,如果没有定义。
· 地址操作符,如果没有定义。
通常在没有自己定义的情况下,编译器将自动生成上述的最后4中函数的定义。
从上面的结果表明编译器是自动生成了隐式复制构造函数和隐式赋值操作符引起的,下面就这几种隐式成员函数进一步讨论:
一、默认构造函数
如果没有提供任何构造函数,C++将默认创建一个不带任何参数的构造函数。
例如定义了一个Test的类
class Test { public: int data; };
当调用Test类的时候,C++将生成一个默认构造函数:
Test::Test(){}
但是如果定义了一个一个或者多个构造函数的时候C++将自动选择最优的哪一个构造函数:
#include <iostream> using namespace std; //定义Test类 class Test { public: int data; //不带参数的构造函数 Test() { data = 10; } //带参数的构造函数 Test(int data) { this->data = data; } //....其他的构造函数 }; int main() { Test t_1;//将选择不带参数的构造函数 cout << "调用默认构造函数:data = " << t_1.data << endl; Test t_2(30); cout << "调用带参数的构造函数:data = " << t_2.data << endl; return 0; }
测试结果:
调用默认构造函数:data = 10 调用带参数的构造函数:data = 30
二、复制构造函数
复制构造函数,用于将一个对象复制到新创建的对象中。他用于对象的初始化中,而不是常规的赋值过程中。
类的复制构造函数的原型通常如下:
Class_name (const Class_name &);
他接受一个指向类对象的常量引用作为参数,例如最开始的例子中StringBad类的复制构造函数原型如下:
StringBad (const StringBad &);
对于复制构造函数:
· 调用复制构造函数的时机
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。对于开始的StringBad对象来说下面四种情况都将调用复制构造函数(将设temp是一个StringBad对象):
1)、StringBad s (temp); //直接调用 StringBad (const StringBad &);
2)、StringBad s = temp; //可能会生成临时对象,取决于具体实现
3)、StringBad s = StringBad(temp); //可能会生成临时对象,取决于具体实现
4)、StringBad* ps = new StringBad(temp); //初始化一个匿名对象,并将新对象的地址赋给ps.
每当程生成了副本时,编译器都会使用复制构造函数。例在最开始的例子中callme2(headline2);调用时,就使用了复制构造函数,所以对于对象的传递一般都是按引用传递。
· 复制构造函数的功能
默认的复制构造函数逐个复制非静态成员(浅复制),如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象,静态成员不会受到影响,因为他们属于类,而不是属于对象。
现在我们就可以最开始的奇怪结果做出解释:
原因很简单:在按值传递的时候程序调用了默认的复制构造函数,创建了一个临时对象,默认复制构造函数没有说明他的行为,函数调用之后,临时对象又调用了析构函数,更新了一次计数器(num_strings),导致析构的次数增加1次,所以就出现了最后哦剩余的对象的个数为-1的现象。
那么,怎么解决这个问题呢?
一个很直接的办法就是:提供一个对计数进行更新的显示复制构造函数:
StringBad::StringBad(const StringBad &s) { num_strings++; ...// 加入其它内容 }
但是直接增加上面的代码会出现问题,因为隐式复制构造函数是按值进行复制的,显式的调用是按引用复制的,调用析构函数的时候就会出现问题:
这是因为同一片内存被释放了两次。
解决这个问题的办法是深度复制(Deep Copy)。也就是说,复制构造函数应当复制字符串,而不是复制指向字符串的指针,并将复制的副本的地址赋给str成员。这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。所以StringBad类的复制构造函数应该为:
//显式的复制构造函数 //使用深度复制(Deep Copy) StringBad::StringBad(const StringBad &s) { num_strings++;//调用复制构造函数的时候就将计数器的值增加1 len = s.len; str = new char[len + 1]; std::strcpy(str, s.str); cout << num_strings << "(复制构造函数钓调用): \"" << str << " \" object created\n"; }
修改后的输出结果为:
1: "第一个字符串 " object created. 2: "第二个字符串 " object created. 3: "第三个字符串 " object created. 4: "C++ " object created. Headline_1:第一个字符串 Headline_2:第二个字符串 Sports:第三个字符串 字符串按引用传递: "第一个字符串" 5(复制构造函数钓调用): "第二个字符串 " object created 字符串按值传递: "第二个字符串" " 第二个字符串 " object deleted, 4 left " C++ " object deleted, 3 left " 第三个字符串 " object deleted, 2 left " 第二个字符串 " object deleted, 1 left " 第一个字符串 " object deleted, 0 left
可以看到复制构造函数在调用callme2()函数的时候被调用了。
必须定义复制构造函数的意义就在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。
由此我们得出一个结论:
如果类中包含了使用new 初始化的指针成员,应当定义一个复制构造函数进行深复制。
三、赋值操作符
对于最开始的StringBad类还存在一个隐患的问题:赋值操作符。
C++允许类对象赋值,这是通过自动为类重载赋值操作符来实现的。
原型如下:
Class_name & Class_name::operator=(const Class_name &);
由于进行赋值的时候会出现与隐式复制构造函相同的问题,即在复制的时候没有进行深复制而是进行浅复制。
但是和默认数值构造函数也有一些区别:
· 由于目标对象可能引用了以前分配的数据,所以应用delete[]来释放这些数据
· 函数应当避免将对象赋给自身,否则,给对象重新赋值之前释放内存操作可能删除对象的内容
· 函数返回一个纸箱调用对象的引用(因为需要考虑到这样的操作s0 = s1 = s2;)
下面通过重载赋值操作符来解决这个问题:
//重载赋值操作符,解决赋值的问题 StringBad& StringBad::operator=(const StringBad & st) { if(this == &st){ return *this;//考虑到 s = s;的情况 } delete[] str;//释放将被覆盖的内容 len = st.len; str = new char[len+1]; strcpy(str, st.str);//进行赋值操作 return *this;//返回对象以便方便连续赋值 }