C++Prime Plus(3)

51.抽象和类

类型的构成
1.数据占用的空间量;
2.如何解释内存中的比特串;
3.该类型的实例化可以执行的操作;

“类”是用户自定义的类型。

定义类需要定义两个方面:成员声明(或类声明)和方法定义
成员声明(或类声明):描述数据存储(数据成员),数据操作(成员函数),表现为一个接口文件(头文件);
类方法定义:成员函数的定义;

类声明:
fig1

private和public的出现次序可以是任意的,也可以多次出现,class也可以改用struct。

类与结构的区别:没有指定访问控制时,struct默认是public,类默认是private。

访问控制
public:公有
使用到类对象的程序可以直接访问;
作为对象和程序之间的接口;

private:私有
只有类自己的成员函数可以访问;
实现数据隐藏;

数据成员通常是私有的,作为接口的成员函数通常是公有的。

示例:
fig2
成员函数的实现
存放在一个与接口文件(头文件)同名的源文件中;

定义成员函数
用作用域解析运算符(::)指出函数所属的类;
类的成员函数可以访问类的所有成员。
注意,直接定义在类中的函数被作为内联函数,不用inline强调。

示例:
fig3

52.类的使用

“类”是用户自定义的类型,”类”类型的变量叫”对象”。
对象的定义:存储持续性声明
类名 对象名,对象数组,对象指针
比如:Stock s1, sArray[10], *sp;

对象访问:和访问结构类型一样,都是访问成员;

成员表示:
对象名.成员 或者 指针->成员

和结构的区别:对象的访问有访问限制。
公有成员可以被所有函数访问;
私有成员:只能被自己的成员函数访问。

示例:
fig4
也可以用指针访问对象:
fig5

53.对象构造

对于结构类型的变量,允许在定义时赋初值,但类不允许在定义对象时像结构类型变量定义那样赋初值(因为类的对象存在访问限制)。
fig6
解决方案:在给对象初始化时就自动赋初值的过程写成一个函数,让编译器在定义对象时自动调用这个函数。这个成员函数称为构造函数

构造函数
函数名与类名相同,无返回值,且不能指明返回类型

示例:
fig7
fig8
构造函数的使用
隐式调用:定义对象时自动调用
fig9
显式调用:
fig10
默认构造函数
问题:加了构造函数后,Stock st; 会出错。

解决方案一:增加一个默认的构造函数

Stock::Stock () {
    
     } //构造函数的重载

注意记得在类声明的头文件中声明Stock();
如果没有定义构造函数,编译器会生成一个函数体为空的构造函数。

解决方案二:为构造函数指定缺省值

Stock::Stock (const string &co=””, long n=0, double pr=0) {
    
    }

C++11的列表初始化
像结构类型变量定义一样,以列表的形式给出初始值,将列表中的参数作为实际参数调用对应的构造函数。如:

Stock st1={
    
    “comp1”, 20, 3.4}; 

54.对象析构

如果将Stock中的string类型的变量company改成char*的company。
fig11
我们在构造函数中,需要为该指针指向的字符串分配空间:
fig12


strcpy(s1,s2);

将s2拷贝到s1,(从s1的首字符开始),注意s1数组的空间要大于等于s2。

strlen(s);

返回字符串s的长度,不包含’\0’。


在main中实例化对象后,函数执行结束,由于动态内存的原因,会出现内存泄漏。

析构函数
对象生命周期结束被销毁前会调用析构函数做善后处理。主要是处理对象中的动态空间。

如果没有这些特殊处理(需要销毁动态内存)的情况,不需要定义析构函数,或定义一个空析构函数。

注意,如果类中没有定义析构函数,编译器也会自动生成一个空的析构函数。

fig13
构造函数在对象定义时调用,析构函数在对象销毁时调用。

55.const与类

定义后值不能被修改的对象;

const对象的定义

const MyClass obj (参数列表); //必须有初值

对于const对象只能调用const成员函数(不修改数据成员的成员函数)。

const成员函数,格式:

返回类型 函数名 (形式参数列表) const {
    
     }

一旦被定义成const成员函数,编译器会检查是否修改数据成员,并报错。

