第5章 类
# Cpp之旅(学习笔记)第5章 类
# 5.1 类的概述
Cpp最核心的语言特性就是类。
类(class)是一种用户自定义的类型,用于在程序代码中表示某种实体。
接下来,我们优先考虑对三种重要的类的基本支持:
- 具体类。
- 抽象类。
- 类层次结构中的类。
# 5.2 具体类型
具体类的基本思想是它们的行为“就像内置类型一样”。
例如:如数类型和无穷精度整数与内置的int非常像,当然它们有自己的语义和操作集合。
具体类型的典型定义特征是:它的成员变量是其定义的一部分。
它允许我们:
- 把具体类型的对象置于栈、静态分配的内存或者其他对象中。
- 直接引用对象(而非仅通过指针或引用)。
- 创建对象后立即进行完整的初始化(比如使用构造函数)。
- 拷贝与移动对象。
# 5.3 容器
容器是指一个包含若干元素的对象,因为Vector的对象都是容器,所以我们称Vector是一种容器类型。
Vector作为一种容器具有许多优点,但是存在一个致命的缺陷:它使用new分配元素,但从来没有释放这些元素。因此我们迫切需要一种机制以确保构造函数分配的内存一定会被销毁,这种机制就叫作析构函数:
class Vector {
public:
Vector(int s):elem{new double[s],sz{s}}{ //构造函数:获取资源
for(int i = 0; i != s; ++i) //初始化元素
elem[i] = 0;
}
~Vector(){delete[] elem;} //析构函数:释放资源
double& operator[](int i);
int size() const;
private:
double* elem; //elem是指向有sz个double类型的元素的数组
int sz;
};
2
3
4
5
6
7
8
9
10
11
12
13
析构函数的命名规则是在一个求补操作符~后面跟上类的名字,从含义上来说,它是构造函数的补充。
Vector的构造函数使用new操作符从自由存储(也称为堆或动态存储)分配一些内存空间,析构函数则使用delete[]操作符释放该空间以达到清理资源的目的。单独的delete释放一个独立对象,delete[]则释放一个数组。
在构造函数中获取资源,然后在构造函数中释放它们,这种技术称为资源获取即初始化,又叫RAII。这种技术使得我们可避免“裸new操作”和“裸delete操作”,避免资源泄露。
# 5.3.1 容器的初始化
一种笨方法:先用若干元素创建一个Vector,然后再依次为这些元素赋值。显然这不够优雅。
下面这两种更为简洁:
- 初始值列表构造函数:使用元素列表进行初始化。
- push_back():在序列的末尾添加一个新元素。
class Vector{
public:
Vector(); //默认初始化为空,意味着没有元素
Vector(std::initializer_list<double>); //使用double类型的值列表进行初始化
//...
void push_back(double); //在末尾添加一个元素,容器的长度加1
//...
};
//其中push_back()可用于添加任意数量的元素。
Vector read(istream& is)
{
Vector v;
for(double d; is >> d; ) //将浮点值读入d
v.push_back(d); //把d加到v中
return v;
}
//这里循环负责执行输入操作,终止条件是到达文件末尾或者遇到格式错误。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5.4 抽象类型
Vector之所以被称为具体类型,是因为它们的实现属于定义的一部分。
而,抽象类型把使用者与类的实现细节完全隔离开来。为此,我们将接口与实现解耦,并且放弃来的纯局部变量。因为我们对抽象类型的的实现一无所知,所以必须从自由存储为对象分配空间,然后通过引用或指针访问对象。
首先,为Container类设计接口,Container类可以看成比Vector更抽象的一个版本:
class Container{
public:
virtual double& operator[](int) = 0; //纯虚函数
virtual int size() const = 0; //const成员函数
virtual ~Container(){} //析构函数
};
2
3
4
5
6
上面这个类是一个纯粹的接口。==关键字virtual的意思是“可能在随后的派生类中被重新定义”。==我们把这种关键字virtual声明的函数称为虚函数。===0说明该函数是纯虚函数,意味着“Container的派生类必须定义这个函数”。==
因此:我们不能单纯定义一个Container的对象。
Container c; //错误,不可定义抽象类的对象
Container* p = new Vector_container(10);//可行,Container是Vector_container的接口
2
Container只是作为接口出现,它的派生类负责具体实现operator和size()函数。含有纯虚函数的类被称为抽象类。
Container的用法如下:
void use(Container& c)
{
const int sz = c.size();
for(int i = 0; i != sz; ++i)
cout << c[i] << '\n';
}
2
3
4
5
6
注意:use()是如何在完全忽视实现细节的情况下使用Container接口的。它使用了size()和[],却根本不知道是哪个类型做到的。
我们把一个常用来为其他类型提供接口的类型为多态类型。
作为一个抽象类,Container中没有构造函数,毕竟它不需要初始化数据。
为了获得一个有用的容器,必须实现抽象类Container接口所需的函数,为此,可以使用具体类Vector:
class Vector_container : public Container {
public:
Vector_container(int s) : v(s) {}
~Vector_container(){}
double& operator[](int i) override {return v[i];}
int size() const override {return v.size();}
private:
Vector v;
};
2
3
4
5
6
7
8
9
- 这里的:public可读作“派生自”或“是......的子类型”。
- 派生类从它的基类继承成员,所以我们通常把基类和派生类的这种关联关系叫做继承。
- 此处显示声明了override以描述程序员的意图。使用override指令是可选的。显示指定override在大型的类层次结构中特别有用,否则我们很难知道谁试图覆盖谁。
# 5.5 虚函数
编译器将虚函数的名字转换成函数指针表中对应的索引值,这张表就是所谓的虚函数表,或简称为vtbl。
每个含有虚函数的类都有它直接的vtbl,用于辨识虚函数。
# 5.6 类层次结构
Container是一个非常简单的类层次结构的例子,所谓类层次结构是指通过派生类(如public)创建的一组在框架中有序排列的类。
比如:消防车是卡车的一种,卡车是车辆的一种;笑脸是一种圆,圆是一种形状。
# 5.6.1 类层次结构的益处
主要体现在以下两个方面:
- 接口继承:派生类的对象可以被用在任何需要基类对象的地方。也就是说,基类看起来像是派生类的接口。
- 实现继承:基类负责提供可以简化派生类实现的函数或数据。
具体类,尤其是表现形式不复杂的类,具有非常类似于内置类型的行为:我们将其定义为局部变量,通过它们的名字进行访问或随意拷贝。
类层次结构中的类则与之有所区别:我们倾向于通过new在自由存储中为其分配空间,然后=通过指针或引用访问它们。
# 5.6.2 避免资源泄露
当我们获取了资源并且没有释放它们的时候,通常用泄露这个词来描述。因为资源泄露导致系统无法访问相关资源,所以必须尽量避免。否则,资源泄露导致的资源耗尽,最终将导致系统卡顿或者崩溃。
函数返回一个指向自由存储中的对象的指针是非常危险的:不应当用“旧式裸指针”来表达所有权。
void user(int x)
{
Shape* p = new Circle(Point{0,0},10);
//...
if(x < 0) throw Bad_x{};//潜在的泄露
if(x == 0) return; //潜在的泄露
//...
delete p;
}
2
3
4
5
6
7
8
9
除非x是正数,否则这都会造成泄露。将new的返回值赋值给“裸指针”是自找麻烦。
上述问题的简单解决办法就是使用标准库unique_ptr替代"裸指针"
class Smiley : public Circle {
//...
private:
vector<unique_ptr<Shape>> eyes;//通常是两只眼睛
unique_ptr<Shape> mouth;
};
2
3
4
5
6
# 5.7 建议
- 具体类是最简单的类,在适用的情况下,与复杂类或者普通数据结构相比,请优先选择使用具体类;
- 如果成员函数不会改变对象的状态,则把它声明成const;
- 如果类的构造函数获取了资源,那么类需要使用析构函数释放这些资源;
- 避免“裸new”和“裸delete”操作;
- 抽象类通常不需要构造函数;
- 含有虚函数的类应该同时包含一个虚析构函数;
- 在规模较大的类层次结构中使用override显式地指明函数覆盖;
- 为了防止忘记用delete销毁 用new创建地对象,建议使用unique_ptr或者shared_ptr;