读书笔记——《高质量C++/C编程指南》(5)

简介: 读书笔记——《高质量C++/C编程指南》(5)

前言

前两篇笔记对这本书里面的文件结构、代码风格、命名规则、表达式和基本语句的良好编程习惯,将记录常量与函数设计做了记录。本篇读书笔记(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;
}


相关文章
|
17天前
|
算法 编译器 C语言
读书笔记——《高质量C++/C编程指南》(6)
读书笔记——《高质量C++/C编程指南》(6)
|
17天前
|
存储 安全 编译器
读书笔记——《高质量C++/C编程指南》(4)
读书笔记——《高质量C++/C编程指南》(4)
|
17天前
|
安全 编译器 程序员
读书笔记——《高质量C++/C编程指南》(3)
读书笔记——《高质量C++/C编程指南》(3)
|
17天前
|
程序员 编译器 C语言
读书笔记——《高质量C++/C编程指南》(2)
读书笔记——《高质量C++/C编程指南》(2)
|
17天前
|
安全 编译器 程序员
读书笔记——《高质量 C++/C 编程指南》(1)
读书笔记——《高质量 C++/C 编程指南》(1)
|
8月前
|
存储 算法 Java
[笔记]读书笔记 C++设计新思维《二》技术(Techniques)(二)
[笔记]读书笔记 C++设计新思维《二》技术(Techniques)(二)
|
3天前
|
存储 Serverless 数据安全/隐私保护
C++ 类的成员函数和数据成员的技术性探讨
C++ 类的成员函数和数据成员的技术性探讨
11 0
|
7天前
|
存储 编译器 程序员
c++存储类
c++存储类
26 3
|
7天前
|
C++
c++类&对象
c++类&对象
21 3
|
11天前
|
C++
C++派生类
C++派生类
26 0