fig14
建议设计类时,不修改数据成员的函数都加上const

56.this指针

每个类的成员函数都是对这个类的数据成员做操作,比如:
fig15
这些成员函数对于对象的数据做操作取决于是哪个对象调用了成员函数。

如何在成员函数中表示完整的当前对象(对象自身)?
比如比较两个股票账户中的价值,返回价值大的账户。

//const成员函数, 参数是const对象的引用, 返回const对象的引用 (引用返回)
const Stock & Stock::topVal(const Stock &a) const
{
    
    
	当前对象.账户<a.账户,返回a;
	当前对象.账户>a.账户,返回当前对象;
}

this指针
每个成员函数都有一个隐含的参数:当前对象的指针。指向调用函数的对象,函数中涉及的成员都是this指针指向的对象的成员。
fig16
所以对于返回当前对象,可以return *this
fig17

57.类作用域

程序中的变量,在复合语句和函数内定义的变量,作用域(块作用域)仅限于复合语句和函数内,离开了这些区域,该变量就”看不见”了,所以不同的函数可以有同名的局部变量。如果变量定义在函数外,通常就成为文件作用域。


注意文件作用域是整个文件,其实就是整个程序,因为C++是将物理上的文件链接成为一个文件。所以,只要从定义之后的范围都可以看见,只是要注意在其他物理文件上声明该变量的时候加extern。


类中定义的名称的作用域是整个类,不同的类可以有相同的成员名称。
类中的成员函数可以直接使用这些名称。在类外使用时必须加上类名的限定(类名::成员名)
fig18
类作用域的常量:一个类中所有对象共享的常量
对于一个类中所有对象共享的常量(多个对象共享一个常量,节省空间),有两种定义方式:
第一种:使用static
fig19
注意,如果是非int类型的常量,不能在类声明中直接赋值。需要在类声明外赋值:
fig20
第二种:使用枚举类型
枚举类型的定义:

enum typeName{
    
     valueName1, valueName2, valueName3, ...... };

enum是一个新的关键字,专门用来定义枚举类型,这也是它在C++中的唯一用途;typeName是枚举类型的名字;valueName1, valueName2, valueName3, …是每个值对应的名字的列表。注意最后的;不能少。
比如,列出一个星期:

enum week{
    
     Mon, Tues, Wed, Thurs, Fri, Sat, Sun };

也可以给每个名字指定值:

enum week{
    
     Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };

fig21

58.运算符重载

使对象也能用运算符操作,例如类A的对象a1,a2,a3,可以执行a3=a1+a2;

运算符重载的限制:
不是所有的运算符都能重载,重载不能改变运算符的优先级和结合性,重载不能改变运算符的操作数个数,不能创建新的运算符。

运算符重载的方法
写一个函数解释某个运算符在某个类中的含义
问题:如何使编译器能自动找到重载的这个函数?
解决方法:
fig22
函数原型
形式参数个数以及类型:与运算符的运算对象相同。
注意:成员函数有一个隐含参数this,所以函数的形式参数个数比运算数少1。

返回值:与运算结果类型一致。
fig23

59.运算符重载的实例

设计一个处理时间的类,能提供时间的加法:
fig24
fig25
为了实现t3=t1+t2的方便操作,我们用运算符重载方法:

fig26
fig27
fig28

60.友元

类的朋友,允许访问私有成员。

友元分类:
友元函数:全局函数
友元类:另一个类
友元成员函数:另一个类的成员函数

友元声明
在类声明中用friend创建友元
fig29
友元声明可以出现在类中的任何地方,通常放在最前面或最后面。

友元函数的应用:主要是用于运算符重载
fig30
fig31
重载输出运算符,使对象可以用”cout<<对象”输出,因为类是我们自己定义的,直接用”cout<<类的对象”时,编译器不知道该怎么输出这个对象,所以我们需要自己写重载。

<<是一个二元运算符,因为第一个运算数cout不是正在定义类的对象,而是ostream类的对象,所以我们最好重载为友元函数。
fig32
版本一的缺点是不能连续输出,因为我们重载<<时没有返回值;
由于左结合,cout<<x<<y时,编译器执行为(cout<<x)<<y,当执行完cout<<x后,没有返回值,面对 void表达式<<y,编译器就不知道该干什么了。

