《Qt C++ 跨平台 图形界面程序设计基础(第2版)》
殷立峰 主编,祁淑霞、房志峰 副主编,计算机系列教程,清华大学出版社,2018.2第2版
文章目录
Typist : Akame Qixisi / Excel Bloonow
一、函数的其他特性
(一)内联函数
在函数定义前或函数声明前加上关键字inline
,就称为内联函数(内置函数),C++提供内联函数是为了提高运行的速度。因当程序执行期间发生函数调用时,需要保存现场状态,进行参数传递、现场恢复等。这些会造成系统的时间和空间开销。为了提高程序的运行效率,可以将功能简单、代码较短、使用频繁的函数声明为内联函数。编译系统对内联函数与常规函数的处理方式不同。内联函数在编译过程中,系统将直接使用该函数代码替换函数调用语句,同时用实参取代形参,程序执行期间不再发生函数调用,免去调用开销,所以内联函数的运行效率要更高一些。注:内联函数如果定义在主调函数之后,则只需在函数原型声明时加上关键字inline。通常规模小(1~5条语句),内联函数体内不包含复杂的控制语句,若代码较长或复杂,多数编译器会按普通函数处理。
(二)带默认参数值的函数
参数默认值的设定不可以同时出现在函数原型与函数定义中。一般在函数原型中设定,若无函数原型,才在定义中说明。函数中参数必须从右向左设置默认值。指定默认的参数必须放在形参列表的最右端,因为函数调用时,实参是按从左到右的顺序被依次赋给相应的形参,不会跳过任何形参。
(三)函数重载
C++支持创建多个参数列表不同的函数,编译器通过参数列表区分调用哪个函数,相应的同名函数称为重载函数。重载函数应在参数个数,参数类型或参数顺序上有所不同,否则编译系将无法确定匹配哪一个重载函数的版本。即使返回类型不也不能区分。重载函数不应该与带默认值的函数一起使用。
二、变量、类型的说明
(一)动态变量与静态变量
动态变量:用时分配,用完回收。静态变量:用时分配,保持不变,到程序结束或下次修改其值.。静态局部变量只在第一次被调用时分配内存并赋初值,其后的调用始终是访问的同一内存单元,其值是上次调用的结果。(虽静态局部变量和全局变量都贯穿程序运行,但其作用域仍是局部)。
(二)字符串与string类
C++中支持两种类型的字符串,一种是从C沿袭而来,有C中字符串的全部特征与函数,称为C风格字符串,简称C串。另一种是C++标准类库中的字符串类string
,可定义一个它的变量(对象)来表示字符串,头文件为#include <string>
。string类对象的定义,同时可以初始化。例:string str1 = "china";
或 string str2("china);
。一个学符串对象存储的字符串长度是可变的,且不含'\0'
,不像C串的最后有 ‘\0’ 。常用字符串操作,例如:赋值运算=
,关系运算>
、>=
、<=
、==
,连接运算+
,下标运算[]
,及输入输出>>
、<<
等。string类对象的确定字符串长度的方法size()
,用string类定义的数组可以处理多个守符串。
(三)指针与引用
指针本质上就是一个存放变量地址的变量。指针变量的赋值就是把相应类型变量的地址赋给指针变量。
引用就是为一个已定义的变量或对象另外取一个名字;引用作为一个变量的别名,定义形式如下:数据类型& 引用名 = 已定义的变量名;
,此时,& 不表示地址运算符,而是一种引用类型标识符,如 int& 是int变量的引用,必须在定义时初始化。使用引用就等于一个已有对象多了一个关联的名字,修改引用的值就是修改原有对象的值,而引用的操作地址也就是其代表对象的地址。引用其实是一种隐性指针,即引用是直接访向其指向的变量,与指针相比,引用不能进行自身的地址操作,提高了访问的安全性。
说明:引用在被初始化后,就不能再改变其引用的对象。不能创建指向引用的指针,但可以声明对指针的引用。如:int* p; int*& rp = p;
。不能比较两个引用的值,但可以比较被引用变量的值;不能对void类型进行引用;不能建立引用数组;不能建立引用的引用。引用作为一个变量的别名直接应用并不多,除非变量名很长。引用的重要用处是作为函数的参数和函数的返回值。
引用作函数的参数,系统将实参对象的名字传递给形参引用。这时形参名作为引用关联于实参对象,被调用函数内对形参的操作,就是对实参的操作。即:引用作为函数参数时,形参对应实参的别名,对形参的操作也影响实参。在引用传递方式下,如果被调用函数只是使用实参的值,而不改变其值,那么函数定义时可以对形参类型加const约束,如果再修改就会编译错误。
使用引用返回函数值,此时,该函数的调用可以作为左值被赋值。例:int& min(int& m, int& n) { return m < n ? m : n; } int x = 10, y = 20; min(x, y) = 0;
,此程序中,通过min()函数返回m的引用,而m是x的引用,最后x赋值为0。注:并不是所有函数都可以返回引用,一般地,当返回值不是本函数的局部变量时可以返回一个引用;否则当函数返回时该引用的变量会被自动释放,再对其进行引用就是非法的了。通常情况下,引用返回值只用在需要对函数的返回值重新赋值的时候。
(四)const修饰符
const修饰符可用来限制变量、指针和函数参数等,可限制其值不被修改。
1. 常量定义
const 数据类型 变量名 = 常数值;
,建议用const取代#define
的常量定义。两者定义的常量不同,const常量具有数据类型,而#define不具有数据类型,在置换时容易出错。
2. 常量与指针
从右向左解读:const *
,常量,* const
,常指针。根据const出现的位置不同,可分为三种形式。
- 指向常量的指针,用const修饰所指对象的数据类型,
const int *
或int const *
。该指针可以保存变量或者常量的地址,并且方向方式为只读,即不能通过指针改变所指对象的值,但可以改变指针的指向。 - 常量型指针,在指针前面加关键字const修饰,
int * const
。定义时必须初始化,指针的指向不可变,但若指向变量,则可以通过指针间接修改所指变量的值。 - 指向常量的常量型指针,都加const修饰,
const int * const
或int const * const
,都不可变。
int a = 5, b = 10, c = 15, d = 20;
const int * p1 = &a; // 指向常量的指针,或 int const * p1 = &a;
//*p1 = 0; // error
p1 = &b; // ok
int * const p2 = &b; // 常型指针
*p2 = 0; // ok
//p2 = &a; // error
const int * const p3 = &c; // 指向常量的常型指针,或 int const * const p3 = &c;
//*p3 = 0; //error
//p3 = &d; //error
3. const修饰函数参数
const修饰函数参数,则可以保证形参在函数内部不被改变。
(五)动态内存管理
一个运行的程序在内存中包括:代码区、全局数据区、常量区、栈区和堆区。栈区由系统完成,而堆区要显式分式配和释放。在C++中,除了保留了C中的malloc()
和free()
外,还提供了运算符new
和delete
。
new动态分配堆内存,指针变量 = new 数据类型(常量);
,按指定数据类型的长度分配内存并返回首地址,若失败(如内存不足),则返回NULL,其中常量是初始化值,可以省略。动态数组的分配:指针变量 = new 数据类型[表达式]
,按“数据类型”的长度从堆区分配表达式的值个连续存储空间,并返回首地址。
delete释放已分配的内存空间,delete 指针变量;
,delete[] 指针变量;
。其中指针变量必须是一个new返回的指针。如果指针指向的内存不是用new申请的堆内存(例如,该内存在栈中),则会产生一个严重的运行错误。
三、类
C++ 中的结构类型
struct
也可以包含函数,其默认是公共权限的。
(一)类的基本知识
类的一般形式为:
class 类名 {
public: 公有数据和函数成员;
private: 私有数据和函数成员;
protected: 保护数据和函数成员;
}
类成员的访问权限分为三种。private私有成员只能被类内的成员访问,类外的任何对象对它的访问都是不允许的。当声明缺省时,系统默认为私有成员。public公有成员可以被类外部的对象访向,是类与外界的接口。protected保护成员一般与私有成员相同,它们的区别表现在类的继承中对新类的影响不同。注:类的成员默认为私有,而结构的为公有。类的数据成员可以是任何数据类型,但不能用auto
,register
,extern
来说明。类的成员函数可以在类内定义,也可在类内声明而在类外定义,在类外对函数的定义格式如下:返回类型 类名::函数名(参数列表) { // 函数体; }
。其中 :: 称作域运算符,可以将类外定义的成员函数指定为内联函数,即在声明或定义时最前面加上关键字inline。将简单的成员函数声明为内联函数可以才是高程序的运行效序,但必须将类的声明和内联函数的定义都放在同一文件中,否则编译时无法置换。
(二)对象
1. 对象的定义与使用
定义对象如同前面定义一般数据类型的变量一样,一般格式如下:类名 对象名;
。对象成员的方向,只要是公有的都可以通过对象进行访问,可以通过.
、->
、*.
的方式进行访问。对象的指针(指向对象的指针)。
2. 对象的存储与this指针
各个对象的数据成员占用不同的存储空向,存储各自的数据成员值。但它们的成员函数代码却是相同的,从物理角度,成员函数代码是存储在对象空间之外的,而且只在内存中保存一份。为让同一段函数代码识别所操作的不同对象的数据,系统自动为成员函数添加了一个名称为this的指针参数。类的所有非静态成员函数都自动拥有一个隐含this指针参数,形式为class_type * const this;
,通常this指针是成员函数的第一个参数,通过实参传通,使得this指针指向当前调用的对象,在成员函数中通过this指针可实现对当前调用对象的成员的访向。注:this是一个常指针,不可改指向,this指针作为一个隐含参数,是由系统自动设置,不能被显示声明,但可显式使用。
3. 对象数组
定义一维对象数组的形式如下:类名 数组名 [长度];
。在建立对象数组时,每一个对象都要调用一次构造函数,在上述定义中,每一个对象都没有提供实参数据,所以需要调用无参构造函数或带有全部默认值的构造函数。如果程序中没有提供这样的构造函数,就需要根据构造函数的定义提供相应的实参。为此在定义数组对象的同时,应按顺序显式给出每个构造函数的调用,并用花括号括起来。注:若定义动态new对象数组,则只能调用默认构造函数,也可以使用系统默认构造函数。
(三)类的封装和信息隐藏——共有接口与私有实现的分离
实际上,在面向对象的程序开发中,一般做法是将类的声明(其中包括成员函数的声明)放在一个头文件中,而将函数定义(实现)部分放在另一个文件中(源文件),即将接口与实现分离。使用类时,只要把相关的头文件包含进来即可,由于在头文件中包含了类的声明,所以在程序中就可以用该类来定义对象,由于在类体中包含了对成员函数的声明,在程序中就可以调用这些对象的公用成员函数。
由于头文件存放在当前目录中,因此包含时用#include "filename.h"
,而非 <> ,否则编译时会找不到此文件(放在同一工程中)。
在实际应用中,通常将若干个常用的功能相近的类声明在一起,形成类库。类库由两部分组成一是类声明的头文件,二是已编译过的实现部分的目标文件。开发商把用户所需的各种类的声明按类放在不同的头文件中,同时对包含成员函数定义的源代码进行编译,得到成员函数定义的目标代码。软件商向用户提供这些头文件和类的实现的目标代码(不提供函数定义的源代码)。用户在使用类库中的类时,只需将有关头文件包含到自己的代码程序中,编译后就会自动与库中的目标代码相连接,最后生成可执行文件。这和在程序中使用C++系统提供的标准函数的方法是一样的。(目标文件和头文件同名)。
(四)构造函数与析构函数
1. 构造函数
类是一种抽象数据类型,它不被分配内存空间,不能存放具体数据,所以不能在类的声明中给数据成员赋初值。构造函数可由系统自动生成,无参数,只负责创建对象,不负责初始化。也可自己定义(此时系统不再生成)。构造函数一般为公有成员,其名称与类名相同。构造函数的参数可为任何数据类型,但它无返回值(也不是void)。构造函数不能被显式地调用,在定义对象时,系统自动地调用构造函数完成对象内存空间的分配和初始化工作。构造函数是类的成员函数,具有一般成员函数的所有性质,可访向类的所有成员,可以是内联函数,可带参数列表,可带默认形参值,可以重载,也可以类内声明类外定义。对带参数的构造函数,应考虑实参传递。类名 对象名(实参列表);
。
除在构造函数的函数体内用赋值语句实现数据成员的初始化,C++还提供了一种成员初始化列表的方式。在构造函数首部末尾添加一个:
号,即构造函数名(类型 形参1, ...) : 数据成员(形参1), ... { }
。创建对时数据成员的初始顺序只与它们在类中的声明顺序有关,而与它们在初始化列表中给的数据无关。如果构造函数内还有其他语句,先执行初始化列表,再执行函数体。
用new动态申请对象,也需调用构造函数初始化类名* 指针变量 = new 类名(实参列表);
,动态申请的对象也只能通过指针访问。
(1) 构造函数的重载
多个参数个数或参数类型不同的同名函数。注:使用无参构造函数初始化为类名 对象名;
而不是类名 对象名();
。因为后一种表示声明一个返回类型为类名类型的普通函数。尽管一个类可有多个构造函数,但建立对象时只执行其中一个。如果找不到构造函数,编译阶段就会出错。
带默认参数值的构造函数。如果构造函数是类内声明,类外定义的,则应在类内声明构造函数时指定默认值,在类外定义时不再指定,定义了带有全部默认参数的构造函数后,不能再重载函数,否则会产生歧义,带有全部默认参数值的构造函数与无参的构造函数无法同时存在。如果构造函数的全部参数都指定了默认值,则定义对象时实参可自右向左者略。当定义一个对象不给出任何初始参数值时,系统所调用的构造函数称为默认构造函数。可见,默认构造承数是无参的或参数全部省略的。(全部默认参数时,若想重载,其形参列表的形参个数比默认的形参个数多。)
(2) 复制构造函数
复制构造函数是一种特殊的构造函数,某作用是在建立一个新对象时,用一个已存在的对象去初始化该对象。复制构造函数也是一种构造函数,其函数名与类名相同,并且复制构造函数也没有返回数类型。复制构造函数有一个形参,是本类对象的引用。这样就可以避免在参数传递时,建立新的对象,而为了保护实参对象,通常使用const引用。复制构造函数的一般形式为:类名::复制构造函数名(const 类名& src) { //copy operations; }
。每个类都有一个复制构造函数,如果程序中没有显式定义复制构造函数,则系统会自动生成一个默认的复制构造函数,用于复制出完全相同的新对象。调用复制构造函数:类名 新对象(旧对象);
或类名 新对象 = 旧对象;
。一般情况下,默认复制构造函数就可以完成初始化任务,但当类中有指针类型时,可能会出错。因默认仅是指针的复制(两个对象的指针成员指向同一地址的动态内存空间),这样对象之间复制后还共享某些资源,这种称为钱复制。与之对应的深复制,通过使用new创建新的内存空间,再将旧对象的值复制过来。
通常,在建立新对象时,用已有对象初始化新对象时,需要调用复制构函数。在遇到函数参数为类名类型、函数返回值为类名类型时也会(隐式)调用复制构造函数,完成对新建的局部对象的初始化。
2. 析构函数
析构函数也是一种特殊的成员函数,用于在撤销对象时释放分配给对象的内存空间,并做一些善后工作。析构函数为公有成员,名称与类名相同,但在名字前加波浪号~
。一般形式为:~类名() { // 析构函数体; }
。说明:析构函数没有参数,没有返回值,不能重载。每个类中必有一个析构函数,若无显式定义,则系统会自动生成一个默认的析构函数,它的实现为空。当撤销对象时,系统自动调用(不显式调用)。当:
- 主函数结束(或调用exit()函数)时;
- 函数内定义的局部变量(包括形参),当函数调用结束时;
- 用new创建的动态对象,使用delete释放时;
对象被撤销。
对于大多数类而言,默认析构函数就能满足要求。如果对象在完成操作前需要做内部处理,则应显式地定义析构函数。构造函数和析构函数的常见用法是:在构造函数中用new运算符为对象分配空间,在析构函数中用delete运算符释放空间。(当撤销时要释放new开配的堆空间,则要显藏定义析构函数用delete释放)。
(五)静态成员
类中的静态成员为同类对象提供了一个共享机制。用static
声明,该类创建的所有对象都共享这个static成员。静态成员是局部于类的,而不是某个对象特有的成员。
1. 静态数据成员
对静态数据成员的值的更新,就是对所有对象的该静态数据成员的更新。静态数据成员只在类内声明时使用关键字static。与普通数据成员一样,静态数据成员也有public、private、protected之分。在类外只能直接访问public的静态数据成员,访向形式有两种,类名::静态数据成员名
或对象名.静态数据成员名
。静态成员是独立于对象存在的,因此可以在创建对象之前通过类名引用。静态数据成员一定要在类体外进行初始化(与类名同一作用域)。静态数据成员存储在全局数据区,在编译时分配内存,供所有对象共用。数据类型 类名::静态成员 = 初始值;
。
2. 静态成员函数
主要用来方向类的静态成员,不能直接访问类的非静态成员,与静态数据成员一样,类外调用public静态成员函数时,可以通过类名::静态成员函数名(参数列表)
或对象名.静态成员函数名(参数列表);
。说明:静态成员函数是一个特殊的成员函数,没有隐含的this指针。静态成员函数的调用不依赖于任何一个特定的对象,所以系统不会为其添加this指针,即使通过对象调用也不会传递当前对象的地址。所以在静态成员函数中,如果要引用非静态成员则要提供访问的对象。一般地,静态成员函数主要引用静态数据成员,通常,静态成员函数可以实现在建立任何对象之前处理静态数据成员,完成建立任何对象之前都需要的预操作,这是非静态成员函数所不能实现的。可以对静态方法传递对象指针或引用,以访问非静态成员。
对象调用类的public成员。类的成员为static修饰为类成员。对象可调用实例成员和类成员,类只可调用类成员。
(六)常量成员与常量对象
1. 类的常数据成员
类的常数据成员,声明格式如下:const 数据类型 数据成员名;
。类的常数据成员必须进行初始化,而且只能通过构造函数的成员初始化列表的方式来进行。包含常数据成员的类不能使用默认构造函数。在对象被创建以后,其常数据成员的值就不允许被修改。
2. 类的常成员函数
类的常成员函数,声明格式:返回类型 成员函数名(参数列表) const;
。常成员函数中不能修改对象的数据成员值。修饰符const要加在函数声明的尾部,并且作为函数类型的一部分,不能省略。如果常成员函数定义在类体外,则不论是类内声明还是类外定义,都不能省略关键字cons。一般地,如果一个成员函数只是引用数据成员的值而不是改变其值,就要声明为常成员函数。说明:const是函数类型的一部分,在声明和定义时都要加const。const成员函数既可引用const数据,也可引用非const数据,但都不能改变数据值。const成员承数不能访向非const成员函数。非const成员函数既可引用const数据,也可引用非const数据,但不能改变const数据。
3. 常对象
常对象,格式如下:类名 const 对象名(实参列表);
或const 类名 对象名(实参列表);
。一般地,对象的数据成员都是由成员函数来修改的。当定义了一个常对象后,为防止其数据被修改,就规定常对象只能调用常成员函数,而不能调用非常成员函数,也就限制了其数据值的修改。常对象的数据成员为常数据成员,不能被改变值。常对象的成员函数不自动成为常成员函数,但常对象不能调用非常成员函数。
(七)友元
类对象中的私有(或保护)数据一般只能通过该对象的成员函数才能访问。但有时需要在类的外部频繁访向类的私有(或保护)成员,可以为这样的类声明友元,友元可以访问该类的所有成员。友元包括友元函数和友元类。友元提高了程序运行效率,但同时也破坏了类的封装性。
1. 友元函数
友元函数可以是一般的全局函数,也可以是另一类的成员函数。声明友元函数时,只需在函数名前加上关键字friend,并且可声明在类的任何位置,不受private、protected和public的限制,可在类内或类外定义,友元函数不是类的成员函数,调用形式与普通函数相同。同一个函数可声明为多个类的友元函数。在定义一个类时,可以把另一个类的成员函数声明为当前类的友元函数,该成员函数就能访问当前类的成员。在声明其他类的成员函数为友元时,应在成员函数名前加上相应的类名和域运算符。注:如果要在某个类的定义之前引用该类,则应进行前向引用声明,只声明被引用的类名,而不涉及类的任何细节。
2. 友元类
在一个类中也可用friend关键字声明其他类为前类的友元类,友元类的每个成员函数都自动成为当前类的友元函数,可以访向当前类中的所有成员。声明格式friend class 友元类名;
。说明,友元关系是单向的,且是非传递的。
(八)类的组合
在定义一个新类时,其数据成员是任意的数据类型,包括已定义的类类型。如:类A有一个数据成员是类B的对象,则类A称为组合类,其中类B的对象称为子对象(或对象成员)。在类的组合应用中,应特别注意子对象成员的初始化问题。当定义一个类的对象时,如果这个类具有子对象成员,则子对象也将被建立。所以,在创建组合类的对象时,不仅要负责对本类中的基本类型数据成员初始化,还要对子对象成员初始化,而子对象成员的初始化是通过调用子对象所属类的构造方法实现的。所以,在组合类中,其构造函数的定义和调用需要考虑其中的子对象成员。声明格式:类名::构造函数名(子对象所需形参列表, 本类成员形参列表) : 子对象1(参数表), 子对象2(参数表), ... { // 本类初始化; }
。在组合类的构造函数中,以成员初始化列表形式为子对象所属类的构造函数传递实参,实现对子对象的数据初始化。有时,在组合类构造函数成员初始化列表中,并没有明显给出子对象的构造函数,则调用默认构造函数。组合类的对象释放时,调用析构函奏的顺序与调用构造函数的顺序相反。
(九)类的继承
派生类的定义一般形式:class 子类名 : 访问权限修饰符1 基类名1, 访问权限修饰符2 基类名2, ... { // 类体 };
。访问控制有三种:public、private和protected。如果缺者继承方式,默认为私有继承。如果是对结构的继承,则默为公有。一个派生类可以继承多个基类称为多继承,如果只继承一个基类就称为单继承。原则上,派生类不会继承基类的构造函数、析构函数和友元。虽然基类的构造函数和析构函数没有被继承,但一个派生类的新对象被创建或撤销的时候总是会调用基类的构造函数和析构函数以完成对基类继承来的成员的处理工作。三种不同继承方式,派生类从基类继承的成员的访问属性不同,如表:
基类成员属性: | public | protected | private |
---|---|---|---|
public公有继承 | public | protected | 不可见 |
private私有继承 | private | private | 不可见 |
protected保护继承 | protected | protected | 不可见 |
无论哪种继承方式,基类的私有成员都不可见,共有和保护成员都是可见的,且继承后变成何种控制限制类型则与继承方式有关。派生类的对象对继承来的成员访问,取决于基类的成员在派生类中变成了什么类型的成员。
1. 重名的成员
C++允许在派生类中新定义的成员与基类的成员名字相同,即派生类中出现了重名的成员,这包括数据成员和成员函数。在派生类中直接访向重名成员时意味着访向派生类中新定义的成员,此时,继承来的基类的同名成员被屏蔽。如果要在派生类中访向基类继承的同名成员,则需要显式使用基类名和域运算符。如下:对象名.基类名::成员
。派生类可以通过提供具有相同特征的函数的新版本而覆盖基类的成员函数(如果特征不同,则是函数重载,而不是覆盖)。
2. 派生类中访问静态成员
派生类中访向静态成员,在继承过程中,若基类中定义了静态成员,则在整个类继承层次中只有一个静态成员,所有派生类和基类的对象都共享该成员。静态成员在类体系中的访向规则跟一般成员相同。
3. 派生类的构造函数与析构函数
派生类中既有从基类迷承的成员又有新定义的成员,所以在创建派生类的对象时,要初始化两部分的数据成员。但是基类的构造函数和析构函数不能被派生类继承,所以派生类要定义自己的构造函数和析构函数。在创建对象时,派生类的构造函数要为基类构造函数传递参数,并且先初始化基类,后是派生类中对象成员,后是派生类中新成员。其使用初始化列表,一般定义形式如:派生类名(形参表) : 基类名(参数表), 对象成员(参数表), ... { // 新增成员的初始化; }
。当基类为默认构造函数、无参构造函数或带全部默认参数的造函数时,创建派生类对象时自动调用基类默认构造函数完成基类继承的成员初始化,此时可省略基类的初始化列表。
派生类中析构函数,执行顺序与构造函数相反。即先调用派生类析构函数,释放新增成员,调用对象成员的析构函数(隐式),调用基类析构函数。
4. 多继承
一个类可以继承多个基类,该类将自动拥有所有基类的属性和行为,这种派生方式称为多继承或多重继承。一般情况下,一个派生类的多个基类的继承方式应该相同,这样做可使派生类的复杂性降低,方便程序维护,方便派生类的使用。定义格式:class 派生类名 : 继承方式1 基类1, 继承方式2 基类2, ... { // 类体 }
。多继承中,派生类的构造函数要负责给所有基类的构造函数传递参数。定义派生类时,对基类名的声明顺序,决定了基类构造函数的执行顺序及派生类对象的内存组织顺序。多继承中,派生类继承了所有基类的成员,这些成员的访向控制属性与单继承方式相同。但当多个基类中存在同名成员时,就要用类名::成员
进行限定,否则会产生二义性。
在多继承中,如果在多条继承路径上有一个公共基类,则在这些路径的汇合点,便会产生来自不同路径的公共基类的数据成员的多份副本。如果想只保留基类的一份副本,就必须用关键字virtual
把这个公共基类定义为虚基类,这种维承方式被称为虚继承,虚继承使得最终的派生类对象中只保留公共基类(虚基类)的一份数据成员,避免了二义性问题。虚基类是在它的派生类中(直接派生)声明的,格式如下:class 派生类名 : virtual 继承方式 基类名 { // 类体 };
。说明:关键字virtual与派生方式关键字的先后顺序无关紧要,为了保证基类成员在派生类中只被继承一次,应将其直接派生类都说明为按虚方式派生,这样可以避免由于同一基类的多次复制而引起的二义性。
(十)多态、虚函数与抽象类
1. 编译时多态和运行时多态
编译时多态(静态联编),函数的重载、运算符的重载,在编译器编译代码时,根据参数列表的匹配关系,就可以确定下来具体调用的函数,也就是在程序执行前,就已经确定下来函数调用关系。即为编译时多态。
运行时多态(动态联编),程序中的函数调用与函数体代码之间的关联需要推迟到运行阶段才能确定,就是动态联编:在继承的类体中,基类定义虚函数,派生类中可各自定义虚函数的不同实现版本,当程序中用基类指针或引用调用该虚函数时,将引发动态联编,系统会在运行阶段根据基类指针具体指向的对象调用该对象所属类的虚函数版本,从而实现运行时的多态。
2. 类指针的关系
在继承的类层次中,通过基类定义出一种派生类,所以派生类是属于基类的一种子类型,在公有继承时,派生类保留了基类中除构造函数、析构函数之外的所有成员,所以基类的接口也存在于派生类中,由此,C++中基类和派生类对象及指针具有关系:派生类对象可以赋值给基类对象(引用),可用派生类对象的地址赋给基类类型的指针,即基类的指针可以指向派生类的对象。
这种从派生类到基类的类型转换也称为向上类型转换,向上类型转换会失去派生类中新增的成员,但却是一种安全的类型转换。所以指向基类的指针也可以用来指向派生类对象,反之不行。基类指针可指向派生类对象,但不能访向派生类中新增的成员函数(可通过强制类型转换,将指针转换成派生类指针;同理也可将基类对象强制转换为派生类对象,实现用派生类指针),如果要打破此限制,就要定义虚函数,实现动态联编。
3. 虚函数及其特性
虚函数是指一个在基类中被声明为virtual
并在一个或多个派生类中被重新定义的函数。虚函数(也可称为virtual函数)的特别之处在于用一个指向派生类对象的基类指针(或引用)访问虚函数时,C++可以根据指向的对象类型确定在运行时调用哪个函数。所以当指向不同的对象时,将执行该虚函数在不同派生类中的实现版本。一个虚函数的定义是通过在基类的成员函数前面加上关键字virtual进行声明的,当一个派生类重新定义一个virtual函数时,关键字virtual可以者略。
虚函数的访向特性。其是类的成员函数,所以遵循类成员的访向规则,当通过对象名.
调用虚函数时,则在编译阶段根据调用对象的类型确定虚函数版本,属于静态联编多态。通过基类指针、引用(基类名* 变量名
或基类名& 变量名
)调用虚函数时,是在程序运行阶段根据具体指向或引用的对象,调用该对象所属类的虚函数实现版本,实现动态联编。
虚函数使用说明:虚函数必是类的成员函数不能是全局(非成员)函数和静态成员函数。在一个派生类中重定义一个虚函数时,必须与基类的虚函数原型相同,否则其虚特性将丢失而成为普通的重载函数。当覆盖一个虚函数时,参数列表必须相同,且返回类型也须相同。用虚函数实现运行时多态性的关键之处必须是用基类的指针(或引用)调用虚函数。一旦一个函数被说明为virtual,其将保virtual特性,而不管其经历了多少层派生。
4. 虚析构函数
虚析构函数,在析构函数前面加上关键字virtual进行声明。当用基类指针指向一个动态申请的派生类对象时,就需要应用虚析构函数正确释放派生类对象。与之对应,不允许定义虚构造函数,在建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数,不能选择地调用构造函数,所以虚构造函数会出现语法错误。当基类的析构函数被声明为虚函数时,其派生类的析构函数自动成为虚析构函数,可以省略virtual声明。一般来说,如果一个类中定义了虚函数,析构函数也应该定义为虚析构函数。
5. 纯虚函数与抽象类
许多情况下,在基类中不能给出有意义的虚函数定义,这时可以把它说明成纯虚函数,即不需要具体定义函数的函数体,而把它的定义留给派生类来实现。形式:virtual 返回类型 函数名(参数表) = 0;
。纯虚函数是一个在基类中说明的虚函数,它在基类中没有定义,而要求派生类根据需要定义自己的实现版本。通过纯虚函数,基类为各派生类提供一个公共提口。从基类继承来的纯虚函数,在派生类中仍是纯虚函数,若派生类中没有实现,则仍然是纯虚函数。
抽象类,即含有纯虚函数的类,不能生成对象(可以有基类指针)。抽象类的主要作用是将有关的类组织在一个继承层次结构中,由它来为它们提供一个公共的根,相关的子类是从这根派生出来的。抽象类刻画了一组子类的操作接口的通用语义,这些语义也传给子类。一般而言,抽象类只描述这组子类共同的操作接口,而完整的完现留给子类。抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立具体对象的类了。
(十一)运算符函数重载
C++中预定义的运算符只能对基本数据类型实现运算,而不适用于用户自定义类型(结构,类)。通过重载运算符函数,为自定义类型定义运算符重载函数,就可以应用已有运算符对自定义类型进行运算。运算符重载是通过函数重载实现的,系统在编译时可根据操作数的不同自动区分调用不同的运算符重载函数。
引例:表达式1+2
和1.5+3.4
,编译器在处理它们时,并不需要知道符号+
表示什么意思,而是将表达式解释成以下函数的调用形式:operator+(1, 2)
和 operator+(1.5, 3.4)
。此处函数名为operator+
,为运算符重载函数。编译系统根据参数类型的不同调用该函数的不同重载版本。在此operator
是关键字,它经常和C++中的一个运算符联用,表示一个运算符函数名,也称为运算符重载函数。
运算符重载的实质就是函数重载。在实现过程中,首先把指定的运算符表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参,然后根据实参的数据类型(包括自实义类型)来确定要调用的函数,这个过程是在系统编译阶段完成的,运算符重载应遵循一定的规则,如:不能改变运算符操作数的个数,不能改变原有的优先级、结合性、语法结构,并且除了=
外重载运算符可以为任向派生类所继承。不可重载运算符包括.
、.*
、::
、?:
和sizeof
,其余都可重载(如new,delete等)。
为了能够访问类的私有成员,就需要将运算等重载函数声明了成类的友元函数或成员函数,一般形式,友元:friend 返回类型 operator#(参数表) { // 函数体 }
,成员:返回类型 opentor#(参数表) { // 函数体 }
。其中#
表示某种运算符。两者区别:成员函数拥有一个隐式指针this,而友元函数则没有,因此对成员函数对表达式:x#y
和x#
(或#x
),编译器解译为x.operator(y)
和x.operator#()
;而友元函数编译器解译为operator#(x, y)
和operator#(x)
。即成员函数时,运算符的一个操作数(左操作数)是通过this指针传递的,因此函数的参数比实际操作数少一个,但此时要求左操作数必须是运算符重载函数所属类的一个对象(或者是该类对象的引用)。说明:=
、()
、[]
、->
不能重载为类的友元函数。一般情况下,单目运算符最好重载为类的成员函数,双目运算符重载为类的友元函数。类型转换函数只能定义为成员函数。若一个运算符的操作需要修改对象的状态,推荐成员函数。若运算符所需的操作数(尤其是第一个操作数)希望进行隐式类型转换,则只能选用友元函数。C++规定前置运算符一元运算符重载,规则同前面一样;而后置运算符则按双月运算符重载,重载函数添加一个int形参(一般被传递0值),用于区分是后置运算符,无其他意义。赋值运算符只能重载为成员函数,避免了出现左值为常量的情况,用于类成员有损针类型时,实现深复制(用this指针)。以递归方式返回值,可以连续使用运算符。
四、模板
将数据类型定义为参数,使得同一段代码可以处理不同类型的对象,编译器会根据实参类型生成相应的代码,实现参数的多态化。分为模板函数和模板类。将相似的函数归为函数族,相似的类归为类族,就是模板编程也称泛型编程。
一般地,函数模板的定义形式如下:template<类型形参表> 返回类型 函数名(形参表) { // 函数体 }
。每个类型参数用class或typename说明,模板说明的类型参数必须在函数定义中至少出现一次,函数的形参表可使用模板类型参数,也可以使用一般类型参数。同一般函数一样,使用前先声明。函数模板中的类型参数可实例化为各种类型,但同一个参数必须采用一致的数据类型,模板类型不具有隐式类型转换。函数模板中使用用户自定义类型的参数时,需在类内进行运算符重载,以实现对自定义类型的运算支持。
类模板在表示如数组、表、图等数据结构时显得特别重要,这些数据结构的表示和算法不受所包含的元素类型的影响。类的模板定义:template<类型形参表> class 类名 { // 类体 };
,类模板的成员函数是函数模板,在类外实定义时应按函数模板定义:template<类型形参表> 返回类型 类名<类的类型形参表>::成员函数名(形参表) { // 函数体 };
。类模板的实例化一般形式:类名<类型实参> 对象名;
。
标准模板库(STL)。容器(container)是以类模板的方式定义的常用数据结构,头文件有<vector>
、<list>
、<deque>
、<set>
、<map>
、<stack>
、<queue>
等。算法本身是一种函数模板,适用于不同的容器类型,也称泛型算法,头文件<algorithm>
、<numeric>
、<functional>
等。迭代器,为了将容器和算法联系起来,需要一种指向容器中元素的指针,它的使用方法类似于指针,一般称为泛型指针或抽象指针,头文件<iterator>
、<utility>
和<memory>
组成。选代器是为了方便遍历容器中内容。
五、异常处理的实现
C++异常处理机制是由检查try
、抛出throw
和捕获catch
三个部分组成。一般格式:try { // 被检查的语句块 } catch(异常1) { // 处理 } catch (异常2) { // 处理 }
,说明:try语句是一个复合语句块,标识了有可产生异常的语句,也称为保护段,并且根据异常的情况使用不同的throw表达式抛出异常。throw一般形式:throw 对象或表达式;
,表达式的值不能用来区别不同异常,而是根据其数据类型,匹配与之类型相同的catch并处理。catch语句紧跟在try语句块之后,可创建若干catch块,catch块按其出现顺序被检查,只要找到一个匹配的异常类型,后面的catch块将被忽略。若用catch(...)
则表示捕获所有类型的异常。try和catch块中必须要用花括号,即使只有一个语句。C++中一旦执出一个异常,如果程序没有任何捕获,那么系统将会自动调用一个系统函数terminate()
,由它调用abort()
终止程序。若执行完划中的语句,没有引发任何异常,则程序会跳过try后面的catch语句块。C++中标准异常基类为exception
。
六、输入输出流
输入流(外部设备到内存),输出流(内存到外部设备)。C++流类库是一个应用继承实现的类体系,主要包含两个并行的流类结构:一个是以
streambuf类
为基类的类层次,另一个是以ios类
为基类的类层次。
(一)标准I/O流对象
cerr
在任何情况下,指定标准错误输出设备总是显示终端,不能重定向。cerr流用来输出错误信息,因其不经过缓冲区,所以cerr的每个流插入将导致输出立即显示。clog
与cerr相似,它是基于缓冲区的。
流对象cin
使用提取运算符>>
可以从cin输入流中读取数据存入指定内存。输入流中提取运算是以空格作为数据分隔的(空格、Tab键或换行符),在提取时把遇到的数据后面的分隔符作为数据的结束。输入的数据应与变量的数据类型相匹配,这是因为提取运算符除了检查是否有空白分隔,还会自动根据变量的类型分隔数据的输入。通过输入流的状态值,可以判断流读取数据是否成功,当提取数据正确时,cin流返回true值,当数据类型不心配、遇到无效字符或文件结束符(文件中的数据已读完)时,cin流就处于出错状态,返回false,此时无法正常提取数据,对cin流的所有提取操作将终止。数据输入是基于输入缓冲区的,当一次键盘输入结束时(按回车键)才将输入的数据存入缓冲区,提取运算符>>是从缓冲区中读取数据的。
(二)重载插入/提取运算符
在C++的流类中仅仅重载了针对基本类型的插入<<
、提取>>
运算符,也就是只能完成对C++内部预定义数据类型的I/O操作。为使提取和插入运算符能够对用户自定义的数据类型进行输入输出必须在程序中重载 << 和 >> 运算符。重载插入运算符 operator<<() 的一般格式: ostream& operator<<(ostream& out, const UserType& obj) { out << obj.something; return out; }
。插入运算符可以理解为一个双目运算符,其左操作数是输出流对象,右操作数是输出的数据。cout<< obj 被解释为operator<<(cout, obj) 。函数的返回值是一个对类ostream的引用,这样可以支持连续使用多个插入运算符输出多个数据,且重载函数内的流对象应该使用引用参数,实现在任何情况下都能使用该函数。重载插入运算符函数不能是类的成员函数,只能是一般函数或类的友元函数,因为重载函数 operator<<() 的左操作数必须是一个流对象,不能是自定义类的对象(隐式this指针)。同时,为了能句多在重载运算符函数中直接引用类内私有成员,通常将重载函数声明为类的友元函数。重载提取运算符 operator>>() 的一般形式:istream& operator>>(istream& in, UserType& obj) { in >> obj.something; return in; }
。提取与插入运算符类似,其返回值是istream类型的引用,支持连续提取操作,连续数据之间应用空格、Tab、换行分隔。但要注意的是,提取运算符的第二个参数必须是自定义类型对象的引用,通过引用参数将输入的数据传递给实参对象,否则实参对象不能得到输入的数据值。
(三)输入输出流的成员函数(用对象cin、cout调用)
1. put()函数
ostream& put(char ch)
,将字符ch插入输出流中,可输出单个字符,可连读调用如:cout.put('o').put('k').put('\n')
。
2. get()函数
-
int get()
,不带参数,从流中提取字符(包括空格)并作为函数的返回值,遇到文件结束时返回系统常量EOF。 -
istream& get(char& rch)
,从流中提取字符(包括空格)并写入rch引用的对象,遇到文件结束时返回0,否则返回istream对象的引用。 -
istream& get(char* pch, int nCount, char delim = '\n')
,把读取的字符串写入数组pch,当读取 n-1 个或读到终止字符时结束(不提取终止字符),并在最后添加结束标记'\0'
。
总结:get()函数可以读取包括空格的数据,读取成功返回非0值,否则(遇到文件结束符EOF)返回0值。
3. getline()函数
原型:istream& getline (char* pch, int nCount, char delim = '\n')
,从流中提取一行字符串,用于输入一个字符串,当读取 n-1 个字符或读到终止符时结束,与get()类似。
4. write()函数
原型:ostream& write(const char* s, int nCount)
,将s中的nCount字节序列插入输出流中,并不进行任何格式转换,主要用于非格式化的二进制数据块的写出。
5. ignore()函数
原型:istream& ignore(streamsize num = 1, int delim = EOF)
,跳过输入流中num个字符,或在遇到指定的终止符delim(默认EOF)时提前结束(此时跳过包括终止符在内的若干字符)。表示从当前指针(不含当前)开始,忽略后面cin流的num个字符,或遇到字符delim为止(delim会被跳过)。
(四)流格式控制
与C中的scanf等对应,C++流类提供了用于执行格式化输入输出的成员函数(ios类)和流操纵算子(操纵符,manipulator,特殊函数)。
设置格式符流对象.setf(ios::标志字)
;清除格式符流对象.unsetf(ios::标志字)
;设置域宽数流对象.width(int n)
只对后面的第一个输出有效;设置填充字符流对象.fill(char ch)
不满宽度用ch填充,默认为空格;设置精度流对象.precision(int n)
可用ios::fixed固定小数位。
操纵算子,它可以作为插入和提取运算符的有操作数,直接包含在I/O语句中,头文件为<iomanip>
。标准操作算子如表:
操纵算子 | 用途 | 适用流对象 |
---|---|---|
dec | 设置整数的基数为十进制 | I/O |
hex | 设置整数的基数为十六进制 | I/O |
oct | 设置整数的基数为八进制 | I/O |
endl | 输出一行新字符并刷新流 | O |
ends | 输出一个空字符 | O |
flush | 刷新一个流 | O |
ws | 跳过引导空格符 | I/O |
setiosflags(long flags) | 设置flags指定的标识 | I/O |
resetiosflags(long flags) | 清除flags指定的标识 | I/O |
setbase(int base) | 设置转换基数为base | I/O |
setfill(int ch) | 设置填充字符ch | I/O |
setprecision(int p) | 设置精度,小数点后位数p | I/O |
setw(int w) | 设置域宽w | I/O |
flags的格式标志字是在ios类中定义的枚举值,如下:skipw
跳过输入空白、left
左对齐、right
右对齐、internal
、dec
、hex
、oct
、showbase
、showpoint
、uppercase
十六进制A~F大写、showpos
正数前有+符号、scientific
科学计数法、fixed
定点形式表示浮点数、unitbuf
插入操作后刷新流缓冲区、stdio
插入操作后刷新stdout和stderr。
(五)文件的输入输出
C++中提供的用于文件操作的输出文件流ofstream
、输入文件流ifstream
和输入输出文件流在头文件<fstream.h>
中定义。打开文件就是将一个文件与指定的流对象建立关联,然后就可以通过流对象对文件进行读写操作。定义文件流对象的方法如下:类名 对象名;
或类名 对象名(文件名, 打开方式, 属性)
。若无参则调用无参构造函数,不与任何文件关联,此时需要用open()成员函数打开要关联的文件:对象名.open(文件名, 打开方式, 属性);
,open()返回值为void,若指定多种打开方式,可用按位或运算符 | 进行组合,如:ios::in | ios::out
。打开方式如下:
标志字 | 说明 |
---|---|
ios::app | 向文件输出的内容都添加到文件尾,seek()失效 |
ios::ate | 文件打开时将文件指针定于文件末尾 |
ios::in | 打开一个文件用于读(已存在文件) |
ios::out | 打开一个文件用于写,若不存在先创建,若存在先清除 |
ios::trunc | 如果文件已存在,将长度截为0,并清除原有内容 |
ios::nocreate | 如果文件不存在,则打开操作失败 |
ios::noreplace | 如果文件存在,除非设成ios::app或ios::ate,否则打开操作失败 |
ios::binary | 操作以二进制方式打开;默认为文本方式 |
open()函数是ofstream、ifstream、fstream流类的公有成员函数,可以通过流对象直接调用。ifstream只能用于文件输入,默认ios::in打开方式;ofstream对象只能用于文件输出,默认打开方式ios::out。属性确定如何访向文件,表示如下:1为一般文件、2为只读文件、3为隐含文件、4为系统文件、5为档案位设置,默认值为0。使用带参构造函数的方法打开文件(带默认参数值)。如下(mode,prot都有默认值,可省略):
ofstream::ofstream(const char* filename, int mode = ios::out, int prot = filebuf::openprot);
ifstream::ifstream(const char* filename, int mode = ios::in, int prot = filebuf::openprot);
fstream::fstream(const char* filename, int mode = ios::out | ios::in, int prot = filebuf::openprot);
若由于某种原因文件不能打开,则相应的流对象返回0。关闭文件就是取消关联,用成员函数close()
完成。注:关闭文件时,系统会先将缓冲区中未来得及写出的数据先写到文件中再关闭连接,防止数据丢失。
文件的默认打开方式是文本方式。对一个文本文件进行读写可以直接使用插入 << 和提取 >> 运算符。从文件流中提取 >> 数据也是以空格,回车键为结束符的。文本文件也可以使用流成员函数实现读写操作,put()、get()、getline()、write()、ignore() 等。注:每打开一个文件都有一个文件指针,该指针的初始位置由I/O方式指定,每次读写都从文件指针的当前位置开始。每读入一个字节,指针就后移一个字节。当文件指针移到最后,就会遇到文件结束EOF(文件结束也占一个字节,其值为 -1),此时该对象的成员函数eof()
的值为非0值(一般为1),表示文件结束。(先读写再判断是否EOF。)
二进制文件,打开时需要用ios::binary指定文件的打开方式。(文本文件读写时涉及字符的转换,而二进制文件是直接内存中的内容)。顺序读写文件,流成员函数 istream& get(char& ch)
和ostream& put(char ch)
。当流对象到达文件尾时,返回0值。流成员函数read()
和write()
读写二进制数据块,istream& read(unsigned char* buf, int num)
和ostream& write(const unsinged char& buf, int num)
。read()函数从相关流中读入num个字节,并把它们放入buf指示的缓冲区;write()函数是把buf指示的缓冲区中num个字节写入相关的流中。注:read()和write()函数的第一参数是字符型指针,因而对一个非字符型指针的缓冲区操作时必须进行类型转换(char*)
,第二个参数常为sizeof(buf)
。
随机读写文件,对C++的文件进行读写时使用文件指针表示数据的位置,文件指针分为读指针和写指针。文件打开时,读写指针都指向文件的起始位置。C++流类中定义了操作文件指针的函数,如下:
ifstream& ifstream::seekg(long pos); // 读指针从流的起始位置向后移动pos字节(byte)
ifstream& ifstream::seekg(long off, ios::seek_dir); // 读指针从dir位置移动off字节
ifstream& ifstream::tellg(); // 返回读指针当前所指位置
ofstream& ofstream::seekp(long pos); // 写指针从流的起始位置向后移动pos字节(byte)
ofstream& ofstream::seekp(long off, ios::seek_dir); // 写指针从dir位置移动off字节
ofstream& ofstream::tellp(); // 返回写指针当前所指位置
其中,文件的指针位置和移动守节都是整型值,seek_dir位置表示参照位置,有三个枚举常量表示:ios::beg
开始位置、ios::cur
当前位置、ios::end
结束位置。注:seekg() 与 seekp() 函数的第二个参数seek_dir可以省略,此6时默认为ios::beg。
七、命名空间
基本形式:namespace 空间名 { }
,在命名空间中定文的任东西都局限于该命名空间内,从而避免命名冲突。在空间外可用域运算符 :: 使用空间中成员,也可使用using关键字导入整个空间,using namespace 空间名;
,也可以选择性导入,using namespace 空间名.成员;
。不要在头文件中使用using,否则每次包含头文件都会导入空间,则空间就变得无意义了。
八、Qt Creator集成开发环境
Qt Creator提供了一个集源程序代码编辑、编译、调试和运行于一体的可视化开发环境,它包含本文编辑器、源代码浏览器、资源编辑器、代码调式和工程编译等工具,以及联机帮助文档。