6.构造函数和析构函数的调用顺序
构造函数和析构函数是由系统自动调用的,函数的调用顺序取决于过程进入和离开对象范围的顺序。一般来说,析构函数的调用顺序与构造函数相反。
对象的存储类型可以改变析构函数的调用顺序。
全局对象的构造函数在程序开始执行之前调用,在程序结束时调用其析构函数;
自动局部对象的构造函数在执行到对象定义时被调用,其析构函数则在离开定义对象的块时被调用;
静态局部对象的构造函数在程序执行首次到达对象定义时调用一次,其析构函数在程序结束时被调用。
7.字符串类
(1)标准C++类库预定义了string字符串类,它封装了字符串的属性,并提供了访问属性的成员函数。利用string可以直接声明字符串变量,并能进行字符串的赋值、相加、比较、查找、插入、删除、取子串等操作。
(2)使用string类时必须包含头文件string,并使用命名空间std。
(3)常用方法和运算符。
8. 对象数组与对象指针
(1)对象数组
①概念:是指每一个元素都是相同类型的对象的数组。每个数组元素都是一个对象,不仅具有数据成员,还有成员函数。
②一维对象数组的定义格式: 类名 数组名[常量表达式];
③说明:
建立数组时需要调用构造函数,调用次数与数组长度一致。
使用对象数组时只能访问单个数组元素的成员,访问形式:数组名[下标].成员名
初始化对象数组:若构造函数只有一个参数,定义数组时可以直接在等号后面的花括号内给出初值列表。考虑初始化的各种需要,当各对象元素的初值相同时,可在类中定义无参构造函数或带默认参数的构造函数;当各对象元素的初值要求不同时,需定义带参(无默认值)构造函数。
带多个参数的构造函数初始化对象数组:定义对象数组时,只需在花括号内分别写出构造函数并指定实参即可初始化各对象元素。
当对象数组生命期结束时,系统会自动调用析构函数来完成扫尾工作。
(2)对象指针
①对象指针:是存放对象地址的变量,通过对象指针可以间接访问它所指向对象的成员。
②使用指针访问单个对象成员:
a.定义对象指针: 类名 *对象指针名;
b.给对象指针的赋值: 对象指针名=&对象名;
c.使用对象指针访问对象成员:
(*对象指针名).成员名 或 对象指针名->成员名
③使用对象指针访问对象数组的方法:
a.定义对象指针: 类名 *对象指针名;
b.为对象指针赋值:对象指针名=对象数组名;
c.通过对象指针访问对象数组各元素的成员:
对象指针名[下标].成员名 或 对象指针名->成员名 (对象指针名++;)
*
9. 向函数传递对象
①传值调用:调用函数时,系统创建形参对象,并自动调用拷贝构造函数完成实参对象向形参对象的值复制工作。函数调用过程中只能访问形参对象,函数中对形参对象的任何修改均不影响调用该函数的实参对象本身——单向传递。
②传址调用:调用函数时,创建形参指针变量,并将实参对象的指针传递给形参变量,函数调用过程中,通过形参可间接访问其所指对象(即实参对象),则对其所指对象的修改就是对实参的修改。
③引用调用:调用函数时,为实参对象声明引用(别名),不会为形参重新分配空间,也不会进行参数传递。函数调用过程中,通过形参可对其引用的对象(即实参对象)进行直接访问,则对其引用的对象的修改就是对实参对象的修改。可见,引用调用既具有传址调用可改变实参的特点,还具有调用时节省时间和空间、操作简单直接的特点。
10.常(const)类型
①常引用
声明常引用:在声明引用时用const修饰,则被声明的引用为常引用。如果用常引用作为函数形参,则不会产生对实参对象不希望的修改操作(避免对实参对象的更改),保证了数据的安全性,其作用效果与传值调用相同,却在调用中节省的时间和空间的消耗。
语法格式: const 类型标识符 &引用名=对象名;
②常数据成员
声明const数据成员: const 类型标识符 成员名;
说明:除构造函数外,任何其他函数不能对常数据成员进行赋值,并且在构造函数中只能利用初始化列表向常数据成员提供初始值。含有常数据成员的类的构造函数的定义格式为:
类名::类名(形参表): 数据成员名(值),……
{ 函数体 }
另外,对于类的引用数据成员,也应通过初始化表进行数据的初始化。
③常成员函数
声明常成员函数:需在函数声明和函数定义中分别指定其const属性,在调用时不必加const。
函数声明中指定const的限定格式:
类型标识符 函数名(参数表)const;
函数定义中指定const的限定格式:
类型标识符 函数名(参数表)const
{ 函数体 }
说明:
const成员函数的定义中,不允许修改类对象的数据成员。
常成员函数可以访问本类中的数据成员(普通/常),但不能修改其值,也不能调用本类的普通(非const)成员函数——保证常成员函数不会改变数据成员的值。
通过常对象只能调用它的常成员函数,而不能调用其普通成员函数——常成员函数是常对象唯一的对外接口。
关键字const可以用于区分重载函数。常成员函数可以用普通成员函数重载,编译器根据对象是否为const自动选择所用的重载函数。
常对象的构造函数和析构函数不需要const声明。将构造函数和析构函数声明为const是个语法错误。在常对象的构造函数中调用非const成员函数是合法的。
④常对象
声明常对象:声明对象时使用const修饰,其声明格式:
const 类名 对象名(初值表); 或 类名 const 对象名(初值表);
如: const CPoint point1(12, 0);
说明:
常对象必须通过构造函数进行初始化;
常对象的数据成员在对象的生存期内不能被修改,试图修改则会产生编译时错误
C++不允许常对象调用普通(非const)的成员函数。
11.静态成员
(1)静态数据成员
同一个类的不同对象都有自己的数据成员的空间,它们可以具有不同的属性值(实例属性),当类的所有对象需要共享一个成员副本时(类属性),可以通过声明静态数据成员来实现。
①定义格式:static 类型标识符 成员名;
②说明:
类的静态数据成员是同一类的所有对象共有的。不管有多少个对象,静态数据成员只有一个,同时可被任何一个同类对象访问。
在一个类对象的空间内不包含静态数据成员的空间,其所占的空间不会随对象的产生而分配,或随对象的撤销而消失。它是在程序开始时被分配的,即使没有创建类的一个对象。
静态数据成员不具体属于哪一个对象,不能在构造函数或其它成员函数中初始化。必须初始化且在类外进行,其语句不属于任何类、任何函数,其在文件范围内只能初始化一次,格式为:
类型标识符 类名::静态数据成员名=初值;
对于在类的public部分声明的静态数据成员,可以不使用成员函数而通过对象直接访问,若未定义类的对象,需用类名指明。访问格式为:类名::成员名
在private和protected部分声明的静态数据成员只能通过类的成员函数访问。若未定义类的对象,则需提供一个类的静态成员函数,并在调用时使用类名和作用域运算符指定。
(2)静态成员函数
①定义格式:其定义与一般成员函数的定义相同,只是在其前面需加上static修饰符。
②说明:
即使在类没有实例化任何对象时,类的静态数据成员和静态成员函数就已经存在并可使用。
与静态数据成员一样,静态成员函数是独立于类对象而存在的,它不与类的对象相关联。静态成员函数没有this指针。访问静态成员函数时不需对象,但需在函数名前加上类名和作用域运算符。
一个静态成员函数不与任何对象关联,因此,它不能对类中的非静态成员进行默认访问。解决办法:将对象作为静态成员函数的参数,在函数中通过对象访问其非静态成员。
12.对象成员(组合类)
(1)定义:
组合类是指一个类将其他类对象作为自己的数据成员。其定义格式为:
class X
{ 类名1 成员名1;
类名2 成员名2;
……
};
(2)组合类构造函数的定义形式:
如果其成员对象的构造函数为有参函数时,组合类对象的构造函数参数表需指定传递给成员对象的参数,并利用初始化列表的方式显式调用对象成员的构造函数。
其定义形式为:
类名::类名(对象成员形参,基本类型成员形参):对象成员1(形参表1),对象成员2(形参表2),…
{ 基本类型成员的初始化 }
(3)说明:
①组合类对象的构造函数首部中,各成员对象的初值表均来自于构造函数的形参表。
②组合类对象按照由内到外的顺序构造,按照由外到内的顺序删除。
③若一个类在定义中将它的成员对象指定为public,将不会破坏该成员对象的private成员的封装和隐藏。
④构造组合类对象的顺序:系统首先调用内嵌成员对象的构造函数,依次初始化成员对象;然后再执行组合类对象构造函数的函数体,对其他的非对象数据成员进行初始化。若该组合类对象含有多个内嵌成员对象,则按类定义时的声明顺序构造成员对象,而不是按照初始化表中的顺序。如果成员对象不需要提供初始值,系统将隐式调用其默认构造函数。
(4)组合类的拷贝构造函数:
如果没有为组合类定义一个拷贝构造函数,编译系统会自动为组合类添加一个默认的拷贝构造函数,其功能是:自动调用内嵌对象的拷贝构造函数,对各内嵌对象成员进行初始化。组合类拷贝构造函数的定义格式如下例:
C::C(C &c):a(c.a)
{ … }
第4章继承与派生
1.继承的基本概念
继承是面向对象程序设计的一个重要特性,它允许在已有类的基础上创建新的类。新类继承了现有类的属性和方法(不必重新编写代码),同时又添加了自己的新特性,从而在继承的基础上实现扩充。
2.基类与派生类:
在继承关系中,被继承的原有类称为基类(base class),又称为父类或超类;通过继承关系定义出来的新类被称为派生类(derived class),又称为子类。
(1)单继承派生类的定义格式:
class 派生类名 : [继承方式] 基类名
{ 派生类中新成员的声明 };
(2)说明:
①“继承方式”可以是public、pretected或private方式,若缺省,则默认为private。通过它可以实现派生类对继承自基类的成员的访问控制,通常多使用public继承方式。
②基类必须是已定义的一个类。
③派生类首部列出的是派生类的直接基类。
④派生类中成员:
继承基类的成员;
增加新的成员;
重新定义继承自基类的成员。
3.派生类对基类成员的访问控制
(1)说明:
①无论是哪种继承方式,基类的private成员在派生类中都是不可见的。这符合数据封装和信息隐藏的思想。
②派生类虽然不能直接访问基类的private成员,但可以通过基类的public或protected成员函数间接访问。
③不同继承方式的影响主要体现为:
派生类成员对基类成员的访问控制(类域内内部访问)
派生类对象对基类成员的访问控制(类域外对象访问)
(2)私有继承的访问规则:
基类中的成员 公有成员 保护成员 私有成员
访问方式 内部访问 可访问 可访问 不可访问
对象访问 不可访问 不可访问 不可访问
(3)公有继承的访问规则:
基类中的成员 公有成员 保护成员 私有成员
访问方式 内部访问 可访问 可访问 不可访问
对象访问 可访问 不可访问 不可访问
(4)保护继承的访问规则:
基类中的成员 公有成员 保护成员 私有成员
访问方式 内部访问 可访问 可访问 不可访问
对象访问 不可访问 不可访问 不可访问
4.同名成员:同名覆盖
在派生类中可以对从基类中继承的成员函数进行重新定义,使之满足派生类的具体需要。——同名覆盖
通过派生类对象调用一个被重定义过的基类成员函数,被调用的是派生类的成员函数,此
时若想调用基类的成员函数,必须使用基类名和作用域运算符加以限定。
5.派生类的构造函数和析构函数
派生类不能继承基类的构造函数和析构函数。由于派生类继承了基类成员,在建立派生类的对象时,必须先调用(隐式或显式)基类的构造函数来初始化派生类对象的基类成员。
(1)派生类构造函数:
①定义格式:
派生类名::派生类构造函数名(形参表):基类构造函数(参数表)
{ 函数体 }
②说明:
调用基类构造函数的参数表来自于派生类构造函数的参数表中。
若在基类中没有定义任何构造函数,则派生类的构造函数定义时可省略对基类构造函数的调用,在创建对象时,系统将隐式调用基类的默认构造函数;如果基类构造函数存在,且为有参函数,则必须定义派生类的构造函数。
③构造函数的调用顺序:
a)在创建派生类对象时,系统首先调用基类的构造函数,初始化派生类中的基类成员;
b)若派生类包含对象成员,则调用对象成员的构造函数。若包含多个成员对象,其调用顺序按照它们在类中的声明顺序;
派生类名(参数总表):基类名(参数表0),对象成员名1(参数表1),….,对象成员名n(参数表n)
c)调用派生类的构造函数,初始化派生类中的新增数据成员;
(2)派生类析构函数
①定义方法与普通类的析构函数相同:
~派生类类名() { 函数体 }
②派生类析构函数的工作:完成派生类对象的清理工作
③析构函数的调用顺序与构造过程相反:
a)调用派生类的析构函数,清理普通成员;
b)调用对象成员的析构函数,清理对象成员;
c)调用基类的析构函数,清理自基类继承来的成员。
6.基类与派生类对象之间的赋值兼容关系
(1)规则:在需要基类对象的任何地方,都可以用公有派生类的对象替代,只能使用从基类继承来的成员,但反之则不被允许。
(2)具体体现:
①派生类对象可以向基类对象赋值,即用派生类对象中从基类继承来的数据成员逐个赋值给基类对象的数据成员。
②派生类对象可以初始化基类对象的引用。
③派生类对象的地址可以赋给指向基类对象的指针,即指向基类的指针也可以指向派生类对象。
(3)注意:
通过基类对象名、指针访问派生类对象时,只能访问从基类继承的成员。
(4)基类指针、派生类指针、基类对象、派生类对象的混合匹配有以下四种情况:
①直接用基类指针指向基类对象。
②直接用派生类指针指向派生类对象。
③用基类指针指向其派生类对象。这种方式是安全的,但基类指针仅能访问派生类对象的基类部分。
④用派生类指针指向基类对象。这种方式不被允许,会导致语法错误。可以通过强制类型转换将基类指针转换为派生类指针,实现对基类对象的访问,但不安全,程序员需正确使用指针。