大家好
欢迎回来继续学习
C++语言程序设计
这一节我们来学习
共享数据的保护
如果我们既需要共享数据
又需要保证被共享的这些数据
不被修改
保证它的安全性
这个时候呢
我们就可以将这些数据
定义为常类型
也就是用const修饰的类型
那么在类的成员函数中呢
有一些函数
它是不会改变对象状态的
那么这样的函数呢
我们又可以把它定义成常函数
也就是
可以用const来修饰这个函数
首先我们来看看
我们可以在程序中
定义哪些常类型
我们可以定义常对象 也就是说在定义对象的时候呢
前面加这样一个const来修饰
常对象就像我们定义变量的时候
定义常量的时候一样
它必须在定义的时候
进行初始化
我们定义基本类型的常量的时候
不也是吗
必须在定义的时候初始化
定义常对象的时候也必须初始化
而且一旦初始化以后
就不能再被更新了
在类中
我们还可以把类的成员
定义成const成员 常成员
这个常成员又包括常数据成员
和常函数成员
另外呢
我们很多时候会传递引用
去作为函数的参数 对吧
我们也知道引用是可以达到
数据双向传递的
那这个时候
如果我们既要用引用
又不想被引用的对象被更新
不想达到双向传递
那么怎么办呢
可以将参数定义为常引用
也就是只读的引用
另外在第六章
我们还会学习常数组
以及常指针
接下来我们首先来看看
如何定义一个常对象
以及常对象它有什么样的特点
那常对象呢
就是用const修饰的对象
比如说我们这样有一个a类
定义好了
接下来
我们要去定义a类的对象的时候
我们在普通的对象
定义的这种语法形式里面
加一个const
就说这个对象是常对象了
常对象它的特点是什么
就是不能再被更新了呀
不能被改变了呀
那我们再想一个问题
编译器做语法检查的时候
怎么能够保证你这个常对象
是不被改变的呢
好 我们把这个问题留着
后面会给大家介绍
专门用来处理常对象的函数
叫常函数
那么对象的成员
实际上也可以是const成员
在定义这些成员的时候
都可以加const修饰
把它定义成常成员
那常成员又分成常数据成员
和常成员函数
常成员函数就是我们刚才说了
专门用来处理常对象的
它是用const来修饰的
这样的函数
相当于它做出了承诺
它承诺说我绝不改变对象的状态
所以在这样的函数原型的最后
要加const
那么const也是重载函数的一个
区分的因素
那么常数据成员
就是用const修饰的数据成员
下面我们来看一个例题
体会一下常成员函数
它是有什么特点的
是用来干什么用的
在这个例题中呢
我们使用了常成员函数
大家看这个R类里面呢
定义了两个print函数
其中一个呢后面有const
是个常函数
const也是区分
不同重载形式的因素之一
在此之前我们只学过
要区分不同的重载函数
就要用不同的参数表
那么看这两个print函数
它的参数表都是一样的
都是空的
但是其中一个后缀了const
也是合法的重载形式
所以const也是区分
重载函数的一个因素
现在我们来看这个
print函数的实现
我们看到这两个print函数
它的函数体实现
基本上是一样的
但是
这个有const后缀的这个函数
它就特别地承诺了
这个函数它绝不改变对象的状态
那有同学可能觉得
我这第一个print函数
也并没有改变对象的状态
它只是输出R1 R2
对于状态没有什么改变
那么这个呢
必须要显式地标出来是const
编译器在编译的时候
才会去认真审查你这个函数体
看还有没有改变状态的语句
如果在其中有改变
对象状态的语句
它就会报语法错
但是如果你不后缀const
编译器在编译的时候
就不会去审查这一点
所以就不能保证这个函数
它不改变对象状态
我们有了这样的常函数
目的是什么呢
目的是为了处理常对象
我们知道一个对象
如果它定义的时候
定义为const
就说明这个对象
它是一个常量
一旦在定义的时候被初始化了
它就不能再被修改了
而修改对象的状态
有多少种办法呢 很多啊
你调用它每一个函数
都有可能修改对象的状态
都有这个危险性
不像基本类型的变量
你要修改它的状态
一个是给它赋值
一个是从键盘输入数据装进去
大概也就是这样的途径吧
但是对象呢
它有那么多的成员函数
我们这里头举例举的比较简单
其实可以罗列很多成员函数的
它对每个成员函数的运行
都有可能改变它的状态
那么编译器怎么控制这一点呢
就用常函数
也就是说
像这样定义的一个常对象
你只能用常函数去处理它
不是常函数
不能通过常对象调用
我们看这儿通过b
调用同一个函数b.print
那么调用的是谁呢
调用的就是
有const修饰的这个print
而a.print呢
调用的就是
没有const修饰的这个print
那么现在呢
我们考虑一个问题
如果这个print函数
反正它是不需要改变对象状态的
那么我也就没有必要
写两个版本了 对吧
我只要有一个常函数print就行了
不带const的这个print
我不要了行不行呢
希望大家课后去试一试
应该是可以的
如果你这个类里面
只有一个常函数
没有这个第一个print函数
那么a.print调用谁呢
它也去调用常函数
一个普通对象
调用常函数是没有任何问题的
所以通过这个例题呢
实际上我们得到了一种经验
在一个类中
如果你定义一个函数的时候
发现这个函数
就类似于print这样的函数
它的本意就是不打算改变
对象的状态
反正它是不会改变的
那么你就加一个const
加一个const
表示一种保证 一种承诺
承诺我不改变对象的状态
我可以用来处理常对象
所以如果能将一个类里面
不打算改变对象状态的函数
主动都说明为常函数
那么以后定义这个类
常对象的时候
使用起来就会比较方便
定义类数据成员的时候呢
我们也可以用const来修饰
那么接下来呢
我们再来看一个
类里面有const数据成员
也就是常数据成员的例子
我们看在这个例子中呢
我们定义了两个常数据成员
看看这个数据成员变量a
前面有const
这个a是属于每个对象的成员
再看这个数据成员b
它是一个静态成员
是属于整个类的
也定义为常成员
所以对象的成员 类的成员
都可以定义为常成员
一旦一个成员定义为常成员了
那么就只能在初始化的时候
给它初始值
不可能
也不允许在别的地方
再给它赋值了
我们看怎么来做初始化的我们来看怎么做初始化的
首先看这个静态成员b
它在类体中声明
在类体外定义和初始化
所以它就需要在这儿进行初始化
初始化完了以后
就再也不许改变了
因为它是常量
再看这个对象的成员a
它是在构造每一个对象的时候
完成初始化的
比如我们来看这儿
构造对象a1的时候
给一个100作为初始值
然后这个时候会调用构造函数
我们看
在这个构造函数的初始化列表中
就用参数i去初始化了这个
常数据成员a
那么如果是其他的成员
如果不是常成员的话
我们知道
既可以在初始化列表中
完成初始化
也可以在函数体中进行赋值
至于有的成员呢
它不是一个简单的赋值
就可以初始化的
还要经过一些计算
才能得到初始值
那么在函数提中
可能它要发生一些计算
最后将计算结果
复给这个成员变量
但是这个常成员
它是不可以放在
构造函数的函数体中赋值的
它必须在初始化列表中
进行初始化
以后就不许再赋值了
像a2也是
a2用0作为参数去构造它
那么0传过来
就传到i里面
也是在初始化列表里面
它初始化的成员变量a里面了
接下来我们输出对象a1 a2的
它的数据成员a b
我们看到a1的数据成员a是100
b1的成员a是b.a 是0
它们的b成员都是10
因为这个b是一个静态成员
为所有的对象所共享的
那么什么是常引用呢
常引用也是用const修饰的引用
这样的引用相当于是个只读的
只能用它
去读取它指向对象的内容
但是不能去修改它指向的对象
下面我们来看一个
用常引用做参数的例子
用引用做参数
一方面
它可以实现双向的数据传递
另一方面
它传递参数的效率比较高
那么如果我们仅仅需要
这个高效率的特点
而不希望进行双向传递
这个时候我们就可以这个引用
加一个限制
让它是常引用
它也就是只读的引用了
下面我们在这个例题中
给大家演示
大家还记得在前面
友元函数的那个例子中
遗留的那个问题吗
当时我们用引用做函数的参数
这样能够提高参数传递的效率
但是也存在着一个隐患
就是说引用
它是可以双向传递数据的
用引用做参数呢
如果在函数中
对参数做了任何修改
就会影响到实参
现在我们就来解决这个问题
大家看仍然是以这个点类为例
仍然是在点类中
声明了一个友元函数dist
友元函数的函数体
跟我们上一个例子是一样的
计算两个点的距离
只不过呢
我们来看参数表
有了少许变化
在这两个参数前面都加了const
说明这两个参数都是常引用
常引用是什么意思呢
就是只读的引用
也就是说通过这个引用
只能读取
它所指向的对象里面的值
不能通过这个引用
去修改它指向的对象
具体到这个例子中呢
就是说通过引用p1
可以去取得p1.x的值 p1.y的值
但是你不能修改p1.x
不能修改p1.y
那p2也是一样
可以读取p2.x的值拿来用
读取p2.y的值拿来用
拿来参与运算
但是你都不能去修改它
那大家回去可以改一改这个题目
你看看编译器会不会给你报错
你就试图在这个dist函数里面
给p1.x重新赋值
或者给p2.y重新赋值
看看编译器会不会给你报错
肯定是会报错的
大家可以验证一下
这样你的印象就比较深刻了
好 那计算还是一样的
这样我们就可以比较放心地
用这个友元函数了
既提高了访问效率
让它能够直接访问私有成员
又不会因此对私有成员的安全性
有任何破坏