C++面向对象编程

Yizumi Konata Lv3

第一章 C++引论

程序设计语言

image-20240605231831993

面向过程程序设计程序基本单元是函数,面向对象程序设计程序基本单元是类。

程序编译技术

编译过程包括:

  • 预处理:替换预处理指令#define #include
  • 词法分析:产生单词序列(token)
  • 语法分析:简单分析语法,如分号,括号的配对,if else配对等
  • 代码生成:生成低级语言代码如机器语言或汇编语言。标识符在生成低级代码会进行换名,C和C++换名策略不同
  • 模块连接:将中间代码和标准库、非标准库连接起来,形成一个可执行的程序。静态连接lib库与动态连接dll库

面向对象的程序设计语言

  • 纯OO型语言:程序全部由类构成:SMALLTALK、JAVA、 C#、OBJECT-ORIENTED PASCAL
  • 混合型OO语言:程序由类、全局过程或函数以及全局变量定义构成:C++

面向对象的基本概念

  • 函数绑定:找到函数入口地址的过程。其中早期绑定发生在程序开始执行前,由编译程序或者操作系统静态或动态连接完成;晚期绑定发生在程序运行过程中,由程序自己完成
  • 对象:抽象出来的数据对象,描述一个问题中的一件事物
  • 类:描述对象共同特征和行为的数据类型。
  • 封装:将对象的数据结构和算法包装在一起,描述对象的组织结构和功能,提供外部访问接口但是屏蔽对象的行为细节
  • 交互:直接交互指一对象调用另一对象的“操作”、“功能”或“函数”; 间接交互通过发送或监听消息完成。
  • 重载:用一个函数名称来定义完成不同功能的多个函数。重载函数要么参数个数不同,要么参数类型不同,或者都不同
  • 多态:通过一个函数名能表现出不同行为。重载函数行为发生在早期绑定,属于静态多态;虚函数行为发生在晚期绑定,属于动态多态。多态一般指动态多态。多态增强了程序的泛用性。
  • 继承:一个对象获得另一个或多个对象的“特征”和“行为”, 从而实现了软件重用。
  • 抽象:从事物到概念,从低级概念到高级概念
  • 抽象类:抽象级别最高的类,无法描述具体特征和行为。

C++语言的特点

  • 兼容C,代码质量高,速度快
  • 多继承,强类型,混合型面向对象
  • 支持面向对象的运算符重载
  • 提供函数模板和类模板等高级抽象机制
  • 支持面向对象的异常处理
  • 支持名字空间

C++由三种头文件,一种是老式的C头文件,与C的头文件一致;一种是C++新的头文件,没有.h后缀;一种是老式C头文件的封装,将老式头文件的.h后缀删去,在前面加上c,与老式头文件的区别在于有名字空间。

第二章 类型、常量和变量

C++的单词

单词包括标识符和保留字。保留字不可用做自定义标识符。保留字表如下

保留字表

基础类型变量及其类型解析

内置数据类型

数据类型告诉我们数据在内存中的表示方式。C++的内置数据类型如下表

类型含义最小尺寸
bool布尔类型未定义
char字符8位
wchat_t宽字符16位
char16_tUnicode字符16位
char32_tUnicode字符32位
short短整型16位
int整型16位
long长整型32位
long long长整型64位
float单精度浮点数6位有效数字
double双精度浮点数10位有效数字
long double扩展精度浮点数10位有效数字

注:每种数据类型的字节数与硬件、操作系统、编译有关。默认一般整数常量当作为int类型,浮点常量当作double类型。

预定义类型的格式化输入输出格式与C一致,不做赘述

C++在使用变量前要先对变量进行定义或声明

  • 变量声明:描述变量的类型及名称,没有初始化,声明是针对已经定义过的变量进行的,可以声明多次
  • 变量定义:描述变量的类型及名称,为变量分配内存空间并初始化,只能定义一次

声明变量时需要用到关键字extern,且不能初始化(若初始化则定义一个变量)

C++提供了多种初始化变量的方法,如下

1
2
3
4
int a = 0; //C风格
int b={0};
int c(0);
int d{0}; //列表初始化,在存在精度损失时会报告编译错误,相对安全

如果定义变量时没有指定初始值,则变量呗赋予默认值,有一下几种情况

  • 内置类型全局变量和局部静态变量初始化为0
  • 函数内部的内置类型局部变量的值未确定,直接访问会报错
  • 类对象取决于构造函数的定义

关键字static,const,constexpr,volatile,inline

  • static用以定义静态变量,分为模块静态变量和局部静态变量。模块静态变量在函数外部定义,作用域为当前文件,在函数内部可以用::访问。局部静态变量在函数中定义,作用域为该函数,但生命周期为整个程序。
  • const或constexpr说明或定义的变量,定义时必须同时初始化。当前程序只能读不能修改其值。其中constexpr变量必须用编译时可计算表达式( 常量表达式)初始化。
  • volatile说明或定义的变量,可以后初始化。其值不一定非要由当前程序修改,可由其他程序修改。
  • 保留字inline用于定义函数外部变量或函数外部静态变量、类内部的静态数据成员。inline函数外部变量的作用域和inline函数外部静态变量一样,都是局限于当前代码文件的,相当于默认加了static。

指针类型变量

主体知识与C中一样。需要强化理解的是如何识别复合类型指针。

指针类型的变量使用说明和定义的,即 *后面的才是描述指针的内存类型的,而*前面的部分描述的是指针所指向的数据的基本类型以及内存类型的。

指针使用注意事项

  • 只读单元的指针(地址)不能赋给指向可写单元的指针变量,而可写单元的指针(地址)能赋给指向只读单元的指针变量。
  • 父类指针可以指向子类对象
  • 指针初始化三种方法:
1
2
3
int *p1=nullptr;//推荐
int *p2=0;
int *p3=NULL//需要先#include<cstdlib>

引用类型变量

引用为变量起了一个别名,引用类型变量的声明符用&来修饰变量名(指左值引用)。引用类型变量必须马上初始化。

在C++中,左值指的是可以寻址的值,右值指的是不能寻址的值。左值持久、右值短暂。左值具有持久的状态(取决于对象的生命周期),而右值要么是字面量,要么是在表达式求值过程中创建的临时对象。注意,const类型的左值引用可以引用右值,如直接const int &w=4;此时相当于为4分配了一块内存空间。

引用的本质是常量指针。但是引用不分配物理内存,因此无法定义引用的有一定以及引用类型的数组,且不能被重新赋值。同时由于计算机没有按位编址,因此不能引用位段成员。

引用初始化时,除了二种例外情况,引用类型都要与绑定的对象严格匹配,除了const类型引用和父类引用绑定子类(与指针类似)

右值引用用&&修饰变量名,必须引用右值。右值引用可以进行赋值,如int &&a = 1;a = 2; 是合法的但是右值引用使用的是右值对象的缓存,自己并不分配内存,这一点的性质与左值引用一致。但是右值引用可以引用位段成员,因为位段成员无址。

