从零到专业的 C 与 C++:100 个编译通过的示例与真实输出

从零到专业的 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 主要用于编译期的配置。

示意图

C 或 C++ 程序如何变成可执行文件:预处理器展开宏和头文件,编译器把每个源文件转换为目标代码,链接器把目标文件合并成一个程序
C 或 C++ 程序如何变成可执行文件:预处理器展开宏和头文件,编译器把每个源文件转换为目标代码,链接器把目标文件合并成一个程序

示例 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、智能指针、标准容器与算法、模板、概念、异常、线程和格式化。每个示例都展示了完整代码和真实输出。最好的练习方法是修改某个值,预测结果,然后运行程序进行验证。