mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
24589 字
64 分钟
C++从入门到实战
2026-03-30

第一篇: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(重大更新,引入 autolambda、智能指针等)、C++14、C++17、C++20(引入概念、协程、模块等)和 C++23。每个新标准都在不断完善语言特性,使其更加强大和易用。

编译过程

编译器是将源代码(.cpp 文件)翻译为机器可执行文件的程序。常见的 C++ 编译器包括 MSVC(Windows)、GCC(跨平台)和 Clang(跨平台)。在 Visual Studio 中,默认使用 MSVC 编译器。编译过程分为预处理、编译和链接三个阶段:

  1. 预处理:处理 #include(头文件包含)和 #define(宏定义)等预处理指令,生成扩展后的源代码。
  2. 编译:将预处理后的源代码翻译为目标文件(.obj),进行语法检查和优化。
  3. 链接:将目标文件与库文件合并,生成最终的可执行文件(.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(输出)等对象。注意:预处理指令不以分号结尾。
2using namespace std;C++ 标准库中的所有名称(如 coutendlcin)都定义在 std(standard)命名空间中。这行代码表示”使用标准命名空间”,这样我们就可以直接写 cout 而不用写 std::cout
3int main() {main 是每个 C++ 程序的入口函数,程序从 main 函数的第一行开始执行。int 表示该函数返回一个整数值。() 中是参数列表,这里为空。{ 标记函数体的开始。
4cout << "Hello, C++!" << endl;cout 是 “character output” 的缩写,是标准输出流对象。<<流插入运算符,将右侧的内容发送到输出流。endl 是 “end line” 的缩写,输出一个换行符并刷新缓冲区。
5return 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;
}
参考答案

该代码不能正确编译。因为程序使用了 coutendl,它们定义在 <iostream> 头文件中。如果没有 #include <iostream> 这行预处理指令,编译器就不知道 coutendl 是什么,会报出”未声明的标识符”(undeclared identifier)错误。解决方法是在文件开头添加 #include <iostream>


1.2 基础语法#

基本数据类型、变量与常量、运算符#

知识点概述#

C++ 是一种强类型语言,这意味着每个变量在使用前都必须明确声明其数据类型。C++ 提供了丰富的基本数据类型,包括整型(int)、浮点型(floatdouble)、字符型(char)和布尔型(bool)等。

变量是程序中用于存储数据的”容器”,每个变量都有类型、名称和值。常量则是程序运行过程中固定不变的值,一旦定义就不能被修改。

运算符是用于执行各种运算的符号,包括算术运算符、关系运算符和逻辑运算符等。理解运算符的优先级对于编写正确的表达式至关重要。

核心概念#

基本数据类型#

C++ 的基本数据类型及其大小和范围如下:

类型大小(字节)说明典型范围
short2短整型-32768 ~ 32767
int4整型约 ±21 亿
long4 或 8长整型取决于平台
long long8更长整型约 ±9.2 × 10¹⁸
float4单精度浮点型约 7 位有效数字
double8双精度浮点型约 15 位有效数字
char1字符型-128 ~ 127 或 0 ~ 255
bool1布尔型truefalse

变量声明与初始化#

// 变量声明的语法格式
数据类型 变量名 = 初始值;
// 也可以先声明,再赋值
数据类型 变量名;
变量名 = 初始值;
// C++11 支持的列表初始化(推荐)
数据类型 变量名{初始值};

常量定义#

// 方式一:使用 const 关键字(推荐)
const 数据类型 常量名 = 值;
// 方式二:使用 #define 宏定义(C 风格,不推荐)
#define 宏名 值

注意const 定义的常量有类型检查,更加安全;#define 只是简单的文本替换,没有类型检查。

运算符优先级#

运算符优先级从高到低排列:

  1. 乘、除、取模* / %
  2. 加、减+ -
  3. 关系运算符> < >= <= == !=
  4. 逻辑运算符!(非)&&(与)||(或)

变量、常量与运算符示例#

// 基础语法示例 —— 演示变量声明、常量定义和运算符使用
#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 / ba 强制转换为 double,此时 / 执行浮点除法,b 也会被自动提升为 double,最终结果为 3.33333

常量定义const double PI = 3.14159265; 声明了一个名为 PIdouble 类型常量,初始化为 3.14159265const 关键字告诉编译器这个值不可修改,如果后续尝试对 PI 赋值,编译器会报错。

字符型char 类型用于存储单个字符,字符用单引号包裹(如 'A'),而字符串用双引号包裹(如 "Hello")。字符在底层实际存储的是其 ASCII 码值,'A' 对应 ASCII 码 65。通过 (int)ch 可以查看字符的 ASCII 值。

布尔型bool 类型只有两个值:truefalse。比较表达式(如 a > b)的结果就是布尔值。在输出时,true 显示为 1false 显示为 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 可以编写出平台无关的代码。
  • 数值字面量的后缀(如 LLLf)用于明确指定字面量的类型,避免编译器的类型推断警告。

重点难点#

  • 整数除法陷阱:两个整数相除结果仍为整数,小数部分被截断。需要浮点结果时,应使用强制类型转换。
  • 运算符优先级:乘除取模 > 加减 > 关系运算符 > 逻辑运算符。不确定优先级时,多用括号。

学习建议#

  • 多写代码尝试不同的数据类型,理解它们的范围和精度差异。
  • 养成使用 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 = 7b = 2,分别计算并输出:

  1. a / b 的结果(整数除法)
  2. (double)a / b 的结果(浮点除法)
  3. 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-elseswitch-case,用于根据条件选择不同的执行路径
  • 循环语句forwhiledo-while,用于重复执行某段代码
  • 跳转语句breakcontinue,用于改变循环的正常执行流程

掌握流程控制是编程的基本功,几乎所有程序都离不开条件判断和循环。

核心概念#

条件语句的语法格式#

// 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 时要注意更新循环变量,避免死循环。

学习建议#

  • 多画流程图帮助理解条件判断和循环的逻辑。
  • 尝试用不同的循环方式(forwhiledo-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=1
1×2=2 2×2=4
1×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 的数组,有效索引范围是 0n-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 &aint &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;
// 定义自定义命名空间 MyMath
namespace 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 的命名空间,将 addsubtract 函数组织在其中。命名空间类似于文件夹的概念,用于避免不同代码之间的名称冲突。
  • 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)。类是对象的蓝图或模板,而对象是类的具体实例。

本节将介绍类的基本概念,包括如何定义类、声明构造函数、编写成员函数,以及使用 privatepublic 等访问控制修饰符来实现封装。

核心概念#

类定义的语法格式#

// 类定义的语法格式
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):构造函数,函数名与类名相同,没有返回类型。参数 nas 分别用于初始化姓名、年龄和成绩。
  • : 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; 这行代码是错误的,因为 scoreprivate 成员,只能在类内部访问。这就是封装的作用——保护数据不被外部随意修改。

类的创建与使用语法总结#

// 创建对象的语法格式
类名 对象名(构造函数参数);
// 调用公有成员函数的语法格式
对象名.成员函数名(参数);
// 访问公有成员变量的语法格式
对象名.成员变量名;

重点难点#

  • 封装的理解: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 的面积更大

代码讲解:同一个类可以创建多个独立的对象。每个对象都有自己的 widthheight 副本,互不影响。通过点运算符 . 分别调用各自的方法,获取各自的数据。


1.6 C++ 核心特性补充#

函数重载、默认参数、动态内存管理#

知识点概述#

本节补充介绍 C++ 的几个重要特性:函数重载(Function Overloading)、默认参数(Default Arguments)和动态内存管理(Dynamic Memory Management)。这些特性让 C++ 程序更加灵活和强大。

  • 函数重载允许定义多个同名函数,只要它们的参数列表(参数类型或数量)不同即可。
  • 默认参数允许在函数声明中为参数指定默认值,调用时可以省略这些参数。
  • 动态内存管理使用 newdelete 运算符在程序运行时动态地分配和释放内存。

核心概念#

函数重载的语法格式#

// 函数重载的语法格式:同名函数,参数列表不同
返回类型 函数名(参数类型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。
  • 函数重载的好处是:可以使用一个直观的函数名来处理多种情况,而不需要取 addIntaddDoubleaddThreeInt 这样冗长的名字。
  • 在每个函数内部添加了 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 = 0string city = "未知" 为参数设置了默认值。调用函数时,如果省略了某个参数,编译器会使用对应的默认值。
  • 默认参数必须从右到左连续设置。即,如果某个参数有默认值,它右边的所有参数也必须有默认值。不能出现左边有默认值而右边没有的情况。这是因为 C++ 的参数匹配是按位置从左到右进行的,不能跳过中间的参数。
  • print("张三", 18, "北京"):提供了所有三个参数,默认值不生效。
  • print("李四", 20):省略了 city,使用默认值 "未知"
  • print("王五"):省略了 agecity,分别使用默认值 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)较大(受物理内存限制)
速度较慢
生命周期局部的(作用域内)由程序员控制

重点难点#

  • 函数重载的判断依据是参数列表,不能仅靠返回类型区分。
  • 默认参数必须从右向左连续设置,调用时不能跳过中间参数。
  • newdelete 必须配对使用new 对应 deletenew[] 对应 delete[]
  • 内存泄漏是 C++ 常见的 bug,忘记 delete 会导致内存被永久占用。

学习建议#

  • 在实际项目中,建议优先使用 C++ 标准库的智能指针(如 std::unique_ptrstd::shared_ptr)来管理动态内存,它们会自动释放内存,避免内存泄漏。
  • 注意 delete 后将指针置为 nullptr 的好习惯。
  • 初学者可以先理解 newdelete 的基本用法,后续深入学习智能指针后,可以完全替代手动内存管理。

课后练习#

练习 1:函数重载——计算面积

编写三个同名的 area 函数,分别计算:

  1. 正方形面积(参数:边长 double side
  2. 矩形面积(参数:长度 double length,宽度 double width
  3. 圆形面积(参数:半径 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] 动态分配了 sizeint 的内存空间,返回首元素的地址赋给指针 arr
  • cin >> arr[i] 从标准输入读取数据,cin 是标准输入流对象,>> 是流提取运算符。
  • (double)sum / size 使用强制类型转换避免整数除法陷阱,确保得到精确的平均值。
  • delete[] arr 释放数组内存,必须使用 delete[] 而不是 delete。然后将指针置为 nullptr 防止悬空指针。

本篇小结#

经过第一篇的学习,你已经掌握了 C++ 的核心基础知识:

课时主题关键知识
1.1C++ 概述与环境搭建C++ 历史、编译过程、Visual Studio 配置、Hello World
1.2基础语法数据类型、变量与常量、运算符优先级、整数除法陷阱
1.3流程控制if-else、switch-case、for、while、break、continue
1.4数组与函数数组声明与遍历、函数声明与定义、引用、命名空间
1.5类与面向对象入门类定义、构造函数、封装、getter/setter
1.6C++ 核心特性补充函数重载、默认参数、动态内存管理(new/delete)

这些知识是 C++ 编程的基石。在后续的学习中,我们将深入探讨指针的高级用法、面向对象编程的继承与多态、模板编程、标准模板库(STL)等重要主题。持续练习、多写代码是学好 C++ 的关键。

🎯 下一步学习建议:尝试不看教程,独立编写一个小程序(如简单的计算器、学生成绩管理系统),检验本篇所学知识的掌握程度。

第二篇:面向对象编程深入#

本篇深入探讨 C++ 面向对象编程的核心概念和高级特性,涵盖面向对象思想、构造与析构函数、特殊成员(this 指针、static、const、友元)、继承与派生、以及多态等内容。


2.1 面向对象思想#

知识点概述#

面向对象编程(Object-Oriented Programming, OOP)是 C++ 的核心编程范式。本节将对比面向过程与面向对象两种编程思想,理解类与对象的关系。

核心概念#

  • 面向过程:程序 = 数据结构 + 算法。以函数为中心,数据在各函数间传递。
  • 面向对象:程序 = 对象 + 消息传递。以对象为中心,数据和操作数据的方法被封装在一起。
  • class 默认访问权限为 privatestruct 默认访问权限为 public

class 与 struct 的区别#

// class 默认权限为 private
class MyClass {
int x; // 默认 private
public:
int y; // 显式声明 public
};
// struct 默认权限为 public
struct MyStruct {
int x; // 默认 public
private:
int y; // 显式声明 private
};

代码讲解

  • class 定义的成员默认是 private 的,外部无法直接访问,需要通过 public 接口。
  • struct 定义的成员默认是 public 的,外部可以直接访问。
  • 除了默认访问权限不同外,classstruct 在 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 结构体作为数据结构,包含 xy 两个成员。
  • 第 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(类型/蓝图),定义了数据的结构和操作。
  • s1s2对象(实例),每个对象都有自己独立的 nameage 数据。
  • 同一个类的不同对象共享相同的成员函数代码,但各自拥有独立的数据。

构造函数语法格式#

// 构造函数声明/定义语法
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 表示是否成功。
  • getBalanceconst 成员函数,只读查询不修改数据。

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. 析构函数
  2. 拷贝构造函数
  3. 拷贝赋值运算符

课后练习#

练习 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 的私有成员 xy
  • 友元关系是单向的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 声明 MechanicCar 的友元类,Mechanic 的所有成员函数都可以访问 Car 的私有成员。
  • 第 16-20 行:Mechanic::inspect 中直接访问 c.modelc.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 在构造函数中递增、析构函数中递减,始终反映当前存活的员工数量。
  • setNamesetSalary 返回 *this,支持链式调用。
  • compareSalary 是友元函数,可以直接访问 Employee 的私有成员 namesalary
  • operator<< 重载使 Employee 对象可以直接用 cout 输出。

2.4 继承与派生#

知识点概述#

继承是面向对象编程的核心机制之一,允许创建新类(派生类)来复用和扩展现有类(基类)的功能。

核心概念#

  • 继承方式publicprotectedprivate 继承
  • 构造/析构顺序:先构造基类,再构造派生类;先析构派生类,再析构基类
  • 菱形继承与虚继承:解决多重继承中的二义性问题

继承语法格式#

// 单继承语法
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 成员在派生类中仍为 publicprotected 仍为 protectedprivate 不可访问。这体现了 “is-a” 关系(派生类是一种基类)。
  • protected 继承:基类的 publicprotected 成员在派生类中都变为 protected
  • private 继承:基类的 publicprotected 成员在派生类中都变为 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 和 Bird
class 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 类通过 MammalBird 两条路径继承了 Animal,导致 Bat 中存在两份 Animal 的数据(两个 age)。
  • b.age 产生二义性错误,必须使用 b.Mammal::ageb.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 中只有一份 Animal
class 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 关键字,使 MammalBird 虚继承 Animal
  • 虚继承后,Bat 中只保留一份 Animal 的数据,消除了二义性。
  • 虚继承的底层实现使用了虚基类表(vbtable),由编译器管理,会有轻微的性能开销。
  • 注意:虚继承时,最终派生类(Bat)负责直接初始化虚基类(Animal)。

虚继承语法格式#

// 虚继承语法
class Derived : virtual public Base {
// 派生类成员
};
// 多重虚继承
class Final : public Derived1, public Derived2 {
// Derived1 和 Derived2 都 virtual 继承 Base
// Final 中只有一份 Base
public:
Final() : Base(参数) { } // 最终派生类负责初始化虚基类
};

课后练习#

练习 1:设计一个简单的图形类层次结构。基类 Shape 有颜色属性和计算面积的方法;派生类 RectangleTriangle 分别实现各自的面积计算。要求演示构造/析构顺序。

参考答案
#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 是抽象基类,提供颜色属性和虚析构函数。
  • RectangleTriangle 的构造函数通过初始化列表调用 Shape(c) 初始化基类。
  • area() 使用 override 覆盖基类的虚函数。
  • 析构顺序严格遵循”先派生后基类”规则。

练习 2:实现一个简单的”交通工具”类层次。基类 Vehicle,派生类 CarElectricCarElectricCar 继承自 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 finalbreathe 标记为 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——继承关系CircleSquare 都继承自 Shape
  • 条件 2——虚函数area()draw() 在基类中声明为 virtual
  • 条件 3——基类指针/引用vector<unique_ptr<Shape>> 存储基类智能指针,每个指针实际指向不同的派生类对象。
  • 第 40-44 行:在循环中通过基类指针调用 draw()area(),程序在运行时根据对象的实际类型(CircleSquare)决定调用哪个版本。
  • 使用 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;
}

代码讲解

  • 如果基类析构函数不是 virtualdelete p 只调用 Base::~Base()Derived::~Derived() 不会被调用,data 指向的内存泄漏。
  • 如果基类析构函数 virtualdelete p 先调用 Derived::~Derived()(释放 data),再调用 Base::~Base()
  • 经验法则:只要类可能被继承且通过基类指针管理对象生命周期,基类的析构函数就必须声明为 virtual

课后练习#

练习 1:设计一个简单的”图形编辑器”。定义抽象基类 Shape,包含纯虚函数 area()draw()perimeter()。派生类 CircleRectangleTriangle 分别实现这三个方法。使用 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()
  • CircleRectangleTriangle 分别实现了各自的计算逻辑。
  • 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,包装另一个 Shape
class 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 *thisthis 是指向当前对象的指针,*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

选择原则:

  1. 必须用友元函数:当左操作数不是当前类对象时(如 ostream << obj
  2. 必须用成员函数=[]()->type() 只能作为成员函数重载
  3. 推荐友元函数:对称二元运算符(如 a + bb + 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 + Complex
Complex 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 + ComplexComplex + double(double 会隐式构造为 Complex)的情况
friend Complex operator+(double d, const Complex &c)声明友元函数以实现 double + Complex。因为左操作数 ddouble 类型,无法调用成员函数
Complex(d + c.real, c.imag)将 double 加到实部上,虚部保持不变,返回新的 Complex 对象

3.1.3 不能重载的运算符#

以下是 C++ 中不允许重载的运算符:

运算符说明
.成员访问运算符
.*成员指针访问运算符
::作用域解析运算符
?:三目条件运算符
sizeof获取类型/对象大小
typeid运行时类型识别

重载注意事项:

  1. 不能改变运算符的优先级和结合性
  2. 不能改变运算符的操作数个数(一元运算符不能变成二元)
  3. 不能发明新的运算符符号
  4. 重载运算符应保持语义一致性(+ 应该做加法,不应该做减法)
  5. 至少有一个操作数必须是用户自定义类型

课后练习#

练习 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>声明一个模板参数 TT 代表任意类型。编译器会根据调用时的实参自动推导 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>声明两个不同的类型参数 T1T2,可以接受不同类型的参数
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 动态分配 capacityT 类型的元素空间
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 的私有成员 firstsecond,因为已被声明为友元函数

课后练习#

练习 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++ 的表达能力、安全性和编程效率。

本节核心概念:

  • 异常处理:trycatchthrow 机制
  • 文件流:ifstreamofstream 的使用
  • 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(输出文件流)来读写文件,使用方式与 cincout 类似。

语法格式#

#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.firstpair.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_uniquenew 更安全(避免异常安全问题)
unique_ptr<Student> p2 = move(p1)unique_ptr 不可拷贝(编译报错),只能通过 move 转移所有权。转移后 p1 变为 nullptr
p2->show()通过 -> 运算符访问智能指针指向对象的成员函数,用法与原始指针一致
make_shared<Student>("Bob", 88)创建 shared_ptrmake_shared 一次性分配对象和控制块内存,比 new 更高效
sp1.use_count()返回当前有多少个 shared_ptr 指向同一个对象(引用计数)
shared_ptr<Student> sp2 = sp1sp1sp2 共享同一个对象,引用计数增加到 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; // 10
cout << *(p + 2) << endl; // 30
cout << 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; });当前线程释放锁并阻塞等待,直到被通知且 readytrue 时才继续执行
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 为泛型类型参数,使用时可以替换为 intstring 等任意类型
struct Node { T data; Node* next; ... }定义私有内部结构体 Node,包含数据域 data 和指针域 next
Node(const T &val) : data(val), next(nullptr) {}Node 的构造函数,使用初始化列表,将 data 设为 valnext 设为空指针
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 GameObjectPlayer 类公有继承自 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)runsigned char 输出时默认按字符显示,强制转为 int 才能输出数值
(r << 16) | (g << 8) | b将 R/G/B 三个通道通过左移和按位或合成为一个 24 位颜色值
hex / decI/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;
}