数组,结构,联合

没啥好说的。。。。

第三章 语句,函数及程序设计

C++的语句

与C一致,需要了解的是asm语句和static assert静态断言。asm语句可以在C或C++程序中插入汇编代码。static assert使用格式为static assert(条件表达式," 输出信息"),不满足条件则编译报错并抛出报错信息。

C++的函数

基本知识与C一致。

函数可说明或定义为四种作用域:

  • 全局函数(默认)
  • 内联即inline函数:可在程序文件内或类内说明或定义,只能被当前程序文件的程序调用。它是局部文件作用域的,可被编译优化掉
  • 外部即extern函数:声明外部函数用
  • 静态即static函数:可在程序文件内或类内说明或定义。类内的静态函数不是局部文件作用域的,程序文件内的静态函数是局部文件作用域的。

函数可以定义省略参数,用... 表示可以接受0至任意个任意类型的参数。通常须提供一个参数表示省略了多少个实参。由于省略参数连续存放,故可以通过指针移动访问。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
long sum(int n, ...) //n为省略参数的个数
{
long s = 0; int* p = &n + 1; //p指向第1个省略参数
//若省略参数为其他类型,需要声明为相应类型的指针并对n的地址进行强制类型转换
for (int k = 0; k < n; k++) s += p[k];
return s;
}
void main( )
{
int a = 4;
long s = sum(3, a, 2, 3); //执行完后s=9
}

函数可以定义默认参数,调用时若未传实参则用默认值。默认值所用的表达式不能出现同参数表的参数,否则可能会因为传递参数的顺序的问题造成错误。所有默认值必须出现在参数表的右边。

内联inline函数调用会被编译进行优化,即直接将其函数体插入到调用处,而不是编译为call指令,这样可以减少调用开销,提高程序执行效率。若函数为虚函数、或包含分支或取过函数地址,或调用时未见函数体(函数体定义在调用处后面),则内联失败。失败不代表程序有错,只是被编译为函数调用指令。

重载函数:通过参数差异识别重载函数,即若参数的个数或者类型有所不同(至少一个参数类型不一致),则同名的函数被自动视为重载函数。重载只与参数类型有关,与返回类型无关,若参数个数和类型完全相同、仅仅返回类型不同是不允许的。当使用缺省参数和省略号时,函数重载容易引起歧义,会使编译器报错

作用域

  • 程序可由若干代码文件(.cpp)构成,整个程序为全局作用域全局变量和函数**属于此作用域。
  • 稍小的作用域是代码当前文件作用域:函数外的static变量和函数属此作用域。
  • 更小的作用域是函数体函数局部变量和函数参数属于此作用域。
  • 在函数体内又有更小的复合语句块作用域
  • 最小的作用域是数值表达式:常量在此作用域。

除全局作用域外,同层不同作用域可以定义同名的常量、变量、函数。但他们为不同的实体。如果变量和常量是对象,则进入面向对象的作用域。同名变量、函数的作用域越小、被访问的优先级越高。

生命周期

  • 常量的生命期即其所在表达式。
  • 函数参数或自动变量的生命期当退出其作用域时结束。
  • 静态变量的生命期从其被运行到的位置开始,直到整个程序结束。
  • 全局变量的生命期从其初始化位置开始,直到整个程序结束。
  • 通过new产生的对象如果不delete,则永远生存(内存泄漏)。
  • 外层作用域变量不要引用内层作用域自动变量(包括函数参数),否则导致变量的值不确定:因为内存变量的生命已经结束(内存已做他用)

第四章 C++的类

类的声明和定义

classstructunion可用来声明和定义类。类的声明直接使用class 类型名;即可,而类的定义则需要给出其成员的声明或定义,类的实现则需要即定义类的函数成员的函数体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Circle
{
private:
double radius;//数据成员
public:
Circle();//构造函数声明
Circle(double r);//构造函数声明
double findarea();//成员函数声明
};//类的定义,注意分号

//类的实现
Circle::Circle()//构造函数定义
{
radius=1.0;
}

Circle::Circle(double r)//构造函数定义
{
radius=r;
}
doucle Circle::findarea()//成员函数定义
{
return radius*radius*3.14;
}

