C++ 程序设计复习资料(按 PPT 顺序精读版)
这份资料按 cpp 文件夹中老师课件的章节和小节顺序重写。它不是逐页转录稿,而是把 PPT 中的概念、规则、代码例子、课堂易错点整理成可读的复习笔记。每章末尾附有对应 C++ 练习题和答案提示。
使用建议:先按章节读“核心笔记”,再看“易错点”,最后做练习题。遇到不熟的语法,建议在本地写一个小程序跑一遍。
第 1 章 绪论
1.1 程序设计概述
- 程序设计的目标:设计出让计算机完成某个任务的程序。
- 计算机科学关注的核心问题不是“计算机本身”,而是:哪些问题可以计算、如何计算、计算代价有多大。
- 计算:利用计算机执行程序来解决问题。
- 程序:用程序设计语言描述的、可以被计算机执行的指令序列。
- 算法:解决问题的步骤。程序是算法的具体实现。
- 学程序设计不是只学语法,而是训练把问题分解、抽象、表达和验证的能力。
1.2 计算机组成
- 计算机系统通常由硬件和软件组成。
- 硬件层面需要理解:CPU、内存、外部存储、输入设备、输出设备。
- CPU 负责执行指令;内存保存当前正在运行的程序和数据;外部存储保存长期数据。
- 程序运行时,数据和指令都要以二进制形式存入内存。
- 内存可以看作连续编号的存储单元,每个单元有地址。后续指针章节会直接使用“地址”这个概念。
- 数据表示要点:整数、字符、浮点数最终都以二进制保存,类型决定如何解释这些二进制位。
1.3 程序设计语言
- 机器语言:计算机能直接执行,但可读性差。
- 汇编语言:用助记符表示机器指令,仍接近硬件。
- 高级语言:更接近人类表达,需要编译或解释后才能执行。
- C++ 是编译型语言。一般过程是:源代码
.cpp-> 编译 -> 目标文件 -> 链接 -> 可执行文件。 - 编译器能发现语法错误和一部分类型错误;运行错误和逻辑错误需要测试和调试。
1.4 程序设计过程
- 问题分析:明确输入、输出、约束和边界情况。
- 算法设计:先想清楚步骤,再写代码。
- 编码实现:用 C++ 把算法翻译成程序。
- 编译链接:检查语法、类型、库调用和链接问题。
- 测试调试:用正常数据、边界数据、异常数据验证。
- 维护改进:规则变化时,应尽量少改代码,这也是后续函数、结构体、类、模块化的目的。
易错点
- 把“会写语句”误认为“会程序设计”。真正的重点是问题分解和边界处理。
- 忽略数据表示。比如整数除法、浮点误差、字符编码,都会影响程序结果。
- 写代码前不明确输入输出,容易导致程序结构混乱。
练习题
- 说明程序、算法、计算三者的关系。
- 答案提示:算法是步骤,程序是算法的语言实现,计算是执行程序得到结果的过程。
- 用自然语言写出求两个正整数最大公约数的算法。
- 答案提示:可用欧几里得算法,循环执行
a%b,直到余数为 0。
- 答案提示:可用欧几里得算法,循环执行
- 将十进制
42转为二进制;将二进制101101转为十进制。- 答案提示:
42 = 101010₂,101101₂ = 45。
- 答案提示:
- 按程序设计过程说明如何写“输入成绩,输出是否及格”的程序。
- 答案提示:明确输入成绩,输出及格/不及格;算法是判断
score >= 60;再编码测试59,60,100,-1。
- 答案提示:明确输入成绩,输出及格/不及格;算法是判断
第 2 章 程序的基本组成
2.1 两个例子:程序的组成
一个最小 C++ 程序通常包含:注释、预处理命令、名字空间、主函数、语句。
#include <iostream>
using namespace std;
int main()
{
cout << "hello world!" << endl;
return 0;
}
- 注释:写给人看的说明,编译器忽略。
//注释到本行结束。/* ... */可以跨行。
- 预处理命令:以
#开头,在编译前处理。#include <iostream>表示包含标准输入输出库的接口。#define可定义宏,但 C++ 中常量更推荐const。
- 名字空间:解决命名冲突。
- 标准库实体在
std中。 - 可以写
std::cout,也可以使用using namespace std;。
- 标准库实体在
main函数:程序入口。每个完整程序必须有且只有一个main。- 语句以分号结束,函数体用
{}包围。
2.2 变量与常量
- 变量:程序运行中可以改变的命名存储空间。
- 常量:运行中不改变的值。
- 标识符命名:由字母、数字、下划线组成,不能以数字开头,不能使用关键字。
- 变量定义包含类型和名字,例如:
int age;
double radius;
char grade;
bool pass;
- 基本类型:
int:整数。double/float:实数,double精度更高。char:字符,本质上可按整数编码保存。bool:逻辑值,true或false。
- 常量定义:
const double PI = 3.1415926;
- 相比
#define PI 3.14,const有类型,编译器能检查,更符合 C++ 风格。 - 浮点数不是所有小数都能精确表示,因此比较浮点数时不要直接依赖
==。
2.3 输入与输出
- 输出流对象
cout绑定标准输出设备,通常是显示器。 - 输入流对象
cin绑定标准输入设备,通常是键盘。 <<是流插入运算符,用于输出。>>是流提取运算符,用于输入。endl表示换行并刷新输出缓冲。
int x;
cin >> x;
cout << "x = " << x << endl;
cin >>默认以空白字符分隔数据,不能直接读入含空格的一整行字符串。- 连续输出依赖
<<返回输出流本身:
cout << "area = " << area << endl;
2.4 算术和赋值运算
- 算术运算:
+ - * / %。 %只能用于整数,表示取余。- 整数除法会截断小数部分:
4 / 5 == 0。 - 若想得到实数结果,至少一个操作数要是实数:
4.0 / 5或4 / 5.0。 - 赋值运算:
=,右结合,可链式赋值:
d = i = 1.5; // i 得到 1,d 得到 1.0
- 复合赋值:
+= -= *= /= %=。 - 自增自减:
++i、i++、--i、i--。- 前置:先改变再使用。
- 后置:先使用再改变。
- 优先级大致顺序:括号最高,然后算术,再关系,再逻辑,再赋值。
易错点
=是赋值,==才是相等判断。4/5是整数除法,结果不是0.8。- 宏替换没有类型检查,也不自动加括号,复杂宏容易出错。
using namespace std;方便入门,但大型项目中更推荐显式写std::。
练习题
- 输入圆半径,输出面积和周长,要求使用
const double PI。- 答案提示:
area = PI*r*r,perimeter = 2*PI*r。
- 答案提示:
- 设
int a=5,b=2;,求a/b、a%b、a*1.0/b。- 答案提示:分别为
2、1、2.5。
- 答案提示:分别为
- 解释
#include <iostream>、using namespace std;、return 0;的作用。- 答案提示:引入库接口、使用标准名字空间、表示程序正常结束。
#define SQUARE(x) x*x计算SQUARE(1+2)为什么错?- 答案提示:展开为
1+2*1+2,应写成((x)*(x)),但 C++ 更推荐函数。
- 答案提示:展开为
第 3 章 分支程序设计
3.1 关系表达式
- 关系表达式用于比较两个值。
- 关系运算符:
>、>=、<、<=、==、!=。 - 结果是
true/false,也可看作1/0。 - 优先级:算术运算高于关系运算,关系运算高于赋值运算。
==和!=的优先级低于< <= > >=。- 关系运算符左结合:
0 < x < 10 // 错误区间写法,等价于 (0 < x) < 10
正确写法:
x > 0 && x < 10
3.2 逻辑表达式
- 逻辑运算符:
&&:逻辑与。||:逻辑或。!:逻辑非。
- 短路求值:
A && B中,如果A为假,B不再计算。A || B中,如果A为真,B不再计算。
- 短路求值常用于保护危险操作:
if (p != nullptr && *p > 0) {
// 只有 p 非空时才会解引用
}
3.3 if 语句
- 单分支:
if (condition) {
statement;
}
- 双分支:
if (condition) {
statement1;
} else {
statement2;
}
- 多分支:
if (score >= 90) grade = 'A';
else if (score >= 80) grade = 'B';
else if (score >= 70) grade = 'C';
else if (score >= 60) grade = 'D';
else grade = 'E';
- 嵌套
if中,else总是与最近的未匹配if配对。用{}可以避免歧义。 - 分支程序要特别测试边界值,例如
59/60/89/90。
3.4 switch 语句
switch适合离散值分支,表达式通常是整型、字符型或枚举型。
switch (op) {
case '+': result = a + b; break;
case '-': result = a - b; break;
default: cout << "unknown operator" << endl;
}
case后必须是常量表达式。break用于跳出switch。漏写break会继续执行后续 case,这称为贯穿。default用于处理未列出的情况。
易错点
if (x = 0)是赋值,不是判断。判断应写if (x == 0)。- 不能写
0 < x < 10表示区间。 switch的case不能写范围,例如case score >= 90不合法。- 嵌套分支必须用缩进和
{}保持结构清楚。
练习题
- 求值:
int a=2,b=4,c=6,d=0,x; x = a + b > c == ++d;执行后x和d是多少?- 答案提示:
a+b为 6,6>6为 0,++d为 1,0==1为 0,所以x=0,d=1。
- 答案提示:
- 当
x=20时,0 < x < 10的值是什么?- 答案提示:为真,因为按
(0<x)<10计算。
- 答案提示:为真,因为按
- 输入百分制成绩,输出等级,处理非法输入。
- 答案提示:先判断
<0或>100,再用if-else if分段。
- 答案提示:先判断
- 输入一元二次方程系数,按判别式输出根的情况。
- 答案提示:注意
a==0时不是一元二次方程。
- 答案提示:注意
第 4 章 循环控制
4.1 for 循环
- 循环适合处理有规律的重复计算。
- 循环三要素:初始化、循环条件、控制变量变化。
int sum = 0;
for (int i = 1; i <= 100; ++i) {
sum += i;
}
for常用于循环次数明确的场景。- 三个表达式都可以省略,但分号不能省略。
4.2 while 循环
while先判断条件,再执行循环体。- 适合循环次数不确定、由条件控制的场景。
while (t <= 100) {
do_heat();
t = get_temp();
}
- 要保证循环体中有使条件趋向结束的变化,否则会死循环。
4.3 do-while 循环
do-while先执行一次,再判断条件。- 适合至少执行一次的场景,例如菜单输入。
do {
cin >> choice;
} while (choice < 0 || choice > 5);
4.4 循环的中途退出
break:立即退出当前循环。continue:跳过本轮剩余语句,进入下一轮。- 嵌套循环中,
break只跳出它所在的最近一层循环。
4.5 枚举法
- 枚举法:把所有可能情况逐个试一遍,筛选满足条件的解。
- 适合范围有限、规则明确的问题。
for (int a = 0; a <= 20; ++a)
for (int b = 0; b <= 10; ++b)
for (int c = 0; c <= 4; ++c)
if (a + 2*b + 5*c == 20)
cout << a << ' ' << b << ' ' << c << endl;
4.6 贪婪法
- 贪婪法:每一步都选择当前看起来最优的方案。
- 优点是简单高效;缺点是不一定得到全局最优。
- 使用贪婪法前要证明局部最优能推出全局最优,或者至少知道它只是近似策略。
易错点
- 忘记更新循环控制变量会死循环。
while可能一次都不执行,do-while至少执行一次。- 循环条件边界容易错,例如
< n和<= n。 - 枚举时应尽量缩小范围,避免无意义循环。
练习题
- 用
for、while、do-while分别计算1+2+...+n。- 答案提示:三种写法都要处理
n<=0的情况。
- 答案提示:三种写法都要处理
- 判断一个整数是否为素数。
- 答案提示:从
2枚举到sqrt(n),发现整除则不是素数。
- 答案提示:从
- 输出九九乘法表。
- 答案提示:外层控制行,内层控制列。
- 用 1、2、5 元硬币凑 20 元,输出所有组合。
- 答案提示:三重循环枚举,条件为
a + 2*b + 5*c == 20。
- 答案提示:三重循环枚举,条件为
第 5 章 批量数据处理:数组
5.1 一维数组
- 数组用于保存一批同类型数据。
- 定义格式:
类型 数组名[元素个数];
示例:
double altitude[60];
- 元素个数必须是常量表达式。课件中用
#define NumOfElement 60的写法可行,但 C++ 更推荐const int N = 60;。 - 下标从
0开始,到n-1结束。 - C++ 不自动检查数组越界,越界访问是严重错误。
- 初始化:
float x[5] = {-1.1, 0.2, 33.0}; // 后两个元素自动补 0
- 遍历数组时,循环范围通常是
0 <= i < n。
5.2 查找和排序
- 顺序查找:从前到后逐个比较,适合无序数组。
- 查找结果通常返回下标;找不到返回
-1。
int find(const int a[], int n, int key) {
for (int i = 0; i < n; ++i)
if (a[i] == key) return i;
return -1;
}
- 排序常见方法:选择排序、冒泡排序。
- 选择排序思想:每轮在未排序部分找最小值,放到前面。
- 冒泡排序思想:相邻元素比较,大的逐步移动到后面。
5.3 二维数组
- 二维数组可表示矩阵、表格。
int a[3][4];
- 第一个下标表示行,第二个下标表示列。
- 存储上仍是连续内存,按行优先存储。
- 作为函数参数时,第二维大小通常必须写明:
void print(int a[][4], int rows);
5.4 字符串
- C 风格字符串本质上是以
\0结尾的字符数组。
char s[20] = "hello";
- 字符数组不等于字符串。只有包含终止符
\0的字符序列才是 C 风格字符串。 - 常见操作:求长度、复制、比较、连接。
- 使用字符数组时必须预留
\0的空间。 - 后续指针章节会继续讨论字符指针与字符串。
易错点
- 下标越界不会报语法错误,但会破坏内存。
- 数组定义时的长度不能是普通运行期变量。动态长度要等指针和动态内存章节。
- 字符串数组长度至少要比有效字符数多 1,用于保存
\0。 - 对字符数组不能直接用
==比较字符串内容,应使用字符串处理函数或逐字符比较。
练习题
- 输入 10 个整数,输出最大值、最小值和平均值。
- 答案提示:初始化最大最小值为第一个元素,然后遍历更新。
- 在数组中顺序查找某整数,存在输出下标,否则输出
-1。- 答案提示:找到后立即返回或记录位置。
- 写选择排序或冒泡排序,将数组升序排列。
- 答案提示:选择排序每轮确定一个最小值位置。
- 输入 3x3 矩阵,输出主对角线和。
- 答案提示:累加
a[i][i]。
- 答案提示:累加
- 不用
strlen,计算字符数组中字符串长度。- 答案提示:从 0 开始数,遇到
\0停止。
- 答案提示:从 0 开始数,遇到
第 6 章 过程封装:函数
6.1 函数定义
- 函数把完成特定任务的语句封装起来,是模块化设计的基本工具。
- 定义格式:
返回类型 函数名(形式参数表)
{
语句序列
}
示例:
int getMax(int a, int b)
{
return a > b ? a : b;
}
- 函数名应表达功能。
- 返回类型为
void表示不返回值。 return会结束函数执行并返回结果。
6.2 函数的使用
- 使用函数前,编译器必须知道函数声明。
- 函数声明也叫函数原型:
int getMax(int a, int b);
- 实参传给形参时,默认是值传递,函数内修改形参不会影响实参。
- 若希望函数修改实参,可用引用或指针。
- 数组作为参数时会退化为指向首元素的指针,因此必须额外传入长度。
double average(const int a[], int n);
6.3 变量作用域与存储类别
- 局部变量:定义在函数或代码块内,只在该范围有效。
- 全局变量:定义在所有函数外,整个文件范围内可见,但会增加耦合,应谨慎使用。
- 作用域决定名字在哪里能用;生命周期决定对象存在多久。
static局部变量:只初始化一次,函数调用结束后仍保留值。- 同名变量内层会隐藏外层。
6.4 重载函数
- 函数重载:同一作用域内,函数名相同,参数列表不同。
- 参数列表不同包括参数个数或参数类型不同。
- 只有返回类型不同不能构成重载。
int maxValue(int a, int b);
double maxValue(double a, double b);
6.5 递归函数
- 递归:函数直接或间接调用自己。
- 必须有递归出口,否则无限递归。
- 递归适合问题能拆成同类子问题的场景。
int factorial(int n)
{
if (n <= 1) return 1;
return n * factorial(n - 1);
}
6.6 函数模板
- 函数模板用于描述类型无关的函数模式。
template <class T>
T myMax(T a, T b)
{
return a > b ? a : b;
}
- 编译器会根据实参类型生成具体函数。
易错点
- 函数声明和定义的参数类型、顺序必须一致。
- 值传递不能修改调用者变量。
- 数组参数没有携带长度,必须单独传
n。 - 递归一定要先确认出口和规模缩小。
- 重载不是“返回类型不同”。
练习题
- 写函数
int max3(int a,int b,int c)。- 答案提示:先求两个数最大值,再与第三个比较。
- 写
void swap(int &a,int &b)并解释引用的作用。- 答案提示:引用形参绑定实参,函数内修改会影响调用者。
- 写
double average(const int a[], int n)。- 答案提示:数组传地址,长度必须由
n给出。
- 答案提示:数组传地址,长度必须由
- 写递归函数计算
n!。- 答案提示:出口
n<=1。
- 答案提示:出口
- 写一个函数模板返回两个值中较大者。
- 答案提示:
template <class T> T myMax(T a,T b)。
- 答案提示:
第 7 章 间接访问:指针
7.1 指针的概念
- 指针保存的是内存地址,也是一种数据。
- 直接访问:通过变量名访问对象。
- 间接访问:通过指针保存的地址访问对象。
int x = 10;
int *p = &x;
cout << *p; // 输出 x 的值
&x:取变量x的地址。*p:访问p指向的对象。- 指针变量本身也有类型、值和地址;它指向的对象也有类型、值和地址。复习时要区分“指针自己”和“它指向的东西”。
7.2 指针运算与数组名
- 数组名在多数表达式中会转换为首元素地址。
a[i]等价于*(a+i)。- 指针加 1 不是地址数值加 1,而是移动到下一个同类型元素。
int a[5] = {1,2,3,4,5};
int *p = a;
cout << *(p + 2); // 3
- 指针运算必须在同一数组范围内才有意义。
7.3 动态内存分配
new申请动态对象,delete释放。new[]申请动态数组,delete[]释放。
int *a = new int[n];
// 使用 a[0] ... a[n-1]
delete[] a;
- 申请和释放形式必须匹配。
- 常见错误:内存泄漏、重复释放、释放后继续使用、越界访问。
7.4 字符串再讨论
- 字符数组:保存可修改的字符串副本。
char s[] = "abc";
- 字符指针指向字符串字面量时,应使用
const char*:
const char *p = "abc";
- 字符串处理时必须考虑
\0和数组容量。
7.5 指针与函数
- 指针形参可让函数修改调用者对象。
void setZero(int *p)
{
if (p != nullptr) *p = 0;
}
- 传指针时要检查空指针。
- 指针也可用于返回动态分配对象,但必须明确由谁释放。
7.6 引用与函数
- 引用是对象的别名,定义时必须初始化。
- 引用形参常用于修改实参或避免大对象拷贝。
const引用可避免拷贝且保护对象不被修改。
void printStudent(const Student &s);
7.7 指针数组与多级指针
- 指针数组:数组元素是指针。
char *names[10];
- 多级指针:指向指针的指针,例如
int **pp。 - 复习时按从变量名向外读的方式分析声明。
7.8 函数指针
- 函数也有地址,函数指针可保存函数入口地址。
int add(int a, int b) { return a + b; }
int (*pf)(int, int) = add;
cout << pf(2, 3);
- 函数指针适合把“要执行的操作”作为参数传递。
易错点
- 未初始化指针不能解引用。
delete后指针最好置为nullptr。new[]必须配delete[]。char *p = "abc"在现代 C++ 中不应修改字面量。- 数组名不是普通指针变量,不能给数组名赋新地址。
练习题
- 定义
int x=10; int *p=&x;,说明p、*p、&p的含义。- 答案提示:地址、指向对象的值、指针变量自身地址。
- 动态申请
n个整数,输入后求和并释放。- 答案提示:
new int[n]和delete[]配对。
- 答案提示:
- 写函数
void setZero(int *p)。- 答案提示:先判断
p != nullptr。
- 答案提示:先判断
- 说明
char s[]="abc"与const char *p="abc"的区别。- 答案提示:前者是数组副本,后者指向字面量。
- 定义一个指向
int f(int,int)的函数指针。- 答案提示:
int (*pf)(int,int) = f;。
- 答案提示:
第 8 章 数据封装:结构体
8.1 结构体类型的定义
- 当一个对象有多个不同类型的信息项时,结构体比平行数组更合适。
- 结构体把相关数据放在一起,保持数据相关性。
struct studentT {
char id[10];
char name[8];
int chinese;
int math;
int english;
};
- 结构体类型定义本身不分配对象空间;定义结构体变量时才分配空间。
- 成员类型可以是任意类型,包括另一个结构体类型。
- 注意:结构体不能直接包含自身类型的成员,但可以包含指向自身类型的指针。
struct Node {
int data;
Node *next;
};
8.2 结构体类型的变量
- 定义变量:
studentT aStudent;
- 成员访问使用
.:
cin >> aStudent.chinese;
- 同类型结构体变量可以相互赋值,含义是逐成员赋值。
- 结构体变量在内存中总体上是一块连续空间,但成员之间可能有对齐填充,所以
sizeof不一定等于成员大小简单相加。 #pragma pack(n)可改变对齐方式,但一般不建议随意使用。
8.3 结构体类型的指针
- 结构体指针保存结构体变量地址。
studentT *sp = &student1;
(*sp).chinese -= 2;
sp->chinese -= 2;
->是通过指针访问成员的简写,优先级很高。- 动态结构体:
studentT *sp = new studentT;
sp->chinese = 0;
delete sp;
8.4 结构体作为函数参数
- 结构体按值传递时会复制每个成员,可能开销较大。
- 常用方式:指针、引用、
const指针或const引用。
void print_student(const studentT &s);
void print_student(const studentT *s);
- 如果函数需要修改结构体,使用非
const引用或指针。 - 返回结构体可以直接返回局部结构体对象;不要返回局部变量的地址或引用。
8.5 结构体应用:链表
- 链表由节点通过指针连接而成。
- 数组优点:随机访问快;缺点:插入删除移动元素,容量固定。
- 链表优点:插入删除方便,容量动态;缺点:查找第
i个元素慢,实现复杂。
单链表节点:
struct linkNode {
int score;
linkNode *next;
};
头插法创建节点:
linkNode *head = nullptr;
linkNode *aNode = new linkNode;
cin >> aNode->score;
aNode->next = head;
head = aNode;
在节点 p 后插入:
aNode->next = p->next;
p->next = aNode;
删除 p 后的节点:
if (p->next) {
linkNode *aNode = p->next;
p->next = aNode->next;
delete aNode;
}
遍历:
for (linkNode *p = head; p != nullptr; p = p->next) {
cout << p->score << endl;
}
- 约瑟夫环可用循环链表实现:每次数到第
m个节点,输出并删除该节点,直到只剩一个节点。
易错点
studentT.age错,因为类型名不能直接访问成员,必须通过对象或指针。(*p).age与p->age等价。- 结构体不能直接包含自身对象成员,否则大小无限递归;可以包含自身指针。
- 链表插入删除一定要先保存必要指针,否则会断链或丢失节点。
- 动态节点删除后不能继续访问。
练习题
- 定义
Student,包含学号、姓名、三门成绩和平均分。- 答案提示:用
struct声明多个成员。
- 答案提示:用
- 写函数
void calcAverage(Student &s)。- 答案提示:引用传递可修改原结构体。
- 输入学生数组,输出平均分最高学生。
- 答案提示:遍历记录最大下标。
- 判断
stu.age、p->age、(*p).age、Student.age哪个非法。- 答案提示:
Student.age非法。
- 答案提示:
- 写单链表遍历代码。
- 答案提示:从
head开始,循环p = p->next。
- 答案提示:从
第 9 章 模块化开发
9.1 结构化程序设计
- 复杂程序不能都写在一个
main中。 - 结构化程序设计强调自顶向下、逐步求精。
- 先把大问题分成子问题,再把子问题继续分解,直到可直接编码。
- 函数是模块化的基本单位。
9.2 模块划分
- 模块化开发:封装实现细节,对外提供接口。
- 优点:便于调试、复用、升级和多人协作。
- 缺点:可能增加调用链和接口沟通成本。
- 好的模块应做到高内聚、低耦合。
- 接口应稳定、清晰,隐藏实现细节。
9.3 库的设计与实现
- 一个模块通常拆成头文件和源文件:
.h:类型定义、常量声明、函数声明。.cpp:函数实现。
- 头文件要加 include guard:
#ifndef STUDENT_H
#define STUDENT_H
// declarations
#endif
- 不要在头文件里随意放全局变量定义,否则多文件编译可能重复定义。
- 源文件包含自己的头文件,以保证声明和实现一致。
9.4 使用你定义的库
- 使用模块时,调用方包含头文件:
#include "student.h"
- 编译链接时,要把相关
.cpp文件一起编译。 - 模块接口变更会影响调用者,实现细节变更不应影响调用者。
易错点
- 头文件只声明不实现,是常见工程习惯;模板例外。
- 忘记 include guard 会导致重复包含。
- 只编译
main.cpp,忘记把模块.cpp一起链接,会出现未定义引用。 - 接口设计过于依赖全局变量,会破坏模块独立性。
练习题
- 把学生成绩管理拆分为输入、计算、排序、输出四个模块,写出函数接口。
- 答案提示:如
readStudents、calcAvg、sortByAvg、printStudents。
- 答案提示:如
- 说明
student.h和student.cpp分别应放什么。- 答案提示:头文件放声明,源文件放实现。
- 写一个头文件保护宏。
- 答案提示:
#ifndef、#define、#endif。
- 答案提示:
main.cpp使用student.cpp函数时,为什么只包含.h还不够?- 答案提示:包含头文件只让编译器知道声明,链接还需要实现文件。
第 10 章 创建功能更强的类型
10.1 面向对象程序设计
- 面向过程:数据和操作分离,用函数一步步解决问题。
- 面向对象:程序由相互协作的对象组成,对象封装数据和操作。
- 类:具有相同属性和行为的事物抽象。
- 对象:类的具体实例。
- 面向对象三大特点:封装、继承、多态。
10.2 类的定义
class 类名 {
private:
私有数据成员和成员函数;
public:
公有数据成员和成员函数;
};
private成员只能被类内成员函数或友元访问。public成员构成类的外部接口。class默认访问权限是private,struct默认是public。- 成员函数可以在类内定义,也可以在类外用作用域限定符定义:
bool DoubleArray::get(int index, double &value)
{
if (index < low || index > high) return false;
value = storage[index - low];
return true;
}
this指针:每个非静态成员函数都有隐藏形参this,指向当前调用对象。- 对象操作:
- 对象访问成员:
obj.member。 - 对象指针访问成员:
p->member。 - 同类对象默认可以逐成员赋值。
- 对象访问成员:
10.3 对象的构造与析构
- 构造函数:对象定义时自动调用,用于初始化。
- 析构函数:对象生命周期结束时自动调用,用于善后。
- 构造函数特点:
- 名字与类名相同。
- 没有返回类型,
void也不写。 - 可以重载。
- 通常声明为
public。
- 析构函数特点:
- 名字为
~类名()。 - 无参数,无返回类型,不能重载。
- 名字为
- 动态资源类必须特别关注析构和拷贝。
class MyArray {
private:
int n;
double *data;
public:
MyArray(int size) : n(size), data(new double[size]) {}
~MyArray() { delete[] data; }
};
- 拷贝构造函数:用已有对象初始化新对象时调用。
MyArray(const MyArray &other);
- 若类中有指针指向动态内存,默认拷贝构造只会复制指针值,造成浅拷贝。应自定义深拷贝。
10.4 const 与类
const对象不能调用非const成员函数。- 不修改对象状态的成员函数应加
const:
void print() const;
const成员函数中,this相当于指向常量对象的指针,不能修改普通数据成员。
10.5 静态成员
- 静态数据成员属于类,而不是某个对象。
- 所有对象共享同一份静态数据成员。
- 静态成员函数没有
this指针,只能直接访问静态成员。 - 静态数据成员通常要在类外定义:
class SavingAccount {
private:
static double rate;
};
double SavingAccount::rate = 0.03;
- 可通过对象或类名访问静态成员,推荐类名访问:
SavingAccount::setRate(0.05);
10.6 友元
- 友元函数不是类的成员函数,但可以访问类的私有成员。
- 友元破坏一定封装性,应谨慎使用。
- 典型用途:运算符重载、需要访问两个类私有成员的函数。
class Point {
friend double distance(const Point &a, const Point &b);
private:
double x, y;
};
易错点
- 析构函数名字不要拼错,必须是
~ClassName()。 - 构造函数没有返回类型。
- 默认赋值和默认拷贝对指针成员是浅拷贝。
const成员函数漏写会导致const对象无法调用。- 静态成员函数不能访问非静态成员,因为没有
this。
练习题
- 把
Student结构体改成类,数据成员私有,提供set/get/print。- 答案提示:外部不能直接访问私有成员,要通过公有接口。
- 写类
Trace,构造和析构时输出信息,观察局部对象的顺序。- 答案提示:构造按定义顺序,析构反序。
- 实现
MyString:动态字符数组、构造、拷贝构造、setString、printString、析构。- 答案提示:拷贝构造中重新申请空间并复制内容。
- 说明
DoubleArray arr1(5,10), arr2(arr1); arr2 = foo(arr2);中会调用哪些函数。- 答案提示:普通构造、拷贝构造、按值传参拷贝、按值返回拷贝、赋值运算符、析构。
- 为
Point类写友元函数计算两点距离。- 答案提示:友元可直接访问
x,y。
- 答案提示:友元可直接访问
第 11 章 运算符重载
11.1 对类重载运算符的方法
- 运算符本质上可以看作函数。运算符
+的函数名是operator+。 - 运算符重载让自定义类型也能使用自然的运算形式。
- 限制:
- 不能创建新运算符。
- 不能改变操作数个数。
- 不能改变优先级和结合性。
- 不是所有运算符都能重载。
- 可重载为成员函数,也可重载为全局函数/友元函数。
成员函数形式:
Rational Rational::operator+(const Rational &r) const
{
Rational tmp;
tmp.num = num * r.den + r.num * den;
tmp.den = den * r.den;
tmp.reductFraction();
return tmp;
}
全局友元形式:
Rational operator+(const Rational &a, const Rational &b)
{
Rational tmp;
tmp.num = a.num * b.den + b.num * a.den;
tmp.den = a.den * b.den;
tmp.reductFraction();
return tmp;
}
- 成员函数左操作数必须是本类对象。
- 全局函数更适合支持
2 + r这类左操作数不是本类对象的表达式。
11.2 几个特殊运算符的重载
- 必须重载为成员函数的运算符:
=、[]、()、->。
赋值运算符
- 如果类需要自定义拷贝构造,通常也需要自定义赋值运算符。
- 赋值运算符要处理:自赋值、释放旧资源、申请新资源、返回
*this。
DoubleArray& DoubleArray::operator=(const DoubleArray &right)
{
if (this == &right) return *this;
delete[] storage;
low = right.low;
high = right.high;
storage = new double[high - low + 1];
for (int i = 0; i <= high - low; ++i)
storage[i] = right.storage[i];
return *this;
}
下标运算符
double& DoubleArray::operator[](int index)
{
return storage[index - low];
}
- 返回引用才能作为左值使用:
arr[3] = 5.0;。
函数调用运算符
- 重载
()后,对象可以像函数一样调用。
class A {
public:
int operator()(int x) const { return x * x; }
};
前置和后置自增
Rational& operator++(); // 前置 ++r
Rational operator++(int); // 后置 r++,int 是区分标记
输入输出运算符
<<和>>通常重载为全局函数。- 返回流引用,支持连续输入输出。
ostream& operator<<(ostream &os, const Rational &r)
{
os << r.num << '/' << r.den;
return os;
}
11.3 自定义类型转换运算符
- 基本类型到类类型:可通过单参数构造函数实现。
- 使用
explicit可禁止隐式转换。
explicit Rational(int n = 0, int d = 1);
- 类类型到其他类型:用类型转换函数。
operator double() const;
- 类型转换方便,但过多隐式转换会让代码难读,应谨慎。
易错点
- 只改返回类型不能重载函数,也不能重载运算符。
operator=必须是成员函数。operator<<的左操作数是ostream,通常不能写成类的成员函数。- 后置
++的int参数不表示实际传入值,只用于区分。 explicit能避免意外隐式转换。
练习题
- 为
Rational重载+并约分。- 答案提示:通分后相加,分母相乘,再调用约分。
- 说明成员函数和友元函数重载二元运算符时形参个数差异。
- 答案提示:成员函数隐含
this,显式参数少一个。
- 答案提示:成员函数隐含
- 为
Rational重载<<。- 答案提示:返回
ostream&。
- 答案提示:返回
- 含动态数组的类为什么要重载
operator=?- 答案提示:默认赋值浅拷贝指针,可能重复释放。
- 写出前置和后置
++的函数头。- 答案提示:
T& operator++()与T operator++(int)。
- 答案提示:
第 12 章 组合与继承
12.1 组合
- 组合:用已有类的对象作为新类的数据成员。
- 组合表示 has-a 关系,例如复数 has-a 实部和虚部。
- 对象成员在构造函数体执行前先构造。
- 初始化顺序按成员定义顺序,不按初始化列表书写顺序。
class Complex {
private:
Rational real;
Rational image;
double modulus;
public:
Complex(int r1, int r2, int i1, int i2)
: real(r1, r2), image(i1, i2)
{
double a = real.getNum() * 1.0 / real.getDen();
double b = image.getNum() * 1.0 / image.getDen();
modulus = sqrt(a*a + b*b);
}
};
- 如果对象成员没有默认构造函数,必须在初始化列表中初始化。
12.2 继承
- 继承:在已有类基础上定义新类。
- 基类/父类:已有类。
- 派生类/子类:继承得到的新类。
- 继承表示 is-a 关系,例如桃树 is-a 果树。
- 派生类继承基类的数据成员和普通成员函数,并可增加或重定义功能。
class point_3d : public point_2d {
private:
int z;
public:
void setpoint3(int a, int b, int c) {
setpoint2(a, b);
z = c;
}
};
- 派生类不能直接访问基类
private成员,只能通过基类公有或保护接口。 - 继承方式影响访问属性:
public继承:基类 public/protected 在派生类中保持 public/protected。protected继承:基类 public/protected 在派生类中都变 protected。private继承:基类 public/protected 在派生类中都变 private。
- 派生类对象构造:先基类,再对象成员,最后派生类构造函数体。
- 析构顺序相反:先派生类析构函数体,再对象成员,再基类。
- 派生类对象可自动转换为基类对象,但会发生对象切片:只保留基类部分。
- 基类指针或引用可以指向派生类对象,但只能直接访问基类接口。
12.3 运行时的多态性
- 多态:不同对象收到相同消息时表现出不同动作。
- 静态联编:编译时决定调用哪个函数,例如函数重载、运算符重载。
- 动态联编:运行时根据实际对象类型决定调用哪个函数。
- C++ 通过虚函数和基类指针/引用实现运行时多态。
class Shape {
public:
virtual void printShapeName() { cout << "Shape" << endl; }
};
- 派生类重定义虚函数时,函数原型必须一致。
- 基类析构函数通常应声明为虚函数:
virtual ~Shape() {}
- 若通过基类指针删除派生类对象,而基类析构函数不是虚函数,可能只调用基类析构,造成资源泄漏。
12.4 纯虚函数与抽象类
- 纯虚函数:没有具体实现的虚函数。
virtual double area() const = 0;
- 含至少一个纯虚函数的类是抽象类。
- 抽象类不能创建对象,通常用作统一接口。
- 派生类如果实现了所有纯虚函数,就可以实例化;否则仍是抽象类。
易错点
- 初始化列表书写顺序不决定成员构造顺序,定义顺序才决定。
- 派生类不继承基类构造函数、析构函数和赋值运算符,但会调用它们。
- 重定义函数和重载函数不同:原型相同是覆盖/重定义,参数不同是隐藏或重载问题。
- 没有
virtual时,用基类指针调用函数会按静态类型绑定。 - 多态场景下基类析构函数应为虚函数。
练习题
- 定义
Complex,包含两个Rational对象成员,说明初始化列表作用。- 答案提示:对象成员先构造,没有默认构造函数时必须初始化。
- 说明派生类对象构造和析构顺序。
- 答案提示:构造先基类、成员、派生类;析构反序。
- 比较
public/protected/private继承对访问权限的影响。- 答案提示:public 保持,protected 降为 protected,private 降为 private。
- 写基类
Shape和派生类Circle、Rectangle,用虚函数求面积。- 答案提示:通过
Shape*调用area()。
- 答案提示:通过
- 什么是抽象类?能否实例化?
- 答案提示:含纯虚函数的类,不能直接创建对象。
第 14 章 输入输出与文件
输入输出与流
- 输入输出是程序和外部设备交换信息。
- C++ 把 I/O 看成字节流。
- 输入流:外设到内存。
- 输出流:内存到外设。
- C++ I/O 由标准库提供,不是语言核心语法。
- I/O 类型:控制台 I/O、文件 I/O、字符串 I/O。
- 低级 I/O 直接处理字节;高级 I/O 处理整数、浮点数、字符、字符串、对象等数据单元。
- 无格式 I/O 速度快;格式化 I/O 会按类型转换和解释,开销更大。
标准库和缓冲
- 常见头文件和类型:
<iostream>:istream、ostream、iostream。<fstream>:ifstream、ofstream、fstream。<sstream>:istringstream、ostringstream、stringstream。
- 标准流对象:
cin:标准输入。cout:标准输出。cerr:标准错误,无缓冲。clog:标准错误,有缓冲。
- 输出缓冲刷新情况:程序正常结束、缓冲区满、使用
endl、设置unitbuf、读关联输入流前。
控制台输出
<<输出标准类型数据,返回输出流引用。- 对字符指针,
cout默认输出其指向的字符串;若要输出地址,可转为void*。 put输出单个字符:
cout.put('A').put('\n');
write无格式输出指定字节数:
char buffer[] = "HAPPY BIRTHDAY";
cout.write(buffer, 10);
控制台输入
>>以空白字符分隔数据,不读取空白本身。while (cin >> grade)可连续读取直到输入结束或失败。get可读取字符,包括空白字符。getline可读取一整行,并丢弃行结束符。read无格式读取指定字节数,gcount可获取实际读取字节数。- 输入失败后要处理流状态:
cin.clear();
cin.ignore(10000, '\n');
格式化输入输出
- 需要
<iomanip>。 - 整数进制:
hex、oct、dec、setbase(16/10/8)。 - 浮点精度:
setprecision(n)或precision(n)。 - 宽度:
setw(n)或width(n),只影响下一次输入/输出。 - 对齐和填充:
left、right、setfill(ch)。 - 常见固定小数输出:
cout << fixed << setprecision(3) << x << endl;
- 可自定义流操纵符:
ostream& tab(ostream &os) { return os << '\t'; }
文件和流
- 文件被看作有序字节流,以文件结束标记结束。
- 文件访问过程:定义流对象、打开文件、访问文件、关闭文件。
ifstream infile("input.txt");
ofstream outfile("output.txt");
- 打开失败会设置
failbit,应检查:
if (!infile) {
cerr << "open file error" << endl;
}
- 文件模式:
ios::in:读。ios::out:写。ios::app:追加。ios::binary:二进制。ios::trunc:截断。
- 读文件结束判断推荐直接检查读取表达式:
int x;
while (infile >> x) {
// use x
}
不要写成先 while (!infile.eof()) 再读,因为最后一次读取可能已经失败。
文件定位与随机读写
- 读指针:
get pointer,相关函数tellg、seekg。 - 写指针:
put pointer,相关函数tellp、seekp。 - 固定长度记录可以用
read/write随机访问。
file.seekg((recordNo - 1) * sizeof(Book));
file.read(reinterpret_cast<char*>(&book), sizeof(Book));
- 二进制读写对象时,要注意对象中不能含有裸指针指向外部动态内存,否则写入的只是地址值。
图书馆系统案例
- 用二进制文件保存固定长度
book记录。 - 每条记录包含馆藏号、书名、借书标记。
- 功能模块:初始化系统、添加书、借书、还书、显示书目。
- 书号自动生成可用类的静态成员保存最大馆藏号。
- 借还书时根据馆藏号计算记录位置,用随机读写修改对应记录。
字符串流
<sstream>支持内存中的输入输出。istringstream:从字符串读。ostringstream:向字符串写。stringstream:读写字符串。
string line = "Tom 90 85 88";
stringstream ss(line);
string name;
int a, b, c;
ss >> name >> a >> b >> c;
易错点
endl会刷新缓冲,频繁使用可能降低效率;只换行可用\n。setw只影响下一次输出,不是永久设置。setprecision默认设置有效数字;配合fixed时表示小数位数。while (!fin.eof())是常见错误,应使用while (fin >> x)。- 文本文件和二进制文件处理方式不同,不能混淆。
reinterpret_cast<char*>常用于二进制读写,但要理解它只是按字节处理内存。
练习题
- 输出
double x=3.1415926,宽度 10,保留 3 位小数。- 答案提示:
cout << setw(10) << fixed << setprecision(3) << x;。
- 答案提示:
- 说明输入失败后为什么要
clear和ignore。- 答案提示:
clear清状态,ignore丢弃缓冲区错误内容。
- 答案提示:
- 写程序复制
input.txt到output.txt。- 答案提示:用
ifstream和ofstream,可按字符或行复制。
- 答案提示:用
- 从文件读取整数,统计个数、总和和平均值。
- 答案提示:
while (fin >> x)循环统计。
- 答案提示:
- 用
stringstream解析字符串"Tom 90 85 88"。- 答案提示:
ss >> name >> a >> b >> c。
- 答案提示:
- 为
Student重载operator<<。- 答案提示:函数头
ostream& operator<<(ostream& os, const Student& s),最后返回os。
- 答案提示:函数头