C++内存管理学习笔记(6) (一)

2014-11-24 00:56:17 · 作者: · 浏览: 3

上期内容回顾:

C++内存管理学习笔记(5)

2.5 资源传递 2.6 共享所有权 2.7 share_ptr


--------------------------------------------------------------------------------

3 内存泄漏-Memory leak
3.1 C++中动态内存分配引发问题的解决方案
假设我们要开发一个String类,它可以方便地处理字符串数据。我们可以在类中声明一个数组,考虑到有时候字符串极长,我们可以把数组大小设为200,但一般的情况下又不需要这么多的空间,这样是浪费了内存。很容易想到可以使用new操作符,但在类中就会出现许多意想不到的问题,本小节就以这么意外的小问题的解决来看内存泄漏这个问题。。现在,我们先来开发一个String类,但它是一个不完善的类。存在很多的问题!如果你能一下子把潜在的全找出来,ok,你是一个技术基础扎实的读者,直接看下一小节,或者也可以陪着笔者和那些找不到问题的读者一起再学习一下吧。

下面上例子,

1: /* String.h */ 2: #ifndef STRING_H_ 3: #define STRING_H_ 4: 5: class String 6: { 7: private: 8: char * str; //存储数据 9: int len; //字符串长度 10: public: 11: String(const char * s); //构造函数 12: String(); // 默认构造函数 13: ~String(); // 析构函数 14: friend ostream & operator<<(ostream & os,const String& st); 15: }; 16: #endif 17: 18: /*String.cpp*/ 19: #include 20: #include 21: #include "String.h" 22: using namespace std; 23: String::String(const char * s) 24: { 25: len = strlen(s); 26: str = new char[len + 1]; 27: strcpy(str, s); 28: }//拷贝数据 29: String::String() 30: { 31: len =0; 32: str = new char[len+1]; 33: str[0]='"0'; 34: } 35: String::~String() 36: { 37: cout<<"这个字符串将被删除:"< 48: #include 49: #include "String.h" 50: using namespace std; 51: int main() 52: { 53: String temp("String类的不完整实现,用于后续内容讲解"); 54: cout< 运行结果(运行环境Dev-cpp)如下图所示,表面看上去程序运行很正确,达到了自己程序运行的目的,但是,不要被表面结果所迷惑!


这时如果你满足于上面程序的结果,你也就失去了c++中比较意思的一部分知识,请看下面的这个main程序,注意和上面的main加以区别,

1: #include 2: #include 3: #include "String.h" 4: using namespace std; 5: 6: void show_right(const String& a) 7: { 8: cout<

运行结果(环境Dev-cpp):程序运行最后崩溃!!!到这里就看出来上面的String类存在问题了吧。(读者可以自己运行一下看看,可以换vc或者vs等等试试)


为什么会崩溃呢,让我们看一下它的输出结果,其中有乱码、有本来被删除的但是却正常打印的“第二个范例”,以及最后析构删除的崩溃等等问题。

通过查看,原来主要是复制构造函数和赋值操作符的问题,读者可能会有疑问,这两个函数是什么,怎会影响程序呢。接下来笔者慢慢结识。

首先,什么是复制构造函数和赋值操作符?------>限于篇幅,详细分析请看《c++中复制控制详解(copy control)》

Tip:复制构造函数和赋值操作符

(1)复制构造函数(copy constructor)

复制构造函数(有时也称为:拷贝构造函数)是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用.当定义一个新对象并用一个同类型的对象对它进行初始化时,将显示使用复制构造函数.当将该类型的对象传递给函数或者从函数返回该类型的对象时,将隐式使用复制构造函数。

复制构造函数用在:

对象创建时使用其他相同类型的对象初始化;
1: Person q("Mickey"); // constructor is used to build q. 2: Person r(p); // copy constructor is used to build r. 3: Person p = q; // copy constructor is used to initialize in declaration. 4: p = q; // Assignment operator, no constructor or copy constructor.
复制对象作为函数的参数进行值传递时;

1: f(p); // copy constructor initializes formal value parameter.
复制对象以值传递的方式从函数返回。

一般情况下,编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值。使用默认的复制构造函数是叫做浅拷贝。

相对应与浅拷贝,则有必要有深拷贝(deep copy),对于对象中动态成员,就不能仅仅简单地赋值了,而应该有重新动态分配空间。

如果对象中没有指针去动态申请内存,使用默认的复制构造函数就可以了,因为,默认的复制构造、默认的赋值操作和默认的析构函数能够完成相应的工作,不需要去重写自己的实现。否则,必须重载复制构造函数,相应的也需要重写赋值操作以及析构函数。

2.赋值操作符(The Assignment Operator)

一般而言,如果类需要复制构造函数,则也会需要重载赋值操作符。首先,了解一下重载操作符。重载操作符是一些函数,其名字为operator后跟所定义的操作符符号,因此,可以通过定义名为operator=的函数,进行重载赋值定义。操作符函数有一个返回值和一个形参表。形参表必须具有和该操作数数目相同的形参。赋值是二元运算,所以该操作符有两个形参:第一个形参对应的左操作数,第二个形参对应右操作数。

赋值和赋值一般在一起使用,可将这两个看作一个单元,如果需要其中一个,几乎也肯定需要另一个。

ok,现在分析上面的程序问题。

a)程序中有这样的一段代码,


1: String* String1=new String(test1); 2: cout<<*String1<

假设test1中str指向的地址为2000,而String中str指针同样指向地址2000,我们删除了2000处的数据,而test1对象呢?已经被破坏了。大家从运行结果上可以看到,我们使用cout<

b)另外一段代码,


1: cout<<"使用错误的函数:"<

show_String函数的参数列表void show_String(const Str