代码讲解#

代码含义
0b00010110C++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 位为 1
value |= (1 << n);
// 清除第 n 位为 0
value &= ~(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); // 获取窗口 DC
COLORREF color = GetPixel(hdc, x, y); // 取色
int r = GetRValue(color); // 提取 R
int g = GetGValue(color); // 提取 G
int b = GetBValue(color); // 提取 B
ReleaseDC(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.dllLoadLibraryA 函数的地址
(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 >= 0nCode 大于等于 0 时,可以处理此消息
CallNextHookEx(...)将消息传递给钩子链中的下一个钩子
SetWindowsHookEx(WH_KEYBOARD_LL, ...)安装低级键盘钩子,WH_KEYBOARD_LL 表示全局键盘钩子
GetModuleHandle(NULL)获取当前程序模块的句柄
GetMessage(&msg, NULL, 0, 0)从消息队列中获取消息,阻塞等待
TranslateMessage / DispatchMessage翻译并分发消息
UnhookWindowsHookEx(hook)卸载钩子,释放系统资源

课后练习#

练习 1:简述 DLL 注入的四个步骤,并说明每一步调用的 Win32 API 函数名称。

参考答案
  1. 打开目标进程:调用 OpenProcess() 获取目标进程句柄
  2. 分配内存:调用 VirtualAllocEx() 在目标进程中分配可读写内存
  3. 写入数据:调用 WriteProcessMemory() 将 DLL 路径写入目标进程
  4. 创建远程线程:调用 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);
}
// 安装 Hook
void 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 安装流程:

  1. 调用 VirtualProtect 将目标函数入口处的内存保护属性改为 PAGE_EXECUTE_READWRITE
  2. 备份原始 5 个字节
  3. 计算跳转偏移量:offset = hookAddr - targetAddr - 5
  4. 写入 0xE9(JMP 操作码)和 4 字节偏移量
  5. 恢复原始内存保护属性

需要 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 &gGDI+ 绘图对象引用
RectF rect浮点精度的矩形区域,包含 XYWidthHeight
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
  • 字符串流(stringstreamostringstream

一、文件写入#

#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>引入文件流头文件,包含 ifstreamofstreamfstream
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++ 零基础入门教程 ━━━━━━━━━━━━━━━━━━━━━━━━ 感谢观看 | 持续学习 | 不断进步

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

C++从入门到实战
http://blog.mcstarland.top/posts/cpp/
作者
MEMZGBL
发布于
2026-03-30
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00