目录
前言
类的构造函数、析构函数与赋值函数
构造函数与析构函数的起源
构造函数的初始化表
构造和析构的次序
示例:类String 的构造函数与析构函数
不要轻视拷贝构造函数与赋值函数
示例:类String 的拷贝构造函数与赋值函数
偷懒的办法处理拷贝构造函数与赋值函数
如何在派生类中实现类的基本函数
前言
前两篇笔记对这本书里面的文件结构、代码风格、命名规则、表达式和基本语句的良好编程习惯,将记录常量与函数设计做了记录。本篇读书笔记(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_dataint 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; // upcastdelete 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;
}