类和对象的基本知识

类和对象的概述

类和对象的理解

每个对象都有两个特征,分别是属性(静态特征)和行为(动态特征),即对象拥有自己的属性和行为。
将一组对象抽象为一个类,就是将这一组对象的共同属性和共同行为归纳成一类,类就拥有这一组对象共有属性和行为。在C++中 行为即为函数,属性即为数据。

以汽车类为例子,它具有品牌、生产厂、型号、颜色等静态特征,称为属性( Attribute);此外,它还具有行驶、转弯、鸣笛、刹车等动态特征,称为行为(Behavior)。一辆具体的汽车,品牌、生产厂、型号、颜色等属性都有具体的属性,那么这这辆汽车就是这个汽车类的对象。

类和对象的关系

类在C++中是对象的类型,是抽象的,不占用内存单元,而对象是该类型的一个变量,是具体的,占用内存空间。对象和类的关系相当于一般的程序设计语言中变量和变量数据类型的关系。

面向对象基本特征

抽象特征

抽象对于一组具有相同属性和行为的对象,可以把它们抽象成一种类型。在C++中.这种类型就称为类(Class )。类是对象的抽象,而对象是类的实例,是类的具体表现形式。

举一个例子,一个学校的学生,就是一组具有相同属性和行为的对象,我们可以把这些对象抽象成一个学生类,我们定义学生类具有姓名、班级、学号、成绩等属性(类有哪些属性可以由设计者自己去定义)。此时一个具体的学生对象,就是这个类的实例,而这个学生类就是具体学生对象的抽象。

封装特征 ^b27b51

当我们将某个事物抽象成一个类的时候,那么这个事物的属性和行为就被封装在这个类当中。使用这个类的人不需要知道这个类的内部的实现细节,只需要知道怎么用就可以了。
日常生活中人们操作某个对象时,只需了解其外部的功能,而不必知道对象内部的细节。例如,使用数码相机时,对照相机的光学成像原理、镜头内部结构、电路组成以及压缩算法等可以一无所知,只需要调整取景框按动快门即可照相,这就是应用封装原理的典型例子。对象的一部分属性和功能对外界屏蔽,具体的操作细节在内部实现,对外界是透明的,从外界看不到甚至感觉不到它的存在。这样,把对象的内部实现和外部行为分割开来,人们在外部进行控制,可以大幅降低人们操作对象的复杂程度。

封装(Encapsulation)性是面向对象程序设计方法的一个重要特性。封装包含有两层含义,一是将抽象得到的有关数据和操作代码相结合,形成一个有机的整体,对象之间相对独立,互不干扰;第二,封装将对象封闭保护起来,对象中某些部分对外隐蔽,隐藏内部实现细节,只留下一些接口接收外界的消息,与外界联系,这种方法称为信息隐蔽( Information Hiding)。信息隐蔽有利于数据安全,防止无关的人了解和修改数据。

封装保证了类具有较好的独立性,防止外部程序破坏类的内部数据,使得程序维护修改比较容易。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低限度。

继承特征

在面向对象程序设计中,如果已经建立了一个类A,又需要建立另一个与A基本相同,但增加了一些属性和方法的类B,这时没有必要从头设计一个新类,只需在类A的基础上增加一些新的内容即可。即一个新类可以从现有的类中派生,这个过程称为类继承( Inheritance)。新类继承了原来类的特性,称为原来类的派生类(子类),而原来类称为新类的基类(父类)。
利用继承可以简化程序设计的步骤,程序员能通过只对新类与已有类之间的差异进行编码而很快地建立新类,当然也可以对其进行修改或增加新的方法使其更适合特殊的需要。

继承是一种联结类与类的层次模型。继承机制可以方便地利用一个已有的类建立新类,这样可以重用已有软件中的一部分甚至很大的部分,减少了编程工作量,这就是软件重用(SoftwareReusability)的思想。继承提供了一种明确表述共性的方法,允许和鼓励类的重用,不仅可以利用自己建立的类,还可以使用别人建立的或者存放在类库中的类,从而大幅缩短了软件开发的周期;同时,这些已有的类通常都已经进行了反复的测试,无须再进行调试,可以提高软件的质量。

多态性特征

多态性(Polymorphism)是指允许不同的对象对同一消息做出不同的响应,执行不同的操作。
例如,用同样的加法把两个时间加在一起和把两个整数加在一起的内涵肯定完全不同。

多态性是通过函数重载和虚函数等技术来实现的,第7章介绍的函数重载就是实现多态性的一种手段。利用多态性,可以在基类和派生类中使用同样的函数名,而定义不同的操作,从而实现“一个接口,多种方法”,这是一种在运行时出现的多态性,它通过派生类和虚函数来实现。

类的定义

类和结构体类型的关系

C++的类是由C语言中的结构体类型演变而来的

类的相关概念

类的数据部分称为数据成员(Data Member)或者属性( Atribute ) ,类的函数部分称为成员函数(Member Func-tion) ,有时也称为方法。

类的定义形式


在C++中,使用关键字class来定义这种包含数据成员和成员函数的类。类的定义就相当于声明。
在声明类时,声明为private , protected和声明为public成员的顺序可以是任意的,可以先出现private部分,也可以先出现public部分,甚至可以包含多个private , protected和 public部分,当然也可以只有 private或只有public 部分。如果没有显式地指定,在类中声明的成员都是私有的。声明为private的成员对外界是隐蔽的,在类外不能直接访问,充分体现了类的封装性。但如果一个类的所有成员都声明为私有的,即只有 private部分,那么该类将完全与外界隔绝,这样的类没有实际意义。一般的做法是在声明类时把数据隐藏起来,而把访问数据的成员函数声明为public ,作为对外界的接口。

类的定义举例


其中,类的定义以关键字class开始, Point为类的名字,类定义体放在左右花括号之间,用分号终止。Point类定义包含两个无符号整形数据成员x,y和一个成员函数ShowMe( )。
类的声明中列出了类的全部成员,包括数据成员和对这些数据操作的函数,数据和操作封装在一起。注意,类的定义体必须用一对花括号括起来,类的声明以分号结束。

Person类的定义

类的定义格式

对象的定义

对象的定义形式

<类名><对象1>,<对象2>,… ;
如:Person personl , person2 ;
声明了两个名为personl和 person2的Person类的对象。
对象是类的实例,声明了类之后,就可以用类名定义该类的对象。一般来说,一个对象就是一个具有某种类型的变量。与普通变量一样,对象也必须先经声明才可以使用。

成员函数

成员函数的理解

类的成员函数是函数的一种,它也有函数名,返回值类型声明和参数表,其操作与普通函数没有任何区别,只是它属于一个类的成员,可以被指定为私有的、保护的或公有的。其中,私有的成员函数只能被本类中其他成员函数所调用,不能被类外调用。某些情况下,一些成员函数只用来支持本类中其他函数的操作,称为工具函数(Utility Function) ,此时可以将它们指定为private,类外用户不能使用这些私有的工具函数。需要被外界调用的成员函数必须被指定为public ,它们是类的对外接口。

成员函数是类十分重要和具有标志性特征的部分,如果一个类中不包含成员函数,它与结构
体就没有区别了,只是定义了一种复合数据类型。
反过来说,也可以让类只包含属性,来当做结构体来用。

成员函数的定义

成员函数可以在类的声明中定义。在类声明中定义的话,就跟普通函数的定义方式一样。

如果类的定义中仅给出了成员函数的原型声明,没有定义函数体,这时就要在类声明的外部来定义成员函数。为了能表示与所属类的关系,需要使用二元作用域运算符“::”。下面是函数在类外的定义形式:
<类型><类名>::<函数名>(<参数表>)
{
函数体
}
其中,作用域运算符“::”指出成员函数的类属,即该函数属于哪一个类的成员函数。

举例

函数在类内的定义

函数在类外的定义

访问控制权

类的作用域划分

类的声明内属于类的作用范围,而对于成员函数来说,无论是在类的声明内定义,还是在类的声明外定义,其函数体也是在类的作用域范围内。(在C++的类的作用域内,可以对其私有成员变量进行操作)

访问一个类里面的数据的方式

一般有两种,一个是成员函数的访问,一个是对象的调用。

对象的调用

对象调用分两种一个是在类作用域内,一个是在类作用域外。

在类的作用域外,本类的对象只可以访问其公有数据成员或成员函数,这时需使用运算符“.”,例如:person1 . ShowMe( )。如果访问的是一个对象中的私有成员和保护成员,则属于非法操作,将导致编译错误。

在类作用域内,对象就可以访问私有和保护成员。

成员函数的访问

所谓的成员函数访问,就是在函数体内去调用。
因为函数体是在类的作用域内,所以可以调用类的数据。
成员函数体内可以访问同类中的数据成员,或调用同类中的其他成员函数。这时可以直接使用数据成员名或成员函数名.

