本篇笔记主要分为两个部分,第一部分是以String类为例的基于对象的编程,重点在于构造与析构、拷贝构造函数、拷贝赋值函数三个重要函数。这一部分与笔记(1)中的内容结合起来就是基于对象编程的主要内容。第二部分是在掌握了基于对象编程的基础上的面向对象编程(OOP)学习,讲解了类之间的组合、继承、委托关系。
第一部分、以String类(有指针类)为例讲解关键函数“Big Three”
1 class String 2 { 3 public: 4 String(const char* cstr=0); 5 String(const String& str); 6 String& operator=(const String& str); 7 ~String(); 8 char* get_c_str() const { return m_data; } 9 private: 10 char* m_data; 11 };
- 构造函数与析构函数
如笔记(1)中描述的,构造函数是在对象生成时被调用的特殊函数;相应的,析构函数是变量的生命结束时被调用的特殊函数。对于Complex类的对象来说,析构函数不需要进行特殊的操作;这时,编译器会自动提供一个默认的析构函数,其函数体为空。
为何String类需要特殊的析构函数?
首先看String类在产生时候进行了什么样的操作:
inline String::String(const char* cstr) { if (cstr) { m_data = new char[strlen(cstr)+1]; strcpy(m_data, cstr); } else { m_data = new char[1]; *m_data = ‘\0‘; } }
可以看到,在构造String类的对象时,依靠指针,运用new方法在堆中申请了一块空间。如果不采用特殊的析构函数的话,成员指针变量的生命周期已经结束,但是指针变量所申请的内存并没有被归还,从而导致内存溢出。因此,必须有相应的析构函数。析构函数的操作很简单,只需要delete [] m_data;一个操作即可。
需要注意的是,构造函数是先进行分配成员内存和赋初值的工作再进入函数体;而析构函数是先进入函数体执行操作,退出函数体后编译器自动归还成员变量的内存。
有此例可知,在构造函数中存在申请堆空间的new操作时,必须要在析构时进行delete操作。
2. 默认拷贝构造函数和默认拷贝赋值函数
所谓拷贝构造函数,是指当使用一个对象去初始化同类对象时会进行调用的函数。
例如:
1 String A; 2 String B(A); 3 String C = A;
String对象B和C产生时就调用了复制构造函数。其函数声明为
String(const String& str); 所谓拷贝赋值函数,就是用一个对象去给另一个对象赋值时所用到的函数,也就是‘=’运算符的运算符重载。
默认情况下,C++所提供的拷贝构造函数和拷贝赋值函数就是简单的位复制,将成员变量一位一位的进行复制,所有数据完全相同,称这种复制方式叫做浅复制。对于纯数据类,并不需要设计者提供拷贝构造函数和拷贝赋值函数即可,然而对于有指针的类来说,浅复制有这样的问题:
经过浅复制a=b,指针的地址进行了位拷贝,就会变成
这种情况下,堆空间下的“WORLD\0”字符串将永远不会被delete,产生内存泄露。对于拷贝构造的操作(即String b = a);位复制不会给b多分配一个内存,但是它会导致“HELLO\0”字符串被两个指针同时指向,当其中一个析构时,指针所指向的区域被delete;剩下的指针成为了野指针。此时如果另一个也发生了析构,则此在野指针上进行delete操作,会发生内存溢出。
这些问题的核心就在于浅复制上,将浅复制转化为深复制,即复制时为b中的指针分配一段堆内存,然后在这段堆内存里面写上a对象的对应字符串即可。
3.