从构建一个Date类入门C++类与对象
类的定义
1 | class Date |
抽象数据类型(类)
通过如上代码,我们就在源代码中通过class
声明了一个抽象数据类型Date
,简称类
,那么封装一个类有什么好处呢?
好处是类把相关的操作分为两类:
- 类的设计者:负责考虑类的具体实现,提供类的接口,成员变量等
- 类的使用者:只关心类提供了哪些功能,而不关心具体实现,从而简化思路
以上面的Date
类为例
对设计者
- 要考虑实现
Date
,就需要声明成员变量_year
_month
_day
,以及声明及实现成员函数Init
和Print
对使用者
- 只需知道可以调用
Date
的成员函数Init
和Print
,以及知道它们的用处即可
实例化 – 将类真正投入使用
类也可以用于声明变量,例如Date d
就声明了一个变量d
,但由于是由类
声明的,我们将这一过程称为实例化
,其中Date
这样的抽象数据类型称为类
,像d
这样的变量称为对象
实例化后的对象拥有私有的成员变量
和整个类公有的成员函数
,接下来对对象
的操作都是对成员变量
和成员函数
的操作
访问成员函数/变量
在类的内部
对于类的成员函数,除了显式声明的函数参数
外,还有隐式传入的this
指针,这是个默认非const
修饰的,指向调用该成员函数的对象的指针,编译器可以通过这个指针访问该对象的成员变量
和成员函数
。
而我们作为类的设计者,既然语法都隐式地传入this
指针了,自然也可以隐式地调用成员
,即直接写变量名/函数名调用
当然,手动显式调用this指针也是可以的
以
Date
为例
1 | //该函数声明在Date类中,成员变量见文章开头 |
但由于const
修饰的对象
传出的是const
修饰的this
指针,普通的this
形参无法接收。
那么如何让成员函数
传入const
修饰的this
指针,来使const
修饰的对象
有成员函数可调用呢?
语法规定,在函数的参数列表(圆括号后面)紧跟一个const
可使函数传入const
修饰的this
指针
这种函数称为常量成员函数
举个例子
1 | //示例代码 |
在类的外部
和C语言的结构体一样,访问对象内的成员有两种方式
- 对象名 +
.
+ 成员名 : 用.
操作符访问对应成员 - 对象的指针 +
->
+ 成员名 : 用->
操作符访问指针指向对象的对应成员
以
Date
实例化一个d
为例
1 | // class Date |
1 | //test1输出 |
代码如上,test1
运行的很好,但test2
报错了,原因在于test2
作为非成员函数访问了访问限定符private
控制的成员_year
,权限冲突,就会报错。
由此,C++类和对象还有一个重要概念需要强调–访问控制
访问权限控制与封装
使用类和对象编程的一大优点就是类可以封装
代码,让使用者只能使用公有的接口和成员变量,而对内部的具体实现不可见,来提高类的易用性
和安全性
所以C++语法提供了三种访问说明符
(access specifiers)
- public: 该说明符之后的成员在整个程序内可被访问
- private: 之后的成员仅可被该类的的类域里(如成员函数)访问
- protected: 一般同
private
,主要特点体现在类的继承,这里不作讨论
作用范围
某一访问说明符
的作用范围开始于它的冒号
,终止于下一个访问说明符
或类的结尾
,而类的开始
到第一个访问说明符
前的访问权限取决于声明类的关键字
,分类如下
class
默认为private
权限struct
默认为public
权限
图例如下
实际上class
和struct
除了默认权限不一样,基本没有差别。
所以为了防止误读,提高可读性,不建议在默认区
写代码,而是保证每段语句前都有合适的访问限定符
封装
作为类的设计者,一个类按访问权限
可以分为两个区
public
: 将提供给使用者
的接口(函数)
和成员变量
声明在此,用于外部调用接口和修改非私有的成员函数private
: 用于存放受保护的成员变量
和成员函数
,防止外部使用者意外或恶意调用或修改,造成类的内部结构被破坏等安全问题,所以特别重要的成员变量,和不希望被外部调用的函数声明在这里
例如在声明
Date
类时,我们将Init
和public
控制;_year
等成员变量不希望被外部随意修改,就用private
控制
进阶
学完以上内容,不过是会写个高级点的结构体
而已,要写一个完整的类,还需要学习更多的语法知识
构造函数
像本篇的Date
类那样显式地调用Init
函数来初始化是非常挫的,既然语言本身的内置类型
可以在声明的时候初始化,那么类的设计者设计出来的类也应当提供初始化的接口
,而支持这一功能的接口便是构造函数
按语法规定,构造函数
的函数名必须是类名
,没有返回值,const
修饰的成员变量必须位于初始化列表
,其它则可省略。关于初始化列表,稍后详细解释
以
Date
类为例
1 | class Date |
以上的构造函数基本能用了,但还有两个问题
- 构造函数没有初始化成员变量,而是采用赋值操作,无法初始化
const
修饰的成员变量 - 使用
Date d
是会报错的,因为没有提供默认构造函数
对于第一个问题,就要引入初始化列表
这一概念,让初始化函数直接拥有初始化成员变量
的功能
初始化列表位于构造函数的参数列表之后,花括号之前,以:
开头,用,
分隔成员变量
以
Date
为例
1 | Date(int year,int month,int day):_year(year) , _month(month) , _day(day) {} |
通过这样初始化列表
,便能在声明对象时,直接初始化成员变量
对于问题二,我们开启另一个个小专题
构造函数的重载和缺省参数
没错,构造函数和函数一样,也是能重载
和给参数传缺省值
的
也就是说我们能写好几个构造函数
下面特别说明几个特殊的构造函数
默认构造函数
原则上对于每一个类,都应该提供有且仅有一个默认构造函数(*多个默认构造函数
会报错!*)
而要声明默认构造函数,只需声明无参数
构造函数,或者全缺省参数
构造函数即可
以
Date
为例
1 | Date():_year(2024),_month(4),_day(1){} |
以上就是两种默认构造函数的声明形式
拷贝构造函数
有时候我们会希望用现有的的对象去初始化一个新对象,此时对应的构造函数就称为拷贝构造(函数)
拷贝构造
的声明方式为构造函数
+参数类型为类本身的引用传参
,不加&
的话就会死递归报错,有无const
皆可,但由于是实现拷贝功能
,一般是加const
的
以
Date
为例
1 | Date(const Date& d):_year(d._year),_month(d._month),_day(d._day){} |
使用模板的类的函数缺省值
有时我们在使用类模板来设计类时,需要给模版类
类型的形参提供一个缺省值,有些人可能会写个0
,但是其实是错的,正确的做法是传一个临时变量
但此时要求模板参数中的类
有可用的默认构造函数
和拷贝构造
用于调用
以链表节点
Node
为例
1 | template<class value_type> |
析构函数
对于声明在栈区
或静态区
的成员函数,程序完全可以自动销毁,
但如果成员变量
有指向在堆区
声明的某段内存块
,在该如果只是仍由程序自动
销毁这个指针,那么那段内存块
就会一直处于未释放的状态,也就是造成内存泄漏,
也就是说此时编译器自动生成的析构函数
已经不能满足需求,编译器并不知道如何处理声明在堆区
上的数据,
这部分操作应由类的设计者来规划
所以我们应当显式地声明一个合理的析构函数
析构函数
的函数名也是由语法规定的,为~
+类名
,并且不能声明形参
以一个
指针类
为例
1 | class Ptr |
类的重载操作符
C++语法提供了重载操作符的函数,而由于this
指针的存在,在类的内部声明
重载操作符函数会稍有不同
-对于一元操作符,[]
,->
之类的重载,不再需要显式传参
-对于二元操作符,+
,>
之类只需要传右操作数
以
Date
类为例
1 | //在类的内部,重构一个 == |
小结
至此,C++类和对象已基本入门,再进阶的迭代器
,继承
,虚继承
等将单独出博客。