对象调用的举例

class A
{
private:
int m_para;
public:
void Func();
}
void A::Func()
{
A temobject; //定义A类的临时对象
temobject.m_para = 1; //对临时对象的私有成员变量进行赋值
}

类的成员的访问控制权限

在C++中,通过成员的访问控制属性来实现封装。类成员的访问控制权限有3种:私有 ( Private)、公有( Public)和保护( Protected )。
公有成员,可以在本类成员函数内被访问 以及被本类对象调用
私有成员和保护,只能被成员数访问以及被同类对象在类作用域内被调用。
类对象无法在作用域外去调用私有数据和保护数据。

内联函数和外联函数(inline关键字)

内联成员函数理解

在类内定义的函数,即该函数的函数体放在类体内,被默认成内联成员函数。

外联成员函数理解

外联成员函数是声明在类体内,定义在类体外的成员函数。(在外联函数的定义开头用inline关键字修饰,该外联函数就变成内联函数。  ),外联成员函数的调用方式,也跟普通函数一样。

举例

类A

主函数

输出结果

   说明:类A中,直接定义了3个内联函数,又使用inline定义了2个内联函数。

注意

。内联函数一定要在调用之前进行定义,并且内联函数无法递归调用。内外联函数区别就在于,运行时侯的效率与定义的方法不同。

对象的存储

对象存储的理解

使用类创建对象时,系统会为每一个对象分配一块存储空间。类中包含有数据成员和成员函数,数据和函数的代码都应该有相应的存储空间。现在思考一下,如果用一个类声明了5个对象,系统是否需要分别为这5个对象分配存储数据和成员函数代码的存储空间。

从前面的分析可以看出,给对象赋值都是给对象的数据成员赋值,不同对象数据成员的值是不同的,而其成员函数的代码是相同的,不论调用哪一个对象的成员函数,实际上调用的都是相同的代码。因此,没有必要为每一个对象都开辟存储成员函数的空间。的州个

事实上,C++的编译系统可使用一段空间来存放这个公共的函数代码段,在调用各个对象的成员函数时,都去调用这个公共的函数代码。因此,每个对象的存储空间都只是该对象的数据成员所占用的存储空间,而不包括成员函数代码所占用的空间,函数代码是存储在对象空间之外的。而且,不论成员函数是在类的内部定义还是在类的外部定义,不论成员函数是否用inline声明,其代码段都不占用对象的存储空间。

类的组合

类组合定义

一个类的对象可以作为另一个类的数据成员,称为类的组合(Composition)。

举例


其中Date为一种类。

注意

如果成员类也是在本程序中定义的,则应将成员类的定义或声明放在另一个类的前面。如下图。

接口与实现分离

接口与实现分离的目的

到目前为止,本书中介绍的例子程序都是包含在一个源文件中的。事实上,在实际编程时,通常将类的声明放在一个头文件中,形成类的public接口,并向客户提供调用类成员函数所需的函数声明。类成员函数的定义放在另一个源文件中,形成类的实现方法(公有的成员函数对数据的操作称为类的实现),就类的用户来说,类实现方法的改变并不影响用户的使用,只要类的接口保持不变即可,而类的功能可能扩展到原接口以外。类的用户使用类时不需要关心类的源代码,但客户需要连接类的目标码。

举例

Person.h文件

Person.cpp文件

主函数

说明
Person.h是接口文件,Person.cpp是对接口的实现。主函数调用person接口。
具体例子和细节在“多文件编译执行(调用其他文件的函数的方法)”

构造函数(对象的初始化方式)

构造函数的基础知识

错误的初始化

类是一种抽象的数据类型,其数据成员不能在声明时初始化。下面的描述是错误的。

对象为什么要初始化

与使用变量一样,对象也应该是先赋值,后使用。对象的初始化体现在对数据成员的赋值。对象是一个实体,在使用一个对象时,它的每一个数据成员都应该有确定的值

构造函数的定义(构造函数初始化方式)

C++专门提供了构造函数(Constructor)来处理对象的初始化。构造函数是类的一个特殊的成员函数,它会在每次生成类对象(实例化)时自动被调用。

构造函数的声明格式

类名(参数表);

即构造函数与类同名,且没有返回值类型。构造函数既可在类外定义,也可作为内联函数在类内定义。构造函数允许重载,提供初始化类对象的不同方法。在生成类对象时,其成员可以用类的构造函数自动初始化。

构造函数的调用

Date类的构造函数

定义对象时直接调用
Date date1 ( 2006,3,28 ) ;
创建对象时date1被初始化为2006年3月28日。

定义对象时,间接调用
Date date1=Date(2006,328);
创建对象时date1被初始化为2006年3月28日。

前面已经说明,构造函数不能由用户直接调用,只能在定义对象时调用,因此实参是在定义对象时给出的。

构造函数生成匿名对象

Date(参数)单独写表示生成了一个匿名对象,当前行结束后,该对象立即析构。注意不能利用拷贝构造函数,初始化一个匿名对象,编译器认为是一个对象声明,例如Person(p),编译器会将括号删除,直接表示为 Person p;

举例

类声明

主函数

输出结果

说明
在添加了构造函数之后,在创建对象时,对象的数据成员不再是不可确定的,构造函数自动执行,对数据成员进行赋值。即当遇到说明
Date date1 ,date2 ;
时,编译器就自动调用无参构造函数
Date : : Date( )
来创建对象date1和 date2并初始化其数据成员。

构造函数注意要点

  • 与类的其他成员函数一样,构造函数也可以直接在类的声明中定义,此时它们就是内联构函数。
  • 构造函数是在创建对象时由系统自动调用的,而且只执行一次不需要也不能够被用户调用。例如,下面的用法是错误的:
    date1 . Date( ) ;
  • 构造函数一般为public类型构造函数没有返回值,不需要在声明时指定返回值的类型。
  • 构造函数的作用是为对象进行初始化,通常由一系列赋值语句构成,但也可以包含其他语句,只是一般不提倡在构造函数中加入与初始化无关的内容。
  • 如果用户没有定义构造函数,则系统会自动生成一个默认构造函数在创建对象时调用只是这个构造函数的函数体是空的,不执行对数据成员的初始化操作。
  • 调用无参构造函数不能加括号,如:Person p();原因是c++会将此代码视为一个函数声明,参考函数声明,如:int test();

拷贝构造函数和赋值运算符重载的区别

拷贝构造函数是在创建新对象时使用现有对象进行初始化的特殊构造函数。
赋值运算符重载是一种特殊的成员函数,用于在已存在的对象上将另一个对象的值赋给它。
注意
当使用类似 student s1 = s;student s2(s); 的语法来创建对象时,会调用拷贝构造函数。
而如果 是student s1; s1=s;它是赋值操作,会调用赋值运算符重载函数来将对象 s 的值赋给已经存在的对象 s1

构造函数的重载

构造函数的重载的定义

在一个类中可以定义多个构造函数,为类的对象提供不同的初始化方法。这些构造函数有相同的名字,而参数的个数或类型不同,这就是构造函数的重载。

要点

如果在定义对象时没有给出参数,对象将通过默认构造函数创建,显然,无参的构造函数属于默认构造函数,一个类只能有一个默认构造函数。

如果在类中用户没有定义构造函数,系统会自动提供一个默认构造函数,只不过其函数体为空,不能对数据成员进行初始化。注意:系统仅在用户没有定义任何形式的构造函数时才会自动提供默认构造函数。

此外,还要注意使用默认构造函数时,不能写成Date date1( );的形式,而是得写程Date date1;

如果类已经仅定义了构造函数Date( int , int,int)而没有定义Date(),那么Date类就没有默认构造函数,此时使用Date date1;来创建对象date1就是错误的。来创建对象datel就是错误的。这是因为Date类已经有一个包含3个参数的构造函数。

尽管一个类可以包含多个构造函数,但对每一个对象来说,创建时只执行其中的一个构造函数。

举例






主函数

结果

结构体方式初始化

结构体方式初始化举例

如果一个类中所有的成员(包括数据成员和成员函数)都是公有的如下图。

那么在定义对象时可以对数据成员进行初始化。如下图。

这种情况类似于结构体。但是,如果类中包含私有的或保护的成员,就不能这样进行初始化,只能使用公有的成员函数对它们赋值。

new初始化对象

new调用构造函数初始化对象

类声明

main函数

输出结果

注意

当然,用new建立的对象要用delete释放。

类内嵌对象初始化

含内嵌对象的类的对象的初始化方式(内嵌对象的类的构造函数的组成)

在类的构造函数的声明里,去调用内嵌对象的构造函数,以此来初始化内嵌对象。

含有内嵌对象的类的构造函数形式

类名::类名(形参列表):内嵌对象1 (参数列表),内嵌对象2(参数列表)…
{
本类成员初始化
}

构造函数调用顺序