所以,正确的输出运算符重载应该返回os对象:
fig33

61.运算符重载-成员或非成员

运算符重载可以让我们在对象的计算上变得像内置类型一样。在前面的内容中我们发现,运算符重载函数可以被写成 成员函数 或者 友元函数非成员函数)。现在就有问题,什么时候写成 成员函数,什么时候写成 友元函数

当第一个运算数不是当前类的对象时,不能重载成成员函数,比如>>和<<;

当第一个运算数必须是当前类对象时,一般重载成成员函数。

对于某些运算符,C++规定必须重载成成员函数,比如:赋值,下标运算符等。

当重载成成员函数时,函数的形式参数个数比运算数少1,因为第一个运算数是this指针指向的对象;

建议
第一个运算数不是当前类对象,重载成友元函数;
第一个运算数是当前类对象,建议重载为成员函数。
fig34
注意,对于t3=2+t1t3=x+t1,在参数传递时进行了类型的转换,2(int)会被转换为Time类型,编译器会去Time类型的构造函数重载集合里面找到符合这种参数初始化的重载函数;
发现了重载函数Time(int h, int m=0)这个带缺省值的形式,即符合构造函数,所以2作为h,m还是0,并用该函数初始化这个Time对象。

再举一个+=运算符的例子:
fig35

62.类的类型转换

在C++中,不同类型的变量可以放在同一个表达式,比如3+5.7,3会从int类型转换为double类型。
如果表达式中有我们自己定义的类,我们需要自己定义类的类型转换。

类的类型转换有两种:
1.内置类型到类类型的隐式转换;
2.类类型到其他类型的转换;

内置类型到类类型的转换
通过构造函数实现,只要有带一个参数的构造函数,就能实现参数类型到类类型的转换。
比如,有理数类的定义:
fig36
在构造函数Rational(int n=0, int d=1)中,n表示分子,d表示分母。

当执行:
fig37
r1=5,=是赋值运算,C++在执行赋值运算时,会将右边对象默认转换为左边对象的类型,载赋值到左边。5是int类型,在将int类型转为Rational类类型时,编译器会去找Rational的构造函数,5被传给第一个参数n。

另外,在执行r2=4+r1时,4被默认转换为Rational类类型。

类类型到其他类型的转换
通过类型转换函数实现,类型转换函数必须重载为成员函数。

类型转换函数的格式:
fig38
类型转换函数
1.无返回类型(因为返回对象的类型就是目标类型名,所以为了简化写法,就不写返回类型了),
2.无参数(因为参数就是当前对象),
3.由于是类型转换运算符重载,所以前面有operator。

类型转换:
C++的风格为:类型 (表达式)
C的风格为:(类型) 表达式
fig39


类型转换出现的场合:
1.赋值或初始化时,如果右边的表达式计算结果类型与左边的变量类型不同:右边值被转换成左边变量类型;
2.函数参数传递时,实际参数被转换成形式参数的类型;
3.表达式中的运算数类型不一致,需要遵循转换规则:把精度小的运算数向精度大的运算数转换;


63.拷贝构造函数与赋值运算符重载

在定义变量时,可以给变量赋初值,内置类型变量的初值,往往是同类型的常量或者同类型的变量。在定义对象时,也可以赋初值,对象的初值往往是一组内置类型的值。

在定义对象时,我们可以将一个同类的对象B作为对象A的初值。(在对应的数据成员之间赋值),这在之前的Time类实验中已经得到了验证。但是如果存在成员变量来自动态内存,我们还需要额外的方法来赋值,因为函数退出时候,类的析构函数会销毁动态内存,导致对象的赋值是不稳定的

比如现在设计一个string类,提供的操作有:
字符串连接;
字符串比较;
字符串输出;

设计考虑
数据成员:用于动态数组存放字符串
成员函数:
构造函数;
析构;
我们需要的操作应该用运算符重载实现;

fig40
fig41
fig42
如果取消了最后两行的注释,会异常终止,MyString类与Time类相比,数据成员包含了动态内存的变量。执行s1+s2,调用友元函数operator+,返回对象tmp,tmp对象被赋值到同类类型的变量s4,由于tmp是函数内部的,当函数退出后,tmp被析构,即tmp的data(来自动态内存)被归还系统,所以s4是得不到data的。

