内容主体来自于QJH先生,向其表示敬意。
C++高级程序设计
题目类型
-
简述题(5题、25分)
- 什么是数据抽象与封装?相比于过程抽象与封装,数据抽象与封装有什么好处?
- ……(不用死记硬背)
-
程序分析题(5题、40分)
- 指出下面程序的错误和错误原因
- 写出下面程序的运行结果。
- 写出下面程序的运行结果,并指出可能存在的问题。
-
实现下面的程序(3题、35分)
-
把下面的递归函数改成尾递归函数
-
小明同学想要xxx。请根据下面的使用示例和相应的输出,设计相应的类和全局函数。
-
……
-
1. 栈、λ 表达式、数据抽象与封装
1.1-栈
-
程序实体在内存中的安排
- 静态数据区:用于全局变量、static存储类的局部变量以及常量的内存分配 。如果没有显式初始化,系统将把它们初始化成0
- 代码区:用于存放程序的指令,对C++程序而言,代码区存放的是所有函数代码
- 栈区:用于自动存储类的局部变量、函数的形式参数以及函数调用时有关信息(如:函数返回地址等)的内存分配(栈空间被各个函数共享,从而节省空间)
- 堆区:用于动态变量的内存分配
-
栈空间被各个函数共享
函数调用时,该函数返回地址首先入栈,函数中的变量按序依次入栈,若过程中有其他函数被调用,如上顺序入栈,函数返回时,栈中为该函数分配的空间全部出栈。
-
递归函数执行中栈的使用情况
- 把一个递归函数看成多个同名的函数(多个实例,Instance),然后按函数的嵌套调用来理解递归调用过程
- 对递归函数的每一次递归调用都将产生一组新的局部变量(包括形参),虽然它们的名字相同,但它们是不同的变量(属于不同的实例),拥有不同的栈空间
-
栈对函数调用的限制
过深的函数嵌套或递归调用会造成栈空间不足,出现“栈溢出”(stack overflow)错误,从而引起程序的异常终止
1.2-λ 表达式
-
推出版本:C++ 11
-
格式:[<环境变量使用说明>]<形式参数><返回值类型指定><函数体>
- <形式参数>:指出函数的参数及类型,其格式为:
- (<形式参数表>)
- 如果函数没有参数,则这项可以省略。
- <返回值类型指定>:指出函数的返回值类型,其格式为:
- -> <返回值类型>
- 它可以省略,这时根据函数体中return返回的值隐式确定返回值类型。
- <函数体>为一个复合语句
- <环境变量使用说明>:指出函数体中对外层作用域中的自动变量的使用限制:
- 空:不能使用外层作用域中的自动变量。
- &:按引用方式使用外层作用域中的自动变量(可以改变这些变量的值)。
- =:按值方式使用使用外层作用域中的自动变量(不能改变这些变量的值)。
- &和=
- n可以用来统一指定对外层作用域中自动变量的使用方式;
- 也可以用来分别指定可使用的外层自动变量(变量名加&,默认为=)
- 一些合法λ表达式
int k,m,n; //环境变量 ...... ...[](int x)->int { return x*x; }...//不能使用k、m、n ...[&](int x)->int { k++; m++; n++; return x+k+m+n; }...//k、m、n可以被修改 ...[=](int x)->int { return x+k+m+n; }...//k、m、n不能被修改 ...[&,n](int x)->int { k++; m++; return x+k+m+n; }...//n不能被修改 ...[=,&n](int x)->int { n++; return x+k+m+n; }... //n可以被修改 ...[&k,m](int x)->int { k++; return x+k+m; }...//只能使用k和m,k可以被修改 ...[=]{ return k+m+n; }... //没有参数,返回值类型为int
- <形式参数>:指出函数的参数及类型,其格式为:
-
使用方式
-
直接调用它定义的函数
[](int x)->int { return x*x; }(10)
-
把它定义的函数作为参数传给另一个函数(主要用途)
f([](int x)->int { return x*x; })
-
1.3-数据抽象与封装
- 概念
- 抽象是指该程序实体外部可观察到的行为,使用者不考虑该程序实体的内部是如何实现的。(复杂度控制)
- 封装是指把该程序实体内部的具体实现细节对使用者隐藏起来,只对外提供一个接口。(信息保护)
- 主要的程序抽象与封装机制
- 过程抽象与封装
- 数据抽象与封装
- 过程抽象与封装
- 过程抽象:用一个名字来代表一段完成一定功能的程序代码,代码的使用者只需要知道代码的名字以及相应的功能,而不需要知道对应的程序代码是如何实现的
- 过程封装:
- 把命名代码的具体实现隐藏起来(对使用者不可见,或不可直接访问),使用者只能通过代码名字来使用相应的代码
- 命名代码所需要的数据是通过参数来获得,计算结果通过返回值机制返回
- 子程序(C/C++中称为函数):实现过程抽象与封装的程序实体
- 过程抽象与封装是基于功能分解与复合的过程式程序设计的基础
- 数据抽象与封装
- 数据抽象:只描述对数据能实施哪些操作以及这些操作之间的关系,数据的使用者不需要知道数据的具体表示形式(数组或链表等)
- 数据封装:把数据及其操作作为一个整体(封装体)来进行实现,其中,数据的具体表示被隐藏起来(使用者不可见,或不可直接访问),对数据的访问(使用)只能通过封装体对外接口中提供的操作来完成
- 数据抽象与封装可实现更好的数据保护
- 数据抽象与封装是面向对象程序设计的基础,其中的对象体现了数据抽象与封装
- 比较三种途径实现栈(非抽象与封装、过程抽象与封装、数据抽象与封装)
- 非抽象与封装缺点
- 操作必须知道数据的具体表示形式
- 数据表示形式发生变化将会影响操作
- 麻烦并易产生误操作,因此不安全
- 过程抽象与封装缺点
- 数据定义与操作的定义是分开的,二者之间没有必然的联系,仅仅依靠操作的参数类型把二者关联起来
- 数据表示仍然是公开的,无法防止使用者直接操作栈数据,因此也会面临直接操作栈数据所面临的问题
- 数据抽象与封装
- 非抽象与封装缺点
2. 对象与类
-
面向对象程序设计特征:封装、继承、多态、通过消息传递来实现程序的运转
-
消息传递:
-
从程序外部向程序中的某个对象发送第一条消息启动计算过程
-
该对象在处理这条消息的过程中,又向程序中的其它对象发送消息,从而引起进一步的计算
-
消息传递方式:
- 同步消息处理:消息发送者必须等待消息处理完才能继续执行其它操作(顺序执行)
- 异步消息处理:消息发送者不必等待消息处理完就能继续执行其它操作(并发执行)
-
-
C++是混合编程范式语言:过程式+面向对象+函数式
-
影响软件开发效率和软件质量的主要因素:
-
抽象(控制复杂度)
-
封装(保护信息)
-
模块化(组织和管理大型程序)
-
软件复用(缩短开发周期)
-
可维护性(延长软件寿命)
-
软件模型的自然度(缩小解题空间与问题空间之间的语义间隙,实现从问题到解决方案的自然过渡)
-
-
过程式程序设计的特点:
-
以功能为中心,强调过程(功能)抽象,但数据与操作分离,二者联系松散。
-
实现了操作的封装,但数据表示是公开的,数据缺乏保护。
-
按子程序划分模块,模块边界模糊。
-
子程序往往针对某个程序的具体功能而设计,这使得程序难以复用。
-
功能易变,程序维护困难。
-
基于子程序的解题方式与问题空间缺乏对应。
-
-
面向对象程序设计的特点:
-
以数据为中心,强调数据抽象,操作依附于数据,二者联系紧密。
-
实现了数据的封装,加强了数据的保护。
-
按对象类划分模块,模块边界清晰。
-
对象类往往具有通用性,再加上继承机制,使得程序容易复用。
-
对象类相对稳定,有利于程序维护。
-
基于对象及其交互的解题方式与问题空间有很好的对应。
-
2.1-成员的访问控制
- 成员访问修饰符
- public:访问不受限制
- private:只能在本类和友元的代码中访问
- protected:只能在本类、友元和派生类的代码中访问
- 注:在C++类定义中,若未标注修饰符,默认访问控制是private;而在结构和联合成员定义中,默认访问控制是public
- 使用情况
- private:类的数据成员和在类的内部使用的成员函数
- public:提供给外界使用的成员函数(构成类与外界的接口)
- protected:在派生类中使用
2.2-this指针
- 类中描述的数据成员(静态数据成员除外)对该类的每个对象分别有一个拷贝
- 类中的成员函数对该类所有对象只有一个拷贝
- 类的每一个成员函数(静态成员函数除外)都有一个隐藏的形参this,其类型为该类对象的指针;在成员函数中对类成员的访问是通过this来进行的,当通过对象访问类的成员函数时,将会把相应对象的地址传给成员函数的参数this
- 使用this指针的情况
- 在类的成员函数中访问类的成员时不必显式使用this指针
- 如果成员函数中要把this所指向的对象作为整体来操作,则需要显式使用this指针
- 为了与同名的非成员进行区别,需要显式使用this
2.3-构造函数、析构函数、拷贝构造函数
-
构造函数
-
特点:名字与类名相同;无返回值类型;在创建对象时被自动调用;可被重载
-
默认构造函数:不带参数(或参数均有默认值)的构造函数
-
创建对象时,可显式指定调用某一构造函数,若未指定,则调用默认构造函数
-
对象创建后,不能再次调用构造函数
-
常量(const)和引用(&)数据成员的初始化
-
不能在说明时初始化
-
不能在构造函数中采用赋值操作
-
使用成员初始化表
class A { int x; const int y; int& z; public: A(): z(x),y(1) //成员初始化表 { x = 0; } };
-
-
包含成员对象的对象初始化次序:先调用本身类的构造函数,在进入函数体前调用成员对象类的构造函数,然后再执行本身类构造函数的函数体
-
-
析构函数
- 特点:名字为"~<类名>";无返回类型;不带参数;无法被重载
- 调用情况
- 一个对象消亡时,系统在收回它的内存空间之前(自动调用)
- 暂时归还对象额外申请的资源,对象未消亡(显式调用)
- 自定义析构函数的情况:
- 需要完成对象被删除前的一些清理工作
- 对象创建后,自己又额外申请了资源(如:额外申请了内存空间),需归还额外申请的空间
- 包含成员对象的对象消亡次序:先调用本身类的析构函数,本身类析构函数的函数体执行完后,再调用成员对象类的析构函数
-
拷贝构造函数
- 定义:参数类型为本类的引用的构造函数
- 调用情况
- 创建对象时显式指出
- 把对象作为值参数传给函数时
- 把对象作为函数的返回值时
- 隐式拷贝构造函数:在程序中,如果没有为某个类提供拷贝构造函数,则编译器将会为其生成一个隐式拷贝构造函数;隐式拷贝构造函数将逐个成员进行拷贝初始化(浅拷贝:只拷贝数据成员本身的值)
- 对于非对象成员:采用通常的拷贝操作
- 对于成员对象:调用成员对象类的拷贝构造函数对成员对象初始化
- 自定义拷贝构造函数
- 需要自定义拷贝构造函数的情况举例:浅拷贝导致两个对象的指针指向同一块区域,则需显示定义拷贝构造函数实现深拷贝
- 自定义的拷贝构造函数不会自动调用成员对象类的拷贝构造函数;若需要调用,则需要在该拷贝构造函数的成员初始化表中显式指出
- 拷贝构造存在的问题:用一个临时或即将消亡的对象去初始化另一个同类的对象时,目前的拷贝构造函数的实现效率有时不高
- 改进
- 右值引用类型(&&)
- 当函数的参数类型为右值引用类型时,它将接受临时对象或即将消亡的对象(例:void f(A&& x){ ...... })
- 转移构造函数
- 特点:参数为右值引用类型;必须显式定义
- 调用情况:用一个临时或即将消亡的对象去初始化另一个对象时,且对象类中有转移构造函数
- move函数:将左值类型转换成右值类型
- 右值引用类型(&&)
- 改进
3. 常成员函数、静态成员、友元
3.1-常成员函数
- 作用
- 防止在一个获取对象状态的成员函数中无意中修改对象数据成员的值
- 对常量对象只能调用类中的常成员函数
- 格式:void f() const;
3.2-静态成员
-
需要用static显式指出
-
同类对象共享数据的方式
-
采用全局变量:共享的数据与对象缺乏显式联系;数据缺乏保护
-
采用静态数据成员
class A { int y; ...... static int x; //静态数据成员声明 void f() { y = x; x++; ...... } //访问共享的x }; int A::x=0; //静态数据成员定义及初始化 A a,b; a.f(); b.f(); //上述操作对同一个x进行
-
-
静态数据成员
- 特点:对该类所有对象只有一个拷贝
-
静态成员函数
- 特点:只能访问类的静态成员;无隐藏的this参数
-
静态成员除了通过对象来访问外,也可以直接通过类来访问
3.3-友元
- 目的:提高在类的外部对类数据成员的访问效率;是数据保护和数据访问效率之间的一种折衷方案
- 可以作为友元的程序实体(需要用friend显式指出)
- 全局函数
- 其他类的所有成员函数
- 其他类的某个成员函数
- 特点:不是类的成员;不对称性;无传递性
4.操作符重载
是实现多态性的一种语言机制
4.1-基本操作符重载
-
实现途径:定义一个名为"operator #"(#:某个可重载的操作符)的函数,可作为:
- 类的非静态成员函数(除new和delete的重载)
- 全局(友元)函数
-
基本原则
- 只重载C++已有操作符
- 不可重载的操作符:“.”,“.*”(引用指向类成员的指针),“?:”,“::”,“sizeof”
- 遵循已有操作符的语法:操作数个数不变;优先级、结合性不变
- 尽量遵循已有操作符的原语义
-
操作符++和--的重载
-
单目操作符++(--):一个操作数,且为左值表达式
-
区分前置和后置用法:为后置用法另写一个重载函数,且有一个额外的int型参数
class Counter { int value; public: Counter() { value = 0; } Counter& operator ++() //前置的++重载函数 { value++; return *this; } const Counter operator ++(int) //后置的++重载函数 { Counter temp=*this; //保存原来的对象 value++; //写成:++(*this);更好!调用前置的++重载函数 return temp; //返回原来的对象 } };
-
4.2-特殊操作符重载、智能指针
-
赋值操作符“=”
- 重载原因:隐式的赋值操作有时会出现问题(如:浅拷贝导致两个对象中的指针指向同一地址,引发内存泄露等问题)
- 自定义的赋值操作符重载函数不会自动调用成员对象类的赋值操作,需在重载函数中显式指出
- 一般,需要自定义拷贝构造函数的类通常也需要自定义赋值操作符重载函数
- 存在的问题:当用于赋值的对象是一个临时或即将消亡的对象时,目前的赋值操作符重载函数的实现效率有时不高
- 改进:转移赋值操作符重载函数(类似转移构造函数)
-
访问数组元素操作符“[]”
- 重载目的:访问由具有线性关系的元素所构成的对象中的元素
-
函数调用操作符“()”
-
主要用于具有函数性质的对象(函数对象)
class A { int value; public: A(int i) { value = i; } int g() { return value; } int operator () (int x,int y) //函数调用操作符()的重载函数 { return x*y+value; } }; A a(1); //a是个对象 cout << a.g() <<endl; //把a当对象来用 cout << 10+a(10,20) << endl; //把a当函数来用! //a(10,20)等价于:a.operator()(10,20) ...... void func(A& f) //f是个对象 { ... 10 + f(10,20) ... //把f当函数来使用 //f(10,20)等价于:f.operator()(10,20) } ...... func(a); //把对象a传给f
-
函数对象除了具有一般函数的行为外,它还可以拥有状态
class RandomNum { unsigned int seed; //状态 public: RandomNum(unsigned int i) { seed = i; } unsigned int operator ()() //函数调用操作符重载 { seed = (25173*seed+13849)%65536; //修改了状态 return seed; } }; ...... RandomNum random_num(1); //创建一个函数对象 ... random_num() ... //把函数对象当作一个函数,调用它产生一个随机数
-
C++中,λ表达式是通过函数对象来实现的
-
-
类成员访问操作符“->”(智能指针)
-
好处:通过智能指针去访问它指向的对象成员之前能做一些额外的事情
-
例:在程序执行的某个时刻获取某个对象被访问的次数
class PtrA //智能指针类 { A *p_a; //指向A类对象的普通指针 int count; //用于对p_a指向的对象进行访问计数 public: PtrA(A *p) { p_a = p; count = 0; } A *operator ->() //操作符“->”的重载函数,按单目操作符重载! { count++; return p_a; } int num_of_a_access() const { return count; } }; void func(PtrA &p) //p是个PtrA类对象! { ... p->f(); ... p->g(); ... }
-
C++标准库中提供的智能指针类型
- shared_ptr:带引用计数,利用它能够实现动态对象空间的自动回收
- unique_ptr:独占动态对象
-
-
动态对象创建与撤销操作符new和delete
-
重载目的:使该类能以自己的方式来实现动态对象空间的分配和释放功能
-
重载操作符new:
- 必须作为静态成员函数重载(static可不写)
- 格式:void *operator new(size_t size,......);
- 返回类型必须为void *
- 例:在非“堆区”为动态对象分配空间
#include <cstring> class A { int x,y; public: A(int i, int j) { x=i; y=j; } void *operator new(size_t size, void *p) { return p; } }; ...... char buf[sizeof(A)]; A *p=new (buf) A(1,2);//动态对象的空间分配为buf ...... p->~A(); //使得p所指向的对象消亡。 //不能用系统的delete,可以用自己重载的delete
-
重载操作符delete
- 若重载了new,一般要重载delete
- 格式:void operator delete(void *p, size_t size);
- 返回类型必须为void;第二个参数可有可无
-
make_shared函数:创建自带引用计数的动态对象
-
-
类型转换操作符
-
定义
-
隐式:类中带一个参数的构造函数可以用作从其它类型到该类的转换
-
显式:定义从一个类到其它类型的转换
class A { int x,y; public: ...... operator int() //类型转换操作符int的重载函数 { return x+y; } }; ... A a; int i=1; ... (i + a) ... //将调用类型转换操作符int的重载函数 //把对象a隐式转换成int型数据。
-
-
对于歧义问题,可用显式类型转换解决;也可给该构造函数加上修饰符explicit,禁止把它当作隐式类型转换符来用
-
5.继承-派生类
继承属于目标代码复用
5.1-单继承
-
格式:class <派生类名>:[<继承方式>] <基类名> {<成员说明表>};
-
定义派生类时一定要见到基类的定义
-
关于友元
- 如果在派生类中没有显式指出,则基类的友元不是派生类的友元
- 如果基类是另一个类的友元,而该类没有显式指出,则派生类不是该类的友元
-
在派生类中访问基类成员
- 使用场景
- 通过类的对象(实例)使用(private)
- 在派生类中使用(private,protected)
- 如果派生类中定义了与基类同名(参数可以不同)的成员,则基类的成员名在派生类的作用域内不直接可见(被隐藏,Hidden),在派生类中访问基类同名的成员时可以:
- 用基类名受限
- 在派生类中用using声明把基类中某个函数名对派生类开放
- 使用场景
-
继承方式
- 继承方式分类:public,private,protected;默认继承方式:private
- 继承方式的含义
| $_{继承方式} | 派生类 | ^{基类成员}$ | public | private | protected |
| :----------------------------------: | :-------: | :------: | :-------: |
| public | public | 不可访问 | protected |
| private | private | 不可访问 | private |
| protected | protected | 不可访问 | protected |-
继承方式的调整
class A { public: void f1(); void f2(); void f3(); protected: void g1(); void g2(); void g3(); }; class B: private A { public: A::f1; //把f1调整为public A::g1; //把g1调整为public //是否允许弱化基类的访问控制要视具体的实现而定 protected: A::f2; //把f2调整为protected A::g2; //把g2调整为protected ...... };
-
子类型:对用类型T表达的所有程序P,当用类型S去替换程序P中的所有的类型T时,程序P的功能不变,则称类型S是类型T的子类型(以public方式继承的派生类可看作是基类的子类型)
class A //基类 { int x,y; public: void f() { x++; y++; } ...... }; class B: public A //派生类 { int z; public: void g() { z++; } ...... }; //以下操作均合法 A a; B b; b.f(); //OK,基类的public操作可以实施到派生类对象 a = b; //OK,派生类对象可以赋值给基类对象, //属于派生类但不属于基类的数据成员将被忽略 A *p=&b; //OK,基类的指针可以指向派生类对象 A &a2=b; //OK,基类的引用可以引用派生类对象 ...... void func1(A *p); void func2(A &x); void func3(A x); func1(&b); func2(b); func3(b); //OK //以下操作不合法 A a; B b; a.g(); //Error,基类对象a没有g这个成员函数。 b = a; //Error,它将导致b有不一致的成员数据 //(a中没有属于派生类的数据)。 B *q=&a; //Error,“q->g();”将修改不属于a的数据! B &b2=a; //Error,“b2.g();”将修改不属于a的数据! ...... void func1(B *p); void func2(B &x); void func3(B x); func1(&a); func2(a); func3(a); //Error
-
派生类对象的初始化和消亡处理
- 初始化:由基类和派生类共同完成
- 从基类继承的数据成员由基类的构造函数初始化
- 派生类新的数据成员由派生类的构造函数初始化
- 创建派生类对象时:先调用本身类构造函数,进入函数体前调用基类构造函数,再执行本身构造函数函数体(要调用基类非默认构造函数必须再派生类构造函数的成员初始化表中显式指出)
- 派生类对象消亡时:先调用本身类析构函数,执行完后调用基类析构函数
- 例:三个类的关系B->D->M
- 创建D类对象时构造函数的执行次序:B->M->D
- D类对象消亡时析构函数的执行次序:D->M->B
- 初始化:由基类和派生类共同完成
-
派生类拷贝构造函数
- 派生类隐式拷贝构造函数
- 对派生类中新定义的成员进行拷贝初始化外
- 调用基类的拷贝构造函数实现对基类成员的初始化
- 派生类自定义拷贝构造函数
- 在默认情况下调用基类的默认构造函数对基类成员初始化
- 需要在“成员初始化表”中显式地指出调用基类的拷贝构造函数来实现对基类成员的初始化
- 派生类隐式拷贝构造函数
-
派生类对象的赋值操作
-
派生类隐式赋值操作
- 对派生类新定义的成员进行赋值
- 调用基类的赋值操作对基类成员进行赋值
-
派生类自定义赋值操作
- 不会自动调用基类的赋值操作
-
需要在自定义的赋值操作符重载函数中显式地指出调用基类的赋值操作
-
5.2-虚函数与消息的动态绑定
-
消息的多态性:相同的一条消息可以发送到不同类的对象,从而会得到不同的解释
-
消息的静态绑定:在编译时刻根据对象的类型来决定采用哪一个消息处理函数
-
消息的动态绑定
- 定义:在运行时刻,根据函数中参数实际引用(或指向)的对象来决定采用哪一个消息处理函数
- 用虚函数指出动态绑定(virtual)
-
虚函数
-
格式:virtual <成员函数声明>;
-
作用
- 指定消息采用动态绑定
- 指出基类中可以被派生类重定义的成员函数
- 重定义(override):对于基类中的一个虚函数,在派生类中定义的、与之具有相同型构的成员函数是对基类该成员函数的重定义
- 相同型构
- 派生类中定义的成员函数的名字、参数个数和类型与基类相应成员函数相同
- 其返回值类型与基类成员函数返回值类型或者相同,或者是基类成员函数返回值类型的public派生类
-
说明
- 只有类的成员函数才可以是虚函数,但静态成员函数不能是虚函数
- 构造函数不能是虚函数,析构函数可以(往往)是虚函数
- 只要在基类中说明了虚函数,在派生类、派生类的派生类、...中,与基类同型构的成员函数都是虚函数
- 只有通过基类的指针或引用访问基类的虚函数时才进行动态绑定
- 基类的构造函数和析构函数中对虚函数的调用不进行动态绑定
-
通过基类指针访问派生类中新定义的成员
class A { ....... public: virtual void f(); }; class B: public A { ...... public: void f(); void g(); }; A *p; ...... B *q=dynamic_cast<B *>(p); if (q != NULL) q->g();
-
需要定义虚函数的情况
- 在设计基类时,有时虽然给出了某些成员函数的实现,但实现的方法可能不是最好,今后可能还会有更好的实现方法
- 在基类中根本无法给出某些成员函数的实现,它们必须由不同的派生类根据实际情况给出具体的实现(抽象类与纯虚函数)
-
5.3-抽象类、多继承
-
纯虚函数
- 定义:没给出实现的虚函数
- 格式:virtual <成员函数声明>=0;
-
抽象类
- 定义:包含纯虚函数的类称为抽象类
- 抽象类不能用于创建对象
- 作用
- 为派生类提供一个基本框架
- 为同一个功能的不同实现提供一个抽象描述(接口)
-
多继承
-
定义:派生类可以有一个以上的直接基类
-
格式
class <派生类名>: [<继承方式>] <基类名1>, [<继承方式>] <基类名2>, … { <成员说明表> };
-
优点:增强了语言的表达能力,它使得语言能够自然、方便地描述问题领域中的存在于对象类之间的多继承关系
-
主要问题
- 名冲突问题(解决:基类名受限)
- 重复继承问题(解决:虚基类)
-
虚基类
- 在继承时用virtual声明
- 虚基类构造函数的调用
- 虚基类的构造函数由该类(继承虚基类的类)的构造函数直接调用
- 虚基类的构造函数优先非虚基类的构造函数执行
-
6.IO
C++中,IO是由具体的实现作为标准库的功能提供
6.1-面向控制台的IO
-
用于进行控制台IO操作的IO对象(#include <iostream>)
- cin(istream类的对象):对应着计算机系统的标准输入设备。(通常为键盘)
- cout(ostream类的对象):对应着计算机系统的标准输出设备。(通常为显示器)
- cerr和clog(ostream类的对象):对应着计算机系统用于输出特殊信息(如程序错误信息)的设备。(通常也对应着显示器,但不受输出重定向的影响)。cerr为不带缓冲的,clog为带缓冲的
-
控制台输出操作
-
使用插入操作符(<<)
- 特殊情况:输出指向字符的指针时,并不是输出指针的值,而是输出它指向的字符串;若要输出该指针的值,需要将其转换为其他类型的指针(如void *)
-
使用ostream类的成员函数(基于字节)
//输出一个字节。 ostream& ostream::put(char ch); cout.put('A'); //输出p所指向的内存空间中count个字节。 ostream& ostream::write(const char *p,int count); char info[100]; int n; ...... cout.write(info,n);
-
-
输出格式控制(#include <iomanip>)
操纵符 含义 endl 输出换行符,并执行flush操作 flush 使输出缓存中的内容立即输出 dec 十进制输出 oct 八进制输出 hex 十六进制输出 setprecision(int n) 设置浮点数的精度(由输出格式决定是有效数字的个数还是小数点后数字的位数) setiosflags(long flags)/resetiosflags(long flags) 设置/重置输出格式,flags的取值可以是:ios::scientific(以指数形式显示浮点数),ios::fixed(以小数形式显示浮点数),等等 -
控制台输入操作
-
使用抽取操作符(>>)
-
使用istream类的成员函数(基于字节)
//输入一个字节到ch中。 istream& istream::get(char &ch); //输入count个字节至p所指向的内存空间中。 istream& istream::read(char *p,int count); //输入一个字符串放入p指向的内存空间中。输入过程直到输入了count-1个字符或遇到delim指定的字符为止,并自动在最后加上一个'\0'字符。 istream& istream::get(char *p, int count, char delim='\n'); istream& istream::getline(char *p, int count, char delim='\n'); //跳过输入缓存中的若干字符: cin.ignore(n,'\n'); //跳过输入缓存中n个字符,或碰到回车 //用getline输入一行时,当输入串的长度超出指定大小,这会导致cin进入错误状态,后续输入不能正常进行,这时,可采用下面方法处理: cin.getline(str, 5); //最多读入4个字符 if (cin.fail()) //如果本行还有未读的字符 { cin.clear(); //撤销错误状态 cin.ignore(20, '\n'); //把本行未读完的字符从缓存删除 }
-
-
操作符>>和<<的重载
6.2-面向文件的IO
-
需求
- 程序运行结果有时需要永久性地保存起来,以供其它程序或本程序下一次运行时使用
- 程序运行所需要的数据也常常要从其它程序或本程序上一次运行所保存的数据中获得
-
文件数据的存储方式
- 文本方式:由可显示的字符和控制字符的编码字节构成;一般存储有行结构的文字数据
- 二进制方式:由纯二进制字节构成
-
文件的读写过程
- 打开文件:把程序内部的一个表示文件的变量/对象与外部的一个具体文件关联起来,并创建内存缓冲区
- 文件读/写:存取文件中的内容
- 关闭文件:把暂存在内存缓冲区中的内容写入到文件中,并归还打开文件时申请的内存资源(包括内存缓冲区)
-
文件的位置指针
-
在利用I/O类库中的类进行文件的输入/输出时,需包含<iostream>、<fstream>
-
文件的输出操作
-
打开文件:创建ofstream类的一个对象,并建立它与外部某个文件之间的联系
-
直接方式
ofstream out_file(<文件名> [,<打开方式>]);
-
间接方式
ofstream out_file; out_file.open(<文件名> [,<打开方式>]);
-
打开方式
- ios::out
- 打开一个外部文件用于写操作
- 如果外部文件已存在,则首先把它的内容清除;否则,先创建该外部文件(内容为空)
- ios::out是默认打开方式
- ios::app
- 打开一个外部文件用于添加操作(不清除文件内容,文件位置指针在末尾)
- 如果外部文件不存在,则先创建该外部文件(内容为空)
- ios::out | ios::binary 或 ios::app | ios::binary
- 按二进制方式打开文件。(默认的是文本方式)
- 对以文本方式打开的文件,当输出的字符为'\n'时,在某些平台上(如:Windows平台)将会自动把它转换成'\r'和'\n'两个字符写入外部文件。
- 对以二进制方式打开的文件,对输出的字节不做任何转换,原样输出。
- ios::out
-
-
判断打开操作是否成功
if (!out_file.is_open()) //或:out_file.fail() //或:!out_file { ...... //失败处理 }
-
输出数据
//按文本方式输出数据 ofstream out_file("d:\\myfile.txt",ios::out); if (!out_file) exit(-1); out_file << x << ' ' << y << endl; //输出:12 12.3 //按二进制方式输出数据 ofstream out_file("d:\\myfile.dat",ios::out|ios::binary); if (!out_file) exit(-1); out_file.write((char *)&x,sizeof(x)); //输出:4个字节 out_file.write((char *)&y,sizeof(y)); //输出:8个字节
-
关闭文件
out_file.close();
-
-
文件的输入操作
-
打开文件:创建ifstream类的一个对象,并把它与外部文件建立联系
-
直接方式
ifstream in_file(<文件名> [,<打开方式>]);
-
间接方式
ifstream in_file; //用默认构造函数初始化 in_file.open(<文件名> [,<打开方式>]);
-
打开方式
- ios::in:n打开一个外部文件用于读操作(默认)
- ios::in | ios::binary
- 按二进制方式打开文件(默认为文本方式)
- 对以文本方式打开的文件,当连续读入的两个字符是'\r'和'\n'时,在某些平台上(如:Windows平台)将自动转换成一个字符'\n'
- 对以二进制方式打开的文件,对输入的字节不做任何转换,原样输入
-
-
判断打开操作是否成功(同文件输出)
-
输入数据
//按文本方式输入数据 ifstream in_file("D:\\myfile.txt",ios::in); if (!in_file) exit(-1); in_file >> x >> y; //按二进制方式输入数据 ifstream in_file("D:\\myfile.dat",ios::in|ios::binary); if (!in_file) exit(-1); in_file.read((char *)&x,sizeof(x)); in_file.read((char *)&y,sizeof(y));
-
判断读入操作是否成功
//返回true表示操作失败,返回false表示操作成功 bool ios::fail() const;
-
关闭文件
in_file.close();
-
-
文件输入和输出、读写和打开时的存储方式应相同
-
以二进制方式存取文件不利于程序的兼容性和可移植性
-
既能输入、又能输出的文件(打开方式)
- ios::in|ios::out(可在文件任意位置写)
- ios::in|ios::app(只能在文件末尾写)
-
文件的随机存取
-
指定文件内部读指针的位置
istream& istream::seekg(<位置>);//指定绝对位置 istream& istream::seekg(<偏移量>,<参照位置>); //指定相对位置 streampos istream::tellg(); //获得指针位置
-
指定文件内部写指针的位置
ostream& ostream::seekp(<位置>);//指定绝对位置 ostream& ostream::seekp(<偏移量>,<参照位置>); //指定相对位置 streampos ostream::tellp(); //获得指针位置
-
参照位置
- ios::beg(文件头)
- ios::cur(当前位置)
- ios::end(文件尾)
-
一般用于以二进制方式存贮的文件
-
7.异常处理
运行异常:程序设计对程序运行环境考虑不周而造成的程序运行错误
为了保证程序的鲁棒性,必须在程序中对可能出现的异常错误进行预见性处理
7.1-异常的异地处理、结构化异常处理
-
C++结构化异常处理机制
-
启动异常处理机制:把有可能出现异常的一系列操作(语句或函数调用)放在一个try语句块中
-
生成异常对象:如果try语句块中的某个操作在执行中发现了异常,则通过执行一个throw语句生成一个异常对象,接在throw之后的操作不再进行
-
捕获异常对象:生成的异常对象由程序中能够处理这个异常的地方通过catch语句块来捕获并处理之
void f(char *filename) { ifstream file(filename); if (file.fail()) throw filename; //生成异常对象 int x; cin >> x; ...... return; } int main() { char str[100]; ...... try { f(str); } //启动异常处理机制 catch (char *fn) //捕获异常对象 { ...... //处理异常 } ...... //正常情况 }
-
-
try语句(格式:try { <语句序列> })
-
throw语句
- 格式:throw <表达式>;
- 执行throw指令后,接在后面的语句不再继续执行,而是转向异常处理
-
catch语句
- 格式: catch (<类型> [<变量>])
- <类型>用于指出捕获何种异常对象
- <变量>用于存储捕获到的异常对象。它可以缺省,表明catch语句块只关心异常对象的类型,而不考虑具体的异常对象
- catch语句块要紧接在某个try语句的后面
- 一个try语句块的后面可以跟多个catch语句块,用于捕获并处理不同类型的异常对象,它们采用精确匹配与throw所产生的异常对象进行绑定
- 若try语句块的<语句序列>执行中没有抛掷异常对象,则继续执行try语句块后的非catch语句
- 若try语句块的<语句序列>执行中抛掷了异常对象
- 若该try语句块之后有能够捕获该异常对象的catch语句,则执行这个catch语句中的<语句序列>,然后继续执行之后的非catch语句
- 若该try语句块之后没有能够捕获该异常对象的catch语句,则按嵌套的异常处理规则进行处理
-
异常处理的嵌套
- 在try语句块的语句序列执行过程中还可以包含try语句块
- 规则
- 当在内层的try语句的执行中产生了异常,则首先在内层try语句块之后的catch语句序列中查找与之匹配的处理;如果内层不存在能捕获相应异常的catch,则逐步向外层进行查找
- 如果生成的异常对象在程序的函数调用链上没有给出捕获,则调用系统的terminate函数(默认调用abort函数)进行标准的异常处理
-
基于断言的程序调试
- 测试:发现程序存在的错误
- 调试:对错误定位
- 常用手段
- 利用调试工具
- 在程序中的某些地方加上一些输出语句,在程序运行时把一些调试信息(如变量的值)输出
- 断言
- 帮助对程序进行理解和形式化验证。
- 在程序开发阶段,帮助开发者发现程序的错误和进行错误定位
- 宏assert(#include <cassert>)
- 格式:assert(<表达式>);
- 常用手段
8. 范型、STL、函数式程序设计
8.1-函数模板、类模板
-
类属
- 定义:一个程序实体能对多种类型的数据进行操作的特性
- 具有类属特性的程序实体:类属函数;类属类
- 实现类属函数的机制
- 采用通用指针类型的参数(void *)
- 函数模板
-
函数模板
-
格式
template <class T1, class T2, ...> //class也可以写成typename <返回值类型> <函数名>(<参数表>) { ...... }
-
实例化:给函数模板提供一个具体类型(通常为隐式)
-
有时,编译程序无法根据调用时的实参类型来确定所调用的模板实例函数
-
解决办法:显式类型转换;显式实例化
template <class T> T max(T a, T b) { return a>b?a:b; } ...... int x,y,z; double l,m,n; //显式类型转换 max((double)x,m); //实例化:double max(double a,double b) max(x,(int)m); //实例化:int max(int a,int b) //显式实例化 max<double>(x,m); //实例化:double max(double a,double b) max<int>(x,m); //实例化:int max(int a,int b)
-
-
带非类型参数的函数模板:使用时需显式实例化
-
-
类模板
-
格式
template <class T1,class T2,...> //class也可以写成typename class <类名> { <类成员说明> }
-
实例化:需显式指出
-
带非类型参数的类模板:需显式实例化
-
-
模板的复用
// file1.h template <class T> class S //类模板s的定义 { T a; public: void f(); }; template <class T> void S<T>::f() //类模板S中f的实现 { ...... } extern void func(); // file1.cpp #include "file1.h" void func() { S<float> x; //实例化“S<float>”并创建该类的一个对象x x.f(); //实例化“void S<float>::f()”并调用之 }
-
重复实例的处理
- 由开发环境来解决:记住已编译过的模块信息,编译第二个模块的时候不生成重复实例
- 由链接程序来解决:相同的实例只保留一个,其余的舍弃
-
类模板的友元函数
friend void f<T>(A<T>& a); //f的实例与A的实例是一对一友元 template <class T1> friend void f(A<T1>& a);//f的实例与A的实例是多对多友元
8.2-STL中的容器类模板、算法模板、迭代器
-
STL中的容器类模板
- vector
- list
- deque
- stack
- queue
- priority_queue
- map、multimap
- set、multiset
- basic_string(string、wstring)
-
迭代器
- 属于一种智能指针,指向容器中的元素
- 分类
- 输出迭代器
- 输入迭代器
- 前向迭代器
- 双向迭代器
- 随机访问迭代器
- 各容器的迭代器类型
- 随机访问迭代器:vector、deque、basic_string
- 双向迭代器:list、map/multimap、set/multiset
- 不支持:queue、stack、priority_queue
-
算法模板
-
算法与容器之间的关系
- 将容器的某些迭代器传给算法,在算法中通过迭代器访问和遍历相应元素
- 提高算法的通用性
-
算法的操作范围
- 一般用两个迭代器指出操作的元素的范围
- 可有多个操作范围(同一操作范围的迭代器必须属于同一容器)
- 目标范围内已有元素个数不能小于源范围中元素个数
-
算法的自定义操作条件
-
定义:一个函数/函数对象,其参数类型为相应容器的元素类型,返回值类型为bool
-
分类
- 一元谓词
- 二元谓词
vector<int> v; //从大到小排序,这里的排序条件采用了λ表达式(函数对象) sort(v.begin(),v.end(),[](int x1, int x2) { return x1>x2; });
-
-
算法的自定义操作
-
定义:一个函数/函数对象,其参数和返回值类型由相应的算法决定
-
分类
- 一元操作
- 二元操作
void display(int x) { cout << ' ' << x; } vector<int> v; for_each(v.begin(),v.end(),display); //对v中的每个元素去调用 //函数display进行操作
-
-
8.3-函数式程序设计
-
程序设计范式
- 命令式程序设计范式
- 定义:针对一个目标,需要给出达到目标的操作步骤
- 代表:过程式;面向对象
- 声明式程序设计范式
- 定义:只需要给出目标,不需要对如何达到目标(操作步骤)进行描述
- 代表:函数式;逻辑式
- 命令式程序设计范式
-
函数式程序设计
-
基本特征
- “纯”函数(引用透明、无副作用)
- 没有状态(无赋值操作)
- 函数也是值(高阶函数)
- 递归是主要的控制结构
- 表达式的惰性(延迟)求值
- 潜在的并行性
-
C++支持函数式编程的主要语言机制
- 递归函数
- 函数对象与λ表达式
- 基于范围的for
- STL
-
基本手段
-
递归(通常采用尾递归)
-
尾递归
-
定义:递归调用是函数执行的最后一步操作
-
尾递归转换为迭代
//一般形式的尾递归 T f(T1 x1, T2 x2, ...) { ...... if (...) return f(m1,m2,...); ...... if (...) return f(n1,n2,...); ...... } //转换成等价的迭代 T f(T1 x1, T2 x2, ...) { while (true) { ...... if (...) { T1 t1=m1; T2 t2=m2; ... x1 = t1; x2 = t2; ... continue;} ...... if (...) { T1 t1=n1; T2 t2=n2; ... x1 = t1; x2 = t2; ... continue;} ...... } }
-
-
-
过滤/映射/规约操作
-
过滤:把一个集合中满足某条件的元素选出来,构成一个新的集合(如:求某整数集合中正整数)
-
映射:对一个集合中的每个元素分别进行某个操作,结果放到一个新集合中(如:求某整数集合中各整数的平方)
-
规约:对一个集合中的所有元素连续进行某个操作,最后得到一个值(如:求某整数集合中所有整数之和)
-
Ranges库(C++20)(#include <ranges>)
-
范围库中算法无需使用迭代器
-
视图(Views)机制
-
不拥有实际数据,只是实际数据的虚拟映射
-
属于惰性求值
vector<int> numbers={ 5, 3, 1, 4, 2 }; auto squares=ranges::transform_view(numbers, [](int x) { return x*x; }); for (auto n: squares) { cout << n << ' '; }; //25 9 1 16 4 auto evens=ranges::filter_view(numbers, [](int x) { return x%2==0; }); for (auto n: evens) { cout << n << ' '; }; //4 2
-
多个视图可以组合
vector<int> numbers = { 5, 3, 1, 4, 2 }; auto even_squares=ranges::transform_view(ranges::filter_view(numbers,[](int x) { return x%2==0; }),[](int x) { return x * x; }); for (auto n: even_squares) { cout << n << ' '; }; //16 4 //组合操作也可以写成流水线式的管道形式 auto even_squares= numbers | views::filter([](int x) { return x%2==0; }) | views::transform([](int x) { return x*x; }); for (auto n: even_squares) { cout << n << ' '; }; //16 4
-
-
-
-
部分函数应用
-
定义:对一个多参数的函数,只给它的某些参数提供值,从而生成一个新函数,该新函数不包含原函数中已提供值的参数
-
通过bind实现
#include <functional> using namespace std; using namespace std::placeholders; void print(int n,int base); function<void(int)> print10=bind(print,_1,10); //或者 auto print10=bind(print,_1,10);
-
-
柯里化
-
定义:把一个多参数的函数变换成一系列单参数的函数
-
意义:不必把一个多参数的函数所需要的参数同时提供给它,可以逐步提供
int add(int x,int y) { return x+y; } function<int (int)> add_cd(int x) //返回值是个单参数函数 //或者,auto add_cd(int x) { return bind(add,x,_1); //或 return [x](int y)->int { return add(x,y); }; } ...... cout << add_cd(1)(2);
-
-
-
9.事件驱动的程序设计
9.1-事件驱动的程序结构
- 特点
- 程序的任何一个动作都是由某个事件激发的
- 事件可以是用户的键盘、鼠标、菜单等操作
- 每个事件都会向应用程序发送一些消息
- WM_KEYDOWN/WM_KEYUP(键盘按键)
- WM_CHAR(按键有对应的字符)
- WM_LBUTTONDOWN/WM_LBUTTONUP/WM_LBUTTONDBLCLK/WM_MOUSEMOVE (鼠标按键)
- WM_COMMAND(菜单选取)
- WM_PAINT(窗口内容刷新)
- WM_TIMER(设置的定时器时间到了)
- ......
- 每个应用程序都有一个消息队列
- Windows系统会把属于各个应用程序的消息放入各自的消息队列
- 应用程序不断地从自己的消息队列中获取消息并处理之,当取到某个特定消息后,消息循环结束
- 每个窗口都有一个消息处理函数
- 大部分的消息都关联到某个窗口
- 应用程序取到消息后将会去调用相应窗口的消息处理函数
9.2-面向对象的事件驱动程序设计
- Windows应用程序中的对象
- 窗口对象
- 显示程序的处理数据
- 处理Windows的消息、实现与用户的交互
- 窗口对象类之间可以存在继承和组合关系
- 文档对象
- 管理在各个窗口中显示和处理的数据
- 文档对象与窗口对象之间可以存在1:1和1:n的关系
- 应用程序对象
- 管理属于它的窗口对象和文档对象
- 实现消息循环
- 它与窗口对象及文档对象之间构成了组合关系
- ......
- 窗口对象
- 支持Windows应用开发的类库
- MFC
- Qt
- ......
- 应用程序框架
- 定义:一种通用的、可复用的应用程序结构,该结构规定了程序应包含哪些组件以及这些组件之间的关系,它封装了程序处理流程的控制逻辑
- 优点:使应用的开发速度更快、质量更高、成本更低