先调用内嵌对象的构造函数(按照内嵌对象在组合类的定义中出现的次序),后调用本类对象的构造函数(析构函数调用顺序相反)

构造函数的初始化方式

1普通的初始化方式

类的定义

当遇到声明
Date date1 ( 2006,3,28) ;
时,编译器就调用构造函数
Date : : Date( int yy , int mm , int dd)
来创建对象date1并用实参初始化其数据成员。

2引用的方式来初始化

构造函数的形参还可以是本类的对象的引用,其作用是用一个已经存在的对象去初始化一个新的同类对象,也称为拷贝构造函数。例如,

当遇到声明
Date date2( date1 ) ;
时,编译器就调用上面的构造函数来创建对象date2 ,并用对象datel初始化date2。

3使用参数初始化表初始化

在构造函数的头部使用参数初始化表实现对数据成员的初始化

形式

例如

即在函数首部的末尾加一个冒号,再列出参数的初始化表。上述初始化表中的year(yy),等价于在函数体内,写year=yy;

4混合初始化方式

即参数的初始化表和赋值语句相结合的方法。如:

5使用默认参数初始化

在函数中可以使用带默认值的参数,构造函数也可以包含默认参数,即参数的值既可以通过实参传递,也可以指定为某些默认值。如果用户不指定实参值,编译系统就给形参取默认值。例如

那么,当按以下方法创建对象时,
Date date1 ( 2006 ) ;
Date date2 ( 2006,4);
Date date3 ( 2006,4,8 ) ;
date1初始化为2006年1月1日,date2初始化为2006年4月1日,date3初始化为2006年4月8日。

用构造函数初始化注意要点

1.如果构造函数在类的声明外定义,那么构造函数的默认参数应该在类内声明构造函数原型时指定,而不能在构造函数定义时指定。虽然在任意一处指定都能得到正确的结果(不能两处都指定,否则会编译出错),但类的声明是类的外部接口,用户可以看到,而函数的定义作为类的实现细节用户往往是看不到的。因此,声明时指定默认参数,可以保证用户在创建对象时正确使用默认参数。

2.如果构造函数的参数全部指定了默认值,则在创建对象时可以指定一个或几个实参,也可以不给出实参,这时的构造函数属于默认构造函数,例如:
Date( int yy = 1900 , int mm = 1 , int dd = 1 ) ;
因为一个类只能有一个默认构造函数,因此不能再声明无参的默认构造函数
Date( ) ;
否则,如用下面的语句创建对象
Date date1 ;
事实上,对于全部指定了默认值的构造函数,不能再定义其他参数类型与之相同的重载构造函数,否则可能会造成歧义。例如,再声明Date类的一个重载构造函数
Date( int , int ) ;
如用下面的语句创建对象
Date date2 ( 2006,4);
编译系统将无法判断应该调用哪个构造函数。因此,一般情况下,不要同时使用构造函数的重载和有默认参数的构造函数。

析构函数

析构函数的定义

与构造函数相对应,析构函数( Destructor)也是类的一个特殊的成员函数。析构函数在对象撤销时被调用,即当一个对象的生存期结束时,系统将自动地调用析构函数。其本身并不实际删除对象,而是进行一些销毁对象前的扫尾工作,例如用delete运算符释放动态分配的存储空间等。

(注意如果类的成员用动态存储来申请的空间,我们就必须在析构函数里面去释放动态申请的空间。因为如果没有释放动态申请的空间,这部分空间是不会被系统自动回收,就会产生垃圾。
而如果只是普通的数据成员,那么在程序结束后会被系统回收,就不必我们特地在在析构函数里面去回收,也就不必写析构函数了。)

析构函数的说明格式

~类名();

析构函数的要点

析构函数名与类名相同,只是在其前面需加上波浪号“~”以与构造函数区分开。
析构函数不带有任何参数,因此不能重载。

析构函数没有返回类型。

举例(为Person增加构造函数和析构函数)

preson类

main函数

输出结果

说明
在创建一个对象时,系统自动调用对象所属类的构造函数。程序结束前,需要清除对象person1 , person2 ,析构函数被自动调用。

用delete与析构函数的关系

类A

类B

未使用delete的main函数和输出结果

使用delete的main函数和输出结果

分析
普通的对象,在程序结束后会被系统回收,也就是被系统撤销。当对象被撤销时,系统会自动调用析构函数。
动态存储申请的对象,在程序结束后不会被系统回收,因此也就没有调用析构函数。而当我们用delete去释放用动态存储申请的对象时,那么系统才开始自动调用析构函数。因此凡是用动态申请的对象,都需要在不用的时候用delete进行释放,否则所以程序即使结束了,对象是不会自动析构的,这就产生了垃圾。
同时还可以得出delete的执行步骤: 先调用析构函数然后释放内存。

String类成员函数*

248.

对象与指针

指向对象的指针

对象指针的理解

与基本数据类型的变量一样,在创建对象时也会分配存储空间用以保存对象的数据成员。可以声明指向对象的指针,存放对象存储空间的起始地址。

指向对象指针的定义

类名 *对象指针名;

定义举例

例如Person类,可以声明
Person person1;
Person * ptr = &person1 ;
这样, ptr就是指向Person类对象的指针变量,它指向对象person1。注意,对象的地址也使用取地址运算符“&”得到。

指针访问成员数据的方式

通过指向对象的指针访问对象的成员要用运算符“->”,例如:
ptr->ShowMe( ) ;
这条语句和
person1.ShowMe( ) ;
是等价的。

同样,”*“运算符出现在指向对象的指针变量前面,表示对象本身。下面的语句
( * ptr ) .ShowMe( ) ;
与前面两条语句也是等价的,需要说明的是,使用指针也只能够访问对象的公有成员。

指向对象成员的指针

指向对象成员的指针的理解

对象中的成员也有地址,可以声明指向对象成员的指针变量,指向对象中公有的数据成员或成员函数。

指向对象数据成员的指针变量的定义方法

指向对象数据成员的指针变量的声明方法和指向普通变量的指针完全一样。
例如,对于Date 类,可以进行下面的操作:
Date date1;//声明对象
int * p; //定义指向整型数据的指针
p = &date1. year;//p指向对象date1的数据成员year
*p = 2006;//给date1的数据成员year赋值2006

指向对象函数成员指针变量的定义方法

指向对象成员函数的指针变量的声明方法和指向普通函数的指针有所不同。
声明指向对象成员函数的指针必须指明它所属的类。
Date datel;//声明对象
void ( Date : : * p) ( int , int , int );//声明指向Date类成员函数的指针p
p = &Date : : init ;//p指向Date类的成员函数init
( data1.* p) ( 2006,4,8 );//调用对象data1中p所指的成员函数(即init)

类成员函数的地址以及普通函数的地址

普通函数的地址: &函数名
类成员函数的地址: &类名::函数名字
加上类名的原因,是要因为要告诉C++函数所属的类。

普通函数和成员函数的地址赋值

普通函数在把地址赋值给函数指针的时候,可以直接用函数名赋值。而类的成员函数要赋值给函数指针,则得在函数名前面加上&类名::。

而普通函数在赋值给函数指针的时候,可以不用取地址符号,直接用函数名,是因为函数名与函数的地址两者的值是一样的,都是函数的首地址,但两者的类型不同,而普通函数的函数名在赋值给函数指针的时候,表达式中会进行类型的转换。
函数地址的类型和指向函数的指针是一样的,类型的转换仅在表达式中才会发生,这仅是函数名众多性质中的一个,而非本质,函数名的本质就是函数实体的代表。

而C++非静态成员函数的左值不可获得,因此非静态成员函数不存在隐式左值转换,即不存在像常规函数那样的从函数到指针的隐式转换,因此要获取非静态成员函数的地址,必须在非静态成员函数前使用&操作符才能获得地址。
如 &Date : : init //Date类里的成员函数init的地址

而如果是静态成员函数,则就可以像普通函数一样,不用在函数前使用&来取函数地址。 但是以防万一,还是都用&来获取函数的地址。

this指针

this指针的理解

每个对象的存储空间都只是该对象的数据成员所占用的存储空间,而不包括成员函数代码所占用的空间。C++为了节省存储空间,把函数代码存储在对象空间之外,让不同的对象调用同一个函数代码段。(这样就不需要每个对象的存储空间都存储函数代码段)
为此,C++在成员函数里设立了一个指针,用来指向不同的对象。也就是说,C++在每一个类的成员函数里,都设了一个可以指向本类对象的指针,叫this指针。
这个名为this的指针包含了当前被调用的成员函数所在对象的起始地址,
对象调用函数的时候,会自动把对象的地址传递到函数的this指针。函数通过这个this指针,就可以获得调用这个函数的对象的数据和成员函数。

this指针的传递

this 指针,是对象调用函数的时候,自动进行传递的。
因为形参表,以及实参表里面都没有直接使用this 这个参数,所以他是隐式的自动传递的;

