admin管理员组文章数量:1630019
C++语言程序设计 第5版
- 1.前言
- 2.内容
- 第一章 绪论
- 1.1语言发展
- 1.1.1 概述 机器语言——>汇编语言——>高级语言(包括C++)
- 1.1.2 面向对象
- 结构化程序设计(面向过程的程序设计方法)
- 面向对象的方法
- 1.2 信息存储
- 1.2.1 二进制
- 1.2.2 进制转换
- 1.2.3 信息存储单位
- 1.2.4 二进制数编码表示(原、反、补)
- 1.2.5 程序开发概念
- 第二章 C++语言简单程序设计
- 2.1 概述
- 2.1.1 C++语言特点
- 2.1.2 词法记号
- 2.2 基本数据类型和表达式
- 2.2.1 基本数据类型
- 2.2.2 常量
- 2.2.3 变量
- 2.2.4 运算符与表达式
- 2.3 数据的输入输出
- 2.3.1 I/O流
- 2.3.2 I/O格式控制
- 2.3.2.1 输入输出补充:setw、setprecision、fixed
- 2.4 控制结构
- 2.5 类型别名、auto
- 第三章 函数
- 3.1 函数的定义与使用
- 递归的小例子
- 1. 阶乘
- 2.汉诺塔问题 [书上 P76 例3-10]
- 汉诺塔进阶问题[洛谷] [高精度 + 递归]
- 3.2 函数参数传递
- 3.3 内联函数
- 3.4 带默认形参值的函数
- 3.5 函数重载
- 3.6 C++系统库函数
- 第四章 类与对象
- 4.1 面向对象的基本特点
- 4.1.1 抽象
- 4.1.2 封装
- 4.1.3 继承
- 4.1.4 多态
- 4.2 类和对象
- 4.2.1 类的定义和使用
- 4.2.2 类成员的访问控制
- 4.2.3 对象
- 4.2.4 类的成员函数
- 4.3 构造函数
- 4.4 拷贝构造函数
- 一个有趣的问题:为什么拷贝函数的参数是引用?
- 4.4重点 拷贝构造函数调用的三种情况
- 实例[调用拷贝构造函数的方式]
- 小提问[变形 + 拓展]
- 4.5 析构函数
- 4.6 类的组合
- P115页,左值、右值可自行查看 [回头会补上,暂时鸽了]
- 4.6重点 构造、析构函数的调用顺序
- 4.7 类的组合
- 4.8 UML图形标识
- 4.9 结构体 struct
- 4.10 组合体 union(联合体)
- 4.11 枚举类型 enum
- 4.11.1 不限定作用域的枚举类型
- 4.11.2 限定作用域的枚举类
- 4.11.3注意
- 共用体 和 枚举类型 混合使用实例
- 第五章 数据的共享与保护
- 5.1 标识符的作用域与可见性
- 5.1.1 作用域
- 作用域实例
- 5.1.2 可见性
- 5.2 对象(包括类的对象 + 简单变量)的生存期
- 5.2.1 静态生存期
- 5.2.2 动态生存期
- 简单变量的生存期与可见性实例
- 对象的生存期实例
- 5.3 类的静态成员
- 5.3.1 类的静态数据成员
- 5.3.2 静态函数成员
- 深入理解静态数据成员、静态函数成员
- 5.4 类的友元
- 5.4.1 友元函数
- 5.4.2 友元类
- 5.4.2 重点 友元类的注意事项
- 5.5 共享数据的保护
- 5.5.1 常对象
- 初始化 与 赋值
- 5.5.2 用const修饰的类成员
- 5.5.3 常引用
- 5.6 多文件结构和编译预处理命令
- 5.6.1 C++程序的一般组织结构
- 5.6.2 外部变量与外部函数
- 5.6.3 标准C++库
- 5.6.4 编译预处理
- 第六章 数组、指针与字符串
- 6.1 数组
- 6.1.2数组的注意点
- 1.数组初始化时,元素的值?
- 2.范围for语句
- 3.声明为常量的数组,必须给定初值
- 4.数组作为参数时,一般不指定数组第一维大小,即使指定也会被忽略
- 5.对象数组
- 6.2 指针
- 指针重点识记!!
- 1. * 与 &
- 指向常量的指针 、 指针常量
- 2.void指针
- 3.空指针
- 4.用指针处理数组元素
- 5.指针数组、数组指针
- 6.指针型函数、 指向函数的指针
- 7.对象指针
- 8.this指针
- 9.指向类的非静态成员的指针 、 指向类的静态成员的指针
- 6.3 动态内存分配
- 1.new创建
- 2.delete删除
- 3.new数组
- 6.4用vector创建数组对象
- 6.5 深拷贝 、浅拷贝
- 6.6 字符串
- 第七章 类的继承
- 7.1 基类与派生类
- 1.派生类定义
- 2.类派生的过程
- 7.2 访问控制
- 1.公有继承 public
- 2.私有继承 private
- 3.保护继承
- 7.3类型兼容规则
- 7.4 派生类的构造和析构函数
- 1.构造函数
- 重点:派生类构造函数执行的一般顺序
- 2.拷贝构造函数
- 3.析构函数 + 构造函数 两者的调用顺序
- 7.5 派生类成员的标识与访问
- 1.多继承同名隐藏
- 多继承同名隐藏实例1 [并行继承,基类无交集]
- 多继承同名隐藏规则实例2[基类由共同基类继承而来]
- 2.虚基类
- 1.虚基类基本使用方式
- 2.虚基类及其派生类构造函数
- 重点:构造一个类的对象的一般顺序
- 第八章 多态性
- 8.1多态性的概述
- 1.多态类型
- 2.多态实现
- 8.2 运算符重载
- 1.重载规则
- 2.运算符重载为成员函数
- 复数类加减法运算重载----成员函数形式
- 重载单目运算符 ++ 为成员函数形式(前置、后置)
- 3.运算符重载为非成员函数
- 8.3虚函数
- 1.一般虚函数成员
- 2.虚析构函数
- 错误示例❌ (不设置虚析构函数)
- 正确做法✔
- 8.4纯虚函数与抽象类
- 1.纯虚函数
- 纯虚函数、函数体为空的虚函数
- 2.抽象类
- 3.总结
- 4.更新日志
1.前言
临近期末 整理笔记后续复习使用
前三章 大多关于C
后面的 才是真正C++精华
语法内容,直接背住即可;至于有些逻辑上的问题,应该深入了解
里面夹带一些自己整理的例题,或者是小tips
看不懂的地方,可以多看几遍或者回头再看,发现bug,欢迎提出~
2.内容
第一章 绪论
1.1语言发展
1.1.1 概述 机器语言——>汇编语言——>高级语言(包括C++)
软件:各种程序+文档资料
指令:计算机可识别的命令(所有指令的集合叫做指令系统)
汇编语言:将机器语言映射为一些可被读懂的助记符 eg:ADD
高级语言:屏蔽机器细节,采用有一定含义的数据命名和执行语句
面向对象的语言:为了能更直接地描述客观世界中存在的事物
1.1.2 面向对象
面向对象的编程语言将客观事物看作具有属性和行为(或称服务)的对象,通过抽象找出同一类对象的共同属性和行为,形成类。
结构化程序设计(面向过程的程序设计方法)
①自顶向下、逐步求精;
②程序结构按功能划分为若干个基本模块,模块形成树状结构
③模块之间关系尽量简单,功能上相对独立
④每个模块内部均是由顺序、选择、循环结构组成
⑤模块化的实现具体方法是 使用子程序
面向对象的方法
①将数据 及 对数据的操作方法(即 函数 ) 封装在一起,作为对象
②对同类型对象抽象出共性,形成类
③类通过外部接口与外界发生关系,对象与对象之间通过 **消息(即 函数调用 )**进行通信
基本概念:
1.对象:描述客观事物的实体(由一组属性和一组行为构成) [直升飞机]
2.类:具有相同属性和服务的一组对象的集合 [飞机类]
3.封装:将对象的属性和服务组合成独立的系统单位,并尽可能隐蔽对象的细节
4.继承:特殊类拥有一般类的全部属性和服务 [儿子继承父亲 直升飞机类继承飞机类]
5.多态性:一般类中定义的行为,被特殊类继承后,可以有不同的实现 [同一个行为,根据调用对象不同,进行不同的响应]
(先大致了解即可,后续会详细介绍~)
面向对象软件开发流程:
分析——>设计——>编程——>测试——>维护
OOA——>OOD——>OOP——>OOT——>OOSM
1.2 信息存储
1.2.1 二进制
1.2.2 进制转换
1.R进制转换为十进制: 乘以对应的 权 再求和
2.十进制转换为R进制:
①整数部分 除R取余,倒读
②小数部分 乘R取整,正读
3.二、八、十六进制转换
每位八进制数相当于3位二进制数
每位十六进制数相当于4位二进制数
1.2.3 信息存储单位
位 bit
字节 byte/B 1B=8bit 1KB=1024B 1MB=1024KB 1GB=1024MB
字 word :独立的信息单位 (8位、16位、32位、64位)
机器字长 与机器硬件指标有关,一般指参加运算的寄存器所含有的二进制位数(32位 、 64位)
1.2.4 二进制数编码表示(原、反、补)
原码: 由于原码对于 +0 和 -0 的表示不同,所以才去寻找其它表示方法
反码: 原码除了符号位外,逐位取反 (相当于原码、补码转换的跳板)
补码: 反码+1
从原码到补码的转换:原码 除了符号位 按位取反再加1
从补码到原码也可以 除了符号位 按位取反再加1
注意:补码运算时,可能会 “溢出”,导致结果不是预想的
1.2.5 程序开发概念
源程序: 用源语言(汇编语言、高级语言…)编写的、有待翻译的程序
目标程序: 源程序通过翻译加工后生成的程序 (机器语言、汇编语言或其它中间语言)
翻译程序: 包括以下三种
①汇编程序 (将汇编语言编写的源程序 翻译为 机器语言形式的程序)
②编译程序 (将高级语言编写的源程序翻译为 机器指令)
③解释程序( 逐句 将高级语言编写的源程序翻译为 机器指令)
完整程序过程
编辑 ——> 编译 ——> 连接 ——> 执行
.cpp .obj .exe
第二章 C++语言简单程序设计
2.1 概述
2.1.1 C++语言特点
①尽量兼容C ②支持面向对象的方法
初步体验
#include <iostream>
using namespace std;
int main()
{
cout<<"Hello~"<<endl;
return 0;
}
2.1.2 词法记号
1.关键字
①基本数据类型: bool、char、short、int、float、double
②执行: default、case、continue、break、goto、static、extern、auto
③类: class、delete、friend、virtual、union、inline、operator、template
(涵盖常用的部分~)
2.操作符(运算符)
用于各种运算的符号 + - * / 以及 and(与) or(或) not(非) xor(异或)…
3.标识符
程序员定义的单词
规则: 大小写字母或下划线开头、由大小写字母下划线和数字组成、不能是C++的关键字或者操作符
4.文字
程序中直接使用符号表示的数据 包括 数字、字符、字符串、布尔文字
5.分隔符
用于分隔各个词法记号或程序正文
() {} , : ;
6.空白
空格、制表符(TAB)、垂直制表符、换行符、回车符、注释
2.2 基本数据类型和表达式
2.2.1 基本数据类型
2.2.2 常量
1.整型常量:正整数、负整数、零 eg: 123 0123 0x5af 123ll(ll表示类型至少为 long long)
2.实型常量:
①一般形式 12.5 -12.5
②指数形式 3.1415e+3 即 3.1415 * 103
3.字符常量
单引号括起来的字符 ‘a’ ‘!’
4.字符串常量
双引号括起来的字符 “abcde”
5.布尔常量
true 、 false
6.符号常量
const int PI=3.14159;
//PI即为符号常量,表示3.14159
2.2.3 变量
1.声明形式:
数据类型 变量名1,变量名2.....
int i;
2.变量的初始化
在定义变量的同时,对其设置初始值
//四种方式
int i=10;
int i={10};
int i{10};
int i(10);
3.变量的存储类型
auto 暂时性存储,其存储空间可被若干变量多次覆盖使用
register 放在通用寄存器中,访问的速度较快
extern 在所有函数、程序段中均可引用
static 内存中固定地址存放 ,整个程序运行期间均有效
2.2.4 运算符与表达式
表达式是计算求值的基本单位
①算术运算符:
+ - * / % ++ --
int i=10;
cout<<i++; //输出10 此时i为11
int i=10;
cout<<++i //输出11 此时i为11
②赋值运算符
a=4;
a=b=9;
③逗号运算
a=3*5,a*4; //结果为60,但a的值为15(因为并非a*=60)
④逻辑运算
关系运算是一种比较简单的逻辑运算
优先级较高 < <= > >=
较低 == !=
其余
优先级逐渐降低
! && ||
非 与 或
注意:
&& || 具有 “短路” 特性,即运行时先对第一个操作数求值,
对于 && 而言,如果第一个操作数其值为false,则不再对第二个操作数求值
对于|| 而言,如果第一个操作数其值为true,则不再对第二个操作数求值
⑤条件运算符
表达式1?表达式2:表达式3
先计算表达式1,如果表达式1为true,则执行表达式2,否则执行表达式3
//输出a,b中较大的数字
int a=3,b=5;
cout<< a>b ? a : b;
⑥sizeof运算符
sizeof运算符用于计算某种类型的对象在内存中所占的字节数
sizeof(int);
int a;
sizeof(a);
⑦位运算
& | ^
按位与 按位或 按位异或
全1为1 有1则1 相同为0,不同为1
⑧运算符的优先级和结合性
⑨混合运算时数据类型转换
转换基本原则:由低类型数据转换为高类型数据
char(unsigned) short(unsigned) int(unsigned) long(unsigned) long long float double
低 高
2.3 数据的输入输出
2.3.1 I/O流
cin 输入
cout 输出
<< 预定义的插入符
>> 提取符
2.3.2 I/O格式控制
头文件 iomanip
例子: 输出浮点数3.14159,设置域宽为5个字符,小数点后保留两位有效数字
#include <iostream>
using namespace std;
#include <iomanip>
int main()
{
double a = 3.14159;
cout << setw(5) << setprecision(3) << a << endl;
//域宽5 //包括小数点,共3位
return 0;
}
上图中 在3前面有一个空格字符,加上小数点一个字符,共5个字符,即域宽为5
2.3.2.1 输入输出补充:setw、setprecision、fixed
1.setw
setw(n)
作用效果:设置字段的宽度,即域宽
作用范围:只对紧接着的输出产生作用
补充说明:当字段长度小于n时,前面用空格补齐,超出n,则原样输出
#include <iostream>
using namespace std;
#include <algorithm>
#include <iomanip>
const double PI = acos(-1);
int main()
{
cout << PI << endl;
cout << setw(8) << PI << endl;
cout << PI;
return 0;
}
2.setprecision
setprecision(n)
作用效果:设置保留的宽度(包括小数点)
作用范围:其之后的所有范围
#include <iostream>
using namespace std;
#include <algorithm>
#include <iomanip>
const double PI = acos(-1);
int main()
{
cout << PI << endl;
cout << setprecision(4) << PI << endl << PI << endl;
cout << PI;
return 0;
}
3.fixed
如果一个数字太大,无法使用 setprecision 指定的有效数位数来打印,则许多系统会以科学表示法的方式打印,为了防止出现这种情况,可以使用另一个流操作符 fixed,它表示浮点输出应该以固定点或小数点表示法显示
cout<<fixed;
作用范围:其之后的所有范围
#include <iostream>
using namespace std;
#include <algorithm>
#include <iomanip>
const double PI = acos(-1);
int main()
{
cout << PI << endl;
//两者结合起来,实质上就是保留小数点后的位数
cout << fixed << setprecision(3) << PI << endl << PI << endl;
cout << PI << endl;
return 0;
}
小彩蛋~:
对输入输出感兴趣还可自行学习 cerr 、 clog等等
2.4 控制结构
if
if else
if else if else (嵌套)
while
do while {};
for() [范围for语句挺方便的,着重学习掌握一下]
例子:
遍历数组a
int a[5]={1,2,3,4,5};
for(int i=0;i<5;i++)
cout<<a[i]<<' ';
//等价于
for(auto x:a) //相当于用临时变量x替代a中的每个元素,并依次遍历输出 [a必须为一个序列 如:数组,vecotr string 等类型]
cout<<x<<' ';
2.5 类型别名、auto
typedef long long ll;
//用 ll 替代原来的 long long 两者表示的含义相同
long long a=10;
//等价于
ll a=10;
auto 即令编译器自己判断变量的类型(可能猜错)
double a=3.5;
auto b=a;
cout<<b;
第三章 函数
一个较复杂的系统,往往需要划分为若干的子系统,然后对这些子系统分别进行进行开发和调试
高级语言中的子程序就是用来实现这种模块划分的
而在C和C++语言中,子程序体现为函数
【通常会将 相对独立、经常使用的功能抽象为函数】
3.1 函数的定义与使用
//函数定义
类型说明符 函数名(形参列表)
{
}
//函数(原型)声明 (函数调用之前,要进行函数声明)
类型说明符 函数名(形参列表);
//函数调用
函数名(实参列表)
//函数调用形式:
1.直接调用
2.嵌套调用 A调用B,B调用C...
3.递归调用 函数直接或间接地调用自身(注意写好 递归base,把握边界条件,防止无限递归、爆栈)
递归的小例子
1. 阶乘
洛谷 P5739
#include <iostream>
using namespace std;
typedef long long LL;
LL jc(int n)
{
if (n == 1) return 1; //递归base[递归边界]
return n * jc(n - 1);
}
int main()
{
int n;
cin >> n;
cout << jc(n);
return 0;
}
2.汉诺塔问题 [书上 P76 例3-10]
//汉诺塔问题
#include <iostream>
using namespace std;
int move_times;
void move_way(char a, char b) //移动方式
{
printf("%c --> %c\n", a, b);
}
void Hanio(int n, char From, char Temp, char To) //From 经由 Temp 转移至 To
{
//递归base
if (n == 1) { //From上仅剩一只圆盘,此时可直接移动
++move_times;
move_way(From, To); //若需查看移动方式,再编写函数即可
return;
}
//此时不写else,是因为上面if如果符合条件,执行所有语句后会return,不可能继续执行下方代码
//分三步走
//1. 将From上方的n-1只圆盘,经由To,转移至 Temp
Hanio(n - 1, From, To, Temp);
//2. 将From上面仅剩的一只圆盘直接移动至 To 即可
++move_times;
move_way(From, To);
//3. 考虑如何移动 将Temp上面的 n-1 只圆盘 经由 From 转移至To [最终目标]即可
Hanio(n - 1, Temp, From, To);
}
int main()
{
int n;
cin >> n;
//目标是:将A上的所有圆盘,经由B,移到C上
Hanio(n, 'A', 'B', 'C');
cout << move_times;
return 0;
}
汉诺塔进阶问题[洛谷] [高精度 + 递归]
P1096 [NOIP2007 普及组] Hanoi 双塔问题
//汉诺双塔问题 [递归 + 高精度]
#include <iostream>
using namespace std;
#include <algorithm>
#include <vector>
#include <cstring>
const int N = 5000;
int res[N];
string Solv(string s) //高精度乘法 2x + 2
{
reverse(s.begin(), s.end());
vector<int> ts;
ts.push_back((s[0] - '0')* 2 + 2);
for (int i = 1; i < s.size(); ++i) ts.push_back((s[i] - '0') * 2);
string t;
int tmp = 0;
for (int i = 0; i < ts.size(); ++i)
{
int x = ts[i] + tmp;
tmp = x / 10;
t.push_back(x % 10 + '0');
}
if (tmp) t.push_back(tmp + '0');
reverse(t.begin(), t.end());
return t;
}
string Hanio(int n) //n个盘子 从src 经由 tmp 移至 des
{
//递归base
if (n == 2) {
return "2";
}
return Solv(Hanio(n - 2));
}
int main()
{
int n;
cin >> n;
n *= 2;
cout << Hanio(n);
return 0;
}
3.2 函数参数传递
函数参数传递指的就是形参和实参结合(简称 形实结合),有以下两种方式
1.值传递
发生函数调用时,才给形参分配内存空间,并用实参来初始化形参(直接将实参的值传递给形参) [这一过程是参数值的单向传递过程]
2.引用传递
形参为引用(引用可以被认为是变量的别名), 则可以通过形参的修改影响实参
void swap(int &x,int &y)
{
int t=x;
x=y;
y=t;
}
swap(a,b);
3.含有可变数量形参的函数
后续补充
3.3 内联函数
将功能简单、规模较小、使用频繁的函数,设计为内联函数,节省参数传递、控制转移的开销
(内联函数并非在调用时发生控制转移,而是在编译时将函数体嵌入在每个调用处)
inline 类型说明符 函数名(形参表)
{
语句序列
}
//注意:
1.inline关键字只是表示一个要求,但编译器并不承诺将inline修饰的函数作为内联函数;
同时,无inline修饰的函数也可能被编译为内联函数;
2.对自身进行直接调用递归的函数肯定无法以内联方式处理 【简单思考一下】
3.4 带默认形参值的函数
函数在定义时,可以预先声明默认的形参值
有默认值的形参必须在形参列表的最后!!!(因为实参和形参的结合顺序是从左向右)
在相同的作用域内,不允许在同一个函数的多个声明中对同一个参数的默认值重复定义,即使前后定义的值相同也不行。如下图:
3.5 函数重载
两个以上的函数,具有相同的函数名,但是形参个数或者类型不同,编译器根据实参和形参的类型,个数的最佳匹配,自动确定调用哪一个函数
补充:还有一种重载类型是根据类型中是否包含 const 修饰[具体说明 在 第五章 5.5.2 P166]
3.6 C++系统库函数
cmath 等 c开头的基本上是继承c而来的,不建议使用.h结尾的
第四章 类与对象
4.1 面向对象的基本特点
4.1.1 抽象
抽象:对具体问题(对象)进行概括,抽出一类对象的公共性质并加以描述的过程。
包括 数据抽象(即基本性质)、行为抽象(功能抽象、代码抽象)
前者描述某类对象的属性或状态,后者描述的是某类对象的共同行为或功能特征
eg:对于时钟类,数据抽象是指抽离出 时、分、秒 行为抽象是指 显示时间、设置时间
4.1.2 封装
将抽象所得到的数据和行为相结合,形成有机整体(将抽象出来的东西形成一个 ”类“)
即 将数据 与 函数 有机结合形成类
4.1.3 继承
特殊类与一般类的关系
继承使得特殊概念之间既能共享一般的属性和行为,又能具有特殊的属性和行为
4.1.4 多态
多态性:一段程序可以处理多种类型对象的能力
实现方式包括 重载多态[重载]、强制多态[强制类型转换]、包含多态[虚函数实现]、参数多态[模板]
4.2 类和对象
面向对象程序设计中,程序模块是由类构成的。
类是对逻辑上相关的函数与数据的封装,它是对问题的抽象描述。
定义一个类,便可以定义该类的变量,该变量称为该类的对象(类的实例化)
4.2.1 类的定义和使用
class 类名称{
public:
外部接口
private:
私有成员
protected:
保护成员
};
4.2.2 类成员的访问控制
对类成员权限的控制,可通过设置成员的访问控制属性而实现。
访问控制属性可以有以下3种:public 、private、protected
public:外部接口
private:只可本类成员访问,类外部任何访问都不可
protected: 与private类似,但是在继承时与private不同
4.2.3 对象
类的特定实体(实例)
类名 对象名
Clock my_clock;
注意:对象所占用的内存空间只是用于存放数据成员,函数成员并不在每个对象中存储副本,每个函数的代码在内存中只占据一份空间
4.2.4 类的成员函数
在类的外部,只可访问到类的公有成员,类的成员函数可以访问到类的全部成员
类外访问成员函数 用 .
注意:类的成员函数中,既可以访问目的对象的私有成员,又可以访问当前类的其他对象的私有成员。
1.带默认形参值的成员函数
2.内联成员函数
1.隐式声明:将函数体放在类内
2.显式声明:类外前面加上inline [为了类定义的简洁,比较推荐使用显式声明]
4.3 构造函数
在定义对象的时候进行数据成员设置,称为对象的初始化
构造函数:在对象被创建时,利用特定的值构造对象,将对象初始化为一个特定的状态
(在对象创建时自动被调用)
使用初始化列表初始化,效率更高,如果不需要对初始值进行复杂的计算,则首选初始化列表方式~
构造函数:[构造函数均可以使用初始化列表进行初始化]
1.默认构造函数(系统提供)
2.构造函数(为了完成自己想要的初始化)
3.拷贝构造函数(复制构造函数)
4.4 拷贝构造函数
使用一个已经存在的对象,去初始化同类的一个对象
参数为 本类对象的引用
class 类名{
public:
类名(形参表) //构造函数
类名(类名 &对象名) //拷贝构造函数
}
类名::类名(类名 &对象名)
{
函数体
}
一个有趣的问题:为什么拷贝函数的参数是引用?
因为当非引用传递时,也就是传值引用[传地址/指针均属于传值调用]时,会调用拷贝构造函数,而此时拷贝构造函数未完成,会造成无穷地递归调用
以下为VS2019检测的报错
4.4重点 拷贝构造函数调用的三种情况
(1)用类的一个对象去初始化类的另一个对象时
Point p1(1,2);
//以下两种均可
Point p2(p1);
Point p3=p2;
注意是初始化!!!并非赋值!!!
Point p1(1,2);
Point p2;
p2=p1;
//这样则不会调用拷贝构造函数,而是利用 = 的重载 进行赋值
(2)函数的形参是类的对象,调用函数时,进行形参和实参结合时
void fun(Point p)
{
cout<<p.getx()<<endl;
}
int main()
{
Point p1(1,2);
fun(p1); //调用函数时,拷贝构造函数被调用
}
(3)如果函数的返回值是类的对象,函数执行完成返回调用者时
Point fun()
{
Point p1(1,2);
retunr p1; //创建临时无名对象,执行拷贝构造函数
}
int main()
{
Point b=fun(); //无名对象在执行完成后消失
}
实例[调用拷贝构造函数的方式]
小提问[变形 + 拓展]
这样写会报错!
因为这样写相当于是在试图调用拷贝构造函数,但由于CopyTime()的返回值是临时变量,为了保证拷贝构造函数的正确性,编译器不允许将其赋值给非const引用~
如何更改?
在拷贝构造函数前面加上const即可
但为什么未显示调用拷贝构造函数?
这里和 编译器的 RVO(return value optimization)即 返回值优化有关
简单理解就是,编译器会尽可能地减少调用拷贝构造函数,对这种接受函数返回值来初始化类对象调用拷贝构造函数的情况,进行优化
详细的可点击跳转查看 或 自行百度
4.5 析构函数
完成对象被删除前的一些清理工作
在对象的生存期即将结束时自动被调用
析构函数不接受任何参数
~类名() {} //这样即可
函数体为空的析构函数未必不做任何事情,在后续有 虚析构函数会帮助调用其他派生类析构函数
析构函数不接受任何参数,但可以是虚函数(第八章)。
当不进行显示说明,系统也会生成一个函数体为空的隐含析构函数
4.6 类的组合
一个类中内嵌其他类的对象作为成员的情况
类名::类名 (所有所需形参):内嵌对象1(从所有形参中取出部分),内嵌对象2(同理)...
{类的初始化}
P115页,左值、右值可自行查看 [回头会补上,暂时鸽了]
4.6重点 构造、析构函数的调用顺序
(1)调用内嵌对象的构造函数,调用顺序按照 内嵌对象在组合类定义中出现的次序
(与 内嵌对象在构造函数的初始化列表中出现的顺序 无关!!!!)
(2)执行本类构造函数
析构函数调用顺序与构造函数调用顺序 相反~~
4.7 类的组合
#include <iostream>
using namespace std;
//线段类 类的组合 一个线段两个端点以及长度
#include <cmath>
class Point {
public:
Point(double x = 0, double y = 0) :x(x), y(y) {}
Point(Point& p);
double get_x() { return x; }
double get_y() { return y; }
private:
double x, y;
};
Point::Point(Point& p) //拷贝构造函数
{
x = p.x;
y = p.y;
cout << "调用Point类拷贝构造函数" << endl;
}
class Line {
public:
Line(Point p3, Point p4) :p1(p3), p2(p4) { len = sqrt((p3.get_x() - p4.get_x()) * (p3.get_x() - p4.get_x()) + (p3.get_y() - p4.get_y() * (p3.get_y() - p4.get_y()))); } //构造函数
Line(Line& l);
private:
Point p1, p2; //两个端点
double len; //线段长度
};
Line::Line(Line& l)//:p1(l.p1),p2(l.p2) //注意区分两种写法,决定是否调用拷贝构造函数
{
//如果放在里面写,则不会调用Point类的拷贝构造函数,因为利用的是 = 号的重载
p1 = l.p1;
p2 = l.p2;
len = l.len;
cout << "调用Line类拷贝构造函数" << endl;
}
int main()
{
Point p1(1, 2), p2(4, 5);
Line l1(p1, p2);
Line l2(l1);
return 0;
}
#include <iostream>
using namespace std;
class Point {
public:
Point(int xx = 0, int yy = 0);
Point(Point& p);
int Get_X() { return x; }
int Get_Y() { return y; }
~Point() {}
private:
int x, y;
};
Point::Point(int xx, int yy):x(xx),y(yy)
{
cout << " Point 构造函数" << endl;
}
Point::Point(Point& p) : x(p.x), y(p.y)
{
cout << " Point 拷贝构造函数" << endl;
}
class Line {
public:
Line(Point p1, Point p2);
Line(Line& l1);
~Line() {};
private:
Point p1, p2;
int len;
};
Line::Line(Point p1, Point p2) :p1(p1), p2(p2)
{
int x = p1.Get_X() - p2.Get_X();
int y = p1.Get_Y() - p2.Get_Y();
len = x * x + y * y;
cout << "Line 的构造函数" << endl;
}
Line::Line(Line& l) : p1(l.p1), p2(l.p2),len(l.len)
{
cout << "Line 的拷贝构造" << endl;
}
int main()
{
Point p1(1, 2), p2(4, 6); //调用 2 P 构造
Line l1(p1, p2); // 形实参结合 4次 拷贝P, 1L 构造
Line l2(l1); //形实参结合 2P拷贝构造, 形实参结合1L拷贝构造
return 0;
}
前向引用声明,自己看看
4.8 UML图形标识
UML :Unified Modeling Language 统一建模语言,是面向对象建模语言,并非编程语言
UML类图:由类和与之相关的各种静态关系共同组成的图形
UML表示关系
1.依赖关系
A----->B 表示类A使用了类B 或者说 类A依赖类B
2.作用关系——关联
重数A 重数B
类A———————————————————————————类B
重数A决定了类B的每个对象与类A的多少个对象发生作用
重数B决定了类A的每个对象与类B的多少个对象发生作用
3.包含关系——聚集和组合
聚集:整体包含部分
组合:整体与部分共存
4.9 结构体 struct
结构体中,未指定访问控制属性的成员,默认为public
struct 结构体名称{
(public:)
公有成员
protected:
保护成员
private:
私有成员
};
//用结构体表示学生的信息 学号 姓名 性别 年龄
#include <iostream>
using namespace std;
struct student {
int id;
string name;
string sex;
int age;
}st[5]; //创建类型为student 名称为st 大小为5 的数组
int main()
{
student stu = { 421000,"小黑","男",21 }; //创建名称为 stu的对象
cout << "INFORMATION:" << endl;
cout << stu.id << " " << stu.name << " " << stu.sex << " " << stu.age << endl;
return 0;
}
#include <iostream>
using namespace std;
struct Student {
int id;
string name;
char sex;
int age;
};
int main()
{
Student s1 = { 1, "LiMing", 'n', 18 };
cout << s1.id << " " << s1.name << " " << s1.sex << " " << s1.age;
return 0;
}
4.10 组合体 union(联合体)
应用场景:一组数据,任何两个数据不会同时有效的情况下
联合体的默认访问控制属性也是public
联合体的全部数据成员共享同一组内存单元
限制:
1.联合体的各个对象成员,不可有自定义的构造函数、自定义的析构函数和重载的赋值运算符
2.联合体不能继承,也不支持多态
(一般只用联合体来存储一些公有的数据)
tip:联合体也可以不声明名称称为无名联合体,可以由成员项的名字直接访问
union{
int i;
float f;
};
//使用方法
i=10;
f=2.2;
4.11 枚举类型 enum
将变量的可取值一一列举出来,便可构成一个枚举类型
类型: 不限定作用域的枚举类型 和 限定作用域的枚举类
4.11.1 不限定作用域的枚举类型
enum (枚举类型名) {变量值列表};
enum weekday {sun,mon,tue,wed,thu,fri,sat};
//枚举类
enum class (枚举类型名) {变量值列表};
//或者
enum struct (枚举类型名) {变量值列表};
enum class weekday {sun,mon,tue,wed,thu,fri,sat};
4.11.2 限定作用域的枚举类
略
4.11.3注意
1.对枚举元素,按常量处理,不能对他们赋值 (指的是枚举元素不可修改,枚举变量的值当然可以修改~)
sun=0 //非法
2.也可以在声明时另行定义枚举元素的值
enum weekday {sun=7,mon=1,tue,wed,thu,fri,sat}; //tue=2 wed=3 ....依次+1
3.枚举值可以进行关系运算
4.整数值不可直接赋给枚举变量,如需赋值,需要进行强制类型转换
5.可以将enum 作为 switch 语句的表达式 ,而将枚举值作为 case 标签
#include <iostream>
using namespace std;
enum gameresult {
win, lose, tie, cancel //注意不加 ;
}; //注意加 ;
int main()
{
gameresult t = cancel;
for (int i = win; i <= cancel; i++) //枚举类型 可 隐式转换为 int 类型
{
int result = (gameresult)i;
if (result == t)
cout << "CANCEL";
else
{
if (result == win)
cout << "WIN" << endl;
if (result == lose)
cout << "LOSE" << endl;
if (result == tie)
cout << "TIE" << endl;
}
}
return 0;
}
共用体 和 枚举类型 混合使用实例
//使用联合体保存成绩信息,并且根据不同数据,进行输出
#include <iostream>
using namespace std;
class Exam {
public:
Exam(string name, char gd) :name(name), mode(GRADE), grade(gd) {}
Exam(string name, bool pa) :name(name), mode(PASS), pass(pa) {}
Exam(string name, int pt) :name(name), mode(PERCENT),percent(pt) {}
void show();
private:
string name;
enum {
GRADE,
PASS,
PERCENT
}mode;
union {
char grade; //等级制
bool pass; //是否通过
int percent; //百分比
};
};
void Exam::show()
{
cout << name << ":";
switch (mode)
{
case GRADE:
cout << grade << endl;
break;
case PASS:
cout << (pass ? "PASS" : "FAIL")<< endl;
break;
case PERCENT:
cout << percent << endl;
break;
}
}
int main()
{
Exam c1("C++", 'A');
Exam c2("高数", 90);
Exam c3("离散", true);
c1.show();
c2.show();
c3.show();
return 0;
}
第五章 数据的共享与保护
5.1 标识符的作用域与可见性
作用域讨论的是标识符的有效范围
可见性是讨论标识符是否可以被引用
5.1.1 作用域
作用域是一个标识符在程序正文中有效的区域
种类:
1.函数原型作用域
2.局部作用域(块作用域)
3.类作用域
4.文件作用域
5.命名空间作用域
6.限定作用域的枚举类
1.函数原型作用域
C++程序中最小的作用域。
函数原型声明时形式参数的作用范围就是函数原型作用域
//如下函数声明 x , y 在其他地方不可用
//并且在函数声明时,真正起作用的只有形参类型,可省去形参名称,为了增加可读性,加上更好
int add(int x,int y);
#include <iostream>
using namespace std;
int add(int m, int n)
{
return m + n;
}
int main()
{
//这两种声明均可 ~
int add(int, int);
int add(int x, int y); //x、y并无实际意义
cout<<add(3, 4);
return 0;
}
2.局部作用域
函数形参列表中形参的作用域,从形参列表中的声明处开始,到整个函数体结束
函数体内声明的变量,作用域从声明处开始,一直到声明所在的块结束的花括号为止(块:用花括号括起来的)
[具有局部作用域的变量也称为局部变量]
3.类作用域
类X的成员m具有类作用域,对m的访问方式有
1.如果在X的成员函数中,未声明同名的局部作用域标识符,那么可在函数内可以直接访问成员m
2.通过表达式x.m或者X::m 这正是程序中访问对象成员的基本方法
3.ptr->m ptr为指向X类的一个对象的指针
4.文件作用域
不在前述各个作用域中出现的声明,就具有文件作用域,这样的标识符作用域开始于声明点,结束于文件尾
具有文件作用域的变量也称为全局变量
5.命名空间作用域
为了使编译器区分出不同库的同名实体
1.声明方式
namespace namespace_name{
//代码声明
}
2.使用方式:
命名空间::实体名称,或者通过using namespace namespace_name
6.限定作用域的enum枚举类
定义限定作用域的枚举类型方式:
enum class 名称 {...};
此时,枚举元素名字遵循常规的作用域规则,即类作用域,枚举类型的作用域外不可访问的
不加限定作用域出现的问题:
不加class就是非限定作用域,不可使用 类名::方式
正确使用方式
作用域实例
5.1.2 可见性
程序运行到某一点,能够引用到的标识符,即为该处可见的标识符
作用域关系:
文件作用域 > 类作用域 >局部作用域
(文件作用域包含类作用域,类作用域包含局部作用域,局部作用域在最里面)
可见性表示:从内层作用域向外层作用域“看到什么”
作用域可见性的一般规则:
1.标识符先声明,再引用
2.同一作用域中,不可声明同名标识符
3.在没有互相包含关系的不同的作用域中声明同名的标识符,并无影响
4.在两个或多个具有包含关系的作用域中,声明了同名标识符,则外层标识符在内层不可见
5.2 对象(包括类的对象 + 简单变量)的生存期
从诞生到结束的这段时间即为生存期
5.2.1 静态生存期
对象的生存期 与 程序的运行期 相同 即具有静态生存期
分为两种:
1.文件作用域 中声明的对象都是具有静态生存期
2.函数内部的 局部作用域 中声明具有静态生存期的对象要用 static
static int a;//定义时未指定初值的基本类型静态生存期变量,会被以0值初始化
static int b=5;
//局部作用域中,静态变量的特点是,并不随着每次函数调用而产生一个副本
//也不会随着函数返回而失效
//也就是说,函数返回后,下一次再调用,则会保持上次返回的值
//变量会在各次调用间共享
5.2.2 动态生存期
除了上述两种静态生存期,其余对象均具有动态生存期。
在局部作用域中声明的具有动态生存期的对象,习惯上也被称为局部生存期
对象
局部生存期对象诞生于声明点,结束于声明所在的块执行完毕时
简单变量的生存期与可见性实例
#include <iostream>
using namespace std;
int i = 1; //全局变量 静态生存期 文件作用域
void other()
{
//a b 为静态局部变量, 静态生存期 局部作用域(局部可见) 只第一次进入函数时被初始化
static int a = 2;
static int b;
//c为局部变量, 动态生存期 局部作用域
int c = 10;
a += 2;
i += 32;
c += 5;
cout << "------other-----" << endl;
cout << "i= " << i << " a=" << a << " b=" << b << " c=" << c << endl;
b = a;
}
int main()
{
//此处a为 静态局部变量 ,静态生存期 局部作用域(局部可见)
static int a;
//b,c为局部变量 ,具有动态生存期 局部作用域
int b = -10;
int c = 0;
cout << "-----main-----" << endl;
cout << "i= " << i << " a=" << a << " b=" << b << " c=" << c << endl;
c += 8;
other();
cout << "-----main-----" << endl;
cout << "i= " << i << " a=" << a << " b=" << b << " c=" << c << endl;
i += 10;
other();
return 0;
}
对象的生存期实例
#include <iostream>
using namespace std;
//Clock 类
class Clock {
public:
Clock(int hour=0,int minute=0,int second=0);
Clock(Clock& c1);
void set_time_to(int h, int m, int s);
void showtime();
private:
int h, m, s; //时 分 秒
};
Clock::Clock(int hour, int minute, int second):h(hour),m(minute),s(second)
{}
Clock::Clock(Clock& c1)
{
h = c1.h, m = c1.m, s = c1.s;
}
void Clock::set_time_to(int h, int m, int s)
{
this->h = h, this->m = m, this->s = s;
}
Clock my_clock; //声明全局变量 具有静态生存期 文件作用域
void Clock::showtime()
{
cout << h << ":" << m << ":" << s << endl;
}
int main()
{
//对象成员函数具有类作用域
my_clock.showtime();
my_clock.set_time_to(3, 4, 5);
//your_clock 为具有 块作用域(局部作用域)、动态生存期的 局部变量
Clock your_clock(my_clock);
your_clock.showtime();
return 0;
}
5.3 类的静态成员
5.3.1 类的静态数据成员
1.实例属性 :一个类的所有对象,具有相同的属性 是指 属性的个数、名称、数据类型相同,但各个对象的属性值则可以不同,这样的属性 叫做 “实例属性”
2.类属性:某个属性为整个类所共有,不属于任何一个具体对象,则采用
static关键字来声明为静态成员
(静态成员在每个类中只有一份,由该类的所有对象共同维护使用)
类属性是描述所有对象共同特征的一个数据项,对于任何对象实例,它的属性值是相同的。
3.静态数据成员具有静态生存期
4.在类中可对静态数据成员进行引用性声明,必须在文件作用域中的某个地方,使用类名限定进行定义性声明,同时进行初始化
//对Point的数量进行计数
#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0) :x(x), y(y) { ++cnt; }
Point(Point& p);
~Point() { --cnt; }
static void show_cnt() { cout << cnt << endl; }
private:
int x, y;
static int cnt; //并不分配空间
};
int Point::cnt = 0; //此处定义,是为了分配空间
Point::Point(Point& p)
{
x = p.x;
y = p.y;
++cnt;
}
int main()
{
Point p1;
Point::show_cnt();
Point p2(p1);
Point::show_cnt();
return 0;
}
5.3.2 静态函数成员
静态成员函数可以直接访问该类的静态数据成员和函数成员,而访问非静态成员,必须通过对象名
示例一:见 5.3.1
示例二:
由于静态函数成员也是所有对象共同拥有一份,所以想具体查看某一个对象的具体数据成员时,要将某个对象传入查看
class A{
public:
static void f(A a);
private:
int x;
}
void A::f(A a)
{
cout<<x; //错误,必须指定对象
cout<<a.x; //正确
}
深入理解静态数据成员、静态函数成员
静态函数成员不可访问非静态数据成员,指的是,当未传入对象时,它不知道到底该访问哪个对象中的非静态数据成员~
#include <iostream>
using namespace std;
class Point {
public:
Point(int xx = 0, int yy = 0);
Point(Point& p);
~Point() { --cnt; }
int Get_X() { return x; }
int Get_Y() { return y; }
static void ShowCount();
private:
int x, y;
static int cnt; //类属性 点的数量
};
int Point::cnt; //类外的定义非常重要,因为这是为了给cnt分配空间[同时,static类型数据默认值为0]
Point::Point(int xx, int yy) :x(xx), y(yy)
{
++cnt;
}
Point::Point(Point& p)
{
x = p.x, y = p.y;
++cnt; //此处需要++cnt,因为通过拷贝构造函数创建对象后不会再调用有参构造函数
}
void Point::ShowCount()
{
//cout << x << " " << y; //静态成员函数不可引用 非静态数据成员
//思考:为什么?
//因为静态成员函数 是 属于类的,而非某个对象所有,调用静态成员函数时,不知道所调用的类的对象是哪个,则无从谈起访问其非静态数据成员
cout << "点的数量为:" << cnt << endl;
}
int main()
{
Point::ShowCount();
Point p1(1, 2);
cout << p1.Get_X() << " " << p1.Get_Y() << endl;
Point::ShowCount();
Point p2(p1);
cout << p2.Get_X() << " " << p2.Get_Y() << endl;
Point::ShowCount();
return 0;
}
当传入 对象 为参数时,静态函数成员便可以访问该对象的非静态数据成员
//如何通过静态成员函数调用非静态数据成员?
//给静态成员函数传入对象
#include <iostream>
using namespace std;
class T {
public:
//T(int A) :A(A) {}
//T(T& t) :A(t.A) {}
T(int A);
T(T& t);
static void func(T t);
private:
int A; //A 为 非静态数据成员 属于对象
static int B; // B 为 静态数据成员 属于类
};
int T::B; //初始化静态数据成员B
T::T(int A)
{
this->A = A;
B += A; // 非静态成员函数可访问静态成员
}
T::T(T& t)
{
this->A = t.A;
B += t.A;
}
void T::func(T t)
{
cout << t.A << endl; // 由于传入对象,则静态函数成员明确了要访问的对象,则可正确访问非静态数据成员
cout << B << endl;
}
int main()
{
T t1(1); //调用构造函数 B += A
T::func(t1);
//为什么结果是 2 ?
//因为 调用 func函数时,形实参结合,调用拷贝构造函数使得B += A
return 0;
}
5.4 类的友元
为什么需要友元的存在?
为了进行数据共享,对数据的封装进行一些微小的破坏[eg:计算两点的距离,需要访问私有数据成员]
友元关系提供了不同类或对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制
5.4.1 友元函数
友元函数是在类中用关键字friend修饰的非成员函数
(在它的函数体中可以通过对象名访问类的私有和保护成员)
形式:
1.普通的函数
2.其他类的成员函数
//计算两点间距离
#include <iostream>
using namespace std;
class Point {
public:
Point(double x=0,double y=0):x(x),y(y){}
double get_x() { return x; }
double get_y() { return y; }
friend double dist(const Point& p1, const Point& p2);
private:
double x, y;
};
double dist(const Point& p1, const Point& p2) //为了防止对传入的对象误操作,可用const修饰为常对象
{
double tx = p1.x - p2.x;
double ty = p1.y - p2.y;
return sqrt(tx * tx + ty * ty);
}
int main()
{
Point p1(1, 4), p2(4, 8);
cout << dist(p1, p2);
return 0;
}
5.4.2 友元类
若A类为B类的友元类,则A类的所有成员函数都是B类的友元函数,都可以访问B类的私有和保护成员
class B{
...
friend class A; //声明A类为B类的友元类
...
};
栗子:B类为A类的友元类,则在B中可以直接访问A类对象的私有成员
class A{
public:
void display() {cout<<x<<endl;}
int getx() {return x;}
friend class B; //B类为A类的友元类
private:
int x;
class B{
public:
void set(int i);
void display();
private:
A a;
};
void B::set(int i)
{
a.x=i; //由于B为A的友元类,则可直接使用a.x访问A中的私有数据成员x
}
5.4.2 重点 友元类的注意事项
1.友元关系不可传递
2.友元关系是单向的
3.友元关系不可被继承
5.5 共享数据的保护
对于既需要共享、又需要防止改变的数据 应该声明为 常量
5.5.1 常对象
数据成员值在对象整个生存期内不能被改变。即 常对象必须进行初始化,并且不能更新
[常量其实也是一种特殊的常对象]
const 类型说明符 对象名;
(const 和 类型名调换顺序也可,不过习惯const在前)
class A{
public:
A(int i,int j):x(i),y(j) {}
...
private:
int x,y;
};
const A a(3,4); //a为常对象,不可被更新
初始化 与 赋值
初始化: 定义一个变量或常量时,为它指定初值,叫做初始化
赋值:在定义一个变量 或 常量之后,使用赋值运算符修改其值,叫做赋值
5.5.2 用const修饰的类成员
1.常成员函数
2.常数据成员
1.常成员函数
#include <iostream>
using namespace std;
class Bk {
public:
Bk(int x){ this->x = x; }
void Print() { cout << "调用非const " << x << endl; }
void Print()const { cout << "调用const " << x << endl; } //常成员函数
private:
int x;
};
int main()
{
Bk b1(2);
b1.Print();
const Bk b2(4); // b2 为常对象
b2.Print();
return 0;
}
//Print()成员函数,仅仅通过const进行重载,非const对象b1调用时,两个都可以匹配,编译器选择最近的函数---非const的函数,也就是非 常成员函数
2.常数据成员
#include <iostream>
using namespace std;
class T {
public:
T(int x);
void print();
private:
const int A; //A 为 常数据成员
static const int B; // B 为 静态常数据成员
};
const int T::B = 10; //静态常数据成员,只可在类外初始化时给定值,并且不可更改 注意:const int都要写上
T::T(int x):A(x) //不在这里A(x),编译器会提示: 未提供初始值设定项
{
//A = x; //不可写在里面
}
void T::print()
{
cout << A << " " << B << endl;
}
int main()
{
T t(1);
t.print();
return 0;
}
5.5.3 常引用
声明引用时,用const修饰,则被声明的引用即为常引用
常引用的对象不可被更新
const 类型 & 引用名
#include <iostream>
using namespace std;
class Point {
public:
Point(double xx = 0, double yy = 0);
friend double dist(const Point& p1, const Point& p2);
void print() { cout << x << " " << y << endl; }
private:
double x, y;
};
Point::Point(double xx, double yy)
{
this->x = xx, this->y = yy;
}
double dist(const Point& p1, const Point& p2)
{
double x = p1.x - p2.x;
double y = p1.y - p2.y;
return sqrt(x * x + y * y);
}
int main()
{
Point p1(2, 3), p2(5, 7);
p1.print(), p2.print();
cout << dist(p1, p2);
return 0;
}
5.6 多文件结构和编译预处理命令
5.6.1 C++程序的一般组织结构
xxxxx.h 类定义
xxxxx.cpp 类实现
yyy.cpp 主函数文件
5.6.2 外部变量与外部函数
5.6.3 标准C++库
5.6.4 编译预处理
1. #include
2. #define #undef
3. #if #endif
4. defined
第六章 数组、指针与字符串
6.1 数组
为什么需要数组?
数组是用于存储和处理大量同类型数据的数据结构
数组:具有一定顺序关系的若干对象的集合体
数组的元素:组成数组的对象
每个元素有n个下标的数组称为n维数组
6.1.2数组的注意点
由于之前C语言已经讲述,此处不再赘述基本知识,只列举一些注意点
1.数组初始化时,元素的值?
#include <iostream>
using namespace std;
int a[5];
int main()
{
//1. 静态生存期的数组,初值为0
for (int i = 0; i < 5; ++i)
cout << a[i] << " ";
puts("");
//2.指定的初值个数小于数组大小,剩下的为0
int a1[5] = { 1, 2 };
for (int i = 0; i < 5; ++i)
cout << a1[i] << " ";
puts("");
//3. 对于动态生存期的数组,其初值不确定
int a2[5];
for (int i = 0; i < 5; ++i)
cout << a2[i] << " ";
puts("");
return 0;
}
2.范围for语句
C++ 11标准支持的一种简单遍历序列/容器的方法
#include <iostream>
using namespace std;
int main()
{
int a[10] = { 1, 2, 3, 4 };
for (auto x : a) // auto即 编译器自行推断元素类型 x用来指代元素
cout << x << " ";
return 0;
}
3.声明为常量的数组,必须给定初值
#include <iostream>
using namespace std;
int main()
{
const int a[5] = { 1, 2 };
for (auto x : a)
cout << x << " ";
puts("");
return 0;
}
4.数组作为参数时,一般不指定数组第一维大小,即使指定也会被忽略
5.对象数组
数组的元素可以为自定义类型
#include <iostream>
using namespace std;
class Point {
public:
Point(int xx = 0, int yy = 0);
Point(const Point& p);
int Get_X()const { return x; }
int Get_Y() const { return y; }
void move_to(int xx, int yy);
static void showCount();
private:
int x, y;
static int cnt;
};
int Point::cnt; //类外初始化
Point::Point(int xx, int yy)
{
++cnt;
this->x = xx, this->y = yy;
}
Point::Point(const Point& p)
{
++cnt;
this->x = p.x, this->y = p.y;
}
void Point::showCount()
{
cout << "点的个数为 :" << cnt << endl;
}
void Point::move_to(int xx, int yy)
{
this->x = xx, this->y = yy;
}
int main()
{
Point p1(1, 2);
Point::showCount();
Point p2(3, 4);
Point p3(p2);
Point::showCount();
Point arr_p[3] = { p1, p2, p3 };
for (const auto & x : arr_p) // 此处的 x 为 const对象,如果调用函数,会将this指针转换为const this指针,从而调用const成员函数,所以上述的 Get_X(), Get_Y() 函数要为常成员函数
cout << x.Get_X() << " "<< x.Get_Y() << endl;
return 0;
}
6.2 指针
指针类型:专门用来存放内存单元地址的变量类型
指针变量:具有指针类型的变量称为指针变量
指针变量是用来存放内存单元地址的
指针重点识记!!
指针变量前面的类型名是用来规定 它所指向变量的类型,而非它自身的类型
任何一个指针自身类型均为 unsigned long int;
也就是说,所有指针变量占用的内存单元数量是相同的;
#include <iostream>
using namespace std;
class Point {
public:
Point(int xx = 0, int yy = 0);
private:
int x, y;
};
Point::Point(int xx, int yy):x(xx),y(yy){}
int main()
{
char c = 'c';
char* pc = &c;
long long i = 10;
long long* pi = &i;
Point p;
Point* pp = &p;
cout << sizeof(pc) << endl << sizeof(pi) << endl << sizeof(pp) << endl;
return 0;
}
1. * 与 &
* 与 & 在 声明 和 执行语句中的含义是不同的
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a; //1.此为声明,b为a的引用,即a,b占用同一片内存空间
int arr[10] = { 9, 4, 3 };
int* pa = arr; //2.此为声明,pa为指针,指向arr(的首元素)
cout << *pa << " " << arr[0] << endl; //3. 此为 执行语句, 输出pa所指向的内容
pa = &a; //4. 此为执行语句,pa指向a
cout << *pa << " " << a << endl;
return 0;
}
指向常量的指针 、 指针常量
1. 指向常量的指针
#include <iostream>
using namespace std;
int main()
{
int a = 10;
const int* pa = &a; //pa 为 指向常量的指针,不能通过指针来修改所指对象的值
cout << *pa << " " << a << endl;
//*pa = 10; //错误
int b = 15;
pa = &b; //正确, 指向常量的指针,可以修改其指向
cout << *pa << " " << b << endl;
return 0;
}
2. 指针类型的常量
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int* const pa = &a; // pa 为指针常量 即 不能修改其指向
cout << *pa << " " << a << endl;
//int b = 30;
//pa = &b; //不可 ,因为pa为常量
*pa = 110;
cout << *pa << " " << a << endl;
return 0;
}
注意根据const所处的位置判断谁可被修改,谁不可被修改
const int* pa;
const 在 *pa 前, 则说明 不可 执行*pa,即说明pa所指向的是常量,不可被修改
则pa为指针常量
也可
从内层向外看, *pa说明pa为指针,前面加了const 说明pa为指向常量的指针,则pa所指的内容不可被修改
2.void指针
#include <iostream>
using namespace std;
int main()
{
void* pv;
int i = 10;
pv = &i;
cout << *(int*)pv; //但在输出时,首先要将pv进行显示转换,而后才可使用
return 0;
}
3.空指针
0 、 NULL 均可表示空指针
空指针的存在,是为了防止用不确定的值作为地址去访问内存单元,否则将会造成不可预知的错误
4.用指针处理数组元素
#include <iostream>
using namespace std;
#include <iterator>
//数组作为函数的形参 等价于 把指向数组元素类型的指针作为形参
//void f(int p[]); //以下三种写法是等价的
//void f(int p[3]);
void f(int* p)
{
}
int main()
{
int a[10] = { 1, 2, 3, 4, 6 };
for (int* p = a; p < (a + 10); ++p)
cout << *p << " ";
puts("");
//更安全的写法
int* st = begin(a);
int* ed = end(a);
for (int* p = st; p < ed; ++p)
cout << *p << " ";
puts("");
return 0;
}
5.指针数组、数组指针
1.指针数组
#include <iostream>
using namespace std;
int main()
{
//指针数组的含义是,它是数组,里面存放的每个元素都是指针
int a = 1, b = 2, c = 3;
int* pt[] = { &a, &b, &c };
for (const auto& x : pt)
cout << *x << " ";
return 0;
}
2.数组指针 即 指向数组的指针
#include <iostream>
using namespace std;
int main()
{
int a[2][3] = { 1, 2, 5, 7}; // a数组每行3个元素
int(*pa)[3] = a; //pa是指针,它指向 含有三个int类型的数组
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 3; ++j)
cout << pa[i][j] << " ";
puts("");
}
return 0;
}
6.指针型函数、 指向函数的指针
1. 函数的返回值为指针类型,则该函数为指针型函数
2. 指向函数的指针是指,它是指针,指向一个函数
#include <iostream>
using namespace std;
//pfunc 为 指向 有一个类型为double的形参,返回类型为int的函数 的 指针 的别名
//这样可以方便定义使用
typedef int (*Double_Int_Ptr) (double);
int add(double a)
{
return a += 5;
}
int mul(double a)
{
return a *= 2;
}
int main()
{
Double_Int_Ptr p; //定义一个指针
p = add; // p 指向 add函数
cout << p(11) << endl;
p = mul; // p 指向 mul函数
cout << p(11) << endl;
return 0;
}
7.对象指针
#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0);
int Get_X() { return x; }
int Get_Y() { return y; }
private:
int x, y;
};
Point::Point(int x, int y)
{
this->x = x, this->y = y;
}
int main()
{
Point p(3, 4);
Point* pa = &p;
cout << pa->Get_X() << " " << (*pa).Get_X() << " " << p.Get_X();
return 0;
}
8.this指针
隐含于每一个类的非静态成员函数中的特殊指针(包括构造函数、析构函数),它用于指向正在被成员函数操作的对象
Point类...
...
Get_X() {return x;} // 其实系统相当于执行 return this->x;
...
9.指向类的非静态成员的指针 、 指向类的静态成员的指针
1.指向类的非静态成员
#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0);
int Get_X()const { return x; }
int Get_Y()const { return y; }
private:
int x, y;
};
Point::Point(int x, int y)
{
this->x = x, this->y = y;
}
int main()
{
//以 指向类的非静态函数成员 为例
int (Point::*pfunc)()const = &Point::Get_X; // int(Point::*)()const 为完整类型名
Point a(2, 5);
cout << (a.*pfunc)() << " " << a.Get_X();
return 0;
}
2. 指向类的静态成员
#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0);
Point(const Point& p);
static void show_Count() { cout << count << endl; }
private:
int x, y;
static int count;
};
int Point::count; //类外声明并默认初始化为0
Point::Point(int x, int y)
{
++count;
this->x = x, this->y = y;
}
Point::Point(const Point& p)
{
++count;
x = p.x, y = p.y;
}
int main()
{
//为什么此时Ptr_show_Count不需要加Point:: ?
//因为,访问类的静态成员是不依赖于对象的,即 count、show_Count 为类属性
void (* Ptr_show_Count)() = &Point::show_Count;
Point a(3, 4);
Point b(a);
Point c(b);
//这两种是等价的写法
Ptr_show_Count();
Point::show_Count();
return 0;
}
小补充:
加不加&均可
void (* Ptr_show_Count)() = &Point::show_Count;
void (* Ptr_show_Count)() = Point::show_Count;
*因为 函数名 就是这个函数的入口地址*
6.3 动态内存分配
1.new创建
程序运行过程中,申清和释放的存储单元也称为堆对象
#include <iostream>
using namespace std;
int main()
{
//new 的功能是 动态分配内存,或者称动态创建堆对象
int* ptr1 = new int; //创建堆对象,并不分配初值
int* ptr2 = new int(); //创建堆对象,并用0初始化
int* ptr3 = new int(2); //创建堆对象,并用2初始化
cout << *ptr1 << " " << *ptr2 << " " << *ptr3;
return 0;
}
#include <iostream>
using namespace std;
class Point {
public:
Point():x(0),y(0){}
Point(int x, int y) { this->x = x, this->y = y; }
int Get_X()const { return this->x; }
int Get_Y()const { return this->y; }
private:
int x, y;
};
int main()
{
Point* ptr1 = new Point; //调用默认构造函数
Point* ptr2 = new Point(3, 4);
int(Point:: * ptr_Get_X)()const = &Point::Get_X; //函数指针 [此时,需要加上&,语法规定]
cout << (ptr1->*ptr_Get_X)() << " " << ptr1->Get_X() << endl;
cout << (ptr2->*ptr_Get_X)() << " " << ptr2->Get_X() << endl;
return 0;
}
2.delete删除
用new分配的内存,必须使用delete加以释放,否则会导致动态分配的内存无法回收,使得程序占据的内存越来越大,从而造成 “ 内存泄漏 ”。
#include <iostream>
using namespace std;
class Point {
public:
Point():x(0),y(0){}
Point(int x, int y) { this->x = x, this->y = y; }
int Get_X()const { return this->x; }
int Get_Y()const { return this->y; }
private:
int x, y;
};
int main()
{
Point* ptr1 = new Point; //调用默认构造函数
Point* ptr2 = new Point(3, 4);
int(Point:: * ptr_Get_X)()const = &Point::Get_X; //函数指针 [此时,需要加上&,语法规定]
cout << (ptr1->*ptr_Get_X)() << " " << ptr1->Get_X() << endl;
cout << (ptr2->*ptr_Get_X)() << " " << ptr2->Get_X() << endl;
delete ptr1;
delete ptr2;
cout << (ptr1->*ptr_Get_X)() << " " << ptr1->Get_X() << endl;
cout << (ptr2->*ptr_Get_X)() << " " << ptr2->Get_X() << endl;
return 0;
}
在delete释放空间之后,指针的指向则不确定,故程序无法确定访问的内容
3.new数组
#include <iostream>
using namespace std;
int main()
{
int* ptr1= new int[7]; //创建,并不赋初值
int* ptr2 = new int[7](); //创建,并以0初始化
for (int i = 0; i < 7; ++i)
cout << ptr1[i] << " ";
puts("");
for (int i = 0; i < 7; ++i)
cout << ptr2[i] << " ";
puts("");
return 0;
}
6.4用vector创建数组对象
#include <iostream>
using namespace std;
#include <vector> //头文件
#include <iterator>
int main()
{
//vector 并非一个类,而是一个类模板
vector<int> VI1; //空vector对象
vector<int> VI2(100); //大小为100的vector对象
vector<int> VI3(10, 5); //大小10, 值均为5
//范围for语句遍历
for (const auto& x : VI3)
cout << x << " ";
puts("");
//正向迭代器
for (vector<int>::iterator i = VI3.begin(); i < VI3.end(); ++i)
cout << *i << " ";
puts("");
//常量正向迭代器
for (vector<int>::const_iterator i = VI3.begin(); i < VI3.end(); ++i)
cout << *i << " ";
puts("");
return 0;
}
更深入的可以自行搜索 vector / STL相关内容
6.5 深拷贝 、浅拷贝
#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0);
Point(const Point& p);
int get_x()const { return this->x; }
int get_y()const { return this->y; }
void move_to(int x, int y); //移动 点
private:
int x, y;
};
Point::Point(int x, int y)
{
this->x = x, this->y = y;
}
Point::Point(const Point& p)
{
this->x = p.x, this->y = p.y;
}
void Point::move_to(int x, int y)
{
this->x = x, this->y = y;
}
class ArrayOfPoints {
public:
ArrayOfPoints(int size) :size(size) {
points = new Point[size];
}
//未定义拷贝构造函数,则为浅拷贝,会造成问题
~ArrayOfPoints() {
delete[] points; //销毁空间
}
Point& elem(int idx) { // 获取下标为idx的元素
return points[idx];
}
private:
Point* points;
int size;
};
int main()
{
ArrayOfPoints arr1(5);
ArrayOfPoints arr2(arr1);
arr1.elem(0).move_to(5, 10);
cout << arr1.elem(0).get_x() << " " << arr1.elem(0).get_y() << endl;
cout << arr2.elem(0).get_x() << " " << arr2.elem(0).get_y() << endl;
return 0;
}
错误原因:两个对象指向同一片空间,在结束时,析构函数会析构同一片空间两次
深拷贝:即需要完善ArryOfPoints的拷贝构造函数,使得通过调用拷贝构造函数创建的对象可以指向一片新空间,使得两个对象互不影响
#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0);
Point(const Point& p);
int get_x()const { return this->x; }
int get_y()const { return this->y; }
void move_to(int x, int y); //移动 点
private:
int x, y;
};
Point::Point(int x, int y)
{
this->x = x, this->y = y;
}
Point::Point(const Point& p)
{
this->x = p.x, this->y = p.y;
}
void Point::move_to(int x, int y)
{
this->x = x, this->y = y;
}
class ArrayOfPoints {
public:
ArrayOfPoints(int size) :size(size) {
points = new Point[size];
}
//未定义拷贝构造函数,则为浅拷贝,会造成问题
//下面定义拷贝构造函数
ArrayOfPoints(const ArrayOfPoints& a) {
this->size = a.size;
//新创建空间 并 赋值
this->points = new Point[this->size];
for (int i = 0; i < size; ++i)
this->points[i] = a.points[i];
}
~ArrayOfPoints() {
delete[] points; //销毁空间
}
Point& elem(int idx) { // 获取下标为idx的元素
return points[idx];
}
private:
Point* points;
int size;
};
int main()
{
ArrayOfPoints arr1(5);
ArrayOfPoints arr2(arr1);
arr1.elem(0).move_to(5, 10);
cout << arr1.elem(0).get_x() << " " << arr1.elem(0).get_y() << endl;
cout << arr2.elem(0).get_x() << " " << arr2.elem(0).get_y() << endl;
return 0;
}
当被复制的对象数据成员为指针类型,并非复制该指针成员本身,而是将指针所指对象进行复制
6.6 字符串
string 类
append()
assign()
compare()
insert()
substr()
find()
length()
swap()
#include <iostream>
using namespace std;
#include <string>
int main()
{
string s1;
cin >> s1;
cout << s1 << endl;
getchar(); //为了去除缓冲区的\n,防止getline遇到该\n直接结束
string s2;
getline(cin, s2);
cout << s2;
return 0;
}
第七章 类的继承
以原有的类为基础产生新的类,即 新类继承了原有类的特征,也可以说是从 原有类派生出新类
其好处?
代码的重用性 和 可扩充性
派生新类的过程:
- 吸收已有类的成员
- 调整已有类成员
- 添加新成员
7.1 基类与派生类
类的继承,是新的类从已有类那里得到已有的特性
(从已有类产生新类的过程即为类的派生)
原有的类称为基类或父类,产生的新类称为派生类或子类
1.派生类定义
class 派生类名: 继承方式1 基类名1,继承方式2 基类名2,..继承方式n 基类名n
{
派生类成员声明;
};
1.基类名是已有类的类名称,派生类名是继承原有类的特性产生的新类
2.一个派生类,可以同时有多个基类,这种情况被称为多继承
一个派生类,只有一个直接基类,称为单继承
3.直接参与派生出某类的基类 称为 直接基类
4.基类的基类甚至更高层的基类 称为 间接基类
5.继承方式规定了如何访问从基类继承的成员
public、private 、protected
默认为 private
6.派生类成员 是指 除了从基类继承来的所有成员外,新增加的数据和函数成员
2.类派生的过程
1.吸收基类成员
2.改造基类成员
3.添加新的成员
7.2 访问控制
派生类继承了基类的 全部数据成员 和 除了构造、析构函数之外的全部函数成员
1.公有继承 public
类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可直接访问
无论是派生类的成员还是派生类的对象都无法直接访问基类的私有成员
#include <iostream>
using namespace std;
class Point {
public:
//Point(int x = 0, int y = 0); //为什么不写成构造函数,而写为initPoint ? 因为子类不会继承基类的构造函数和析构函数!
void initPoint(int x = 0, int y = 0) {
this->x = x, this->y = y;
}
void move_to(int x, int y) {
this->x = x, this->y = y;
}
int Get_x() const{ return this->x; }
int Get_y() const{ return this->y; }
private:
int x, y;
};
class Rectangle :public Point { //公有继承Point
public:
void initRectangle(int x, int y, int len, int wid) {
initPoint(x, y);
this->length = len, this->width = wid;
}
int Get_length() { return this->length; }
int Get_width() { return this->width; }
private:
int length, width;
};
int main()
{
Rectangle r;
r.initRectangle(2, 3, 20, 10);
r.move_to(13, 14);
cout << r.Get_x() << " " << r.Get_y() << " " << r.Get_length() << " " << r.Get_width() << endl;
return 0;
}
2.私有继承 private
当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问
经过私有继承之后,所有基类成员都成为派生类的私有成员或不可直接访问成员 即 私有继承之后,基类的成员再也无法在以后的派生类中直接发挥作用,相当于是终止了基类功能的继续派生,鉴于此,私有继承使用较少
#include <iostream>
using namespace std;
class Point {
public:
void initPoint(int x = 0, int y = 0) {
this->x = x, this->y = y;
}
int Get_x() { return this->x; }
int Get_y() { return this->y; }
void move_to(int x, int y) {
this->x = x, this->y = y;
}
private:
int x, y;
};
class Rectangle :private Point{ //私有继承 pc、pd -> pt pt -> 不可直接访问
public:
void initRectangle(int x, int y, int len, int wid) {
initPoint(x, y);
this->length = len, this->width = wid;
}
//由于继承来的已变成私有,类外不可直接访问,所以需要重写同名函数
int Get_x() { return Point::Get_x(); }
int Get_y() { return Point::Get_y(); }
void move_to(int xx, int yy) {
Point::move_to(xx, yy);
}
int Get_length() { return length; }
int Get_width() { return width; }
private:
int length, width;
};
int main()
{
Rectangle r;
r.initRectangle(1, 3, 10, 5); //左上角坐标为(1, 3) 长 10 宽 5
cout << r.Get_x() << " " << r.Get_y() << " " << r.Get_length() << " " << r.Get_width() << endl;
return 0;
}
3.保护继承
保护继承中,基类的公有和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可直接访问
即派生类的其它成员可以直接访问从基类继承来的公有和保护成员,但在类外部通过派生类对象无法直接访问他们
而无论是派生类成员或是派生类的对象都无法直接访问基类的私有成员
#include <iostream>
using namespace std;
class Test {
public:
void initTest(int xx = 0) {
this->x = xx;
}
int Get_x() { return this->x; }
protected:
int x;
};
class Derived :public Test {
public:
void initDerived(int x) {
initTest(x);
}
int Get_x() {
return x; //子类可直接访问父类的protected
Test t;
t.initTest(1000);
//return t.x; //受保护的成员 不可Test类型的指针或对象访问 即 类外不可直接访问类内protected成员
return t.Get_x(); // 而是应该通过 public类型 的函数接口进行
}
private:
};
int main()
{
Test t;
t.initTest(15);
cout << t.Get_x() << endl;
Derived d;
d.initDerived(22);
cout << d.Get_x() << endl;
return 0;
}
7.3类型兼容规则
类型兼容规则:在需要基类的对象的任何地方,都可以使用公有派生类的对象来代替
通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员
即 公有派生实际具备了基类的所有功能
Conditions:
class B{...}
class D:public B {...}
B b;
B* pb;
D d;
D* pd;
①派生类对象可以隐含转换为基类对象
b = d;
②派生类对象可以初始化基类的引用
B& rb = d;
③派生类的指针可以隐含转换为基类的指针
pb = &d
补充:书上只说了三种,但后续其实还用到了以下两种
④派生类指针可以指向基类对象
pd = &b;
⑤派生类引用可以引用基类对象
D& rd = b;
#include <iostream>
using namespace std;
class Base1 {
public:
void display() const{ cout << " Base1::display" << endl; }
};
class Base2 :public Base1 {
void display()const { cout << " Base2::display" << endl; }
};
class Derived :public Base2 {
void display()const { cout << " Derived::display" << endl; }
};
void func(Base1* b)
{
b->display();
}
int main()
{
Base1 b1;
Base2 b2;
Derived d;
func(&b1); // 基类Base1对象的指针 调用函数
func(&b2); //派生类Base2对象的指针 调用函数
func(&d); //派生类Derived对象的指针 调用函数
//但运行时,通过指针只能使用继承下来的基类成员
return 0;
}
根据类型兼容规则可以在基类对象出现的场合使用派生类对象进行替代,但是替代之后派生类仅仅发挥出基类的作用。
而之后的第八章将学习另一个重要特征—多态性,此时可以保证在类型兼容的前提下,基类、派生类分别以不同方式响应相同的消息
7.4 派生类的构造和析构函数
派生类的构造函数只负责对派生类新增的成员进行初始化,对所有继承下来的成员,其初始化,还是由 基类的构造函数 完成
1.构造函数
构造派生类的对象时,就要对其所有的成员对象 即 基类的成员对象 + 新增成员对象 初始化
①首先调用基类的构造函数
②然后按照构造函数初始化列表中指定方式初始化派生类新增的成员对象
③最后执行派生类构造函数的函数体
对基类初始化时,需要调用基类的带有形参表的构造函数时,派生类就必须声明构造函数
重点:派生类构造函数执行的一般顺序
①调用基类构造函数, 调用顺序按照被继承时声明的顺序(从左向右)
②对派生类新增的成员(内嵌对象)初始化,初始化顺序按照它们在类中声明的顺序
③执行派生类的构造函数体中的内容
三个基类Base1、Base2、Base3,其中Base3只有一个默认的构造函数,其余两个基类是带有含一个参数的构造函数。类Derived经过此三个基类经过公有派生而来。且派生类Derived新增了三个私有对象成员b1, b2, b3,分别是Base1、Base2、Base3类的对象
#include <iostream>
using namespace std;
//记得声明为 public, 否则将默认为 private
class Base1 {
public:
Base1(int i) { cout << "Constructing Base1 " << i << endl; }
};
class Base2 {
public:
Base2(int j) { cout << "Constructing Base2 " << j << endl; }
};
class Base3 {
public:
Base3() { cout << "Constructing Base3 " << "*" << endl; }
};
class Derived :public Base2, public Base1, public Base3 { //公有继承声明 顺序为: Base2 、 Base1 、 Base3,则调用基类构造函数顺序与此相同
public:
Derived(int a, int b, int c, int d) :member1(a), member2(b), Base1(c), Base2(d) {} // 这里的顺序可任意
private:
Base3 member3; //内嵌对象声明 顺序为: Base3 、Base2 、 Base1, 则初始化基类对象顺序与此相同
Base2 member2;
Base1 member1;
};
int main()
{
Derived(5, 6, 7, 8);
return 0;
}
C++11标准中,派生类可以重用其 直接基类定义的构造函数
class Derived:public Base{
public:
using Base::Base // 继承Base的构造函数
}
多数情况下,基类含有几个构造函数,派生类将会继承所有这些构造函数
但有两个例外
第一个例外:派生类可以继承一部分构造函数,为其它构造函数重新定义自己的版本。此时,如果派生类定义的该构造函数,和基类的构造函数具有相同的参数列表,那么该构造函数将不会被继承,重新定义在派生类中的构造函数将替换继承而来的构造函数
第二个例外:默认、拷贝(复制)、移动构造函数不会被继承
2.拷贝构造函数
假设Derived类是Base类的派生类,那么Derived类的拷贝构造函数如下:
Derived::Derived(const Derived& v):Base(v) {}
Base(v) 的用法 符合类型兼容规则,即 可用 派生类的对象,去初始化基类的引用
3.析构函数 + 构造函数 两者的调用顺序
#include <iostream>
using namespace std;
class Base1 {
public:
Base1(int i) { cout << "Constructing Base1 " << i << endl; }
~Base1() { cout << "Destructing Base1" << endl; }
};
class Base2 {
public:
Base2(int j) { cout << "Constructing Base2 " << j << endl; }
~Base2() { cout << "Destructing Base2" << endl; }
};
class Base3 {
public:
Base3() { cout << "Constructing Base3 " << "*" << endl; }
~Base3() { cout << "Destructing Base3" << endl; }
};
class Derived :public Base2, public Base1, public Base3 { //公有继承 2 1 3
public:
//初始化列表顺序只要保证参数和调用的对象或函数匹配即可 , 此顺序无关紧要
Derived(int a, int b, int c, int d, int e) :Base1(a), Base2(b), member0(c), member1(d), member2(e) { cout << "Constructing Derived" << endl; }
//此析构函数只是为了 清理 新增的且非对象成员 :member0 的工作
~Derived() { cout << "Destructing Derived" << endl; }
private:
Base3 member3; //内嵌对象调用顺序 3 2 1
Base2 member2;
Base1 member1;
int member0;
};
int main()
{
Derived d(3, 4, 5, 6, 7);
return 0;
}
可以发现,构造函数、析构函数的调用顺序完全相反,对称记忆即可
7.5 派生类成员的标识与访问
派生类中,成员按访问属性划分为4种:
不可访问成员、私有成员、保护成员、公有成员
1.多继承同名隐藏
可见性原则:
如果存在两个或多个具有包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名的标识符,那么外层标识符在内层仍然可见;如果在内层声明了同名标识符,则外层标识符在内层不可见,这时,称内层标识符隐藏了外层同名标识符,这种现象称为隐藏规则。
应用于派生类与基类的关系中(基类、派生类相互包含,派生类在内层)
如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏
同时,如果某派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将隐藏所有基类的同名成员
如果子类中定义的函数与父类的函数同名但具有不同的参数表,不属于函数重载,此时子类中的函数将隐藏父类中的同名函数,调用父类中的同名函数必须使用父类名称来限定
只有在相同作用域中定义的函数才可以重载
多继承同名隐藏实例1 [并行继承,基类无交集]
#include <iostream>
using namespace std;
class Base1 {
public:
int var;
int func() { cout << "Base1::" << endl; return var; }
};
class Base2 {
public:
int var;
int func(int x) { cout << "Base2::" << endl; return var; }
};
class Derived :public Base1, public Base2 {
public:
//Derived 会隐藏 所有与其同名的基类成员 : var 、 func(...)
int var;
int func() { cout << "Derived::" << endl; return var; }
};
int main()
{
Derived d;
Derived* pd = &d;
d.var = 100;
cout << d.func() << endl;
d.Derived::var = 1;
cout << d.Derived::func() << endl;
d.Base1::var = 2;
cout << d.Base1::func() << endl;
pd->Base2::var = 3;
cout << pd->Base2::func(55) << endl;
return 0;
}
也可用using引入,则不会产生二义性
#include <iostream>
using namespace std;
class Base1 {
public:
int var;
int func() { cout << "Base1::" << endl; return var; }
};
class Base2 {
public:
int var;
int func(int x) { cout << "Base2::" << endl; return var; }
};
class Derived :public Base1, public Base2 {
public:
Derived 会隐藏 所有与其同名的基类成员 : var 、 func(...)
//int var;
//int func() { cout << "Derived::" << endl; return var; }
using Base1::var;
using Base1::func;
};
int main()
{
Derived d;
Derived* pd = &d;
d.var = 100;
cout << d.func() << endl;
d.Derived::var = 1;
cout << d.Derived::func() << endl;
d.Base1::var = 2;
cout << d.Base1::func() << endl;
pd->Base2::var = 3;
cout << pd->Base2::func(55) << endl;
return 0;
}
using还可使派生类中可共存基类中的同名但参数名不同的函数,即基类的函数并不会被隐藏,两个重载版本将会并存
class Derived:public Base1, public Base2{
public:
using Base2::func;
void func(){...}
}
#include <iostream>
using namespace std;
class Base1 {
public:
int var;
int func() { cout << "Base1::" << endl; return var; }
};
class Base2 {
public:
int var;
int func(int x) { cout << "Base2::" << endl; return var; }
};
class Derived :public Base1, public Base2 {
public:
//此时Derived类,里面有两个func()函数,重载并存
using Base2::func;
int var;
int func() { cout << "Derived::" << endl; return var; }
};
int main()
{
Derived d;
Derived* pd = &d;
d.var = 100;
cout << d.func() << endl;
d.Base2::var = 111;
cout << d.func(1000) << endl; //调用的是被引入Derive类的重载函数
/*d.Derived::var = 1;
cout << d.Derived::func() << endl;
d.Base1::var = 2;
cout << d.Base1::func() << endl;
pd->Base2::var = 3;
cout << pd->Base2::func(55) << endl;*/
return 0;
}
多继承同名隐藏规则实例2[基类由共同基类继承而来]
Base0,声明数据成员var0和函数func0(),再由Base0公有派生了类Base1和Base2,再以Base1、Base2作为基类共同公有派生产生新类Derived,这时,Derived类中就会有两个var0、func0()分别继承自Base1、Base2
#include <iostream>
using namespace std;
class Base0 {
public:
int var0;
int func0() { cout << "Base0::" << endl; return var0; }
};
class Base1 :public Base0 {
public:
int var1;
void func1() { cout << "Base1::" << endl; }
};
class Base2 :public Base0 {
public:
int var2;
void func2() { cout << "Base2::" << endl; }
};
class Derived :public Base1, public Base2 {
public:
int var;
void func() { cout << "Derived::" << endl; }
};
int main()
{
Derived d;
Derived* pd = &d;
//不可直接访问,因为var0、func0均有两份,不知道要访问来自Base1的,还是Base2的
//d.var0 = 100;
//cout << d.func0() << endl;
d.Base1::var0 = 1;
cout << d.Base1::func0() << endl;
d.Base2::var0 = 2;
cout << d.Base2::func0() << endl;
return 0;
}
在很多情况下,其实我们只需要一份这样的数据,同一份成员的多份数据增加了内存的开销
而C++提供了 虚基类 解决这样的问题
2.虚基类
将共同基类设置为虚基类,则这时从不同路径继承过来的同名数据成员在内存中就只有一个,同一个函数名也只有一个映射
语法形式
class 派生类名:virtual 继承方式 基类名 //将基类声明为派生类的虚基类
//virtual的作用范围也是只对紧随其后的基类起作用
1.虚基类基本使用方式
声明了虚基类之后,虚基类的成员在进一步派生过程中,和基类一起维护同一个内存数据
#include <iostream>
using namespace std;
class Base0 {
public:
int var0;
int func0() { cout << "Base0::" << endl; return var0; }
};
class Base1 :virtual public Base0 {
public:
int var1;
void func1() { cout << "Base1::" << endl; }
};
class Base2 :virtual public Base0 {
public:
int var2;
void func2() { cout << "Base2::" << endl; }
};
class Derived :public Base1, public Base2 {
public:
int var;
void func() { cout << "Derived::" << endl; }
};
int main()
{
Derived d;
Derived* pd = &d;
//维护同一个Base0
//无论通过什么途径修改,都会改变同一份Base0的数据
d.var0 = 1;
cout << d.var0 << endl;
cout << d.func0() << endl;
cout << d.Base1::var0 << endl;
cout << d.Base1::func0() << endl;
cout << d.Base2::var0 << endl;
cout << d.Base2::func0() << endl;
puts("");
d.Base1::var0 = 2;
cout << d.var0 << endl;
cout << d.func0() << endl;
cout << d.Base1::var0 << endl;
cout << d.Base1::func0() << endl;
cout << d.Base2::var0 << endl;
cout << d.Base2::func0() << endl;
puts("");
d.Base2::var0 = 3;
cout << d.var0 << endl;
cout << d.func0() << endl;
cout << d.Base1::var0 << endl;
cout << d.Base1::func0() << endl;
cout << d.Base2::var0 << endl;
cout << d.Base2::func0() << endl;
return 0;
}
2.虚基类及其派生类构造函数
当虚基类声明含有非默认形式(带形参的)构造函数,并且没有声明默认形式的构造函数,
这时,在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化列表
但是,虽然写了多次,在实际调用构造函数时,只会通过 最远派生类(建立对象时所指定的类) 初始化一次,该派生类的其他基类对虚基类构造函数的调用都自动被忽略
#include <iostream>
using namespace std;
class Base0 {
public:
Base0(int v) {
this->var0 = v;
}
int var0;
int func0() { cout << "Base0::" << endl; return this->var0; }
};
class Base1 :virtual public Base0 {
public:
Base1(int v) :Base0(v) { /*Base0(v);*/ } //不可写在函数体内部
int var1;
void func1() { cout << "Base1::" << endl; }
};
class Base2 :virtual public Base0 {
public:
Base2(int v):Base0(v){}
int var2;
void func2() { cout << "Base2::" << endl; }
};
class Derived :public Base1, public Base2 {
public:
Derived(int v):Base2(v),Base1(v),Base0(v){}
int var;
int func() { cout << "Derived::" << endl; return var; }
};
int main()
{
Derived d(1); //初始化时,将Base0 的 var0 初始化为1
d.var = 2; //Derived类中var 赋值为2
cout << d.func() << endl;
cout << d.func0();
return 0;
}
重点:构造一个类的对象的一般顺序
- 如果该类有直接或间接的虚基类,则先执行虚基类的构造函数
- 如果该类有其他基类,则按照它们在继承声明列表中出现的次序,分别分别执行它们的构造函数
且在构造过程中,不再执行它们的虚基类的构造函数 - 按照在类定义中出现的顺序,对派生类新增成员对象进行初始化。
对于类类型的成员对象,如果出现在构造函数初始化列表中,则以其中指定的参数执行构造函数,如果未出现,则执行默认构造函数
对于基本数据类型的成员对象,如果出现在了构造函数的初始化列表中,则使用其中指定的值为其赋初值,否则什么也不做 - 执行构造函数的函数体
#include <iostream>
using namespace std;
class Base0 {
public:
Base0(int v) :var0(v) { cout << "Constructing Base0" << endl; }
int var0;
int func0() { return this->var0; }
};
class Base1 :virtual public Base0{
public:
Base1(int v0, int v1) :Base0(v0),var1(v1) { cout << "Constructing Base1" << endl; }
int var1;
int func1() { return this->var1; }
};
class Base2 :virtual public Base0 {
public:
Base2(int v0, int v2) :Base0(v0), var2(v2) { cout << "Constructing Base2" << endl; }
int var2;
int func2() { return this->var2; }
};
class Derived :public Base2, public Base1 { // 调用基类的构造函数顺序: 2 1
public:
Derived(int v0, int v1, int v2, int v3, int v4, int v) :Base1(v3, v1), Base2(v4, v2), var(v), Base0(v0) { cout << "Entering Derived Constructing function...." << endl; }
int var;
int func() { return this->var; }
};
//虚基类的初始化到底是由谁完成的?
//构造一个类的顺序到底是?
int main()
{
Derived d(1, 2, 3, 4, 5, 6);
puts("");
cout << d.func() << endl; // Derived 中 var的值 即var == v == 6
cout << d.Base1::func1() << endl; //Base1 中 var1 的值 即var1 == v1 == 2
cout << d.Base2::func2() << endl; //Base2 中 var2 的值 即var2 == v2 == 3
//虚基类Base0 中 var0 的值 即var0 == v0 == 1
//传入v3 v4,只是为了进一步确定调用Base1、Base2的构造函数时,会不会再次调用Base0的构造函数,验证的结果是:不会
cout << d.func0() << endl;
cout << d.Base1::func0() << endl;
cout << d.Base2::func0() << endl;
return 0;
}
防止日后忘了此过程,还是在此赘述一下 😀
【结合上面、下面的代码查看】
1.Derived类是最远派生类,它由Base1、Base2共同公有派生而来,而Base1、Base2含有相同的基类 Base0,所以将Base0设置为虚基类
则Derived类有间接基类Base0,所以执行时首先调用虚基类Base0的构造函数
2.Derived类还有Base1、Base2类,按照 在继承时声明的顺序 class Derived :public Base2, public Base1
顺序执行,并且不再执行它们的虚基类Base0的构造函数
3.按照Derived中声明的顺序,例如下面的 Base2 b2; Base1 b1;
先执行构造b2的函数,再执行构造b1的函数,而对于基本数据类型 int var;
则直接初始化即可
4.最后执行构造函数的函数体 。对此Derived类来说,函数体中,只有一条提示性的语句{ cout << "Entering Derived Constructing function...." << endl; }
下面的是稍微复杂一点点的探究版本
#include <iostream>
using namespace std;
class Base0 {
public:
Base0(int v) :var0(v) { cout << "Constructing Base0" << endl; }
int var0;
int func0() { return this->var0; }
};
class Base1 :virtual public Base0{
public:
Base1(int v0, int v1) :Base0(v0),var1(v1) { cout << "Constructing Base1" << endl; }
int var1;
int func1() { return this->var1; }
};
class Base2 :virtual public Base0 {
public:
Base2(int v0, int v2) :Base0(v0), var2(v2) { cout << "Constructing Base2" << endl; }
int var2;
int func2() { return this->var2; }
};
class Derived :public Base2, public Base1 { // 调用基类的构造函数顺序: 2 1
public:
Derived(int v0, int v1, int v2, int v3, int v4, int v, int vb10, int vb11, int vb20, int vb21) :Base1(v3, v1), Base2(v4, v2), var(v), Base0(v0), b2(vb20, vb21), b1(vb10, vb11){ cout << "Entering Derived Constructing function...." << endl; }
Base2 b2;
Base1 b1;
int var;
int func() { return this->var; }
};
//虚基类的初始化到底是由谁完成的?
//构造一个类的顺序到底是?
int main()
{
Derived d(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
puts("");
cout << d.func() << endl; // Derived 中 var的值 即var == v == 6
cout << d.Base1::func1() << endl; //Base1 中 var1 的值 即var1 == v1 == 2
cout << d.Base2::func2() << endl; //Base2 中 var2 的值 即var2 == v2 == 3
puts("");
//虚基类Base0 中 var0 的值 即var0 == v0 == 1
//传入v3 v4,只是为了进一步确定调用Base1、Base2的构造函数时,会不会再次调用Base0的构造函数验证的结果是:不会
cout << d.func0() << endl;
cout << d.Base1::func0() << endl;
cout << d.Base2::func0() << endl;
puts("");
//Derived中新增了两个类对象,分别是 Base1类的b1, Base2类的b2 按照它们被声明的顺序 先调用Base2类的构造函数 再调用Base1类的构造函数
//同时,调用过程中,也会对它们各自的虚函数Base0中的var0进行初始化
//因为设置Base0为虚基类,是为了只在Derived类中存放一份从Base1,Base2继承而来的Base0,不会影响到Base1、Base2各自的Base0的正常使用
cout << d.b1.func0() << endl;
cout << d.b2.func0() << endl;
cout << d.b1.func0() << endl;
cout << d.b2.func0() << endl;
return 0;
}
第八章 多态性
多态是指同样的消息被不同类型的对象接收时导致不同的行为
消息:对类的成员函数的调用
行为:实现[调用不同的函数]
8.1多态性的概述
1.多态类型
4类: 重载多态、强制多态、包含多态、参数多态
专用多态:重载多态、强制多态
通用多态:包含多态、参数多态
- 重载多态:普通函数及类的成员函数的重载、运算符重载
- 强制多态:将一个变元的类型加以变化,以符合一个函数或者操作的要求
- 包含多态:类族中定义于不同类中的同名成员函数的多态行为,主要通过虚函数实现
- 参数多态:与类模板相关联,使用时必须赋予实际的类型才可以实例化
2.多态实现
多态从实现角度划分为两类:编译时的多态 和运行时的多态
前者是在编译过程中确定了同名操作的具体操作对象
后者则是在程序运行过程中才动态地确定操作所针对的具体对象。
这种确定操作的具体对象的过程就是绑定
绑定:计算机程序自身彼此关联的过程,就是把一条消息 和 一个对象的方法相结合的过程
绑定工作在编译连接阶段完成的情况称为静态绑定 : 重载、强制、参数多态
绑定工作在运行阶段完成的情况称为动态绑定: 包含多态
8.2 运算符重载
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为
运算符重载的实质就是函数重载
实现过程:首先把指定的运算符表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参,根据实参类型确定需要调用的函数 [ 这个过程是在编译过程完成]
1.重载规则
- 只可重载C++中已经存在的运算符,且除了
类属运算符. 成员指针运算符.* 作用域分辨符:: 三目运算符 ?:
- 重载之后运算符的优先级、结合性都不会改变
- 重载功能应与原功能类似,不能改变原运算符操作对象的个数,同时至少要有一个操作对象是自定义类型
语法形式
①重载为类的成员函数
返回类型 类名::operator 运算符(形参表)
{
函数体
}
②重载为非成员函数
(friend) 返回类型 operator 运算符(形参表)
{
函数体
}
//以非成员函数形式重载运算符时,有时需要访问运算符参数所涉及类的私有成员,此时可以将其声明为类的友元函数
运算符重载为类的成员函数时,函数的参数个数比原来操作数个数少一(后置++、–除外)
当重载为非成员函数时,参数个数与原操作数相同
原因:重载为类的成员函数时,第一个操作数会被作为函数调用的目的对象,则无需出现在参数表中,函数体中可以直接访问第一个操作数的成员;而重载为非成员函数时,运算符的所有操作数必须显示地通过参数传递
运算符重载的主要优点:可以改变现有运算符的操作方式,以用于类类型,使得程序看起来更加直观
2.运算符重载为成员函数
复数类加减法运算重载----成员函数形式
#include <iostream>
using namespace std;
class Complex {
public:
Complex(double x = 0, double y = 0):x(x),y(y){}
Complex operator+(const Complex& c)const{
return Complex(x + c.x, y + c.y);
}
Complex operator-(const Complex& c)const {
return Complex(x - c.x, y - c.y);
}
double Get_x()const { return this->x; }
double Get_y()const { return this->y; }
private:
double x, y;
};
int main()
{
Complex c1, c2(3, 4); // c1 = 0 + 0i c2 = 3 + 4i
Complex c3 = c1 - c2;
cout << c3.Get_x() << " " << c3.Get_y() << endl;
Complex c4 = c3 + c2;
cout << c4.Get_x() << " " << c4.Get_y() << endl;
return 0;
}
重载单目运算符 ++ 为成员函数形式(前置、后置)
语法规定:
对于前置单目运算符,重载函数没有形参
对于后置运算符,重载函数有一个int型形参
#include <iostream>
using namespace std;
class Clock {
public:
Clock(int h, int m, int s):h(h),m(m),s(s){}
//重载前置++
Clock& operator++() {
++s;
if (s >= 60)
{
s -= 60;
m += 1;
if (m >= 60)
{
m -= 60;
h = (h + 1) % 24;
}
}
return *this; //返回的是前置++后的自身
}
//重载后置++
Clock operator++(int) {
Clock old = *this;
++(*this);
return old; // 返回++前的对象
}
int Get_h() { return this->h; }
int Get_m() { return this->m; }
int Get_s() { return this->s; }
void show_Time() {
cout << h << " " << m << " " << s << endl;
}
private:
int h, m, s;
};
int main()
{
Clock c(23, 59, 59);
c.show_Time();
//前置++
(++c).show_Time();
//后置++
(c++).show_Time();
//最终
c.show_Time();
return 0;
}
3.运算符重载为非成员函数
//重载 + -
//重载前置 ++、-- 后置++、--
//重载 <<
//为非成员函数
#include <iostream>
using namespace std;
class Complex {
public:
Complex(int x = 0, int y = 0) {
this->x = x, this->y = y;
}
// + -
friend Complex operator+(const Complex& c1, const Complex& c2);
friend Complex operator-(const Complex& c1, const Complex& c2);
//前置++ 、 --
friend Complex& operator++(Complex& c);
friend Complex& operator--(Complex& c);
//后置++、 --
friend Complex operator++(Complex& c, int);
friend Complex operator--(Complex& c, int);
// <<
friend ostream& operator<<(ostream& out, const Complex& c);
private:
int x, y;
};
Complex operator+(const Complex& c1, const Complex& c2)
{
return Complex(c1.x + c2.x, c1.y + c2.y);
}
Complex operator-(const Complex& c1, const Complex& c2)
{
return Complex(c1.x - c2.x, c1.y - c2.y);
}
//前置++ 、 --
Complex& operator++(Complex& c)
{
++c.y;
return c;
}
Complex& operator--(Complex& c)
{
--c.y;
return c;
}
//后置++、 --
Complex operator++(Complex& c, int)
{
Complex old = c;
c.y++;
return old;
}
Complex operator--(Complex& c, int)
{
Complex old = c;
c.y--;
return old;
}
//ostream 是 cout类型的一个基类 , 为了防止出现 cout << c1 << c2 ,所以设定ostream类型返回值
ostream& operator<<(ostream& out, const Complex& c)
{
out << c.x << " " << c.y;
return out;
}
int main()
{
Complex c1(1, 3), c2(3, 6);
//验证 << 和 前置++
cout << (++c1) << endl;
//验证 << 和 后置++
cout << (c1++) << endl;
cout << c1 << endl;
puts("");
cout << (--c2) << endl;
cout << (c2--) << endl;
cout << c2 << endl;
puts("");
cout << (c1 + c2) << endl;
cout << (c1 - c2) << endl;
return 0;
}
8.3虚函数
虚函数是动态绑定的基础。
虚函数必须是非静态的成员函数
虚函数经过派生之后,在类族中就可以实现运行过程中的多态
根据赋值兼容规则,可以使用派生类的对象代替基类对象,如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数。这样,通过基类类型的指针就可以使属于不同派生类的不同对象产生不同的行为,从而实现运行过程中的多态
1.一般虚函数成员
一般虚函数成员的声明语法
virtual 函数类型 函数名(形参表)
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候
运行过程中的多态满足3个条件
- 类之间满足赋值兼容规则
- 声明虚函数
- 由成员函数来调用 或是 通过指针、引用来访问虚函数
而如果使用对象名来访问虚函数,则绑定 在编译过程中就可以进行(静态绑定),而无需在运行过程中进行
虚函数一般不声明为内联函数,因为对虚函数调用需要动态绑定,而对内联函数的处理是静态的,所以虚函数一般不能以内联函数处理,但将虚函数声明为内联函数也不会引起错误
//一般虚函数成员
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void display() const; //虚函数
void test() { display(); }
};
void Base1::display()const
{
cout << "Base1::display()" << endl;
}
class Base2:public Base1 {
public:
void display()const { cout << "Base2::display()" << endl; }
};
class Derived :public Base2 {
public:
void display()const { cout << "Derived::display()" << endl; }
};
void func(Base1* ptr)
{
ptr->display();
}
int main()
{
Base1 b1;
Base2 b2;
Derived d;
//使用对象名访问虚函数,则绑定在编译过程中即可进行(静态绑定),无需在运行过程中进行
b1.display();
puts("");
//由成员函数来调用
b1.test();
b2.test();
d.test();
//或者是通过指针 、 引用访问虚函数,则是动态绑定
func(&b1);
func(&b2);
func(&d);
return 0;
}
在本程序中,派生类并未显式给出虚函数声明,这时系统就会遵循以下规则来判断派生类的一个函数成员是不是是不是虚函数
- 该函数是否与基类的虚函数有相同的名称
- 该函数是否与基类的虚函数有相同的参数个数及相同的对应参数类型
- 该函数是否与基类的虚函数有相同的返回值 或者 满足赋值规则的指针、引用型的返回值 (同类型返回值)
如果从名称、参数、返回值3个方面检查之后,派生类的函数满足了上述条件,就会自动被确定为虚函数,这时,派生类的虚函数便覆盖了基类的虚函数,同时,派生类中的虚函数还会隐藏基类中同名函数的其他所有重载形式
只有虚函数是动态绑定的
只有通过基类指针 或 应用调用虚函数,才会发生动态绑定
基类的指针可以指向派生类的对象,基类的引用可以作为派生类对象的别名
但基类的对象却不能表示派生类的对象
Derived d;
Base* ptr = &d; //正确,基类指针可以指向派生类对象
Base& b = d; //正确, 基类引用,可以作为派生类对象的别名
Base b = d; //正确,调用Base类的拷贝构造函数
//用派生类对象拷贝构造基类对象的行为被称作对象切片
2.虚析构函数
C++中,不能声明虚构造函数,但是可以声明虚析构函数
如果一个类的析构函数是虚函数,那么由它派生而来的所有子类的析构函数也是虚函数
在析构函数设置为虚函数之后,使用指针引用时,可以动态绑定,实现运行时的多态,保证使用基类类型的指针便可以调用适当的析构函数针对不同对象进行清理工作
错误示例❌ (不设置虚析构函数)
#include <iostream>
using namespace std;
class Base {
public:
~Base() { cout << "Base destructed" << endl; }
};
class Derived :public Base {
public:
Derived() { p = new int(0); }
~Derived() { cout << "Derived destructed" << endl; delete p; }
private:
int* p;
};
//如果不将Base的析构函数设置为虚析构函数
//那么通过基类Base的指针,无法调用派生类Derive的析构函数,从而无法回收分配给p的空间,从而造成内存泄漏
void func(Base* b)
{
delete b;
}
int main()
{
Base* b = new Derived();
func(b);
return 0;
}
我们会发现,通过基类指针,并不能调用派生类对象的析构函数
正确做法✔
将Base类的析构函数设置为虚析构函数
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() { cout << "Base destructed" << endl; }
};
class Derived :public Base {
public:
Derived() { p = new int(0); }
~Derived() { cout << "Derived destructed" << endl; delete p; }
private:
int* p;
};
//如果不将Base的析构函数设置为虚析构函数
//那么通过基类Base的指针,无法调用派生类Derive的析构函数,从而无法回收分配给p的空间,从而造成内存泄漏
//通过将Base类的析构函数设置为虚析构函数,此时可通过基类Base指针,调用派生类Derived的析构函数
void func(Base* b)
{
delete b;
}
int main()
{
Base* b = new Derived();
func(b);
return 0;
}
8.4纯虚函数与抽象类
抽象类是一种特殊的类,它为一个类族提供统一的操作界面。
建立抽象类,就是为了通过它多态地使用其中的成员函数
抽象类位于类层次的上层,一个抽象类自身无法实例化,即无法定义一个抽象类的对象,只能通过继承机制,生成抽象类的非抽象派生类,然后再实例化。
1.纯虚函数
纯虚函数是一个在基类中声明的虚函数
它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本
virtual 函数类型 函数名(参数表)= 0;
声明为纯虚函数之后,基类中不再给出函数的实现部分,纯虚函数的函数体由派生类给出
基类中仍然允许对纯虚函数给出实现,但即使给出实现,也必须由派生类覆盖,否则无法实例化;
如果将析构函数声明为纯虚函数,则必须给出它的实现,因为派生类的析构函数体执行完后需要调用基类的纯虚函数
纯虚函数、函数体为空的虚函数
纯虚函数无函数体,而空的虚函数的函数体为空
纯虚函数所在的类为抽象类,不可直接实例化,而后者所在的类可实例化
它们的共同特点是,都可以派生出新的类,然后在新类中给出虚函数新的实现,而且这种实现可以具有多态特征
2.抽象类
带有纯虚函数的类是抽象类
抽象类不能实例化 即 不可定义一个抽象类的对象
但可以定义一个抽象类的指针和引用,通过指针或引用,就可以指向并访问派生类对象,进而访问派生类成员,这种访问是具有多态特征的
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void display()const = 0; //纯虚函数
};
class Base2:public Base1 {
public:
void display()const { cout << "Base2::display()" << endl; }
};
class Derived :public Base2 {
public:
void display()const{ cout << "Derived::display()" << endl; }
};
void func(Base1* ptr)
{
ptr->display();
}
int main()
{
//Base1 b1; //抽象类无法实例化,即无法定义一个抽象类的对象
Base2 b2;
Derived d;
func(&b2);
func(&d);
return 0;
}
抽象类Base1通过纯虚函数为整个类族提供了通用的外部接口
通过公有派生而来的子类给出了纯虚函数的具体实现
派生类中display()也可加上virtual声明
3.总结
C++由C发展而来,许多细碎的知识点,但重要的知识点
较为集中,理解掌握即可
4.更新日志
2022.7.1 计划10天左右整理完成 前八章 主要知识点~ (考试范围)
2022.7.2 整理至 第四章 类和对象
2022.7.12 整理至 第五章(绝对不是因为懒才 鸽了)
2022.9.8 突然想起来没整理完,更新2.3节并添加彩蛋~
2022.11.5 重整大纲,并着手整理后续章节
2022.11.19 前八章整理完毕(考试倒计时一天)
欢迎评论留言、指正~~
版权声明:本文标题:C++语言程序设计(第5版 郑莉、董渊)学习笔记 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/xitong/1729070163a1184953.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论