定义类时应注意的问题

  • 使用private、protected和public保留字标识主体中每一区间的访问权限,同一保留字可以多次出现。
  • 成员在类定义体中出现的顺序可以任意,函数成员的实现既可以放在类的外面,也可以内嵌在类定义体中(此时会自动成为内联函数);但是数据成员的声明定义顺序与初始化顺序有关(先声明定义的先初始化)。
  • 同一区间内可以有数据成员、函数成员和类型成员,习惯上按类型成员、数据成员和函数成员分开。
  • 类的定义体花括号后要有分号作为定义体结束标志。
  • 在类定义体中允许对数据成员定义默认值,若在构造函数的参数列表后面的和函数体的{之间对其进行了初始化(在成员初始化列表进行初始化),则默认值无效,否则用默认值初始化;
  • 构造函数和析构函数都不能定义返回类型。
  • 如果类没有自定义的构造函数和析构函数,则C++为类生成默认的构造函数(不用给实参的构造函数)和析构函数。

构造函数用来产生对象,为对象申请资源,初始化数据成员。构造函数是函数名与类名相同的函数成员,可以在参数表中显式定义参数,通过参数变化实现重载。定义变量或者其生命期开始时自动调用构造函数,由于一个对象仅自动构造一次,故构造函数不能被显式调用。

析构函数是用来毁灭对象的,释放对象申请的所有资源。析构函数的函数名与类名相同且带波浪线,参数表无参,无法重载。变量的生命期结束时会自动调用析构函数,析构函数可以显式调用,大师应当防止反复析构(自动变量不要显式调用析构函数,系统会自动调用,若析构函数没有防止反复释放资源显式调用可能会引起程序错误)

不同对象的析构与构造:

  • 全局对象:由开工函数构造,收工函数析构,储存在数据段里
  • 静态局部变量:定义时自动构造,main函数执行完后由收工函数析构,储存在数据段里
  • 局部自动变量:定义时自动构造,函数返回时自动析构,储存在堆栈里
  • new:调用new时自动构造,需要手动调用delete,储存在堆里

程序不同结束形式对对象的影响:

  • exit:静态和全局变量会在main函数exit时由收工函数析构,其他变量都不会自动执行析构函数
  • abort:所有对象都不会被析构
  • return:除new出来的对象以外所有的对象都会按照既定方式自动调用析构函数。

成员访问权限及对成员的访问

  • private:私有成员,本类函数成员可以访问;派生类函数成员、其他函数成员和普通成员都不能访问
  • protected:保护成员:本类和派生类的成员函数可以访问,其他类成员和普通函数都不能访问
  • public:共有成员,任何函数均可访问

注意:

  1. 类的友元可以访问类的所有成员函数
  2. 构造函数和析构函数可以定义为任何访问权限
  3. class定义的类中,缺省访问权限为privatestructunion定义的类冲缺省的访问权限为public

内联,匿名类和位段

在类体内定义的任何函数都会自动内联。当然用inline修饰的函数也会内联。匿名类(即类定义时没有名字)的函数成员只能在类体内定义(内联),函数的局部类成员也只能在类体内定义(内联)

没有对象的全局匿名联合必须定义为static,局部的匿名联合不能定义为static,匿名联合只能定义公有数据成员,vi是相当与定义了几个变量,但是这些变量公用存储空间。

位段的用法和C一样

new和delete

C语言的内存管理使用mallocfree函数,C++使用newdelete运算符

new:用法为new 类型表达式{初始值},其中初始值可以用圆括号,也可以省略,在为数组分配内存时第一维下标可以为任意表达式,其他必须为常量表达式。 new的底层依然是调用malloc函数,因此对简单对象newmalloc没什么不同,与malloc不同的是new还会调用类的构造函数。

delete:用法为delete 指针或者delete[] 数组指针前者只能用于销毁单个实体,后者可以用于销毁对象数组,其底层为先调用析构函数,再调用free

隐藏参数this

this指针是一个特殊的指针,它是普通函数成员(非静态函数成员)隐含的第一个参数,其类型是指向要调用该函数成员的对象的const指针。可以认为this指向这个类,可以用this->调用类的成员。this指针不允许移动

对象的构造与析构

非静态数据成员可以再如下位置初始化

  • 就地初始化,在定义类的数据成员的时候初始化赋值
  • 成员初始化列表初始化
  • 就地初始化与成员初始化列表同时使用时以初始化列表为准
  • 赋值,即在构造函数体内赋值

同样注意 const和引用类型变量定义时必须初始化,且只能就地初始化或者在成员初始化列表中初始化。

若未定义或生成构造函数,则可以用{}的形式初始化,这里和C的结构初始化是一样的。

若类未定义构造函数,编译器会尝试自动生成构造函数,成为合成的默认构造函数,一旦定义了构造函数,编译器生成的构造函数将不会被接受,除非用default

合成的默认构造函数工作结果分为以下几种情况

  • 如果为数据成员提供了就地初始化,则工作成功
  • 如果没有就地初始化
    • 若只包含非const、非应用内置数据类型成员,如果类的对象是全局或局部静态的,则这些成员默认初始化为0;如果类的对象是局部自动的,当程序访问这些成员时编译器报错,如果对象是new出来的,则不会报错,但访问结果是随机值
    • 如果包含const,引用数据成员,其他无默认构造类型的class类型成员,一旦实例化对象,编译器报错

若自定义了构造函数之后仍要求编译器提供合成的默认构造函数,可以使用构造函数名称()=default来使用不带参的默认构造函数。

转换构造函数

如果构造函数只接受一个实参,实际上它定义了转换为此类类型的转换机制,这种构造函数称为转换构造函数。如下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
class Integer
{
int value;
public:
integer(int value):value(value){};
};

int main()
{
Integer i=25;//这里相当于隐式将int转换为Integer
return 0
}

这里的隐式转换的原因就是我们在定义一个对象时由于其构造函数只就收一个变量,故可以用=将其初始化。注意这种转换只能进行一次,比如我们由下面的一个类

1
2
3
4
5
6
class Age
{
Integer age;
public:
Age(Integer i):age(0){}
}

我们无法在主函数中直接Age a = 100

要避免这种隐式转换,可以在构造函数前面加上explicit,这样的话我们便不能用=对变量进行初始化,只能用()

拷贝构造函数

如果class A的构造函数的第一个参数是自身类型引用(const A &A &), 且其它参数都有默认值(或没有其它参数),则此构造函数是拷贝构造函数。用一个已经构造好对象去构造另外一个对象时会调用拷贝构造函数

如果没有为类定义拷贝构造函数,编译器会为我们定义一个合成的默认拷贝构造函数,编译器提供的合成的默认拷贝构造函数原型是ACopyable(const ACopyable &o),在没有自定义构造函数或者用了default之后可以直接使用。编译器提供的合成的默认拷贝构造函数其行为是:按成员依次拷贝。如果用类型A的对象o1拷贝构造对象o2,则依次将对象o1的每个非静态数据成员拷贝给对象o2的对应的非静态数据成员。

浅拷贝

按成员依次拷贝称为浅拷贝。在浅拷贝过程中,若实参对象包含指针类型的实例数据成员,只复制指针值则两个对象的该成员指向同一块内存,可能会造成内存的错误。

深拷贝

在传递参数时先为形参对象的指针成员分配新的存储单元,而后将实参对象的指针成员所指向的单元内容复制到新分配的存储单元中。为了在传递参数时能进行深拷贝,必须自定义参数类型为类的引用的拷贝构造函数,即定义A (A &)A (const A&) 等形式的构造函数。建议使用A (const A&)

如上面转换构造函数中提到的,单参数构造函数可以用=调用,当我们没有自定义=重载时,编译器会提供一个重载,但此时为浅拷贝构造。因此安全的做法是在自定义深拷贝构造函数后还要自己实现=运算符的深拷贝重载

移动构造和移动赋值

当函数的参数为值参以及函数返回类型为值时,都会出现对象的频繁拷贝。在很多情况下,对象被拷贝后就立即销毁(特别是函数返回值时),在这些情况下,采用移动对象会大幅提高性能。所谓移动构造和移动赋值,就是将对象的指针指向实参指向的空间,然后将实参进入安全可析构的状态。

移动构造的本质是浅拷贝,但与浅拷贝不同的是由于实参是马上要被销毁的对象,因此我们可以对其随意更改,所以我们将实参的指针设为空指针,使其析构时不会释放其指向的内存。

如果一个类既有拷贝构造函数,也有移动构造函数,编译器根据参数类型进行匹配来确定使用哪个构造函数,赋值操作类似。因此这时遵循一个重要原则:移动右值,拷贝左值。若没有移动构造/移动赋值函数,右值也被拷贝构造/拷贝赋值。

前面提到过可以用=default来显式要求编译器提供默认版本,还可以用=delete定义为删除

第五章 成员及成员指针

成员指针:指向类的成员(普通和静态成员)的指针,分为实例成员指针和静态成员指针。变量、数据成员、函数参数和返回类型都可定义为成员指针类型, 即普通指针能用的地方成员指针都能用。例如int A::*p即指向A类的int类型的实例数据成员。可将其赋值为p=&A::m

利用指针访问类的成员使用.*或者->*运算符,用法为对象名.*指针名对象指针->*指针名

注意这是指向类的成员的指针,不是指向实例化了的对象的成员的指针

实例成员指针

实例成员指针是指向实例成员的指针,可分为实例数据成员指针和实例函数成员指针。由于构造函数不能被显式调用,故不能有指向构造函数的实例成员指针。

实例成员指针式数据成员相对于对象首地址的偏移,不是真正的地址,所以实例成员指针不能移动,也不能转换类型。

其实很好理解,实例成员指针指向的是类的成员,没有实例化的时候绝对地址没有意义,所以记录的是相对地址。由于数据成员的大小,访问权限类型不一定相同,轻易移动指针很可能会出现问题。类型转换也是,若对指针进行类型转换其实相当于移动了指针

const、volatile和mutable

const只读、volatile易变、mutable机动,前两者可以用来定义变量,类的数据成员、函数成员以及普通函数的参数和返回值,最后一个只能用来定义类的数据成员。含const的数据成员的类必须定义有初始化,volatile mutable 则不一定

关于这三个关键字在修饰类的成员时,应注意以下几点

  • 普通函数成员参数表后出现constvolatile修饰隐含参数this指向的对象
    • 出现const表示this指向的对象的非只读类型的静态数据成员可以修改,但不能修改调用对象的非静态数据成员,但如果该数据成员的存储类为mutable,则该数据成员就可以被修改。
    • 出现volatile,常表示调用该函数成员的对象是挥发对象,这通常意味着存在并发执行的进程。
    • mutable说明数据成员为机动数据成员,该成员不能用const、volatile或static修饰。
  • const volatile对隐含参数的修饰还会影响函数成员的重载
    • 普通对象应调用参数表后不带const和volatile的函数成员;
    • const和volatile对象应分别调用参数表后出现const和volatile的函数成员,否则编译程序会对函数调用发出警告。

静态数据成员

静态成员用static声明, 包括静态数据成员和静态函数成员,static声明只能出现在类内即别在类外实现函数的时候出现inline

const、非inline静态数据成员在类体内声明类体外定义并初始化const inline修饰的静态数据成员可以在类内就地初始化

类的静态数据成员在类还没有实例化对象前就已存在,相当于Java的类变量,用于描述类的总体信息,如对象总数、连接所有对象的链表表头等。访问权限同普通成员。逻辑上,所有对象共享静态数据成员内存,任何对象修改静态数据成员的值,都会同时影响其他对象关于该成员的值。物理上,静态数据成员相当于独立分配内存的变量,不属于任何对象内存的一部分。静态数据成员相当于有类名限定、带访问权限的全局变量

静态函数成员

静态函数成员通常在类内以static说明或定义,没有this参数。因此有this的构造和析构函数、虚函数以及纯虚函数都不能定义为静态函数成员。

由于没有this参数,静态函数成员参数表后不能用constvolatileconst volatile等修饰符修饰。静态函数成员也不能访问对象的非静态成员函数

静态成员指针

静态成员指针是指向类的静态成员的指针,包括静态数据成员指针和静态函数成员指针。

由于静态成员是在没有实例化对象前就已经存在了的,所以是有地址的,因此指向静态成员指针就用普通指针

第六章 继承与构造

继承是C++类型演化的重要机制,在保留原有类的属性和行为的基础上,派生出的新类可以有某种程度的变异。接受成员的新类称为派生类,提供成员的原有类型称为基类。

C++既支持单继承又支持多继承。单继承只能获取一个基类的属性和行为。多继承可获取多个基类的属性和行为。

单继承类

单继承是只有一个基类的继承方式。单继承的定义格式为

1
2
3
4
5
6
class 派生类名:继承方式 基类名
{
派生类新成员定义
派生类重新定义基类同名的数据和函数成员
派生类声明修改基类成员访问权限
}

继承方式

继承方式指明派生类采用什么继承方式从基类获得成员,分为三种:private表示私有继承基类;protected表示保护继承基类;public表示公有继承基类。 三种继承方式派生类对基类成员的访问权限为

  1. 基类私有成员对派生类函数是不可见的。派生类不能访问基类私有成员,除非将派生类的声明为基类的友元类,或者将要访问基类私有成员的派生类函数成员声明为基类的友元。
  2. 公有继承时基类的公有成员和保护成员派生到派生类时都保持原有的状态
  3. 保护继承时基类的公有成员和保护乘员都称为派生类的保护乘员
  4. 私有继承时基类的功有成员和保护成员派生后都作为派生类的私有成员

class声明的类的继承方式缺省为private,用struct声明的继承方式和访问权限缺省为public。用union声明的类既不能作派生类的基类,也不能作任何基类的派生类。

派生类可以通过using 基类名::基类成员或者直接基类名::基类成员来改变访问权限,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A{
int a;
protected:
int b,c;
int bb;
public:
int d,e;
int cc;
~A() {};
};

class B:protected A{
int a;
A::cc; // using A::cc;降低
protected:
int b,f;
public:
int e,g;
using A::d; //恢复
A::bb;//提升
};

标识符的作用范围可分为从小到大四种级别:①作用于函数成员内;②作用于类或者派生类内;③作用于基类内;④作用于虚基类内。 标识符的作用范围越小,被访问到的优先级越高。如果希望访问作用范围更大的标识符,则可以用类名和作用域运算符进行限定。

构造与析构

单继承派生类的构造顺序为:

  1. 调用虚基类的构造函数
  2. 调用基类的构造函数
  3. 按照派生类中数据成员的声明顺序,依次调用数据成员的构造函数或初始化数据成员
  4. 调用派生类的构造函数

析构是构造的逆序。

析构时应该注意:

  1. 如果被引用的对象是new生成的,引用变量必须用delete &r析构
  2. 若被指针指向的对象是new生成的,则指针变量必须用delete p析构,不能用free(p)

父类与子类

如果派生类的继承方式位为public,则这样的派生类称为基类的子类,而相应的基类则称为派生类的父类。

C++允许父类指针直接指向子类对象,也允许父类引用直接引用子类对象。编译程序只能根据类型定义静态地检查语义。由于父类指针可以直接指向子类对象,而到底是指向父类对象还是子类对象只能在运行时确定。编译时,只能把父类指针指向的对象都当作父类对象。因此编译时父类指针访问对象的数据成员或函数成员时不能超越父类为其相应对象规定的访问权限,也不能通过父类指针访问子类新增的成员。

子类指针指向父类对象时要经过强制类型转换。

第七章 可访问性

作用域

作用域:标识符起作用的范围。作用域运算符::既是单目运算符,又是双目运算符。其优先级和结合性与括号相同。单目用于限定全局标识符,双目用于限定类的枚举元素、数据成员、函数成员以及类型成员等。双目运算符还用于限定名字空间成员,以及恢复从基类继承的成员的访问权限。

面向对象的编程中作用域大小前面说过了

名字空间

名字空间是C++引入的一种新作用域,C++名字空间既面向对象又面向过程:除可包含类外,还可包含函数、变量定义。

名字空间必须在全局作用域内用namespace定义,不能在类、函数及函数成员内定义,最外层名字空间名称必须在全局作用域唯一。同一名字空间内的标识符名必须唯一,不同名字空间内的标识符名可以相同。当程序引用多个名字空间的同名成员时,可以用名字空间加作用域运算符::限定。

名字空间(包括匿名名字空间)可以分多次定义(在不同文件里):

  • 可以先在初始定义中定义一部分成员,然后在扩展定义中再定义另一部分成员;
  • 或者先在初始定义中声明的函数原型,然后在扩展定义中再定义函数体;

所谓扩展定义其实和初始定义一样,只是分了几次说而已。

名字空间成员有三种访问方式:

  • 直接访问:名字空间名称::成员名称
  • 引用成员:using 名字空间名称::成员名称,引用时只能给出函数名,不能带函数参数。引用其实相当于extern声明,直接访问则是直接加个前缀就开始用
  • 引用名字空间:using namespace 名字空间名称

using声明以关键字using开头,后面是名字空间成员名, 如果是声明嵌套多层的名字空间里的成员,要多级限定。如果using直接引用了成员,则在当前作用域下不能再定义同名变量;若引用的是整个名字空间,假设在当前作用域下定义了与名字空间中某个成员重名的变量,在未使用同名变量前不会报错,一旦在没有加上限定符的情况下就使用会报错。

嵌套名字空间:名字空间内可定义名字空间,形成多个层次的作用域,引用时多个作用域运算符自左向右结合。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace A {	// A的初始定义     
int x = 5;
int f( ) { return 6; }
namespace B { int y = 8, z = 9; }
using namespace B; }
using A::x; //特定名字空间成员using声明,不能再定义变量x
using A::f; //特定名字空间成员using声明,不能再定义函数f
using namespace A::B; //非特定成员using,可访问A::B::y, A::B::z,还可重新定义
int y = 10; //定义全局变量y
void main(void) {
f( ); //调用A::f( )
A::f( ); //调用A::f( )
::A::f( ); //调用A::f( )
cout<<x+ ::y + z + A::B::y; //同一作用域有两个y,必须区分
}

可以为名字空间定义别名,以代替过长和多层的名字空间名称。对于嵌套定义的名字空间,使用别名可以大大提高程序的可读性。如namespace AB=A::B

匿名名字空间:匿名名字空间的作用域为当前程序文件,名字空间被自动引用,其成员定义不加入当前作用域(面向过程或面向名字空间),即可以在当前作用域定义同名成员。一旦同名冲突,自动引用的匿名名字空间的成员将是不可访问的。 在同一个文件里,匿名名字空间也可分多次定义。

成员友元

类的友元函数是定义在类外部,但有权访问类的所有私有成员和保护成员。**要声明一个函数为一个类的友元,则在该类中用friend声明该函数即可,函数的实现放在类外。

成员友元是一种将一个类的函数成员声明为其它类友元的函数。

如果类A的实例函数成员被声明为类B的成员友元,则这种友元称为实例成员友元。如果类A的静态函数成员被声明为类B的成员友元,则这种友元称为静态成员友元。

如果某类A的所有函数成员都是类B的友元,则可以简单的在B的定义体内用friend A;声明,不必列出A的所有函数成员。此时称类A为类B的友元类。

友元的派生不具有传递性和对称性

普通友元及其注意事项

包括主函数在内任何一个普通函数都可以定义为一个类的普通成员友元。普通友元不是类的函数成员,故普通友元可在类的任何访问权限下定义。一个普通函数可以定义为多个类的普通友元。

第八章 虚函数与多态

虚函数

虚函数是用virtual定义的实例成员函数。当基类对象指针或引用指向或引用不同类型的派生类对象时,通过虚函数到基类或派生类中同名函数的映射实现动态多态。静态多态与动态多态的概念在第一章中有论述。下面是一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//静态多态
class A{
void info() { cout << “A\n”); }
};
class B: public A{
void info() { cout << “B\n”); }
};
class C: public B{
void info() { cout << “C\n”); }
};
//现在编写全局函数,使其能够打印A、B、C类对象的信息,使用函数重载的静态多态实现
void PrintInfo (A* a) { a->info( );}
void PrintInfo (B* b) { b->info( );}
void PrintInfo (C* c) { c->info( );}
int main()
{
A*p=new A();
PrintInfo(p);//此时调用第一个PrintInfo,如果想要调用其他的PrintInfo需要传入不同类型的指针。如果再派生一个子类D出来则需要重新编写一个新的重载函数
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//动态多态
class A{
virtual void info() { cout << “A\n”); }
//首先将info定义为虚函数
};