举例

init函数的声明
void Date::init(int yy,int mm,int dd)
{
this -> year = yy ;
this -> month = mm ;
this - > day = dd;
}

声明了Date类的两个对象date1和date2,执行下列语句
date1. init( yy,mm, dd ) ;
date2. init( yy,mm, dd ) ;

首先执行date1. init( yy,mm, dd ) 语句的时候,会调用date1的成员函数init,编译器会把对象的起始地址先赋予this 指针,然后调用init函数,执行
this -> year = yy ;
this -> month = mm ;
this - > day = dd;
因为当前this指针指向date1 , init函数执行的实际是
date1 . year = yy ;
datel . month = mm;
date1 . day = dd;
成员函数访问的是date1的数据成员。

然后执行date2. init( yy,mm, dd ) 语句的时候,调用date2的成员函数init,编译器会把对象的起始地址先赋予this 指针,然后调用init函数,执行
this -> year = yy ;
this -> month = mm ;
this - > day = dd;
因为当前this指针指向date2,所以init函数执行的实际是
date2 . year = yy ;
date2 . month = mm;
date2 . day = dd;
成员函数访问的是date2的数据成员。

继承

与继承有关的一些概念

为什么要有继承

一般来说,不同类中的数据成员和成员函数是不会相同的。但有些时候,两个类的内容会出现基本相同或部分相同的情况。例如Person类。

如果现在要声明一个学生类,除了包含姓名、年龄、性别等属性外,还有学号和班级信息。可以如下声明一个Student类

可以看出, Student类中的内容很大一部分是Person类中已经具备的,只是增加和修改了很少的一部分。这样,人们自然会想到能否可以利用Person类作为基础,稍做修改和增加一部分新的内容来创建Student 类,以减少重复的工作。而这就是继承功能出现的原因。C++中通过继承机制来处理这样的问题

继承的理解

一个新类从现有的类那里获得其已有的特性,这种现象称为类的继承。换句话说,从已有的类产生一个新的类,称为类的派生。在此,已有的类称为“父类”或“基类”,新建立的类称为“子类”或“派生类”。

在建立一个新类时,程序员可以让新类继承已定义基类的所有数据成员和成员函数,而不必重新编写这些数据成员和成员函数。派生类还可以对这些数据成员和成员函数进行增加和调整,使新类具备已有类没有的一些特定的功能。一个基类可以派生出多个派生类,派生类本身也可再作为基类派生出其他的派生类。

通常情况下,派生类需要添加基类所没有的数据成员和成员函数。派生类比基类更具体,可以这样认为派生类是基类的对象,基类是派生类的抽象。

直接派生类和间接派生类

A为基类,类B是类A的派生类,类C是类B的派生类,则类C也是类A 的派生类。类B称为A的直接派生类,类C称为类A的间接派生类

派生类的声明

声明格式


其中,基类必须是已有的类的名称,派生类名则是新建的类名。继承方式有3种,即公有继承( Public) ,私有继承( Private)和保护继承( Protected )。如果不显式地给出继承方式关键字,系统的默认值是私有继承。不同继承方式下,派生类自身及其对象对基类成员的访问控制权限不同。

单继承和多继承

一个派生类可以只有一个基类,这种情形称为单继承;也可以同时有多个基类(这些基类之间可能毫无关系),这种情形称为多继承.另一方面,一个基类可以派生出多个派生类.

单继承举例

基类

派生类

多继承举例


这是多继承的情形。航天飞机类拥有机翼,起落架和火箭发动机3个属性,同时有着陆和发射两个成员函数。

^03dff7

继承的过程

例图

继承过程的理解

对于派生类的构造而言,并不是把基类的成员和增加的成员简单地加在一起,而是包含三部分工作:
(1)从基类接收成员。派生类在继承了基类除构造函数和析构函数以外的所有成员时是没有选择的,不能只接收基类的一部分成员而舍弃另一部分成员。

(2)派生类对基类的扩充。增加新的成员是派生类对基类的扩充,体现了派生类功能的扩展。在上图中派生类Student增加了数据成员Number和ClassName 、成员函数ShowStu( ),Teacher类增加了数据成员Department 和 Salary ,扩充了基类。在
中派生类SpaceShuttle虽然没有直接增加新的成员,但它将Plane类和Rocket类的成员集中在一起,也是对基类扩充的一种方式。

(3)派生类对基类函数的隐藏。隐藏(Hiding)指的是派生类中定义了与基类同名的函数(非虚函数),无论该同名函数的参数是否与基类同名函数一样,派生类的都会隐藏所继承基类的同名函数,使得所继承基类的同名函数在派生类对象上无法直接访问。如果想要调用基类的被隐藏函数,可以使用作用域运算符(::)显式地指明基类名字进行调用。

派生类的不同继承方式和访问属性

类成员的访问属性

类成员的访问属性

在C++类里,成员具有public、protected、private三种访问权限。

类里面的数据的访问的方式

有两种,一个是在类内部访问(成员函数访问),一个是在类外部访问(对象调用)。
所谓在类内部访问数据,就是在类的成员函数的函数体内去访问其他成员。此时无论被访问的成员是什么访问属性,在类内部,即在成员函数的函数体内,成员可以被随意的访问。

在类外部访问数据,就是用对象直接去调用成员,此时对象就只能访问公有成员。

公有继承

公有继承的理解

在类的公有继承中
基类的公有或者保护属性的成员,在派生类里保持原有属性。基类的私有属性成员,仍然为基类的私有,不可被派生类的成员函数访问以及对象调用。

即派生类的成员函数,可以只直接访问基类公有和保护的数据和成员函数。派生类对象在类作用域外只可以访问,基类的公有成员,在作用域内则可以访问基类的公有和保护成员。

因为基类的私有成员对派生类来说是不可访问的,因此不允许派生类成员函数直接引用基类的私有成员 ,而只能通过基类的公有成员函数来引用基类的私有成员。

公有继承的举例

错误举例


正确举例
基类

派生类

main函数

输出结果

私有继承

私有继承的理解

在类的私有继承中
基类的公有或者保护属性的成员,在派生类变为私有属性。
基类的私有属性成员,仍然为基类的私有,不可被派生类的成员函数访问以及对象调用。

保护继承

保护继承的理解

在类的保护继承中
基类的公有或者保护属性的成员,在派生类作为为保护属性。
基类的私有属性成员,仍然为基类的私有,不可被派生类的成员函数访问以及对象调用。

派生类的构造函数

派生类的对象的本质

假定派生类B公有继承基类A,其示意代码如下:


当创建B类对象b后,对象b可以访问x ,y这两个成员。那么,是否可以认为b实际上是下面C类的对象呢?

从逻辑上看,可以将b看做C类的一个对象,但从本质上讲,b并不是C类的一个对象。实际上,在对象b的创建过程中,会先创建一个基类A的隐含对象,从而使对象b可以访问属于隐含对象的成员x。

派生类对象的初始化方式(派生类对象的构造函数的组成)

