第一篇:C++ 基础入门
本篇是整个 C++ 学习旅程的起点,适合完全没有编程基础的初学者。我们将从 C++ 语言的历史背景和开发环境搭建开始,逐步介绍基本数据类型、变量与常量、运算符、流程控制语句、数组与函数等核心概念,并初步接触面向对象编程的思想。每一节都配有详细的代码示例和学习建议,帮助你在实践中理解理论知识。学完本篇后,你将具备编写简单 C++ 程序的能力,为后续深入学习打下坚实基础。
1.1 C++ 概述与环境搭建
C++ 语言简介与 Visual Studio 开发环境配置
知识点概述
C++ 是由 Bjarne Stroustrup 于 1979 年在贝尔实验室开始设计的一种通用编程语言,它是 C 语言的扩展和升级版本。C++ 保留了 C 语言高效、灵活的特点,同时引入了面向对象编程(OOP)的机制,包括类、继承、多态和封装等特性。经过四十多年的发展,C++ 已成为系统编程、游戏开发、嵌入式系统和高性能计算等领域最重要的编程语言之一。
学习 C++ 的第一步是搭建开发环境。Visual Studio(简称 VS)是微软公司推出的集成开发环境(IDE),它提供了代码编辑、编译、调试和项目管理等一站式功能,是 Windows 平台上最主流的 C++ 开发工具。本节将详细讲解如何下载、安装和配置 Visual Studio,以及如何创建你的第一个 C++ 项目。
在安装过程中,需要选择「使用 C++ 的桌面开发」工作负载,这会自动安装 C++ 编译器(MSVC)、调试器、Windows SDK 等必要组件。安装完成后,打开 Visual Studio,选择「创建新项目」,在模板列表中选择「空项目」,设置项目名称和存储路径,即可开始你的 C++ 编程之旅。
核心概念
C++ 标准版本演进
C++ 的发展历史经历了多个标准版本:C++98(第一个国际标准)、C++03(小幅修订)、C++11(重大更新,引入 auto、lambda、智能指针等)、C++14、C++17、C++20(引入概念、协程、模块等)和 C++23。每个新标准都在不断完善语言特性,使其更加强大和易用。
编译过程
编译器是将源代码(.cpp 文件)翻译为机器可执行文件的程序。常见的 C++ 编译器包括 MSVC(Windows)、GCC(跨平台)和 Clang(跨平台)。在 Visual Studio 中,默认使用 MSVC 编译器。编译过程分为预处理、编译和链接三个阶段:
- 预处理:处理
#include(头文件包含)和#define(宏定义)等预处理指令,生成扩展后的源代码。 - 编译:将预处理后的源代码翻译为目标文件(
.obj),进行语法检查和优化。 - 链接:将目标文件与库文件合并,生成最终的可执行文件(
.exe)。
开发环境的核心组件
开发环境的核心组件包括:
- 编辑器(Editor):编写代码
- 编译器(Compiler):翻译代码
- 调试器(Debugger):查找错误
- 构建系统(Build System):管理项目
Visual Studio 将这些组件整合在一起,极大地提高了开发效率。
第一个 C++ 程序
下面是经典的 “Hello, World!” 程序,也是学习任何编程语言的第一步:
// Hello.cpp —— 第一个 C++ 程序:在屏幕上输出 "Hello, C++!"#include <iostream> // 引入输入输出流头文件using namespace std; // 使用标准命名空间
int main() { // main 函数是程序的入口 cout << "Hello, C++!" << endl; // 向屏幕输出字符串,endl 表示换行 return 0; // 返回 0 表示程序正常结束}代码讲解
| 行号 | 代码 | 详细解释 |
|---|---|---|
| 1 | #include <iostream> | 这是预处理指令,告诉编译器在编译前将 iostream 头文件的内容包含到当前文件中。iostream 全称 “input/output stream”(输入输出流),提供了 cin(输入)和 cout(输出)等对象。注意:预处理指令不以分号结尾。 |
| 2 | using namespace std; | C++ 标准库中的所有名称(如 cout、endl、cin)都定义在 std(standard)命名空间中。这行代码表示”使用标准命名空间”,这样我们就可以直接写 cout 而不用写 std::cout。 |
| 3 | int main() { | main 是每个 C++ 程序的入口函数,程序从 main 函数的第一行开始执行。int 表示该函数返回一个整数值。() 中是参数列表,这里为空。{ 标记函数体的开始。 |
| 4 | cout << "Hello, C++!" << endl; | cout 是 “character output” 的缩写,是标准输出流对象。<< 是流插入运算符,将右侧的内容发送到输出流。endl 是 “end line” 的缩写,输出一个换行符并刷新缓冲区。 |
| 5 | return 0; | return 语句结束函数执行并将值返回给调用者。返回 0 是编程惯例,表示程序正常退出。非零值通常表示出错。 |
| 6 | } | 标记 main 函数体的结束。 |
关键语法点总结:
- C++ 中每条语句以分号
;结尾 - 字符串用双引号
" "包裹 - 注释用
//表示单行注释,用/* */表示多行注释 - C++ 程序的执行总是从
main()函数开始
C++ 程序的基本结构语法
// C++ 程序的基本结构语法#include <头文件名> // 预处理指令:包含头文件
using namespace 命名空间名; // 使用命名空间(可选但常用)
int main() { // 主函数:程序入口 // 在这里编写程序代码 return 0; // 返回 0 表示正常退出}重点难点
- 理解编译过程:预处理(处理
#include和#define)→ 编译(将源代码转为目标文件)→ 链接(将目标文件合并为可执行文件)。 - 区分源代码文件(
.cpp)、头文件(.h)、目标文件(.obj)和可执行文件(.exe)的作用。
学习建议
- 动手实践是最好的学习方式。不要只看教程,务必在自己的开发环境中输入代码、编译和运行。
- 如果 Visual Studio 安装后找不到 C++ 相关选项,请确认安装时勾选了「使用 C++ 的桌面开发」工作负载。
- 初学者可以先忽略复杂的工程配置,专注于编写和运行简单的单文件程序。
课后练习
练习 1:修改输出内容
编写一个 C++ 程序,在屏幕上输出以下三行内容:
Hello, C++!我正在学习 C++ 编程!加油!参考答案
// 练习1:输出多行文本#include <iostream>using namespace std;
int main() { // 使用多条 cout 语句分别输出每一行 cout << "Hello, C++!" << endl; cout << "我正在学习 C++ 编程!" << endl; cout << "加油!" << endl; return 0;}练习 2:理解预处理指令
以下代码能否正确编译?如果能,输出什么?如果不能,说明原因。
// 缺少 #include <iostream> 的程序using namespace std;
int main() { cout << "Hello" << endl; return 0;}参考答案
该代码不能正确编译。因为程序使用了 cout 和 endl,它们定义在 <iostream> 头文件中。如果没有 #include <iostream> 这行预处理指令,编译器就不知道 cout 和 endl 是什么,会报出”未声明的标识符”(undeclared identifier)错误。解决方法是在文件开头添加 #include <iostream>。
1.2 基础语法
基本数据类型、变量与常量、运算符
知识点概述
C++ 是一种强类型语言,这意味着每个变量在使用前都必须明确声明其数据类型。C++ 提供了丰富的基本数据类型,包括整型(int)、浮点型(float、double)、字符型(char)和布尔型(bool)等。
变量是程序中用于存储数据的”容器”,每个变量都有类型、名称和值。常量则是程序运行过程中固定不变的值,一旦定义就不能被修改。
运算符是用于执行各种运算的符号,包括算术运算符、关系运算符和逻辑运算符等。理解运算符的优先级对于编写正确的表达式至关重要。
核心概念
基本数据类型
C++ 的基本数据类型及其大小和范围如下:
| 类型 | 大小(字节) | 说明 | 典型范围 |
|---|---|---|---|
short | 2 | 短整型 | -32768 ~ 32767 |
int | 4 | 整型 | 约 ±21 亿 |
long | 4 或 8 | 长整型 | 取决于平台 |
long long | 8 | 更长整型 | 约 ±9.2 × 10¹⁸ |
float | 4 | 单精度浮点型 | 约 7 位有效数字 |
double | 8 | 双精度浮点型 | 约 15 位有效数字 |
char | 1 | 字符型 | -128 ~ 127 或 0 ~ 255 |
bool | 1 | 布尔型 | true 或 false |
变量声明与初始化
// 变量声明的语法格式数据类型 变量名 = 初始值;
// 也可以先声明,再赋值数据类型 变量名;变量名 = 初始值;
// C++11 支持的列表初始化(推荐)数据类型 变量名{初始值};常量定义
// 方式一:使用 const 关键字(推荐)const 数据类型 常量名 = 值;
// 方式二:使用 #define 宏定义(C 风格,不推荐)#define 宏名 值注意:
const定义的常量有类型检查,更加安全;#define只是简单的文本替换,没有类型检查。
运算符优先级
运算符优先级从高到低排列:
- 乘、除、取模:
*/% - 加、减:
+- - 关系运算符:
><>=<===!= - 逻辑运算符:
!(非)&&(与)||(或)
变量、常量与运算符示例
// 基础语法示例 —— 演示变量声明、常量定义和运算符使用#include <iostream>using namespace std;
int main() { // ========== 整数除法陷阱 ========== int a = 10, b = 3; // 直接用整数相除,结果仍为整数(小数部分被截断) cout << "整数除法: " << a / b << endl; // 输出 3(不是 3.333...)
// 使用强制类型转换为 double,得到精确结果 double c = (double)a / b; cout << "浮点除法: " << c << endl; // 输出 3.33333
// ========== 常量定义 ========== const double PI = 3.14159265; // const 常量,不可修改 // PI = 3.14; // 错误!const 常量不能被重新赋值 cout << "PI = " << PI << endl;
// ========== 字符型 ========== char ch = 'A'; // 字符用单引号包裹 cout << "字符: " << ch << endl; // 输出 A cout << "ASCII值: " << (int)ch << endl; // 输出 65(A 的 ASCII 码)
// ========== 布尔型 ========== bool flag = (a > b); // 比较运算的结果是布尔值 cout << "10 > 3: " << flag << endl; // 输出 1(true 显示为 1)
// ========== 运算符优先级示例 ========== int result = 2 + 3 * 4; // 先乘后加,结果为 14 cout << "2 + 3 * 4 = " << result << endl;
result = (2 + 3) * 4; // 括号改变优先级,结果为 20 cout << "(2 + 3) * 4 = " << result << endl;
return 0;}代码讲解
整数除法陷阱:当两个整数相除时,C++ 会执行整数除法,结果的小数部分直接被截断(不是四舍五入)。例如 10 / 3 的结果是 3 而非 3.333...。如果需要得到浮点结果,必须将至少一个操作数转换为浮点类型。(double)a / b 将 a 强制转换为 double,此时 / 执行浮点除法,b 也会被自动提升为 double,最终结果为 3.33333。
常量定义:const double PI = 3.14159265; 声明了一个名为 PI 的 double 类型常量,初始化为 3.14159265。const 关键字告诉编译器这个值不可修改,如果后续尝试对 PI 赋值,编译器会报错。
字符型:char 类型用于存储单个字符,字符用单引号包裹(如 'A'),而字符串用双引号包裹(如 "Hello")。字符在底层实际存储的是其 ASCII 码值,'A' 对应 ASCII 码 65。通过 (int)ch 可以查看字符的 ASCII 值。
布尔型:bool 类型只有两个值:true 和 false。比较表达式(如 a > b)的结果就是布尔值。在输出时,true 显示为 1,false 显示为 0。
运算符优先级:*(乘法)的优先级高于 +(加法),所以 2 + 3 * 4 等价于 2 + (3 * 4) = 14。使用括号 () 可以改变运算顺序,(2 + 3) * 4 = 20。
数据类型完整示例
// 数据类型完整示例 —— 展示各种基本数据类型的声明和使用#include <iostream>using namespace std;
int main() { // 整型 short s = 100; int i = 100000; long l = 100000L; long long ll = 10000000000LL;
// 浮点型 float f = 3.14f; // f 后缀表示 float 类型 double d = 3.1415926535;
// 字符型 char letter = 'Z';
// 布尔型 bool isTrue = true; bool isFalse = false;
// 使用 sizeof 运算符查看各类型占用的字节数 cout << "short: " << sizeof(s) << " 字节" << endl; cout << "int: " << sizeof(i) << " 字节" << endl; cout << "long: " << sizeof(l) << " 字节" << endl; cout << "long long: " << sizeof(ll) << " 字节" << endl; cout << "float: " << sizeof(f) << " 字节" << endl; cout << "double: " << sizeof(d) << " 字节" << endl; cout << "char: " << sizeof(letter) << " 字节" << endl; cout << "bool: " << sizeof(isTrue) << " 字节" << endl;
return 0;}代码讲解
sizeof是一个运算符(不是函数),用于获取某个类型或变量在内存中占用的字节数。不同平台上各类型的大小可能不同,使用sizeof可以编写出平台无关的代码。- 数值字面量的后缀(如
L、LL、f)用于明确指定字面量的类型,避免编译器的类型推断警告。
重点难点
- 整数除法陷阱:两个整数相除结果仍为整数,小数部分被截断。需要浮点结果时,应使用强制类型转换。
- 运算符优先级:乘除取模 > 加减 > 关系运算符 > 逻辑运算符。不确定优先级时,多用括号。
学习建议
- 多写代码尝试不同的数据类型,理解它们的范围和精度差异。
- 养成使用
const定义常量的好习惯,避免使用#define。 - 遇到不确定优先级的表达式时,主动使用括号来明确运算顺序。
课后练习
练习 1:温度转换
编写程序,将摄氏温度 36.5 度转换为华氏温度。转换公式为:F = C × 9 / 5 + 32。使用 const 定义公式中的常量,并输出转换结果。
参考答案
// 练习1:摄氏温度转华氏温度#include <iostream>using namespace std;
int main() { const double celsius = 36.5; // 定义摄氏温度常量 double fahrenheit; // 声明华氏温度变量
// 使用转换公式:F = C × 9 / 5 + 32 fahrenheit = celsius * 9.0 / 5.0 + 32.0;
cout << "摄氏温度: " << celsius << "°C" << endl; cout << "华氏温度: " << fahrenheit << "°F" << endl;
return 0;}// 输出:摄氏温度: 36.5°C 华氏温度: 97.7°F代码讲解:这里使用 9.0 / 5.0 而非 9 / 5 是为了避免整数除法陷阱。9 / 5 的结果为 1,而 9.0 / 5.0 的结果为 1.8,从而得到正确的转换结果。
练习 2:算术运算与类型转换
编写程序,声明两个整数变量 a = 7 和 b = 2,分别计算并输出:
a / b的结果(整数除法)(double)a / b的结果(浮点除法)a % b的结果(取模运算)
参考答案
// 练习2:算术运算与类型转换对比#include <iostream>using namespace std;
int main() { int a = 7, b = 2;
// 整数除法 cout << "a / b = " << a / b << endl; // 输出 3
// 浮点除法(强制类型转换) cout << "(double)a / b = " << (double)a / b << endl; // 输出 3.5
// 取模运算(求余数) cout << "a % b = " << a % b << endl; // 输出 1
return 0;}代码讲解:%(取模运算符)只能用于整数,用于求除法的余数。7 % 2 = 1 表示 7 除以 2 余 1。取模运算在判断奇偶性、循环索引等场景中非常常用。
1.3 流程控制
条件语句与循环语句
知识点概述
流程控制语句是编程中控制程序执行顺序的核心机制。C++ 提供了以下主要的流程控制结构:
- 条件语句:
if-else、switch-case,用于根据条件选择不同的执行路径 - 循环语句:
for、while、do-while,用于重复执行某段代码 - 跳转语句:
break、continue,用于改变循环的正常执行流程
掌握流程控制是编程的基本功,几乎所有程序都离不开条件判断和循环。
核心概念
条件语句的语法格式
// if-else 语句的语法格式if (条件表达式1) { // 条件1为真时执行} else if (条件表达式2) { // 条件2为真时执行} else { // 以上条件都不满足时执行}
// switch-case 语句的语法格式switch (表达式) { case 常量1: // 表达式等于常量1时执行 break; case 常量2: // 表达式等于常量2时执行 break; default: // 以上 case 都不匹配时执行 break;}循环语句的语法格式
// for 循环的语法格式for (初始化; 条件; 更新) { // 循环体}
// while 循环的语法格式while (条件) { // 循环体}
// do-while 循环的语法格式(至少执行一次)do { // 循环体} while (条件);条件语句示例:成绩等级判断
// 条件语句示例 —— 根据分数输出对应的等级#include <iostream>using namespace std;
int main() { int score = 85; // 定义成绩变量
// 使用 if-else if-else 进行多条件判断 if (score >= 90) { cout << "优秀" << endl; } else if (score >= 80) { cout << "良好" << endl; // score = 85,执行这一分支 } else if (score >= 60) { cout << "及格" << endl; } else { cout << "不及格" << endl; }
return 0;}代码讲解
程序使用 if-else if-else 结构对成绩进行多级判断:
if (score >= 90):首先判断成绩是否大于等于 90。如果是,输出”优秀”。else if (score >= 80):如果上一条件不满足(即分数 < 90),再判断是否 >= 80。注意:因为已经排除了 >= 90 的情况,所以这里隐含的条件是 80 <= score < 90。else if (score >= 60):类似地,隐含条件是 60 <= score < 80。else:以上条件都不满足时执行,即 score < 60。
重要提示:else if 的条件判断是从上到下依次进行的,一旦某个条件为真,执行对应的代码块后就跳出整个 if-else 链,不会再检查后面的条件。因此条件的顺序很重要,应该从严格到宽松排列。
循环语句示例:求和
// 循环语句示例 —— 计算 1 到 100 的累加和#include <iostream>using namespace std;
int main() { int sum = 0; // 累加器,必须初始化为 0
// for 循环:i 从 1 递增到 100 for (int i = 1; i <= 100; i++) { sum += i; // 等价于 sum = sum + i }
cout << "Sum = " << sum << endl; // 输出 Sum = 5050
return 0;}代码讲解
int sum = 0;:声明累加器变量sum并初始化为0。务必初始化,否则sum中可能是随机值(垃圾值),导致计算结果错误。for (int i = 1; i <= 100; i++):for循环由三部分组成:- 初始化
int i = 1:循环变量i从 1 开始 - 条件
i <= 100:每次循环前检查,如果i<= 100 则继续循环 - 更新
i++:每次循环体执行完毕后,i自增 1
- 初始化
sum += i;:+=是复合赋值运算符,sum += i等价于sum = sum + i。- 循环执行过程:i=1 时 sum=1,i=2 时 sum=3,…,i=100 时 sum=5050。
switch-case 语句示例
// switch-case 语句示例 —— 根据数字输出星期几#include <iostream>using namespace std;
int main() { int day = 3; // 假设今天是星期三
switch (day) { case 1: cout << "星期一" << endl; break; case 2: cout << "星期二" << endl; break; case 3: cout << "星期三" << endl; break; // break 防止"穿透"到下一个 case case 4: cout << "星期四" << endl; break; case 5: cout << "星期五" << endl; break; case 6: cout << "星期六" << endl; break; case 7: cout << "星期日" << endl; break; default: cout << "无效的日期" << endl; break; }
return 0;}代码讲解
switch (day)将day的值与各case的常量进行比较,匹配时执行对应的代码。- 每个分支的末尾必须有
break;,否则程序会穿透(fall-through)到下一个case继续执行。例如,如果case 3没有break,那么输出”星期三”后还会继续输出”星期四”。 default是可选的,当没有任何case匹配时执行,类似于if-else中的else。switch语句只能判断整型或字符型的表达式,不能用于浮点数或字符串。
while 循环与 break、continue 示例
// while 循环与 break、continue 示例#include <iostream>using namespace std;
int main() { // ===== while 循环:输出 1 到 10 之间的奇数 ===== int num = 1; while (num <= 10) { if (num % 2 == 0) { // 如果是偶数,跳过本次循环的剩余部分 num++; continue; // 直接跳到 while 条件判断 } cout << num << " "; // 只输出奇数 num++; } cout << endl; // 输出:1 3 5 7 9
// ===== break 示例:找到第一个能被 7 整除的数 ===== for (int i = 1; i <= 100; i++) { if (i % 7 == 0) { cout << "第一个能被7整除的数是: " << i << endl; break; // 找到后立即退出循环 } } // 输出:第一个能被7整除的数是: 7
return 0;}代码讲解
continue:跳过当前循环迭代的剩余部分,直接进入下一次迭代。在while循环中使用continue时,要特别注意确保循环变量会更新,否则可能造成死循环。上面的代码中,在continue之前先执行了num++,所以不会死循环。break:立即终止整个循环,跳出循环体。上面的代码在找到第一个能被 7 整除的数(7)后,就通过break跳出了循环,不再检查后续的数。
重点难点
if-else条件判断的顺序很重要,应从严格到宽松排列。for循环中的累加器必须初始化为 0。switch-case中忘记写break会导致”穿透”问题。while循环中使用continue时要注意更新循环变量,避免死循环。
学习建议
- 多画流程图帮助理解条件判断和循环的逻辑。
- 尝试用不同的循环方式(
for、while、do-while)实现同一个功能,加深理解。
课后练习
练习 1:判断闰年
编写程序,判断一个年份是否为闰年。闰年的判断规则:
- 能被 4 整除但不能被 100 整除,或者能被 400 整除的年份是闰年。
请分别测试 2000、2024、1900 这三个年份。
参考答案
// 练习1:判断闰年#include <iostream>using namespace std;
int main() { int years[] = {2000, 2024, 1900};
for (int i = 0; i < 3; i++) { int year = years[i]; // 闰年条件:能被4整除且不能被100整除,或者能被400整除 if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) { cout << year << " 年是闰年" << endl; } else { cout << year << " 年不是闰年" << endl; } }
return 0;}// 输出:// 2000 年是闰年// 2024 年是闰年// 1900 年不是闰年代码讲解:逻辑运算符 &&(与)的优先级高于 ||(或),所以 (year % 4 == 0 && year % 100 != 0) 会先计算。也可以用额外的括号来明确优先级:((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)。
练习 2:九九乘法表
使用嵌套的 for 循环,打印九九乘法表。格式如下:
1×1=11×2=2 2×2=41×3=3 2×3=6 3×3=9...参考答案
// 练习2:打印九九乘法表#include <iostream>#include <iomanip> // 用于格式化输出using namespace std;
int main() { // 外层循环控制行号(第几行) for (int i = 1; i <= 9; i++) { // 内层循环控制每行的列数(第几列) for (int j = 1; j <= i; j++) { cout << j << "×" << i << "=" << setw(2) << i * j << " "; } cout << endl; // 每行结束后换行 }
return 0;}代码讲解:
- 外层循环变量
i从 1 到 9,代表乘法表的行数。 - 内层循环变量
j从 1 到i,确保每行只打印i个乘法式。 setw(2)来自<iomanip>头文件,用于设置输出宽度为 2 个字符,使结果对齐美观。- 嵌套循环是编程中的常见模式,外层循环执行一次,内层循环执行完所有迭代。
1.4 数组与函数
数组、函数定义与调用、引用、命名空间
知识点概述
数组是一种用于存储多个相同类型数据的数据结构。通过数组,我们可以用一个变量名管理一组数据,并通过索引(下标)来访问每个元素。
函数是组织好的、可重复使用的代码块,用于执行特定的任务。使用函数可以提高代码的可读性、可维护性和复用性。函数包括声明(告诉编译器函数的存在)和定义(实现函数的具体功能)。
引用是 C++ 中的重要特性,它是某个已存在变量的别名。通过引用参数,函数可以修改调用者的变量,避免值拷贝带来的开销。
命名空间用于解决命名冲突问题,将代码组织到不同的逻辑区域中。C++ 标准库的名称都定义在 std 命名空间中。
核心概念
数组声明的语法格式
// 数组声明的语法格式数据类型 数组名[数组大小];
// 声明的同时初始化数据类型 数组名[] = {值1, 值2, 值3, ...};
// 指定大小并部分初始化(未初始化的元素默认为 0)数据类型 数组名[大小] = {值1, 值2};
// 访问数组元素(索引从 0 开始)数组名[索引];重要:C++ 数组的索引从 0 开始。对于大小为
n的数组,有效索引范围是0到n-1。
函数声明与定义的语法格式
// 函数声明的语法格式(也叫函数原型)返回类型 函数名(参数类型 参数名, 参数类型 参数名, ...);
// 函数定义的语法格式返回类型 函数名(参数类型 参数名, ...) { // 函数体 return 返回值; // void 类型函数不需要 return}
// 函数调用的语法格式函数名(实际参数);引用声明的语法格式
// 引用声明的语法格式数据类型 &引用名 = 已存在的变量名;
// 引用作为函数参数的语法格式(传引用)返回类型 函数名(数据类型 &参数名) { // 在函数内修改参数会直接修改原始变量}命名空间的语法格式
// 定义命名空间的语法格式namespace 命名空间名 { // 变量、函数、类等的声明和定义}
// 使用命名空间中成员的语法格式命名空间名::成员名;数组示例:查找最大值
// 数组示例 —— 在数组中查找最大值#include <iostream>using namespace std;
int main() { // 声明并初始化一个整型数组 int arr[] = {3, 7, 2, 9, 1, 5, 8};
// 计算数组元素个数:数组总字节数 / 单个元素字节数 int n = sizeof(arr) / sizeof(arr[0]); cout << "数组元素个数: " << n << endl; // 输出 7
// 假设第一个元素是最大值 int maxVal = arr[0];
// 遍历数组,逐个比较寻找最大值 for (int i = 1; i < n; i++) { if (arr[i] > maxVal) { maxVal = arr[i]; // 发现更大的元素,更新最大值 } }
cout << "数组中的最大值是: " << maxVal << endl; // 输出 9
return 0;}代码讲解
int arr[] = {3, 7, 2, 9, 1, 5, 8};:声明一个整型数组并初始化。由于提供了初始化列表,数组的大小由编译器自动推算(此处为 7 个元素)。数组在内存中是连续存储的。sizeof(arr) / sizeof(arr[0]):sizeof(arr)返回整个数组占用的总字节数(7 × 4 = 28 字节),sizeof(arr[0])返回单个元素的字节数(4 字节),两者相除得到元素个数 7。这是获取数组大小的常用技巧。int maxVal = arr[0];:先将最大值初始化为第一个元素(不能用 0,因为数组可能包含负数)。for (int i = 1; i < n; i++):从第二个元素开始遍历(索引 1),因为第一个元素已经作为初始最大值。if (arr[i] > maxVal):如果当前元素大于已知的最大值,则更新maxVal。这是经典的打擂台算法。
函数与引用示例:交换两个变量
// 函数与引用示例 —— 使用引用参数交换两个变量的值#include <iostream>using namespace std;
// 声明一个使用引用参数的函数void swap(int &a, int &b) { int temp = a; // 用临时变量保存 a 的值 a = b; // 将 b 的值赋给 a b = temp; // 将临时变量的值赋给 b}
int main() { int x = 10, y = 20;
cout << "交换前: x = " << x << ", y = " << y << endl;
// 调用 swap 函数,x 和 y 的值会被交换 swap(x, y);
cout << "交换后: x = " << x << ", y = " << y << endl;
return 0;}// 输出:// 交换前: x = 10, y = 20// 交换后: x = 20, y = 10代码讲解
void swap(int &a, int &b):void表示该函数不返回任何值。参数int &a和int &b是引用参数,&符号表示引用。引用参数使得函数内部直接操作调用者传入的原始变量,而非其副本。- 传值 vs 传引用:如果使用普通参数(如
int a),函数内交换的只是副本,原始变量不会改变。使用引用参数(int &a),a成为x的别名,对a的修改就是对x的修改。 int temp = a;:使用临时变量temp保存a的原始值。如果不使用临时变量直接赋值(a = b; b = a;),两个变量最终会变成相同的值(b的值)。- 引用是 C++ 的重要特性,它既能让函数修改外部变量,又能避免大对象复制带来的性能开销。
命名空间示例
// 命名空间示例 —— 自定义命名空间并使用其中的函数#include <iostream>using namespace std;
// 定义自定义命名空间 MyMathnamespace MyMath { // 在命名空间内定义函数 int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }}
int main() { // 使用 "命名空间名::成员名" 的方式调用 int result1 = MyMath::add(3, 5); cout << "3 + 5 = " << result1 << endl; // 输出 8
int result2 = MyMath::subtract(10, 4); cout << "10 - 4 = " << result2 << endl; // 输出 6
// 也可以使用 using 声明,简化调用 using MyMath::add; cout << "7 + 8 = " << add(7, 8) << endl; // 输出 15
return 0;}代码讲解
namespace MyMath { ... }:定义一个名为MyMath的命名空间,将add和subtract函数组织在其中。命名空间类似于文件夹的概念,用于避免不同代码之间的名称冲突。MyMath::add(3, 5):使用作用域解析运算符::来访问命名空间中的成员。MyMath::add明确表示调用的是MyMath命名空间中的add函数。using MyMath::add;:using声明可以将命名空间中的某个成员引入当前作用域,之后就可以直接使用add()而不需要加MyMath::前缀。与之相比,using namespace MyMath;会引入该命名空间中的所有成员。
重点难点
- 数组索引从 0 开始,越界访问会导致未定义行为。
sizeof技巧只能在定义数组的同一作用域中使用,将数组传给函数后会退化为指针,无法再用sizeof获取大小。- 理解传值和传引用的区别是掌握函数参数的关键。
- 命名空间是组织大型代码的基础,
using namespace std;在小型程序中方便,但在大型项目中应避免。
学习建议
- 多练习数组遍历、查找、排序等基本操作。
- 编写函数时,先想清楚函数的输入(参数)和输出(返回值),再实现具体逻辑。
- 尝试不使用引用参数实现交换函数,观察区别。
课后练习
练习 1:数组的反转
编写一个函数 reverseArray,将一个整型数组中的元素顺序反转。在 main 函数中测试该函数,并输出反转前后的数组。
参考答案
// 练习1:数组反转#include <iostream>using namespace std;
// 反转数组的函数// 参数:数组(作为指针传入)、数组大小void reverseArray(int arr[], int n) { int left = 0; // 左指针,从数组开头开始 int right = n - 1; // 右指针,从数组末尾开始
// 左右指针向中间靠拢,交换对应位置的元素 while (left < right) { // 交换 arr[left] 和 arr[right] int temp = arr[left]; arr[left] = arr[right]; arr[right] = temp;
left++; // 左指针右移 right--; // 右指针左移 }}
// 打印数组的辅助函数void printArray(int arr[], int n) { for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl;}
int main() { int arr[] = {1, 2, 3, 4, 5, 6, 7}; int n = sizeof(arr) / sizeof(arr[0]);
cout << "反转前: "; printArray(arr, n);
reverseArray(arr, n);
cout << "反转后: "; printArray(arr, n);
return 0;}// 输出:// 反转前: 1 2 3 4 5 6 7// 反转后: 7 6 5 4 3 2 1代码讲解:采用双指针法,left 指针从数组头部向右移动,right 指针从数组尾部向左移动。每次交换两个指针所指的元素,直到两个指针相遇或交叉。注意数组作为函数参数时会退化为指针,所以需要额外传入数组大小 n。
练习 2:函数重载
编写两个同名的 max 函数:一个用于比较两个整数,另一个用于比较两个浮点数。在 main 函数中分别调用并输出结果。
参考答案
// 练习2:函数重载 —— 同名函数处理不同类型#include <iostream>using namespace std;
// 整数版本的最大值函数int maxVal(int a, int b) { return (a > b) ? a : b; // 使用三目运算符}
// 浮点数版本的最大值函数double maxVal(double a, double b) { return (a > b) ? a : b;}
int main() { // 编译器根据参数类型自动选择匹配的函数 cout << "max(3, 7) = " << maxVal(3, 7) << endl; // 调用 int 版本 cout << "max(3.14, 2.72) = " << maxVal(3.14, 2.72) << endl; // 调用 double 版本
return 0;}// 输出:// max(3, 7) = 7// max(3.14, 2.72) = 3.14代码讲解:两个函数都叫 maxVal,但参数类型不同(一个接受 int,一个接受 double),这就是函数重载(Function Overloading)。编译器根据调用时传入的参数类型自动选择匹配的函数版本。注意不要使用 max 作为函数名,因为 <algorithm> 头文件中已经定义了 std::max,可能会产生命名冲突。
1.5 类与面向对象入门
类的定义、构造函数、成员函数与访问控制
知识点概述
面向对象编程(Object-Oriented Programming,OOP)是 C++ 最重要的特性之一。其核心思想是将数据和操作数据的方法封装在一起,形成”类”(Class)。类是对象的蓝图或模板,而对象是类的具体实例。
本节将介绍类的基本概念,包括如何定义类、声明构造函数、编写成员函数,以及使用 private 和 public 等访问控制修饰符来实现封装。
核心概念
类定义的语法格式
// 类定义的语法格式class 类名 {private: // 私有成员:只能在类内部访问 数据类型 成员变量; // ...
public: // 公有成员:可以在类外部访问 // 构造函数的声明 类名(参数类型 参数名, ...);
// 成员函数的声明 返回类型 成员函数名(参数类型 参数名, ...);
// getter 和 setter 方法 返回类型 get成员变量名() const; void set成员变量名(数据类型 新值);};构造函数声明的语法格式
// 构造函数声明的语法格式类名(参数类型 参数名, ...);
// 使用初始化列表的构造函数类名(参数类型 参数名, ...) : 成员变量1(参数1), 成员变量2(参数2), ...{ // 构造函数体}重要特性:
- 构造函数的函数名与类名相同,没有返回类型(连
void都不写)- 构造函数在创建对象时自动调用,用于初始化成员变量
- 可以定义多个构造函数(重载),以适应不同的初始化需求
类定义完整示例:Student 类
// 类与面向对象示例 —— 定义一个 Student(学生)类#include <iostream>#include <string>using namespace std;
// 定义 Student 类class Student {private: // 私有成员:外部不能直接访问 string name; // 姓名 int age; // 年龄 double score; // 成绩
public: // 公有成员:外部可以通过对象访问 // 构造函数:使用初始化列表初始化成员变量 Student(string n, int a, double s) : name(n), age(a), score(s) {}
// const 成员函数:显示学生信息(不修改成员变量) void display() const { cout << "姓名: " << name << " 年龄: " << age << " 成绩: " << score << endl; }
// getter 方法:获取成绩(const 表示不修改对象状态) double getScore() const { return score; }
// setter 方法:设置成绩 void setScore(double s) { score = s; }};
int main() { // 使用构造函数创建 Student 对象 Student stu("张三", 18, 95.5);
// 调用公有成员函数 stu.display(); // 输出学生信息
// 通过 getter 获取成绩 cout << "当前成绩: " << stu.getScore() << endl;
// 通过 setter 修改成绩 stu.setScore(88.0); cout << "修改后成绩: " << stu.getScore() << endl;
// stu.score = 100; // 错误!score 是 private 成员,不能直接访问
return 0;}代码讲解
类定义部分:
class Student { ... };:定义一个名为Student的类。类的定义以关键字class开始,以分号;结束(注意最后这个分号不能省略)。private::访问控制修饰符,表示其后的成员是私有的,只能在类内部的成员函数中访问。通常将成员变量放在private中,以实现封装(数据隐藏)。public::访问控制修饰符,表示其后的成员是公有的,可以在类外部通过对象访问。通常将成员函数放在public中,作为对外的接口。
构造函数部分:
Student(string n, int a, double s):构造函数,函数名与类名相同,没有返回类型。参数n、a、s分别用于初始化姓名、年龄和成绩。: name(n), age(a), score(s):这是成员初始化列表(Member Initializer List),在构造函数体执行之前,直接用参数值初始化各成员变量。相比在函数体内赋值,初始化列表更高效。- 创建对象时,构造函数被自动调用:
Student stu("张三", 18, 95.5);这行代码创建了Student类的一个实例stu,并调用了构造函数。
成员函数部分:
void display() const:末尾的const关键字表示这是一个常量成员函数,承诺不会修改对象的任何成员变量。如果试图在const成员函数中修改成员变量,编译器会报错。double getScore() const:getter 方法,用于在外部获取私有成员score的值。void setScore(double s):setter 方法,用于在外部安全地修改私有成员score的值。通过 getter/setter 可以在将来加入数据验证逻辑。
对象使用部分:
Student stu("张三", 18, 95.5);:创建对象,使用点运算符.调用成员函数。stu.score = 100;这行代码是错误的,因为score是private成员,只能在类内部访问。这就是封装的作用——保护数据不被外部随意修改。
类的创建与使用语法总结
// 创建对象的语法格式类名 对象名(构造函数参数);
// 调用公有成员函数的语法格式对象名.成员函数名(参数);
// 访问公有成员变量的语法格式对象名.成员变量名;重点难点
- 封装的理解:
private成员对外隐藏,只能通过public的成员函数(getter/setter)访问。 - 构造函数的特点:与类同名、无返回类型、创建对象时自动调用。
- 成员初始化列表比在构造函数体内赋值更高效,推荐使用。
const成员函数承诺不修改对象状态,适合用于 getter 和 display 类函数。
学习建议
- 从简单的类开始练习,比如定义一个
Rectangle类(长、宽属性,计算面积和周长的方法)。 - 理解”类是蓝图,对象是实例”的类比。
Student是蓝图,stu是根据蓝图创建的具体学生。 - 养成使用 getter/setter 的好习惯,不要将成员变量声明为
public。
课后练习
练习 1:定义 Rectangle 类
定义一个 Rectangle(矩形)类,要求:
- 私有成员:
width(宽度)和height(高度),类型为double - 构造函数:接收宽度和高度参数,使用初始化列表初始化
- 公有成员函数:
getArea()(计算面积)、getPerimeter()(计算周长)、display()(显示信息) - 在
main中创建一个宽度为 5.0、高度为 3.0 的矩形,输出面积和周长
参考答案
// 练习1:定义 Rectangle(矩形)类#include <iostream>using namespace std;
class Rectangle {private: double width; // 宽度 double height; // 高度
public: // 构造函数:使用初始化列表 Rectangle(double w, double h) : width(w), height(h) {}
// 计算面积 double getArea() const { return width * height; }
// 计算周长 double getPerimeter() const { return 2 * (width + height); }
// 显示矩形信息 void display() const { cout << "矩形: " << width << " x " << height << endl; cout << "面积: " << getArea() << endl; cout << "周长: " << getPerimeter() << endl; }};
int main() { // 创建矩形对象 Rectangle rect(5.0, 3.0);
// 显示信息 rect.display();
return 0;}// 输出:// 矩形: 5 x 3// 面积: 15// 周长: 16代码讲解:getArea() 和 getPerimeter() 都是 const 成员函数,因为它们只读取成员变量进行计算,不修改对象状态。在 display() 中调用了其他成员函数,这是允许的——一个 const 成员函数可以调用同类的其他 const 成员函数。
练习 2:创建多个对象
在练习 1 的基础上,在 main 函数中创建两个 Rectangle 对象(例如 5×3 和 10×4),分别输出它们的面积和周长,并比较哪个面积更大。
参考答案
// 练习2:创建多个对象并比较面积#include <iostream>using namespace std;
class Rectangle {private: double width; double height;
public: Rectangle(double w, double h) : width(w), height(h) {} double getArea() const { return width * height; } double getPerimeter() const { return 2 * (width + height); } void display() const { cout << width << " x " << height << " => 面积: " << getArea() << ", 周长: " << getPerimeter() << endl; }};
int main() { // 创建两个矩形对象 Rectangle rect1(5.0, 3.0); Rectangle rect2(10.0, 4.0);
// 分别输出信息 cout << "矩形1: "; rect1.display(); cout << "矩形2: "; rect2.display();
// 比较面积 if (rect1.getArea() > rect2.getArea()) { cout << "矩形1 的面积更大" << endl; } else if (rect1.getArea() < rect2.getArea()) { cout << "矩形2 的面积更大" << endl; } else { cout << "两个矩形面积相等" << endl; }
return 0;}// 输出:// 矩形1: 5 x 3 => 面积: 15, 周长: 16// 矩形2: 10 x 4 => 面积: 40, 周长: 28// 矩形2 的面积更大代码讲解:同一个类可以创建多个独立的对象。每个对象都有自己的 width 和 height 副本,互不影响。通过点运算符 . 分别调用各自的方法,获取各自的数据。
1.6 C++ 核心特性补充
函数重载、默认参数、动态内存管理
知识点概述
本节补充介绍 C++ 的几个重要特性:函数重载(Function Overloading)、默认参数(Default Arguments)和动态内存管理(Dynamic Memory Management)。这些特性让 C++ 程序更加灵活和强大。
- 函数重载允许定义多个同名函数,只要它们的参数列表(参数类型或数量)不同即可。
- 默认参数允许在函数声明中为参数指定默认值,调用时可以省略这些参数。
- 动态内存管理使用
new和delete运算符在程序运行时动态地分配和释放内存。
核心概念
函数重载的语法格式
// 函数重载的语法格式:同名函数,参数列表不同返回类型 函数名(参数类型1 参数名1); // 版本1返回类型 函数名(参数类型2 参数名1); // 版本2返回类型 函数名(参数类型1 参数名1, 参数类型2 参数名2); // 版本3
// 注意:不能仅靠返回类型不同来重载函数// int foo(int x); // 正确// double foo(int x); // 错误!仅返回类型不同,编译器无法区分重载的判断依据:参数的个数不同、参数的类型不同、参数的顺序不同(当有不同类型的参数时)。返回类型不参与重载判断。
默认参数的语法格式
// 默认参数的语法格式返回类型 函数名(参数类型 参数名 = 默认值);
// 多个参数时,默认参数必须从右向左连续设置返回类型 函数名(类型1 参数1, 类型2 参数2 = 默认值2, 类型3 参数3 = 默认值3);// 错误示例:不能在非默认参数前设置默认参数// 返回类型 函数名(类型1 参数1 = 默认值1, 类型2 参数2); // 编译错误!动态内存管理的语法格式
// 动态分配单个变量的语法格式数据类型 *指针变量名 = new 数据类型(初始值);
// 动态分配数组的语法格式数据类型 *指针变量名 = new 数据类型[数组大小]{初始值列表};
// 释放单个变量的内存delete 指针变量名;
// 释放数组的内存(注意使用 delete[])delete[] 指针变量名;重要警告:每个
new都必须有对应的delete,否则会导致内存泄漏(Memory Leak)——分配的内存永远不会被释放,长时间运行的程序会耗尽内存。
函数重载示例
// 函数重载示例 —— 同名函数处理不同类型和数量的参数#include <iostream>using namespace std;
// 版本1:两个整数相加int add(int a, int b) { cout << "调用 int add(int, int)" << endl; return a + b;}
// 版本2:两个浮点数相加double add(double a, double b) { cout << "调用 double add(double, double)" << endl; return a + b;}
// 版本3:三个整数相加int add(int a, int b, int c) { cout << "调用 int add(int, int, int)" << endl; return a + b + c;}
int main() { // 编译器根据参数的类型和数量自动选择匹配的版本 cout << add(3, 5) << endl; // 调用版本1:两个 int cout << add(2.5, 3.7) << endl; // 调用版本2:两个 double cout << add(1, 2, 3) << endl; // 调用版本3:三个 int
return 0;}// 输出:// 调用 int add(int, int)// 8// 调用 double add(double, double)// 6.2// 调用 int add(int, int, int)// 6代码讲解
- 三个函数都叫
add,但参数列表各不相同:版本1 接受两个int,版本2 接受两个double,版本3 接受三个int。 - 编译器如何选择:当调用
add(3, 5)时,编译器看到两个int参数,匹配到版本1。当调用add(2.5, 3.7)时,两个double字面量匹配版本2。当调用add(1, 2, 3)时,三个int参数匹配版本3。 - 函数重载的好处是:可以使用一个直观的函数名来处理多种情况,而不需要取
addInt、addDouble、addThreeInt这样冗长的名字。 - 在每个函数内部添加了
cout输出,方便观察到底调用了哪个版本。在实际开发中不需要这样做。
默认参数示例
// 默认参数示例 —— 函数参数的默认值#include <iostream>#include <string>using namespace std;
// name 是必选参数,age 和 city 是可选参数(有默认值)void print(string name, int age = 0, string city = "未知") { cout << name << ", " << age << " 岁, 来自 " << city << endl;}
int main() { // 提供所有参数 print("张三", 18, "北京"); // 输出:张三, 18 岁, 来自 北京
// 省略最后一个参数(使用 city 的默认值 "未知") print("李四", 20); // 输出:李四, 20 岁, 来自 未知
// 省略后两个参数(使用 age 的默认值 0 和 city 的默认值 "未知") print("王五"); // 输出:王五, 0 岁, 来自 未知
// 注意:不能跳过中间参数 // print("赵六", "上海"); // 错误!不能跳过 age 直接给 city 赋值
return 0;}代码讲解
int age = 0和string city = "未知"为参数设置了默认值。调用函数时,如果省略了某个参数,编译器会使用对应的默认值。- 默认参数必须从右到左连续设置。即,如果某个参数有默认值,它右边的所有参数也必须有默认值。不能出现左边有默认值而右边没有的情况。这是因为 C++ 的参数匹配是按位置从左到右进行的,不能跳过中间的参数。
print("张三", 18, "北京"):提供了所有三个参数,默认值不生效。print("李四", 20):省略了city,使用默认值"未知"。print("王五"):省略了age和city,分别使用默认值0和"未知"。
动态内存管理示例
// 动态内存管理示例 —— 使用 new 和 delete 管理堆内存#include <iostream>using namespace std;
int main() { // ========== 动态分配单个变量 ========== int *p = new int(42); // 在堆上分配一个 int,初始值为 42 cout << "p 指向的值: " << *p << endl; // 输出 42 cout << "p 的地址: " << p << endl; // 输出内存地址
delete p; // 释放内存,防止内存泄漏 p = nullptr; // 好习惯:将指针置为空,避免悬空指针
// ========== 动态分配数组 ========== int *arr = new int[5]{1, 2, 3, 4, 5}; // 在堆上分配含5个元素的数组
// 遍历并输出数组元素 cout << "数组元素: "; for (int i = 0; i < 5; i++) { cout << arr[i] << " "; } cout << endl; // 输出:数组元素: 1 2 3 4 5
delete[] arr; // 释放数组内存(注意使用 delete[] 而非 delete) arr = nullptr;
// ========== 注意事项演示 ========== int *p2 = new int(100); // delete p2; // 如果忘记 delete,就会产生内存泄漏! delete p2; // 正确:用完即释放 p2 = nullptr;
cout << "动态内存管理演示完成" << endl;
return 0;}代码讲解
int *p = new int(42);:new运算符在堆(Heap)上动态分配了一块int大小的内存,并初始化为42,然后返回该内存的地址,赋值给指针p。与栈上的变量不同,堆内存的生命周期由程序员手动控制。*p:*是解引用运算符,用于通过指针访问其指向的内存中的值。*p的值就是42。delete p;:delete运算符释放new分配的内存,将其归还给系统。每块new分配的内存都必须用delete释放,否则这块内存会一直被占用,造成内存泄漏。p = nullptr;:释放内存后,将指针设为nullptr(空指针)是一个好习惯。这样可以防止后面不小心通过该指针访问已释放的内存(悬空指针问题),使用nullptr的指针进行解引用会立即报错,更容易发现问题。int *arr = new int[5]{1, 2, 3, 4, 5};:使用new[]动态分配数组。注意初始化列表{1, 2, 3, 4, 5}的用法。delete[] arr;:释放数组内存时,必须使用delete[](带方括号),而非普通的delete。使用delete释放数组只释放第一个元素,其余元素的内存不会被释放。
栈内存 vs 堆内存对比
// 栈内存 vs 堆内存对比示例#include <iostream>using namespace std;
int main() { // 栈内存(Stack):自动分配和释放 int stackVar = 10; // 函数结束时自动释放 cout << "栈变量: " << stackVar << endl;
// 堆内存(Heap):手动分配和释放 int *heapVar = new int(20); // 需要 new 分配 cout << "堆变量: " << *heapVar << endl; delete heapVar; // 需要 delete 释放 heapVar = nullptr;
return 0; // stackVar 在此处自动销毁}代码讲解
| 特性 | 栈内存(Stack) | 堆内存(Heap) |
|---|---|---|
| 分配方式 | 自动(声明变量时) | 手动(使用 new) |
| 释放方式 | 自动(离开作用域时) | 手动(使用 delete) |
| 大小 | 较小(通常 1~8 MB) | 较大(受物理内存限制) |
| 速度 | 快 | 较慢 |
| 生命周期 | 局部的(作用域内) | 由程序员控制 |
重点难点
- 函数重载的判断依据是参数列表,不能仅靠返回类型区分。
- 默认参数必须从右向左连续设置,调用时不能跳过中间参数。
new和delete必须配对使用:new对应delete,new[]对应delete[]。- 内存泄漏是 C++ 常见的 bug,忘记
delete会导致内存被永久占用。
学习建议
- 在实际项目中,建议优先使用 C++ 标准库的智能指针(如
std::unique_ptr、std::shared_ptr)来管理动态内存,它们会自动释放内存,避免内存泄漏。 - 注意
delete后将指针置为nullptr的好习惯。 - 初学者可以先理解
new和delete的基本用法,后续深入学习智能指针后,可以完全替代手动内存管理。
课后练习
练习 1:函数重载——计算面积
编写三个同名的 area 函数,分别计算:
- 正方形面积(参数:边长
double side) - 矩形面积(参数:长度
double length,宽度double width) - 圆形面积(参数:半径
double radius,使用3.14159作为 π 的值)
在 main 函数中分别调用并输出结果。
参考答案
// 练习1:函数重载 —— 计算不同形状的面积#include <iostream>using namespace std;
// 版本1:正方形面积(1个参数)double area(double side) { cout << "正方形面积: "; return side * side;}
// 版本2:矩形面积(2个参数)double area(double length, double width) { cout << "矩形面积: "; return length * width;}
// 版本3:圆形面积(1个 double 参数,但用第二个参数区分)// 由于前两个版本的参数个数分别是 1 和 2,需要另想办法// 这里我们改为提供 3 个版本,利用参数数量和类型的不同double circleArea(double radius) { cout << "圆形面积: "; return 3.14159 * radius * radius;}
int main() { // 通过函数名区分 cout << area(4.0) << endl; // 正方形,边长 4 cout << area(5.0, 3.0) << endl; // 矩形,长 5 宽 3 cout << circleArea(2.0) << endl; // 圆形,半径 2
return 0;}// 输出:// 正方形面积: 16// 矩形面积: 15// 圆形面积: 12.5664代码讲解:由于”正方形面积”和”圆形面积”的函数签名都是接受一个 double 参数,无法通过函数重载来区分,所以圆形面积使用了不同的函数名 circleArea。在实际开发中,如果函数逻辑有本质不同,使用不同的函数名反而更清晰。函数重载适合用于”对不同的数据类型做相同操作”的场景。
练习 2:动态数组管理
编写程序,使用 new 动态创建一个包含 5 个整数的数组,从键盘读入这 5 个整数,计算它们的平均值,然后使用 delete[] 释放内存。
参考答案
// 练习2:动态数组管理 —— 动态分配、使用和释放数组#include <iostream>using namespace std;
int main() { int size = 5;
// 使用 new 动态分配数组 int *arr = new int[size];
// 从键盘读入数据 cout << "请输入 " << size << " 个整数: "; for (int i = 0; i < size; i++) { cin >> arr[i]; }
// 计算总和 int sum = 0; for (int i = 0; i < size; i++) { sum += arr[i]; }
// 计算并输出平均值(注意将 sum 转为 double 避免整数除法) double average = (double)sum / size; cout << "平均值: " << average << endl;
// 释放动态数组内存 delete[] arr; arr = nullptr;
return 0;}// 运行示例:// 请输入 5 个整数: 80 90 75 88 92// 平均值: 85代码讲解:
new int[size]动态分配了size个int的内存空间,返回首元素的地址赋给指针arr。cin >> arr[i]从标准输入读取数据,cin是标准输入流对象,>>是流提取运算符。(double)sum / size使用强制类型转换避免整数除法陷阱,确保得到精确的平均值。delete[] arr释放数组内存,必须使用delete[]而不是delete。然后将指针置为nullptr防止悬空指针。
本篇小结
经过第一篇的学习,你已经掌握了 C++ 的核心基础知识:
| 课时 | 主题 | 关键知识 |
|---|---|---|
| 1.1 | C++ 概述与环境搭建 | C++ 历史、编译过程、Visual Studio 配置、Hello World |
| 1.2 | 基础语法 | 数据类型、变量与常量、运算符优先级、整数除法陷阱 |
| 1.3 | 流程控制 | if-else、switch-case、for、while、break、continue |
| 1.4 | 数组与函数 | 数组声明与遍历、函数声明与定义、引用、命名空间 |
| 1.5 | 类与面向对象入门 | 类定义、构造函数、封装、getter/setter |
| 1.6 | C++ 核心特性补充 | 函数重载、默认参数、动态内存管理(new/delete) |
这些知识是 C++ 编程的基石。在后续的学习中,我们将深入探讨指针的高级用法、面向对象编程的继承与多态、模板编程、标准模板库(STL)等重要主题。持续练习、多写代码是学好 C++ 的关键。
🎯 下一步学习建议:尝试不看教程,独立编写一个小程序(如简单的计算器、学生成绩管理系统),检验本篇所学知识的掌握程度。
第二篇:面向对象编程深入
本篇深入探讨 C++ 面向对象编程的核心概念和高级特性,涵盖面向对象思想、构造与析构函数、特殊成员(this 指针、static、const、友元)、继承与派生、以及多态等内容。
2.1 面向对象思想
知识点概述
面向对象编程(Object-Oriented Programming, OOP)是 C++ 的核心编程范式。本节将对比面向过程与面向对象两种编程思想,理解类与对象的关系。
核心概念
- 面向过程:程序 = 数据结构 + 算法。以函数为中心,数据在各函数间传递。
- 面向对象:程序 = 对象 + 消息传递。以对象为中心,数据和操作数据的方法被封装在一起。
class默认访问权限为private,struct默认访问权限为public。
class 与 struct 的区别
// class 默认权限为 privateclass MyClass { int x; // 默认 privatepublic: int y; // 显式声明 public};
// struct 默认权限为 publicstruct MyStruct { int x; // 默认 publicprivate: int y; // 显式声明 private};代码讲解:
class定义的成员默认是private的,外部无法直接访问,需要通过public接口。struct定义的成员默认是public的,外部可以直接访问。- 除了默认访问权限不同外,
class和struct在 C++ 中几乎等价。
面向过程 vs 面向对象 示例
面向过程的写法——数据与操作分离:
#include <iostream>#include <cmath>using namespace std;
// 面向过程:数据结构与操作函数分离struct Point { int x; int y;};
// 独立函数计算两点距离double distance(Point p1, Point p2) { double dx = p1.x - p2.x; double dy = p1.y - p2.y; return sqrt(dx * dx + dy * dy);}
int main() { Point p1 = {3, 4}; Point p2 = {0, 0}; cout << "距离: " << distance(p1, p2) << endl; // 输出: 距离: 5 return 0;}代码讲解:
- 第 7-10 行:定义
Point结构体作为数据结构,包含x和y两个成员。 - 第 13-17 行:定义一个独立函数
distance,接收两个Point参数,使用勾股定理计算距离。 - 第 20-23 行:在
main中创建两个点并调用函数计算距离。 - 核心问题:数据(
Point)和操作(distance函数)是分离的,随着程序变大,维护困难。
面向对象的写法——数据与操作封装在一起:
#include <iostream>#include <cmath>using namespace std;
// 面向对象:数据与操作封装在类中class Point { double x, y; // 私有数据成员public: // 构造函数,使用初始化列表 Point(double x, double y) : x(x), y(y) {}
// 成员函数:计算到另一个点的距离 double distanceTo(const Point &other) const { double dx = x - other.x; double dy = y - other.y; return sqrt(dx * dx + dy * dy); }
// getter 函数 double getX() const { return x; } double getY() const { return y; }};
int main() { Point p1(3, 4); Point p2(0, 0); cout << "距离: " << p1.distanceTo(p2) << endl; // 输出: 距离: 5 cout << "p1.x = " << p1.getX() << endl; // 输出: p1.x = 3 return 0;}代码讲解:
- 第 8 行:
double x, y声明为private(class 默认),外部无法直接访问。 - 第 11 行:构造函数
Point(double x, double y)与类同名,用于初始化对象。: x(x), y(y)是成员初始化列表语法,将参数直接赋值给成员变量。 - 第 14-18 行:
distanceTo是一个const成员函数(const在参数列表后),表示该函数不会修改对象的状态。参数const Point &other使用常量引用避免拷贝开销。 - 第 21-22 行:
getX()和getY()是访问器(getter),提供对私有成员的只读访问。 - 第 25-29 行:在
main中通过对象调用成员函数p1.distanceTo(p2),语义更加清晰——“p1 到 p2 的距离”。
类与对象的关系
// 类是蓝图,对象是实例class Student { string name; int age;public: Student(string n, int a) : name(n), age(a) {} void introduce() const { cout << "我叫" << name << ",今年" << age << "岁" << endl; }};
int main() { Student s1("张三", 20); // s1 是 Student 类的一个对象(实例) Student s2("李四", 22); // s2 是另一个独立的对象 s1.introduce(); // 输出: 我叫张三,今年20岁 s2.introduce(); // 输出: 我叫李四,今年22岁 return 0;}代码讲解:
Student是类(类型/蓝图),定义了数据的结构和操作。s1、s2是对象(实例),每个对象都有自己独立的name和age数据。- 同一个类的不同对象共享相同的成员函数代码,但各自拥有独立的数据。
构造函数语法格式
// 构造函数声明/定义语法class ClassName {private: // 成员变量public: // 无参构造函数 ClassName();
// 带参构造函数 ClassName(参数类型1 参数名1, 参数类型2 参数名2);
// 使用初始化列表的构造函数 ClassName(参数类型1 参数名1, 参数类型2 参数名2) : 成员变量1(参数名1), 成员变量2(参数名2) {}};课后练习
练习 1:定义一个 Rectangle(矩形)类,包含宽度和高度两个私有成员,提供构造函数、计算面积和周长的方法,在 main 中创建对象并测试。
参考答案
#include <iostream>using namespace std;
class Rectangle { double width, height;public: Rectangle(double w, double h) : width(w), height(h) {}
double area() const { return width * height; }
double perimeter() const { return 2 * (width + height); }};
int main() { Rectangle rect(5.0, 3.0); cout << "面积: " << rect.area() << endl; // 输出: 面积: 15 cout << "周长: " << rect.perimeter() << endl; // 输出: 周长: 16 return 0;}代码讲解:
Rectangle类封装了矩形的宽高数据,通过构造函数初始化。area()和perimeter()都是const成员函数,不会修改对象状态。- 在
main中创建对象后直接调用成员方法即可。
练习 2:定义一个 BankAccount(银行账户)类,包含账户名和余额,提供存款 deposit、取款 withdraw(余额不足时提示)和查询余额 getBalance 方法。
参考答案
#include <iostream>#include <string>using namespace std;
class BankAccount { string owner; double balance;public: BankAccount(string name, double initBalance = 0.0) : owner(name), balance(initBalance) {}
void deposit(double amount) { if (amount > 0) { balance += amount; cout << owner << " 存入 " << amount << ",余额: " << balance << endl; } }
bool withdraw(double amount) { if (amount <= 0) { cout << "取款金额必须大于 0" << endl; return false; } if (amount > balance) { cout << "余额不足!当前余额: " << balance << endl; return false; } balance -= amount; cout << owner << " 取出 " << amount << ",余额: " << balance << endl; return true; }
double getBalance() const { return balance; }};
int main() { BankAccount acc("张三", 1000.0); acc.deposit(500); // 张三 存入 500,余额: 1500 acc.withdraw(200); // 张三 取出 200,余额: 1300 acc.withdraw(2000); // 余额不足!当前余额: 1300 return 0;}代码讲解:
- 构造函数使用默认参数
initBalance = 0.0,创建账户时可以不传初始余额。 deposit方法检查金额有效性后再存款。withdraw方法检查余额是否充足,返回bool表示是否成功。getBalance是const成员函数,只读查询不修改数据。
2.2 string 类与构造/析构函数
知识点概述
本节学习 C++ 标准库中的 string 类常用操作,以及类的三大特殊成员函数:构造函数、析构函数和拷贝构造函数。
核心概念
- string 常用操作:构造、拼接、查找、替换、截取、比较、长度
- 构造函数特征:与类同名、无返回类型、可重载
- 析构函数特征:
~类名、无参数、不可重载 - 拷贝构造函数:
T(const T &other),浅拷贝 vs 深拷贝
string 类常用操作
#include <iostream>#include <string>using namespace std;
int main() { // 1. 构造方式 string s1 = "Hello"; // 直接初始化 string s2(" World"); // 括号初始化 string s3(5, 'A'); // 重复字符构造: "AAAAA" string s4(s1); // 拷贝构造
// 2. 拼接 string s5 = s1 + s2; // "Hello World" s1 += "!"; // s1 变为 "Hello!"
// 3. 长度 cout << s5.length() << endl; // 11 cout << s5.size() << endl; // 11(与 length() 等价)
// 4. 查找 cout << s5.find("World") << endl; // 6(从位置 0 开始找) cout << s5.find("xyz") << endl; // string::npos(未找到)
// 5. 截取(子串) string sub = s5.substr(6, 5); // 从位置6取5个字符: "World"
// 6. 替换 string s6 = s5; s6.replace(6, 5, "C++"); // "Hello C++"
// 7. 比较 string a = "abc", b = "abd"; cout << a.compare(b) << endl; // 负数(a < b,按字典序)
return 0;}代码讲解:
- 构造方式:
string支持多种构造方式——直接赋值= "Hello"、括号构造" World"、重复字符构造(5, 'A')得到"AAAAA"、拷贝构造s4(s1)。 - 拼接:使用
+运算符或+=进行字符串拼接。 - 长度:
length()和size()功能完全相同,都返回字符串中的字符数。 - 查找:
find()返回子串首次出现的位置索引(从 0 开始),未找到时返回string::npos(通常为-1转换为最大无符号整数)。 - 截取:
substr(pos, len)从位置pos开始截取len个字符。 - 替换:
replace(pos, len, str)将从pos开始的len个字符替换为str。 - 比较:
compare()按字典序比较,返回负数表示小于,0 表示相等,正数表示大于。
构造函数详解
构造函数是类中特殊的成员函数,在创建对象时自动调用。
构造函数语法格式
class ClassName {public: // 1. 默认构造函数(无参或所有参数都有默认值) ClassName();
// 2. 带参构造函数 ClassName(类型1 参数1, 类型2 参数2);
// 3. 使用初始化列表(推荐方式,效率更高) ClassName(类型1 参数1, 类型2 参数2) : 成员1(参数1), 成员2(参数2) { // 函数体 }
// 4. 委托构造函数(C++11) ClassName() : ClassName(默认值1, 默认值2) {}
// 5. 禁止默认构造(C++11) ClassName() = delete;};构造函数完整示例
#include <iostream>#include <string>using namespace std;
class Student { string name; int age; double score;public: // 默认构造函数 Student() : name("未知"), age(0), score(0.0) { cout << "默认构造函数被调用" << endl; }
// 带参构造函数(初始化列表) Student(string n, int a, double s) : name(n), age(a), score(s) { cout << "带参构造函数被调用: " << name << endl; }
// 委托构造函数 Student(string n) : Student(n, 0, 0.0) {}
void display() const { cout << "姓名: " << name << ", 年龄: " << age << ", 成绩: " << score << endl; }};
int main() { Student s1; // 调用默认构造函数 Student s2("张三", 20, 95.5); // 调用带参构造函数 Student s3("李四"); // 调用委托构造函数
s1.display(); // 姓名: 未知, 年龄: 0, 成绩: 0 s2.display(); // 姓名: 张三, 年龄: 20, 成绩: 95.5 s3.display(); // 姓名: 李四, 年龄: 0, 成绩: 0 return 0;}代码讲解:
- 第 12-14 行:默认构造函数不需要参数,使用初始化列表为成员赋予默认值。
- 第 17-20 行:带参构造函数接收三个参数,通过初始化列表高效初始化成员。初始化列表比函数体内赋值更高效,因为对于类类型成员,初始化列表直接调用其构造函数,而函数体内赋值会先默认构造再赋值。
- 第 23 行:委托构造函数——构造函数调用另一个构造函数,减少重复代码。
- 当创建对象时,根据参数自动匹配对应的构造函数。
析构函数详解
析构函数在对象生命周期结束时(离开作用域或 delete 时)自动调用,用于释放资源。
析构函数语法格式
class ClassName {public: // 析构函数语法:~类名(),无参数,无返回值,不可重载 ~ClassName();};析构函数完整示例
#include <iostream>using namespace std;
class Logger { string name;public: Logger(string n) : name(n) { cout << "[" << name << "] 对象已创建" << endl; }
~Logger() { cout << "[" << name << "] 对象已销毁" << endl; }};
int main() { cout << "--- 进入 main ---" << endl; { Logger l1("局部对象1"); Logger l2("局部对象2"); cout << "--- 离开内部作用域 ---" << endl; } // l1、l2 在此处被析构(后构造的先析构) cout << "--- main 结束 ---" << endl; return 0;}代码讲解:
- 析构函数以
~开头,与类同名,无参数、无返回值、不可重载。 - 对象析构顺序与构造顺序相反(LIFO——后进先出):先析构
l2,再析构l1。 - 作用域结束时(
}),局部对象自动调用析构函数。
拷贝构造函数详解
拷贝构造函数在用已有对象初始化新对象时自动调用。
拷贝构造函数语法格式
class ClassName {public: // 拷贝构造函数:参数为同类对象的常量引用 ClassName(const ClassName &other);};浅拷贝 vs 深拷贝
#include <iostream>#include <cstring>using namespace std;
class MyString { char *data; // 动态分配的字符数组 int len;public: // 构造函数:根据 C 字符串创建对象 MyString(const char *s = "") { len = strlen(s); data = new char[len + 1]; // 动态分配内存 strcpy(data, s); cout << "构造: " << data << endl; }
// 析构函数:释放动态内存 ~MyString() { cout << "析构: " << (data ? data : "null") << endl; delete[] data; // 释放 new[] 分配的内存 }
// 拷贝构造函数(深拷贝):为每个对象独立分配内存 MyString(const MyString &other) { len = other.len; data = new char[len + 1]; // 分配新内存 strcpy(data, other.data); // 复制内容 cout << "拷贝构造: " << data << endl; }
// 拷贝赋值运算符(深拷贝) MyString& operator=(const MyString &other) { if (this != &other) { // 防止自赋值 delete[] data; // 释放旧内存 len = other.len; data = new char[len + 1]; strcpy(data, other.data); } cout << "拷贝赋值: " << data << endl; return *this; }
void print() const { cout << "内容: " << (data ? data : "null") << ", 长度: " << len << endl; }};
int main() { MyString s1("Hello"); // 调用构造函数 MyString s2 = s1; // 调用拷贝构造函数(深拷贝) MyString s3("World"); s3 = s1; // 调用拷贝赋值运算符
s1.print(); // 内容: Hello, 长度: 5 s2.print(); // 内容: Hello, 长度: 5(独立拷贝) s3.print(); // 内容: Hello, 长度: 5
return 0;} // s3、s2、s1 依次析构,各自释放独立内存,无重复释放问题代码讲解:
- 浅拷贝问题:如果不自定义拷贝构造函数,编译器生成的默认版本只做指针值的复制(浅拷贝),导致两个对象的
data指针指向同一块内存。析构时同一块内存被释放两次,造成未定义行为。 - 第 22-27 行:深拷贝——拷贝构造函数为新对象分配独立的内存空间,然后复制字符串内容,确保两个对象各自拥有独立的
data指针。 - 第 30-39 行:拷贝赋值运算符
operator=需要先检查自赋值(this != &other),再释放旧内存,最后分配新内存并复制内容。 - 析构函数中使用
delete[] data释放new[]分配的内存,二者必须配对使用。 - 析构顺序仍然是后构造的先析构。
Rule of Three(三法则)
如果一个类需要自定义以下三者之一,则通常三个都需要自定义:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
课后练习
练习 1:实现一个 DynamicArray(动态数组)类,使用动态内存管理。要求包含构造函数、析构函数、拷贝构造函数、push_back(添加元素)、print(打印元素)方法。
参考答案
#include <iostream>using namespace std;
class DynamicArray { int *data; int size; int capacity;public: // 构造函数 DynamicArray(int cap = 10) : size(0), capacity(cap) { data = new int[capacity]; cout << "构造,容量: " << capacity << endl; }
// 析构函数 ~DynamicArray() { delete[] data; cout << "析构,元素数: " << size << endl; }
// 拷贝构造函数(深拷贝) DynamicArray(const DynamicArray &other) : size(other.size), capacity(other.capacity) { data = new int[capacity]; for (int i = 0; i < size; i++) { data[i] = other.data[i]; } cout << "拷贝构造,元素数: " << size << endl; }
// 拷贝赋值运算符 DynamicArray& operator=(const DynamicArray &other) { if (this != &other) { delete[] data; size = other.size; capacity = other.capacity; data = new int[capacity]; for (int i = 0; i < size; i++) { data[i] = other.data[i]; } } return *this; }
// 添加元素 void push_back(int value) { if (size >= capacity) { capacity *= 2; int *newData = new int[capacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; } delete[] data; data = newData; } data[size++] = value; }
// 打印元素 void print() const { cout << "["; for (int i = 0; i < size; i++) { cout << data[i]; if (i < size - 1) cout << ", "; } cout << "]" << endl; }};
int main() { DynamicArray arr; arr.push_back(10); arr.push_back(20); arr.push_back(30); arr.print(); // [10, 20, 30]
DynamicArray arr2 = arr; // 深拷贝 arr2.push_back(40); arr.print(); // [10, 20, 30](原数组不受影响) arr2.print(); // [10, 20, 30, 40]
return 0;}代码讲解:
- 构造函数分配初始容量的动态数组,默认容量为 10。
- 拷贝构造函数实现深拷贝,分配独立的内存并逐元素复制。
push_back在空间不足时自动扩容为原来的 2 倍。- 析构函数释放动态数组内存。
- 拷贝赋值运算符实现了完整的自赋值检查、旧内存释放、新内存分配。
练习 2:编写一个 SmartPointer(简易智能指针)类,封装裸指针,在构造时接管指针,析构时自动释放。要求支持 * 解引用和 -> 成员访问。
参考答案
#include <iostream>using namespace std;
class SmartPointer { int *ptr;public: // 接管裸指针 explicit SmartPointer(int *p = nullptr) : ptr(p) { cout << "SmartPointer 创建" << endl; }
// 析构时自动释放 ~SmartPointer() { delete ptr; cout << "SmartPointer 销毁,内存已释放" << endl; }
// 禁止拷贝(避免双重释放) SmartPointer(const SmartPointer &) = delete; SmartPointer& operator=(const SmartPointer &) = delete;
// 解引用运算符 int& operator*() const { return *ptr; }
// 箭头运算符 int* operator->() const { return ptr; }};
int main() { SmartPointer sp(new int(42)); cout << *sp << endl; // 42 *sp = 100; cout << *sp << endl; // 100
// SmartPointer sp2 = sp; // 编译错误:拷贝已禁用 return 0;}代码讲解:
explicit关键字禁止隐式转换,防止意外将裸指针自动转为SmartPointer。- 通过
= delete禁用拷贝构造和拷贝赋值,避免多个SmartPointer管理同一块内存导致双重释放。 operator*()和operator->()让SmartPointer的使用方式与裸指针类似。
2.3 this 指针与特殊成员
知识点概述
本节学习 C++ 类中的四个重要特殊成员概念:this 指针、static 静态成员、const 成员函数、以及友元(friend)。
核心概念
- this 指针:类型为
T* const,指向调用成员函数的对象本身 - static 成员:属于类而非对象,所有对象共享
- const 成员函数:承诺不修改成员变量
- 友元:允许外部函数或类访问私有成员
this 指针
this 是一个隐式参数,在每个非静态成员函数中都可用,指向调用该函数的对象。
this 指针语法格式
class ClassName { 类型 成员变量;public: // this 的类型是 ClassName* const // 即指向当前对象的常量指针(不能改变 this 的指向)
void func() { this->成员变量 = 值; // 显式使用 this 成员变量 = 值; // 隐式使用 this(编译器自动添加) }};this 指针与链式调用
#include <iostream>#include <string>using namespace std;
class Builder { string name; int age; string city;public: // 每个 set 方法返回 *this 的引用,实现链式调用 Builder& setName(const string &n) { name = n; return *this; // 返回当前对象的引用 }
Builder& setAge(int a) { age = a; return *this; }
Builder& setCity(const string &c) { city = c; return *this; }
void display() const { cout << "姓名: " << name << ", 年龄: " << age << ", 城市: " << city << endl; }};
int main() { Builder b; // 链式调用:每次调用返回同一对象的引用 b.setName("张三").setAge(18).setCity("北京"); b.display(); // 输出: 姓名: 张三, 年龄: 18, 城市: 北京 return 0;}代码讲解:
- 第 12 行:
return *this;返回当前对象本身的引用(Builder&),使得后续可以继续调用该对象的其他方法。 - 第 30 行:
b.setName("张三")返回b的引用,接着调用.setAge(18),再返回b的引用,形成链式调用。 this指针的类型是Builder* const——指向Builder对象的常量指针(指针本身不可修改,但指向的对象可以修改)。- 链式调用在设计模式(如 Builder 模式)和标准库(如
cout << a << b)中广泛使用。
static 静态成员
static 成员属于类本身,而非某个具体对象。所有对象共享同一个 static 成员。
static 成员语法格式
class ClassName { static 类型 静态成员变量; // 声明(类内)public: static 返回类型 静态成员函数(参数列表);};
// 类外初始化(静态成员变量必须在类外初始化一次)类型 ClassName::静态成员变量 = 初始值;static 成员完整示例
#include <iostream>#include <string>using namespace std;
class Counter { static int count; // 静态成员变量声明 int id; // 实例成员变量public: Counter() : id(++count) { cout << "创建第 " << id << " 个 Counter 对象" << endl; }
~Counter() { cout << "销毁第 " << id << " 个 Counter 对象" << endl; }
// 静态成员函数:通过类名调用,没有 this 指针 static int getCount() { return count; // 只能访问静态成员 }};
// 类外初始化静态成员变量int Counter::count = 0;
int main() { cout << "当前对象数: " << Counter::getCount() << endl; // 0
Counter c1; // 创建第 1 个 Counter c2; // 创建第 2 个 Counter c3; // 创建第 3 个
cout << "当前对象数: " << Counter::getCount() << endl; // 3
// 也可以通过对象调用(但不推荐) cout << "当前对象数: " << c1.getCount() << endl; // 3
return 0;}代码讲解:
- 第 7 行:
static int count在类内声明,不属于任何单个对象。 - 第 17 行:
Counter::count = 0在类外定义并初始化。静态成员变量必须在类外初始化一次(C++17 后可用inline static在类内初始化)。 - 第 14 行:
++count在构造函数中对静态变量递增,每次创建对象时自动计数。 - 第 20-22 行:静态成员函数
getCount()没有this指针,因此只能访问静态成员,不能访问实例成员。 - 第 31 行:通过
Counter::getCount()使用类名调用静态成员函数(推荐方式)。
const 成员函数
const 成员函数承诺不会修改对象的任何成员变量(mutable 成员除外)。
const 成员函数语法格式
class ClassName { 类型 成员变量;public: // const 成员函数:函数体中不能修改成员变量 返回类型 函数名(参数列表) const;
// 非const成员函数:可以修改成员变量 返回类型 函数名(参数列表);};const 成员函数示例
#include <iostream>using namespace std;
class Circle { double radius;public: Circle(double r) : radius(r) {}
// const 成员函数:只读取成员,不修改 double area() const { // radius = 10; // 编译错误!const 函数中不能修改成员 return 3.14159 * radius * radius; }
double getRadius() const { return radius; }
// 非 const 成员函数:可以修改成员 void setRadius(double r) { radius = r; }};
void printCircle(const Circle &c) { // const 引用参数 cout << "半径: " << c.getRadius() << ", 面积: " << c.area() << endl; // c.setRadius(5); // 编译错误!const 引用只能调用 const 成员函数}
int main() { Circle c(5.0); printCircle(c); // 半径: 5, 面积: 78.5398
const Circle c2(10.0); // const 对象 cout << c2.area() << endl; // OK:const 对象可以调用 const 成员函数 // c2.setRadius(20); // 编译错误!const 对象不能调用非 const 成员函数 return 0;}代码讲解:
const出现在成员函数参数列表之后(area() const),表示该函数是一个只读函数。const成员函数中不能修改任何非mutable的成员变量,否则编译报错。const对象(const Circle c2)只能调用const成员函数,不能调用非const成员函数。const引用参数(const Circle &c)同样只能调用const成员函数。- 最佳实践:如果成员函数不修改对象状态,都应该声明为
const。
友元(friend)
友元允许外部函数或另一个类访问当前类的私有成员。
友元声明语法格式
class ClassName {private: 类型 私有成员;public: // 1. 友元函数声明 friend 返回类型 友元函数名(参数列表);
// 2. 友元类声明 friend class 友元类名;};友元完整示例
#include <iostream>#include <string>using namespace std;
class Vector2D { double x, y; // 私有成员public: Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// 声明友元函数:可以访问私有成员 friend double dotProduct(const Vector2D &a, const Vector2D &b); friend Vector2D operator+(const Vector2D &a, const Vector2D &b); friend ostream& operator<<(ostream &os, const Vector2D &v);};
// 友元函数定义(不是成员函数,没有 ClassName:: 前缀)double dotProduct(const Vector2D &a, const Vector2D &b) { return a.x * b.x + a.y * b.y; // 可以直接访问私有成员 x, y}
Vector2D operator+(const Vector2D &a, const Vector2D &b) { return Vector2D(a.x + b.x, a.y + b.y); // 访问私有成员}
ostream& operator<<(ostream &os, const Vector2D &v) { os << "(" << v.x << ", " << v.y << ")"; // 访问私有成员 return os;}
int main() { Vector2D v1(3, 4); Vector2D v2(1, 2);
cout << "v1 = " << v1 << endl; // v1 = (3, 4) cout << "v2 = " << v2 << endl; // v2 = (1, 2) cout << "v1 + v2 = " << (v1 + v2) << endl; // v1 + v2 = (4, 6) cout << "点积 = " << dotProduct(v1, v2) << endl; // 点积 = 11 return 0;}代码讲解:
- 第 10-12 行:通过
friend关键字声明三个友元函数。友元声明放在类内部(通常在public区域),但友元函数不是成员函数。 - 第 16-37 行:友元函数在类外部定义,不需要
Vector2D::前缀,但可以直接访问Vector2D的私有成员x和y。 - 友元关系是单向的:
A声明B为友元,B可以访问A的私有成员,但A不一定能访问B的私有成员。 - 友元关系不能继承。
- 友元函数常用于运算符重载(如
operator<<、operator+),因为左侧操作数是ostream等非类类型,无法作为成员函数。
友元类示例
#include <iostream>using namespace std;
class Engine; // 前置声明
class Car { friend class Mechanic; // Mechanic 是 Car 的友元类private: string model; int horsepower;public: Car(string m, int hp) : model(m), horsepower(hp) {}};
class Mechanic {public: void inspect(const Car &c) { // 可以访问 Car 的私有成员(因为 Mechanic 是 Car 的友元) cout << "检查车型: " << c.model << ", 马力: " << c.horsepower << endl; }};
int main() { Car car("Tesla Model 3", 283); Mechanic m; m.inspect(car); // 检查车型: Tesla Model 3, 马力: 283 return 0;}代码讲解:
- 第 8 行:
friend class Mechanic声明Mechanic为Car的友元类,Mechanic的所有成员函数都可以访问Car的私有成员。 - 第 16-20 行:
Mechanic::inspect中直接访问c.model和c.horsepower,这两个是Car的私有成员。 - 友元类的使用场景:测试框架、工厂类、需要深度操作另一个类内部数据的情况。
课后练习
练习 1:实现一个 Employee 类,使用静态成员统计创建的员工总数。每个员工有姓名和工资,支持通过链式调用设置属性,并实现一个友元函数 compareSalary 比较两个员工的工资。
参考答案
#include <iostream>#include <string>using namespace std;
class Employee { string name; double salary; static int totalEmployees; // 静态成员:员工总数public: Employee() : name(""), salary(0) { totalEmployees++; }
Employee(string n, double s) : name(n), salary(s) { totalEmployees++; }
~Employee() { totalEmployees--; }
// 链式调用 Employee& setName(const string &n) { name = n; return *this; }
Employee& setSalary(double s) { salary = s; return *this; }
double getSalary() const { return salary; } string getName() const { return name; }
static int getTotal() { return totalEmployees; }
// 友元函数声明 friend void compareSalary(const Employee &e1, const Employee &e2); friend ostream& operator<<(ostream &os, const Employee &e);};
int Employee::totalEmployees = 0; // 类外初始化
// 友元函数定义void compareSalary(const Employee &e1, const Employee &e2) { cout << e1.name << "(" << e1.salary << ") vs " << e2.name << "(" << e2.salary << "): "; if (e1.salary > e2.salary) cout << e1.name << " 工资更高" << endl; else if (e1.salary < e2.salary) cout << e2.name << " 工资更高" << endl; else cout << "工资相同" << endl;}
ostream& operator<<(ostream &os, const Employee &e) { os << e.name << " (工资: " << e.salary << ")"; return os;}
int main() { Employee e1("张三", 8000); Employee e2("李四", 12000); Employee e3;
e3.setName("王五").setSalary(10000); // 链式调用
cout << e1 << endl; // 张三 (工资: 8000) cout << e2 << endl; // 李四 (工资: 12000) cout << e3 << endl; // 王五 (工资: 10000)
cout << "员工总数: " << Employee::getTotal() << endl; // 3
compareSalary(e1, e2); // 李四(12000) vs 张三(8000): 李四 工资更高 compareSalary(e2, e3); // 李四(12000) vs 王五(10000): 李四 工资更高
return 0;}代码讲解:
- 静态成员
totalEmployees在构造函数中递增、析构函数中递减,始终反映当前存活的员工数量。 setName和setSalary返回*this,支持链式调用。compareSalary是友元函数,可以直接访问Employee的私有成员name和salary。operator<<重载使Employee对象可以直接用cout输出。
2.4 继承与派生
知识点概述
继承是面向对象编程的核心机制之一,允许创建新类(派生类)来复用和扩展现有类(基类)的功能。
核心概念
- 继承方式:
public、protected、private继承 - 构造/析构顺序:先构造基类,再构造派生类;先析构派生类,再析构基类
- 菱形继承与虚继承:解决多重继承中的二义性问题
继承语法格式
// 单继承语法class 派生类名 : 继承方式 基类名 { // 新增成员};
// 继承方式class Derived : public Base { }; // public 继承(最常用)class Derived : protected Base { }; // protected 继承class Derived : private Base { }; // private 继承
// 多重继承语法class Derived : public Base1, public Base2 { // 新增成员};三种继承方式对比
#include <iostream>using namespace std;
class Base {public: int pub = 1;protected: int pro = 2;private: int pri = 3;};
// ============ public 继承 ============class PublicDerived : public Base {public: void test() { cout << pub << endl; // OK:public -> public cout << pro << endl; // OK:protected -> protected // cout << pri << endl; // 错误:private 不可访问 }};
// ============ protected 继承 ============class ProtectedDerived : protected Base {public: void test() { cout << pub << endl; // OK:public -> protected cout << pro << endl; // OK:protected -> protected // cout << pri << endl; // 错误:private 不可访问 }};
// ============ private 继承 ============class PrivateDerived : private Base {public: void test() { cout << pub << endl; // OK:public -> private cout << pro << endl; // OK:protected -> private // cout << pri << endl; // 错误:private 不可访问 }};
int main() { PublicDerived pd; cout << pd.pub << endl; // OK:public 成员仍为 public // cout << pd.pro << endl; // 错误:外部不能访问 protected
ProtectedDerived ptd; // cout << ptd.pub << endl; // 错误:public 降为 protected
PrivateDerived pvd; // cout << pvd.pub << endl; // 错误:public 降为 private
return 0;}代码讲解:
- public 继承(最常用):基类的
public成员在派生类中仍为public,protected仍为protected,private不可访问。这体现了 “is-a” 关系(派生类是一种基类)。 - protected 继承:基类的
public和protected成员在派生类中都变为protected。 - private 继承:基类的
public和protected成员在派生类中都变为private。这体现的是 “has-a” 关系(派生类内部使用了基类的实现,但不暴露给外部)。 - 无论哪种继承方式,基类的
private成员在派生类中都不可直接访问,但仍然存在(可通过基类的public/protected方法间接访问)。
构造与析构顺序
#include <iostream>using namespace std;
class Animal {protected: string name;public: Animal(string n) : name(n) { cout << "Animal 构造: " << name << endl; }
virtual ~Animal() { cout << "Animal 析构: " << name << endl; }
virtual void speak() { cout << name << ": ..." << endl; }};
class Dog : public Animal {public: // 派生类构造函数:在初始化列表中调用基类构造函数 Dog(string n) : Animal(n) { cout << "Dog 构造: " << name << endl; }
~Dog() { cout << "Dog 析构: " << name << endl; }
void speak() override { cout << name << ": 汪汪!" << endl; }};
class Cat : public Animal {public: Cat(string n) : Animal(n) { cout << "Cat 构造: " << name << endl; }
~Cat() { cout << "Cat 析构: " << name << endl; }
void speak() override { cout << name << ": 喵喵!" << endl; }};
int main() { cout << "=== 创建 Dog ===" << endl; Dog dog("旺财"); dog.speak(); // 旺财: 汪汪!
cout << "\n=== 创建 Cat ===" << endl; Cat cat("咪咪"); cat.speak(); // 咪咪: 喵喵!
cout << "\n=== 离开 main ===" << endl; return 0; // 析构顺序:先 Cat 析构 -> 再 Animal 析构 -> 先 Dog 析构 -> 再 Animal 析构}代码讲解:
- 构造顺序:先调用基类构造函数,再调用派生类构造函数。
Dog("旺财")先执行Animal("旺财"),再执行Dog的函数体。 - 析构顺序:与构造顺序相反。先析构派生类,再析构基类。
- 第 12 行:基类析构函数声明为
virtual(虚析构函数),这是非常重要的实践——确保通过基类指针delete派生类对象时,能正确调用派生类的析构函数,避免资源泄漏。 - 第 26-29 行:
override关键字(C++11)显式表示该函数覆盖基类的虚函数。如果基类没有对应签名的虚函数,编译器会报错,有助于避免拼写错误。 - 派生类构造函数通过初始化列表
: Animal(n)向基类构造函数传递参数。如果不显式调用,则默认调用基类的无参构造函数。
菱形继承与虚继承
当两个派生类继承同一个基类,而第三个类又多重继承这两个派生类时,就会形成菱形继承,导致基类数据重复。
菱形继承问题
#include <iostream>using namespace std;
class Animal {public: int age; Animal() : age(0) { cout << "Animal 构造" << endl; }};
// 菱形继承的两个分支class Mammal : public Animal {public: Mammal() { cout << "Mammal 构造" << endl; }};
class Bird : public Animal {public: Bird() { cout << "Bird 构造" << endl; }};
// 菱形顶端:Bat 同时继承 Mammal 和 Birdclass Bat : public Mammal, public Bird {public: Bat() { cout << "Bat 构造" << endl; }};
int main() { Bat b; // b.age = 5; // 错误!编译器不知道是 Mammal::age 还是 Bird::age(二义性) b.Mammal::age = 5; // 必须显式指定 b.Bird::age = 3;
cout << "Mammal::age = " << b.Mammal::age << endl; // 5 cout << "Bird::age = " << b.Bird::age << endl; // 3 return 0;}代码讲解:
Bat类通过Mammal和Bird两条路径继承了Animal,导致Bat中存在两份Animal的数据(两个age)。b.age产生二义性错误,必须使用b.Mammal::age或b.Bird::age显式指定。
虚继承解决菱形问题
#include <iostream>using namespace std;
class Animal {public: int age; Animal() : age(0) { cout << "Animal 构造" << endl; }};
// 虚继承:使用 virtual 关键字class Mammal : virtual public Animal {public: Mammal() { cout << "Mammal 构造" << endl; }};
class Bird : virtual public Animal {public: Bird() { cout << "Bird 构造" << endl; }};
// 虚继承后,Bat 中只有一份 Animalclass Bat : public Mammal, public Bird {public: Bat() { cout << "Bat 构造" << endl; }};
int main() { Bat b; b.age = 5; // OK!不再有二义性,只有一份 Animal
cout << "age = " << b.age << endl; // 5 cout << "Mammal::age = " << b.Mammal::age << endl; // 5 cout << "Bird::age = " << b.Bird::age << endl; // 5(同一份) return 0;}代码讲解:
- 第 14、18 行:在继承列表中添加
virtual关键字,使Mammal和Bird虚继承Animal。 - 虚继承后,
Bat中只保留一份Animal的数据,消除了二义性。 - 虚继承的底层实现使用了虚基类表(vbtable),由编译器管理,会有轻微的性能开销。
- 注意:虚继承时,最终派生类(
Bat)负责直接初始化虚基类(Animal)。
虚继承语法格式
// 虚继承语法class Derived : virtual public Base { // 派生类成员};
// 多重虚继承class Final : public Derived1, public Derived2 { // Derived1 和 Derived2 都 virtual 继承 Base // Final 中只有一份 Basepublic: Final() : Base(参数) { } // 最终派生类负责初始化虚基类};课后练习
练习 1:设计一个简单的图形类层次结构。基类 Shape 有颜色属性和计算面积的方法;派生类 Rectangle 和 Triangle 分别实现各自的面积计算。要求演示构造/析构顺序。
参考答案
#include <iostream>#include <string>using namespace std;
class Shape {protected: string color;public: Shape(string c = "白色") : color(c) { cout << "Shape 构造,颜色: " << color << endl; }
virtual ~Shape() { cout << "Shape 析构,颜色: " << color << endl; }
virtual double area() const { return 0.0; }
string getColor() const { return color; }};
class Rectangle : public Shape { double width, height;public: Rectangle(double w, double h, string c = "白色") : Shape(c), width(w), height(h) { cout << "Rectangle 构造: " << w << "x" << h << endl; }
~Rectangle() { cout << "Rectangle 析构" << endl; }
double area() const override { return width * height; }};
class Triangle : public Shape { double base, height;public: Triangle(double b, double h, string c = "白色") : Shape(c), base(b), height(h) { cout << "Triangle 构造: 底=" << b << ", 高=" << h << endl; }
~Triangle() { cout << "Triangle 析构" << endl; }
double area() const override { return 0.5 * base * height; }};
int main() { cout << "=== 创建 Rectangle ===" << endl; Rectangle rect(4, 5, "蓝色"); cout << "面积: " << rect.area() << endl; // 20
cout << "\n=== 创建 Triangle ===" << endl; Triangle tri(6, 3, "红色"); cout << "面积: " << tri.area() << endl; // 9
cout << "\n=== 离开作用域 ===" << endl; return 0; // 析构顺序:Triangle -> Shape -> Rectangle -> Shape}代码讲解:
Shape是抽象基类,提供颜色属性和虚析构函数。Rectangle和Triangle的构造函数通过初始化列表调用Shape(c)初始化基类。area()使用override覆盖基类的虚函数。- 析构顺序严格遵循”先派生后基类”规则。
练习 2:实现一个简单的”交通工具”类层次。基类 Vehicle,派生类 Car 和 ElectricCar(ElectricCar 继承自 Car)。演示多层继承的构造/析构顺序,并为每层添加特有属性。
参考答案
#include <iostream>#include <string>using namespace std;
class Vehicle {protected: string brand; int speed;public: Vehicle(string b, int s) : brand(b), speed(s) { cout << "Vehicle 构造: " << brand << ", 速度: " << speed << "km/h" << endl; }
virtual ~Vehicle() { cout << "Vehicle 析构: " << brand << endl; }
virtual void info() const { cout << brand << ", 速度: " << speed << "km/h"; }};
class Car : public Vehicle { int seats;public: Car(string b, int s, int seats) : Vehicle(b, s), seats(seats) { cout << "Car 构造: " << seats << "座" << endl; }
~Car() { cout << "Car 析构" << endl; }
void info() const override { Vehicle::info(); cout << ", " << seats << "座"; }};
class ElectricCar : public Car { double battery;public: ElectricCar(string b, int s, int seats, double battery) : Car(b, s, seats), battery(battery) { cout << "ElectricCar 构造: 电池" << battery << "kWh" << endl; }
~ElectricCar() { cout << "ElectricCar 析构" << endl; }
void info() const override { Car::info(); cout << ", 电池: " << battery << "kWh"; }
void charge() const { cout << brand << " 正在充电..." << endl; }};
int main() { cout << "=== 创建 ElectricCar ===" << endl; ElectricCar tesla("Tesla", 250, 5, 75.0); tesla.info(); // Tesla, 速度: 250km/h, 5座, 电池: 75kWh cout << endl; tesla.charge(); // Tesla 正在充电...
cout << "\n=== 离开作用域 ===" << endl; return 0; // 析构顺序:ElectricCar -> Car -> Vehicle}代码讲解:
- 三层继承结构:
Vehicle->Car->ElectricCar,每层添加特有属性。 - 构造顺序:
Vehicle->Car->ElectricCar(从最顶层基类开始)。 - 析构顺序:
ElectricCar->Car->Vehicle(从最底层派生类开始)。 info()在每层通过Base::info()调用父类实现,再追加自己的信息。
2.5 多态
知识点概述
多态是面向对象编程的三大特性之一(封装、继承、多态)。本节深入讲解运行时多态的实现机制。
核心概念
- 虚函数:用
virtual声明,允许派生类覆盖 - 虚函数表(vtable):编译器为每个含虚函数的类生成虚函数表,实现动态绑定
- 纯虚函数:
virtual 返回类型 函数名(参数) = 0,定义抽象类 - final:禁止覆盖或禁止继承
- override:显式标记函数覆盖(C++11)
- 动态多态条件:继承 + 虚函数 + 基类指针/引用指向派生类对象
虚函数语法格式
class Base {public: // 虚函数声明 virtual 返回类型 函数名(参数列表);
// 虚函数定义 virtual 返回类型 函数名(参数列表) { // 函数体 }
// 虚析构函数(强烈建议:基类析构函数声明为 virtual) virtual ~Base();
// 使用 default 指定默认虚析构函数(C++11) virtual ~Base() = default;};动态多态基础示例
#include <iostream>#include <string>#include <cmath>using namespace std;
class Shape {public: // 纯虚函数:使 Shape 成为抽象类 virtual double area() const = 0;
// 虚函数:有默认实现,派生类可选择覆盖 virtual void draw() const { cout << "绘制图形" << endl; }
// 虚析构函数 virtual ~Shape() = default;};
class Circle : public Shape { double radius;public: Circle(double r) : radius(r) {}
// override:覆盖纯虚函数 double area() const override { return 3.14159 * radius * radius; }
// override:覆盖虚函数 void draw() const override { cout << "绘制圆形, r=" << radius << endl; }};
class Rectangle : public Shape { double width, height;public: Rectangle(double w, double h) : width(w), height(h) {}
double area() const override { return width * height; }
void draw() const override { cout << "绘制矩形, " << width << "x" << height << endl; }};
int main() { // 基类指针指向派生类对象——动态多态的核心 Shape *s1 = new Circle(5.0); Shape *s2 = new Rectangle(4.0, 6.0);
s1->draw(); // 绘制圆形, r=5 cout << s1->area() << endl; // 78.5398
s2->draw(); // 绘制矩形, 4x6 cout << s2->area() << endl; // 24
// 基类引用也可以实现多态 Circle c(3.0); Shape &ref = c; ref.draw(); // 绘制圆形, r=3
// 释放内存——虚析构函数确保正确调用派生类析构函数 delete s1; delete s2;
return 0;}代码讲解:
- 第 10 行:
virtual double area() const = 0声明纯虚函数。包含纯虚函数的类称为抽象类,不能直接实例化(Shape s;会编译错误)。 - 第 14 行:
virtual void draw() const是普通虚函数,提供了默认实现。派生类可以选择覆盖或直接使用。 - 第 19 行:
virtual ~Shape() = default声明虚析构函数并使用默认实现。这确保通过基类指针delete时,能正确调用派生类的析构函数。 - 第 44-55 行:这是动态多态的核心——基类指针
Shape*指向派生类对象,调用draw()时,实际执行的是派生类的版本。编译器通过**虚函数表(vtable)**在运行时决定调用哪个函数。 override关键字(C++11)告诉编译器”这个函数要覆盖基类的虚函数”,如果基类没有对应的虚函数,编译器报错,避免拼写错误。
纯虚函数与抽象类语法格式
// 纯虚函数声明语法virtual 返回类型 函数名(参数列表) = 0;
// 抽象类示例class AbstractClass {public: // 至少有一个纯虚函数 virtual void pureVirtualFunc() = 0;
virtual ~AbstractClass() = default;};
// 抽象类不能实例化// AbstractClass obj; // 编译错误!
// 派生类必须实现所有纯虚函数才能实例化class ConcreteClass : public AbstractClass {public: void pureVirtualFunc() override { // 实现纯虚函数 }};虚函数表(vtable)原理
#include <iostream>using namespace std;
class Base {public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; }};
class Derived : public Base {public: void func1() override { cout << "Derived::func1" << endl; } // func2 不覆盖,继承 Base 的版本};
int main() { Base b; Derived d;
Base *ptr = &d;
cout << "--- 指向 Base 对象 ---" << endl; Base *ptr1 = &b; ptr1->func1(); // Base::func1 ptr1->func2(); // Base::func2
cout << "--- 指向 Derived 对象 ---" << endl; Base *ptr2 = &d; ptr2->func1(); // Derived::func1(运行时动态绑定) ptr2->func2(); // Base::func2 (继承的版本)
return 0;}代码讲解:
- 编译器为每个含虚函数的类生成一个虚函数表(vtable),其中存储了该类所有虚函数的地址。
- 每个对象内部有一个虚表指针(vptr),指向其所属类的 vtable。
Base的 vtable:{&Base::func1, &Base::func2}Derived的 vtable:{&Derived::func1, &Base::func2}(func1被覆盖,func2继承)- 调用
ptr2->func1()时,程序先通过ptr2的 vptr 找到Derived的 vtable,再从中取出func1的地址并调用——这就是**动态绑定(后期绑定)**的过程。 - 虚函数调用比普通函数调用多了一次间接寻址(通过 vtable),因此有轻微的性能开销。
final 关键字
final 有两个用途:禁止函数覆盖、禁止类继承。
final 语法格式
// 1. 修饰类:禁止被继承class FinalClass final { // ...};
// class Derived : public FinalClass { }; // 编译错误!
// 2. 修饰虚函数:禁止被覆盖class Base {public: virtual void func() final; // 派生类不能再覆盖 func};
class Derived : public Base { // void func() override; // 编译错误!func 已标记为 final};final 完整示例
#include <iostream>using namespace std;
class Animal {public: virtual void speak() const { cout << "..." << endl; }
// 标记为 final:派生类不能再覆盖 virtual void breathe() const final { cout << "呼吸中..." << endl; }
virtual ~Animal() = default;};
class Dog : public Animal {public: void speak() const override { cout << "汪汪!" << endl; }
// void breathe() const override { } // 编译错误!breathe 已是 final};
// 标记为 final:该类不能再被继承class FinalDog final : public Animal {public: void speak() const override { cout << "最后的汪汪!" << endl; }};
// class UltraDog : public FinalDog { }; // 编译错误!FinalDog 不能被继承
int main() { Dog dog; dog.speak(); // 汪汪! dog.breathe(); // 呼吸中...
FinalDog fd; fd.speak(); // 最后的汪汪! fd.breathe(); // 呼吸中...
return 0;}代码讲解:
- 第 11 行:
virtual void breathe() const final将breathe标记为final,任何派生类都不能再覆盖这个函数。 - 第 25 行:
class FinalDog final将整个类标记为final,该类不能再作为基类被继承。 final用于设计上不应该被修改的接口,增强代码的安全性和可维护性。
override 关键字
override(C++11)用于显式标记派生类中覆盖基类虚函数的函数,帮助编译器检查错误。
override 语法格式
class Base {public: virtual void func(int x); virtual void display() const;};
class Derived : public Base {public: // 正确覆盖:签名完全匹配 void func(int x) override;
// 编译错误!签名不匹配(缺少 const) // void display() override;
// 编译错误!基类没有这个虚函数 // void print() override;};动态多态的三个必要条件
#include <iostream>#include <vector>#include <memory>using namespace std;
class Shape {public: virtual double area() const = 0; virtual void draw() const = 0; virtual ~Shape() = default;};
class Circle : public Shape { double radius;public: Circle(double r) : radius(r) {} double area() const override { return 3.14159 * radius * radius; } void draw() const override { cout << "○ 圆形 r=" << radius << endl; }};
class Square : public Shape { double side;public: Square(double s) : side(s) {} double area() const override { return side * side; } void draw() const override { cout << "□ 正方形 side=" << side << endl; }};
// 多态的三大条件:// 1. 继承关系(Circle/Square 继承 Shape)// 2. 虚函数(area/draw 声明为 virtual)// 3. 基类指针或引用指向派生类对象(Shape* 指向 Circle/Square)
int main() { // 使用基类指针数组统一管理不同类型的图形 vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(3.0)); shapes.push_back(make_unique<Square>(4.0)); shapes.push_back(make_unique<Circle>(1.5)); shapes.push_back(make_unique<Square>(2.5));
double totalArea = 0; for (const auto &s : shapes) { s->draw(); // 运行时多态:根据实际类型调用 cout << " 面积: " << s->area() << endl; totalArea += s->area(); }
cout << "\n总面积: " << totalArea << endl; return 0;}代码讲解:
- 条件 1——继承关系:
Circle和Square都继承自Shape。 - 条件 2——虚函数:
area()和draw()在基类中声明为virtual。 - 条件 3——基类指针/引用:
vector<unique_ptr<Shape>>存储基类智能指针,每个指针实际指向不同的派生类对象。 - 第 40-44 行:在循环中通过基类指针调用
draw()和area(),程序在运行时根据对象的实际类型(Circle或Square)决定调用哪个版本。 - 使用
unique_ptr(智能指针)管理内存,离开作用域时自动释放,避免内存泄漏。 - 这种设计使得添加新图形类型时,
main函数的循环代码完全不需要修改——体现了开闭原则。
虚析构函数的重要性
#include <iostream>using namespace std;
class Base {public: // 如果不声明为 virtual,delete 基类指针时只调用 Base 的析构函数 virtual ~Base() { cout << "Base 析构" << endl; }};
class Derived : public Base { int *data;public: Derived() : data(new int[100]) { cout << "Derived 构造,分配了资源" << endl; }
~Derived() { delete[] data; cout << "Derived 析构,释放了资源" << endl; }};
int main() { Base *p = new Derived(); // 通过基类指针创建派生类对象
delete p; // 有 virtual:先 Derived 析构 -> 再 Base 析构 ✓ // 无 virtual:只 Base 析构 ✗(Derived 的资源泄漏!) return 0;}代码讲解:
- 如果基类析构函数不是
virtual:delete p只调用Base::~Base(),Derived::~Derived()不会被调用,data指向的内存泄漏。 - 如果基类析构函数是
virtual:delete p先调用Derived::~Derived()(释放data),再调用Base::~Base()。 - 经验法则:只要类可能被继承且通过基类指针管理对象生命周期,基类的析构函数就必须声明为
virtual。
课后练习
练习 1:设计一个简单的”图形编辑器”。定义抽象基类 Shape,包含纯虚函数 area()、draw() 和 perimeter()。派生类 Circle、Rectangle、Triangle 分别实现这三个方法。使用 vector<Shape*> 存储多个图形,计算总面积和总周长。
参考答案
#include <iostream>#include <vector>#include <cmath>#include <memory>using namespace std;
class Shape {public: virtual double area() const = 0; virtual double perimeter() const = 0; virtual void draw() const = 0; virtual ~Shape() = default;};
class Circle : public Shape { double radius;public: Circle(double r) : radius(r) {}
double area() const override { return 3.14159265 * radius * radius; }
double perimeter() const override { return 2 * 3.14159265 * radius; }
void draw() const override { cout << "○ 圆形, 半径=" << radius << ", 面积=" << area() << ", 周长=" << perimeter() << endl; }};
class Rectangle : public Shape { double width, height;public: Rectangle(double w, double h) : width(w), height(h) {}
double area() const override { return width * height; }
double perimeter() const override { return 2 * (width + height); }
void draw() const override { cout << "□ 矩形, " << width << "x" << height << ", 面积=" << area() << ", 周长=" << perimeter() << endl; }};
class Triangle : public Shape { double a, b, c; // 三条边public: Triangle(double a, double b, double c) : a(a), b(b), c(c) {}
double perimeter() const override { return a + b + c; }
double area() const override { // 海伦公式 double s = perimeter() / 2; return sqrt(s * (s - a) * (s - b) * (s - c)); }
void draw() const override { cout << "△ 三角形, 边长=" << a << ", " << b << ", " << c << ", 面积=" << area() << ", 周长=" << perimeter() << endl; }};
int main() { vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(3.0)); shapes.push_back(make_unique<Rectangle>(4.0, 5.0)); shapes.push_back(make_unique<Triangle>(3.0, 4.0, 5.0));
cout << "=== 图形列表 ===" << endl; double totalArea = 0, totalPerimeter = 0; for (const auto &s : shapes) { s->draw(); totalArea += s->area(); totalPerimeter += s->perimeter(); }
cout << "\n=== 统计 ===" << endl; cout << "图形总数: " << shapes.size() << endl; cout << "总面积: " << totalArea << endl; cout << "总周长: " << totalPerimeter << endl;
return 0;}代码讲解:
Shape是抽象基类,定义了三个纯虚函数接口:area()、perimeter()、draw()。Circle、Rectangle、Triangle分别实现了各自的计算逻辑。Triangle使用海伦公式计算面积:sqrt(s*(s-a)*(s-b)*(s-c)),其中s是半周长。- 通过
vector<unique_ptr<Shape>>统一管理不同类型的图形,遍历时利用多态自动调用对应类型的实现。 unique_ptr自动管理内存,无需手动delete。
练习 2:在练习 1 的基础上,添加一个 ShapeDecorator(图形装饰器)类,它继承自 Shape 并持有一个 Shape* 成员。装饰器在 draw() 时先调用被装饰图形的 draw(),再输出装饰效果(如边框)。演示装饰器模式的用法。
参考答案
#include <iostream>#include <vector>#include <cmath>#include <memory>using namespace std;
class Shape {public: virtual double area() const = 0; virtual double perimeter() const = 0; virtual void draw() const = 0; virtual ~Shape() = default;};
class Circle : public Shape { double radius;public: Circle(double r) : radius(r) {} double area() const override { return 3.14159265 * radius * radius; } double perimeter() const override { return 2 * 3.14159265 * radius; } void draw() const override { cout << "○ 圆形, r=" << radius; }};
class Rectangle : public Shape { double width, height;public: Rectangle(double w, double h) : width(w), height(h) {} double area() const override { return width * height; } double perimeter() const override { return 2 * (width + height); } void draw() const override { cout << "□ 矩形, " << width << "x" << height; }};
// 装饰器:继承 Shape,包装另一个 Shapeclass BorderDecorator : public Shape { const Shape *inner; // 被装饰的对象(不拥有所有权) string borderColor;public: BorderDecorator(const Shape *s, string color = "红色") : inner(s), borderColor(color) {}
double area() const override { return inner->area(); // 委托给内部对象 }
double perimeter() const override { return inner->perimeter(); }
void draw() const override { cout << "╔══════════╗" << endl; cout << "║ "; inner->draw(); // 先绘制原始图形 cout << " ║" << endl; cout << "╚══════════╝ (边框颜色: " << borderColor << ")" << endl; }};
class ShadowDecorator : public Shape { const Shape *inner;public: ShadowDecorator(const Shape *s) : inner(s) {}
double area() const override { return inner->area(); } double perimeter() const override { return inner->perimeter(); }
void draw() const override { cout << " ████████ [阴影]" << endl; cout << " "; inner->draw(); cout << endl; }};
int main() { Circle circle(3.0); Rectangle rect(4.0, 5.0);
cout << "=== 原始图形 ===" << endl; circle.draw(); cout << endl; rect.draw(); cout << endl;
cout << "\n=== 添加边框装饰 ===" << endl; BorderDecorator borderedCircle(&circle, "蓝色"); borderedCircle.draw();
cout << "\n=== 添加阴影装饰 ===" << endl; ShadowDecorator shadowRect(&rect); shadowRect.draw();
cout << "\n=== 双重装饰 ===" << endl; BorderDecorator borderedShadow(new ShadowDecorator(&circle), "金色"); borderedShadow.draw();
return 0;}代码讲解:
BorderDecorator继承Shape,通过组合持有另一个Shape指针,这是装饰器模式的经典实现。- 装饰器的
draw()先调用内部对象的draw(),再添加装饰效果(边框/阴影)。 - 装饰器可以嵌套使用(双重装饰示例),实现灵活的功能扩展。
- 装饰器模式的优势:不需要修改原有类的代码即可动态添加功能,符合开闭原则。
3.1 运算符重载
知识点概述
运算符重载(Operator Overloading)是 C++ 的重要特性之一,它允许我们为自定义类型(类/结构体)赋予运算符新的含义,使得自定义类型的对象可以像内置类型一样使用 +、-、<<、>> 等运算符,从而编写出更自然、更易读的代码。
本节核心概念:
- 运算符重载的语法与规则
- 成员函数 vs 友元函数的选择策略
- 输入输出运算符的重载
- 自增自减运算符的前置与后置区别
- 不能重载的运算符列表
- 重载的注意事项与最佳实践
3.1.1 运算符重载语法
运算符重载的本质是定义一个特殊的函数,函数名为 operator 加上要重载的运算符符号。它可以用成员函数或**友元函数(非成员函数)**两种方式实现。
语法格式
// 成员函数方式:左操作数必须是当前类的对象返回类型 operator运算符(参数列表) { // 函数体}
// 友元函数方式:左操作数可以是其他类型(如 ostream)friend 返回类型 operator运算符(参数列表) { // 函数体}完整示例:复数类的运算符重载
#include <iostream>using namespace std;
// 复数类:演示运算符重载的基本用法class Complex {private: double real; // 实部 double imag; // 虚部
public: // 构造函数,带有默认参数 Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载 + 运算符(成员函数方式) // 返回一个新的 Complex 对象,不影响原对象 Complex operator+(const Complex &c) const { return Complex(real + c.real, imag + c.imag); }
// 重载 += 运算符(成员函数方式) // 返回引用以支持链式调用,如 a += b += c Complex& operator+=(const Complex &c) { real += c.real; imag += c.imag; return *this; // 返回当前对象自身的引用 }
// 重载前缀 ++ 运算符(++obj) // 前缀版本返回引用,直接修改对象后返回 Complex& operator++() { ++real; ++imag; return *this; }
// 重载后缀 ++ 运算符(obj++) // 后缀版本返回值的副本(非引用),参数中 int 仅用于区分前缀和后缀 Complex operator++(int) { Complex temp = *this; // 保存修改前的状态 ++real; ++imag; return temp; // 返回修改前的副本 }
// 声明友元函数,重载 << 运算符(输出流) friend ostream& operator<<(ostream &os, const Complex &c); // 声明友元函数,重载 >> 运算符(输入流) friend istream& operator>>(istream &is, Complex &c);};
// 重载 << 运算符:实现复数的格式化输出ostream& operator<<(ostream &os, const Complex &c) { os << c.real; if (c.imag >= 0) os << "+"; os << c.imag << "i"; return os; // 返回 ostream 引用以支持链式输出}
// 重载 >> 运算符:实现复数的输入istream& operator>>(istream &is, Complex &c) { is >> c.real >> c.imag; return is;}
int main() { Complex a(1, 2), b(3, 4);
// 使用重载的 + 运算符 cout << a + b << endl; // 输出: 4+6i
// 使用重载的 += 运算符 a += b; cout << "a += b: " << a << endl; // 输出: a += b: 4+6i
// 使用重载的前缀 ++ 和后缀 ++ Complex c(1, 1); cout << "c原始值: " << c << endl; // 输出: 1+1i cout << "后缀c++: " << c++ << endl; // 输出: 1+1i(返回旧值) cout << "c新值: " << c << endl; // 输出: 2+2i(已自增) cout << "前缀++c: " << ++c << endl; // 输出: 3+3i(返回新值)
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
Complex(double r = 0, double i = 0) | 构造函数使用默认参数,既可以无参构造 Complex(),也可以有参构造 Complex(1, 2) |
Complex operator+(const Complex &c) const | 重载 + 运算符。const Complex &c 是右操作数的常引用,避免拷贝;末尾的 const 表示该函数不修改当前对象(左操作数) |
return Complex(real + c.real, imag + c.imag) | 创建并返回一个新的 Complex 对象,实部和虚部分别相加,原对象不变 |
Complex& operator+=(const Complex &c) | 重载 += 运算符。返回类型是 Complex&(引用),允许链式调用 a += b += c |
return *this | this 是指向当前对象的指针,*this 就是当前对象本身。返回引用以便链式调用 |
Complex& operator++() | 前缀自增 ++obj。返回引用,先修改再返回,效率更高 |
Complex operator++(int) | 后缀自增 obj++。参数 int 是编译器的约定,仅用于区分前缀/后缀版本,实际调用时不传参。返回临时副本,效率略低 |
friend ostream& operator<<(ostream &os, const Complex &c) | 声明 << 为友元函数。必须用友元,因为左操作数是 ostream 类型,不是 Complex |
os << c.real << (c.imag >= 0 ? "+" : "") << c.imag << "i" | 格式化输出复数:当虚部为正数时显示 + 号,避免出现 4+-6i 的情况 |
return os | 返回 ostream 的引用,支持链式输出如 cout << a << " " << b |
3.1.2 成员函数 vs 友元函数的选择
| 比较项 | 成员函数 | 友元函数(非成员) |
|---|---|---|
| 左操作数 | 必须是当前类的对象 | 可以是任意类型 |
| 参数个数 | 运算符目数 - 1(隐含 this) | 等于运算符目数 |
| 适用场景 | +、-、=、+=、++、[]、() 等 | <<、>>、对称运算符如 double + Complex |
| 访问权限 | 可直接访问私有成员 | 需要在类中声明 friend |
选择原则:
- 必须用友元函数:当左操作数不是当前类对象时(如
ostream << obj) - 必须用成员函数:
=、[]、()、->、type()只能作为成员函数重载 - 推荐友元函数:对称二元运算符(如
a + b和b + a),可以避免隐式类型转换的限制
友元函数实现对称运算符的示例
#include <iostream>using namespace std;
class Complex {private: double real, imag;public: Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 成员函数:Complex + Complex Complex operator+(const Complex &c) const { return Complex(real + c.real, imag + c.imag); }
// 友元函数:double + Complex // 成员函数无法实现此场景(左操作数是 double,不是 Complex) friend Complex operator+(double d, const Complex &c);};
// 友元函数定义:实现 double + ComplexComplex operator+(double d, const Complex &c) { return Complex(d + c.real, c.imag);}
int main() { Complex a(1, 2);
cout << a + 3.0 << endl; // 成员函数调用: 4+2i cout << 3.0 + a << endl; // 友元函数调用: 4+2i
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
Complex operator+(const Complex &c) const | 成员函数重载 +,处理 Complex + Complex 和 Complex + double(double 会隐式构造为 Complex)的情况 |
friend Complex operator+(double d, const Complex &c) | 声明友元函数以实现 double + Complex。因为左操作数 d 是 double 类型,无法调用成员函数 |
Complex(d + c.real, c.imag) | 将 double 加到实部上,虚部保持不变,返回新的 Complex 对象 |
3.1.3 不能重载的运算符
以下是 C++ 中不允许重载的运算符:
| 运算符 | 说明 |
|---|---|
. | 成员访问运算符 |
.* | 成员指针访问运算符 |
:: | 作用域解析运算符 |
?: | 三目条件运算符 |
sizeof | 获取类型/对象大小 |
typeid | 运行时类型识别 |
重载注意事项:
- 不能改变运算符的优先级和结合性
- 不能改变运算符的操作数个数(一元运算符不能变成二元)
- 不能发明新的运算符符号
- 重载运算符应保持语义一致性(
+应该做加法,不应该做减法) - 至少有一个操作数必须是用户自定义类型
课后练习
练习 1:为 Vector2D 类重载 +、-、*(标量乘法)和 << 运算符。
#include <iostream>using namespace std;
class Vector2D {private: double x, y;public: Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// 请在此处补充:重载 + 运算符
// 请在此处补充:重载 - 运算符
// 请在此处补充:重载 * 运算符(Vector2D * double)
friend ostream& operator<<(ostream &os, const Vector2D &v);};
ostream& operator<<(ostream &os, const Vector2D &v) { os << "(" << v.x << ", " << v.y << ")"; return os;}
int main() { Vector2D a(1, 2), b(3, 4); cout << "a + b = " << (a + b) << endl; // 期望输出: (4, 6) cout << "a - b = " << (a - b) << endl; // 期望输出: (-2, -2) cout << "a * 3 = " << (a * 3) << endl; // 期望输出: (3, 6) return 0;}参考答案
// 重载 + 运算符Vector2D operator+(const Vector2D &v) const { return Vector2D(x + v.x, y + v.y);}
// 重载 - 运算符Vector2D operator-(const Vector2D &v) const { return Vector2D(x - v.x, y - v.y);}
// 重载 * 运算符(Vector2D * double)Vector2D operator*(double scalar) const { return Vector2D(x * scalar, y * scalar);}练习 2:实现一个简单的 String 类,重载 == 运算符判断两个字符串是否相等。
参考答案
#include <iostream>#include <cstring>using namespace std;
class String {private: char *data;public: String(const char *s = "") { data = new char[strlen(s) + 1]; strcpy(data, s); } ~String() { delete[] data; }
// 重载 == 运算符 bool operator==(const String &other) const { return strcmp(data, other.data) == 0; }};
int main() { String s1("hello"), s2("hello"), s3("world"); cout << (s1 == s2 ? "相等" : "不等") << endl; // 输出: 相等 cout << (s1 == s3 ? "相等" : "不等") << endl; // 输出: 不等 return 0;}3.2 模板编程
知识点概述
模板编程(Template Programming)是 C++ 实现泛型编程的核心机制。通过模板,我们可以编写与类型无关的代码,一份代码可以适用于多种数据类型,大大提高了代码的复用性和灵活性。
本节核心概念:
- 函数模板:定义类型参数化的函数
- 类模板:定义类型参数化的类
- 模板特化:为特定类型提供特殊实现
- 模板类与友元的配合使用
3.2.1 函数模板
函数模板允许我们编写一个通用的函数,它可以处理不同类型的数据,而不需要为每种类型都写一个单独的函数。
语法格式
template <typename 类型参数1, typename 类型参数2, ...>返回类型 函数名(参数列表) { // 函数体}说明:
typename也可以用class替代,两者在模板参数声明中含义相同。
完整示例:通用的 getMax 函数
#include <iostream>#include <string>using namespace std;
// 函数模板:获取两个值中的最大值// T 是类型参数,编译器在调用时自动推导实际类型template <typename T>T getMax(T a, T b) { return (a > b) ? a : b;}
// 多类型参数的函数模板示例template <typename T1, typename T2>void printPair(T1 first, T2 second) { cout << "(" << first << ", " << second << ")" << endl;}
int main() { // 编译器自动推导 T 为 int cout << getMax(3, 7) << endl; // 输出: 7
// 编译器自动推导 T 为 double cout << getMax(3.14, 2.72) << endl; // 输出: 3.14
// 编译器自动推导 T 为 string cout << getMax(string("apple"), string("banana")) << endl; // 输出: banana
// 显式指定模板参数(当编译器无法自动推导时使用) cout << getMax<double>(3, 3.14) << endl; // 输出: 3.14
// 多类型参数 printPair(1, "hello"); // 输出: (1, hello) printPair(3.14, 42); // 输出: (3.14, 42)
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
template <typename T> | 声明一个模板参数 T,T 代表任意类型。编译器会根据调用时的实参自动推导 T 的具体类型 |
T getMax(T a, T b) | 函数返回值和两个参数都使用类型 T。当传入 int 时,T 变为 int;传入 string 时,T 变为 string |
return (a > b) ? a : b | 使用三元运算符返回较大值。这要求类型 T 必须支持 > 运算符比较 |
template <typename T1, typename T2> | 声明两个不同的类型参数 T1 和 T2,可以接受不同类型的参数 |
getMax<double>(3, 3.14) | 显式指定模板参数为 double。编译器会将 3 隐式转换为 double,避免模板推导失败 |
3.2.2 类模板
类模板允许我们定义一个通用的类,其中的成员变量和成员函数可以使用类型参数。实例化类模板时,需要显式指定具体的类型。
语法格式
template <typename 类型参数>class 类名 { // 类体中使用类型参数};
// 实例化类模板类名<具体类型> 对象名;完整示例:泛型栈(Stack)类
#include <iostream>#include <string>using namespace std;
// 类模板:泛型栈,可以存储任意类型的元素template <typename T>class Stack {private: T *data; // 动态数组,存储栈元素 int top; // 栈顶指针,-1 表示栈为空 int capacity; // 栈的最大容量
public: // 构造函数:初始化栈,默认容量为 100 Stack(int cap = 100) : top(-1), capacity(cap) { data = new T[capacity]; }
// 析构函数:释放动态分配的内存 ~Stack() { delete[] data; }
// 入栈操作:将元素压入栈顶 void push(const T &val) { if (top >= capacity - 1) { cout << "栈已满!" << endl; return; } data[++top] = val; }
// 出栈操作:弹出并返回栈顶元素 T pop() { if (isEmpty()) { cout << "栈为空!" << endl; return T(); // 返回类型的默认值 } return data[top--]; }
// 查看栈顶元素(不弹出) T peek() const { if (isEmpty()) { cout << "栈为空!" << endl; return T(); } return data[top]; }
// 判断栈是否为空 bool isEmpty() const { return top == -1; }
// 获取栈中元素个数 int size() const { return top + 1; }};
int main() { // 实例化 int 类型的栈 Stack<int> intStack;
intStack.push(10); intStack.push(20); intStack.push(30);
cout << "栈顶元素: " << intStack.peek() << endl; // 输出: 30 cout << "弹出: " << intStack.pop() << endl; // 输出: 30 cout << "弹出: " << intStack.pop() << endl; // 输出: 20 cout << "栈大小: " << intStack.size() << endl; // 输出: 1
// 实例化 string 类型的栈 Stack<string> strStack;
strStack.push("Hello"); strStack.push("World");
cout << "弹出: " << strStack.pop() << endl; // 输出: World cout << "弹出: " << strStack.pop() << endl; // 输出: Hello
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
template <typename T> | 声明类模板的类型参数 T,整个类都可以使用 T 作为类型占位符 |
T *data | 声明一个 T 类型的指针,用于动态分配数组存储栈元素 |
data = new T[capacity] | 使用 new 动态分配 capacity 个 T 类型的元素空间 |
delete[] data | 析构函数中释放动态分配的数组,防止内存泄漏 |
data[++top] = val | 先将 top 加 1,然后将 val 存入新的栈顶位置 |
return data[top--] | 先返回当前栈顶元素的值,然后将 top 减 1(即后缀 --) |
return T() | 返回类型 T 的默认值。对于 int 返回 0,对于 string 返回 "" |
Stack<int> intStack | 实例化类模板:将 T 替换为 int,创建一个存储整数的栈 |
Stack<string> strStack | 实例化类模板:将 T 替换为 string,创建一个存储字符串的栈 |
3.2.3 模板特化
有时我们需要为某个特定类型提供与通用模板不同的实现,这时可以使用模板特化。
语法格式
// 全特化语法:为某个具体类型提供专门的实现template <> // 空的模板参数列表,表示这是全特化返回类型 函数名<具体类型>(参数列表) { // 针对具体类型的特殊实现}完整示例:为 char* 类型特化 getMax 函数
#include <iostream>#include <cstring>using namespace std;
// 通用模板template <typename T>T getMax(T a, T b) { return (a > b) ? a : b;}
// 全特化版本:针对 const char*(C 风格字符串)类型// 比较字符串内容而非指针地址template <>const char* getMax<const char*>(const char* a, const char* b) { return (strcmp(a, b) > 0) ? a : b;}
int main() { cout << getMax(3, 7) << endl; // 输出: 7(使用通用模板) cout << getMax("apple", "banana") << endl; // 输出: banana(使用特化版本)
// 注意:如果不用特化,"apple" 和 "banana" 比较的是指针地址,结果不确定 return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
template <> | 空的模板参数列表,告诉编译器这是一个全特化(Full Specialization),为特定类型提供定制实现 |
const char* getMax<const char*>(const char* a, const char* b) | 函数名后用 <const char*> 指定特化的类型。参数列表和返回值都使用具体类型 |
strcmp(a, b) > 0 | 使用 strcmp 按字典序比较两个 C 风格字符串的内容,而非比较指针地址。这是特化版本与通用版本的关键区别 |
3.2.4 模板类与友元
当类模板需要访问其他类的私有成员,或者需要将某个非成员函数声明为友元时,需要特殊处理模板类中的友元声明。
语法格式
template <typename T>class MyClass { // 方式一:将所有实例化的某类模板声明为友元 template <typename U> friend class OtherClass;
// 方式二:将特定实例化的类声明为友元 friend class OtherClass<int>;
// 方式三:将非模板函数声明为友元 friend void someFunction(MyClass<T> &obj);
// 方式四:将函数模板的某个特定实例声明为友元 friend void func<int>(MyClass<int> &obj);
// 方式五:将函数模板的所有实例声明为友元 template <typename U> friend void func(MyClass<U> &obj);};完整示例:模板类重载输出运算符
#include <iostream>using namespace std;
// 前置声明类模板template <typename T>class Pair;
// 声明友元函数模板template <typename T>ostream& operator<<(ostream &os, const Pair<T> &p);
// 类模板:存储一对值template <typename T>class Pair {private: T first, second;
public: Pair(T f, T s) : first(f), second(s) {}
// 将 operator<< 的所有实例声明为友元 template <typename U> friend ostream& operator<<(ostream &os, const Pair<U> &p);};
// 定义友元函数模板:重载 << 运算符template <typename T>ostream& operator<<(ostream &os, const Pair<T> &p) { os << "(" << p.first << ", " << p.second << ")"; return os;}
int main() { Pair<int> pi(1, 2); Pair<double> pd(3.14, 2.72); Pair<string> ps("hello", "world");
cout << pi << endl; // 输出: (1, 2) cout << pd << endl; // 输出: (3.14, 2.72) cout << ps << endl; // 输出: (hello, world)
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
template <typename T> class Pair; | 前置声明类模板,让编译器知道 Pair 是一个模板类,以便在声明友元函数时使用 |
template <typename T> ostream& operator<<(ostream &os, const Pair<T> &p); | 前置声明友元函数模板,使其可以在类内声明为友元 |
template <typename U> friend ostream& operator<<(ostream &os, const Pair<U> &p); | 在类内声明友元。注意这里使用了 U(而非 T)作为参数,使得 operator<< 的所有实例化版本都是该类对应实例的友元 |
os << "(" << p.first << ", " << p.second << ")"; | 访问 Pair 的私有成员 first 和 second,因为已被声明为友元函数 |
课后练习
练习 1:编写一个函数模板 swapValues,交换两个变量的值。
参考答案
#include <iostream>#include <string>using namespace std;
template <typename T>void swapValues(T &a, T &b) { T temp = a; a = b; b = temp;}
int main() { int x = 10, y = 20; swapValues(x, y); cout << "x=" << x << ", y=" << y << endl; // 输出: x=20, y=10
string s1 = "hello", s2 = "world"; swapValues(s1, s2); cout << "s1=" << s1 << ", s2=" << s2 << endl; // 输出: s1=world, s2=hello
return 0;}练习 2:编写一个类模板 Array<T>,实现动态数组,支持 [] 运算符随机访问和 getSize() 获取大小。
参考答案
#include <iostream>#include <stdexcept>using namespace std;
template <typename T>class Array {private: T *data; int sz;
public: Array(int size) : sz(size) { data = new T[size]; } ~Array() { delete[] data; }
// 重载 [] 运算符(非常量版本,用于修改元素) T& operator[](int index) { if (index < 0 || index >= sz) { throw out_of_range("数组下标越界!"); } return data[index]; }
// 重载 [] 运算符(常量版本,用于读取元素) const T& operator[](int index) const { if (index < 0 || index >= sz) { throw out_of_range("数组下标越界!"); } return data[index]; }
int getSize() const { return sz; }};
int main() { Array<int> arr(5); for (int i = 0; i < arr.getSize(); i++) { arr[i] = i * 10; } for (int i = 0; i < arr.getSize(); i++) { cout << arr[i] << " "; // 输出: 0 10 20 30 40 } cout << endl; return 0;}3.3 知识点补充与 C++11 新特性
知识点概述
本节补充几个重要的 C++ 编程知识点,并介绍 C++11 标准引入的若干重要新特性。这些特性极大地提升了 C++ 的表达能力、安全性和编程效率。
本节核心概念:
- 异常处理:
try、catch、throw机制 - 文件流:
ifstream和ofstream的使用 - C++11 新特性:
auto类型推导、lambda 表达式、智能指针、范围 for 循环
3.3.1 异常处理
异常处理机制允许程序在运行时检测到错误后,将错误信息传递到调用链上的适当位置进行处理,从而避免程序直接崩溃。
语法格式
try { // 可能抛出异常的代码 throw 异常对象; // 抛出异常}catch (异常类型1 &e) { // 处理异常类型1}catch (异常类型2 &e) { // 处理异常类型2}catch (...) { // 捕获所有其他异常(省略号表示任意类型)}完整示例:除法运算的异常处理
#include <iostream>#include <stdexcept>using namespace std;
// 可能抛出异常的函数double divide(double a, double b) { if (b == 0) { // 抛出标准异常对象,附带错误信息 throw runtime_error("除数不能为零!"); } return a / b;}
// 自定义异常类class NegativeInputException : public exception {private: string message;public: NegativeInputException(const string &msg) : message(msg) {} const char* what() const noexcept override { return message.c_str(); }};
// 使用自定义异常的函数double squareRoot(double x) { if (x < 0) { throw NegativeInputException("不能对负数求平方根: " + to_string(x)); } // 简单实现,实际应使用更精确的算法 double result = x; for (int i = 0; i < 10; i++) { result = (result + x / result) / 2; } return result;}
int main() { // 示例 1:捕获 runtime_error try { cout << "10 / 2 = " << divide(10, 2) << endl; // 正常执行 cout << "10 / 0 = " << divide(10, 0) << endl; // 抛出异常 } catch (const runtime_error &e) { // 捕获 runtime_error 类型异常 cerr << "运行时错误: " << e.what() << endl; // 输出错误信息 }
// 示例 2:捕获自定义异常 try { cout << squareRoot(9) << endl; // 正常执行 cout << squareRoot(-1) << endl; // 抛出自定义异常 } catch (const NegativeInputException &e) { cerr << "输入错误: " << e.what() << endl; // 输出自定义错误信息 } catch (const exception &e) { // 捕获所有标准异常的基类 cerr << "未知错误: " << e.what() << endl; } catch (...) { // 捕获所有其他类型的异常 cerr << "发生了未知异常!" << endl; }
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
throw runtime_error("除数不能为零!") | 使用 throw 关键字抛出一个 runtime_error 对象。runtime_error 是 C++ 标准库提供的异常类 |
catch (const runtime_error &e) | 捕获 runtime_error 类型的异常。使用 const 引用避免拷贝,e.what() 返回错误描述字符串 |
class NegativeInputException : public exception | 自定义异常类,继承自标准异常基类 exception,需要重写 what() 虚函数 |
const char* what() const noexcept override | 重写 exception::what() 方法。noexcept 表示此函数不会抛出异常,override 确保正确重写了基类虚函数 |
catch (...) | 省略号 ... 可以捕获任意类型的异常,作为最后的兜底处理。通常用于记录日志后重新抛出 |
cerr | 标准错误流,用于输出错误信息,默认输出到屏幕(与 cout 类似,但用于错误输出) |
3.3.2 文件流
C++ 提供了 ifstream(输入文件流)和 ofstream(输出文件流)来读写文件,使用方式与 cin 和 cout 类似。
语法格式
#include <fstream> // 包含文件流头文件
// 写文件ofstream outFile("文件名.txt");outFile << "内容" << endl;outFile.close();
// 读文件ifstream inFile("文件名.txt");string line;while (getline(inFile, line)) { // 处理每一行}inFile.close();
// 检查文件是否成功打开if (!inFile.is_open()) { cout << "文件打开失败!" << endl;}完整示例:文件的写入与读取
#include <iostream>#include <fstream>#include <string>using namespace std;
int main() { // ========== 写入文件 ========== ofstream outFile("example.txt"); // 创建/打开输出文件
if (!outFile.is_open()) { cerr << "文件创建失败!" << endl; return 1; }
outFile << "C++ 文件流示例" << endl; outFile << "第一行内容" << endl; outFile << "第二行内容" << endl; outFile << 42 << " " << 3.14 << endl; // 可以写入各种类型 outFile.close(); cout << "文件写入完成!" << endl;
// ========== 读取文件 ========== ifstream inFile("example.txt"); // 打开输入文件
if (!inFile.is_open()) { cerr << "文件打开失败!" << endl; return 1; }
cout << "\n--- 文件内容 ---" << endl;
// 方式一:逐行读取 string line; while (getline(inFile, line)) { cout << line << endl; }
inFile.close();
// ========== 追加写入 ========== ofstream appendFile("example.txt", ios::app); // ios::app 表示追加模式
if (appendFile.is_open()) { appendFile << "追加的内容" << endl; appendFile.close(); }
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
ofstream outFile("example.txt") | 创建 ofstream 对象并打开文件。如果文件不存在则自动创建;如果存在则覆盖原有内容 |
!outFile.is_open() | 检查文件是否成功打开。如果打开失败(如权限不足),返回 true |
outFile << "内容" << endl | 使用 << 运算符向文件写入数据,用法与 cout 完全一致 |
outFile.close() | 关闭文件,将缓冲区中的数据写入磁盘。虽然析构函数会自动关闭,但显式关闭是良好的编程习惯 |
ifstream inFile("example.txt") | 创建 ifstream 对象并打开文件用于读取 |
getline(inFile, line) | 从文件中逐行读取内容到字符串 line 中。当读取到文件末尾时返回 false,循环结束 |
ofstream appendFile("example.txt", ios::app) | 以追加模式打开文件。ios::app 标志确保新内容写入到文件末尾,不会覆盖原有内容 |
3.3.3 C++11 新特性
C++11 是 C++ 语言的一次重大更新,引入了许多实用的现代特性。以下介绍最常用的几个。
(1) auto 类型推导
auto 关键字让编译器自动推导变量的类型,减少冗余代码,特别是在处理复杂类型时非常方便。
#include <iostream>#include <vector>#include <map>using namespace std;
int main() { // 基本用法:编译器根据初始值自动推导类型 auto x = 42; // int auto pi = 3.14; // double auto name = string("C++11"); // string
cout << "x 的类型: int, 值: " << x << endl; cout << "pi 的类型: double, 值: " << pi << endl; cout << "name 的类型: string, 值: " << name << endl;
// 在复杂类型中特别有用 vector<int> v = {1, 2, 3, 4, 5};
// 不用 auto:vector<int>::iterator it = v.begin(); // 使用 auto:简洁清晰 for (auto it = v.begin(); it != v.end(); ++it) { cout << *it << " "; // 输出: 1 2 3 4 5 } cout << endl;
// auto 与 map 配合使用 map<string, int> ages = {{"Alice", 25}, {"Bob", 30}, {"Charlie", 35}};
// 不用 auto:map<string, int>::const_iterator iter = ages.begin(); for (auto iter = ages.begin(); iter != ages.end(); ++iter) { cout << iter->first << ": " << iter->second << endl; }
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
auto x = 42 | 编译器根据初始值 42 推导 x 的类型为 int |
auto pi = 3.14 | 编译器推导 pi 的类型为 double |
auto it = v.begin() | 推导为 vector<int>::iterator,避免书写冗长的迭代器类型名 |
auto iter = ages.begin() | 推导为 map<string, int>::const_iterator,在复杂嵌套类型中 auto 的优势更加明显 |
(2) lambda 表达式
lambda 表达式是一种定义匿名函数的方式,特别适合作为回调函数或用于 STL 算法中。
语法格式
[捕获列表](参数列表) -> 返回类型 { // 函数体}
// 返回类型通常可以省略,编译器会自动推导[捕获列表](参数列表) { // 函数体}捕获列表说明:
| 捕获方式 | 含义 | 示例 |
|---|---|---|
[] | 不捕获任何外部变量 | [](int x) { return x * 2; } |
[=] | 按值捕获所有外部变量(只读副本) | [=]() { return x + y; } |
[&] | 按引用捕获所有外部变量 | [&]() { x += 1; } |
[x] | 按值捕获变量 x | [x]() { return x; } |
[&x] | 按引用捕获变量 x | [&x]() { x += 1; } |
[=, &x] | 按值捕获其余变量,按引用捕获 x | [=, &x]() { y + x; } |
[&, x] | 按引用捕获其余变量,按值捕获 x | [&, x]() { y += x; } |
完整示例
#include <iostream>#include <vector>#include <algorithm>#include <numeric>using namespace std;
int main() { vector<int> v = {5, 3, 1, 4, 2};
// 1. 简单 lambda:降序排序 sort(v.begin(), v.end(), [](int a, int b) { return a > b; // 降序:大的在前 }); cout << "降序排序: "; for (auto &x : v) cout << x << " "; // 输出: 5 4 3 2 1 cout << endl;
// 2. 按值捕获外部变量 int threshold = 3; int count = count_if(v.begin(), v.end(), [threshold](int x) { return x > threshold; // 统计大于 threshold 的元素个数 }); cout << "大于 " << threshold << " 的元素个数: " << count << endl; // 输出: 2
// 3. 按引用捕获外部变量:修改外部变量 int sum = 0; for_each(v.begin(), v.end(), [&sum](int x) { sum += x; // 通过引用修改外部的 sum }); cout << "元素总和: " << sum << endl; // 输出: 15
// 4. 用 auto 接收 lambda(C++11 中不能直接声明 lambda 类型) auto print = [](const vector<int> &vec) { cout << "["; for (size_t i = 0; i < vec.size(); i++) { cout << vec[i]; if (i < vec.size() - 1) cout << ", "; } cout << "]" << endl; }; print(v); // 输出: [5, 4, 3, 2, 1]
// 5. 作为参数传递 vector<int> v2 = {10, 20, 30, 40, 50}; auto multiply = [](int x, int factor) { return x * factor; }; cout << "乘以2后: "; for (auto &x : v2) { cout << multiply(x, 2) << " "; // 输出: 20 40 60 80 100 } cout << endl;
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
[](int a, int b) { return a > b; } | 无捕获的 lambda,接收两个 int 参数,返回它们的比较结果(降序),用于 sort 的排序规则 |
[threshold](int x) { return x > threshold; } | 按值捕获外部变量 threshold,在 lambda 内部作为常量使用,统计大于阈值的元素个数 |
[&sum](int x) { sum += x; } | 按引用捕获 sum,lambda 内部可以直接修改外部变量 sum 的值 |
auto print = [](const vector<int> &vec) { ... } | 使用 auto 接收 lambda 表达式。lambda 的实际类型由编译器生成,无法手动写出类型名 |
multiply(x, 2) | 像普通函数一样调用 lambda。lambda 可以赋值给变量后反复调用 |
(3) 范围 for 循环
范围 for 循环(Range-based for loop)提供了一种简洁的语法来遍历容器或数组中的所有元素。
语法格式
for (元素类型 变量名 : 容器/数组) { // 循环体}
// 只读遍历for (const auto &x : container) { ... }
// 需要修改元素for (auto &x : container) { ... }
// 值拷贝(不修改原容器,但效率较低)for (auto x : container) { ... }完整示例
#include <iostream>#include <vector>#include <string>#include <map>using namespace std;
int main() { // 遍历 vector vector<string> fruits = {"apple", "banana", "cherry"};
cout << "水果列表(只读): "; for (const auto &fruit : fruits) { cout << fruit << " "; // 输出: apple banana cherry } cout << endl;
// 遍历并修改元素(使用引用) cout << "转大写: "; for (auto &fruit : fruits) { for (char &c : fruit) { c = toupper(c); } cout << fruit << " "; // 输出: APPLE BANANA CHERRY } cout << endl;
// 遍历普通数组 int arr[] = {10, 20, 30, 40, 50}; cout << "数组元素: "; for (auto x : arr) { cout << x << " "; // 输出: 10 20 30 40 50 } cout << endl;
// 遍历 map map<string, int> scores = {{"数学", 95}, {"语文", 88}, {"英语", 92}}; cout << "成绩单:" << endl; for (const auto &pair : scores) { cout << " " << pair.first << ": " << pair.second << " 分" << endl; }
// 配合 auto 和 lambda 使用 vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6}; sort(nums.begin(), nums.end()); // 先排序 cout << "排序后: "; for (auto &x : nums) cout << x << " "; // 输出: 1 1 2 3 4 5 6 9 cout << endl;
// 使用范围 for + lambda 筛选偶数 cout << "偶数: "; for (auto &x : nums) { auto isEven = [](int n) { return n % 2 == 0; }; if (isEven(x)) cout << x << " "; // 输出: 2 4 6 } cout << endl;
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
for (const auto &fruit : fruits) | const auto & 表示按常量引用遍历,既避免了拷贝开销,又保证不会误修改元素 |
for (auto &fruit : fruits) | auto & 表示按引用遍历,可以在循环中修改容器元素的值 |
for (auto x : arr) | auto x 是值拷贝方式,修改 x 不会影响原数组。对于简单类型(如 int)开销很小 |
for (const auto &pair : scores) | 遍历 map 时,每个元素是 pair<const string, int> 类型,通过 pair.first 和 pair.second 分别访问键和值 |
auto isEven = [](int n) { return n % 2 == 0; }; | 在循环内部定义 lambda 并用 auto 接收,然后在 if 条件中调用 |
(4) 智能指针
智能指针(Smart Pointer)是 C++11 引入的内存管理工具,它自动管理动态分配的内存,在对象不再使用时自动释放,有效避免内存泄漏。
语法格式
#include <memory>
// unique_ptr:独占所有权,不可复制,只能移动unique_ptr<类型> p = make_unique<类型>(构造参数);
// shared_ptr:共享所有权,引用计数管理,最后一个引用释放时销毁对象shared_ptr<类型> p = make_shared<类型>(构造参数);
// weak_ptr:弱引用,不增加引用计数,用于解决 shared_ptr 的循环引用问题weak_ptr<类型> wp = p;完整示例
#include <iostream>#include <memory>#include <vector>using namespace std;
class Student {private: string name; int score;public: Student(const string &n, int s) : name(n), score(s) { cout << "构造: " << name << endl; } ~Student() { cout << "析构: " << name << endl; } void show() const { cout << name << " (成绩: " << score << ")" << endl; }};
int main() { // ========== unique_ptr:独占式智能指针 ========== cout << "=== unique_ptr ===" << endl;
// 创建 unique_ptr(推荐使用 make_unique,C++14 引入) unique_ptr<Student> p1 = make_unique<Student>("Alice", 95); p1->show(); // 输出: Alice (成绩: 95)
// unique_ptr 不支持拷贝,但支持移动 unique_ptr<Student> p2 = move(p1); // p1 的所有权转移给 p2 // p1->show(); // 错误!p1 现在为 nullptr p2->show(); // 输出: Alice (成绩: 95)
// p2 离开作用域时自动调用 Student 的析构函数
// ========== shared_ptr:共享式智能指针 ========== cout << "\n=== shared_ptr ===" << endl;
// 创建 shared_ptr shared_ptr<Student> sp1 = make_shared<Student>("Bob", 88); cout << "引用计数: " << sp1.use_count() << endl; // 输出: 1
{ shared_ptr<Student> sp2 = sp1; // 共享所有权,引用计数 +1 cout << "引用计数: " << sp1.use_count() << endl; // 输出: 2 sp2->show(); // 输出: Bob (成绩: 88) } // sp2 离开作用域,引用计数 -1
cout << "引用计数: " << sp1.use_count() << endl; // 输出: 1
// 使用 shared_ptr 管理容器中的对象 vector<shared_ptr<Student>> students; students.push_back(make_shared<Student>("Charlie", 92)); students.push_back(make_shared<Student>("Diana", 87));
cout << "\n学生列表:" << endl; for (const auto &s : students) { s->show(); // 输出每个学生的信息 }
// 所有智能指针离开作用域后自动释放内存 cout << "\n=== 程序结束,自动释放内存 ===" << endl;
return 0;}代码讲解
| 行号/代码段 | 解释 |
|---|---|
make_unique<Student>("Alice", 95) | 创建 unique_ptr,在堆上构造 Student 对象。make_unique 比 new 更安全(避免异常安全问题) |
unique_ptr<Student> p2 = move(p1) | unique_ptr 不可拷贝(编译报错),只能通过 move 转移所有权。转移后 p1 变为 nullptr |
p2->show() | 通过 -> 运算符访问智能指针指向对象的成员函数,用法与原始指针一致 |
make_shared<Student>("Bob", 88) | 创建 shared_ptr。make_shared 一次性分配对象和控制块内存,比 new 更高效 |
sp1.use_count() | 返回当前有多少个 shared_ptr 指向同一个对象(引用计数) |
shared_ptr<Student> sp2 = sp1 | sp1 和 sp2 共享同一个对象,引用计数增加到 2。当 sp2 离开作用域后,引用计数减为 1 |
vector<shared_ptr<Student>> | 在容器中存储智能指针,容器析构时会自动释放所有对象,避免手动 delete |
课后练习
练习 1:编写一个函数 readFileContent,读取指定文件的内容并返回字符串。如果文件不存在或打开失败,抛出异常。
参考答案
#include <iostream>#include <fstream>#include <string>#include <stdexcept>using namespace std;
string readFileContent(const string &filename) { ifstream inFile(filename); if (!inFile.is_open()) { throw runtime_error("无法打开文件: " + filename); }
string content; string line; while (getline(inFile, line)) { content += line + "\n"; } inFile.close(); return content;}
int main() { try { string content = readFileContent("example.txt"); cout << "文件内容:\n" << content; } catch (const runtime_error &e) { cerr << "错误: " << e.what() << endl; } return 0;}练习 2:使用 lambda 表达式和 shared_ptr 实现以下功能:创建一个 shared_ptr 管理的整数数组,使用 lambda 计算数组中所有偶数的和。
参考答案
#include <iostream>#include <memory>#include <vector>using namespace std;
int main() { // 使用 shared_ptr 管理动态数组 int size = 6; shared_ptr<int[]> arr = make_shared<int[]>(size);
// 初始化数组 for (int i = 0; i < size; i++) { arr[i] = (i + 1) * 10; // 10, 20, 30, 40, 50, 60 }
// 使用 lambda 计算偶数之和 int evenSum = 0; for (int i = 0; i < size; i++) { auto isEven = [](int n) { return n % 2 == 0; }; auto addToSum = [&evenSum](int n) { evenSum += n; };
if (isEven(arr[i])) { addToSum(arr[i]); } }
cout << "数组: "; for (int i = 0; i < size; i++) { cout << arr[i] << " "; // 输出: 10 20 30 40 50 60 } cout << "\n偶数之和: " << evenSum << endl; // 输出: 210
return 0;}本篇小结:
- 3.1 运算符重载:让自定义类型支持运算符,使代码更自然。注意区分成员函数和友元函数的使用场景,牢记自增自减前置后置的区别。
- 3.2 模板编程:通过
template实现代码的泛型化,函数模板和类模板是 C++ 泛型编程的基石。模板特化可以为特定类型提供定制实现。- 3.3 C++11 新特性:
auto简化类型声明,lambda提供匿名函数能力,智能指针自动管理内存,范围for简化遍历。这些特性使 C++ 代码更简洁、更安全、更现代。
第四篇:多线程与游戏开发实战
4.1 环境搭建与指针进阶
核心概念
本课时深入讲解 C++ 的 内存模型 与 指针进阶 用法,包括:
- 内存模型:栈区、堆区、全局区、常量区、代码区
- 多级指针
- 函数指针
void指针const指针
一、内存模型
C++ 程序的内存分为五个区域:
| 区域 | 说明 | 生命周期 |
|---|---|---|
| 代码区 | 存放函数体的二进制代码 | 程序运行期间常驻 |
| 全局区 | 存放全局变量和静态变量 | 程序结束后释放 |
| 常量区 | 存放字符串常量和 const 修饰的全局变量 | 程序结束后释放 |
| 栈区 | 存放局部变量、函数参数,由编译器自动分配释放 | 函数执行结束后自动释放 |
| 堆区 | 由程序员手动 new / delete 管理 | 程序员控制 |
二、指针与数组
int arr[] = {10, 20, 30, 40, 50};int *p = arr;cout << *p << endl; // 10cout << *(p + 2) << endl; // 30cout << p[3] << endl; // 40代码讲解
| 代码 | 含义 |
|---|---|
int arr[] = {10, 20, 30, 40, 50}; | 定义一个包含 5 个元素的整型数组 |
int *p = arr; | 数组名 arr 代表首元素地址,将其赋给指针 p,此时 p 指向 arr[0] |
cout << *p << endl; | 对 p 解引用,输出 p 所指向的值,即 arr[0] = 10 |
cout << *(p + 2) << endl; | 指针算术:p + 2 向后移动 2 个 int 位置,指向 arr[2],解引用输出 30 |
cout << p[3] << endl; | p[3] 等价于 *(p + 3),即 arr[3],输出 40 |
要点:指针
+n不是移动n个字节,而是移动n × sizeof(类型)个字节。对于int *,每次+1移动 4 字节。
三、函数指针
#include <iostream>#include <string>using namespace std;
void greet(const string &name) { cout << "Hello, " << name << "!" << endl;}
int main() { void (*fp)(const string &) = greet; fp("World"); // Hello, World!
greet("C++"); // 通过原名调用 fp("Pointer"); // 通过指针调用 return 0;}代码讲解
| 代码 | 含义 |
|---|---|
void greet(const string &name) | 定义一个函数,接收 const string 的引用作为参数 |
void (*fp)(const string &) = greet; | 声明一个 函数指针 fp,指向与 greet 签名相同的函数,并将其初始化为 greet |
fp("World"); | 通过函数指针 fp 间接调用 greet,输出 Hello, World! |
greet("C++"); | 通过原名直接调用,两种方式等价 |
fp("Pointer"); | 再次通过指针调用,输出 Hello, Pointer! |
函数指针语法格式
// 语法格式返回类型 (*指针名)(参数类型列表);
// 示例int (*funcPtr)(int, int); // 指向返回 int、接收两个 int 参数的函数void (*callback)(const string &); // 指向返回 void、接收 const string& 参数的函数double (*mathOp)(double, double); // 指向返回 double、接收两个 double 参数的函数
// 完整可运行示例#include <iostream>using namespace std;
int add(int a, int b) { return a + b; }int subtract(int a, int b) { return a - b; }
int main() { int (*op)(int, int) = add; // 指向 add cout << op(3, 5) << endl; // 8 op = subtract; // 切换指向 subtract cout << op(3, 5) << endl; // -2 return 0;}四、const 指针
int a = 10, b = 20;
// 1. 指向常量的指针(不能通过指针修改值)const int *p1 = &a;// *p1 = 100; // 错误!不能通过 p1 修改值p1 = &b; // 正确,可以改变指向
// 2. 指针常量(不能改变指向)int *const p2 = &a;*p2 = 100; // 正确,可以通过 p2 修改值// p2 = &b; // 错误!不能改变指向
// 3. 指向常量的指针常量(都不能改)const int *const p3 = &a;// *p3 = 100; // 错误// p3 = &b; // 错误代码讲解
| 类型 | 写法 | 能改指向 | 能改值 |
|---|---|---|---|
| 指向常量的指针 | const int *p | ✅ | ❌ |
| 指针常量 | int *const p | ❌ | ✅ |
| 指向常量的指针常量 | const int *const p | ❌ | ❌ |
记忆技巧:看
const在*的左边还是右边——左边管值,右边管指针本身。
五、void 指针
#include <iostream>using namespace std;
int main() { int a = 42; double b = 3.14; void *vp = &a; // void 指针可以指向任意类型
// cout << *vp << endl; // 错误!void 指针不能直接解引用 cout << *(int *)vp << endl; // 42,使用前必须强制类型转换
vp = &b; cout << *(double *)vp << endl; // 3.14 return 0;}代码讲解
| 代码 | 含义 |
|---|---|
void *vp = &a; | void* 是通用指针类型,可以指向任意类型的数据 |
*(int *)vp | 使用前必须将 void* 强制转换为具体类型指针后才能解引用 |
vp = &b; | void* 可以随时切换指向不同类型的数据 |
课后练习
练习 1:编写程序,使用函数指针实现一个简单的计算器,支持加减乘除四则运算。
参考答案
#include <iostream>using namespace std;
double add(double a, double b) { return a + b; }double sub(double a, double b) { return a - b; }double mul(double a, double b) { return a * b; }double div(double a, double b) { return b != 0 ? a / b : 0; }
int main() { double (*ops[4])(double, double) = {add, sub, mul, div}; string names[] = {"加", "减", "乘", "除"};
int choice; double x, y; cout << "选择运算 (0-加 1-减 2-乘 3-除): "; cin >> choice; cout << "输入两个数: "; cin >> x >> y;
if (choice >= 0 && choice < 4) { cout << x << " " << names[choice] << " " << y << " = " << ops[choice](x, y) << endl; } return 0;}练习 2:定义一个三级指针 int ***ppp,使其最终指向一个值为 100 的变量,并通过它输出该值。
参考答案
#include <iostream>using namespace std;
int main() { int value = 100; int *p = &value; int **pp = &p; int ***ppp = &pp;
cout << ***ppp << endl; // 100 return 0;}4.2 多线程编程实战
核心概念
本课时讲解 C++11 多线程编程,包括:
thread线程创建与同步mutex互斥锁atomic原子操作condition_variable条件变量- 线程池基本原理
一、线程创建与互斥锁
#include <iostream>#include <thread>#include <mutex>#include <atomic>#include <vector>using namespace std;
mutex mtx;atomic<int> counter(0);
void worker(int id, int times) { for (int i = 0; i < times; i++) { lock_guard<mutex> lock(mtx); counter++; }}
int main() { vector<thread> threads; for (int i = 0; i < 10; i++) { threads.emplace_back(worker, i, 1000); } for (auto &t : threads) { t.join(); } cout << counter << endl; // 10000 return 0;}代码讲解
| 代码 | 含义 |
|---|---|
#include <thread> | 引入 C++11 线程库头文件 |
#include <mutex> | 引入互斥锁头文件 |
#include <atomic> | 引入原子操作头文件 |
mutex mtx; | 定义一个互斥锁对象 mtx,用于保护共享资源 |
atomic<int> counter(0); | 定义一个原子整型变量 counter,初始化为 0,保证操作的原子性 |
void worker(int id, int times) | 定义线程函数,id 为线程编号,times 为循环次数 |
lock_guard<mutex> lock(mtx); | 创建 lock_guard 对象自动加锁,离开作用域时自动解锁(RAII 机制),防止死锁 |
counter++ | 对原子变量执行自增操作 |
threads.emplace_back(worker, i, 1000) | 在 vector 中原地构造线程对象,参数分别为函数指针和参数列表 |
t.join() | 等待线程 t 执行完毕,主线程阻塞直到子线程结束 |
cout << counter << endl; | 所有线程执行完毕后输出结果,应为 10000 |
关键理解:如果不加锁也不使用原子变量,多线程同时写
counter会产生 数据竞争(data race),导致结果不确定。
二、线程创建语法格式
// 语法格式#include <thread>
// 方式一:直接创建thread 线程对象名(函数指针, 参数1, 参数2, ...);
// 方式二:使用 lambda 表达式thread 线程对象名([](参数列表) { // 线程逻辑}, 参数1, 参数2, ...);
// 等待线程结束线程对象名.join();
// 分离线程(独立运行,不等待)线程对象名.detach();完整可运行示例
#include <iostream>#include <thread>#include <chrono>using namespace std;
void task(string name, int seconds) { for (int i = 0; i < 3; i++) { cout << name << " 正在工作..." << endl; this_thread::sleep_for(chrono::seconds(seconds)); } cout << name << " 完成!" << endl;}
int main() { thread t1(task, "线程A", 1); thread t2(task, "线程B", 1);
t1.join(); t2.join();
cout << "所有线程结束" << endl; return 0;}三、互斥锁语法格式
// 语法格式#include <mutex>
mutex mtx; // 创建互斥锁mtx.lock(); // 手动加锁// ... 临界区代码 ...mtx.unlock(); // 手动解锁(容易忘记,推荐用 lock_guard)
// 推荐:使用 lock_guard(RAII 自动管理){ lock_guard<mutex> lock(mtx); // 构造时加锁 // ... 临界区代码 ...} // 离开作用域自动解锁
// 推荐:使用 unique_lock(更灵活,可手动 unlock)unique_lock<mutex> lock(mtx);// ... 部分临界区代码 ...lock.unlock(); // 手动提前解锁四、原子操作
#include <iostream>#include <atomic>#include <thread>#include <vector>using namespace std;
atomic<int> sum(0);
void addNumbers(int start, int end) { for (int i = start; i <= end; i++) { sum += i; }}
int main() { vector<thread> threads; // 用 4 个线程分别计算 1~25, 26~50, 51~75, 76~100 的和 for (int i = 0; i < 4; i++) { threads.emplace_back(addNumbers, i * 25 + 1, (i + 1) * 25); } for (auto &t : threads) { t.join(); } cout << "1+2+...+100 = " << sum << endl; // 5050 return 0;}代码讲解
| 代码 | 含义 |
|---|---|
atomic<int> sum(0); | 定义原子变量 sum,+= 操作是原子的,无需加锁 |
sum += i; | 原子加操作,多线程安全 |
threads.emplace_back(addNumbers, i * 25 + 1, (i + 1) * 25); | 将 1~100 的求和任务拆分给 4 个线程 |
原子 vs 互斥锁:原子操作适用于简单的数值操作(
++、--、+=等),性能更高;互斥锁适用于保护复杂的临界区代码。
五、条件变量
#include <iostream>#include <thread>#include <mutex>#include <condition_variable>using namespace std;
mutex mtx;condition_variable cv;bool ready = false;
void producer() { this_thread::sleep_for(chrono::seconds(1)); { lock_guard<mutex> lock(mtx); ready = true; } cv.notify_one(); // 通知等待的线程 cout << "生产者:数据已就绪" << endl;}
void consumer() { unique_lock<mutex> lock(mtx); cv.wait(lock, [] { return ready; }); // 等待直到 ready 为 true cout << "消费者:收到通知,开始处理" << endl;}
int main() { thread t1(producer); thread t2(consumer); t1.join(); t2.join(); return 0;}代码讲解
| 代码 | 含义 |
|---|---|
condition_variable cv; | 创建条件变量,用于线程间的通知机制 |
cv.notify_one() | 唤醒一个正在 wait 的线程 |
cv.wait(lock, [] { return ready; }); | 当前线程释放锁并阻塞等待,直到被通知且 ready 为 true 时才继续执行 |
unique_lock<mutex> lock(mtx); | 条件变量的 wait 必须配合 unique_lock 使用 |
课后练习
练习 1:编写一个多线程程序,创建 5 个线程,每个线程打印自己的编号(0~4),要求使用 mutex 保证输出不交错。
参考答案
#include <iostream>#include <thread>#include <mutex>#include <vector>using namespace std;
mutex mtx;
void printId(int id) { lock_guard<mutex> lock(mtx); cout << "线程 " << id << " 正在运行" << endl;}
int main() { vector<thread> threads; for (int i = 0; i < 5; i++) { threads.emplace_back(printId, i); } for (auto &t : threads) { t.join(); } return 0;}练习 2:使用 atomic<int> 实现一个计数器,4 个线程各对它递增 100 万次,输出最终结果。
参考答案
#include <iostream>#include <thread>#include <atomic>#include <vector>using namespace std;
atomic<int> counter(0);
void increment(int times) { for (int i = 0; i < times; i++) { counter++; }}
int main() { vector<thread> threads; for (int i = 0; i < 4; i++) { threads.emplace_back(increment, 1000000); } for (auto &t : threads) { t.join(); } cout << "最终计数: " << counter << endl; // 4000000 return 0;}4.3 游戏编程与数据结构
核心概念
本课时结合游戏开发场景,讲解:
- 游戏基址与偏移
- 面向对象在游戏中的应用
- 泛型链表的实现
一、游戏基址的概念
在游戏编程中,经常需要通过 基址 + 偏移 的方式定位内存中的数据:
基址(模块基地址) → [基地址 + 0x10] → [基地址 + 0x10 + 0x28] → 目标值提示:未经授权修改游戏内存数据属于违规行为,本内容仅供学习内存管理原理。
二、泛型链表(模板实现)
#include <iostream>using namespace std;
template <typename T>class LinkedList { struct Node { T data; Node* next; Node(const T &val) : data(val), next(nullptr) {} }; Node *head;
public: LinkedList() : head(nullptr) {}
~LinkedList() { while (head) { Node *temp = head; head = head->next; delete temp; } }
void push_front(const T &val) { Node *newNode = new Node(val); newNode->next = head; head = newNode; }
void push_back(const T &val) { Node *newNode = new Node(val); if (!head) { head = newNode; return; } Node *curr = head; while (curr->next) { curr = curr->next; } curr->next = newNode; }
void display() const { Node *curr = head; while (curr) { cout << curr->data << " -> "; curr = curr->next; } cout << "nullptr" << endl; }
int size() const { int count = 0; Node *curr = head; while (curr) { count++; curr = curr->next; } return count; }};
int main() { LinkedList<int> list; list.push_front(30); list.push_front(20); list.push_front(10); list.push_back(40); list.push_back(50);
list.display(); // 10 -> 20 -> 30 -> 40 -> 50 -> nullptr cout << "节点数: " << list.size() << endl; // 5
LinkedList<string> names; names.push_back("Alice"); names.push_back("Bob"); names.push_back("Charlie"); names.display(); // Alice -> Bob -> Charlie -> nullptr
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
template <typename T> | 定义模板类,T 为泛型类型参数,使用时可以替换为 int、string 等任意类型 |
struct Node { T data; Node* next; ... } | 定义私有内部结构体 Node,包含数据域 data 和指针域 next |
Node(const T &val) : data(val), next(nullptr) {} | Node 的构造函数,使用初始化列表,将 data 设为 val,next 设为空指针 |
LinkedList() : head(nullptr) {} | 链表构造函数,将头指针初始化为 nullptr,表示空链表 |
~LinkedList() | 析构函数,遍历链表并逐个 delete 释放堆内存,防止内存泄漏 |
void push_front(const T &val) | 头插法:创建新节点,将其 next 指向当前头节点,再更新头指针 |
void push_back(const T &val) | 尾插法:遍历到链表末尾,将新节点挂到最后 |
void display() const | 遍历链表并依次输出每个节点的数据 |
int size() const | 遍历链表统计节点数量 |
模板链表语法格式
// 语法格式template <typename T>class 类名 { struct Node { T data; Node* next; Node(const T &val) : data(val), next(nullptr) {} }; Node *head;public: 类名() : head(nullptr) {} ~类名() { /* 释放所有节点 */ } void push_front(const T &val); // 头插 void push_back(const T &val); // 尾插 void display() const; // 遍历显示};
// 使用方式类名<int> intList; // 存储 int 类型类名<string> strList; // 存储 string 类型类名<double> dblList; // 存储 double 类型三、面向对象与游戏实体
#include <iostream>#include <string>using namespace std;
class GameObject {protected: string name; int x, y; int hp;
public: GameObject(string n, int px, int py, int health) : name(n), x(px), y(py), hp(health) {}
virtual void update() { cout << name << " 位置: (" << x << ", " << y << ") 血量: " << hp << endl; }
void takeDamage(int dmg) { hp -= dmg; if (hp <= 0) { cout << name << " 已死亡!" << endl; hp = 0; } }
string getName() const { return name; } int getHP() const { return hp; }};
class Player : public GameObject {public: Player(string n, int px, int py) : GameObject(n, px, py, 100) {}
void update() override { cout << "[玩家] " << name << " 位置: (" << x << ", " << y << ") 血量: " << hp << endl; }};
class Enemy : public GameObject {public: Enemy(string n, int px, int py) : GameObject(n, px, py, 50) {}
void update() override { cout << "[敌人] " << name << " 位置: (" << x << ", " << y << ") 血量: " << hp << endl; }};
int main() { Player hero("勇者", 10, 20); Enemy monster("史莱姆", 15, 25);
hero.update(); monster.update();
hero.takeDamage(30); monster.takeDamage(60);
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
class GameObject | 定义游戏对象基类,包含所有游戏实体共有的属性 |
protected: | 保护成员,子类可访问,外部不可访问 |
virtual void update() | 虚函数,子类可以重写以实现多态行为 |
void takeDamage(int dmg) | 通用受伤方法,所有子类继承使用 |
class Player : public GameObject | Player 类公有继承自 GameObject |
void update() override | 子类重写基类的虚函数,override 关键字确保正确重写 |
hero.takeDamage(30) | 调用继承的方法,玩家血量从 100 降到 70 |
课后练习
练习 1:为上面的 LinkedList 类添加一个 bool remove(const T &val) 方法,删除链表中第一个值为 val 的节点,返回是否删除成功。
参考答案
bool remove(const T &val) { if (!head) return false;
// 删除头节点 if (head->data == val) { Node *temp = head; head = head->next; delete temp; return true; }
// 查找并删除中间/尾部节点 Node *prev = head; Node *curr = head->next; while (curr) { if (curr->data == val) { prev->next = curr->next; delete curr; return true; } prev = curr; curr = curr->next; } return false; // 未找到}练习 2:创建一个 LinkedList<float> 链表,插入 3.14、2.71、1.41 三个浮点数,然后遍历输出。
参考答案
int main() { LinkedList<float> fList; fList.push_back(3.14f); fList.push_back(2.71f); fList.push_back(1.41f); fList.display(); // 3.14 -> 2.71 -> 1.41 -> nullptr return 0;}4.4 视觉编程基础
核心概念
本课时讲解视觉编程中的底层技术:
- Win32 API 简介
- 二进制运算在图形中的应用(颜色值分解与合成)
- 位标志(bit flags)
- 内联汇编与显存读取基础
一、颜色值的二进制分解与合成
#include <iostream>using namespace std;
int main() { // === 颜色分解 === unsigned int color = 0xFF8000; // ARGB 格式(或 RGB 格式,取决于上下文)
unsigned char r = (color >> 16) & 0xFF; // 255 (0xFF) unsigned char g = (color >> 8) & 0xFF; // 128 (0x80) unsigned char b = color & 0xFF; // 0 (0x00)
cout << "R: " << (int)r << endl; // 255 cout << "G: " << (int)g << endl; // 128 cout << "B: " << (int)b << endl; // 0
// === 颜色合成 === unsigned int newColor = (r << 16) | (g << 8) | b; cout << "合成颜色: 0x" << hex << newColor << dec << endl; // 0xff8000
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
unsigned int color = 0xFF8000; | 定义一个 32 位无符号整数表示颜色,十六进制 0xFF8000 在内存中占 3 字节 |
(color >> 16) & 0xFF | 右移 16 位取出最高字节(红色通道),再用 & 0xFF 掩码确保只保留低 8 位 |
(color >> 8) & 0xFF | 右移 8 位取出中间字节(绿色通道) |
color & 0xFF | 直接用掩码取出最低字节(蓝色通道) |
(int)r | unsigned char 输出时默认按字符显示,强制转为 int 才能输出数值 |
(r << 16) | (g << 8) | b | 将 R/G/B 三个通道通过左移和按位或合成为一个 24 位颜色值 |
hex / dec | I/O 操纵符,切换输出进制为十六进制 / 十进制 |
图解颜色值分解:
0xFF8000 的二进制表示:┌─────────┬─────────┬─────────┐│ FF │ 80 │ 00 ││ 红色R │ 绿色G │ 蓝色B ││ bits 16-23 │ bits 8-15 │ bits 0-7 │└─────────┴─────────┴─────────┘二、位标志(Bit Flags)
#include <iostream>using namespace std;
int main() { unsigned int flags = 0b00010110; // 二进制字面量(C++14 起)
// 读取第 n 位(从右往左,从 0 开始计数) bool flag0 = (flags >> 0) & 1; // false bool flag1 = (flags >> 1) & 1; // true (第1位=1) bool flag2 = (flags >> 2) & 1; // true (第2位=1) bool flag3 = (flags >> 3) & 1; // false bool flag4 = (flags >> 4) & 1; // true (第4位=1)
cout << "Flag 1: " << flag1 << endl; // 1 (true) cout << "Flag 2: " << flag2 << endl; // 1 (true) cout << "Flag 4: " << flag4 << endl; // 1 (true)
// 设置第 5 位为 1 flags |= (1 << 5); cout << "设置后: " << flags << endl; // 0b00110110 = 54
// 清除第 1 位 flags &= ~(1 << 1); cout << "清除后: " << flags << endl; // 0b00110100 = 52
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
0b00010110 | C++14 二进制字面量,以 0b 开头 |
(flags >> n) & 1 | 将第 n 位移到最低位,再用 & 1 提取,结果为该位的值(0 或 1) |
flags | (1 << 5) | 按位或赋值,将第 5 位设为 1(其他位不变) |
flags &= ~(1 << 1) | 按位与赋值,~(1<<1) 为除第 1 位外全是 1,结果为清除第 1 位 |
位运算语法总结
// 读取第 n 位bool bit = (value >> n) & 1;
// 设置第 n 位为 1value |= (1 << n);
// 清除第 n 位为 0value &= ~(1 << n);
// 切换第 n 位value ^= (1 << n);三、Win32 API 简介
#include <windows.h>#include <iostream>using namespace std;
int main() { // 获取桌面窗口句柄 HWND desktop = GetDesktopWindow(); cout << "桌面窗口句柄: " << desktop << endl;
// 获取控制台窗口句柄 HWND console = GetConsoleWindow(); cout << "控制台句柄: " << console << endl;
// 获取窗口标题 char title[256]; GetWindowTextA(console, title, sizeof(title)); cout << "窗口标题: " << title << endl;
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
#include <windows.h> | 引入 Windows API 头文件 |
HWND | 窗口句柄类型(Handle to WiNDow),本质是一个指针,标识一个窗口 |
GetDesktopWindow() | 获取桌面窗口的句柄 |
GetConsoleWindow() | 获取当前控制台窗口的句柄 |
GetWindowTextA(hwnd, buf, size) | 将指定窗口的标题复制到缓冲区 |
注意:Win32 API 仅在 Windows 平台可用。
课后练习
练习 1:编写程序,将颜色值 0x00FF00(纯绿色)分解为 R、G、B 三个通道,再将它们合成为新的颜色值并输出验证。
参考答案
#include <iostream>using namespace std;
int main() { unsigned int color = 0x00FF00;
unsigned char r = (color >> 16) & 0xFF; // 0 unsigned char g = (color >> 8) & 0xFF; // 255 unsigned char b = color & 0xFF; // 0
cout << "R=" << (int)r << " G=" << (int)g << " B=" << (int)b << endl;
unsigned int newColor = (r << 16) | (g << 8) | b; cout << "验证: 0x" << hex << newColor << dec << endl; // 0xff00
return 0;}练习 2:给定一个 8 位的标志变量 unsigned char status = 0b10110011;,读取第 0、2、4、6 位的值,并将第 7 位设置为 0。
参考答案
#include <iostream>using namespace std;
int main() { unsigned char status = 0b10110011;
cout << "bit0: " << ((status >> 0) & 1) << endl; // 1 cout << "bit2: " << ((status >> 2) & 1) << endl; // 0 cout << "bit4: " << ((status >> 4) & 1) << endl; // 1 cout << "bit6: " << ((status >> 6) & 1) << endl; // 1
status &= ~(1 << 7); cout << "清除第7位后: " << (int)status << endl; // 51 (0b00110011)
return 0;}第五篇:安全攻防与逆向工程实战
⚠️ 法律与道德提示:本篇内容仅供教学和安全研究目的。未经授权对他人软件进行逆向工程、DLL 注入、HOOK 等操作属于违法行为,可能触犯《中华人民共和国网络安全法》等相关法律法规。请务必在合法合规的前提下学习。
5.1 图色算法与界面绘制
核心概念
本课时讲解 Windows 图形编程中的 取色 与 绘图 基础:
- 使用 Win32 API 获取屏幕像素颜色
- 使用 GDI 绘制文本和图形
一、获取屏幕像素颜色
#include <windows.h>#include <iostream>using namespace std;
int main() { // 获取整个屏幕的设备上下文 HDC hdc = GetDC(NULL);
// 获取坐标 (x, y) 处的像素颜色 int x = 100, y = 100; COLORREF color = GetPixel(hdc, x, y);
// 分解为 RGB 三通道 int r = GetRValue(color); int g = GetGValue(color); int b = GetBValue(color);
cout << "坐标 (" << x << ", " << y << ") 处的颜色:" << endl; cout << "R=" << r << " G=" << g << " B=" << b << endl;
// 释放设备上下文(重要!) ReleaseDC(NULL, hdc);
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
HDC hdc = GetDC(NULL); | 获取设备上下文(Handle to Device Context),NULL 表示获取整个屏幕的 DC |
COLORREF color = GetPixel(hdc, x, y); | 获取指定坐标处的像素颜色,返回一个 32 位颜色值 |
GetRValue(color) | 从 COLORREF 中提取红色分量(宏) |
GetGValue(color) | 提取绿色分量 |
GetBValue(color) | 提取蓝色分量 |
ReleaseDC(NULL, hdc); | 释放 DC,每次 GetDC 后必须调用,否则会造成资源泄漏 |
图色相关语法格式
// 语法格式HDC hdc = GetDC(hwnd); // 获取窗口 DCCOLORREF color = GetPixel(hdc, x, y); // 取色int r = GetRValue(color); // 提取 Rint g = GetGValue(color); // 提取 Gint b = GetBValue(color); // 提取 BReleaseDC(hwnd, hdc); // 释放 DC
// RGB 宏:合成颜色值COLORREF c = RGB(255, 0, 0); // 纯红色二、在窗口中绘制文本
#include <windows.h>#include <iostream>using namespace std;
int main() { // 获取控制台窗口的设备上下文 HWND hwnd = GetConsoleWindow(); HDC hdc = GetDC(hwnd);
// 设置文本颜色为红色 SetTextColor(hdc, RGB(255, 0, 0));
// 设置背景模式为透明 SetBkMode(hdc, TRANSPARENT);
// 在坐标 (10, 10) 处绘制文本 TextOut(hdc, 10, 10, "Hello GDI!", 10);
// 释放设备上下文 ReleaseDC(hwnd, hdc);
cout << "文本已绘制到控制台窗口" << endl;
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
GetConsoleWindow() | 获取当前控制台窗口的句柄 |
GetDC(hwnd) | 获取指定窗口的设备上下文 |
SetTextColor(hdc, RGB(255, 0, 0)) | 设置文本颜色为红色,RGB(r, g, b) 宏合成颜色值 |
SetBkMode(hdc, TRANSPARENT) | 设置文本背景为透明模式,否则会显示文字背景色 |
TextOut(hdc, 10, 10, "Hello GDI!", 10) | 在 DC 上绘制文本,参数依次为:DC 句柄、x 坐标、y 坐标、字符串指针、字符数 |
ReleaseDC(hwnd, hdc) | 释放设备上下文 |
绘制文本语法格式
// 语法格式HDC hdc = GetDC(hwnd);SetTextColor(hdc, RGB(R, G, B)); // 设置文本颜色SetBkMode(hdc, TRANSPARENT); // 设置透明背景TextOut(hdc, x, y, "文本", 字符数); // 绘制文本ReleaseDC(hwnd, hdc); // 释放 DC课后练习
练习 1:编写程序,获取屏幕上 (0, 0)、(500, 500)、(960, 540) 三个坐标的颜色值并输出 RGB 分量。
参考答案
#include <windows.h>#include <iostream>using namespace std;
void printColor(HDC hdc, int x, int y) { COLORREF color = GetPixel(hdc, x, y); cout << "(" << x << ", " << y << "): " << "R=" << GetRValue(color) << " G=" << GetGValue(color) << " B=" << GetBValue(color) << endl;}
int main() { HDC hdc = GetDC(NULL); printColor(hdc, 0, 0); printColor(hdc, 500, 500); printColor(hdc, 960, 540); ReleaseDC(NULL, hdc); return 0;}5.2 DLL 注入与消息钩子
核心概念
本课时讲解 Windows 安全领域中的两个重要概念:
- DLL 注入的基本原理
- 消息钩子(Hook)
⚠️ 重要提醒:DLL 注入和消息钩子技术在安全防护软件、调试器等合法场景中有正当用途,但未经授权将其用于游戏外挂或恶意软件是 违法行为。
一、DLL 注入原理
DLL 注入的步骤如下:
1. OpenProcess → 打开目标进程,获取进程句柄2. VirtualAllocEx → 在目标进程中分配内存空间3. WriteProcessMemory → 将 DLL 路径写入目标进程的内存4. CreateRemoteThread → 在目标进程中创建远程线程,调用 LoadLibraryA 加载 DLL#include <windows.h>#include <string>#include <iostream>using namespace std;
void InjectDLL(DWORD targetPID, const string &dllPath) { // 第一步:打开目标进程 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID); if (!hProcess) { cout << "打开进程失败,错误码: " << GetLastError() << endl; return; }
// 第二步:在目标进程中分配内存 LPVOID pMem = VirtualAllocEx( hProcess, // 目标进程句柄 NULL, // 让系统自动选择地址 dllPath.length() + 1, // 分配大小(含 '\0') MEM_COMMIT, // 提交内存 PAGE_READWRITE // 读写权限 ); if (!pMem) { cout << "分配内存失败" << endl; CloseHandle(hProcess); return; }
// 第三步:将 DLL 路径写入目标进程 WriteProcessMemory( hProcess, pMem, dllPath.c_str(), dllPath.length() + 1, NULL );
// 第四步:在目标进程中创建远程线程加载 DLL HANDLE hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)GetProcAddress( GetModuleHandle("kernel32"), "LoadLibraryA" ), pMem, 0, NULL );
// 等待远程线程执行完毕 WaitForSingleObject(hThread, INFINITE);
// 清理资源 VirtualFreeEx(hProcess, pMem, 0, MEM_RELEASE); CloseHandle(hThread); CloseHandle(hProcess);
cout << "DLL 注入完成" << endl;}代码讲解
| 代码 | 含义 |
|---|---|
OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID) | 以完全访问权限打开目标进程,返回进程句柄 |
VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_READWRITE) | 在目标进程的虚拟地址空间中分配一块可读写的内存 |
WriteProcessMemory(hProcess, pMem, data, size, NULL) | 将数据写入目标进程的内存空间 |
GetProcAddress(GetModuleHandle("kernel32"), "LoadLibraryA") | 获取 kernel32.dll 中 LoadLibraryA 函数的地址 |
(LPTHREAD_START_ROUTINE) | 将函数指针强制转换为线程函数类型 |
CreateRemoteThread(...) | 在目标进程中创建一个新线程,该线程会调用 LoadLibraryA 加载我们写入的 DLL 路径 |
VirtualFreeEx(...) | 释放之前分配的内存 |
CloseHandle(...) | 关闭句柄,释放系统资源 |
DLL 注入语法格式
// 步骤1:打开目标进程HANDLE hProcess = OpenProcess(访问权限, 是否继承, 进程ID);
// 步骤2:分配内存LPVOID pMem = VirtualAllocEx(hProcess, NULL, 大小, MEM_COMMIT, PAGE_READWRITE);
// 步骤3:写入数据WriteProcessMemory(hProcess, pMem, 数据指针, 数据大小, &已写字节数);
// 步骤4:创建远程线程HANDLE hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)线程函数地址, 参数, 0, NULL);
// 步骤5:清理CloseHandle(hThread);VirtualFreeEx(hProcess, pMem, 0, MEM_RELEASE);CloseHandle(hProcess);二、消息钩子原理
#include <windows.h>#include <iostream>using namespace std;
// 钩子回调函数LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { // 处理键盘消息 if (wParam == WM_KEYDOWN) { cout << "按键按下: " << (int)wParam << endl; } } // 传递给下一个钩子 return CallNextHookEx(NULL, nCode, wParam, lParam);}
int main() { // 安装全局键盘钩子 HHOOK hook = SetWindowsHookEx( WH_KEYBOARD_LL, // 钩子类型:低级键盘钩子 KeyboardProc, // 回调函数 GetModuleHandle(NULL), // 当前模块句柄 0 // 关联所有线程 );
if (!hook) { cout << "安装钩子失败" << endl; return 1; }
cout << "键盘钩子已安装,按 ESC 退出..." << endl;
// 消息循环,保持钩子运行 MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { if (msg.message == WM_KEYDOWN && msg.wParam == VK_ESCAPE) { break; } TranslateMessage(&msg); DispatchMessage(&msg); }
// 卸载钩子 UnhookWindowsHookEx(hook); cout << "钩子已卸载" << endl;
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
LRESULT CALLBACK KeyboardProc(...) | 定义钩子回调函数,每当有键盘事件时系统会调用此函数 |
nCode >= 0 | 当 nCode 大于等于 0 时,可以处理此消息 |
CallNextHookEx(...) | 将消息传递给钩子链中的下一个钩子 |
SetWindowsHookEx(WH_KEYBOARD_LL, ...) | 安装低级键盘钩子,WH_KEYBOARD_LL 表示全局键盘钩子 |
GetModuleHandle(NULL) | 获取当前程序模块的句柄 |
GetMessage(&msg, NULL, 0, 0) | 从消息队列中获取消息,阻塞等待 |
TranslateMessage / DispatchMessage | 翻译并分发消息 |
UnhookWindowsHookEx(hook) | 卸载钩子,释放系统资源 |
课后练习
练习 1:简述 DLL 注入的四个步骤,并说明每一步调用的 Win32 API 函数名称。
参考答案
- 打开目标进程:调用
OpenProcess()获取目标进程句柄 - 分配内存:调用
VirtualAllocEx()在目标进程中分配可读写内存 - 写入数据:调用
WriteProcessMemory()将 DLL 路径写入目标进程 - 创建远程线程:调用
CreateRemoteThread()在目标进程中执行LoadLibraryA加载 DLL
5.3 内联 HOOK 技术
核心概念
本课时讲解 内联 HOOK(Inline Hook) 的原理与实现:
- 什么是内联 HOOK
- 如何修改目标函数的机器码
- 跳转指令
JMP的编码 VirtualProtect修改内存保护属性
⚠️ 法律提示:HOOK 技术常用于调试器、性能分析工具和反作弊系统。未经授权 HOOK 他人软件属于违法行为。
一、内联 HOOK 原理
内联 HOOK 的核心思想:
原始函数:┌─────────────────────┐│ 目标函数前5个字节 │ ← 被替换为 JMP 到 Hook 函数│ 原始指令... ││ 剩余代码... │└─────────────────────┘
HOOK 后:┌─────────────────────┐ ┌─────────────────────┐│ JMP HookFunc │────→│ HookFunc ││ 剩余代码... │ │ ... 处理逻辑 ... ││ │ │ 调用原始函数 │└─────────────────────┘ └─────────────────────┘二、内联 HOOK 实现
#include <windows.h>#include <cstdio>using namespace std;
void *originalFunc = nullptr;BYTE originalBytes[5];
// Hook 函数(替换目标函数后实际执行的函数)int __fastcall HookFunc(int param1, int param2) { printf("函数被调用,参数: %d, %d\n", param1, param2);
// 调用原始函数 return ((int(__fastcall*)(int, int))originalFunc)(param1, param2);}
// 安装 Hookvoid InstallHook(void *target, void *hook) { DWORD oldProtect;
// 第一步:修改内存保护属性为可读写可执行 VirtualProtect(target, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
// 第二步:保存原始 5 个字节(用于恢复) memcpy(originalBytes, target, 5);
// 第三步:计算跳转偏移量 // JMP rel32 的编码:E9 + 4字节偏移 // 偏移 = Hook地址 - 目标地址 - 5(5 是指令本身的长度) DWORD offset = (BYTE*)hook - (BYTE*)target - 5;
// 第四步:写入 JMP 指令 *(BYTE*)target = 0xE9; // JMP 操作码 *(DWORD*)((BYTE*)target + 1) = offset; // 32 位相对偏移
// 第五步:恢复原始内存保护属性 VirtualProtect(target, 5, oldProtect, &oldProtect);
// 保存原始函数入口地址(跳过 JMP 指令后的地址) originalFunc = (void*)((BYTE*)target + 5);
printf("Hook 安装成功!\n");}
// 恢复原始函数void UninstallHook(void *target) { DWORD oldProtect; VirtualProtect(target, 5, PAGE_EXECUTE_READWRITE, &oldProtect); memcpy(target, originalBytes, 5); // 恢复原始字节 VirtualProtect(target, 5, oldProtect, &oldProtect); printf("Hook 已卸载\n");}代码讲解
| 代码 | 含义 |
|---|---|
void *originalFunc = nullptr; | 全局变量,保存原始函数的地址(跳过 JMP 指令后的位置) |
BYTE originalBytes[5]; | 保存被覆盖的原始 5 个字节,用于卸载 Hook 时恢复 |
__fastcall | 调用约定,前两个参数通过寄存器传递(ECX、EDX),速度更快 |
VirtualProtect(target, 5, PAGE_EXECUTE_READWRITE, &oldProtect) | 将目标地址处 5 字节的内存保护改为可读写可执行 |
memcpy(originalBytes, target, 5) | 备份原始字节 |
DWORD offset = (BYTE*)hook - (BYTE*)target - 5; | 计算 JMP 的相对偏移:目标地址 - 当前地址 - 指令长度 |
*(BYTE*)target = 0xE9; | 在目标地址写入 0xE9(x86 JMP rel32 的操作码) |
*(DWORD*)((BYTE*)target + 1) = offset; | 在 JMP 操作码后写入 4 字节的偏移量 |
VirtualProtect(target, 5, oldProtect, &oldProtect) | 恢复原始的内存保护属性 |
originalFunc = (void*)((BYTE*)target + 5); | 原始函数入口 = JMP 指令之后的地址 |
内联 HOOK 语法格式
// 安装 Hook 的通用步骤:// 1. VirtualProtect 修改内存为可执行可读写// 2. memcpy 备份原始字节// 3. 计算偏移 offset = hookAddr - targetAddr - 5// 4. 写入 JMP 0xE9 + offset (4 bytes)// 5. VirtualProtect 恢复内存保护// 6. 保存原函数入口 target + 5三、JMP 指令编码详解
x86 近跳转指令格式:┌───────┬──────────────────┐│ 0xE9 │ 偏移量 (4字节) │ 共 5 字节│ JMP │ rel32 │└───────┴──────────────────┘
实际跳转目标 = 当前指令地址 + 5 + rel32因此:rel32 = 目标地址 - 当前地址 - 5课后练习
练习 1:简述内联 HOOK 的安装流程,并说明为什么需要先调用 VirtualProtect。
参考答案
内联 HOOK 安装流程:
- 调用
VirtualProtect将目标函数入口处的内存保护属性改为PAGE_EXECUTE_READWRITE - 备份原始 5 个字节
- 计算跳转偏移量:
offset = hookAddr - targetAddr - 5 - 写入
0xE9(JMP 操作码)和 4 字节偏移量 - 恢复原始内存保护属性
需要 VirtualProtect 的原因:代码段所在的内存页默认是只读和可执行的(PAGE_EXECUTE_READ),直接写入会导致访问违例(Access Violation),必须先修改保护属性才能写入。
5.4 高级图形与安全算法
核心概念
本课时讲解 Windows GDI+ 高级绘图技术:
- GDI+ 环境初始化
- 绘制圆角矩形
- 图形路径(GraphicsPath)的使用
一、GDI+ 绘制圆角矩形
#include <windows.h>#include <gdiplus.h>using namespace Gdiplus;#pragma comment(lib, "gdiplus.lib")
// 绘制填充+边框的圆角矩形void DrawRoundRect(Graphics &g, RectF rect, float radius, Color fillColor, Color borderColor) { GraphicsPath path;
// 四个角的圆弧,拼成圆角矩形路径 // 左上角 path.AddArc(rect.X, rect.Y, radius, radius, 180, 90); // 右上角 path.AddArc(rect.X + rect.Width - radius, rect.Y, radius, radius, 270, 90); // 右下角 path.AddArc(rect.X + rect.Width - radius, rect.Y + rect.Height - radius, radius, radius, 0, 90); // 左下角 path.AddArc(rect.X, rect.Y + rect.Height - radius, radius, radius, 90, 90);
path.CloseFigure(); // 闭合路径
// 填充内部 g.FillPath(&SolidBrush(fillColor), &path); // 绘制边框 g.DrawPath(&Pen(borderColor), &path);}代码讲解
| 代码 | 含义 |
|---|---|
#include <gdiplus.h> | 引入 GDI+ 头文件 |
#pragma comment(lib, "gdiplus.lib") | 链接 GDI+ 库 |
using namespace Gdiplus; | 使用 GDI+ 命名空间 |
Graphics &g | GDI+ 绘图对象引用 |
RectF rect | 浮点精度的矩形区域,包含 X、Y、Width、Height |
GraphicsPath path; | 创建图形路径对象,可以添加直线、弧线等元素 |
path.AddArc(x, y, w, h, startAngle, sweepAngle) | 添加一段椭圆弧,参数为:椭圆左上角坐标、宽高、起始角度、扫过角度(单位:度) |
path.CloseFigure(); | 闭合路径,将最后一个点连接到第一个点 |
g.FillPath(&SolidBrush(fillColor), &path); | 用指定颜色填充路径区域 |
g.DrawPath(&Pen(borderColor), &path); | 用指定颜色绘制路径的边框线条 |
GDI+ 圆角矩形原理图解
┌──────────────────────────────┐ │ ╭──────────────────────╮ │ ← 左上角弧 + 右上角弧 │ │ │ │ │ │ 圆角矩形区域 │ │ │ │ │ │ │ ╰──────────────────────╯ │ ← 左下角弧 + 右下角弧 └──────────────────────────────┘
四个角各用一段 90° 的圆弧替代直角GDI+ 环境初始化语法格式
// 在使用 GDI+ 前必须初始化,退出前必须清理ULONG_PTR gdiplusToken;GdiplusStartupInput gdiplusStartupInput;
// 初始化(通常在程序入口处)GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
// 使用 GDI+ 绘图...
// 清理(通常在程序退出前)GdiplusShutdown(gdiplusToken);完整可运行示例
#include <windows.h>#include <gdiplus.h>#include <iostream>using namespace Gdiplus;#pragma comment(lib, "gdiplus.lib")
ULONG_PTR gdiplusToken;
void DrawRoundRect(Graphics &g, RectF rect, float radius, Color fillColor, Color borderColor) { GraphicsPath path; path.AddArc(rect.X, rect.Y, radius, radius, 180, 90); path.AddArc(rect.X + rect.Width - radius, rect.Y, radius, radius, 270, 90); path.AddArc(rect.X + rect.Width - radius, rect.Y + rect.Height - radius, radius, radius, 0, 90); path.AddArc(rect.X, rect.Y + rect.Height - radius, radius, radius, 90, 90); path.CloseFigure();
g.FillPath(&SolidBrush(fillColor), &path); g.DrawPath(&Pen(borderColor, 2.0f), &path);}
int main() { // 初始化 GDI+ GdiplusStartupInput gdiplusStartupInput; GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
// 获取控制台窗口 DC HWND hwnd = GetConsoleWindow(); HDC hdc = GetDC(hwnd);
// 创建 GDI+ Graphics 对象 Graphics graphics(hdc);
// 绘制一个圆角矩形 RectF rect(50.0f, 50.0f, 300.0f, 150.0f); DrawRoundRect(graphics, rect, 20.0f, Color(255, 200, 230, 255), // 填充色(淡蓝) Color(255, 0, 100, 200)); // 边框色(深蓝)
cout << "圆角矩形已绘制" << endl;
ReleaseDC(hwnd, hdc);
// 清理 GDI+ GdiplusShutdown(gdiplusToken);
return 0;}课后练习
练习 1:使用 GDI+ 的 Graphics::DrawLine 方法在窗口上绘制一个简单的十字形。
参考答案
#include <windows.h>#include <gdiplus.h>#include <iostream>using namespace Gdiplus;#pragma comment(lib, "gdiplus.lib")
ULONG_PTR gdiplusToken;
int main() { GdiplusStartupInput input; GdiplusStartup(&gdiplusToken, &input, NULL);
HWND hwnd = GetConsoleWindow(); HDC hdc = GetDC(hwnd); Graphics g(hdc);
Pen redPen(Color(255, 255, 0, 0), 3.0f); // 红色画笔,宽度3
// 水平线 g.DrawLine(&redPen, 100, 200, 400, 200); // 垂直线 g.DrawLine(&redPen, 250, 50, 250, 350);
cout << "十字形已绘制" << endl; ReleaseDC(hwnd, hdc); GdiplusShutdown(gdiplusToken); return 0;}5.5 游戏逆向与注入技术
核心概念
本课时讲解游戏逆向中的关键技术:
- 特征码扫描(Pattern Scanning)
- 进程内存读取
- 内存搜索算法
⚠️ 重要提醒:以下技术仅用于学习和安全研究。未经授权扫描或修改游戏进程内存是违法行为。
一、进程内存读取
#include <windows.h>#include <iostream>#include <vector>using namespace std;
int main() { DWORD pid; cout << "请输入目标进程 PID: "; cin >> pid;
// 打开目标进程 HANDLE hProcess = OpenProcess(PROCESS_VM_READ, FALSE, pid); if (!hProcess) { cout << "打开进程失败,错误码: " << GetLastError() << endl; return 1; }
// 定义要搜索的特征码和掩码 BYTE pattern[] = {0x89, 0x4C, 0x24, 0x08}; BYTE mask[] = {'x', 'x', 'x', 'x'}; // 'x' 表示匹配,'?' 表示跳过 int patternLen = sizeof(pattern);
// 用于存储读取的内存数据 BYTE buffer[4096]; SIZE_T bytesRead; vector<SIZE_T> foundAddresses;
cout << "开始扫描内存..." << endl;
// 遍历用户态内存空间 for (SIZE_T addr = 0x400000; addr < 0x7FFFFFFF; addr += 4096) { // 尝试读取每个内存页 if (ReadProcessMemory(hProcess, (LPCVOID)addr, buffer, 4096, &bytesRead)) { // 在当前页中搜索特征码 for (SIZE_T i = 0; i <= bytesRead - patternLen; i++) { bool match = true; for (int j = 0; j < patternLen; j++) { if (mask[j] == 'x' && buffer[i + j] != pattern[j]) { match = false; break; } } if (match) { foundAddresses.push_back(addr + i); cout << "找到匹配地址: 0x" << hex << (addr + i) << dec << endl; } } } }
cout << "扫描完成,共找到 " << foundAddresses.size() << " 个匹配" << endl;
CloseHandle(hProcess); return 0;}代码讲解
| 代码 | 含义 |
|---|---|
OpenProcess(PROCESS_VM_READ, FALSE, pid) | 以只读内存权限打开目标进程 |
BYTE pattern[] = {0x89, 0x4C, 0x24, 0x08}; | 定义特征码(要搜索的字节序列) |
BYTE mask[] = {'x','x','x','x'}; | 定义掩码,'x' 表示该位置必须匹配,'?' 表示通配(任意值都行) |
ReadProcessMemory(hProcess, addr, buffer, 4096, &bytesRead) | 从目标进程的指定地址读取 4096 字节数据到缓冲区 |
for (SIZE_T addr = 0x400000; addr < 0x7FFFFFFF; addr += 4096) | 遍历用户态内存空间,以 4KB(一页)为单位步进 |
| 双重循环比较特征码 | 外层遍历页内每个偏移,内层逐字节对比特征码 |
foundAddresses.push_back(addr + i) | 找到匹配时记录完整地址 = 页基址 + 页内偏移 |
特征码扫描语法格式
// 语法格式// 1. 打开目标进程HANDLE hProcess = OpenProcess(PROCESS_VM_READ, FALSE, pid);
// 2. 定义特征码和掩码BYTE pattern[] = {0x??, 0x??, ...}; // 要搜索的字节BYTE mask[] = {'x', '?', ...}; // 匹配规则
// 3. 逐页读取并搜索BYTE buffer[4096];SIZE_T bytesRead;for (SIZE_T addr = 起始地址; addr < 结束地址; addr += 4096) { if (ReadProcessMemory(hProcess, (LPCVOID)addr, buffer, 4096, &bytesRead)) { // 在 buffer 中搜索 pattern }}
// 4. 关闭句柄CloseHandle(hProcess);二、内存遍历优化思路
// 优化1:使用 MEMORY_BASIC_INFORMATION 过滤可读内存页MEMORY_BASIC_INFORMATION mbi;for (SIZE_T addr = 0; addr < 0x7FFFFFFF;) { if (VirtualQueryEx(hProcess, (LPCVOID)addr, &mbi, sizeof(mbi))) { // 只扫描已提交(MEM_COMMIT)的可读内存 if (mbi.State == MEM_COMMIT && (mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE))) { // 读取并搜索此区域 ReadProcessMemory(hProcess, mbi.BaseAddress, buffer, mbi.RegionSize, &bytesRead); // 搜索逻辑... } addr = (SIZE_T)mbi.BaseAddress + mbi.RegionSize; } else { addr += 4096; }}代码讲解
| 代码 | 含义 |
|---|---|
MEMORY_BASIC_INFORMATION mbi; | 结构体,存储内存页的详细信息(基址、大小、状态、保护属性等) |
VirtualQueryEx(hProcess, addr, &mbi, sizeof(mbi)) | 查询目标进程中指定地址处的内存信息 |
mbi.State == MEM_COMMIT | 只处理已提交的内存页(实际分配的物理内存) |
mbi.Protect & (...) | 检查保护属性,跳过不可读的页面,避免无效读取 |
mbi.BaseAddress + mbi.RegionSize | 跳到下一个内存区域,比固定 4096 步进更高效 |
课后练习
练习 1:编写程序,使用 ReadProcessMemory 读取目标进程中地址 0x12345678 处的 4 个字节,以十六进制输出。
参考答案
#include <windows.h>#include <iostream>using namespace std;
int main() { DWORD pid; cout << "输入 PID: "; cin >> pid;
HANDLE hProcess = OpenProcess(PROCESS_VM_READ, FALSE, pid); if (!hProcess) { cout << "打开进程失败" << endl; return 1; }
BYTE buffer[4]; SIZE_T bytesRead; LPCVOID addr = (LPCVOID)0x12345678;
if (ReadProcessMemory(hProcess, addr, buffer, 4, &bytesRead)) { cout << "读取成功: "; for (int i = 0; i < 4; i++) { printf("%02X ", buffer[i]); } cout << endl; } else { cout << "读取失败" << endl; }
CloseHandle(hProcess); return 0;}5.6 移动端与综合实战
核心概念
本课时作为综合实战,讲解 C++ 文件 I/O 与字符串流操作,这些技能在数据处理、配置管理和日志系统中非常重要:
- 文件写入(
ofstream) - 文件读取与解析(
ifstream) - 字符串流(
stringstream、ostringstream)
一、文件写入
#include <iostream>#include <fstream>#include <string>using namespace std;
int main() { // 创建输出文件流 ofstream ofs("data.txt");
if (!ofs.is_open()) { cout << "文件打开失败!" << endl; return 1; }
// 写入 CSV 格式数据(逗号分隔) ofs << "姓名" << "," << "年龄" << "," << "成绩" << endl; ofs << "张三" << "," << 18 << "," << 95.5 << endl; ofs << "李四" << "," << 20 << "," << 87.0 << endl; ofs << "王五" << "," << 19 << "," << 92.3 << endl;
ofs.close(); cout << "数据已写入 data.txt" << endl;
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
#include <fstream> | 引入文件流头文件,包含 ifstream、ofstream、fstream |
ofstream ofs("data.txt"); | 创建输出文件流对象,打开文件用于写入(若文件存在则覆盖) |
!ofs.is_open() | 检查文件是否成功打开 |
ofs << "姓名" << "," << "年龄" << "," << "成绩" << endl; | 向文件写入一行 CSV 表头,<< 运算符将数据写入文件 |
ofs << 18 << "," << 95.5 << endl; | 可以混合写入不同类型的数据,自动转换格式 |
ofs.close(); | 关闭文件流,将缓冲区的数据刷入磁盘 |
文件写入语法格式
// 语法格式#include <fstream>ofstream 文件流对象("文件路径"); // 写入模式(覆盖)ofstream 文件流对象("文件路径", ios::app); // 追加模式
文件流对象 << 数据1 << 数据2 << ...;文件流对象.close();
// 完整可运行示例#include <iostream>#include <fstream>using namespace std;
int main() { // 追加模式写入日志 ofstream log("log.txt", ios::app); log << "[INFO] 程序启动" << endl; log << "[INFO] 初始化完成" << endl; log.close(); return 0;}二、文件读取与解析
#include <iostream>#include <fstream>#include <string>#include <sstream>using namespace std;
int main() { // 创建输入文件流 ifstream ifs("data.txt");
if (!ifs.is_open()) { cout << "文件打开失败!" << endl; return 1; }
string line;
// 跳过表头 getline(ifs, line);
// 逐行读取并解析 while (getline(ifs, line)) { stringstream ss(line); // 将每行转为字符串流
string name; int age; double score;
// 使用逗号作为分隔符解析 getline(ss, name, ','); // 读取姓名(遇到逗号停止) ss >> age; // 读取年龄(自动跳过逗号) char comma; ss >> comma; // 跳过逗号 ss >> score; // 读取成绩
cout << name << " | 年龄: " << age << " | 成绩: " << score << endl; }
ifs.close(); return 0;}代码讲解
| 代码 | 含义 |
|---|---|
ifstream ifs("data.txt"); | 创建输入文件流对象,打开文件用于读取 |
getline(ifs, line) | 从文件中读取一整行到 line 字符串 |
stringstream ss(line); | 将字符串 line 转为字符串流,方便按格式提取数据 |
getline(ss, name, ',') | 从字符串流中读取直到遇到逗号,结果存入 name |
ss >> age | 从字符串流中提取一个整数,赋给 age |
ss >> comma | 读取并跳过逗号分隔符 |
ss >> score | 从字符串流中提取一个浮点数,赋给 score |
三、字符串流(StringStream)
#include <iostream>#include <sstream>#include <string>using namespace std;
int main() { // === ostringstream:将多个数据拼接为一个字符串 === ostringstream oss; oss << "Score: " << 95 << "/" << 100; string result = oss.str(); // "Score: 95/100" cout << result << endl;
// === istringstream:从字符串中提取数据 === string data = "Alice 20 95.5"; istringstream iss(data); string name; int age; double score; iss >> name >> age >> score; cout << name << " " << age << " " << score << endl;
// === stringstream:既能读也能写 === stringstream ss; ss << "Value=" << 42; int value; string temp; getline(ss, temp, '='); // 读取 "Value" ss >> value; // 读取 42 cout << temp << " -> " << value << endl;
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
ostringstream oss; | 创建输出字符串流,只能写入 |
oss << "Score: " << 95 << "/" << 100; | 像操作 cout 一样将多种类型的数据写入字符串流 |
oss.str() | 获取字符串流中的完整字符串 |
istringstream iss(data); | 创建输入字符串流,从字符串中读取数据 |
iss >> name >> age >> score | 像操作 cin 一样从字符串流中提取数据,自动类型转换 |
stringstream ss; | 创建双向字符串流,既能读也能写 |
getline(ss, temp, '='); | 从字符串流中读取直到遇到 '=' 字符 |
文件 I/O 语法总结
// ┌──────────────────────────────────────────────────────────┐// │ 文件流类型一览 │// ├──────────────┬───────────────────────────────────────────┤// │ ofstream │ 只写(输出文件流) │// │ ifstream │ 只读(输入文件流) │// │ fstream │ 读写(文件流) │// ├──────────────┼───────────────────────────────────────────┤// │ ostringstream│ 字符串输出流(拼接字符串) │// │ istringstream│ 字符串输入流(解析字符串) │// │ stringstream│ 字符串双向流 │// └──────────────┴───────────────────────────────────────────┘
// 打开模式ios::in // 只读ios::out // 只写ios::app // 追加写入ios::trunc // 截断文件(默认)ios::binary // 二进制模式四、综合实战:简易学生信息管理系统
#include <iostream>#include <fstream>#include <string>#include <sstream>#include <vector>using namespace std;
struct Student { string name; int age; double score;};
// 保存学生数据到文件void saveToFile(const string &filename, const vector<Student> &students) { ofstream ofs(filename); ofs << "姓名,年龄,成绩" << endl; for (const auto &s : students) { ofs << s.name << "," << s.age << "," << s.score << endl; } ofs.close(); cout << "数据已保存到 " << filename << endl;}
// 从文件加载学生数据vector<Student> loadFromFile(const string &filename) { vector<Student> students; ifstream ifs(filename); if (!ifs.is_open()) return students;
string line; getline(ifs, line); // 跳过表头
while (getline(ifs, line)) { stringstream ss(line); Student s; getline(ss, s.name, ','); char comma; ss >> s.age >> comma >> s.score; students.push_back(s); } ifs.close(); return students;}
int main() { // 创建数据 vector<Student> students = { {"张三", 18, 95.5}, {"李四", 20, 87.0}, {"王五", 19, 92.3} };
// 保存到文件 saveToFile("students.csv", students);
// 从文件加载 auto loaded = loadFromFile("students.csv");
// 输出加载的数据 cout << "\n从文件加载的学生数据:" << endl; for (const auto &s : loaded) { cout << s.name << " | " << s.age << " | " << s.score << endl; }
return 0;}代码讲解
| 代码 | 含义 |
|---|---|
struct Student { string name; int age; double score; }; | 定义学生结构体,包含姓名、年龄和成绩三个字段 |
vector<Student> | 使用 vector 容器存储多个学生数据 |
for (const auto &s : students) | 范围 for 循环,遍历 vector 中的每个元素,使用引用避免拷贝 |
saveToFile(filename, students) | 将所有学生数据以 CSV 格式写入文件 |
loadFromFile(filename) | 从 CSV 文件中读取数据,解析为 Student 结构体存入 vector |
getline(ss, s.name, ',') | 解析逗号分隔的姓名字段 |
ss >> s.age >> comma >> s.score | 连续提取年龄、逗号和成绩 |
课后练习
练习 1:编写程序,读取一个文本文件的内容,统计文件中的行数和字符数,并输出结果。
参考答案
#include <iostream>#include <fstream>#include <string>using namespace std;
int main() { string filename; cout << "输入文件名: "; cin >> filename;
ifstream ifs(filename); if (!ifs.is_open()) { cout << "文件打开失败" << endl; return 1; }
int lineCount = 0; int charCount = 0; string line;
while (getline(ifs, line)) { lineCount++; charCount += line.length(); }
ifs.close();
cout << "行数: " << lineCount << endl; cout << "字符数: " << charCount << endl; return 0;}练习 2:使用 ostringstream 将一个整型数组的元素格式化为 "元素1, 元素2, 元素3" 形式的字符串。
参考答案
#include <iostream>#include <sstream>#include <vector>using namespace std;
int main() { vector<int> nums = {10, 20, 30, 40, 50}; ostringstream oss;
for (size_t i = 0; i < nums.size(); i++) { if (i > 0) oss << ", "; oss << nums[i]; }
string result = oss.str(); cout << result << endl; // "10, 20, 30, 40, 50" return 0;}C++ 零基础入门教程 ━━━━━━━━━━━━━━━━━━━━━━━━ 感谢观看 | 持续学习 | 不断进步
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









