面向对象程序设计总结
基本概念
对象:任何一个对象都应该具备两个要素 -- 属性
行为
在 C++ 中每个对象都要数据(体现了属性)和函数(用来对数据进行操作,实现功能)两部分组成.
类: 具有共性的实体的抽象.
- 类是对象的抽象,不占有内存.
- 对象是类的特例,即类的具体表现形式,占有存储空间.
面向对象的三大特性:封装、继承、多态。
封装:将方法和数据封装在类里面,根据访问限定符的使用保证数据的安全性,隐藏了方法的实现细节,方便使用。
封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是通过外部接口以及特定的访问权限来使用类的成员。
继承:对已有类增加属性和功能, 或进行部分修改来建立新的类, 是实现代码的复用的重要手段,继承是类型之间的关系建模。
继承可以使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码,在令子类继承父类的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类的原有属性和方法,使其获得与父类不同的功能,继承是指一个对象直接使用另一个对象的属性和方法。
多态:在面向对象的程序里面,同一个消息被不同的对象接受后可以导致不同的行为,是接口的多种不同的实现方式,极大的提高了代码的重用。
多态性,允许将子类类型的指针赋值给父类类型的指针,多态性在 C++中是通过虚函数实现的。虚函数就是允许被其子类重新定义的成员函数。
抽象:表示同一类事物的本质。
对象的引用
同一类的对象可以相互赋值。
在一个类中至少有一个公用的成员函数作为对外的接口。
公用成员函数是用户使用类的公用接口,或者类的对外接口。外界只能通过公用成员函数来实现对类内的私有函数进行操作。
当类中有指针且有动态内存分配时,务随便赋值,有可能会出现问题。
::
是作用域的限定符,声明函数或者变量是属于哪个类。内置成员函数:C++ 对一般的内置函数需要使用关键字
inline
声明.
对于类内定义的成员函数,可以省略关键字
inline
,C++ 默认类内的成员函数为内置函数。但是如果成员函数在类体外定义时,需要在声明与定义的时候加上inline
关键字说明
- C++ 中类的每个对象所占有的存储空间只是该对象的数据成员所占有的存储空间.
不包括函数代码所占有的存储空间,与成员函数无关。而这些对象的成员函数对应的是同一个函数代码段。
- 不管成员函数在类内定义还是类外定义,是否用
inline
声明,存储方式相同,都不占用对象的存储空间。
inline
函数只影响程序的执行效率,而与成员函数是否占有对象的存储空间无关。
对象成员引用的方式:
通过对象名与成员运算符访问对象中的成员 (对象名.成员名
stud.num
)通过指向对象的指针访问对象的成员 (指针—>成员名
p—>num
)通过对象的引用来访问对象的成员。
类的公用接口与实现分离:
通过成员函数对数据进行操作称为类的功能实现,为了防止用户任意的修改公用成员函数
类内被操作的数据是私有的,类的功能实现细节是对用户屏蔽的。这种实现称为私有实现。
好处:
- 如果要修改或扩充类的功能,只需修改该类中有关的数据成员与成员函数,成语中类以外的部分可以不必修改。
- 编译时发现勒种的数据读写有错,不必检查整个程序,只需要检查本类中访问的这些数据的少数成员函数。
把类的声明放在指定的头文件中,用户要使用该类,只需要把有关的头文件包含进来即可。不必再程序中重复书写类的声明,以减少工作量,节省篇幅,提高编程的效率。
一个 C++程序是有 3 部分组成:
(1)类声明的头文件(.h
)。(2)类实现文件(.cpp
)包含类成员函数的定义。(3)类的使用文件(.cpp
)即主函数文件。
构造函数 和 析构函数
构造函数主要用于在定义对象时,完成对象的初始化.
每一个类都应该有一个构造函数,如果用户没有定义构造函数,编译器会自动生成构造函数(参数和函数体为空的构造函数),如果用户自定义了构造函数,那么编译器不再提供默认的构造函数。
特性:
构造函数的名称必须要与当前类的名称相同。
构造函数仅在定义对象时由系统调用,其他时间无法调用。而且只执行一次。
构造函数可以有参数,也可以没有参数,但是不允许有返回值。
构造函数只能定义为公有成员,不能定义为其他。
调用条件:
1.定义对象时。2.为对象分配动态内存时。3.定义无名对象(稍作了解)
注意:(1)构造函数可以进行重载,以便用于不同形式的对象的定义。(2)构造函数还可以使用默认的缺省参数。如果构造函数既有重载,又有缺省参数时,注意不要产生二义性。
参数初始化表:
除了构造函数进行对成员数据进行初始化,还有参数初始化表对数据初始化,在函数首部实现。
例如:
1 | BOX::BOX(int h,int w,int len):height(h),width(w),length(len){} |
如果数据成员是数组,则需要在构造函数的函数体内用语句对其复制,不能再参数初始化表中进行对其初始化。
重载:
在一个类中定义多个构造函数名具有相同的名字,而参数的个数或者参数的类型不同。
析构函数是一种特殊的成员函数,完成与构造函数相反的工作,对象退出生命周期时,完成清理的工作。如:释放内存等。
特性:
析构函数的名称与类的名称相同。为了区分,析构函数名字前面有
~
构造:
stu(){}
析构:~stu(){}
析构函数无参、无返回值。
析构函数不可重载。每一个类有且只有一个析构函数,但是可以有多个构造函数。
在对象退出生命周期时,编译器会自动调用析构函数。但是,可以人为调用析构函数,不过没意义。
一般情况下,使用系统默认的析构函数就可以。当类中有动态内存分配时,需要增加自定义的析构函数,否则有可能会导致内存泄露。
调用条件:
对象退出生命周期时。定义的对象在调用结束后释放时自动执行析构函数。
释放动态分配的对象空间。使用
new
运算符动态的建立了一个对象,当使用delete
运算符释放该对象时,先调用该对象的析构函数。定义的全局的对象在程序离开其作用域的时候调用该全局对象的析构函数。
静态(static)局部对象在程序调用结束时对象并不释放,因此也不调用析构函数,只在 main 函数结束或调用 exit 函数结束程序时,才调用 static 局部对象的析构函数。
析构顺序
同一作用域下,先构造的后析构。即最先被调用构造函数,其对应的析构函数最后被调用。先进后出。
调用析构函数的顺序与存储类别有关。
this 指针
指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。this
指针是隐式使用的,它是作为参数被传递给成员函数的。
常对象
类名 const 对象名[(实参表)]
或者 Const 类名 对象名[(实参表)]
在定义的时候必须初始化,如果一个对象声明为常对象,只能调用该对象的常成员函数,而不能调用其他的一般成员函数。常成员函数是常对象的唯一对外接口。
常成员函数可以访问常对象中的数据成员,但是不允许修改对象中数据的值。但是一般的成员函数可以引用类中的非 const 数据成员也可以改变他们。
常对象成员
常对象的数据成员都是常数据成员,必须通过构造函数的参数初始化表对常数据成员进行初始化。
1 | Const int hour; |
常成员函数:可以访问常对象中的数据成员,但是不允许修改对象中数据的值。
类型名 函数名(参数表) const;
指向常对象的指针变量
Const 类型名 * 指针变量名
如果一个变量声明为常变量,只能用指向常变量的指针向量指向它。
指向常变量的指针变量除了可以指向常变量外,也可以指向未被声明为 const 的变量,但是不可通过该指针变量改变变量的值。
当希望在调用函数时,对象的值不被修改,就应当把形参定义为指向常变量的指针变量,同时用对象的地址作为实参(对象可以是 const 或者非 const 型的)如果要求该对象不仅在调用函数的时候不被改变,而且要求它在程序执行的过程都不被调用,则需要把对象也定义为 const 型。
Time const t;
t 是一个常对象,其值在任何情况下都不能改变。Void Time::fun() const;
fun 是 Time 类中的常成员函数,可以引用,但是不能改变本类中的数据成员。Time *const p;
p 是指向 Time 类对象的常指针变量,p 的值不能改变。Const Time *p;
p 是指向 Time 类对象的指针变量,P 指向的类对象的值不可以通过 p 来改变。Const Time &t1 = t;
t1 是 Time 类对象 t 的引用,二者指向同一个存储空间,t 的值不能改变。
对象的操作
对象的动态建立与释放
在 C 语言中是利用库函数 malloc
和 free
来分配和撤销内存空间的。C++ 提供了简便而功能较强的运算符 new
和 delete
来取代 malloc
和 free
函数。
注意:
new
和delete
是运算符,不是函数,因此执行效率高。
用 new
分配数组空间时不能指定初值。如果由于内存不足等原因而无法正常分配空间,则 new
会返回一个空指针 NULL
(0 值),用户可以根据该指针的值判断分配空间是否成功。
1 | Box *pt; // 定义一个指向Box类对象的指针变量pt |
delete
指针名; 如果被删除的是普通变量,则会直接释放动态分配的内存。如果被删除的是对象,则该对象的析构函数被调用。
注意,用
new
动态分配的内存只能用delete
释放一次,如果释放第二次会出现错误。
对象的赋值:对象名2 = 对象名1;
是对一个已经存在的对象赋值,必须向定义被赋值的对象,才能进行赋值。
拷贝构造函数(复制构造函数):也是构造函数,但是只有一个参数,这个参数是本类的对象,而且采用对象的引用的形式(一般约定加 const
声明,使参数值不能改变)。此复制构造函数就是将实参对象的各成员值一一赋给新的对象中的成员。
复制构造函数与普通构造函数的区别
形式上的不同:两者虽然函数名与类名相同,也不指定函数类型。但复制构造函数只有一个参数,并且是对同类对象的引用-------复制构造函数无法重载
普通的构造函数:
类名(形参列表)
拷贝构造函数:
类名(类名 &对象名)
在建立对象的时候实参类型不同,系统会根据实参类型决定调用普通的构造函数还是拷贝构造函数。
在 C++中,下面三种对象需要调用拷贝构造函数(有时也称“复制构造函数”):
1 | 1、使用已知对象初始化新对象。 |
补充:
浅拷贝:只复制数据,没复制内存空间。
深拷贝:既拷贝数据,也要复制内存空间。
静态数据成员:
静态数据成员。为各个对象所共有,在内存中只占有一份空间,每个对象都可以引用。这样可以节省空间,提高效率。
只要在类中定义了静态数据成员,即使不定义对象,也为静态的数据成员分配看空间,可以被引用。而且也不随着对象的撤销而释放,到程序结束后才释放空间。
可以初始化,但是只能在类体外进行初始化。
数据类型 类名::静态数据成员名=初值;
不能使用参数初始化表对静态成员数据进行初始化。编译系统会自动赋值为 0;
可以通过对象名引用,也可以通过类名引用。(它是属于类的,不是属于某个对象的)
如果静态数据成员被定义为私有的,则不能在类外直接引用,必须经过公用的成员函数引用。
有了静态数据成员,各对象之间有了沟通的渠道,实现数据共享,可以不使用全局变量,全局变量破坏了封装的原则,不符合面向对象的程序要求。
静态成员函数:
静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装。
静态成员函数只能访问静态数据成员。
静态成员函数与非静态成员函数的根本区别:
非静态成员函数在调用时
this
指针时被当作参数传进。而静态成员函数属于类,没有this
指针,所以不能访问本类中的非静态成员,但是可以引用本类中的静态数据成员,如果一定要引用本类中的非静态成员,需要加上对象名和成员运算符.
。静态成员函数是类的一部分,而不是对象的一部分,在类外调用公用的静态成员函数要使用类名和域运算符
::
。
友元
友元(friend)机制
允许一个类将对其非公有成员的访问权授予指定的函数或者类.
友元的声明以friend
开始,它只能出现在类定义的内部,友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受其声明出现部分的访问控制影响。通常,将友元声明成组地放在类定义的开始或结尾是个好主意。
友元函数
是指某些虽然不是类成员函数却能够访问类的所有成员的函数。类授予它的友元特别的访问权,这样该友元函数就能访问到类中的所有成员。
将普通的函数声明为友元函数(可以访问此类中的私有数据成员):在类体内声明,使用关键字
friend
。在类体外定义不使用类作为限定符,它是非成员函数,不属于任何类。在访问类的私有数据成员,必须加上对象名。友元成员函数:该类中的成员函数声明为另一个类的友元函数。需要对另一个类进行提前声明,只包含类名,不包含类体。在另一个类的类体中声明为友元函数的时候需要加上本类的类名与限定符
::
Eg:
friend void Time:: display(Date &);
,display()
函数是类 Date 的普通成员函数
友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。
关于友元类的注意事项:
友元关系不能被继承。
友元关系是单向的,不具有交换性。若类 B 是类 A 的友元,类 A 不一定是类 B 的友元,要看在类中是否有相应的声明。
友元关系不具有传递性。若类 B 是类 A 的友元,类 C 是 B 的友元,类 C 不一定是类 A 的友元,同样要看类中是否有相应的申明。
优点:
可以灵活地实现需要访问若干类的私有或受保护的成员才能完成的任务;
便于与其他不支持类概念的语言(如 C 语言、汇编等)进行混合编程;
通过使用友元函数重载可以更自然地使用 C++语言的 IO 流库。
缺点:
- 一个类将对其非公有成员的访问权限授予其他函数或者类,会破坏该类的封装性和信息隐蔽,降低该类的可靠性和可维护性。
malloc 函数
malloc
函数的函数原型为:void* malloc(unsigned int size)
,它根据参数指定的尺寸来分配内存块,并且返回一个void
型指针,指向新分配的内存块的初始位置。如果内存分配失败(内存不足),则函数返回NULL
。
关于返回值
malloc
的返回值为void
*。我们在使用的时候,习惯对返回值进行强制类型转换:
1 | char * p = NULL; |
以前的 C,因为没有
void*
这种类型,malloc
函数的返回值被简单地定义为char*
, 所以在使用malloc
函数时通常需要对其返回值进行强制类型转换。现在 C 中,malloc
函数的返回值为void*
。强制类型转换操作已经不需要了。
然而在 C++ 中,任何类型的指针都可以赋给
void*
,而void*
却不可以赋给其他类型的指针,所以在 C++ 中使用malloc
函数的时候,强制类型转换是必须的。另一方面,在 C++ 中应该使用new
来分配内存。
malloc 在堆上分配内存
malloc
函数分配的内存是在堆(heap
)上的。
操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点, 然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序.
另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的 delete
或 free
语句才能正确的释放本内存空间。我们常说的内存泄露,最常见的就是堆泄露(还有资源泄露),它是指程序在运行中出现泄露,如果程序被关闭掉的话,操作系统会帮助释放泄漏的内存。
malloc 的使用
malloc 函数使用起来倒是挺简单的,主要的使用范例有两种:
一是动态分配结构体,通常用于被称为“链表”的数据结构中;
二是分配可变长度的数组。
过程中的注意点:调用 malloc
函数后,应该对函数返回值进行检查。前面说过,内存分配一旦失败,malloc()
会返回 NULL
。
1 | char * p = NULL; |
在程序结束时,应该调用 free
函数对 malloc
函数分配的内存进行释放。
实际上,C 语言标准没有规定要这么做,而且普通的 PC 操作系统,在进程结束时,肯定会释放曾经分配给当前进程的内存空间,也就是说,在程序结束之前,没有必要调用 free()
。
但是,对于一串连续的程序处理事件,如果先前程序分配的内存没有及时释放掉,那后面的工作就遭殃了。所以 malloc
与 free
配套出现还是相当合理的。
从操作系统一次性地取得比较大的内存,当程序调用 malloc()
时,malloc()
便将内存”零售”给应用程序,这是 malloc()
的大体实现。而当这块一次性取出来的内存不够用的时候,就请求操作系统对空间进行扩容。多次调用 malloc()
(导致内存不够用了)会调用一次 brk()
,内存区域向地址较大的一方伸长。malloc()
分配内存,会用到 brk
(用于小内存申请<=128kb,在堆上) 或mmap2
(用于大内存申请,一般是堆和栈中间)系统调用 。
K&R 中记录了malloc()
最简单的一种实现方式:通过链表来实现。malloc()
管理的空间不一定是连续的,空闲存储空间以空闲块链表的方式组织。在这种方式下,每个块之前都加上了一个管理区域,包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。这些快按照储存地址的升序组织。最后一块(最高地址)指向第一块。
当有申请要求时,malloc()
将扫描空闲块链表,直到找到一块足够大的空闲块为止,如果找不到,则向操作系统申请一个大块并加入到空闲链表中。然而在这种内存管理方式的运行环境中,一旦数组越界检查发生错误,越过了 malloc()
分配的内存区域写入了数据,将会破坏下一个块的管理区域,容易造成程序崩溃。
运算符重载
<<
与>>
分别是流插入运算符与流提取运算符,在使用的时候需要在本文件的模块中包含头文件 stream,还用过包含 "using namespace std;"。运算符重载是定义一个重载运算符的函数,使指定的运算符不仅能实现原有的功能,而且也实现在函数中指定的新的功能。
在使用被重载的运算符,系统会自动调用该函数,以实现相应的功能。运算符重载是通过定义函数实现的。实质就是函数的重载。
- 一般格式:
1 | 函数类型 operator 运算符名称 (形参表) |
Eg: Complex operator + (complex &c1 , complex & c2);
规则:
除了类属关系运算符
.
、成员指针运算符*
、作用域运算符::
、sizeof 运算符和三目运算符? :
以外,C++ 中的所有运算符都可以重载。重载运算符不能创建新的运算符。
运算符重载实质上是函数重载,因此编译程序对运算符重载的选择,遵循函数重载的选择原则。
重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数及语法结构。
运算符重载不能改变该运算符用于内部类型对象的含义。
它只能和用户自定义类型的对象一起使用,或者用于用户自定义类型的对象和内部类型的对象混合使用时。其参数至少应该有一个是类对象(或者是类对象的引用)。
运算符重载是针对新类型数据的实际需要对原有运算符进行的适当的改造,重载的功能应当与原有功能相类似,避免没有目的地使用重载运算符。
运算符重载不能有默认的参数,否则就改变了运算符参数的个数。
用于类对象的运算符一般必须重载,但是有两个例外,
=
与&
。赋值运算符=
可以用于每一个类对象,利用它对同类对象间相互赋值。地址运算符&
可以返回类对象在内存中的起始地址。
运算符重载函数的处理方式:
- 作为类的成员函数。
如果运算符重载函数为成员函数,它可以通过
this
指针自由的访问本来的数据成员,因此可以少写一个函数的参数。但是必须要求运算表达式的第一个参数为一个类对象,而且与运算符函数的类型相同。- 不是类的成员函数(可以是一个普通函数),在类内把它声明为友元函数(友元运算符重载函数)。
将双目运算符重载为友元函数,由于友元函数不是该类的成员函数,因此在函数形参列表中必须有两个参数,不能省略。数学上的交换律在此不适用,所以需要运算符左侧的操作数与第一个参数对应,右侧的操作数与第二个参数对应。
两种重载形式的比较, 一般将运算符重载为两种之一都可。但成员函数运算符与友元函数运算符也具有各自的一些特点:
- 一般情况下,单目运算符重载为类的成员函数;双目运算符则重载为类的友元函数。
- 以下一些双目运算符不能重载为类的友元函数:
=
、()
、[]
、->
。 - 类型转换函数只能定义为一个类的成员函数。
- 若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。
- 若运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选用友元函数。
- 当运算符函数是一个成员函数时,最左边的操作数(或者只有最左边的操作数)必须是运算符类的一个类对象(或者是对该类对象的引用)。如果左边的操作数必须是一个不同类的对象,或者是一个内部类型的对象,该运算符函数必须作为一个友元函数来实现。
- 当需要重载运算符具有可交换性时,选择重载为友元函数
重载流插入运算符
<<
与流提取运算符>>
C++ 的流插入运算符
<<
和流提取运算符>>
是 C++ 在类库中提供的,所有 C++ 编译系统都在类库中提供输入流类 istream 和输出流类 ostream。
cin
和cout
分别是 istream 类 和 ostream 类 的对象。在类库提供的头文件中已经对
<<
和>>
进行了重载,使之作为流插入运算符和流提取运算符,能用来输出和输入 C++ 标准类型的数据。因此,凡是用cout<<
和cin>>
对标准类型数据进行输入输出的,都要用#include
把头文件包含到本程序文件中。用户自己定义的类型的数据,是不能直接用
<<
和>>
来输出和输入的。如果想用它们输出和输入自己声明的类型的数据,必须对它们重载。对<<
和>>
重载的函数形式如下:1
2istream & operator >> (istream &,自定义的类&);
ostream & operator << (ostream &,自定义的类&);重载运算符
>>
的函数的第一个参数和函数的类型都必须是istream&
类型,第二个参数是要进行输入操作的类。重载<<
的函数的第一个参数和函数的类型都必须是ostream&
类型,第二个参数是要进行输出操作的类。因此,只能将重载>>
和<<
的函数作为友元函数或普通的函数,而不能将它们定义为成员函数。return output 的作用是什么?回答是能连续向输出流插入信息。output 是 ostream 类的对象,它是实参 cout 的引用,也就是 cout 通过传送地址给 output,使它们二者共享同一段存储单元,或者说 output 是 cout 的别名。因此,return output 就是 return cout,将输出流 cout 的现状返回,即保留输出流的现状。
优点:
通过运算符的重载,扩大了 C++ 已有的运算符的作用范围,使之能用于类对象。
把运算符与类结合起来,可以在 C++ 程序中定义出很有实用意义的而使用方便的新的数据类型,使 C++ 具有良好的扩充性与适应性。
在 C++ 中,运算符重载是很重要的、很有实用意义的。它使类的设计更加丰富多彩,扩大了类的功能和使用范围,使程序易于理解,易于对对象进行操作,它体现了为用户着想、方便用户使用的思想。有了运算符重载,在声明了类之后,人们就可以像使用标准类型一样来使用自己声明的类。类的声明往往是一劳永逸的,有了好的类,用户在程序中就不必定义许多成员函数去完成某些运算和输入输出的功能,使主函数更加简单易读。
转换构造函数
将一个其他类型的数据转换为类的对象。只有一个形参。也是一种构造函数,遵循构造函数的一般规则,通常把有一个参数的构造函数用作类型的转换,称为转换构造函数。
类型转换运算符函数(类型转换运算符重载函数):将一个类的对象转换为另一个类型的数据。在函数名的前面不能指定函数类型,函数没有参数。其返回值的类型由函数名中指定的类型名来确定。只能成为成员函数,因为转换的主体的本类的对象,不能作为友元函数与普通函数。
1 | Operate 类型名() |
如果运算符重载函数是成员函数,它的第一个参数必须是本类的对象。
一般将双目运算符函数重载为友元函数,单目运算符则对重载为成员函数。
继承
(1) 类与类之间的关系
has-A,包含关系,用以描述一个类由多个“部件类”构成,实现 has-A 关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类。
use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现。
is-A,即继承关系,关系具有传递性。
(2) 继承的相关概念
万事万物皆有继承这个现象,所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类。
单继承:一个派生类只从一个基类派生。
多重继承:一个派生类有两个或者多个基类
基类与派生类的关系:基类是派生类的抽象,派生类是基类的具体化。基类综合了派生类的公共特征,派生类则在基类的基础上增加了某些特征,把抽象类变成具体的、实用的类型。
(3) 派生类的声明方式
1 | Class 派生类名:[继承方式] 基类名 |
继承方式包括:公有继承,私有继承,保护继承。默认为私有继承。
(4) 构造一个派生类需要完成的工作
- 从基类接受成员。(可能会造成数据的冗余)
- 调整从基类接受的成员。
- 在声明派生类时增加成员。
(5) 继承方式及访问属性
1. 公用继承(public), 用公用继承方式建立的派生类称为公用派生类。其基类称为公用基类。
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
2) 私有继承(private), 用私有继承方式建立的派生类称为私有派生类。其基类称为私有基类。
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。私有基类的私有成员在派生类中称为不可访问的成员,只有基类的成员函数可以引用。
3) 保护继承(protected), 用保护继承方式建立的派生类称为保护派生类。其基类称为保护基类。
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
基类成员在派生类中的访问属性:
继承方式 | 基类的 public 成员 | 基类的 protected 成员 | 基类的 private 成员 |
---|---|---|---|
public 继承 | 仍为 public 成员 | 仍为 protected 成员 | 不可访问 |
Private 继承 | 变为 private 成员 | 变为 private 成员 | 不可访问 |
Protected 继承 | 变为 Protected 成员 | 变为 Protected 成员 | 不可访问 |
注意:父类中的 private 成员依然存在于子类中,但是却无法访问到。不论何种方式继承父类,子类都无法直接使用父类中的 private 成员。如果需要在派生类中引用基类的某些成员,则应该将基类的这些成员声明为 protected。需要被外界访问的成员设置为 public;只能在当前类中访问设置为 private;
(6) 继承的特点
- 子类拥有父类的所有属性和方法(除了构造函数和析构函数)。
- 子类可以拥有父类没有的属性和方法。
- 子类是一种特殊的父类,可以用子类来代替父类。
- 子类对象可以当做父类对象使用。
(7) 继承中的构造和析构函数
1)父类的构造和析构
当创建一个对象和销毁一个对象时,对象的构造函数和析构函数会相应的被 C++ 编译器调用。当在继承中,父类的构造和析构函数是如何在子类中进行调用的呢,C++ 规定在子类对象构造时,需要调用父类的构造函数完成对对继承而来的成员进行初始化,同理,在析构子类对象时,需要调用父类的析构函数对其继承而来的成员进行析构。
在派生类中定义派生类构造函数的一般形式:
1 | 派生类构造函数名(基类所需的形参,本类成员所需的形参):基类构造函数名名(基类所需参数表) |
在派生类体外定义派生类构造函数的一般形式:
1 | 派生类::派生类构造函数名(基类所需的形参,本类成员所需的形参):基类构造函数名名(基类所需参数表) |
2)父类中的构造和析构执行顺序
子类对象在创建时,会先调用父类的构造函数,如果父类还有父类,则先调用父类的父类的构造函数,依次往上。父类构造函数执行结束后,执行子类的构造函数。当父类的构造函数不是 C++ 默认提供的,则需要在子类的每一个构造函数上使用初始化列表的方式调用父类的构造函数。
析构函数的调用顺序和构造函数的顺序相反。
(8) 从成员函数的角度来讲述重载和覆盖的区别
成员函数被重载的特征有:
- 相同的范围(在同一个类中);
- 函数名字相同;
- 参数不同;
virtual
关键字可有可无。
覆盖的特征有:
- 不同的范围(分别位于派生类与基类);
- 函数名字相同;
- 参数相同;
- 基类函数必须有
virtual
关键字。
分别位于派生类与基类的不同的成员函数,只有在函数名和参数个数相同。类型相匹配的情况下才发生同名覆盖,如果只有函数名相同,不会发生同名覆盖,而属于函数重载。
隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无
virtual
关键字,基类的函数将被隐藏(注意别与重载混淆)。如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有
virtual
关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
(9) 含有子对象的派生类构造函数
类的数据成员中还可以包含类对象,如可以在声明一个类时包含这样的数据成员:
Student s1;// Student是已声明的类名,s1是Student类的对象
这时,s1 就是类对象中的内嵌对象,称为子对象(subobject),即对象中的对象
派生类构造函数的任务应该包括 3 个部分:
- 对基类数据成员初始化;
- 对子对象数据成员初始化;
- 对派生类数据成员初始化。
定义派生类构造函数的一般形式为:
1 | 派生类构造函数名(总参数表列):基类构造函数名(参数表列),子对象名(参数表列) |
或者
1 | 派生类构造函数名(总参数表列):基类构造函数名(参数表列),子对象名(参数表列), |
执行派生类构造函数的顺序是:
- 调用子对象构造函数,对子对象数据成员初始化;
- 再执行派生类构造函数本身,对派生类数据成员初始化。
执行派生类析构函数的顺序是:
- 执行派生类自己的析构函数。
- 对派生类新增加的成员进行清理。
- 调用子对象的析构函数,对子对象进行清理。
- 最后调用基类的析构函数,对基类进行清理。
(10) 注意:
当基类构造函数不带参数时,派生类不一定需要定义构造函数,然而当基类的析构函数哪怕只有一个参数,也要为派生类定义构造函数,甚至所定义的派生类析构函数的函数体可能为空,仅仅起到传递参数的作用.
当基类使用缺省构造函数或不带参数的构造函数时,则在派生类中定义构造函数时,可以省略:基类构造函数名(参数表)
,此时若派生类不需要构造函数,则可以不定义构造函数。
如果派生类的基类也是一个派生类,则每个派生类只需负责其直接基类的构造,依次上溯。
如果析构函数是不带参数的,在派生类中是否要定义析构函数与它所属的基类无关,故基类的析构函数不会因为派生类没有析构函数而得不到执行,他们各自是独立的
(11) 多重继承:
在多重继承中,派生类的构造函数与单继承派生类构造函数相似,它必须负责该派生类所有基类构造函数以及对象成员(如果有的话)构造函数的调用。同时,派生类的参数必须包含完成所有基类、对象成员以及派生类中新增数据成员初始化所需的参数。
派生类构造函数执行顺序如下:
所有基类的构造函数,多个基类构造函数的执行顺序取决于定义派生类时所指定的顺序,与派生类构造函数中所定义的成员初始化列表的参数顺序无关;
对象成员的构造函数;
派生类本省的构造函数。
加上虚基类后,它的初始化在语法上与一般多继承的初始化是相同的,但在调用构造函数的顺序上有点差别:
先调用虚基类构造函数,然后调用非虚基类的构造函数。
当同一层有多个虚基类,按照他们的声明顺序调用它们的构造函数;
当虚基类是由非虚基类派生时,则先调用基类构造函数,再调用派生类构造函数。
(12) 虚基类:
如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。引入虚基类,使得在继承间接共同基类的时候只保留一份成员。
现在,将类 A 声明为虚基类,方法如下:
1 | class A//声明基类A |
注意: 虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。
声明虚基类的一般形式为:
1 | class 派生类名: virtual 继承方式 基类名 |
经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次。
注意: 为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。
(13) 基类与派生类的转换:
基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。
- 派生类对象可以向基类对象赋值。
派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。
如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。
派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。
(14) 类的组合:
在一个类中以另一个类的对象作为数据成员。
多态性
概念:指相同的对象收到不同的消息或者不同的对象收到相同的消息时产生的不同的实现动作。
C++支持两种多态:编译时多态(静态)、运行时多态(动态)。
静态多态是通过函数的重载来实现的(运算符重载实际上也属于函数的重载)。要求在程序编译的时候就知道调用函数的全部信息。静态多态性的函数调用速度快、效率高、但是缺乏灵活性,在程序运行前就决定了执行的函数与方法。
动态多态性是程序运行过程中才动态地确定操作所针对的对象,运行时多态性是通过虚函数来实现的。由虚函数实现的动态多态性就是:同一类族中不同类的对象,对同一函数调用作出不同的响应。
虚函数
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
虚函数的使用方法是:
在基类用
virtual
声明成员函数为虚函数。在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。在类外定义虚函数时,不必再加
virtual
。在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
C++ 规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。
定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
确认具体对象的过程叫关联(binding),在这里指把一个函数与类对象捆绑在一起,建立关联。
静态关联:函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联,由于是在运行前关联的,所以又叫早期关联。
动态关联:在运行时,基类指针变量指向了某个类对象,然后通过这个基类指针去调用虚函数。由于是在运行时把虚函数与对象“绑定”在一起, 因此,此过程称为动态关联。由于动态关联是在编译后运行阶段进行的,所以又称为滞后关联。
使用虚函数时的注意点:
只能用
virtual
声明类的成员函数,使他成为虚函数,而不能将类外的普通函数声明为虚函数。一个成员函数在被声明为虚函数后,在同一类族中的类就不能定义一个非
virtual
的但是与该虚函数具有相同参数和返回值类型的函数。
怎么判断是否把一个成员函数声明为虚函数?
首先看成员函数所在的类是否会成为基类。然后看成员函数在类的继承后有无可能被改进功能,如果希望改变其功能的,一般把它声明为虚函数。
如果成员函数在类被继承后不被修改,或派生类用不到该函数,则不要把它声明为虚函数。
应该考虑对成员函数的调用是通过对象名还是通过基类指针或引用来访问,如果通过基类指针或引用来访问的,应该考虑用虚函数。
有时,在定义虚函数时,不定义其函数体,即函数体是空的。
使用虚函数,系统有一定的开销,但一个类有虚函数时,编译系统会为该类创建一个虚函数表,它是一个指针数组,存放每个虚函数的入口地址。
当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。
但是,如果用 new 运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。
在程序用带指针参数的 delete 运算符撤销对象时,会发生一个情况:系统会只执行基类的析构函数,而不执行派生类的析构函数。
当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统会采用动态关联,调用相应的析构函数,对该对象进行清理工作。
如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。
最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。
这样,如果程序中显式地用了 delete 运算符准备删除一个对象,而 delete 运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。
虚析构函数的概念和用法很简单,但它在面向对象程序设计中却是很重要的技巧。
纯虚函数声明
1 | virtual 函数类型 函数名 (参数表列) = 0; |
注意:
- 纯虚函数没有函数体;
- 最后面的“=0”并不表示函数返回值为 0,它只起形式上的作用,告诉编译系统“这是虚函数”;
- 这是一个声明语句,最后有分号。
纯虚函数只有函数的名字而不具备函数的功能,不能被调用。 纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对他进行定义。如果在基类中没有保留函数名字,则无法实现多态性。 如果在一个类中声明了纯虚函数,在其派生类中没有对其函数进行定义,则该虚函数在派生类中仍然为纯虚函数。
抽象类
不用定义对象而只作为一种基本类型用作继承的类叫做抽象类(也叫接口类),凡是包含纯虚函数的类都是抽象类,抽象类的作用是作为一个类族的共同基类,为一个类族提供公共接口,抽象类不能实例化出对象。
纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。
一个基类如果包含一个或一个以上纯虚函数,就是抽象基类。抽象基类不能也没必要定义对象。
在类的层次结构中,顶层或最上面几层可以是抽象基类。抽象基类体现了本类族中各类的共性,把各类中共有的成员函数集中在抽象基类中声明。
抽象基类是本类族的共公共接口,即就是从同一基类中派生出的多个类有同一接口。
总结:
派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)
基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
只有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数。
如果在类外定义虚函数,只能在声明函数时加 virtual 关键字,定义时不用加。
构造函数不能定义为虚函数,虽然可以将 operator=定义为虚函数,但最好不要这么做,使用时容易混淆。
不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
虚表是所有类对象实例共用的虚表剖析。
输入流与输出流
C++的输入与输出包括以下 3 方面的内容:
对系统指定的标准设备的输入和输出。简称标准 I/O。(设备)
以外存磁盘(或光盘)文件为对象进行输入和输出。简称文件 I/0。(文件)
对内存中指定的空间进行输入和输出。简称串 I/O。(内存)
缓冲区中的数据就是流,输入输出流被定义为类。C++的 I/0 库中的类称为流类(streamclass)。用流类定义的对象称为流对象。C++编译系统提供了用于输人输出的 iostream 类库。
由抽象基类 ios 直接派生出 4 个派生类,即 istream,ostream,fstreambase 和 strstreambase。
fstreambase 是文件流类基类,由它再派生出 ifstream,ofstream 和 fstream。
strstreambase 是字符串流类基类,由它再派生出 lstrstream,ostrsCeam 和 swsWeam 类。
ostream 类定义了 3 个输出流对象,即 cout,cerr,clog。分述如下。
cout 流对象
cout 是 console output 的缩写,意为在控制台(终端显示器)的输出。
cout 不是 C++预定义的关键字,它是 ostream 流类的对象,在 iostream 中定义。cout 流是流向显示器的数据。cout 流是容纳数据的载体。
用
cout<<
输出基本类型的数据时,可以不必考虑数据是什么类型,系统会判断数据的类型,并根据其类型选择调用与之匹配的运算符重载函数。cout 流在内存中对应开辟了一个缓冲区,用来存放流中的数据。当向 cout 流插人一个 endl 时,不论缓冲区是否已满,都立即输出流中所有数据,然后插入一个换行符,并刷新流(清空缓冲区)。
在 iostream 中只对
<<
和>>
运算符用于标准类型数据的输入输出进行了重载,但未对用户声明的类型数据的输入输出进行重载。如果用户声明了新的类型,并希望用<<
和>>
运算符对其进行输入输出,应该按照第 5 章介绍的方法,对<<
和>>
运算符另作重载。
cerr 流对象
cerr 是 console error 的缩写,意为“在控制台(显示器)显示出错信息”。
cerr 流对象是标准出错流。cerr 流已被指定为与显示器关联。cerr 的作用是向标准出错设备(standard error device)输出有关出错信息。
cerr 与标准输出流 cout 的作用和用法差不多。但有一点不同:cout 流通常是传送到显示器输出,但也可以被重定向输出到磁盘文件,而 cerr 流中的信息只能在显示器输出。当调试程序时,往往不希望程序运行时的出错信息被送到其他文件,而要求在显示器上及时输出,这时应该用 cerr。cerr 流中的信息是用户根据需要指定的。
clog 流对象
clog 流对象也是标准出错流,它是 console log 的缩写。
它的作用和 cerr 相同,都是在终端显示器上显示出错信息。它们之间只有一个微小的区别:ccrr 是不经过缓冲区,直接向显示器上输出有关信息,而 clog 中的信息存放在缓冲区中,缓冲区满后或遇 endl 时向显示器输出。
1 | ofstream out; |
标准输入流 cin 重点掌握的函数
1 | cin.get() //读入一个字符并返回它的值 |
C++和 C 的文件打开模式
ios_base::ate
和ios_base::app
都将文件指针指向打开的文件尾,二者的区别在于,ios_base::app
模式只允许将数据添加到文件尾,而ios_base::ate
模式将指针放到文件尾
C++模式 |
C模式 |
含义 |
ios_base::in |
"r" |
打开以读取 |
ios_base::out |
"w" |
等价于ios_base::out | ios_base::trunc |
ios_base::out | ios_base::trunc |
"w" |
打开以写入,如果已存在,则截短文件 |
ios_base::out | ios_base::app |
"a" |
打开以写入,只追加 |
ios_base::out | ios_base::in |
"r+" |
打开以读写,在文件允许的位置写入 |
ios_base::out | ios_base::in | ios_base::trunc |
"w+" |
打开以读写,如果已存在,则截短文件 |
c++mode | ios_base::binary |
"cmodeb" |
以C++mode(或相应的cmode)和二进制模式打开;例如,ios_base::in|ios_base::binary成为"rb" |
c++mode | ios_base::ate |
"cmode" |
以指定的模式打开,并移至文件末尾。C使用一个独立的函数调用,而不是模式编码。例如,ios_base::in|ios_base::ate被转换为"r"模式和C函数调用feek(file, 0, SEEK_END) |
要获取一行输入
有两种选择:成员函数 get()、getline()。
两个函数都是有三个参数:指向存储结果缓冲区的指针、缓冲区大小(不能超过其限度)和知道什么时候停止输入的终止符。终止符有一个经常用到的缺省值‘’。两个函数遇到输入终止符时,都把零存储在结果缓冲区里。
它们的区别如下:
get()遇到输入流的分隔符就停止,而不从输入流中提取分隔符。如果用相同的分隔符再调用一次 get()函数,它会立即返回而不带任何输入。
getline()与其相反,它从输入流中提取分隔符,但仍没有把存储在结果缓冲区里。
总之,当我们在处理文本文件时,无论什么时候需要读出一行,都会想到 getline()函数。
命名空间
命名空间是 ANSIC++引入的可以由用户命名的作用域,用来处理程序中 常见的同名冲突。
在 C 语言中定义了 3 个层次的作用域,即文件(编译单元)、函数和复合语句。C++又引入了类作用域,类是出现在文件内的。在不同的作用域中可以定义相同名字的变量,互不于扰,系统能够区别它们。
命名空间,实际上就是一个由程序设计者命名的内存区域。程序设计者可以根据需要制定一些有名字的空间域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开来。
1 | namespace AA |
namespace
是定义命名空间锁必须写的关键字,AA
是自己制定的命名空间的名字。如果在程序中要使用a
和b
,必须加上命名空间名和作用域分辨符::
,如AA::a
,AA::b
,这种用法称为命名空间限定。
命名空间的作用是建立一些互相分隔的作用域,把一些全局实体分隔开来,以免产生名字冲突。
在引用命名空间成员时,要用命名空间名和作用域分辨符对命名空间成员进行限定,以区别不同的命名空间中的同名标识符。即: 命名空间名::命名空间成员名
标准 C++库中的所有标识符都是在一个名为 std 的命名空间中定义的,或者说标准头文件中的函数、类、对象和模板实在命名空间 std 中定义的。一般用 using namespace 语句对命名空间 std 进行声明,这样可以不必对每个命名空间成员一一进行处理,在文件的开头加入如下语句: using namespace std;