基类的构造函数和析构函数不能被继承,在派生类中,如果对派生类新增的成员进行初始化,就必须创建派生类的构造函数,来对这些新增成员进行初始化。
与此同时,对所有从基类继承下来的成员的初始化工作,还是应由基类的构造函数完成。而要调用基类得构造函数,就得在派生类的构造函数的声明里去调用。
如果派生类的成员有对象,且要对这个对象成员进行初始化,那么就要像[[B2.C++类和对象#类内嵌对象初始化]]一样,在派生类的构造函数里,调用内嵌对象的构造函数。

派生类对象构造函数的形式


(1)当派生类属于多继承形式时,声明中才会出现多个基类名,若是单继承,则只有一个基类名出现。

(2)若基类使用默认构造函数或不带参数的构造函数,则在派生类声明中可略去“基类名(参数表)”;若此时派生类及内嵌对象都不需初始化,则可以不定义派生类构造函数,即采用默认构造函数。
有两种情况必须定义派生类构造函数:一种是派生类本身需要;另一种是基类的构造函数带有参数。

(3)参数总表包含了全部基类和全部内嵌对象的所有参数,同时也应包含派生类新增成员初始化的参数。

(4)派生类构造函数名后面括号内的参数总表包括参数的类型和参数名,而基类构造函数名和内嵌对象名后面括号内的参数表只有参数名而不包括参数类型。这里不是定义基类的构造函数,而是调用基类的构造函数,这些参数是实参而不是形参。

派生类构造函数的执行次序如下

(1)调用基类构造函数,调用顺序按照它们被继承时声明的基类名顺序执行。
(2)调用内嵌对象构造函数,调用次序按各个对象在派生类内声明的顺序。
(3)执行派生类构造函数体中的内容。

派生类的析构函数

派生类对象析构函数的组成

派生类与基类的析构函数没有什么联系,彼此独立,派生类或基类的析构函数只作各自类对象消亡前的善后工作,因此在派生类中有无显式定义的析构函数与基类无关。

派生类析构函数的执行次序如下

派生类析构函数执行过程恰与构造函数执行过程相反。首先,执行派生类析构函数,然后执
行内嵌对象的析构函数,最后执行基类的析构函数。

举例

基类person

派生类

main函数

输出结果

分析:从输出结果可以清楚地看出,构造函数执行顺序为先祖先( Person张弓长),后客人( Person李木子),最后是自己( Student)。这里Monitor是person类的一个对象,被派生类Student所拥有。

同时,从上述输出结果中可以看出,析构函数的执行次序恰好与构造函数相反,先执行自身的析构函数(Student ) ,而后是客人( Person李木子)的析构函数,最后执行祖先( Person张弓长)的析构函数。

显式和隐式访问基类成员

隐式访问类成员的形式

类成员被访问时,常见的形式为“对象名.成员名”,
或在成员函数体内直接写出成员名。这些都属于隐式访问方法。

显示访问类成员的形式

在用对象或者成员函数访问成员的时候,在成员前面加其对应的类名也就是下面的形式。
类名::成员名

隐式和显示访问基类成员的举例

显示访问的好处

  • 在派生类中覆盖了基类同名成员后,如果要在派生类中访问基类同名成员,必须用显式访问方法。
  • 多继承情况下,多个基类拥有的同名成员在派生类中的二义性。

显示访问的运用举例

基类Person

派生类

main函数

输出结果

多继承产生的问题和解决

多继承的理解

C++的多继承是指一个类可以从多个基类继承成员变量和成员函数的能力。通过多继承,一个派生类可以拥有多个不同的父类,从而获得多个父类的特性和功能。

多继承的问题

  1. 名称冲突:当多个基类中存在同名的成员函数或成员变量时,在派生类中使用这些成员时会产生名称冲突的问题。需要使用作用域解析运算符(::),来显示访问哪个基类的成员,或者在派生类中定义同名成员,覆盖基类相关成员。
  2. 菱形继承问题:当一个派生类通过多个路径继承自同一个基类时,可能会出现菱形继承问题。例如,如果类 B 和类 C 都继承自类 A,而类 D 继承自类 B 和类 C,那么在类 D 中就存在两个相同的类 A 的实例,这可能导致二义性和冗余。解决菱形继承的问题,可以用显示访问,或者在派生类中覆盖基类相关成员,或者使用虚继承。

虚继承

通过使用虚继承,可以确保在继承关系中只有一份共享的基类的成员。这样可以避免派生类中存在多个相同基类的成员的问题。
在虚继承中,通过在继承关系中使用关键字 virtual 来标记虚继承。

虚继承的举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
public:
// 基类的成员函数和成员变量
};

class Derived1 : public virtual Base {
// 派生类1
};

class Derived2 : public virtual Base {
// 派生类2
};

class FinalDerived : public Derived1, public Derived2 {
// 最终派生类
};
//在上面的示例中,Derived1 和 Derived2 类都使用了虚继承来继承自 Base 类。这样,在最终派生类 FinalDerived 中,只会有一份 Base 类的实例,避免了二义性和冗余的问题。

多态

多态的概述

多态的两种形式

在C++中,多态性有两种不同的形式;编译时多态性和运行时多态性。

编译时多态

编译时多态性指同一类的不同对象或同一个对象在不同环境下调用名称相同的成员函数,所完成的功能不同。函数(包括类成员函数)的重载和运算符的重载都属于这一类。这种在编译连接阶段完成绑定工作的情况称为静态绑定。

运行时多态

运行时多态性是指同属于某一基类的不同派生类对象,在形式上调用从基类继承的同一成员函数时,实际调用了各自派生类的同名函数成员。运行时多态性是通过使用继承和虚函数来实现的,在程序运行阶段完成绑定工作,称为动态绑定,又称晚期绑定或后绑定。

绑定的理解

确定了函数调用是调用哪个具体函数。
也就是将函数调用与具体函数绑定起来

静态绑定的理解

在编译阶段就将函数实现和函数调用绑定起来。
也就是说代码运行前,就让具体函数与函数调用绑定。执行程序时可以直接执行。

动态绑定的理解

在程序运行的时候才进行函数实现和函数调用的绑定。
也就是说程序运行的阶段,才让具体对象的函数与函数调用绑定。增加程序运行时间。

编译时多态的举例说明(静态绑定的举例说明)


将兔子抽象化为一个类后,可以具有许多成员函数。其中“逃生”成员函数,表达了兔子逃命的不同方法。当遇到老鹰袭击时,兔子会使用“兔子蹬鹰”的绝招。当遇到狼的攻击时,兔子则采用“动如脱兔”的逃跑方法。
显而易见,这就是函数重载。在使用这些函数时,它们的参数都是在编码时设置好的。即当调用“兔子”类的“逃生”函数时,传入的参数是“老鹰”或“狼”的对象,在编译时就已确定,不会改变。因此,在编译代码时使用哪一个版本的函数,也可以确定。这种多态性就是编译时的多态性。

运行时多态的举例说明(动态绑定的举例说明)




一个小孩得知邻居家养了几个宠物,但不知是猫还是狗。于是,小孩丢一块石头到邻居家院中,以探明真相。这里,宠物作为基类,拥有一个speak( )函数,即发声函数。而猫类和狗类是宠物类的派生类,并各有一个基类speak( )函数的同名覆盖成员函数。类伪代码如上


小孩丢石块,相当于调用了宠物类对象的speak( )函数。因为小孩并不知是猫是狗,只知道是宠物。但是,实际接收此消息的却是宠物的派生类对象,如果是猫则给出“miao!miao!”字符串;如果是狗,则给出“wang! wang!”字符串。调用过程的伪代码如上。

由于小孩只知道对象是宠物类,因此 main( )函数定义了宠物类指针p。按照设想:指针p应根据实际情况指向猫类或狗类对象;
最后,语句 p ->speak( )调用的应该是p所指向派生类的speak( )函数。
按照这种设想,程序在编译阶段并不知道指针p将指向什么对象,所以在编译阶段就无法确定p将调用哪个类的speak( )函数。
只有在运行阶段才能确定 p的值,从而动态决定调用哪一个类的speak( )成员函数。这正是运行时多态性的典型形式。

但是,上面的程序段并不会按照设想运行。实际上,当执行上面的程序段时,不论指针p所指对象是宠物类还是其派生类,p ->speak( )都只能调用基类——宠物类的speak( )方法,这是由派生类替代基类对象的原则所决定的。因此,为了学习运行时多态性的实现机制,首先需要了解派生类对象替代基类对象的一些知识。

派生类对象替换基类对象

替换的原则

公有派生类全面继承了基类的成员及其访问权限,因此只有公有派生类对象才可以替代基类对象做本来由基类对象所做的事情。

替换的形式

(1)派生类对象给基类对象赋值。
(2)派生类对象给基类对象的引用赋值。
(3)派生类对象的地址赋值给基类指针。(也就是令基类对象的指针指向派生类对象)

三种替换的举例

类的声明

main函数

输出结果

注意,上面不论哪一种情形,派生类对象替代基类对象后,只能当作基类对象来使用。不论派生类是否存在同名覆盖成员,这样的基类对象所访问的成员都只能来自基类。而要想实现运行时多态性,还得了解虚函数的相关知识。

虚函数

虚函数的定义

根据派生类替代基类对象的原则,可以用基类对象指针指向派生类对象,但只能访问基类的成员。为了实现多态性,即能够通过指向派生类的基类指针,访问派生类中同名覆盖成员函数,因此需要将基类的同名函数声明为虚函数。
则当基类指针指向包含虚函数的派生对象时,C++会根据该指针所指的对象类型决定调用的这个的对象的虚函数版本。这一决定是在运行时做出的,因此当指针指向不同的对象时,就执行该对象的虚函数的版本。这同样适用于基类引用。

虚函数的形式

virtual 函数返回类型 函数名(参数表)
{
函数体
}
虚函数是一个成员函数,该成员函数在基类内部声明并且被派生类重新定义

虚函数使用多态的原理



[[304-多态的实现原理、成立条件、作用.mp4#t=00:23.207,03:55]]

注意
虚函数表是针对类的,类的所有对象共享这个类的虚函数表,因为每个对象内部都保存一个指向该类虚函数表的指针vptr
^c4d3e3

举例

基类

派生类

派生类

主函数

结果

派生类重定义虚函数

在派生类中重定义的基类虚函数的过程就叫重写(Override),通过相同的函数名来覆盖基类中的虚函数。在派生类重写的函数仍为虚函数,同时可以省略virtual关键字。虚函数重定义时,函数的名称、返回类型、参数类型、个数及顺序与基类虚函数完全一致。
如果在重新定义虚函数时改变了它的原型,那么该函数只能被认为是由C++编译器重载的,其虚函数特性也将丧失。如果修改例12-1中Cat类的Speak( )函数为

则此函数就变为一般函数重载,以下程序段

的最后一句就调用了基类的成员。

虚函数的使用说明

(1) 派生类对象替换基类对象有三种形式,但只有后两种,也就是用派生类对象初始化基类对象的引用,以及用基类对象的指针指向派生类对象这两种替换方式,才可以用指针或引用来调用访问派生类中同名覆盖的虚成员函数。
如果用第一种形式,也就是派生类对象给基类对象赋值,此时基类对象所访问的就不是派生类中同名覆盖的虚成员函数,而是基类自身的同名函数

(2)引用有其自身的特点,即引用一旦初始化后,就无法重新赋值。例如,在main( )函数中,采用下面的语句

上面语句p2= &c1错误,因为p2被初始化为d1后,不能进行修改,所以采用引用实现方式显然不够灵活,最好的方式是使用指针调用。

(3)不能定义虚构造函数的原因
虚函数的调用依赖于指针vptr,而指向虚函数表的指针vptr需要在对象构造期间进行初始化,所以无法调用定义为虚函数的构造函数。

(4)基类析构函数定义为虚函数的原因
为了实现动态绑定,基类指针指向派生类对象,如果析构函数不是虚函数,那么在对象销毁时,就会调用基类的析构函数,只能销毁派生类对象中的部分数据,所以必须将析构函数定义为虚函数,从而在对象销毁时,调用派生类的析构函数,从而销毁派生类对象中的所有数据。

虚析构函数的语法

virtual ~类名( ) ;

抽象类和纯虚函数

纯虚函数的形式

virtual 返回类型 函数名(参数表)=0;

容易看出,纯虚函数与虚函数的不同就是在虚函数的最后加上“=0”。

纯虚函数的要点

  • 纯虚函数在基类声明后,不能再定义其函数体,具体实现过程只能在派生类中完成。纯虚函数仅仅是为了实现多态性而存在的。
  • 当一个虚函数变为纯虚函数时,其派生类必须给出自己的定义覆盖该纯虚函数,否则将导致编译错误。

抽象类的定义

至少包含一个纯虚函数的类称为抽象类。

抽象类的要点

  • 抽象类不能实例化,即不能声明抽象类对象。如果 Pet为抽象类,则语句Pet p1是错误的。
  • 抽象类只作为基类被继承,无派生类的抽象类毫无意义。
  • 可以定义指向抽象类的指针或引用,这个指针或引用必然指向派生类对象,从而实现多态性。

抽象类的理解

当把类看做一种数据类型时,通常认定该类型的对象是要被实例化的。但是,在许多情况下,定义不实例化对象的类也很有用处,这种类称为抽象类( Abstract Class)。因为抽象类要作为基类被其他类继承,所以通常也将其称为抽象基类(Abstract Base Class )

抽象类的唯一用途是为其他类提供合适的基类,其他类可从这里继承和实现接口。能够建立实例化对象的类称为具体类(Concrete Class),具体类具有足以能够建立实例化对象的明确含义。

举例

基类pet

派生类Cat

派生类dog

主函数

结果

普通成员函数、虚函数、纯虚函数三者在继承中的

虚函数
派生类可以选择重写也可以不重写,如果不写,就使用直接基类中的该函数。如果直接基类也没写,再往上查找间接基类中的该函数,如此往上找到为止。

纯虚函数?
纯虚函数一定要在子类中实现。但是不一要在直接子类中实现,可以是子子类。如果子类实现了,子子类就不用实现。而子类没实现的话,子子类就必须需要实现,同时他的子类没有实现这个纯虚函数,子类也是纯虚函数,也不可以被实例化!如下图

普通函数
派生类可以选择重写也可以不重写。
重写的话,如果函数名相同,返回类型或者参数不同,编译器会自动辨别使用哪个函数。如果函数名,返回类型和参数都相同,则把基类的函数覆盖掉了。
需要使用基类的函数时,可以这样写,派生类对象.基类名称::函数名(参数);

static修饰成员(静态成员)

静态数据成员的理解

在说明一个某类的对象时,系统将为该对象分配一块内存用来存放类中的所有成员,即在每一个对象中都存放有其所属类中所有成员的副本。但在有些应用中,希望程序中同类对象共享某个数据。对此,一种解决方法是将所要共享的数据说明为全局变量,但这会破坏数据的封装性;较好的解决方法是将所要共享的数据说明成类的静态成员。
类中用关键字static修饰的数据成员是静态数据成员,静态数据成员就会被所有对象共享。

静态数据成员的声明


类MyClass 中有两个数据成员x和Count,前者为普通数据成员,在对象MemberX和 MemberY中都存在有各自的该数据成员的副本;
后者为静态数据成员,在所有类MyClass对象中的该成员实际上是同一个变量。C++编译器将静态数据成员存放在静态存储区,该存储区中的数据被类的所有对象所共享。
在类中说明的静态数据成员属于引用性说明,在使用静态数据成员之前,还必须对静态数据成员进行定义性说明,同时也可对其进行初始化。上例中的倒数第⒉行就是对静态数据成员Count的定义性说明。
静态数据成员的定义性说明,不能再main函数里进行定义。

静态数据成员的调用

静态数据成员的调用方式,与普通的数据成员调用方式是一样的,只不过静态数据成员会被所有对象共享。
其次,如果静态数据成员是public,则它也可以被用类名来调用。具体的方式就是类名::静态数据成员

静态成员函数的理解

用static关键字可以把类普通的成员函数声明为静态成员函数。
静态成员函数是没有this指针的,这就说明在调用静态成员函数的时候,函数没有this指针,无法获得调用对象的信息,因此静态成员函数只能访问类中的静态成员。如果要访问类中的非静态成员,必须借助对象名或指向对象的指针。

C++的实现里,为了节省内存而将一个类所有对象的成员函数只保存一个副本,而这本质上与静态成员的存储方式是一样,因此静态成员函数仅在逻辑上有意义。
静态成员函数的作用,就是可以不用对象,直接用类名调用。

静态成员函数的声明


静态成员函数与其他成员函数一样,也可以说明为内联的,但不能说明为虚函数。

静态成员函数的调用方式

静态成员函数是没有this指针的,那么在调用静态成员函数的时候,就不需要对象来传递地址,也就是说可以不用对象来调用,可以直接调用。
直接调用方式就是类名::成员函数
当然也可以用对象来调用对象名.成员名,但是对象在调用的时候,就不会传递地址给函数。

const修饰成员

const修饰对象的理解

与普通变量一样,可使用关键字const 修饰对象。C++规定,对于const对象,只能访问其中也用const 修饰的成员函数,
如果定义一个类的普通对象,则无论成员函数是否const 均可调用。

const修饰成员函数

即 const成员函数。在const成员函数中不得修改类中的任何数据成

const修饰对象和成员函数的举例


其中成员函数ConstFunc( )就是const 成员函数(注意修饰符const的位置)。

const MyClass ConstObj(3)//定义了一个MyClass类的const对象
int i = ConstObj.ConstFunc();//调用合法
int j = ConstObj.NormalFunc();//调用不合法

运算符重载

运算符重载的理解

在C++中,运算符和函数一样,也可以重载,使它们能够完成和创建与类有关的特殊操作。通过创建运算符函数,可以重载运算符。运算符函数定义了重载运算符将要执行的操作,该操作与其作用的对象有关。运算符函数使用关键字operator创建。

运算符函数的定义

<类型><类名> :: operator<操作符>(<参数表> ){...}
“类型”为函数的返回值类型,即运算付的运算结果值的类型;
“类名”为该运算符重载所属类的类名;
而“操作符”即所重载的运算符,可以是C++中除了“::”、“.”、“ *”(访问指针内容的运算符,与该运算符同形的指针说明运算符和乘法运算符允许重载)和“?:”以外的所有运算符。

举例






上述例子
C1+C2,可以理解为C1.OPERATOR+.(C2)
对象C3使用函数=,参数是C1;



override关键字

理解

如果派生类在所继承的虚函数的声明时使用了 override描述符,那么派生类就必须重新这个虚函数,否则代码将无法通过编译。

也就是说,如果派生类里面有需要重写的虚函数,那么可以加上关键字 override,这样编译器就会提醒你记得重写,并且在不小心漏写了虚函数重写的某个苛刻条件的时候,编译器还会辅助检查是不是正确重写。

而如果没加这个关键字 也没什么严重的 error 只是少了编译器检查的安全性;

举例说明

基类

派生类

说明
派生类继承的虚函数用了override关键字,那么如果虚函数书写错误,则编译器会报错

模板

模板的基础知识

模板的概念

在程序设计中往往存在这样的两种情况,一种是两个或多个函数的程序结构相同,区别仅在于其参数类型或函数返回类型不同;另一种是两个或多个类的结构相同,差别仅在于部分类的成员的类型或成员函数的类型及参数的类型不同。不论哪种情况,其程序框架都基本相同,只是具体细节不同。C++提供了模板(‘Template)机制,利用这一机制,可简化程序代码,实现软件复用。

C++的模板类型

C++有两种模板类型:
(1)函数模板:是一种抽象通用的函数,用它可生成一批具体的函数。这些由函数模板经实例化生成的具体函数称为模板函数。
(2)类模板:是一种抽象通用的类,用它可生成一批具体的类。这些由类模板经实例化生成的具体类称为模板类。

函数模板

函数模板理解

函数模板可以用来创建通用的函数,其作用类似函数重载,但其编码却比函数重载简单得多。

函数模板的定义形式

定义形式

模板函数定义说明
模板参数表中的模板参数形式为,class 类型参数。其中,关键字class与一般所讲的类无关,而是与类型参数一起,来说明类型参数就是一个通用数据类型,类型参数可以是任何一个合法的标识符。
在构建函数模板时,类型参数可以写在函数模板中的类型的位置上,替换掉具体的数据类型,这时原本是一个具体的数据类型,变成一个通用的数据类型。比如,我们把这个类型参数,写在函数里的传入参数的数据类型的位置。作用是在调用这个模板函数时,可以传入不同的数据类型的参数,根据实际传入的参数,来变换对应数据类型,而不是只能传入某种数据类型的参数。所以说,函数模板可以做到类型通用的作用。

函数模板的举例

函数定义

主函数

输出结果
Type int : 5
Type double :5.2
Type string : xjtu

模板函数的调用

模板函数可以直接被调用,此时他会根据你传输的数据类型,来改变类型参数。
以int test(T a1,T a2)函数举例,当我用整形数据传入函数来调用函数,如test(1,2),那么test函数里的T类型就会转换成我传入的参数的类型。也就是根据 传入的参数类型,来改变类型参数。
而如果以这种方式调用函数,就会有一个局限性,那就是得保证给模板函数传入数据的类型是统一的。如果不统一就会报错,如test(1,2.0),此时test函数里的T,就不知道该转换成跟1类型,还是该转换成2.0一样的类型。

另外还有一种调用方式,就没有这种局限性,就是在调用时指定类型参数的数据类型。还是以int test(T a1,T a2)函数举例, 调用的时候指定类型参数的数据类型,如test<\int> (5,6.2),此时无论所给实参是什么类型,test函数里的T就会转成int类型。又因为test里的T是int类型,也就是说test里的a1,a2都是是int类型,那么哪怕你传入的实参是6.2,在传给a2的时候,也会被转成int类型,此时a2的值就是6。

定义函数模板的注意要点

  1. 在函数模板的定义里,参数表以及函数的返回值类型中,至少有一个类型为模板的类型参数。
  2. 模板中可以带有多个参数类型。如:
  3. 函数可以带有模板参数表中未给出的、但已存在的数据类型的参数。如:

重载函数和函数模板之间的关系

由函数模板产生的模板函数都是同名的,因此模板函数和重载是密切相关的,编译器使用重载的方法来调用相应的函数。
函数模板本身可以用多种方法重载,也可以用其他非模板函数重载,只要它们满足函数名相同,而函数参数不同的重载条件即可。
在对函数模板重载时,C++规定,在这种情况下,编译器首先匹配重载的函数,只有在重载的函数类型无法匹配时才使用函数模板。如果编辑器通过这样的匹配过程之后,结果却找不到或产生多个匹配,就会产生编译错误。
恰当运用这种机制,可以很好地处理一般与特殊的关系。

typename关键字

关键字typename和class 在声明template参数时是等价的, typename 比 class后推出,它解决了某些情况下关键字多义性造成的问题,但某些老的编译器可能不支持,故本书仍采用class作为关键字,下同。

类模板

类模板的理解

C++的类模板可以用来创建适应多种数据类型的类。编写了类模板之后,我们就可以使用这个类模板生成不同数据类型的模板类,从而提高代码的重用性和可维护性。

类模板的定义

类模板的定义举例

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
30
31
32
33
//数据节点,用结构体模板定义
template <class T>
struct Node
{

T date; //用来存储数据
Node<T> *next; //用来存储
};

//单链表,用类模板定义
template <class T>
class DLB
{
private:
Node<T> *head;//单链表的头指针,头指针指向的第一个节点为头结点。
int sl;//数量,表示当前容器的元素数量。

public:

//链表的初始化函数,用来初始化你的链表。
DLB();

//插入函数,指定表格的位置,插入你的数值。pos是你指定的位置,value是你要插入的值,既你要把数据插入到第pos-1的结点的后面,如果pos为1,就是要把数据插入到头结点的后面。插入成功返回true,否则返回false;
bool insert_DLB(int pos, T value);

//位置查找函数,根据传入的位置,查找该位置的数据。pos是传入的位置。
T findbypos_DLB(int pos );

//链表的释放函数,在链表结束后,用来释放你的链表。
~DLB();
};

//如在测试函数李,如果执行DLB<int> a,这时候对象a里面的所有T数据类型,就会变成int类型。

类模板的外联成员函数定义说明

在定义模板类时,如果要在类外定义模板类的成员函数的时候,无论成员函数中是否用到模板参数,在类外定义时,都有遵循函数模板的定义形式去定义他们,因为类模板的成员函数都被认为是函数模板。如下,所有的参数类型都必须在类名后一一列出。

模板类的外联成员函数定义举例

1
2
3
4
5
6
7
8
9
template <class T,int Max>
SXZ<T,Max>::SXZ()
{
//更新top指针
top=-1;
//更新数量
sl=0;
}

类模板对象的定义形式


其中,类型实参是任何已存在的数据类型,也可以是非模板类。如果模板类带有多个参数类型,则除默认参数外的所有参数都必须给出其实参类型。

模板类对象的定义举例

1
2
3
DLB<int> a;
此时a对象里面的所有T就变成int类型。这也是最能直观看出模板类的作用

类模板的举例




结构体模板

结构体模板的定义举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <class T>
struct Node
{

T date; //用来存储数据
Node<T> *next; //用来存储
};



//结构体初始化
//Node<T> a;
//Node<T> *a=new Node<T>

不同模板参数的作用

类型模板参数的作用

模板参数列表里的模板参数,为类型模板参数,其作用让类里的各种数据类型变成通用的数据类型。
“模板参数表”中的类型模板参数的形式为class 类型参数(或新的关键字typename 类型参数),也就是说在构建类模板时,类型参数可以写在类模板中的类型的位置上,替换掉具体的数据类型,这时原本是一个具体的数据类型,变成一个通用的数据类型。

如上述用类模板定义的单链表,这时候用这个类创建一个具体的对象,并且传递一个类型,如DLB<int> a,这时候对象a里面的所有T数据类型,就会变成int类型。

非类型模板参数的作用

模板参数列表里的模板参数,为非类型模板参数,其作用就是用就是给类(函数)模板传递信息,在类(函数)模板中可将该参数当成常量来使用。

非类型模板参数的使用举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<class T,int N = 10>
class Mystack
{
public:
//...
private:
T a[N];
int top;
};

int main()
{
Mystack<int,100> st1; //类模板里的N就变成常数100
Mystack<int,200> st2; //N变为常数200
Mystack<int> st3; //不给参数,N就会是我们在参数模板设置的常数10
return 0;
}

模板特例化

模板特例化的理解

当我们使用模板编程时,有时候需要为特定的类型或参数提供专门的实现。模板特例化允许我们为特定的类型或参数提供专门的实现,当遇到这些特定的类型或参数时,就会调用我们特地去定义的实现。

模板特例化的分类

模板特化可以分为两种类型:完全特化(Full Specialization)和部分特化(Partial Specialization)。
完全特化是指对模板的所有参数都进行了具体的类型或值的特化。 完全特化的定义将完全取代通用模板的实现,提供了针对特定参数的定制实现。
部分特化是指对模板的部分参数进行特化,而另一部分参数仍保持通用的模板形式。 部分特化允许我们为一组参数提供特定的实现,而其他参数仍使用通用模板的实现。

举例说明

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
30
31
32
33
34
35
36
37
38
39
#include <iostream>

// 通用的模板
template <typename T>
void printValue(T value) {
std::cout << "Generic Template: " << value << std::endl;
}

// 部分特化:针对指针类型的特化实现
template <typename T>
void printValue(T* value) {
std::cout << "Partial Specialization for Pointer: " << *value << std::endl;
}

// 完全特化:针对int类型的特化实现
template <>
void printValue<int>(int value) {
std::cout << "Full Specialization for int: " << value << std::endl;
}
//这里所有的T都换成类型int

int main() {
int a = 10;
double b = 3.14;
int* ptr = &a;

printValue(a); // 使用完全特化的实现
printValue(b); // 使用通用模板的实现
printValue(ptr); // 使用部分特化的实现

return 0;
}
在上面的示例中,我们定义了一个通用的模板函数 printValue,它可以打印任意类型的值。然后,我们使用模板特化来提供针对特定类型或参数的定制实现。

首先,我们通过部分特化来针对指针类型提供特殊的实现,即 printValue(T* value) 函数。这个函数会打印指针所指向的值。

然后,我们使用完全特化来为 int 类型提供专门的实现,即 printValue<int>(int value) 函数。这个函数会打印 int 类型的值。

在 main 函数中,我们分别调用了 printValue 函数来打印不同类型的值。对于 int 类型的值,会使用完全特化的实现;对于其他类型的值,会使用通用模板的实现;对于指针类型的值,会使用部分特化的实现。

友元函数

友元的理解

虽然封装机制所带来的好处是极其明显的,但是如果绝对不允许类外的函数访问类中的私有成员,确实也有许多不便之处。
为此,C++提供了友元这种机制,允许类外部的函数或者类具有该类私有成员的特权。通过关键字friend可以把其他类或类的非成员函数声明为一个类的友元。作为一个类的友元的类外函数,可以像本类的成员函数一样自由地访问类中的任何成员。

友元函数的理解

友元函数实际上就是一个一般的函数,与其他普通函数的不同之处在于:友元函数需在某个类中声明,它拥有访问声明它的类中所有成员的特权。而其他普通函数则只能访问类中的公有成员。
虽然友元是在类中声明的,但其作用域却在类外。使用友元的主要目的是提高程序的运行效率。

友元函数的声明和定义

一个类的友元函数是在该类中用关键字friend修饰声明的函数。友元函数是在该类以外其他地方定义的。友元函数有权访问类中所有的成员。
声明一个友元的一般形式如下:
friend <类型><函数名>(<参数表>);

举例

代码

说明
在本例子中,友元函数FriFunc()带有一个参数—对类Person 的引用,在函数的定义中可利用该参数来访问对象person中的私有成员。
注意,这是友元函数访问类成员的唯一方法,因为友元虽在类中说明,但它并不是类的成员函数,因此不带this 指针。

在模板类里定义友元函数举例

注意

  1. 除了可以像上例那样将一个全局函数声明为某类的友元之外,也可以将一个类的成员函数声明为另一个类的友元,例如:

    由此可见,一个类的友元可以是类外的任何函数,包括不属于任何类的普通函数和属于某个类的成员函数。
  2. 友元声明可以出现在类的私有部分﹑保护部分和公有部分。在某类中说明友元只是说明该类允许这个函数随意访问它的所有成员,但友元函数并不是它的成员函数,指定对该函数的访问权限是没有意义的。

友元类

友元类的理解

当一个类成为另一个类的友元时,就构成了友元类。这意味着该类的每一个成员函数都是另一个类的友元函数。

友元类的声明

定义友元类的语句格式如下: friend class 类名;
例如,以下语句说明类B是类A的友元类:

经过以上说明后,类B的所有成员函数都是类A的友元函数。
注意类B,需要在类A外面进行定义。或者类B已经是程序中的一个已定义过的类,。

举例

代码

输出结果
5

注意

使用友元虽然可以提高程序的运行效率,但却破坏了类的封装性。友元就如同在封装类中成员的容器上打了一些洞,一个类拥有的友元越多,这种洞就越多,类的封装性就越差。因此,为了保证数据的完整性及数据的封装性与隐藏性,在实际应用中应尽量少用或不用友元。另外,友元是不可继承的,即一个类的友元并不是其派生类的友元。

内部类

内部类的理解

如果一个A类定义在另一个B类的内部,这个A类就叫做内部类,B类就叫外部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
内部类可以理解为,就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
只是内部类比友元类多了一点权限:可以不加类名的访问外部类中的static、枚举成员。其他的都和友元类一样。

注意

内部类可以定义在外部类的public、protected、private都是可以的。

如果内部类定义在public,则可通过 外部类名::内部类名,来定义内部类的对象。

如果定义在private,则外部不可定义内部类的对象,这可实现“实现一个不能被继承的类”问题。

内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。但不能直接访问外部类的成员函数。
因为内部类是一个独立的类,不属于外部类,所以此时还没有外部类的对象,显然也不存在成员变量。而静态成员变量就不同了,不需要外部类的对象就已存在。

举例

类的定义

主函数

输出结果
b:5
statci value:3

异常处理机制

异常的理解

例如,当程序执行时遇到下面几种异常情况:出现除数为0的除法运算;
系统内存空间不够,无法完成指定的操作;
硬盘上文件被移动而无法打开;
数组下标越界(注意,C++编译器是不理会数组下标越界的)等,这些都有可能会造成正在执行的程序提前终止。

异常处理机制的理解

异常处理机制则将异常的检测与处理进行分离,即程序代码只负责检测异常是否发生,而具体的异常处理操作则是在其他地方交给特定的异常处理程序来完成。这样做可以使程序结构更加清晰,错误处理也更加灵活和高效。

异常处理的实现方式

C++的异常处理由3个保留字实现: throw ,try 和 catch。
需要检测异常的程序段(包括函数调用)必须放在try语句块中执行,try检测函数调用是否引发了异常,检测到异常条件并使用throw引发一个异常;
异常由紧跟着try语句块后面的catch语句捕获并处理。

因而try与catch总是结合使用的,其形式如下:

在上述结构中,一个try语句可与多个catch语句相联系。如果某个catch 语句的参数类型与引发异常的信息数据类型相匹配,则执行该catch 语句的异常处理(捕获异常),此时由throw语句抛出的异常信息(值)传送给catch语句中的参数。
在多个catch 语句的最后可以使用catch(…)捕获所有其他类型的异常,其中的省略号表亓可与任何数据类型相匹配。
引发异常的 throw 语句必须包含在try语句块内,或者由 try语句块中直接或间接调用的丞数执行。
throw语句的一般形式如下:
throw exception ;
这里的exception表示一个异常值,它可以是任意类型的变量、对象或值。

注意;catch语句的类型匹配过程中不做任何类型转换,例如unsigned int类型的异常值不能被int类型的catch参数捕获。

异常处理的运行过程

在函数调用过程中,一个函数可以调用另一个函数来执行某个任务。被调用的函数称为被调用函数,调用该函数的函数称为调用函数。假设有函数A调用函数B,而函数B又调用函数C,那么在这个调用链中,函数C是A的下一级函数,B是C的上一级函数,而A是B的上一级函数。
当一个函数在执行过程中出现异常时,首先会在当前的函数进行处理,如果当前函数无法处理,就将这个异常将传递给它的上一级函数来处理,上一级函数就是调用它的函数。如果上一级函数也不能处理该异常,则继续向上一级传递,直到最高一级函数或者C++运行系统处理为止。

假设有一个程序由三个函数组成,分别是 main()、foo() 和 bar()。main() 调用 foo(),foo() 调用 bar()。如果 bar() 在执行过程中出现异常,那么看该异常在bar能否处理,
如果 bar()不能,就把该异常将传递给 foo(),如果 foo() 能够处理该异常,那么它就会在 foo() 中被处理掉。
如果 foo() 不能处理该异常,那么该异常将继续向上一级函数 main() 传递,如果 main() 能够处理该异常,那么它就会在 main() 中被处理掉。如果 main() 也不能处理该异常,那么该异常将被传递给 C++ 运行系统处理。

用try与catch处理异常与用if处理异常的区别

用try与catch处理异常,异常的检测与处理进行分离,就是在try里去检测异常, 然后在某个地方集中用catch里处理异常。
跟使用if检测的区别在于,如果使用if检测异常,那么就得直接在if下面去处理异常,这样久需要在每个可能引发异常的代码块中进行异常处理,这样会导致代码冗长和可读性降低,使得程序的可读性不高。
相比之下,使用try和catch可以将所有的异常处理代码集中在一起,使得代码更加简洁和易于维护。

使用try与catch来进行异常处理的举例




在这里try是进行异常的检查,catch是对异常的处理,也就是异常于检查分离。
在try里如果没有问题,则继续执行代码,如果有问题则执行throw语句抛出异常,然后代码就转到catch处,去处理异常

构造函数不能抛出异常的原因

在类的构造函数中抛出异常可能导致内存泄漏。
致内存泄漏主要原因是,在构造函数中发生异常时,对象的生命周期尚未完成,即对象还没有完全构造成功。此时,对象的析构函数可能不会被调用,它所占用的资源没有被释放,这样就会导致内存泄漏或资源泄漏的问题。

析构函数不能抛出异常的原因

析构函数不可以抛出异常,如果析构函数抛出异常,则异常点之后的程序,比如释放内存等操作,就不会被执行,从而造成内存泄露的问题

[[B2C1.C++常用类和结构体]]