C++面向对象编程经验1——概论、模版类、构造函数、析构函数.
justaLoli

警告⚠️本文章逻辑略混乱,请谨慎阅读。

上一篇文章所言, 接下来我将以我的双向链表为案例, 分享一些在C++进行类封装及面向对象编程时积累的经验. 本文主要涉及概论、构造函数和析构函数.

概论

对象可以理解为现实世界的各个实体. 现实中的实体, 往往具有一些属性(比如物理属性、几何属性等等属性), 也往往具有一些操作或行为(比如猫会叫).

在面向对象编程时, 所有操作都是针对对象进行的. 可以和面向过程编程做出一些比较. 面向对象编程的主体是对象, 即实体; 面向过程编程的主体是过程, 即操作本身.

以猫猫叫这件事情为例. 面向对象关注的是对象, 即猫. 在面向对象编程中, 猫叫被形容为

猫->叫, Cat.meow(), 表示是猫这个对象做出了某种行为, 重点在猫.

而在面向过程编程中, 猫叫被形容为

叫:猫, meow(Cat), 表示叫这个行为被执行了, 重点在叫这个动作.

可以看出, 面向对象的描述方式更符合人类的直观.

实体之间有相互关系. 最重要的关系是, 实体可以分类. 一实体往往具有相似甚至相同的属性, 并且具备一些相同的行为. 比如所有的猫可以归为一猫.

描述一类事物应当具有哪些属性和行为(更准确的说法是方法)的过程称作类的定义. 根据类的定义, 创建一个具体的对象的过程叫类的实例化. 类是抽象概念, 对象是具体概念. 这很符合人们的常识.

类之间也有相互关系, 比如包含(更准确的说法是继承), 引用等等.

类的属性和方法有权限之分. 有些属性和方法可以外人访问和调用, 而有些属性和方法只能内部访问和调用.

定义一个类有多种方式. 我们当然可以用自然语言定义类:

1
2
3
4
5
6
手机
具有的属性:
长、宽、高
型号、品牌、……
具有的操作:
开机、拨号、上网、打开程序……

类的定义在C++语言中可以描述为这样:

1
2
3
4
5
6
7
8
9
class Phone{
public:
int len;
int width;
int height;
//...etc
void open();
//...etc
}

模版类.

注意:从下文开始, 笔者假定读者有基本的C++编程基础. 并且明白双向链表的工作原理.

模版是C++的一个语法特性. 利用模版, 我们可以创建一个含有未定数据类型的成员的类. 从代码上讲, 一个双向链表的节点可以这样定义.

1
2
3
4
5
6
7
8
9
10
template <typename T>
class Node{
public:
T data
Node<T>* next;
Node<T>* prev;
}
//到这里为止,是template <typename T>
//的作用范围.在这个作用范围内,T就指代了一个数据类型.
//这个数据类型在实例化Node类时才被明确.

实例化Node类时,就需要明确模版T对应的具体类型,如

1
Node<int> n;//创建对象

由于双向链表内部需要创建Node,因此双向链表的list类也需要笼罩在一个模版下.故list的定义需要:

1
2
3
4
5
6
7
8
9
10
template <typename T>
class Node{
...
}
template <typename T>
class list{
private:
Node<T>* head;
Node<T>* tail;
}

类似的,实例化list需要明确模版T对应的类型.如:

1
list<int> li;

Remark: 类也是类型.因此这样:

1
list<list<int>> li;

是合法的.你不过是创建了一个类型T为list<int>的列表.

不过需要注意, 应当确保传入的类型具有你对类型T进行的运算. 比如你在对类型T进行了加减法, 你就需要确保你传入的类型具有加减法.

构造函数和析构函数.

在对象创建时, 将执行构造函数; 在对象销毁时, 将执行析构函数.

对于普通的情况, 程序将生成默认的构造函数和析构函数. 不过有时候, 我们需要在创建和删除类的时候进行复杂的操作, 这时就不能依靠默认的构造和析构函数了. 我们要自己写.

构造函数是一个名称和类名称相同,不填写返回值的函数.析构函数是一个波浪线~+类名称,不填写返回值的函数. 它们的定义方式如下:

1
2
3
4
5
6
7
8
9
10
template <typename T>
class list{
private:
Node<T>* head;
Node<T>* tail;
int _size;
public:
list();//定义构造函数.
~list();//定义析构函数.
}

构造函数的实现如下:

1
2
3
4
5
6
7
8
9
10
//填写构造函数的具体内容
template <typename T>
list<T>::list(){
head = new node<T>;
tail = new node<T>;

head->next = tail;
tail->prev = head;
_size = 0;
}

这里, list<T>::代表这是list这个类的函数(方法). 描述类方法的方式有两种: 在类内部定义,在类外部定义. 个人喜欢外部定义. 在外部定义时,需要list<T>::以明确这是list类的方法.同时需要加上template <typename T>

在构造函数中, 我们创建了head和tail两个节点, 并让head和tail首尾相接. 这之后,我们让存储链表长度的变量_size设置为0. 这样, 在实例化一个list时,这些指令会被执行,从而完成链表的初始化.

一般我们不需要重写析构函数.系统会自动释放类里面的所有内存.但是这是链表,还有许多节点零散地分配在内存空间中,你不能保证系统知道它们是相连地,然后帮你释放它们.所以我们要自己实现析构函数.

析构函数的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T>
list<T>::~list(){
node<T> *t = head->next;
while(t!=tail){
t = t->next;
delete t->prev;
}
head->next = tail;
tail->prev = head;
_size=0;
delete head;
delete tail;
head = nullptr;tail = nullptr;
}

我们先删除链表里的所有元素,最后删除头和尾. 由于在使用中, 我们偶尔也想清空链表,因此可以把清空链表的部分拿出来,单独作为一个函数(方法).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template <typename T>
class list{
private:
...
public:
list();//定义构造函数.
~list();//定义析构函数.
void clear();//定义方法
}
template <typename T>
list<T>::list(){...}
template <typename T>
void list<T>::clear(){
node<T> *t = head->next;
while(t!=tail){
t = t->next;
delete t->prev;
}
head->next = tail;
tail->prev = head;
_size=0;
}
template <typename T>
list<T>::~list(){
clear();
delete head;
delete tail;
head = nullptr;tail = nullptr;temp = nullptr;
}