class B: public A{
virtual void info() { cout << “B\n”); }
};

class C: public B{
virtual void info() { cout << “C\n”); }
};
void PrintInfo (A *p) { p ->info(); } //形参定义为顶级父类指针
int main()
{
A *a = new A( ); B *b = new B( ); C *c = new C( );
PrintInfo (a); //调用A的info,显示A。A * p = a;
PrintInfo (b); //调用B的info,显示B。 A *p = b;
PrintInfo (c); //调用C的info,显示C。 A *p = c;
//此时若派生出新的子类D,PrintInfo函数不需要做任何修改就可以直接用
}

注意:只有基类的指针或者引用才能体现出多态,这里的多态指的指针对基类的对象设计的代码可以应用于子类对象,调用的方法时最具体的类的方法

关于虚函数,有以下几个需要注意的问题

  • 虚函数必须是类的实例成员函数,非类的成员函数不能说明为虚函数,普通函数如main不能说明为虚函数。

  • 虚函数一般在基类的publicprotected部分

  • 在派生类中重新定义成员函数时,函数原型必须完全相同

  • 虚函数只有在具有继承关系的类层次结构中定义才有意义,否则引起额外开销 (需要通过VFT访问)

  • 一般用父类指针(或引用)访问虚函数。根据父类指针所指对象类型的不同,动态绑定相应对象的虚函数

  • 虚函数有隐含的this参数,参数表后可出现constvolatile

  • 构造函数构造对象的类型是确定的,不需根据类型表现出多态性,故不能定义为虚函数;析构函数可通过父类指针(引用)或delete调用,父类指针指向的对象类型可能是不确定的,因此析构函数可定义为虚函数(强烈建议,当然此时函数原型不相同,不过没有问题)。

  • 一旦父类(基类)定义了虚函数,所有派生类中原型相同的非静态成员函数自动成为虚函数(即使没有virtual声明);(虚函数特性的无限传递性)

  • 多态并不是只有父子关系之间有,若类之间的继承关系不是父子关系,其影响只是基类指针不能指向派生类的对象,指针或地址强制类型转换后多态性质依然成立

  • 用父类引用实现动态多态性时需要注意,new产生的被引用对象必须用delete &析构,而不能仅仅调用析构函数或者使用free

