C++ 程序设计复习资料

按老师 PPT 顺序整理为可读笔记,保留概念、规则、代码模板、易错点和练习题。适合考前快速过知识网,也适合逐章查漏补缺。

没有匹配的章节。换一个关键词试试。

C++ 程序设计复习资料(按 PPT 顺序精读版)

这份资料按 cpp 文件夹中老师课件的章节和小节顺序重写。它不是逐页转录稿,而是把 PPT 中的概念、规则、代码例子、课堂易错点整理成可读的复习笔记。每章末尾附有对应 C++ 练习题和答案提示。

使用建议:先按章节读“核心笔记”,再看“易错点”,最后做练习题。遇到不熟的语法,建议在本地写一个小程序跑一遍。

第 1 章 绪论

1.1 程序设计概述

  • 程序设计的目标:设计出让计算机完成某个任务的程序。
  • 计算机科学关注的核心问题不是“计算机本身”,而是:哪些问题可以计算、如何计算、计算代价有多大。
  • 计算:利用计算机执行程序来解决问题。
  • 程序:用程序设计语言描述的、可以被计算机执行的指令序列。
  • 算法:解决问题的步骤。程序是算法的具体实现。
  • 学程序设计不是只学语法,而是训练把问题分解、抽象、表达和验证的能力。

1.2 计算机组成

  • 计算机系统通常由硬件和软件组成。
  • 硬件层面需要理解:CPU、内存、外部存储、输入设备、输出设备。
  • CPU 负责执行指令;内存保存当前正在运行的程序和数据;外部存储保存长期数据。
  • 程序运行时,数据和指令都要以二进制形式存入内存。
  • 内存可以看作连续编号的存储单元,每个单元有地址。后续指针章节会直接使用“地址”这个概念。
  • 数据表示要点:整数、字符、浮点数最终都以二进制保存,类型决定如何解释这些二进制位。

1.3 程序设计语言

  • 机器语言:计算机能直接执行,但可读性差。
  • 汇编语言:用助记符表示机器指令,仍接近硬件。
  • 高级语言:更接近人类表达,需要编译或解释后才能执行。
  • C++ 是编译型语言。一般过程是:源代码 .cpp -> 编译 -> 目标文件 -> 链接 -> 可执行文件。
  • 编译器能发现语法错误和一部分类型错误;运行错误和逻辑错误需要测试和调试。

1.4 程序设计过程

  • 问题分析:明确输入、输出、约束和边界情况。
  • 算法设计:先想清楚步骤,再写代码。
  • 编码实现:用 C++ 把算法翻译成程序。
  • 编译链接:检查语法、类型、库调用和链接问题。
  • 测试调试:用正常数据、边界数据、异常数据验证。
  • 维护改进:规则变化时,应尽量少改代码,这也是后续函数、结构体、类、模块化的目的。

易错点

  • 把“会写语句”误认为“会程序设计”。真正的重点是问题分解和边界处理。
  • 忽略数据表示。比如整数除法、浮点误差、字符编码,都会影响程序结果。
  • 写代码前不明确输入输出,容易导致程序结构混乱。

练习题

  1. 说明程序、算法、计算三者的关系。
    • 答案提示:算法是步骤,程序是算法的语言实现,计算是执行程序得到结果的过程。
  2. 用自然语言写出求两个正整数最大公约数的算法。
    • 答案提示:可用欧几里得算法,循环执行 a%b,直到余数为 0。
  3. 将十进制 42 转为二进制;将二进制 101101 转为十进制。
    • 答案提示:42 = 101010₂101101₂ = 45
  4. 按程序设计过程说明如何写“输入成绩,输出是否及格”的程序。
    • 答案提示:明确输入成绩,输出及格/不及格;算法是判断 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:逻辑值,truefalse
  • 常量定义:
const double PI = 3.1415926;
  • 相比 #define PI 3.14const 有类型,编译器能检查,更符合 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 / 54 / 5.0
  • 赋值运算:=,右结合,可链式赋值:
d = i = 1.5;  // i 得到 1,d 得到 1.0
  • 复合赋值:+= -= *= /= %=
  • 自增自减:++ii++--ii--
    • 前置:先改变再使用。
    • 后置:先使用再改变。
  • 优先级大致顺序:括号最高,然后算术,再关系,再逻辑,再赋值。

易错点

  • = 是赋值,== 才是相等判断。
  • 4/5 是整数除法,结果不是 0.8
  • 宏替换没有类型检查,也不自动加括号,复杂宏容易出错。
  • using namespace std; 方便入门,但大型项目中更推荐显式写 std::

