从零到专业的 C 与 C++:100 个编译通过的示例与真实输出
C 是一门小而快的语言,贴近硬件运行。C++ 在 C 的基础上加入了类、模板和庞大的标准库,广泛用于游戏、浏览器、数据库和嵌入式系统。本文用 100 个示例,带你从第一个 printf 一直学到现代 C++20。前 50 个示例是 C:变量、流程控制、函数、数组、指针、结构体、内存、文件和预处理器。后 50 个是 C++:引用、类、RAII、智能指针、模板、标准库、概念、异常、线程和格式化。每个示例都用 GCC 16 编译并运行,代码之后显示的是真实输出。
需要准备的东西
安装了 GCC 或 Clang 的电脑(Windows 上安装 WinLibs GCC 或 MSYS2 工具链;macOS 上安装 Xcode 命令行工具;Linux 上使用包管理器)。用 gcc --version 与 g++ --version 检查安装。普通的文本编辑器即可。不需要编程经验,但前几个示例假设你已经知道数字和文字是什么。
如何编译和运行
C 示例使用 gcc -std=c17,C++ 示例使用 g++ -std=c++20,每个示例一个文件。例如把 C++ 程序保存为 main.cpp,用 g++ -std=c++20 -Wall -Wextra main.cpp -o main 编译,然后运行 ./main。文中显示的输出来自在 GCC 16 上的真实运行。
知识点 1: 入门、变量与类型
每个 C 程序都从 main 开始。这些示例展示如何打印、把值保存到变量中,并为每个值选择合适的类型。
C 程序的结构
C 程序是一组函数,程序从 main 开始执行。#include <stdio.h> 引入 printf 函数。printf 打印文字,格式说明符 %d、%f、%c 分别打印 int、double 和 char。main 返回 0 表示执行成功。
整数类型与浮点类型
int、short、long 存储整数,但具体大小取决于平台,因此可以用 sizeof 来确认。float 和 double 存储精度有限的小数。1.0 / 3.0 并不完全等于三分之一,所以用更多位数打印时会看到误差。
常量
const 创建一个有类型的值,编译器会防止它被修改。#define 在编译器运行之前替换文本,因此没有类型。新代码中有类型的值应使用 const,#define 主要用于编译期的配置。
示意图
示例 1: Hello, World
你的第一个程序:打印一行文字。
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}
输出:
Hello, World!
示例 2: 变量与格式说明符
保存值,并用 printf 的占位符打印出来。
#include <stdio.h>
int main(void) {
int apples = 7;
double price = 2.5;
char initial = 'A';
printf("%d apples, %.2f each, initial %c\n", apples, price, initial);
printf("total = %.2f\n", apples * price);
return 0;
}
输出:
7 apples, 2.50 each, initial A
total = 17.50
示例 3: 整数类型与 sizeof
每种整数类型占用的字节数不同。
#include <stdio.h>
int main(void) {
printf("char : %zu byte(s)\n", sizeof(char));
printf("short : %zu byte(s)\n", sizeof(short));
printf("int : %zu byte(s)\n", sizeof(int));
printf("long : %zu byte(s)\n", sizeof(long));
printf("long long: %zu byte(s)\n", sizeof(long long));
return 0;
}
输出:
char : 1 byte(s)
short : 2 byte(s)
int : 4 byte(s)
long : 4 byte(s)
long long: 8 byte(s)
示例 4: 浮点数与精度
float 和 double 存储精度有限的小数。
#include <stdio.h>
int main(void) {
float f = 1.0f / 3.0f;
double d = 1.0 / 3.0;
printf("float : %.10f\n", f);
printf("double : %.10f\n", d);
printf("0.1 + 0.2 == 0.3 ? %s\n", (0.1 + 0.2 == 0.3) ? "yes" : "no");
return 0;
}
输出:
float : 0.3333333433
double : 0.3333333333
0.1 + 0.2 == 0.3 ? no
示例 5: char 与 ASCII 码
char 是小整数,'a' 的值是 97。
#include <stdio.h>
int main(void) {
char c = 'a';
printf("%c is %d\n", c, c);
printf("upper of %c is %c\n", c, c - 'a' + 'A');
printf("digit '7' minus '0' = %d\n", '7' - '0');
return 0;
}
输出:
a is 97
upper of a is A
digit '7' minus '0' = 7
示例 6: 常量:const 与 #define
程序不得修改的固定值。
#include <stdio.h>
#define MAX_USERS 100
static const double TAX_RATE = 0.07;
int main(void) {
double price = 200.0;
printf("max users: %d\n", MAX_USERS);
printf("price with tax: %.2f\n", price * (1.0 + TAX_RATE));
return 0;
}
输出:
max users: 100
price with tax: 214.00
知识点 2: 运算符与类型转换
运算符组合值。在类型之间转换是常见的错误来源,因此值得尽早掌握规则。
算术运算与整数除法
+、-、*、/、% 作用于数字。两个 int 相除会丢掉小数部分,例如 17 / 5 的结果是 3。若要保留小数,请使用 17.0 / 5 这样的 double 操作数。% 运算符给出余数。
自增、复合赋值与比较
++ 和 -- 把变量加一或减一,写在名字前面还是后面决定了变化何时可见。+= 和 *= 一步完成更新。在 C 中,比较结果为真时是 1,为假时是 0,除非包含 stdbool.h,否则没有单独的布尔类型。
位运算符与类型转换
&、|、^、~ 以及移位运算符 << 和 >> 作用于整数的各个位,常用于标志位。像 (double) 这样的强制转换只在一个表达式中改变值的类型,不会改变变量本身。
示例 7: 算术运算与整数除法
两个 int 相除会丢掉小数部分。
#include <stdio.h>
int main(void) {
int a = 17, b = 5;
printf("%d %d %d %d %d\n", a + b, a - b, a * b, a / b, a % b);
printf("17.0 / 5 = %.2f\n", 17.0 / b);
return 0;
}
输出:
22 12 85 3 2
17.0 / 5 = 3.40
示例 8: 自增与复合赋值
++、-- 和 += 一步改变变量的值。
#include <stdio.h>
int main(void) {
int n = 5;
printf("%d\n", n++);
printf("%d\n", ++n);
n += 10;
n *= 2;
printf("final %d\n", n);
return 0;
}
输出:
5
7
final 34
示例 9: 比较与逻辑运算符
在 C 中,真为 1,假为 0。
#include <stdio.h>
int main(void) {
int age = 20;
int has_ticket = 1;
printf("adult: %d\n", age >= 18);
printf("can enter: %d\n", age >= 18 && has_ticket);
printf("needs help: %d\n", !has_ticket || age < 12);
return 0;
}
输出:
adult: 1
can enter: 1
needs help: 0
示例 10: 位运算符与移位
作用于整数的每一个二进制位。
#include <stdio.h>
int main(void) {
unsigned int flags = 0x5;
printf("flags & 4 = %u\n", flags & 4u);
printf("flags | 2 = %u\n", flags | 2u);
printf("flags ^ 1 = %u\n", flags ^ 1u);
printf("1 << 4 = %u\n", 1u << 4);
printf("256 >> 2 = %u\n", 256u >> 2);
return 0;
}
输出:
flags & 4 = 4
flags | 2 = 7
flags ^ 1 = 4
1 << 4 = 16
256 >> 2 = 64
示例 11: 强制类型转换
用 (类型) 把值转换为另一种类型。
#include <stdio.h>
int main(void) {
int total = 7, count = 2;
double average = (double)total / count;
printf("average = %.2f\n", average);
printf("truncated = %d\n", (int)3.99);
printf("mixed = %.1f\n", total / 2 + 0.5);
return 0;
}
输出:
average = 3.50
truncated = 3
mixed = 3.5
知识点 3: 条件判断与循环
流程控制决定哪些代码会执行,以及执行多少次。选择与问题匹配的语句:用 if 做选择,用 switch 处理一组值,用循环做重复。
if、switch 与条件运算符
if 与 else 根据条件选择一个分支。switch 按值选择 case。如果没有 break,执行会穿透到下一个 case,这有时有用,但也是常见的 bug。条件运算符是能产生值的简短 if-else。
for、while 与 do-while
for 最适合已知重复次数的情况。while 在每次循环前检查条件,因此可能一次也不执行。do-while 至少执行一次循环体。选择让条件最易读的循环。
break、continue 与嵌套循环
break 跳出最内层的循环,continue 跳到下一次循环。循环中嵌套循环是遍历网格或表格的常用方式。嵌套循环应保持简短,并为计数器起清晰的名字。
示例 12: if、else if 与 else
从多个条件中选择一个分支。
#include <stdio.h>
int main(void) {
int score = 73;
if (score >= 80) {
printf("A\n");
} else if (score >= 70) {
printf("B\n");
} else {
printf("C\n");
}
return 0;
}
输出:
B
示例 13: switch 与穿透
按值选择 case,break 阻止继续执行下一个 case。
#include <stdio.h>
int main(void) {
for (int day = 1; day <= 3; day++) {
switch (day) {
case 1:
case 2:
printf("day %d: early\n", day);
break;
default:
printf("day %d: later\n", day);
break;
}
}
return 0;
}
输出:
day 1: early
day 2: early
day 3: later
示例 14: 三元运算符
能产生值的简洁 if-else。
#include <stdio.h>
int main(void) {
int a = 4, b = 9;
int larger = (a > b) ? a : b;
printf("larger = %d\n", larger);
printf("%s\n", (larger % 2 == 0) ? "even" : "odd");
return 0;
}
输出:
larger = 9
odd
示例 15: for 循环
用计数器重复执行代码块。
#include <stdio.h>
int main(void) {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
printf("sum 1..10 = %d\n", sum);
return 0;
}
输出:
sum 1..10 = 55
示例 16: while 与 do-while
按条件循环;do-while 至少执行一次。
#include <stdio.h>
int main(void) {
int n = 3;
while (n > 0) {
printf("while %d\n", n--);
}
int m = 0;
do {
printf("do-while runs once, m = %d\n", m);
} while (m > 0);
return 0;
}
输出:
while 3
while 2
while 1
do-while runs once, m = 0
示例 17: break 与 continue
提前跳出循环,或跳到下一次循环。
#include <stdio.h>
int main(void) {
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) continue;
if (i > 7) break;
printf("%d ", i);
}
printf("\n");
return 0;
}
输出:
1 3 5 7
示例 18: 嵌套循环:表格
循环中再套循环,打印出网格。
#include <stdio.h>
int main(void) {
for (int row = 1; row <= 3; row++) {
for (int col = 1; col <= 3; col++) {
printf("%3d", row * col);
}
printf("\n");
}
return 0;
}
输出:
1 2 3
2 4 6
3 6 9
知识点 4: 函数
函数为一段代码命名,接收输入并返回结果。好的函数只做一件事,并且易于测试。
参数与返回值
在函数名之前声明每个参数的类型和返回值的类型。C 按值传递参数,因此函数处理的是副本。用 return 返回结果,不返回任何值时使用 void。
用指针作为参数
要修改调用者的变量,就用 & 传递它的地址,并用 * 读写。经典的交换函数展示了区别:按值传递的版本只交换了它自己的副本。
递归与静态局部变量
递归函数会调用自己,必须有终止条件,否则永远不会结束并导致栈溢出。静态局部变量在两次调用之间保留其值,适合计数器和缓存,但会让函数更难测试。
示例 19: 带返回值的函数
为代码块命名,并返回结果。
#include <stdio.h>
static int add(int a, int b) {
return a + b;
}
static double average(int a, int b) {
return (a + b) / 2.0;
}
int main(void) {
printf("%d\n", add(3, 4));
printf("%.1f\n", average(3, 4));
return 0;
}
输出:
7
3.5
示例 20: 值传递与指针传递
复制不会改变调用者的变量,指针可以。
#include <stdio.h>
static void try_swap_value(int a, int b) {
int t = a; a = b; b = t;
}
static void swap(int *a, int *b) {
int t = *a; *a = *b; *b = t;
}
int main(void) {
int x = 1, y = 2;
try_swap_value(x, y);
printf("after value swap: %d %d\n", x, y);
swap(&x, &y);
printf("after pointer swap: %d %d\n", x, y);
return 0;
}
输出:
after value swap: 1 2
after pointer swap: 2 1
示例 21: 递归
调用自身的函数,需要有终止条件。
#include <stdio.h>
static unsigned long long factorial(unsigned int n) {
return n <= 1 ? 1ULL : n * factorial(n - 1);
}
static unsigned long long fib(unsigned int n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
int main(void) {
printf("10! = %llu\n", factorial(10));
printf("fib(20) = %llu\n", fib(20));
return 0;
}
输出:
10! = 3628800
fib(20) = 6765
示例 22: 静态局部变量
在两次调用之间保留值的局部变量。
#include <stdio.h>
static int next_id(void) {
static int counter = 0;
counter++;
return counter;
}
int main(void) {
for (int i = 0; i < 3; i++) {
printf("id %d\n", next_id());
}
return 0;
}
输出:
id 1
id 2
id 3
知识点 5: 数组与字符串
数组保存多个同一类型的值,下标从 0 开始。在 C 中,字符串是以零字节结尾的 char 数组。
数组及其长度
int scores[5] 创建五个 int。C 不检查下标,因此越过末尾的写入会悄悄地破坏内存。若数组在同一个函数中声明,sizeof array / sizeof array[0] 可得到元素个数。
二维数组与字符串数组
int grid[2][3] 是两行三列的表格。const char 指针的数组可以保存多个长度不同的字符串,这是保存名字列表的常用方式。
字符串函数
strlen 统计零字节之前的字符数。strcpy 与 strcat 复制和拼接字符串,但不检查缓冲区大小,因此很容易误用。strcmp 比较字符串,相等时返回 0。
示例 23: 数组
固定数量、同一类型的值,下标从 0 开始。
#include <stdio.h>
int main(void) {
int scores[5] = {90, 75, 88, 64, 95};
for (int i = 0; i < 5; i++) {
printf("scores[%d] = %d\n", i, scores[i]);
}
return 0;
}
输出:
scores[0] = 90
scores[1] = 75
scores[2] = 88
scores[3] = 64
scores[4] = 95
示例 24: 求和、最小值与最大值
只遍历一次数组,同时跟踪三个结果。
#include <stdio.h>
int main(void) {
int values[] = {12, 7, 19, 3, 25};
int n = (int)(sizeof values / sizeof values[0]);
int sum = 0, min = values[0], max = values[0];
for (int i = 0; i < n; i++) {
sum += values[i];
if (values[i] < min) min = values[i];
if (values[i] > max) max = values[i];
}
printf("sum=%d min=%d max=%d avg=%.2f\n", sum, min, max, (double)sum / n);
return 0;
}
输出:
sum=66 min=3 max=25 avg=13.20
示例 25: 二维数组
由行和列组成的值表。
#include <stdio.h>
int main(void) {
int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
for (int r = 0; r < 2; r++) {
for (int c = 0; c < 3; c++) {
printf("%d ", grid[r][c]);
}
printf("\n");
}
return 0;
}
输出:
1 2 3
4 5 6
示例 26: 字符数组形式的字符串
字符串是以零字节结尾的 char 数组。
#include <stdio.h>
int main(void) {
char name[] = "Ada";
printf("%s has %zu characters\n", name, sizeof name - 1);
name[0] = 'E';
printf("now: %s\n", name);
return 0;
}
输出:
Ada has 3 characters
now: Eda
示例 27: string.h 中的字符串函数
strlen、strcpy、strcat 和 strcmp。
#include <stdio.h>
#include <string.h>
int main(void) {
char buffer[32] = "Hello";
strcat(buffer, ", C");
printf("%s (%zu)\n", buffer, strlen(buffer));
printf("compare: %d\n", strcmp("apple", "banana") < 0);
char copy[32];
strcpy(copy, buffer);
printf("copy: %s\n", copy);
return 0;
}
输出:
Hello, C (8)
compare: 1
copy: Hello, C
知识点 6: 指针
指针保存值的地址。指针是 C 的核心,它让函数能修改数据、遍历数组并构建动态结构。
地址与解引用
& 取得变量的地址,* 读取或写入指针所指地址的值。指针的类型告诉编译器每一步有多大,例如 int * 每次前进 sizeof(int) 个字节。
数组上的指针运算
数组名的作用就像指向第一个元素的指针。p + 2 指向后面两个元素,*(p + 2) 读取那个元素,它与 p[2] 相同。越过数组末尾是未定义行为。
把数组和指针传给函数
函数以指针的形式接收数组,因此还必须接收长度。指针上的 const 关键字告诉读者和编译器,该函数不会修改数据。
示例 28: 指针:地址与解引用
& 取得地址,* 读取或写入该位置的值。
#include <stdio.h>
int main(void) {
int value = 42;
int *p = &value;
printf("value via pointer: %d\n", *p);
*p = 100;
printf("value now: %d\n", value);
return 0;
}
输出:
value via pointer: 42
value now: 100
示例 29: 指针运算
用 + 和 - 沿数组移动指针。
#include <stdio.h>
int main(void) {
int nums[4] = {10, 20, 30, 40};
int *p = nums;
for (int i = 0; i < 4; i++) {
printf("%d ", *(p + i));
}
printf("\n");
printf("third via index: %d, via pointer: %d\n", nums[2], *(nums + 2));
return 0;
}
输出:
10 20 30 40
third via index: 30, via pointer: 30
示例 30: 把数组传给函数
数组以指针加长度的形式传递。
#include <stdio.h>
static int sum(const int *values, size_t count) {
int total = 0;
for (size_t i = 0; i < count; i++) {
total += values[i];
}
return total;
}
int main(void) {
int data[] = {1, 2, 3, 4, 5};
printf("sum = %d\n", sum(data, sizeof data / sizeof data[0]));
return 0;
}
输出:
sum = 15
示例 31: 字符串数组
指向文本的指针数组。
#include <stdio.h>
int main(void) {
const char *fruits[] = {"apple", "banana", "cherry"};
size_t n = sizeof fruits / sizeof fruits[0];
for (size_t i = 0; i < n; i++) {
printf("%zu: %s\n", i, fruits[i]);
}
return 0;
}
输出:
0: apple
1: banana
2: cherry
知识点 7: 结构体、共用体、枚举与 typedef
这些类型把值组合在一起,并为一组常量命名。它们是 C 数据结构的基础。
结构体与结构体指针
结构体把多个字段组合在一个名字下。箭头运算符 -> 通过指针访问字段,因此 p->x 等同于 (*p).x。对于较大的结构体,应传递指针而不是复制。
共用体与枚举
共用体让多个类型共享同一块内存,因此同一时间只有一个值有意义。枚举为整数常量命名,使 switch 语句更易读。
typedef
typedef 为类型取一个更短的名字,例如把包含两个 double 的 struct 命名为 Rect。使用 typedef 后,函数指针类型会容易读得多。
示例 32: 结构体
把相关的值组合在一个名字下。
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point p = {3, 4};
p.x += 1;
printf("point (%d, %d)\n", p.x, p.y);
return 0;
}
输出:
point (4, 4)
示例 33: 结构体指针与 ->
通过指针访问结构体的字段。
#include <stdio.h>
struct Book {
const char *title;
int pages;
};
static void print_book(const struct Book *b) {
printf("%s has %d pages\n", b->title, b->pages);
}
int main(void) {
struct Book books[] = {{"C Basics", 120}, {"Pointers", 96}};
for (int i = 0; i < 2; i++) {
print_book(&books[i]);
}
return 0;
}
输出:
C Basics has 120 pages
Pointers has 96 pages
示例 34: 共用体
多种类型共享同一块内存,每次只保存一个值。
#include <stdio.h>
union Value {
int as_int;
float as_float;
};
int main(void) {
union Value v;
v.as_int = 65;
printf("as int: %d\n", v.as_int);
v.as_float = 1.5f;
printf("as float: %.1f\n", v.as_float);
return 0;
}
输出:
as int: 65
as float: 1.5
示例 35: 枚举
为一组整数常量命名。
#include <stdio.h>
enum Color { RED, GREEN = 5, BLUE };
int main(void) {
enum Color c = BLUE;
printf("RED=%d GREEN=%d BLUE=%d\n", RED, GREEN, BLUE);
printf("chosen: %d\n", c);
return 0;
}
输出:
RED=0 GREEN=5 BLUE=6
chosen: 6
示例 36: typedef 与函数指针类型
为类型取一个更短的名字。
#include <stdio.h>
typedef struct {
double width;
double height;
} Rect;
typedef double (*Measure)(Rect);
static double area(Rect r) {
return r.width * r.height;
}
int main(void) {
Rect r = {3.0, 4.5};
Measure m = area;
printf("area = %.2f\n", m(r));
return 0;
}
输出:
area = 13.50
知识点 8: 动态内存与文件
动态内存让程序在运行时申请空间,文件让数据在程序结束后仍然保留。在 C 中,程序员负责清理,两者都需要仔细处理。
malloc、calloc、realloc 与 free
malloc 申请一块字节,calloc 返回清零的内存,realloc 调整一块内存的大小。每一次成功的调用都必须对应一次 free。使用结果之前要检查是否为 NULL,因为分配可能失败。
避免泄漏与悬空指针
free 之后把指针设为 NULL,以免误用。每块内存只释放一次。要清楚每个函数拥有哪一块内存,不能使用别的函数已经释放的内存。
文本文件与二进制文件
fopen 打开文件,模式决定是读、写还是追加。fprintf 与 fgets 处理文本,fwrite 与 fread 复制原始字节。务必检查 fopen 的返回值,并关闭每个打开的文件。
示例 37: 用 malloc 与 free 管理动态内存
运行时申请内存,用完后归还。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 5;
int *squares = malloc(n * sizeof *squares);
if (squares == NULL) {
fprintf(stderr, "out of memory\n");
return 1;
}
for (int i = 0; i < n; i++) {
squares[i] = i * i;
}
for (int i = 0; i < n; i++) {
printf("%d ", squares[i]);
}
printf("\n");
free(squares);
return 0;
}
输出:
0 1 4 9 16
示例 38: calloc 与 realloc
清零的内存,以及调整内存块的大小。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *items = calloc(3, sizeof *items);
printf("zeroed: %d %d %d\n", items[0], items[1], items[2]);
int *bigger = realloc(items, 6 * sizeof *bigger);
if (bigger == NULL) {
free(items);
return 1;
}
items = bigger;
items[5] = 99;
printf("last item: %d\n", items[5]);
free(items);
return 0;
}
输出:
zeroed: 0 0 0
last item: 99
示例 39: 安全地释放内存
只释放一次,然后把指针置空。
#include <stdio.h>
#include <stdlib.h>
static char *make_greeting(const char *name) {
char *out = malloc(64);
if (out == NULL) return NULL;
snprintf(out, 64, "Hello, %s", name);
return out;
}
int main(void) {
char *msg = make_greeting("Mali");
if (msg != NULL) {
printf("%s\n", msg);
free(msg);
msg = NULL;
}
return 0;
}
输出:
Hello, Mali
示例 40: 文本文件:写入与读取
使用 fopen、fprintf、fgets 和 fclose。
#include <stdio.h>
int main(void) {
FILE *out = fopen("notes.txt", "w");
if (out == NULL) return 1;
fprintf(out, "first line\nsecond line\n");
fclose(out);
FILE *in = fopen("notes.txt", "r");
if (in == NULL) return 1;
char line[64];
while (fgets(line, sizeof line, in) != NULL) {
printf("read: %s", line);
}
fclose(in);
remove("notes.txt");
return 0;
}
输出:
read: first line
read: second line
示例 41: 用 fwrite 与 fread 处理二进制文件
把原始数值存入文件并读回。
#include <stdio.h>
int main(void) {
int data[] = {10, 20, 30};
FILE *f = fopen("values.bin", "wb");
if (f == NULL) return 1;
fwrite(data, sizeof data[0], 3, f);
fclose(f);
int back[3] = {0};
f = fopen("values.bin", "rb");
if (f == NULL) return 1;
size_t got = fread(back, sizeof back[0], 3, f);
fclose(f);
remove("values.bin");
printf("read %zu values: %d %d %d\n", got, back[0], back[1], back[2]);
return 0;
}
输出:
read 3 values: 10 20 30
知识点 9: 预处理器、函数指针与可变参数函数
这些工具改变代码的构建方式和函数的调用方式。用得好,程序会很灵活;用得不当,程序就很难调试。
宏与条件编译
宏是在编译前替换的文本。要给每个宏参数和整个表达式加上括号,否则 SQUARE(2 + 1) 会得到错误的结果。#if 与 #ifdef 只在构建时条件成立时才包含代码。
函数指针与 qsort
函数指针保存函数的地址,因此可以把行为传给另一个函数。qsort 使用比较回调,使同一个例程能对任何类型的数组排序。
可变参数函数与命令行参数
stdarg.h 让函数接收任意数量的参数,并通过 va_arg 读取。main 接收 argc 与 argv,其中保存了用户在程序名之后输入的参数。
示例 42: 命令行参数
argc 与 argv 保存用户输入的参数。
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("argument count: %d\n", argc);
for (int i = 1; i < argc; i++) {
printf("arg %d: %s\n", i, argv[i]);
}
return 0;
}
输出:
argument count: 1
示例 43: 宏与类函数宏
编译前进行的文本替换;参数要加括号。
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(void) {
printf("SQUARE(4) = %d\n", SQUARE(4));
printf("SQUARE(2 + 1) = %d\n", SQUARE(2 + 1));
printf("MAX(3, 9) = %d\n", MAX(3, 9));
return 0;
}
输出:
SQUARE(4) = 16
SQUARE(2 + 1) = 9
MAX(3, 9) = 9
示例 44: 条件编译
只有在构建时条件为真才包含代码。
#include <stdio.h>
#define DEBUG_MODE 1
int main(void) {
#if DEBUG_MODE
printf("debug build\n");
#else
printf("release build\n");
#endif
#ifdef __STDC_VERSION__
printf("C standard version is known\n");
#endif
return 0;
}
输出:
debug build
C standard version is known
示例 45: 函数指针
把函数存入变量,并通过指针调用。
#include <stdio.h>
static int add(int a, int b) { return a + b; }
static int mul(int a, int b) { return a * b; }
static int apply(int (*op)(int, int), int x, int y) {
return op(x, y);
}
int main(void) {
int (*ops[])(int, int) = {add, mul};
printf("add: %d\n", apply(ops[0], 6, 7));
printf("mul: %d\n", apply(ops[1], 6, 7));
return 0;
}
输出:
add: 13
mul: 42
示例 46: 用 qsort 排序
用比较回调函数排序任意数组。
#include <stdio.h>
#include <stdlib.h>
static int compare_ints(const void *a, const void *b) {
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y);
}
int main(void) {
int nums[] = {42, 7, 19, 3, 25};
size_t n = sizeof nums / sizeof nums[0];
qsort(nums, n, sizeof nums[0], compare_ints);
for (size_t i = 0; i < n; i++) {
printf("%d ", nums[i]);
}
printf("\n");
return 0;
}
输出:
3 7 19 25 42
示例 47: 可变参数函数
用 stdarg 接收任意数量参数的函数。
#include <stdarg.h>
#include <stdio.h>
static int sum_all(int count, ...) {
va_list args;
va_start(args, count);
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int);
}
va_end(args);
return total;
}
int main(void) {
printf("%d\n", sum_all(3, 1, 2, 3));
printf("%d\n", sum_all(5, 10, 20, 30, 40, 50));
return 0;
}
输出:
6
150
知识点 10: 位、错误与作用域
底层的位操作、错误检查和作用域规则,是 C 程序员日常使用的基本技能。
统计与检测位
x &= x - 1 会清除最低位的 1,因此在循环中重复执行它就能统计置位的个数。像 flags & 4 这样的位掩码可以检测单个位而不影响其他位。
用 errno 检查错误
许多库函数在失败时返回特殊值,并设置 errno 说明原因。先检查返回值,再读取 errno,并可用 strerror 把它转换为文字。
文件作用域的 static
文件作用域中的 static 函数或变量只在该文件内可见。这可以避免内部辅助函数与其他文件中的名字冲突,是良好代码结构的基本一步。
示例 48: 统计置位的位数
在循环中清除最低位的 1,直到没有剩余。
#include <stdio.h>
static int count_bits(unsigned int x) {
int count = 0;
while (x != 0) {
x &= x - 1;
count++;
}
return count;
}
int main(void) {
printf("bits in 0 = %d\n", count_bits(0));
printf("bits in 7 = %d\n", count_bits(7));
printf("bits in 255 = %d\n", count_bits(255));
return 0;
}
输出:
bits in 0 = 0
bits in 7 = 3
bits in 255 = 8
示例 49: 用 errno 处理错误
检查返回值,再读取 errno 得到原因。
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void) {
FILE *f = fopen("no-such-file-here.txt", "r");
if (f == NULL) {
printf("open failed: errno set = %s\n", errno != 0 ? "yes" : "no");
return 0;
}
fclose(f);
return 0;
}
输出:
open failed: errno set = yes
示例 50: static 函数与全局状态
static 把名字的作用范围限制在当前文件内。
#include <stdio.h>
static int call_count = 0;
static void record_call(void) {
call_count++;
}
static int calls_so_far(void) {
return call_count;
}
int main(void) {
record_call();
record_call();
printf("calls: %d\n", calls_so_far());
return 0;
}
输出:
calls: 2
知识点 11: C++ 基础:引用、重载、string 与 vector
C++ 保留了 C 的速度,并增加了类型、引用和标准库。这些示例介绍日常工具。
引用与重载
引用是已有变量的另一个名字,因此修改引用就是修改变量。当参数类型不同时,多个函数可以共用一个名字,编译器会选择正确的那个。
std::string 与 std::vector
std::string 会自动伸缩,并且知道自己的长度,因此无需手动管理内存。std::vector 是可调整大小、拥有其元素的数组。两者都很适合范围 for 循环。
auto、默认参数与 iostream
auto 让编译器根据值推断类型,默认参数允许调用者省略某个参数。std::cout 配合 << 取代了 printf,并且在编译时检查类型。
示例 51: 用 iostream 输出 Hello
使用 std::cout 把文字打印到控制台。
#include <iostream>
int main() {
std::cout << "Hello, C++!" << '\n';
return 0;
}
输出:
Hello, C++!
示例 52: 引用(reference)
引用是同一个变量的另一个名字。
#include <iostream>
void increment(int &value) {
++value;
}
int main() {
int count = 5;
int &alias = count;
alias += 10;
increment(count);
std::cout << count << '\n';
}
输出:
16
示例 53: 函数重载
同名函数,由实参类型决定调用哪一个。
#include <iostream>
#include <string>
std::string describe(int n) { return "int " + std::to_string(n); }
std::string describe(double d) { return "double " + std::to_string(d); }
std::string describe(const std::string &s) { return "string " + s; }
int main() {
std::cout << describe(7) << '\n';
std::cout << describe(2.5) << '\n';
std::cout << describe(std::string("hi")) << '\n';
}
输出:
int 7
double 2.500000
string hi
示例 54: 默认参数
调用时可以省略的形参。
#include <iostream>
int power(int base, int exponent = 2) {
int result = 1;
for (int i = 0; i < exponent; ++i) result *= base;
return result;
}
int main() {
std::cout << power(3) << ' ' << power(3, 4) << '\n';
}
输出:
9 81
示例 55: std::string 基础
安全地连接、测量、截取和查找文字。
#include <iostream>
#include <string>
int main() {
std::string name = "Ada";
name += " Lovelace";
std::cout << name << " has " << name.size() << " characters\n";
std::cout << name.substr(0, 3) << '\n';
std::cout << (name.find("Love") != std::string::npos) << '\n';
}
输出:
Ada Lovelace has 12 characters
Ada
1
示例 56: std::vector 基础
可以增长、并且拥有其元素的数组。
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{70, 85, 92};
scores.push_back(64);
int total = 0;
for (int s : scores) total += s;
std::cout << "count=" << scores.size() << " total=" << total << '\n';
}
输出:
count=4 total=311
示例 57: 范围 for 与 auto
遍历容器,无需计算下标。
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> stock{{"apple", 3}, {"pear", 0}};
for (const auto &[item, qty] : stock) {
std::cout << item << ": " << qty << '\n';
}
}
输出:
apple: 3
pear: 0
知识点 12: 类、RAII 与复制和移动
类把数据和行为组合在一起,是 C++ 变得强大的地方。RAII 把资源与对象的生命周期绑定,使程序无需手动清理也能保持安全。
类、构造函数与 const 成员
构造函数在对象创建时完成初始化。标记为 const 的成员函数承诺不修改对象。数据应设为 private,只公开调用者需要的部分,并在保存前检查输入。
析构函数与 RAII
对象离开作用域时会运行析构函数,因此在构造函数中获得的资源会在那里释放。这就是 RAII:即使发生错误,文件、锁或内存块也会被自动释放。
复制、移动与运算符
拥有内存的对象需要复制构造函数和复制赋值运算符,否则两个对象会共享同一块内存并重复释放。std::move 让对象转交资源而不是复制它。运算符重载让 + 和 << 可用于你自己的类型。
示例 58: 类与构造函数
把数据和行为组合在一起,并在构造函数中完成初始化。
#include <iostream>
#include <string>
class Book {
public:
Book(std::string title, int pages) : title_(std::move(title)), pages_(pages) {}
std::string title() const { return title_; }
int pages() const { return pages_; }
private:
std::string title_;
int pages_;
};
int main() {
Book b{"Clean C++", 320};
std::cout << b.title() << " (" << b.pages() << " pages)\n";
}
输出:
Clean C++ (320 pages)
示例 59: const 成员函数
承诺该方法不会修改对象。
#include <iostream>
class Counter {
public:
void add(int n) { total_ += n; }
int total() const { return total_; }
private:
int total_ = 0;
};
int report(const Counter &c) {
return c.total();
}
int main() {
Counter c;
c.add(4);
c.add(6);
std::cout << report(c) << '\n';
}
输出:
10
示例 60: 封装与异常
把数据设为私有,并拒绝无效的输入。
#include <iostream>
#include <stdexcept>
class Account {
public:
explicit Account(int opening) : balance_(opening) {}
void deposit(int amount) {
if (amount <= 0) throw std::invalid_argument("amount must be positive");
balance_ += amount;
}
int balance() const { return balance_; }
private:
int balance_;
};
int main() {
Account a{100};
a.deposit(50);
std::cout << a.balance() << '\n';
}
输出:
150
示例 61: 析构函数与 RAII
对象离开作用域时,析构函数释放资源。
#include <iostream>
#include <string>
class Trace {
public:
explicit Trace(std::string name) : name_(std::move(name)) {
std::cout << "open " << name_ << '\n';
}
~Trace() { std::cout << "close " << name_ << '\n'; }
private:
std::string name_;
};
int main() {
Trace outer{"outer"};
{
Trace inner{"inner"};
std::cout << "work\n";
}
std::cout << "done\n";
}
输出:
open outer
open inner
work
close inner
done
close outer
示例 62: 拷贝构造与拷贝赋值
复制拥有内存的对象,而不与其共享。
#include <iostream>
#include <cstring>
class Buffer {
public:
explicit Buffer(const char *text) : data_(new char[std::strlen(text) + 1]) {
std::strcpy(data_, text);
}
Buffer(const Buffer &other) : data_(new char[std::strlen(other.data_) + 1]) {
std::strcpy(data_, other.data_);
}
Buffer &operator=(const Buffer &other) {
if (this != &other) {
delete[] data_;
data_ = new char[std::strlen(other.data_) + 1];
std::strcpy(data_, other.data_);
}
return *this;
}
~Buffer() { delete[] data_; }
const char *text() const { return data_; }
private:
char *data_;
};
int main() {
Buffer a{"hello"};
Buffer b = a;
b = Buffer{"world"};
std::cout << a.text() << ' ' << b.text() << '\n';
}
输出:
hello world
示例 63: 移动语义
转移资源,而不是复制它。
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::string source = "a long piece of text";
std::string target = std::move(source);
std::cout << "target: " << target << '\n';
std::cout << "source empty: " << source.empty() << '\n';
std::vector<std::string> items;
items.push_back(std::move(target));
std::cout << "items: " << items.size() << '\n';
}
输出:
target: a long piece of text
source empty: 1
items: 1
示例 64: 运算符重载
让 + 和 << 可以用于你自己的类型。
#include <iostream>
struct Vec2 {
double x, y;
Vec2 operator+(const Vec2 &o) const { return {x + o.x, y + o.y}; }
Vec2 operator*(double k) const { return {x * k, y * k}; }
};
std::ostream &operator<<(std::ostream &os, const Vec2 &v) {
return os << '(' << v.x << ", " << v.y << ')';
}
int main() {
Vec2 a{1, 2}, b{3, 4};
std::cout << (a + b) * 2.0 << '\n';
}
输出:
(8, 12)
知识点 13: 继承、智能指针与标准库类型
继承和虚函数让同一段代码能处理多种类型。智能指针、optional 与 variant 则在类型系统中表达所有权和可能性。
继承与虚函数
派生类继承基类并重写虚函数。指向基类的指针会在运行时调用正确的版本。多态基类应始终有一个虚析构函数。
unique_ptr 与 shared_ptr
unique_ptr 只有一个所有者,并会自动释放对象。shared_ptr 统计所有者数量,weak_ptr 在不延长对象生命周期的情况下观察它。应优先使用 unique_ptr,只有在所有权确实共享时才使用 shared_ptr。
optional、variant 与 std::function
std::optional 保存一个可能不存在的值。std::variant 保存多个类型之一,std::visit 处理每一种情况。std::function 把任何可调用对象(例如 lambda)存入变量。
示例 65: 继承与虚函数
派生类通过基类指针改变行为。
#include <iostream>
#include <string>
class Animal {
public:
virtual ~Animal() = default;
virtual std::string sound() const { return "..."; }
};
class Dog : public Animal {
public:
std::string sound() const override { return "Woof"; }
};
class Cat : public Animal {
public:
std::string sound() const override { return "Meow"; }
};
int main() {
Dog d;
Cat c;
const Animal *pets[] = {&d, &c};
for (const Animal *p : pets) std::cout << p->sound() << '\n';
}
输出:
Woof
Meow
示例 66: 抽象类
含纯虚函数的基类不能直接创建对象。
#include <iostream>
#include <memory>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Square : public Shape {
public:
explicit Square(double s) : side_(s) {}
double area() const override { return side_ * side_; }
private:
double side_;
};
int main() {
std::unique_ptr<Shape> s = std::make_unique<Square>(3.0);
std::cout << s->area() << '\n';
}
输出:
9
示例 67: 用智能指针实现多态
把不同类型存入同一个 unique_ptr 向量。
#include <iostream>
#include <memory>
#include <vector>
struct Task {
virtual ~Task() = default;
virtual void run() const = 0;
};
struct Print : Task {
void run() const override { std::cout << "print\n"; }
};
struct Save : Task {
void run() const override { std::cout << "save\n"; }
};
int main() {
std::vector<std::unique_ptr<Task>> queue;
queue.push_back(std::make_unique<Print>());
queue.push_back(std::make_unique<Save>());
for (const auto &t : queue) t->run();
}
输出:
print
save
示例 68: unique_ptr 的所有权
只有一个所有者,对象会自动释放。
#include <iostream>
#include <memory>
struct Resource {
explicit Resource(int id) : id_(id) { std::cout << "acquire " << id_ << '\n'; }
~Resource() { std::cout << "release " << id_ << '\n'; }
int id() const { return id_; }
private:
int id_;
};
std::unique_ptr<Resource> make(int id) {
return std::make_unique<Resource>(id);
}
int main() {
auto r = make(7);
std::cout << "using " << r->id() << '\n';
}
输出:
acquire 7
using 7
release 7
示例 69: shared_ptr 与 weak_ptr
带计数的共享所有权,以及弱引用观察者。
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> a = std::make_shared<int>(42);
std::weak_ptr<int> watcher = a;
std::cout << "use count: " << a.use_count() << '\n';
{
std::shared_ptr<int> b = a;
std::cout << "use count: " << a.use_count() << '\n';
}
std::cout << "after scope: " << a.use_count() << '\n';
std::cout << "still alive: " << !watcher.expired() << '\n';
}
输出:
use count: 1
use count: 2
after scope: 1
still alive: 1
示例 70: std::optional
可能不存在的值,无需使用空指针。
#include <iostream>
#include <optional>
#include <string>
std::optional<int> parse_int(const std::string &text) {
try {
return std::stoi(text);
} catch (...) {
return std::nullopt;
}
}
int main() {
for (const char *s : {"42", "oops"}) {
if (auto v = parse_int(s)) {
std::cout << "value " << *v << '\n';
} else {
std::cout << "not a number: " << s << '\n';
}
}
}
输出:
value 42
not a number: oops
示例 71: std::variant 与 std::visit
一个可以是多种类型之一的值。
#include <iostream>
#include <string>
#include <variant>
using Value = std::variant<int, double, std::string>;
std::string kind(const Value &v) {
return std::visit([](const auto &x) -> std::string {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, int>) return "int";
else if constexpr (std::is_same_v<T, double>) return "double";
else return "string";
}, v);
}
int main() {
std::cout << kind(Value{3}) << ' ' << kind(Value{2.5}) << ' ' << kind(Value{std::string("x")}) << '\n';
}
输出:
int double string
示例 72: Lambda 与捕获
可以使用外部变量的小型匿名函数。
#include <iostream>
int main() {
int base = 10;
auto add_base = [base](int x) { return x + base; };
auto count = 0;
auto bump = [&count]() { ++count; };
bump();
bump();
std::cout << add_base(5) << ' ' << count << '\n';
}
输出:
15 2
示例 73: std::function
把任何可调用对象保存在变量中。
#include <functional>
#include <iostream>
#include <vector>
int main() {
std::vector<std::function<int(int)>> steps{
[](int x) { return x + 1; },
[](int x) { return x * 3; },
};
int value = 2;
for (const auto &step : steps) value = step(value);
std::cout << value << '\n';
}
输出:
9
知识点 14: 算法、容器与模板
标准库提供相互配合的容器、算法和迭代器。模板让你为许多类型只编写一次代码。
算法与迭代器
std::sort、std::find_if 和 std::transform 通过迭代器作用于任何范围。lambda 让条件简短清晰。若有现成的算法,应优先使用,而不是手写循环。
map、set 与 array
std::map 保持键有序,std::unordered_map 使用哈希实现快速查找。std::set 保存有序且唯一的值,std::array 大小固定,配合结构化绑定的 std::pair 能整洁地返回两个值。
模板、特化与概念
函数模板适用于任何支持其所用操作的类型。像 Stack<T> 这样的类模板只需编写一次。C++20 的概念为模板类型必须满足的要求命名,因此错误会出现在调用处。
示例 74: 算法:sort、find_if 与 transform
标准算法可用于任意的值范围。
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{5, 2, 9, 1, 7};
std::sort(v.begin(), v.end());
auto big = std::find_if(v.begin(), v.end(), [](int x) { return x > 4; });
std::transform(v.begin(), v.end(), v.begin(), [](int x) { return x * 10; });
std::cout << "first > 4: " << *big << '\n';
for (int x : v) std::cout << x << ' ';
std::cout << '\n';
}
输出:
first > 4: 50
10 20 50 70 90
示例 75: 迭代器与 std::map
用迭代器遍历有序的键值容器。
#include <iostream>
#include <map>
int main() {
std::map<int, const char *> names{{3, "c"}, {1, "a"}, {2, "b"}};
for (auto it = names.begin(); it != names.end(); ++it) {
std::cout << it->first << '=' << it->second << '\n';
}
auto found = names.find(2);
std::cout << "found " << found->second << '\n';
}
输出:
1=a
2=b
3=c
found b
示例 76: 用 unordered_map 统计词频
用哈希映射统计单词出现次数。
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
int main() {
std::string text = "the cat and the hat and the bat";
std::unordered_map<std::string, int> counts;
std::istringstream words(text);
std::string w;
while (words >> w) ++counts[w];
std::cout << "the=" << counts["the"] << " and=" << counts["and"] << " bat=" << counts["bat"] << '\n';
}
输出:
the=3 and=2 bat=1
示例 77: std::set 与 std::multiset
有序集合,分别不允许和允许重复元素。
#include <iostream>
#include <set>
int main() {
std::set<int> unique{3, 1, 3, 2};
std::multiset<int> all{3, 1, 3, 2};
std::cout << "set size " << unique.size() << '\n';
std::cout << "multiset size " << all.size() << '\n';
for (int x : unique) std::cout << x << ' ';
std::cout << '\n';
}
输出:
set size 3
multiset size 4
1 2 3
示例 78: std::array
固定大小的数组,提供 size 和迭代器。
#include <array>
#include <iostream>
#include <numeric>
int main() {
std::array<int, 4> a{1, 2, 3, 4};
int sum = std::accumulate(a.begin(), a.end(), 0);
std::cout << "size " << a.size() << " sum " << sum << '\n';
}
输出:
size 4 sum 10
示例 79: std::pair 与结构化绑定
返回两个值,并按名字解包。
#include <iostream>
#include <utility>
std::pair<int, int> divide(int a, int b) {
return {a / b, a % b};
}
int main() {
auto [quotient, remainder] = divide(17, 5);
std::cout << quotient << " remainder " << remainder << '\n';
}
输出:
3 remainder 2
示例 80: 函数模板
一个函数适用于多种类型。
#include <iostream>
#include <string>
template <typename T>
T largest(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << largest(3, 8) << '\n';
std::cout << largest(2.5, 1.5) << '\n';
std::cout << largest(std::string("pear"), std::string("apple")) << '\n';
}
输出:
8
2.5
pear
示例 81: 类模板:Stack
编写一次即可复用的通用容器。
#include <iostream>
#include <stdexcept>
#include <vector>
template <typename T>
class Stack {
public:
void push(const T &value) { items_.push_back(value); }
T pop() {
if (items_.empty()) throw std::out_of_range("stack is empty");
T top = items_.back();
items_.pop_back();
return top;
}
bool empty() const { return items_.empty(); }
private:
std::vector<T> items_;
};
int main() {
Stack<int> s;
s.push(1);
s.push(2);
std::cout << s.pop() << ' ' << s.pop() << '\n';
}
输出:
2 1
示例 82: 模板特化
针对某一类型的模板特殊版本。
#include <iostream>
#include <string>
template <typename T>
std::string describe(const T &) { return "other"; }
template <>
std::string describe<int>(const int &x) { return "int " + std::to_string(x); }
template <>
std::string describe<std::string>(const std::string &s) { return "string of " + std::to_string(s.size()); }
int main() {
std::cout << describe(4) << '\n';
std::cout << describe(std::string("hello")) << '\n';
std::cout << describe(1.5) << '\n';
}
输出:
int 4
string of 5
other
示例 83: 可变参数模板
接收任意数量参数的模板。
#include <iostream>
template <typename... Args>
int count_args(Args... args) {
return sizeof...(args);
}
template <typename T, typename... Rest>
void print_all(const T &first, const Rest &...rest) {
std::cout << first;
((std::cout << ' ' << rest), ...);
std::cout << '\n';
}
int main() {
std::cout << count_args(1, 'a', 2.0) << '\n';
print_all("values:", 1, 2.5, 'x');
}
输出:
3
values: 1 2.5 x
示例 84: 概念(C++20)
为模板类型要求的条件命名。
#include <concepts>
#include <iostream>
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <Numeric T>
T twice(T value) {
return value * 2;
}
int main() {
std::cout << twice(21) << '\n';
std::cout << twice(1.25) << '\n';
}
输出:
42
2.5
示例 85: constexpr 与 static_assert
在编译期计算并检查数值。
#include <iostream>
constexpr long long factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr long long kTenFactorial = factorial(10);
static_assert(kTenFactorial == 3628800, "factorial check failed at compile time");
int main() {
std::cout << kTenFactorial << '\n';
}
输出:
3628800
知识点 15: 现代 C++:异常、并发、格式化与测试
最后的主题涵盖专业 C++ 代码每天使用的工具:错误处理、线程、格式化、文件 I/O 以及小型测试框架。
异常、命名空间与 enum class
throw 把错误沿调用栈向上传递,直到匹配的 catch。自定义异常继承自 std::runtime_error。命名空间避免名字冲突,enum class 让枚举值有作用域且类型强。
线程、互斥锁与 future
std::thread 并发运行函数,join 等待其完成。std::mutex 与 lock_guard 保护共享数据免受数据竞争。std::async 返回一个 future,调用 get 时得到结果。
ranges、format、文件与测试
ranges 通过 filter 与 transform 构建易读的流水线。std::format 在编译时把参数与格式字符串进行核对。fstream 读写文件,小型测试框架为每项检查报告通过或失败。
示例 86: 异常:try、catch 与 throw
报告错误,并在更高层处理。
#include <iostream>
#include <stdexcept>
int safe_divide(int a, int b) {
if (b == 0) throw std::domain_error("division by zero");
return a / b;
}
int main() {
try {
std::cout << safe_divide(10, 2) << '\n';
std::cout << safe_divide(1, 0) << '\n';
} catch (const std::domain_error &e) {
std::cout << "caught: " << e.what() << '\n';
}
}
输出:
5
caught: division by zero
示例 87: 自定义异常层次
从 std::runtime_error 派生出你自己的错误类型。
#include <iostream>
#include <stdexcept>
#include <string>
class AppError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
class NotFound : public AppError {
public:
explicit NotFound(const std::string &what) : AppError("not found: " + what) {}
};
void load(const std::string &key) {
if (key != "config") throw NotFound(key);
}
int main() {
try {
load("cache");
} catch (const AppError &e) {
std::cout << e.what() << '\n';
}
}
输出:
not found: cache
示例 88: 命名空间
组织名字,避免冲突。
#include <iostream>
namespace geometry {
namespace units {
constexpr double kMeter = 1.0;
}
double area(double w, double h) { return w * h * units::kMeter; }
}
namespace app = geometry;
int main() {
std::cout << app::area(3.0, 2.0) << '\n';
std::cout << geometry::units::kMeter << '\n';
}
输出:
6
1
示例 89: enum class
强类型的枚举。
#include <iostream>
enum class Level { Low = 1, Medium = 5, High = 10 };
std::string_view name(Level l) {
switch (l) {
case Level::Low: return "low";
case Level::Medium: return "medium";
case Level::High: return "high";
}
return "unknown";
}
int main() {
Level l = Level::Medium;
std::cout << name(l) << ' ' << static_cast<int>(l) << '\n';
}
输出:
medium 5
示例 90: std::chrono 时长
带单位的时间值,由编译器检查单位。
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
using namespace std::chrono_literals;
milliseconds timeout{1500};
seconds rounded = duration_cast<seconds>(timeout);
seconds wait = rounded + 2min;
std::cout << timeout.count() << " ms = " << rounded.count() << " s\n";
std::cout << "wait " << wait.count() << " s\n";
}
输出:
1500 ms = 1 s
wait 121 s
示例 91: std::thread
在另一线程中运行函数,并等待其完成。
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::vector<int> results(4, 0);
std::vector<std::thread> workers;
for (int i = 0; i < 4; ++i) {
workers.emplace_back([&results, i] { results[i] = (i + 1) * (i + 1); });
}
for (auto &t : workers) t.join();
for (int r : results) std::cout << r << ' ';
std::cout << '\n';
}
输出:
1 4 9 16
示例 92: std::mutex 与 lock_guard
保护共享数据,防止数据竞争。
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
int main() {
int counter = 0;
std::mutex m;
std::vector<std::thread> threads;
for (int t = 0; t < 4; ++t) {
threads.emplace_back([&] {
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> lock(m);
++counter;
}
});
}
for (auto &t : threads) t.join();
std::cout << "counter = " << counter << '\n';
}
输出:
counter = 4000
示例 93: std::async 与 std::future
在后台执行任务,之后获取结果。
#include <future>
#include <iostream>
long long sum_range(long long from, long long to) {
long long total = 0;
for (long long i = from; i <= to; ++i) total += i;
return total;
}
int main() {
auto left = std::async(std::launch::async, sum_range, 1, 500);
auto right = std::async(std::launch::async, sum_range, 501, 1000);
std::cout << "total = " << left.get() + right.get() << '\n';
}
输出:
total = 500500
示例 94: Ranges(C++20)
用可读的流水线过滤和转换序列。
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> nums{1, 2, 3, 4, 5, 6, 7, 8};
auto result = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (int v : result) std::cout << v << ' ';
std::cout << '\n';
}
输出:
4 16 36 64
示例 95: std::format(C++20)
类型安全、模板可读的格式化。
#include <format>
#include <iostream>
#include <string>
int main() {
std::string item = "Coffee";
double price = 45.5;
std::cout << std::format("{:<10}|{:>8.2f}", item, price) << '\n';
std::cout << std::format("{} items, {:03d} id", 3, 7) << '\n';
}
输出:
Coffee | 45.50
3 items, 007 id
示例 96: 指定初始化器(C++20)
按名字设置结构体成员。
#include <iostream>
#include <string>
struct Server {
std::string host;
int port = 80;
bool tls = false;
};
int main() {
Server s{.host = "example.com", .tls = true};
std::cout << s.host << ':' << s.port << (s.tls ? " tls" : " plain") << '\n';
}
输出:
example.com:80 tls
示例 97: 用 fstream 读写文件
写入文件,然后逐行读取。
#include <fstream>
#include <iostream>
#include <string>
int main() {
{
std::ofstream out("log.txt");
out << "first\n" << "second\n";
}
std::ifstream in("log.txt");
std::string line;
while (std::getline(in, line)) {
std::cout << "read: " << line << '\n';
}
in.close();
std::remove("log.txt");
}
输出:
read: first
read: second
示例 98: 用 stringstream 解析
把一行文本拆分为带类型的值。
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::istringstream row("apple,12,0.5");
std::string name;
int qty = 0;
double price = 0.0;
std::getline(row, name, ',');
row >> qty;
row.ignore(1);
row >> price;
std::cout << name << " x" << qty << " @ " << price << '\n';
}
输出:
apple x12 @ 0.5
示例 99: 一个小型测试框架
检查结果,并报告通过或失败。
#include <iostream>
#include <string>
int failures = 0;
void check(bool ok, const std::string &name) {
std::cout << (ok ? "PASS " : "FAIL ") << name << '\n';
if (!ok) ++failures;
}
int add(int a, int b) { return a + b; }
int main() {
check(add(2, 2) == 4, "add small numbers");
check(add(-1, 1) == 0, "add opposites");
check(add(3, 3) == 6, "add positives");
std::cout << failures << " failure(s)\n";
return failures == 0 ? 0 : 1;
}
输出:
PASS add small numbers
PASS add opposites
PASS add positives
0 failure(s)
示例 100: 综合运用:库存报告
结合类、智能指针、optional 与 format。
#include <algorithm>
#include <format>
#include <iostream>
#include <memory>
#include <optional>
#include <string>
#include <vector>
struct Item {
std::string name;
int quantity;
double unit_price;
};
class Inventory {
public:
void add(Item item) { items_.push_back(std::move(item)); }
std::optional<Item> cheapest() const {
if (items_.empty()) return std::nullopt;
return *std::min_element(items_.begin(), items_.end(),
[](const Item &a, const Item &b) { return a.unit_price < b.unit_price; });
}
double total_value() const {
double sum = 0.0;
for (const auto &i : items_) sum += i.quantity * i.unit_price;
return sum;
}
const std::vector<Item> &items() const { return items_; }
private:
std::vector<Item> items_;
};
int main() {
auto inventory = std::make_unique<Inventory>();
inventory->add({"pen", 40, 5.0});
inventory->add({"notebook", 12, 45.5});
inventory->add({"stapler", 3, 120.0});
for (const auto &i : inventory->items()) {
std::cout << std::format("{:<10}{:>4} x {:>8.2f}\n", i.name, i.quantity, i.unit_price);
}
if (auto c = inventory->cheapest()) {
std::cout << "cheapest: " << c->name << '\n';
}
std::cout << std::format("total value: {:.2f}\n", inventory->total_value());
}
输出:
pen 40 x 5.00
notebook 12 x 45.50
stapler 3 x 120.00
cheapest: pen
total value: 1106.00
练习项目:小型库存报告
把这些概念合并成一个程序。先自己编写,再与示例比较。每个步骤大约相当于一到两个示例的工作量。
1. 编写一个包含名称、数量和单价的结构体 Item,并把三件商品存入数组。
2. 用 C++ 重写同一个程序:编写类 Inventory,它保存一个 Item 的 std::vector,并提供 total_value 方法。
3. 添加 cheapest 方法,返回 std::optional<Item>,库存为空时返回空值。
4. 用 std::format 打印每一行,名称左对齐到十个字符宽,价格保留两位小数。
5. 添加一个包含三项检查的小型测试框架,并让程序在任何检查失败时返回非零的退出码。
下一步
接下来,请学习 CMake 之类的构建系统,用 Catch2 或 GoogleTest 等框架编写单元测试,并阅读 C++ Core Guidelines。每个示例的输出都是用 GCC 16 实际运行得到的。如果你的输出不同,请先检查编译器版本和编译选项。
总结
C 让你直接控制内存,C++ 在保留这种控制的同时加入了类、模板和庞大的标准库。通过 100 个已编译的示例,本文涵盖了变量、流程控制、函数、数组、指针、结构体、动态内存、文件、预处理器、C++ 的类与 RAII、智能指针、标准容器与算法、模板、概念、异常、线程和格式化。每个示例都展示了完整代码和真实输出。最好的练习方法是修改某个值,预测结果,然后运行程序进行验证。