抽象类

纯虚函数:指不必定义函数体的函数,可以有重载、缺省参数、省略参数、内联等。定义格式为virtual 函数原型 = 0。纯虚函数也有this指针,不能定义为static。纯虚函数的函数体应在派生类中实现,成为非纯虚函数。

含有纯虚函数的类称为抽象类。抽象类不应该有对象或类的实例。如果抽象类的派生类继承了抽象类的纯虚函数却没有实现,则该派生类依然为抽象类。只有在派生过程中所有纯虚函数都被实现,派生类才会成为非抽象类。

抽象类作为抽象级别最高的类,主要用于定义派生类共有的数据和函数成员。抽象类的纯虚函数没有函数体,意味目前尚无法描述该函数的功能。

第九章 多继承类与虚基类

多继承

多继承派生类有多个基类或虚基类。单继承是多继承的一种特例,多继承具有更强的类型表达能力。多继承派生类继承所有基类的数据成员和函数成员。

基类之间的成员可能同名,基类与派生类的成员也可能同名。在出现同名时,如面向对象的作用域不能解析,可使用基类类名加作用域运算符::来指明要访问基类的成员。

单继承语言在描述多继承的对象时,常常通过对象成员委托(代理)实现多继承。即继承一个类,然后将另一个类的数据对象作为数据成员,作为另一个类的行为的代理。

