警告⚠️本文章逻辑略混乱,请谨慎阅读。
如上一篇文章所言, 接下来我将以我的双向链表为案例, 分享一些在C++进行类封装及面向对象编程时积累的经验. 本文主要涉及概论、构造函数和析构函数.
概论
对象可以理解为现实世界的各个实体. 现实中的实体, 往往具有一些属性(比如物理属性、几何属性等等属性), 也往往具有一些操作或行为(比如猫会叫).
在面向对象编程时, 所有操作都是针对对象进行的. 可以和面向过程编程做出一些比较. 面向对象编程的主体是对象, 即实体; 面向过程编程的主体是过程, 即操作本身.
以猫猫叫这件事情为例. 面向对象关注的是对象, 即猫. 在面向对象编程中, 猫叫被形容为
猫->叫
, Cat.meow()
,
表示是猫这个对象做出了某种行为, 重点在猫.
而在面向过程编程中, 猫叫被形容为
叫:猫
, meow(Cat)
, 表示叫这个行为被执行了,
重点在叫这个动作.
可以看出, 面向对象的描述方式更符合人类的直观.
实体之间有相互关系. 最重要的关系是, 实体可以分类. 一类实体往往具有相似甚至相同的属性, 并且具备一些相同的行为. 比如所有的猫可以归为一类猫.
描述一类事物应当具有哪些属性和行为(更准确的说法是方法)的过程称作类的定义. 根据类的定义, 创建一个具体的对象的过程叫类的实例化. 类是抽象概念, 对象是具体概念. 这很符合人们的常识.
类之间也有相互关系, 比如包含(更准确的说法是继承), 引用等等.
类的属性和方法有权限之分. 有些属性和方法可以外人访问和调用, 而有些属性和方法只能内部访问和调用.
定义一个类有多种方式. 我们当然可以用自然语言定义类:
1 | 手机 |
类的定义在C++语言中可以描述为这样:
1 | class Phone{ |
模版类.
注意:从下文开始, 笔者假定读者有基本的C++编程基础. 并且明白双向链表的工作原理.
模版是C++的一个语法特性. 利用模版, 我们可以创建一个含有未定数据类型的成员的类. 从代码上讲, 一个双向链表的节点可以这样定义.
1 | template <typename T> |
实例化Node类时,就需要明确模版T对应的具体类型,如
1 | Node<int> n;//创建对象 |
由于双向链表内部需要创建Node,因此双向链表的list类也需要笼罩在一个模版下.故list的定义需要:
1 | template <typename T> |
类似的,实例化list需要明确模版T对应的类型.如:
1 | list<int> li; |
Remark: 类也是类型.因此这样:
1 | list<list<int>> li; |
是合法的.你不过是创建了一个类型T为list<int>
的列表.
不过需要注意, 应当确保传入的类型具有你对类型T进行的运算. 比如你在对类型T进行了加减法, 你就需要确保你传入的类型具有加减法.
构造函数和析构函数.
在对象创建时, 将执行构造函数; 在对象销毁时, 将执行析构函数.
对于普通的情况, 程序将生成默认的构造函数和析构函数. 不过有时候, 我们需要在创建和删除类的时候进行复杂的操作, 这时就不能依靠默认的构造和析构函数了. 我们要自己写.
构造函数是一个名称和类名称相同,不填写返回值的函数.析构函数是一个波浪线~
+类名称,不填写返回值的函数.
它们的定义方式如下:
1 | template <typename T> |
构造函数的实现如下:
1 | //填写构造函数的具体内容 |
这里, list<T>::
代表这是list这个类的函数(方法).
描述类方法的方式有两种: 在类内部定义,在类外部定义. 个人喜欢外部定义.
在外部定义时,需要list<T>::
以明确这是list类的方法.同时需要加上template <typename T>
在构造函数中, 我们创建了head和tail两个节点, 并让head和tail首尾相接. 这之后,我们让存储链表长度的变量_size设置为0. 这样, 在实例化一个list时,这些指令会被执行,从而完成链表的初始化.
一般我们不需要重写析构函数.系统会自动释放类里面的所有内存.但是这是链表,还有许多节点零散地分配在内存空间中,你不能保证系统知道它们是相连地,然后帮你释放它们.所以我们要自己实现析构函数.
析构函数的实现如下:
1 | template <typename T> |
我们先删除链表里的所有元素,最后删除头和尾. 由于在使用中, 我们偶尔也想清空链表,因此可以把清空链表的部分拿出来,单独作为一个函数(方法).
1 | template <typename T> |