C/C++ 语言基础讲义(-std=c++14)
1. 程序结构与控制流程
1.1 程序框架
#include <iostream>
using namespace std;
int main() { // 程序入口
int a, b;
cin >> a >> b;
cout << a + b << "\n";
return 0;
}
- 注释有
//与/* ... */两种形式;前者用于单行或行尾,后者必须成对出现。 - 使用系统头文件写作
#include <头文件名>;自己的头文件使用双引号,先在当前目录搜索。 - 流、容器和算法通常位于
std命名空间,原文示例使用using namespace std;。 - 程序从
main()开始;竞赛程序应以return 0;正常结束。 - 语句通常以分号结束;由
{}包围的语句块整体也视为一条语句。
1.2 选择结构
if 在条件为真时执行后续语句或语句块,否则执行 else 分支(如存在)。
if (condition) {
// A
} else if (anotherCondition) {
// B
} else {
// C
}
switch 根据表达式的值选择 case。default 可省略;若漏写 break,会继续执行后续 case,直到遇到 break 或 switch 结束。原文强调:switch 结束后有分号。
switch (month) {
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
cout << "这个月有 31 天。\n";
break;
case 4: case 6: case 9: case 11:
cout << "这个月有 30 天。\n";
break;
case 2:
cout << "这个月有 " << (leap ? 29 : 28) << " 天。\n";
break;
default:
break;
};
闰年规则:能被 400 整除,或能被 4 整除但不能被 100 整除。
1.3 循环结构
while (condition) {
// 循环体
}
do {
// 循环体至少执行一次
} while (condition);
for (initialization; condition; update) {
// 循环体
}
for 可等价改写为初始化后执行 while,每轮末尾执行状态转移。三个表达式可以省略,但两个分号必须保留。
break跳出当前层循环;continue跳过本轮余下代码,进入下一轮;二者只影响所在的一层循环。- 写循环时重点检查计数变量、边界使用
</<=等,以及逆序循环的--。
int n;
long long result;
cin >> n;
while (n > -1) {
result = 1;
for (int i = 1; i <= n; ++i) result *= i;
cout << n << "! = " << result << "\n";
cin >> n;
}
1.4 goto 与 C/C++ 差异
goto 可跳至标签,原文给出其跳出多层循环的用途;但它会破坏程序结构,通常不提倡使用。
for (int i = 0; i < 9; ++i)
for (int j = 0; j < 9; ++j)
for (int k = 0; k < 9; ++k)
if (/* 满足条件 */) goto exited;
exited:
竞赛中 C 与 C++ 的主要差异:C++ 支持流输入输出、面向对象、string 与 STL;C 的头文件在 C++ 中宜改为如 <cstdio> 而非 <stdio.h>。
2. 数据类型与复合数据
2.1 基本数据类型、变量和常量
常用整型包括 int(通常 4 字节)、long long(8 字节)及其无符号形式;char 通常 1 字节,bool 表示 true/false;float、double 分别约有 7 位、15 位有效数字。对固定宽度有要求时,可包含 <cstdint> 并使用 int8_t、int16_t、int32_t、int64_t 等;原文推荐竞赛中以 int64_t 代替 long long,以保证位宽一致。
变量定义形式为“类型 + 标识符”,未初始化的局部变量值不确定。常量可使用:
const int N = 90;
long long x = 123'456'789'012LL; // C++14 数字分隔符
int y = 0b1010'1010; // C++14 二进制字面量,值为 170
标识符只能包含字母、数字、下划线,不能以数字开头,不能与关键字冲突,必须先定义后使用;C++ 区分大小写。
2.2 数组与 C 风格字符串
int a[10];
int b[5][3];
- 数组下标从 0 开始,
a[10]的最后一个元素是a[9]。 - 多维数组本质是数组的数组,内存中元素连续存放;原文建议竞赛中尽量回避复杂的指针算术。
- C++ 不会检查数组越界,越界可能导致崩溃或错误结果。
- C 风格字符串是
char数组,末尾必须有空字符\0。 - 原生数组和 C 风格字符串不能直接整体赋值、比较;分别使用
memcpy/memcmp或strcpy/strcmp。
2.3 指针、引用与结构体
& 取变量地址,* 取得指针所指向的值。未初始化的指针指向未知位置,错误解引用可能崩溃。
int a = 0, b = 1;
int c[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int* p = &a;
*p = 3;
p = &b;
p = c + 6;
cout << *p; // c[6]
p = nullptr; // C++11,替代 NULL/0,更类型安全
引用是变量别名:创建时必须初始化并关联合法存储单元,之后不能改变绑定关系;指针则可以改指向。引用适合在函数参数中直接修改实参。
int& ref = b[0][2];
struct Pack {
int value, weight;
};
结构体成员通过 . 访问,结构体指针通过 -> 访问;C++ 中结构体可定义构造函数、成员函数和运算符重载。
3. 运算符与类型转换
- 算术运算符:
+ - * / %。整数除法会直接截断小数部分;C++ 没有乘方运算符,^是按位异或。 - 比较运算符:
> >= < <= == !=,结果为布尔值。警惕把==错写成=。 - 不应直接以
==/!=比较浮点数,应给定精度范围:
const double eps = 0.000001;
if (d >= 2 - eps && d <= 2 + eps) {
// 近似等于 2
}
- 位运算:
& | ^ ~ << >>分别表示按位与、或、异或、取反、左移、右移。原文将移位类比为乘、除以2^n,使用时需注意位运算语义和边界。 - 逻辑运算:
&& || !;条件运算符为A ? B : C。||、&&、?:具有短路性质,避免在其中做函数调用或赋值等有副作用的操作。 - 前缀
++i先改变再取值,后缀i++先取值再改变;不要在同一行混用自增、自减与其他修改变量的操作。 - 比较、位移、逻辑、条件运算符优先级较低,必要时多加括号。
int i = 0, j = 8, k = 5;
j = j + (++i); // i=1, j=9
k = k + (i++); // k=6, 随后 i=2
double value = 6.4 / static_cast<double>(i);
原文以 C 风格强制转换说明类型转换;在 C++ 中也可采用 static_cast<double>(i) 表达目标类型。
4. 函数与 Lambda 表达式
4.1 函数和参数传递
double foo(int i, float j) {
// ...
return 0.0;
}
函数若无返回值使用 void。定义应在调用前,或预先写函数声明。函数返回指针或引用时,不能返回函数内部局部变量的地址/引用;可使用静态变量或全局变量等合法对象。
- 按值传递会复制参数,函数内修改不影响实参;大数组复制代价高。
- 指针传递可修改实参,修改其指向的值需要解引用。
- 引用传递可直接修改实参,且无指针解引用的书写负担,但不能传入常量或表达式。
- 短小函数可用
inline提示编译器内联展开。
4.2 Lambda(C++11/14)
基本形式:[捕获列表](参数列表) -> 返回类型 { 函数体 },常用于 STL 算法和排序比较器。
vector<int> v = {3, 1, 4, 1, 5};
sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
int threshold = 3;
auto count = count_if(v.begin(), v.end(),
[threshold](int x) { return x > threshold; });
常见捕获:[] 不捕获;[x] 按值捕获;[&x] 按引用捕获;[=] 默认按值;[&] 默认按引用;也可混合使用。
C++14 可用泛型 Lambda 递归,原文示例如下:
auto dfs = [&](auto&& self, int u, int parent) -> void {
for (int v : graph[u]) {
if (v != parent) self(self, v, u);
}
};
dfs(dfs, 1, 0);
5. 输入与输出
5.1 C 标准输入输出
<cstdio> 提供 printf、scanf、fprintf、fscanf 等。常见格式字符:%d(整数)、%u(无符号整数)、%f(浮点)、%e/%E(科学计数法)、%c(字符)、%s(字符数组)、%o(八进制)、%x/%X(十六进制)。
%5d右对齐并占 5 位,%-5d左对齐,%05d用 0 补齐,%.2f保留两位小数。scanf/fscanf传入变量地址,勿遗漏&(字符数组除外)。返回值是成功读入的变量个数;fscanf返回EOF表示文件结束。- 这些格式化读取默认忽略空格、制表符、换行;读取单个字符时可使用
fgetc并判断EOF。
5.2 C++ 流输入输出
<iostream> 提供 cin、cout,可链式读写,换行常用 "\n"。<iomanip> 可配合 width、fill 格式化输出;oct、hex 可输出八进制、十六进制。
大数据量下,原文建议在输入输出前关闭 C/C++ 流同步:
ios::sync_with_stdio(false);
6. 库函数、C++11/14 特性与宏
6.1 常用库函数
<cstring>:memset(a, 0, sizeof(a))初始化数组;memcpy复制;memcmp比较。memset按字节填充,原文特别说明填0x7F后int元素为0x7F7F7F7F。<cctype>:tolower、toupper,以及isdigit、isalpha、isupper、islower、isalnum等字符判定。<algorithm>:max、min、swap、sort(begin, end)、sort(begin, end, cmp)、reverse(begin, end);排序区间采用半开区间[begin, end)。<cstdlib>:exit(0)立即结束程序,调用前应已输出结果。<ctime>:clock()可用于记录时间点并计算时间间隔。<cassert>:assert(condition)在条件为假时使程序中止,用于发现代码错误,而非处理用户输入错误。<random>与<chrono>:原文建议不用分布不均、周期较短的rand(),改用mt19937与分布对象。
#include <random>
#include <chrono>
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
uniform_int_distribution<int> dist(0, 99);
int r = dist(rng);
<cmath>:abs、三角函数、sqrt、ceil、floor、exp、log、log10、pow、fmod。pow有精度问题,整数幂场景仍应掌握快速幂。
6.2 容器与算法便利特性
vector<int> v = {1, 2, 3, 4, 5};
for (int x : v) cout << x << ' '; // 只读副本
for (int& x : v) x *= 2; // 修改元素
for (const auto& x : v) cout << x; // 避免复制且只读,推荐
auto a = 5;
decltype(a) b = 5;
pair<int, string> p = {1, "apple"};
int* ptr = nullptr;
- 范围
for是 C++11 特性;const auto&可避免复制大型元素并防止意外修改。 auto从初始化表达式推导类型;decltype从已有变量或表达式推导类型。- 统一初始化可写作
int x{5};、vector<int> v{1, 2, 3};。 pair位于<utility>;nullptr是类型安全的空指针。
6.3 宏与 const/constexpr
宏是预处理阶段的文本替换。条件编译如 #ifdef DEBUG 是原文认可的少数合理用途之一。表达式宏应给参数和整体加括号,但原文明确建议:常量优先用 const/constexpr,类函数宏优先用 inline,竞赛中不要定义影响可读性的复杂宏。
constexpr int MAXN = 100000;
constexpr int square(int x) { return x * x; }
constexpr int fib(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; ++i) { // C++14 允许 constexpr 中使用循环
int t = a + b;
a = b;
b = t;
}
return a;
}
const 可以在运行时或编译时初始化;constexpr 必须能在编译时求值,可用于数组大小和编译时计算。其优势包括类型检查、作用域与可调试性。
7. 字符串操作
本章主要讨论 C 风格字符串,使用 <cstring>。始终保证字符数组末尾有 \0。
| 目的 | 函数/方式 | 要点 |
|---|---|---|
| 输出/读取 | cout << str、printf("%s", str)、scanf("%s", str)、cin >> str |
后两类读到空白即停止 |
| 读取整行 | fgets(str, MAX, fin) |
读取到换行停止 |
| 长度 | strlen(str) |
不计末尾 \0 |
| 拼接 | strcat、strncat |
目标数组必须有足够空间 |
| 复制/比较 | strcpy、strcmp |
strcmp 按字典序比较 |
| 查找 | strchr、strstr |
返回指针,可减去 str 得索引 |
| 格式转换 | sscanf、sprintf、to_string |
可在数值与字符串间转换 |
原文提及 strstr 的更复杂问题可用 KMP 算法处理;并给出 C++11 的 to_string 作为便利函数。
8. 文件操作
正式比赛是否读写文件必须以题目说明为准;在线评测通常不需要文件操作。
// 重定向标准输入输出
freopen("XXXXX.in", "r", stdin);
freopen("XXXXX.out", "w", stdout);
// 文件流
ifstream fin("XXXXX.in");
ofstream fout("XXXXX.out");
fin >> a;
fout << b;
// FILE 指针
FILE* fin = fopen("XXXXX.in", "r");
FILE* fout = fopen("XXXXX.out", "w");
fprintf(fout, "%d", ans);
fclose(fin);
fclose(fout);
使用 FILE* 时,fprintf、fscanf、fgets 的首个参数是文件指针。若要改回屏幕输入输出,可将 fin、fout 设为 stdin、stdout。
9. 算法分析与优化
9.1 时间与空间复杂度
时间复杂度表示主要运算次数,用大 O 表示,只保留最高数量级并忽略常数系数;空间复杂度表示主要内存占用,同样用大 O 表示。多个输入规模都会显著影响运行时间时,应同时写入表达式,例如遍历 m × n 数组为 O(mn)。
原文给出 1 秒大致可承受约 5,000,000 次运算的经验估计,并列出增长趋势:
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
对应的大致适用规模:O(n) 可达数百万,O(n log n) 可达数十万,O(n²) 通常约数千,O(2ⁿ) 常在 24 左右,O(n!) 常在 10 左右。实际可承受规模依机器、常数和实现而变化。
9.2 简单优化原则
- 时间:少做运算,尤其减少循环体、递归体内的工作;原文指出整型通常比实型快,位运算快,除法和取模相对慢,函数调用有额外开销。
- 空间:缩小数组、降低维数,使用压缩存储或覆盖旧数据(如滚动数组)。
- 原则:不重复做已完成的事;不做显然无必要的事;不解决无用子问题;不做无意义引用。
10. 编辑器、编译器与标准
原文按 Windows、macOS、Linux 列举了小熊猫 C++(RedPanda)、Code::Blocks、CP Editor、VS Code、CLion、Visual Studio、Xcode、终端与 Vim/Emacs 等工具,核心建议如下:
- 初学者可选择面向 OI、开箱即用的工具;进阶刷题可使用便于样例测试的竞赛编辑器。
- 赛前应熟悉命令行与
g++,许多竞赛评测环境为 Linux 命令行。 - 竞赛通常使用 GCC(
g++);NOIP/NOI 系列常使用-std=c++14,部分环境支持 C++17,应按指定标准编写。
g++ -std=c++14 -O2 -o program program.cpp
复习清单
- 能写出包含头文件、
main、输入输出和正常返回的基本程序。 - 能正确选择
if、switch、三种循环,并控制循环边界。 - 避免数组越界、未初始化指针、返回局部变量引用/地址、浮点直接相等比较。
- 理解值、指针、引用三种传参方式的影响。
- 熟练使用
sort、范围for、auto、nullptr、Lambda 及 C++14 的constexpr。 - 根据数据范围估算时间/空间复杂度,选择可承受的算法。
—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com