多继承方式定义派生类

1
2
3
4
class 派生类名:派生方式 基类1,派生方式 基类2
{
类体
}

关于多继承有以下几个需要注意的问题

  • 一个类不能多次成为一个派生类的直接基类(即使是虚基类) 否则会报编译错误
  • 一个类可以多次成为一个派生类的间接基类(即使不是虚基类)
  • 一个类可以同时成为一个派生类的直接基类和间接基类,但该类作为直接基类时必须是虚基类。 否则会报警告错误

直接多继承和委托代理可能存在一个问题,如果两个基类在物理上存在同一个基类,可能对同一个物理对象重复初始化以及重复析构。

虚基类

虚基类用virtual声明,可把多个逻辑虚基类对象映射成同一个物理虚基类对象。映射成的这个物理虚基类对象尽可能早的构造、尽可能晚的析构且构造和析构都只进行一次

假设两栖机车AmphibiousVehicle继承基类陆用机车LandVehicle和水上机车WaterVehicleLandVehicleWaterVehicle都继承自Engine,则在构造时可能会对AmphibiousVehicleEngine初始化两次

未引入虚基类

使用虚基类Engine,之后LandVehicleWaterVehicleEngine都指向同一块物理内存,且只会构造和析构一次,则不会出现上述情况。

使用虚基类

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
//直接多继承
class Engine{ /*...*/};
class LandVehicle: Engine{/*...*/};
class WaterVehicle: Engine{/*...*/};
class AmphibiousVehicle:public LandVehicle,public WaterVehicle{/*...*/} ;

//使用虚基类
class Engine{ /*...*/ };
class LandVehicle: virtual public Engine{ /*...*/ };
class WaterVehicle: public virtual Engine{ /*...*/ };
class AmphibiousVehicle: public LandVehicle, public WaterVehicle{ };

同一棵派生树中的同名虚基类,共享同一个存储空间;其构造和析构仅执行1次,且构造尽可能最早执行,而析构尽可能最晚执行。由派生类(根) 、基类和虚基类构成一个派生树的节点,而对象成员将成为一棵新派生树的根。

在非虚拟派生中,派生类只能显式初始化其直接基类,而虚拟基类的初始化则成了每一级派生类的责任,即只要使用了虚基类,虚基类的每一级派生类都必须调用虚基类的构造函数(因为虚基类必须最先构造),有虚基类的派生类构造函数不能使用constexpr定义

派生类成员

虚基类和基类成员同名必然会导致二义性访问,编译程序会对这种二义性访问报错。当出现这种情况时,可用作用域运算符限定要访问的成员。

当派生类成员和基类成员同名时,优先访问作用域小的成员,即优先访问派生类的成员。当派生类数据成员和派生类函数成员的参数同名时,在函数成员内优先访问函数参数。

多继承的构造与析构

派生类对象的构造顺序:

  • 按定义顺序构造派生树中所有虚基类
  • 按定义顺序构造派生类中所有直接基类
  • 按定义顺序构造派生类数据成员
  • 执行派生类构造函数

析构顺序与构造顺序相反。注意:同名派生类在同一棵树中只构造一次

第十章 异常与断言

异常处理

异常时一种意外破坏程序正常处理的时间、由硬件或软件触发的事件。异常处理可以将错误处理流程同正常业务处理流程分离,从而使程序的正常业务处理流程更加清晰顺畅。

异常发生后自动析构调用链中的所有对象,这也使程序降低了内存泄漏的风险。 由软件引发的异常用throw语句抛出,会在抛出点建立一个描述异常的对象,由catch捕获相应类型的异常。

异常处理的一般结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try{
/*程序的正常流程部分*/
throw expression1;//throw可以抛出任意类型的异常,包括字符、字符串、数组、对象
/*继续接程序的正常流程部分*/
throw expression2;//一个try种可以抛出多个异常。但是值啊要抛出第一个异常,try语句块马上结束,转到catch自居。如果try语句块执行成功,则跳过catch
}
catch(expression1){//catch子句必须定义且只能定义一个参数,即接受的异常,该参数不能是void,不能是右值引用。
/*对异常的处理*/
}
catch(expression2){
/*对异常的处理*/
}
//可定义多个catch子句截获可能抛出的各种异常。但最多只会执行其中一个异常处理过程。在相应异常被处理后,其他异常处理过程都将被忽略。如果异常处理成功,则执行后续statements。如果异常处理过程中又抛出新的异常,则后续statements不会被执行

catch必须出现try语句块后面,不能有单独的try语句块或者单独的catch语句块,throw语句要求直接或者间接出现在try语句块中,异常必须被某个trycatch捕获。如果没有被任何catch捕获,则程序将被终止执行。

异常捕获的过程称为栈展开,即当try语句快有异常抛出时,先检查该try语句的相关catch子句,没找到则检查外层try自居配套的catch子句,还没找到则退出当前函数,在调用该函数的外层函数继续寻找,如果到主函数种还没找到catch自居,程序退出,将异常交给OS处理。以上过程递归调用,称为栈展开。

如果在catch子句中需要重新抛出异常,可以直接使用不带表达式的throw语句用以传播已捕获的异常。若希望捕获所有异常,则可以使用catch(...)。若接受的异常类型为字符串常量,则需要用catch(const char *p)捕获。

捕获顺序

总的来说,先声明的异常处理先得到执行机会。特别的,异常的类型只要和catch参数的类型相同或相容即可匹配成功。即派生异常对象(及对象指针或引用)可以被带基类型对象、指针及引用参数的catch子句捕获。注意catch(const volatile void *)能捕获任意指针类型的异常,catch(…)能捕获任意类型的异常。

