C++11 程序设计讲义
本讲义系统梳理 C++11 语言的基础语法与核心程序设计方法,内容覆盖从基本概念、数据类型、控制语句到高级特性的指针、结构体、文件操作及 STL 模板。所有代码均符合 C++11 标准。
1. 程序基本概念
1.1 核心术语与定义
- 标识符 (Identifier):用户定义的名称,用于命名变量、函数、数组等。标识符由字母、数字及下划线组成,且不能以数字开头,区分大小写。
- 关键字 (Keyword):C++ 编译器预留的具有特殊含义的单词(如
int,if,const,return),不能用作标识符。 - 常量 (Constant):在程序运行过程中值不发生改变的量(如
3.14,'A',true)。 - 变量 (Variable):在程序运行中其值可以被修改的内存单元。
- 字符串 (String):由双引号括起来的字符序列(如
"Hello World")。 - 表达式 (Expression):由操作数和运算符组成的式子(如
a + b * 5),计算后产生一个确定的值。
1.2 常量与变量的命名与定义
- 变量定义:必须“先定义,后使用”。定义时系统会为其分配相应大小的内存空间。
- 作用域 (Scope):变量或常量的有效范围,通常由一对花括号
{}划分。
#include <iostream>
// 全局常量
const double PI = 3.1415926535;
int main() {
// 局部变量定义与初始化
int radius = 5;
// 表达式计算
double area = PI * radius * radius;
std::cout << "Area: " << area << std::endl;
return 0;
}
1.3 头文件与命名空间
- 头文件 (Header File):包含预先声明的函数、类型及模版(如
<iostream>,<vector>,<cmath>)。 - 命名空间 (Namespace):为了解决多人协同开发或多库集成时的命名冲突问题。标准库中的所有标识符都定义在
std命名空间中。
#include <iostream> // 引入标准输入输出流头文件
using namespace std; // 声明使用 std 命名空间中的所有标识符
1.4 编辑、编译、解释、调试
- 编辑 (Editing):编写源程序代码并保存为
.cpp文件的过程。 - 编译 (Compiling):编译器将 C++ 高级语言源程序翻译成等价的机器语言目标程序(
.obj或.o),并由链接器链接生成可执行程序(.exe)的过程。C++ 是一门典型的编译型语言。 - 解释 (Interpreting):解释型语言(如 Python)在运行时由解释器逐行翻译并立即执行,不生成独立的可执行文件。
- 调试 (Debugging):运行程序,排查并修正程序中的逻辑错误或运行时错误(如使用 GDB 工具或控制台打印调试)。
2. 基本数据类型
在 C++ 中,不同数据类型决定了变量在内存中所占的空间大小及数据的存储格式。
| 类型分类 | 关键字 | C++11 标准大小(常见 64 位系统) | 范围 |
|---|---|---|---|
| 有符号整型 | int |
4 字节 (32 位) | $[-2^{31}, 2^{31}-1]$ (约 $\pm 2.1 \times 10^9$) |
| 大整型 | long long |
8 字节 (64 位) | $[-2^{63}, 2^{63}-1]$ (约 $\pm 9.2 \times 10^{18}$) |
| 单精度实数 | float |
4 字节 (32 位) | 约 $\pm 3.4 \times 10^{38}$(有效数字约 6~7 位) |
| 双精度实数 | double |
8 字节 (64 位) | 约 $\pm 1.7 \times 10^{308}$(有效数字约 15~17 位) |
| 字符型 | char |
1 字节 (8 位) | 存储对应 ASCII 码值($-128 \sim 127$ 或 $0 \sim 255$) |
| 布尔型 | bool |
1 字节 | true (非零,通常为1) 或 false (0) |
3. 程序基本语句
3.1 输入输出语句
1. C 风格输入输出 (scanf / printf)
速度较快,但在处理复杂自定义类型时不够直观。
#include <cstdio>
int main() {
int a;
long long b;
double c;
char d;
// 注意格式占位符:%d, %lld, %lf, %c
scanf("%d %lld %lf %c", &a, &b, &c, &d);
printf("a = %d, b = %lld, c = %.2lf, d = %c\n", a, b, c, d);
return 0;
}
2. C++ 风格输入输出 (cin / cout)
具备类型安全与链式调用的优点。
#include <iostream>
int main() {
// 提升 cin / cout 的执行效率,使其接近 scanf / printf
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int x;
std::cin >> x;
std::cout << "x = " << x << "\n"; // 推荐用 "\n" 代替 std::endl(避免频繁清空缓冲区)
return 0;
}
3.2 赋值与控制语句
- 赋值语句:将等号右侧表达式的值赋给左侧的变量空间(如
a = b + 3;)。 - 复合语句:用
{}括起来的语句块,在逻辑上视为单条语句,块内声明的变量在块结束时生命周期终结。
3.3 条件控制语句 (if, switch)
#include <iostream>
int main() {
int score = 85;
// 1. if-else 嵌套结构
if (score >= 90) {
std::cout << "Excellent\n";
} else if (score >= 80) {
std::cout << "Good\n";
} else {
std::cout << "Pass\n";
}
// 2. switch 多路分支
char grade = 'B';
switch (grade) {
case 'A':
std::cout << "Score >= 90\n";
break; // 防止 case 穿透
case 'B':
std::cout << "Score >= 80\n";
break;
default:
std::cout << "Other\n";
}
return 0;
}
3.4 循环控制语句 (for, while, do-while)
#include <iostream>
int main() {
// 1. for 循环
for (int i = 1; i <= 5; ++i) {
std::cout << i << " ";
}
std::cout << "\n";
// 2. while 循环
int count = 1;
while (count <= 5) {
std::cout << count << " ";
count++;
}
std::cout << "\n";
// 3. do-while 循环(至少执行一次)
int val = 1;
do {
std::cout << val << " ";
val++;
} while (val <= 5);
std::cout << "\n";
return 0;
}
4. 基本运算
4.1 运算符及其行为
- 除法与求模:整数相除结果为整除商(向下截断)。取模运算符
%的操作数必须是整型。 - 逻辑运算:支持短路求值性质:
- 在
A && B中,若A为false,则不再计算B。 - 在
A || B中,若A为true,则不再计算B。
- 在
- 自增自减:
- 前置(
++i):先自增,再参与表达式计算。 - 后置(
i++):先参与计算,再自增。
- 前置(
4.2 位运算 (Bitwise Operations)
位运算直接对内存中数据的二进制位(补码形式)进行操作,效率极高。
&(按位与):两对应位均为 1 结果才为 1。|(按位或):两对应位只要有一位为 1 结果就为 1。~(按位取反):0 变 1,1 变 0。^(按位异或):相同为 0,不同为 1。满足:a ^ b ^ b = a。<<(左移):高位丢弃,低位补 0。x << n相当于 $x \times 2^n$(在不溢出的前提下)。>>(右移):低位丢弃。对无符号数高位补 0(逻辑右移),对有符号数高位通常补符号位(算术右移)。
5. 数学库函数
C++ 引入 <cmath> 库来处理数学运算:
#include <iostream>
#include <cmath>
int main() {
double x = -9.6;
std::cout << "abs: " << std::abs(x) << "\n"; // 绝对值: 9.6
std::cout << "round: " << std::round(x) << "\n"; // 四舍五入: -10
std::cout << "ceil: " << std::ceil(x) << "\n"; // 向上取整: -9
std::cout << "floor: " << std::floor(x) << "\n"; // 向下取整: -10
std::cout << "sqrt: " << std::sqrt(16.0) << "\n"; // 平方根: 4
std::cout << "pow: " << std::pow(2.0, 3.0) << "\n"; // 指数幂 2^3: 8
std::cout << "log: " << std::log(exp(1.0)) << "\n"; // 自然对数 ln(e): 1
return 0;
}
6. 结构化程序设计
结构化程序设计强调“单入口,单出口”的规范化控制。 1. 三种基本结构:顺序结构、分支(选择)结构、循环结构。 2. 模块化设计:采用“自顶向下,逐步求精”的方法,将大问题拆分为功能单一、模块独立的函数实现。 3. 流程图表示: * 椭圆:起点/终点。 * 矩形:处理步骤。 * 菱形:条件判定分支。 * 平行四边形:输入/输出操作。
7. 数组
数组是在内存中连续存储的相同类型数据的集合。
7.1 一维数组
#include <iostream>
int main() {
// 静态定义一维数组
int arr[5] = {10, 20, 30, 40, 50};
// 利用 0 索引读写访问
for (int i = 0; i < 5; ++i) {
std::cout << "arr[" << i << "] = " << arr[i] << "\n";
}
return 0;
}
7.2 二维数组
二维数组在内存中是按行优先连续排列存储的。
#include <iostream>
int main() {
// 定义 2 行 3 列的二维数组
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << matrix[i][j] << " ";
}
std::cout << "\n";
}
return 0;
}
8. 字符串处理
8.1 字符数组 (C-Style String)
以空字符 '\0'(ASCII 值为 0)作为结束标志。
#include <iostream>
#include <cstring> // 引入 C 字符串处理库
int main() {
char str1[20] = "Hello";
char str2[] = "World";
// 常见标准库操作
std::cout << "Length: " << std::strlen(str1) << "\n"; // 5
std::strcat(str1, str2); // 拼接后 str1 变为 "HelloWorld"
std::cout << str1 << "\n";
// 字符串比较
if (std::strcmp(str1, "HelloWorld") == 0) {
std::cout << "Equal\n";
}
return 0;
}
8.2 std::string 类 (C++ Style)
更安全、更易操作,无需关心底部内存动态伸缩细节。
#include <iostream>
#include <string>
int main() {
std::string s1 = "Hello";
std::string s2 = "World";
// 直接使用 + 号拼接
std::string s3 = s1 + " " + s2;
std::cout << s3 << " (Length: " << s3.length() << ")\n";
// 子串截取 substr(起始索引, 长度)
std::string sub = s3.substr(0, 5); // "Hello"
std::cout << "Sub: " << sub << "\n";
// 查找子串
size_t pos = s3.find("World");
if (pos != std::string::npos) {
std::cout << "Found at: " << pos << "\n";
}
return 0;
}
9. 函数与递归
9.1 函数基本机制
- 形参 (Formal Parameter):定义函数时括号内的占位变量。
- 实参 (Actual Parameter):调用函数时实际传入的常量、变量或表达式。
9.2 参数传递方式比较
- 传值调用 (Pass-by-value):在函数内部为形参开辟临时空间,复制实参的值。函数内对形参的修改不会影响外部实参。
- 引用调用 (Pass-by-reference):形参成为实参的别名,函数内修改形参会直接改变外部实参的值。此方式可避免大对象的内存拷贝开销。
#include <iostream>
// 传值函数
void swap_by_val(int a, int b) {
int temp = a;
a = b;
b = temp;
}
// 引用传递函数
void swap_by_ref(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
swap_by_val(x, y);
std::cout << "After val swap: x=" << x << ", y=" << y << "\n"; // 依然是 10, 20
swap_by_ref(x, y);
std::cout << "After ref swap: x=" << x << ", y=" << y << "\n"; // 变为 20, 10
return 0;
}
9.3 递归函数
函数调用自身的行为。必须设立边界条件以防栈溢出。
// 计算斐波那契数列第 n 项
int fibonacci(int n) {
if (n <= 1) return n; // 递归出口
return fibonacci(n - 1) + fibonacci(n - 2);
}
10. 结构体与联合体
10.1 结构体 (Struct)
结构体是一组不同类型数据成员的集合。
#include <iostream>
#include <string>
struct Student {
int id;
std::string name;
double score;
};
int main() {
// 结构体变量定义与初始化
Student s1 = {1001, "Alice", 95.5};
// 通过成员访问符 '.'
std::cout << "Student: " << s1.name << " Score: " << s1.score << "\n";
// 结构体数组
Student cls[2] = {
{1002, "Bob", 88.0},
{1003, "Charlie", 92.0}
};
return 0;
}
10.2 联合体 (Union)
联合体内的所有数据成员共享同一块内存空间,联合体的大小为其最大成员的大小。同一时间只能激活使用其中一个成员。
#include <iostream>
union Value {
int i_val;
double d_val;
};
int main() {
Value val;
val.i_val = 42;
std::cout << "val.i_val: " << val.i_val << "\n";
val.d_val = 3.14; // 修改 d_val 会覆盖之前 i_val 的数据
std::cout << "val.d_val: " << val.d_val << "\n";
return 0;
}
11. 指针与引用
11.1 指针 (Pointer) 基础
指针是一个变量,其存储的内容是另一个变量的内存地址。
* & 运算符:获取变量的内存地址。
* * 运算符:解引用(Dereferencing),获取该地址中存储的具体数值。
#include <iostream>
int main() {
int val = 100;
int* ptr = &val; // ptr 指向 val 的地址
std::cout << "Address: " << ptr << "\n";
std::cout << "Value: " << *ptr << "\n"; // 解引用,输出 100
*ptr = 200; // 通过指针间接修改原始变量的值
std::cout << "New val: " << val << "\n"; // 输出 200
return 0;
}
11.2 数组的指针访问
数组名在绝大多数表达式中会被隐式转换为指向数组首元素的常量指针。 $$*(arr + i) \equiv arr[i]$$
#include <iostream>
int main() {
int arr[] = {10, 20, 30};
int* p = arr; // 等价于 &arr[0]
for (int i = 0; i < 3; ++i) {
std::cout << "Element " << i << ": " << *(p + i) << "\n"; // 指针偏移访问
}
return 0;
}
11.3 结构体指针与字符指针
- 结构体指针:访问成员使用指向符
->(而非常规变量的.)。 - 字符指针:通常直接指向字符串常量的首字符。
#include <iostream>
struct Point {
int x;
int y;
};
int main() {
Point p = {10, 20};
Point* p_ptr = &p;
// 使用 -> 访问结构体数组成员
std::cout << "Point: x=" << p_ptr->x << ", y=" << p_ptr->y << "\n";
// 字符指针指向常量区字符串
const char* str_ptr = "C++11";
std::cout << "String: " << str_ptr << "\n";
return 0;
}
12. 文件操作
C++11 通过 <fstream> 提供安全的文件输入输出流。
12.1 文本文件基本写与读
#include <iostream>
#include <fstream>
#include <string>
void write_file() {
std::ofstream outfile("test.txt"); // 打开文件用于写入
if (outfile.is_open()) {
outfile << "Line 1: Hello File\n";
outfile << "Line 2: 2026 Year\n";
outfile.close();
}
}
void read_file() {
std::ifstream infile("test.txt"); // 打开文件用于读取
if (infile.is_open()) {
std::string line;
while (std::getline(infile, line)) { // 按行读取
std::cout << line << "\n";
}
infile.close();
}
}
12.2 文件输入输出重定向 (freopen)
在算法竞赛中,常使用 freopen 替换标准输入输出流以方便自动化测试。
#include <cstdio>
void redirect_demo() {
// 将标准输入重定向为 input.txt
std::freopen("input.txt", "r", stdin);
// 将标准输出重定向为 output.txt
std::freopen("output.txt", "w", stdout);
int val;
if (scanf("%d", &val) == 1) {
printf("Read and output: %d\n", val);
}
}
13. STL 模板 (Standard Template Library)
13.1 常用工具函数
min(a, b)/max(a, b):获取两者中的极小/极大值。swap(a, b):交换两者的值。sort(begin, end):快速排序区间 $[begin, end)$ 范围内的元素。
#include <iostream>
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec = {5, 2, 9, 1, 7};
// 从小到大排序 (C++11 迭代器表示法)
std::sort(vec.begin(), vec.end());
// 自定义降序比较器 (使用 C++11 Lambda 表达式)
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b;
});
return 0;
}
13.2 常用容器
1. std::vector(动态数组)
支持尾部高效插入,内部自动扩容。
#include <vector>
void vector_demo() {
std::vector<int> v = {1, 2, 3}; // C++11 初始化列表
v.push_back(4); // 尾部插入 4
// 使用 C++11 范围 for 循环遍历
for (int x : v) {
std::cout << x << " ";
}
std::cout << "\n";
}
2. std::stack(栈)
#include <stack>
void stack_demo() {
std::stack<int> s;
s.push(10);
s.push(20);
while (!s.empty()) {
std::cout << s.top() << " "; // 输出 20 10
s.pop();
}
}
3. std::queue(队列)
#include <queue>
void queue_demo() {
std::queue<int> q;
q.push(100);
q.push(200);
while (!q.empty()) {
std::cout << q.front() << " "; // 输出 100 200
q.pop();
}
}
14. 综合练习题与解析
练习题 1:结构体排序与指针、文件重定向综合题
题目描述:
某班有 $N$ 个学生,每个学生有学号(整型)、姓名(字符串)、三门课成绩(整型)。
1. 编写程序从文本文件 score.in 读取所有学生的数据。
2. 计算每个学生的总分。
3. 按照总分从高到低进行排序;若总分相同,则按学号从小到大排序。
4. 将排序后的学生数据(学号、姓名、总分)输出到文本文件 score.out。
数据范围限制:$1 \le N \le 1000$。
C++11 实现与解析
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <cstdio>
// 1. 结构体定义
struct Student {
int id;
std::string name;
int s1, s2, s3;
int total_score;
};
// 2. 自定义比较规则函数
bool compare_students(const Student& a, const Student& b) {
if (a.total_score != b.total_score) {
return a.total_score > b.total_score; // 总分降序
}
return a.id < b.id; // 总分相同时,学号升序
}
int main() {
// 3. 文件重定向
// 如果重定向成功,接下来的标准输入输出都将与文件绑定
if (std::freopen("score.in", "r", stdin) == nullptr) {
return 1;
}
if (std::freopen("score.out", "w", stdout) == nullptr) {
return 1;
}
int n;
if (!(std::cin >> n)) return 0;
std::vector<Student> students(n);
for (int i = 0; i < n; ++i) {
std::cin >> students[i].id >> students[i].name
>> students[i].s1 >> students[i].s2 >> students[i].s3;
// 计算总分
students[i].total_score = students[i].s1 + students[i].s2 + students[i].s3;
}
// 4. 使用指针和迭代器区间进行 STL 排序
// 此处底层会通过指针偏移对 vector 连续内存的各 Student 对象进行高效率调配
std::sort(students.begin(), students.end(), compare_students);
// 5. 格式化输出至 score.out
for (const auto& stu : students) {
std::cout << stu.id << " " << stu.name << " " << stu.total_score << "\n";
}
return 0;
}
解题思路:
本题将结构体、std::string、std::vector、自定义比较函数的 std::sort 排序,以及 freopen 文件重定向技术整合在一起。通过将外部数据解耦在结构体对象 Student 中,我们可以直观、高效地表达各个逻辑维度。排序的核心基于多关键字比较逻辑:主关键字是 total_score,次关键字是 id。使用引用 const Student& 作为排序比较参数,极大地规避了结构体复制产生的无谓开销。
练习题 2:使用栈完成简单表达式的括号匹配检测
题目描述:
给定一个只包含 '(', ')', '{', '}', '[', ']' 的字符串,判断该字符串是否有效。
有效字符串需满足:
1. 左括号必须用相同类型的右括号闭合。
2. 左括号必须以正确的顺序闭合。
数据范围:字符串长度 $\le 10000$。
C++11 实现与解析
#include <iostream>
#include <string>
#include <stack>
bool is_valid_brackets(const std::string& s) {
std::stack<char> st;
for (char c : s) {
// 如果是左括号,直接压入栈中
if (c == '(' || c == '[' || c == '{') {
st.push(c);
} else {
// 如果遇到右括号,此时栈若空则必不匹配
if (st.empty()) return false;
char top_element = st.top();
// 判定括号配对类型是否对应一致
if ((c == ')' && top_element == '(') ||
(c == ']' && top_element == '[') ||
(c == '}' && top_element == '{')) {
st.pop(); // 成功配对则弹栈
} else {
return false; // 类型不符直接判定失败
}
}
}
// 如果全部扫描完栈且为空,说明所有左括号均找到了对应的右括号
return st.empty();
}
int main() {
std::string test1 = "{[()]}";
std::string test2 = "{[(])}";
std::cout << test1 << " is valid: " << (is_valid_brackets(test1) ? "Yes" : "No") << "\n"; // 输出 Yes
std::cout << test2 << " is valid: " << (is_valid_brackets(test2) ? "Yes" : "No") << "\n"; // 输出 No
return 0;
}
解题思路:
括号匹配检测是栈(后进先出,LIFO)的经典应用实例。当扫描到任意左括号时,该符号需要等待与其匹配的右括号,因此我们将其在栈中暂存。由于靠近字符串内部的左括号应当更先被匹配闭合,这与栈“后存入先处理”的性质高度契合。当遇到右括号时,我们只需要检查栈顶元素是否与其配对:若能配对,便可以将栈顶的左括号剔除(pop);若栈空或者无法配对,则说明括号层级交叉或者多出了右括号。在全部字符扫完之后,如果栈内依然有剩余,说明还有多余的左括号未被匹配,故最终返回 st.empty() 是否为 true。
—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com