前言
前两篇笔记对这本书里面的文件结构、代码风格、命名规则、表达式和基本语句的良好编程习惯,将记录常量与函数设计做了记录。本篇读书笔记(5)将记录类的构造函数、析构函数与赋值函数。
类的构造函数、析构函数与赋值函数
类的构造函数、析构函数与赋值函数构造函数、析构函数与赋值函数是每个类最基本的函数。
每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。
对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A 产生四个缺省的函数,如
A(void); // 缺省的无参数构造函数 A(const A &a); // 缺省的拷贝构造函数 ~A(void); // 缺省的析构函数 A & operate =(const A &a); // 缺省的赋值函数
默认的“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
String的结构如下:
class String { public: String(const char *str = NULL); // 普通构造函数 String(const String &other); // 拷贝构造函数 ~ String(void); // 析构函数 String & operate =(const String &other); // 赋值函数 private: char *m_data; // 用于保存字符串 };
构造函数与析构函数的起源
C++提供了更好的机制来增强程序的安全性。C++编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题。
但是程序通过了编译检查并不表示错误已经不存在了,仍然存在难以察觉的错误:由于变量没有被正确初始化或清除造成的,而初始化和清除工作很容易被人遗忘。因此创造了构造函数和析构函数!!!
当对象被创建时,构造函数被自动执行。
当对象消亡时,析构函数被自动执行。
这下就不用担心忘了对象的初始化和清除工作。
构造函数的初始化表
构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。
初始化表位于函数参数表之后,却在函数体 {} 之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。
构造函数初始化表的使用规则:如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
例如
class A {… A(int x); // A 的构造函数 }; class B : public A {… B(int x, int y);// B 的构造函数 }; B::B(int x, int y) : A(x) // 在初始化表里调用A 的构造函数 { … }
类的 const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化。
类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,
class A {… A(void); // 无参数构造函数 A(const A &other); // 拷贝构造函数 A & operate =( const A &other); // 赋值函数 }; class B { public: B(const A &a); // B 的构造函数 private: A m_a; // 成员对象 };
这两种方式的效率不完全相同。非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。例如
B::B(const A &a): m_a(a) { //… }
B::B(const A &a) { m_a = a; … }
将成员对象m_a 初始化。
类B 的构造函数在函数体内用赋值的方式将成员对象m_a 初始化。
我们看到的只是一条赋值语句,但实际上B 的构造函数干了两件事:
先暗地里创建m_a对象(调用了A 的无参数构造函数),
再调用类A 的赋值函数,将参数a 赋给m_a。
对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但。若类F 的声明如下:
class F { public: F(int x, int y); // 构造函数 private: int m_x, m_y; int m_i, m_j; }
后者的程序版式似乎更清晰些
F::F(int x, int y) : m_x(x), m_y(y) { m_i = 0; m_j = 0; }
F::F(int x, int y) { m_x = x; m_y = y; m_i = 0; m_j = 0; }
构造和析构的次序
构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。
析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
示例:类String 的构造函数与析构函数
// String 的普通构造函数 String::String(const char *str) { if(str==NULL) { m_data = new char[1]; *m_data = ‘\0’; } else { int length = strlen(str); m_data = new char[length+1]; strcpy(m_data, str); } } // String 的析构函数 String::~String(void) { delete [] m_data; // 由于m_data 是内部数据类型,也可以写成 delete m_data; }
不要轻视拷贝构造函数与赋值函数
如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。
倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
class String { public: String(const char *str = NULL); // 普通构造函数 String(const String &other); // 拷贝构造函数 ~ String(void); // 析构函数 String & operate =(const String &other); // 赋值函数 private: char *m_data; // 用于保存字符串 };
以类String 的两个对象a,b 为例,假设a.m_data 的内容为“hello”,b.m_data 的内容为“world”。
现将 a 赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data = a.m_data。
这将造成三个错误:
一是b.m_data 原有的内存没被释放,造成内存泄露;
二是b.m_data 和a.m_data 指向同一块内存,a 或b 任何一方变动都会影响另一方;
三是在对象被析构时,m_data 被释放了两次。
拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。
拷贝构造函数是在对象被创建时调用的,
而赋值函数只能被已经存在了的对象调用。
String a(“hello”); String b(“world”); String c = a; // 调用了拷贝构造函数,最好写成 c(a); c = b; // 调用了赋值函数
示例:类String 的拷贝构造函数与赋值函数
// 拷贝构造函数 String::String(const String &other) { // 允许操作other 的私有成员m_data int length = strlen(other.m_data); m_data = new char[length+1]; strcpy(m_data, other.m_data); } // 赋值函数 String & String::operate =(const String &other) { // (1) 检查自赋值 if(this == &other) return *this; // (2) 释放原有的内存资源 delete [] m_data; // (3)分配新的内存资源,并复制内容 int length = strlen(other.m_data); m_data = new char[length+1]; strcpy(m_data, other.m_data); // (4)返回本对象的引用 return *this; }
类 String 拷贝构造函数与普通构造函数的区别是:
在函数入口处无需与NULL 进行比较,这是因为“引用”不可能是NULL,
而“指针”可以为NULL。
类 String 的赋值函数比构造函数复杂得多,分四步实现:
(1)第一步,检查自赋值(a=a)。需要注意的是间接的自赋值,例如
// 内容自赋值 b = a; … c = b; … a = c; // 地址自赋值 b = &a; … a = *b;
自赋值为了防止多次释放同一块内存,第二步的delete,自杀后就不能复制自己
注意不要将检查自赋值的if 语句
if ( this == & other )
错写成为
if ( * this == other )
(2)第二步,用delete 释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。
(3)第三步,分配新的内存资源,并复制字符串。
注意函数strlen 返回的是有效字符串长度,不包含结束符‘\0’。函数strcpy 则连‘\0’一起复制。
(4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。
注意不要将 return *this 错写成 return this 。
偷懒的办法处理拷贝构造函数与赋值函数
如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,
偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
例如:
class A { … private: A(const A &a); // 私有的拷贝构造函数 A & operate =(const A &a); // 私有的赋值函数 };
如果有人试图编写如下程序:
A b ( a ) ; // 调用了私有的拷贝构造函数
b = a ; // 调用了私有的赋值函数
编译器将指出错误,因为外界不可以操作A 的私有函数。
如何在派生类中实现类的基本函数
基类的构造函数、析构函数、赋值函数都不能被派生类继承。
如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:
派生类的构造函数应在其初始化表里调用基类的构造函数。
//基类与派生类的析构函数应该为虚(即加virtual 关键字)。例如 #include <iostream.h> class Base { public: virtual ~Base() { cout<< "~Base" << endl ; } }; class Derived : public Base { public: virtual ~Derived() { cout<< "~Derived" << endl ; } }; void main(void) { Base * pB = new Derived; // upcast delete pB; }
输出结果为:
~Derived
~Base
如果析构函数不为虚,那么输出结果为
~Base
在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。例如:
class Base { public: … Base & operate =(const Base &other); // 类Base 的赋值函数 private: int m_i, m_j, m_k; }; class Derived : public Base { public: Derived & operate =(const Derived &other); // 类Derived 的赋值函数 private: int m_x, m_y, m_z; }; Derived & Derived::operate =(const Derived &other) { //(1)检查自赋值 if(this == &other) return *this; //(2)对基类的数据成员重新赋值 Base::operate =(other); // 因为不能直接操作私有数据成员 //(3)对派生类的数据成员赋值 m_x = other.m_x; m_y = other.m_y; m_z = other.m_z; //(4)返回本对象的引用 return *this; }