函数的异常接口

通过异常接口声明的异常都是由该函数引发的、而其自身又不想捕获或处理的异常。 异常接口定义的异常出现在函数的参数表后面,用throw列出要引发的异常类型。其中throw()noexceptthrow(void)代表不引发任何异常,如果参数表后面不出现异常接口,表示该函数可能引发任何类型的异常。

对于未经处理的异常或传播后未经处理的异常,C++的监控系统将调用void terminate( )处理。缺省情况下terminate函数调用abort函数来终止程序。可以调用set_terminate( )函数,设置自定义的terminate处理函数。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void (*old_terminate)();//缺省的teminate函数主要是后面用来恢复

void new_terminate(){//自定义的teminate函数,注意函数返回值为void
cout << "Custom terminate" << endl;
abort();
}

void g(){
try {
f5(); //声明不会抛出异常,但实际抛出了异常,异常不会被截获
}
catch(A &ex){ cout << ex.msg;}
catch(B &ex){ cout << ex.msg;}
}
int main() {
old_terminate = set_terminate(new_terminate);//调用set_terminate函数,传入一个函数指针覆盖系统默认的terminate函数
g();
//如果程序运行到这里,说明没有异常
//恢复终止处理函数set_terminate (old_terminate);
set_terminate (old_terminate);
}

断言

函数assert(int)assert.h中定义。 断言是一个带有整型参数的用于调试程序的函数,如果实参的值为真则程序继续执行。 否则,将输出断言表达式、断言所在代码文件名称以及断言所在程序的行号,然后调用abort()终止程序的执行。

保留字static_assert定义的断言在编译时检查,为真时不终止编译运行。

第十一章 运算符重载

运算符概述

运算符分为纯单目运算符,纯双目运算符,三目运算符,既是单目又是双目运算符,多目运算符。根据运算结果又可以分为左值运算符和右值运算符。

C++用返回类型 operator 运算符(参数表)进行运算符重载。如果用类的普通成员函数重载运算符,this隐含参数代表第一个操作数对象。

不能重载的运算符有:sizeof . .* :: ?:

其他运算符能否重载的情况如下图所示

运算符重载

运算符重载基本和函数重载一样,但是由于运算符本身时有意义的,有以下几点要注意

  • 若运算符为左值运算符,则重载后运算符函数最好返回非只读左值引用类型(左值)。当运算符要求第一个参数为左值时,不能使用const说明第一个参数(含this),例如++--=+=等的第一个参数。
  • 重载一般也不改变运算符的操作数个数。特殊的运算符->++-- (区分前置和后置)除外。
  • 重载不改变运算符的优先级和结合性。

运算符参数

重载函数种类不同,参数表列出的参数个数也不同。

  • 重载为普通函数(全局函数):参数个数=运算符目数
  • 重载为类的普通成员函数(实例函数):参数个数=运算符目数 - 1 (即this指针)
  • 重载为类的静态成员函数:参数个数 = 运算符目数(没有this指针)

特殊运算符的重载

  • 重载++--:参数应设为非const左值引用,前置返回值为左值引用,后置返回值为右值。后置的参数应加上一个int类型表示后置
  • 也可以用operator定义强制类型转换函数。由于转换后的类型就是函数的返回类型,所以强制类型转换函数不需要定义返回类型。

第十二章 类型解析,转换和推导

显式与隐式类型转换

字节数少的类型向字节数多的类型转换时,不会引起数据的精度损失。 无风险的转换由编译程序自动完成,这种自动转换也称为隐式类型转换。强制类型转换称为显式类型转换

一般简单类型之间的强制类型转换的结果为右值,而进行左值引用转换时结果为左值。注意只读的简单类型变量如果转化为可写左值,依然不能修改其值,而对类的只读类型成员则可以(就离谱)

cast系列类型转换

  • static_cast同C语言的强制类型转换用法基本相同,但不能从源类型中去除constvolitale属性,不做多态相关的检查。

  • const_cast同C语言的强制类型转换用法基本相同,但能从源类型中去除constvolitale属性。

  • dynamic_cast主要用于有继承关系的基类型和派生类型之间的相互转换。将子类对象转换为父类对象时无须子类多态,而将基类对象转换为派生类对象时要求基类多态。

  • reinterpret_cast通常为运算对象的位模式提供低层级的重新解释。主要用于名字(运算对象)同指针或引用类型之间的转换,以及指针与足够大的整数类型(能够存放地址)之间的转换。

static_cast——静态转换

使用格式为static_cast<T> (expr),如

1
2
double x=2.0;
int y=static_cast<int>(x);

目标类型T不能包含存储位置类修饰符,如staticexternautoregister等。 static_cast仅在编译时静态检查源类型能否转换为T类型,运行时不做动态类型检查。static_cast不能去除源类型的constvolatile。即不能将指向constvolatile实体的指针(或引用)转换为指向非constvolatile实体的指针(或引用)。 只要不转换底层const(和指针与引用等复合类型的基本类型有关。修饰基本类型的const),都可以使用static_cast。即我们无法通过修改指针类型以修改只读变量的值。

const_cast——只读转换

使用格式与static_cast一致,但其目标类型必须是指针或引用或指向对象类型成员的指针。可以去掉指针顶层和底层的constvolatile,但不能改变其类型。

dynamic_cast——动态转换

关键字dynamic_cast主要用于子类向父类转换,以及有虚函数的基类向派生类转换。被转换的表达式必须涉及类类型。 使用格式为dynamic_cast<T> (expr),要求T是类的引用、类的指针或者void*类型,而expr的源类型必须是类的对象(包括常量对象或变量对象:向引用类型转换)、父类或者子类的引用或指针。 dynamic_cast转换时不能去除数值表达式expr源类型中的constvolitale属性。 被转换的基类对象必须包含虚函数或纯虚函数才能转换为派生类对象。

reinterpret_cast——重释转换

关键字reinterpret_cast实现有址表达式(有名字)到指针或(有址或无址)引用类型的转换以及指针与足够大整数类型间的相互转换。 使用格式为reinterpret_cast <T> (expr),用于将数值表达式expr的值转换成T类型的值。T类型不能是实例数据成员指针。 转换为足够大的整数类型是指能够存储一个地址或者指针的整数类型,X86和X64的指针大小不同,X86使用int类型即可。 当T为使用&&&定义的引用类型时,expr必须是一个有址表达式。 有址引用和无址引用之间可以相互转换。

typeid可以检查对象的类型,使用方法为typeid(a)==typeid(A)?或者typeid(*p)!=typeid(D)等,一般都是用作判断。

自动类型推导

保留字auto在C++中用于类型推导。auto让编译器通过初始值来推算类型,因此auto定义的变量必须有初始值;使用auto推导时,被推导实体不能出现类型说明,但是可以出现存储可变特性constvoilatile和存储位置特性如staticregisterinline。使用auto可以在一条语句中声明多个变量,但必须保证这一条声明语句只能有一个基本数据类型