数据成员包含动态变量时,必须定义拷贝构造函数赋值运算符重载函数

拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。

如果我们没有在程序中定义拷贝构造函数,编译器会自动生成一个拷贝构造函数(直接地在对应成员上赋值引用),但是由于有动态成员,我们不能用默认的拷贝构造函数,应该重新实现一个。
拷贝构造函数要和=运算符联合使用,由于拷贝构造函数被我们重新定义,所以也需要重载=运算符
所以类中新增两个声明:
fig43
拷贝构造函数的参数是同类对象的引用。拷贝构造函数在对象赋初值时调用(意思就是将同类对象的引用作为参数拷贝到新对象)

思考之前的问题,本质原因在于返回tmp的引用给s4,s4与tmp共享同一块data变量,然后tmp要归还data,导致s4的data也被销毁。所以我们应该在tmp析构前为s4.data申请动态空间并赋值:
fig44
由于s4=tmp,是依赖于=运算符的,所以我们同时需要重载赋值运算符:
fig45
此时再运行被注释的两行程序即可成功。

我们对s4=21+s2逐语句Debug分析,首先调用+重载,在return tmp并销毁之前,会调用拷贝构造函数生成一个新的对象(无命名的),然后tmp被析构。然后调用=重载,s4被这个新对象=重载,然后这个新的对象被析构(因为在程序中,这个无名的对象的生命周期已到),最后打印s4对象。

重点:数据成员包含动态变量时,必须定义拷贝构造函数和赋值运算符重载函数


拷贝构造函数和赋值运算符的行为很相似,都是将一个对象的值复制给另一个对象,但是其结果却有些不同,拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。这种区别从两者的名字也能轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。

调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生,如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。


64.静态数据成员

我们现在做一个银行账户系统,假设类SavingAccount专门用于存放存款账户,它包括存户的姓名,地址,存款,利率等成员变量:
fig46
问题:有没有必要让每个对象都保存一个利率?
没有必要,银行对每个用户的利率是一视同仁的,没必要浪费空间。同时不便于修改利率。

为了解决该问题,我们应该让这个成员变量在对象之间共享。
一种方式是设为全局变量,这使得所有函数都能访问它,但是也没必要,为了安全,最好是处理一年期的函数(来自一年期的类)只访问一年期的利率,两年期的函数(来自两年期的类)处理两年期的利率。另外其他函数最好也不要可以访问该变量。

所以我们使用静态数据成员

静态数据成员:属于该类的所有对象共享的变量;
fig47
注意事项
静态成员变量不属于对象的一部分,而是类的一部分;
静态数据成员属于类,因此定义对象时不会为静态成员分配空间;
静态成员的定义一般出现在类的实现文件中,定义格式为

Double SavingAccount::rate=0.05

引用方法:
通过作用域操作符从类直接调用,比如:SavingAccount::rate
从对象引用它,比如SavingAccount类的对象obj,则可以用:obj.rate

65.静态成员函数

静态成员函数专门处理静态数据成员,不能处理其他数据成员

静态成员函数声明:
在类定义中的函数原型前加static,
比如:

static void SetRate (double newRate) {
    
     rate=newRate; }

注意,静态数据成员也可以用非静态成员函数处理。

静态成员函数没有this指针。

静态成员函数的访问

类名 :: 静态成员函数名()
对象名.静态成员函数名()

静态成员函数示例
在程序执行的某个时刻,需要知道某个类已创建的对象个数,以及现在仍存活的对象个数。

类设计:
数据成员:两个静态数据成员:obj_count和obj_living
成员函数:

  • 在创建一个对象时,对这两个数各加1:
  • 当消亡一个对象时,obj_living减1;
  • 定义一个静态成员函数返回这两个值。

fig48
fig49
从上面示例可以看出,非静态成员函数也可以访问静态数据成员。静态数据成员在同一个类类型的不同对象之间共享。

猜你喜欢

转载自blog.csdn.net/qq_40943760/article/details/125030957