练习题

  1. 输入圆半径,输出面积和周长,要求使用 const double PI
    • 答案提示:area = PI*r*rperimeter = 2*PI*r
  2. int a=5,b=2;,求 a/ba%ba*1.0/b
    • 答案提示:分别为 212.5
  3. 解释 #include <iostream>using namespace std;return 0; 的作用。
    • 答案提示:引入库接口、使用标准名字空间、表示程序正常结束。
  4. #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 表示区间。
  • switchcase 不能写范围,例如 case score >= 90 不合法。
  • 嵌套分支必须用缩进和 {} 保持结构清楚。

练习题

  1. 求值:int a=2,b=4,c=6,d=0,x; x = a + b > c == ++d; 执行后 xd 是多少?
    • 答案提示:a+b 为 6,6>6 为 0,++d 为 1,0==1 为 0,所以 x=0,d=1
  2. x=20 时,0 < x < 10 的值是什么?
    • 答案提示:为真,因为按 (0<x)<10 计算。
  3. 输入百分制成绩,输出等级,处理非法输入。
    • 答案提示:先判断 <0>100,再用 if-else if 分段。
  4. 输入一元二次方程系数,按判别式输出根的情况。
    • 答案提示:注意 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
  • 枚举时应尽量缩小范围,避免无意义循环。

练习题

  1. forwhiledo-while 分别计算 1+2+...+n
    • 答案提示:三种写法都要处理 n<=0 的情况。
  2. 判断一个整数是否为素数。
    • 答案提示:从 2 枚举到 sqrt(n),发现整除则不是素数。
  3. 输出九九乘法表。
    • 答案提示:外层控制行,内层控制列。
  4. 用 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
  • 对字符数组不能直接用 == 比较字符串内容,应使用字符串处理函数或逐字符比较。

练习题

  1. 输入 10 个整数,输出最大值、最小值和平均值。
    • 答案提示:初始化最大最小值为第一个元素,然后遍历更新。
  2. 在数组中顺序查找某整数,存在输出下标,否则输出 -1
    • 答案提示:找到后立即返回或记录位置。
  3. 写选择排序或冒泡排序,将数组升序排列。
    • 答案提示:选择排序每轮确定一个最小值位置。
  4. 输入 3x3 矩阵,输出主对角线和。
    • 答案提示:累加 a[i][i]
  5. 不用 strlen,计算字符数组中字符串长度。
    • 答案提示:从 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
  • 递归一定要先确认出口和规模缩小。
  • 重载不是“返回类型不同”。

练习题

  1. 写函数 int max3(int a,int b,int c)
    • 答案提示:先求两个数最大值,再与第三个比较。
  2. void swap(int &a,int &b) 并解释引用的作用。
    • 答案提示:引用形参绑定实参,函数内修改会影响调用者。
  3. double average(const int a[], int n)
    • 答案提示:数组传地址,长度必须由 n 给出。
  4. 写递归函数计算 n!
    • 答案提示:出口 n<=1
  5. 写一个函数模板返回两个值中较大者。
    • 答案提示: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++ 中不应修改字面量。
  • 数组名不是普通指针变量,不能给数组名赋新地址。

练习题

  1. 定义 int x=10; int *p=&x;,说明 p*p&p 的含义。
    • 答案提示:地址、指向对象的值、指针变量自身地址。
  2. 动态申请 n 个整数,输入后求和并释放。
    • 答案提示:new int[n]delete[] 配对。
  3. 写函数 void setZero(int *p)
    • 答案提示:先判断 p != nullptr
  4. 说明 char s[]="abc"const char *p="abc" 的区别。
    • 答案提示:前者是数组副本,后者指向字面量。
  5. 定义一个指向 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).agep->age 等价。
  • 结构体不能直接包含自身对象成员,否则大小无限递归;可以包含自身指针。
  • 链表插入删除一定要先保存必要指针,否则会断链或丢失节点。
  • 动态节点删除后不能继续访问。

练习题

  1. 定义 Student,包含学号、姓名、三门成绩和平均分。
    • 答案提示:用 struct 声明多个成员。
  2. 写函数 void calcAverage(Student &s)
    • 答案提示:引用传递可修改原结构体。
  3. 输入学生数组,输出平均分最高学生。
    • 答案提示:遍历记录最大下标。
  4. 判断 stu.agep->age(*p).ageStudent.age 哪个非法。
    • 答案提示:Student.age 非法。
  5. 写单链表遍历代码。
    • 答案提示:从 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 一起链接,会出现未定义引用。
  • 接口设计过于依赖全局变量,会破坏模块独立性。

练习题

  1. 把学生成绩管理拆分为输入、计算、排序、输出四个模块,写出函数接口。
    • 答案提示:如 readStudentscalcAvgsortByAvgprintStudents
  2. 说明 student.hstudent.cpp 分别应放什么。
    • 答案提示:头文件放声明,源文件放实现。
  3. 写一个头文件保护宏。
    • 答案提示:#ifndef#define#endif
  4. main.cpp 使用 student.cpp 函数时,为什么只包含 .h 还不够?
    • 答案提示:包含头文件只让编译器知道声明,链接还需要实现文件。

第 10 章 创建功能更强的类型

10.1 面向对象程序设计

  • 面向过程:数据和操作分离,用函数一步步解决问题。
  • 面向对象:程序由相互协作的对象组成,对象封装数据和操作。
  • 类:具有相同属性和行为的事物抽象。
  • 对象:类的具体实例。
  • 面向对象三大特点:封装、继承、多态。

10.2 类的定义

class 类名 {
private:
    私有数据成员和成员函数;
public:
    公有数据成员和成员函数;
};
  • private 成员只能被类内成员函数或友元访问。
  • public 成员构成类的外部接口。
  • class 默认访问权限是 privatestruct 默认是 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

练习题

  1. Student 结构体改成类,数据成员私有,提供 set/get/print
    • 答案提示:外部不能直接访问私有成员,要通过公有接口。
  2. 写类 Trace,构造和析构时输出信息,观察局部对象的顺序。
    • 答案提示:构造按定义顺序,析构反序。
  3. 实现 MyString:动态字符数组、构造、拷贝构造、setStringprintString、析构。
    • 答案提示:拷贝构造中重新申请空间并复制内容。
  4. 说明 DoubleArray arr1(5,10), arr2(arr1); arr2 = foo(arr2); 中会调用哪些函数。
    • 答案提示:普通构造、拷贝构造、按值传参拷贝、按值返回拷贝、赋值运算符、析构。
  5. 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 能避免意外隐式转换。

练习题

  1. Rational 重载 + 并约分。
    • 答案提示:通分后相加,分母相乘,再调用约分。
  2. 说明成员函数和友元函数重载二元运算符时形参个数差异。
    • 答案提示:成员函数隐含 this,显式参数少一个。
  3. Rational 重载 <<
    • 答案提示:返回 ostream&
  4. 含动态数组的类为什么要重载 operator=
    • 答案提示:默认赋值浅拷贝指针,可能重复释放。
  5. 写出前置和后置 ++ 的函数头。
    • 答案提示: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 时,用基类指针调用函数会按静态类型绑定。
  • 多态场景下基类析构函数应为虚函数。

练习题

  1. 定义 Complex,包含两个 Rational 对象成员,说明初始化列表作用。
    • 答案提示:对象成员先构造,没有默认构造函数时必须初始化。
  2. 说明派生类对象构造和析构顺序。
    • 答案提示:构造先基类、成员、派生类;析构反序。
  3. 比较 public/protected/private 继承对访问权限的影响。
    • 答案提示:public 保持,protected 降为 protected,private 降为 private。
  4. 写基类 Shape 和派生类 CircleRectangle,用虚函数求面积。
    • 答案提示:通过 Shape* 调用 area()
  5. 什么是抽象类?能否实例化?
    • 答案提示:含纯虚函数的类,不能直接创建对象。

第 14 章 输入输出与文件

输入输出与流

  • 输入输出是程序和外部设备交换信息。
  • C++ 把 I/O 看成字节流。
  • 输入流:外设到内存。
  • 输出流:内存到外设。
  • C++ I/O 由标准库提供,不是语言核心语法。
  • I/O 类型:控制台 I/O、文件 I/O、字符串 I/O。
  • 低级 I/O 直接处理字节;高级 I/O 处理整数、浮点数、字符、字符串、对象等数据单元。
  • 无格式 I/O 速度快;格式化 I/O 会按类型转换和解释,开销更大。

标准库和缓冲

  • 常见头文件和类型:
    • <iostream>istreamostreamiostream
    • <fstream>ifstreamofstreamfstream
    • <sstream>istringstreamostringstreamstringstream
  • 标准流对象:
    • 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>
  • 整数进制:hexoctdecsetbase(16/10/8)
  • 浮点精度:setprecision(n)precision(n)
  • 宽度:setw(n)width(n),只影响下一次输入/输出。
  • 对齐和填充:leftrightsetfill(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,相关函数 tellgseekg
  • 写指针:put pointer,相关函数 tellpseekp
  • 固定长度记录可以用 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*> 常用于二进制读写,但要理解它只是按字节处理内存。

练习题

  1. 输出 double x=3.1415926,宽度 10,保留 3 位小数。
    • 答案提示:cout << setw(10) << fixed << setprecision(3) << x;
  2. 说明输入失败后为什么要 clearignore
    • 答案提示:clear 清状态,ignore 丢弃缓冲区错误内容。
  3. 写程序复制 input.txtoutput.txt
    • 答案提示:用 ifstreamofstream,可按字符或行复制。
  4. 从文件读取整数,统计个数、总和和平均值。
    • 答案提示:while (fin >> x) 循环统计。
  5. stringstream 解析字符串 "Tom 90 85 88"
    • 答案提示:ss >> name >> a >> b >> c
  6. Student 重载 operator<<
    • 答案提示:函数头 ostream& operator<<(ostream& os, const Student& s),最后返回 os