关键字decltype用来提取表达式的类型。 使用方法如decltype(f()) sum = x;用函数f的返回类型来声明变量sum,并初始化为x 发生在编译时,且不会调用函数f。凡是需要类型的地方均可出现decltype。 可用于变量、成员、参数、返回类型的定义以及newsizeof、异常列表、强制类型转换。 可用于构成新的类型表达式。

Lamda表达式

Lambda表达式是C++引入的一种匿名函数。存储Lambda表达式的变量被编译为临时类的对象。 该临时类变量的对象被构造时,此时Lambda表达式被计算。 若未定义存储该临时类对象的变量,则称该Lambda表达式没被计算。

Lambda表达式的声明格式为[捕获列表](形参列表)mutable 异常说明->返回类型{函数体},如auto f = [ ](int x=1)->int { return x; };捕获列表的参数用于捕获Lambda表达式的外部变量。

关于捕获列表的参数

  • Lambda表达式的外部变量不能是全局变量或static定义的变量也不能是类的成员。 可以是函数参数或函数定义的局部自动变量。
  • 出现&变量名表示引用捕获外部变量,[&]表示引用捕获所有函数参数或函数定义的局部自动变量。其可写特性同被捕获的外部变量一致。
  • 出现=变量名表示值捕获外部变量的值(值参传递), [=]表示捕获所有函数参数或函数定义的局部自动变量的值。
  • 参数表后有mutable表示在Lambda表达式中可以修改“值参传递的可写变量的值”,但调用后不影响Lambda表达式捕获的对应该可写外部变量的值

第十三章 模板与内存回收

变量模板及其实例

C++提供了3种类型的模板,即变量模板、函数模板和类模板。 变量模板使用类型形参定义变量的类型,可根据类型实参生成变量模板的实例变量。 生成实例变量的途径有三种:一种是从变量模板隐式地或显式地生成模板实例变量;另一种是通过函数模板和类模板生成。 在定义变量模板时,类型形参的名称可以使用关键字class或者typename定义,即可以使用template<class T>或者template<typename T>。 生成模板实例变量时,将使用实际类型名、类名或类模板实例代替T。如

1
2
3
4
5
6
7
template<typename T>
constexpr T pi=T(3.1415926535897932385L)
int main()
{
const float &d=pi<float>;//将T实例化为float,当然也可以实例化为double等
return 0;
}

变量模板不能再函数内部声明,变量模板生成的模板实例也必须为全局或模块静态变量。模板的参数列表除了可以使用类型形参外,还可以使用非类型的形参,如

1
2
3
4
5
6
template<class T, int x=3> 		//定义变量模板girth,其类型形参为T
static T girth = T(3.1415926535897932385L*2*x);
int main()
{
double &g=girth<double,4>;
}

其中非类型的形参可以给出默认值。

函数模板

函数模板是使用类型形参定义的函数框架,可根据类型实参生成函数模板的模板实例函数。 函数模板不能在非成员函数的内部声明。 根据函数模板生成的模板实例函数也和函数模板的作用域相同。 在函数模板时,可以使用类型形参和非类型形参。 实例化时非类型形参需要传递常量作为实参。 可以单独定义类的函数成员为函数模板。

在调用函数时可隐式自动生成模板实例函数(根据实参推断)。 也可使用template 返回类型 函数名<类型实参>(形参列表),显式强制函数模板按类型实参显式生成模板实例函数。 有时候生成的模板实例函数不能满足要求,可定义特化的模板实例函数隐藏自动生成的模板实例函数(直接理解为重载算了)。 在特化定义模板的实例函数时,一定要给出特化函数的完整定义。

1
2
3
4
5
6
7
8
9
10
template <class T>	//class可用typename代替
T sum(const T& x, const T& y)
{
return x + y;
}
int main()
{
template int sum<int>(const int & x, const int &y); //显式实例化模板函数
double b = sum(3.14,2.56);//隐式实例化模板函数
}

类模板

类模板也称为类属类或参数化的类,用于为相似的类定义一种通用模式。 编译程序根据类型实参生成相应的类模板实例类,也可称为模板类或类模板实例。 类模板既可包含类型参数,也可包括非类型参数。 类型参数可以包含1个以上乃至任意个类型形参。 非类型形参在实例化是必须使用常量做为实参。使用方法如下:

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
 
template <class T, int v=20> //类模板的模板参数列表有非类型形参v,默认值为20
class VECTOR
{
T *data;
int size;
public:
VECTOR(int n = v+5); //由v构成表达式v+5,将其作为构造函数成员形参的默认值
~VECTOR( ) noexcept;
T &operator[ ](int);
};

template <class T, int v> //在类体外函数实现时,加template<class T, int v>, v不能给缺省值
VECTOR <T, v>::VECTOR(int n) //须用VECTOR <T, v>作为类名
{
data = new T[size = n];
}

template <class T, int v> //函数实现时,加template<class T, int v>, v不能给缺省值
VECTOR <T, v>::~VECTOR( ) noexcept //须用VECTOR <T, v>作为类名
{
if (data) delete data;
data = nullptr;
size = 0;
}

template <class T, int v>
T &VECTOR <T, v>::operator[ ](int i) //须用VECTOR <T, v>作为类名
{
return data[i];
}
int main()
{
VECTOR<int> L(10);
}

如果函数是在类体里实现的,实现时不用加template<T>

可以用类模板定义基类和派生类。 在实例化派生类时,如果基类是用类模板定义的,也会同时实例化基类。这里的实例化指模板类实例化,生成实例类 派生类函数在调用基类的函数时,最好使用基类<类型参数>::限定基类函数成员的名称,以帮助编译程序识别函数成员所属的基类。 对于实例化的模板类,如果其名字太长,可以使用typedef重新命名定义。

当一个类模板有多个类型形参时,在类中只要使用同样的类型形参顺序,都不会影响他们是同一个类型形参模式。

类模板同样可以定义特化的类,但是和函数的形式有所不同,假设定义一个VECTOR的特化的字符指针的向量类,格式如下

1
2
3
4
5
6
7
8
9
10
11
template < >			//定义特化的字符指针向量类
class VECTOR <char*>
{
char** data;
int size;
public:
VECTOR(int); //特化后其所属类名为VECTOR <char*>
~VECTOR( ) noexcept; //特化后其所属类名为VECTOR <char*>,不是虚函数
virtual char*& operator[ ](int i) { return data[i]; }; //特化后为虚函数
};

实验材料、源码及实验报告

https://github.com/zz12138zz/cpp_experiment_of_HUST

  • Title: C++面向对象编程
  • Author: Yizumi Konata
  • Created at : 2020-12-21 14:36:02
  • Updated at : 2024-06-06 23:04:55
  • Link: https://zz12138zz.github.io/2020/12/21/C++/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments