C++ 基础语法 — 专题笔记
目录
🔴第 1 章 预备知识
C已经学了
🔴第 2 章 开始学习 C++
🔺用 int main() 不用 void main():void main 逻辑上没问题,但不是标准强制要求,部分系统不支持
🔺使用 C++ 输入输出必须写 #include <iostream>,io = input/output
🔺C++ 头文件无扩展名;C 头文件转 C++ 版本时去掉 .h 并加前缀 c,如 math.h → cmath
🔺using namespace std; 让你直接写 cout 而不用写 std::cout,后面会讲
🔺cout << "text" << endl;:<< 是插入运算符**,cout是一个预定义的对象知道如何显示字符串**,数字,单个字符等(cout<<string;)
🔺endl 输出换行并刷新缓冲区(\n 只换行不刷新)
🔺在C语言中,变量需要再函数或者过程的开头声明,而C++不需要,通常在第一次使用的时候声明
🔺cout会自动判断类型,不像C语言要用%d,%s,%f进行区分,这是因为运算符<<重载,后面会讲
🔺cin也一样,它可以通过键盘输入的一系列字符(即输入)转换为接受信息的变量能够接受的形式
🔺类简介
类是用户定义的一种数据类型。要定义类,需要描述它能够表示什么信息和可对数据执行哪些操作。类之于对象就像类型之于变量。也就是说,类定义描述的是数据格式及其用法,而对象则是根据数据格式规范创建的实体。
现在来看 cout,它是一个 ostream 类对象。ostream 类定义(iostream 文件的另一个成员)描述了 ostream 对象表示的数据以及可以对它执行的操作,如将数字或字符串插入到输出流中。同样,cin 是一个 istream 类对象,也是在 iostream 中定义的。
🔺使用命名空间中的函数
总之,让程序能够访问名称空间 std 的方法有多种,下面是其中的 4 种。
- 将
using namespace std;放在函数定义之前,让文件中所有的函数都能够使用名称空间 std 中所有的元素。 - 将
using namespace std;放在特定的函数定义中,让该函数能够使用名称空间 std 中的所有元素。 - 在特定的函数中使用类似
using std::cout;这样的编译指令,而不是using namespace std;,让该函数能够使用指定的元素,如 cout。 - 完全不使用编译指令
using,而在需要使用名称空间 std 中的元素时,使用前缀std::,如下所示:std::cout << "I'm using cout and endl from the std namespace" << std::endl;
🔴第 3 章 处理数据
程序存储数据需要记录三件事:存在哪里、存什么值、存什么类型。
🔺变量命名规则
- 只能用字母、数字、下划线
- 第一个字符不能是数字
- 不能以两个下划线开头,也不能以下划线 + 大写字母开头
- 原因:这类名称保留给编译器实现(标准库内部大量使用
__xxx/_Xxx形式),用了可能产生冲突或未定义行为
- 原因:这类名称保留给编译器实现(标准库内部大量使用
- C++ 对名称长度没有限制;C99 只有前 63 个字符有效
🔺初始化方式
int a = 5; // 传统 C 风格
int b(5); // 构造函数风格
int c = {5}; // C++11 列表初始化
int d{5}; // C++11 列表初始化(推荐)
int e{}; // 初始化为 0C++11 推荐用 {} 大括号初始化,好处是防止窄化转换:把 double 赋给 int 会直接报错,而不是悄悄截断。
🔺整型:short / int / long / long long
保证:short ≤ int ≤ long ≤ long long,short 至少 16 位,long 至少 32 位。
| 类型 | 最小宽度 | 典型宽度(64 位) |
|---|---|---|
short | 16 位 | 16 位 |
int | 16 位 | 32 位 |
long | 32 位 | 32/64 位 |
long long | 64 位 | 64 位 |
sizeof 返回类型占用的字节数(1 字节 = 8 位):
sizeof(int) // 典型值 4
sizeof(long) // 典型值 4 或 8#include <climits> 提供各整型的极限常量,写可移植代码必备:
INT_MAX // 2147483647
INT_MIN // -2147483648
SHRT_MAX // 32767
LLONG_MAX // 9223372036854775807
UINT_MAX // 4294967295(无符号 int 最大值)字面值进制与后缀:
int a = 42; // 十进制
int b = 042; // 八进制(0 开头)
int c = 0x2A; // 十六进制(0x 开头)
42L // long
42LL // long long
42U // unsigned int
42UL // unsigned longcout 输出进制(设置后持续生效直到显式切换):
cout << hex << 255; // ff
cout << oct << 255; // 377
cout << dec << 255; // 255(切回默认十进制)🔺char 类型与宽字符
char 本质是整型,存储字符对应的编码值(通常 ASCII,0~127)。char 是否有符号由实现决定,需要时显式写 signed char 或 unsigned char。
char ch = 'A'; // 存储 65
cout << ch; // 输出字符 A
cout << (int)ch; // 输出 65cout.put(ch):专门输出单个字符,无论参数是什么整型,都按字符显示:
cout.put('A'); // 输出 A
cout.put(65); // 也输出 A
// 对比:cout << 65 输出的是数字 65一般的字节是 8 位,但国际编程可能用到更大的字符集(如 Unicode),因此有些实现使用 16 位甚至 32 位。C++ 提供了三种宽字符类型:
| 类型 | 宽度 | 字面值前缀 |
|---|---|---|
wchar_t | 实现定义(通常 16/32 位) | L |
char16_t | 16 位(C++11) | u |
char32_t | 32 位(C++11) | U |
wchar_t w = L'中';//这里普通的char只有8位,存储不了'中',所以用wchar_t,char16_t,char32_t来存储
char16_t c16 = u'中';
char32_t c32 = U'中';🔺bool 类型
bool flag = true;
bool done = false;- 任何非零整数 →
true;0→false true→1,false→0(参与算术时自动转换)true/false是 C++ 关键字(C 语言需要<stdbool.h>才有)
🔺const 限定符
const int MONTHS = 12; // 必须在声明时初始化,之后不可修改比 #define 好:有类型检查、有作用域、调试器能看到名字。
🔺浮点数:float / double / long double
| 类型 | 有效十进制位数 | 典型范围 |
|---|---|---|
float | 6~7 位 | ±3.4×10³⁸ |
double | 15~16 位 | ±1.8×10³⁰⁸ |
long double | 18~19 位(实现定义) | — |
3.14 // double(默认,没有后缀的小数是 double)
3.14f // float(后缀 f/F)
3.14L // long double(后缀 l/L)
3.14e2 // 科学计数法 = 314.0//这里是C++独有的e表示法
1.5e-3 // 0.0015
.5e4 // 5,000
7.2E+10 // 72,000,000,000#include <cfloat> 提供浮点极限(类比 climits):FLT_MAX、DBL_DIG 等。
🔺算术运算与类型转换
五种运算符:+、-、*、/、%(取余,仅整型)
整数除法直接截断:7 / 2 = 3(不是 3.5)
混合运算时较小类型自动提升:char/short → int → long → long long,浮点:float → double → long double,整型 + 浮点 → 浮点。
详见 → 类型转换详解
🔺auto 关键字(C++11)
让编译器根据初始值自动推导类型:
auto n = 100; // int
auto x = 3.14; // double
auto y = 3.14f; // float
auto ch = 'A'; // char适合类型名很长的场景(如迭代器 std::vector<int>::iterator),简单类型手动写更清晰。
在 C++11 之前,你必须显式地写出迭代器的完整类型,这在处理容器(尤其是嵌套容器)时非常痛苦。
std::vector<double> scores;
// 必须手动写出冗长的类型名称
std::vector<double>::iterator pv = scores.begin();
使用 auto 关键字后,编译器会自动根据 scores.begin() 的返回值推导出 pv 的类型。
std::vector<double> scores;
// 编译器自动推导类型,代码更简洁、易读
auto pv = scores.begin();
🔴第 4 章 复合类型
复合类型是基于基本类型构建的更复杂的类型,包括数组、字符串、结构、共用体、枚举、指针等。
🔺数组
数组是同类型元素的有序集合,声明格式:类型名 数组名[元素个数]
int arr[5]; // 声明,5个int,未初始化
int arr[5] = {1,2,3,4,5}; // 声明+初始化
int arr[5] = {1,2,3}; // 部分初始化,其余补0
int arr[] = {1,2,3}; // 编译器自动推断长度为3
int arr[5] = {}; // 全部初始化为0(C++11)- 下标从 0 开始,
arr[5]访问第6个元素(越界!) - C++ 不检查越界,越界是未定义行为,但会悄悄执行
- 数组名是第一个元素的地址(不能赋值,不能自增)
sizeof(arr)返回整个数组的字节数;sizeof(arr)/sizeof(arr[0])得元素个数
C++11 列表初始化同样适用数组,且禁止窄化:
double arr[3] = {1, 2, 3}; // OK,int 提升为 double
int arr2[3] = {1.5, 2, 3}; // 错误!double→int 窄化🔺字符串(C 风格)
C 风格字符串是以空字符 \0(ASCII 0)结尾的 char 数组,这是 C++ 从 C 继承的。
char str1[6] = {'H','e','l','l','o','\0'}; // 显式加\0
char str2[6] = "Hello"; // 编译器自动加\0,实际占6字节
char str3[] = "Hello"; // 自动推断长度(6)🔺"Hello" 是字符串字面量,隐式包含 \0,所以长度=字符数+1。
🔺char str[5] = "Hello" 是错误的:5个字节放不下5字符+1个 \0。
常用字符串处理函数(#include <cstring>):
strlen(str) // 返回字符串长度(不含\0)
strcpy(dest, src) // 复制字符串
strncpy(dest, src, n) // 安全复制,最多复制n个字符
strcat(dest, src) // 拼接字符串
strcmp(s1, s2) // 比较:相等返回0,s1<s2返回负数🔺不能直接用 = 给 char 数组赋值(只能初始化时用),必须用 strcpy;不能用 == 比较字符串,必须用 strcmp。
cin 读取字符串的问题:
char word[20];
cin >> word; // 遇空格/换行就停止,不读空格
cin.getline(word, 20); // 读整行(含空格),丢弃换行符
cin.get(word, 20); // 读整行,保留换行符在缓冲区🔺cin >> 遗留换行符问题
键盘输入流在你按下 Enter 后长这样:25\n
cin >> 是按词读取,读到空白字符就停下来,但不消耗那个空白字符,\n 还留在缓冲区里。
问题复现:
int age;
char name[20];
cin >> age; // 输入 25 回车 → 读走 "25",\n 留在缓冲区
cin.getline(name, 20); // 立刻读到 \n,认为一行已结束,name 得到空字符串!解决方法:
cin >> age;
cin.get(); // 吃掉那个 \n
// 或者
cin.ignore(1000, '\n'); // 忽略最多1000个字符,直到遇到 \n(更安全)
// 或者链式写法
(cin >> age).get();
cin.getline(name, 20); // 现在才能正确读到下一行各函数对 \n 的处理方式:
| 函数 | 遇到 \n 的行为 | 缓冲区还剩 \n? |
|---|---|---|
cin >> | 停止读取,不消耗 \n | ✅ 还剩 |
cin.get(arr, n) | 停止读取,不消耗 \n | ✅ 还剩 |
cin.getline(arr, n) | 读取并丢弃 \n | ❌ 已清除 |
cin.get() | 读取并丢弃那一个字符 | ❌ 已清除 |
口诀:只要看到 cin >> 后面跟着读整行,就要先清一下缓冲区。
🔺string 类(C++ 风格)
#include <string> 后可用 std::string,比 C 风格字符串更安全方便。
string s1 = "Hello";
string s2("World");
string s3 = s1 + " " + s2; // 拼接,直接用 +
string s4;
getline(cin, s4); // 读整行(含空格)| 操作 | C 风格 | string 类 |
|---|---|---|
| 赋值 | strcpy(a, b) | a = b |
| 拼接 | strcat(a, b) | a + b |
| 比较 | strcmp(a, b) | a == b |
| 长度 | strlen(a) | a.size() |
| 读整行 | cin.getline(a,n) | getline(cin, a) |
🔺string 会自动管理内存,不用担心数组越界或手动分配空间。
🔺s.size() 返回类型是 size_t(无符号整型),与 int 混用时注意。
🔺结构(struct)
结构可以把不同类型的数据组合成一个自定义类型。
struct Person {
string name;
int age;
double height;
};
Person p1 = {"Alice", 20, 1.65}; // 列表初始化
Person p2;
p2.name = "Bob"; // 用 . 访问成员
p2.age = 25;🔺C++ 中声明结构变量不需要加 struct 关键字(C 语言需要):Person p; 而非 struct Person p;
🔺结构可以作为函数参数和返回值,也可以赋值(p2 = p1 是逐成员复制)。
位字段(了解即可): 用于节省内存,指定成员占几个 bit:
struct Flags {
unsigned int a : 1; // 占1位
unsigned int b : 2; // 占2位
};🔺共用体(union)
共用体的所有成员共享同一块内存,大小等于最大成员的大小,同一时刻只有一个成员有效。
union Data {
int i;
double d;
char c;
};
Data x;
x.i = 10; // 使用 int
x.d = 3.14; // 改用 double,i 的值已被覆盖(未定义)🔺用途:节省内存(如网络协议解析),或在不同类型间复用同一存储。不是常规需求,了解即可。
🔺枚举(enum)
枚举创建一组命名的整型常量,默认从 0 开始依次递增。
enum Color { RED, GREEN, BLUE }; // RED=0, GREEN=1, BLUE=2
enum Color { RED=1, GREEN=5, BLUE }; // RED=1, GREEN=5, BLUE=6
Color c = GREEN;🔺枚举变量只能被赋同枚举的值(不能直接赋整数,除非强制转换)。
🔺C++11 强枚举(enum class):避免名称污染,必须加作用域前缀:
enum class Direction { UP, DOWN, LEFT, RIGHT };
Direction d = Direction::UP; // 必须写前缀🔺指针基础
指针存储的是变量的内存地址,而非变量的值本身。
int a = 10;
int* p = &a; // p 存储 a 的地址,& 是取地址运算符
cout << p; // 输出地址(十六进制,如 0x61fe14)
cout << *p; // 输出 10,* 是解引用运算符(取该地址的值)
*p = 20; // 通过指针修改 a 的值,a 现在是 20🔺声明指针时 * 紧跟变量名,不是类型的一部分:
int* p1, p2; // p1 是 int*,p2 是 int(常见坑!)
int *p1, *p2; // 两个都是 int*(推荐写法)🔺指针必须在解引用前初始化,未初始化的指针指向随机地址,解引用是未定义行为(极危险)。
int* p;
*p = 10; // 危险!p 指向不知道哪里🔺如果要让指针指向特定的内存地址,需要用强制类型转换,否则编译器会报错(整数不能隐式转为指针):
int* p = (int*)0x61fe14; // 强制把整数地址转为 int* 类型
*p = 42; // 向该地址写入值这种写法在嵌入式/驱动开发中操作硬件寄存器时会用到,普通程序里直接指定地址会因访问非法内存而崩溃。
🔺new 和 delete(动态内存)
new 在**堆(heap)**上分配内存并返回地址;delete 释放该内存。
int* p = new int; // 在堆上分配一个 int
*p = 42;
delete p; // 释放,p 变成悬空指针
p = nullptr; // 好习惯:释放后置空
int* arr = new int[10]; // 分配10个int的数组
arr[0] = 1;
delete[] arr; // 数组必须用 delete[],不能用 delete
arr = nullptr;🔺new / delete 必须配对,new[] / delete[] 必须配对,混用是未定义行为。
🔺new 失败时(内存不足)会抛出 std::bad_alloc 异常(不是返回 nullptr)。
🔺delete 后指针变为悬空指针,再次解引用是未定义行为,置 nullptr 可避免误用。
栈 vs 堆:
| 特性 | 栈(自动存储) | 堆(动态存储) |
|---|---|---|
| 分配方式 | 自动(作用域) | 手动(new/delete) |
| 生命周期 | 离开作用域自动销毁 | 手动释放前一直存在 |
| 大小限制 | 小(通常几MB) | 大(受物理内存限制) |
| 速度 | 快 | 相对慢 |
🔺指针与数组的关系
数组名本质上是指向首元素的常量指针,两者在很多情况下可互换。
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // p 指向 arr[0],不需要 &
cout << arr[2]; // 30
cout << *(p + 2); // 30,指针算术:+2 跳过 2×sizeof(int) 字节
cout << p[2]; // 30,指针也可以用下标运算符
p++; // OK:p 现在指向 arr[1]
arr++; // 编译错误:arr 是常量,不可修改🔺**arr++ 是编译错误**,编译器直接拒绝,报类似 error: lvalue required as increment operand 的错误,不会产生运行时问题。
🔺指针算术:p + n 移动 n × sizeof(*p) 字节,而不是 n 个字节。
🔺区别:数组名不能被赋值或自增,是常量指针;指针变量可以移动和重新指向。
🔺指针与字符串
指针和 C 风格字符串的结合非常常见,需要单独理解。
用指针指向字符串字面量:
const char* p = "Hello"; // p 指向字符串首字符 'H'
cout << p; // 输出 Hello(cout 遇到 char* 会一直打印到 \0)
cout << *p; // 输出 H(单个字符)
cout << (void*)p; // 输出地址(强转才能看到地址,否则 cout 把它当字符串打)🔺字符串字面量 "Hello" 存储在程序的只读内存区,必须用 const char* 接收,不能通过指针修改:
const char* p = "Hello";
p[0] = 'h'; // 运行时崩溃!只读内存不可写
p = "World"; // OK,p 本身可以指向别处与 char[] 的区别:
char arr[] = "Hello"; // 把字符串复制到栈上的数组,可修改
const char* p = "Hello"; // p 指向只读区,不可修改
arr[0] = 'h'; // OK,arr 是可写的副本
p[0] = 'h'; // 崩溃!p 指向只读区用指针遍历字符串:
const char* p = "Hello";
while (*p != '\0') { // 或直接写 while (*p),\0 转布尔为 false
cout << *p;
p++; // 移到下一个字符
}指针与字符串函数: strlen、strcpy 等函数的参数本质上都是 char*,传入数组名和传入指针效果相同:
char arr[] = "Hello";
char* p = arr;
strlen(arr); // 5
strlen(p); // 5,完全等价🔺cout << p(char* 类型)输出的是整个字符串而不是地址;若想输出地址需强转为 void*:cout << (void*)p。其他类型的指针(如 int*)cout 输出的才是地址,char* 是特例。
🔺const 与指针
const 和指针结合有两种不同含义,位置决定含义:
const int* p = &a; // 指向常量的指针:不能通过 p 修改值,但 p 本身可以指向别处
int* const p = &a; // 常量指针:p 不能指向别处,但可以通过 p 修改值
const int* const p = &a; // 两者都不能改记忆口诀:const 在 * 左边 → 值不变;const 在 * 右边 → 指针不变。
🔺函数参数用 const int* arr 表示”我不会修改你传入的数组”,是良好习惯。
🔺模板类 vector 和 array(C++11)
vector 和 array 是 C++ 标准库提供的更安全的数组替代品。
vector(#include <vector>): 动态大小,可增删:
vector<int> v1(5); // 5个元素,初始化为0
vector<int> v2 = {1,2,3}; // 列表初始化
v2.push_back(4); // 末尾追加元素
v2.size(); // 当前元素个数array(#include <array>,C++11): 固定大小,但比裸数组更安全:
array<int, 5> a = {1,2,3,4,5};
a.size(); // 5
a.at(10); // 越界时抛出异常(裸数组不检查)| 特性 | 裸数组 int[] | array<int,N> | vector<int> |
|---|---|---|---|
| 大小 | 固定 | 固定 | 动态 |
| 越界检查 | 无 | .at() 有 | .at() 有 |
| 传参退化 | 退化为指针 | 不退化 | 不退化 |
| 内存位置 | 栈 | 栈 | 堆 |
🔺现代 C++ 推荐:固定大小用 array,动态大小用 vector,尽量避免裸数组。
🔴第 5 章 循环和关系表达式
🔺for 循环
for 循环适合已知循环次数的场景,由三部分组成:
for (初始化表达式; 测试表达式; 更新表达式) {
循环体;
}执行顺序:初始化 → 测试 → 循环体 → 更新 → 测试 → …(测试为 false 时退出)
for (int i = 0; i < 5; i++) {
cout << i << " "; // 输出 0 1 2 3 4
}🔺三个表达式都可以省略,for(;;) 是无限循环(等价于 while(true))。
🔺i++ 与 ++i:对于简单变量效果相同;对于对象(如迭代器),++i 效率更高(i++ 要创建临时副本),推荐用前缀 ++i。
🔺逗号运算符:for 的初始化和更新部分可用逗号分隔多个表达式:
for (int i = 0, j = 10; i < j; i++, j--) {
cout << i << " " << j << "\n";
}逗号运算符从左到右依次求值,整个表达式的值等于最右侧表达式的值,优先级是所有运算符中最低的。
🔺关系运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
== | 等于 | a == b |
!= | 不等于 | a != b |
< | 小于 | a < b |
> | 大于 | a > b |
<= | 小于或等于 | a <= b |
>= | 大于或等于 | a >= b |
关系运算符返回 bool(true 或 false),优先级低于算术运算符,但高于赋值运算符。
🔺经典错误:= 写成 ==(反过来也是):
if (a = 5) // 赋值,a 变为 5,条件永远为真!
if (a == 5) // 比较,正确
// 防御写法:把常量写在左边,赋值会直接编译报错
if (5 == a) // OK
if (5 = a) // 编译错误,能提前发现笔误🔺浮点数不能用 == 直接比较(精度问题),应比较差值的绝对值:
double a = 0.1 + 0.2; // 实际是 0.30000000000000004
if (a == 0.3) // 可能为 false!
if (fabs(a - 0.3) < 1e-9) // 正确做法,#include <cmath>🔺字符串比较:string 类直接用 ==;C 风格字符串(char*)比较的是地址而非内容,必须用 strcmp():
string s1 = "hello", s2 = "hello";
s1 == s2; // true,正确
char a[] = "hello", b[] = "hello";
a == b; // 比较地址,错误
strcmp(a, b) == 0; // 正确🔺while 循环
while 适合不确定循环次数、依赖条件的场景:
while (测试表达式) {
循环体;
}先判断条件,条件为 false 则一次也不执行。
int n = 1;
while (n < 5) {
cout << n << " ";
n++; // 输出 1 2 3 4
}🔺for 循环和 while 循环完全等价,可以互相改写。for 更适合计数循环,while 更适合事件驱动循环(比如”读到 EOF 为止”)。
用 while 实现延时(了解):
clock_t start = clock();
while (clock() - start < CLOCKS_PER_SEC) {
; // 空循环,等待 1 秒(需 #include <ctime>)
}clock_t 是 <ctime> 中定义的类型,CLOCKS_PER_SEC 是每秒的时钟节拍数。
🔺do-while 循环
do-while 是先执行循环体、再判断条件,至少执行一次:
do {
循环体;
} while (测试表达式); // 注意末尾有分号int choice;
do {
cout << "请输入 1~3:";
cin >> choice;
} while (choice < 1 || choice > 3); // 输入不合法就重来🔺适合”先做一次,再验证”的场景,如菜单交互、输入校验。大多数情况下 for 或 while 更常用。
🔺基于范围的 for 循环(C++11)
C++11 新增,用于遍历容器或数组,语法更简洁:
for (元素类型 变量名 : 容器) {
循环体;
}int arr[] = {1, 2, 3, 4, 5};
for (int x : arr) {
cout << x << " "; // 输出 1 2 3 4 5
}
vector<string> words = {"hello", "world"};
for (const string& w : words) { // 引用避免拷贝
cout << w << " ";
}🔺用 auto 更简洁,用引用 & 避免拷贝,用 const 防止误修改:
for (auto& x : arr) {
x *= 2; // 引用可以修改原数据
}
for (const auto& x : arr) {
cout << x; // 只读,最常见写法
}🔺普通值传递会拷贝每个元素;对于大对象(如 string、结构体)务必用 const auto&。
🔺break 和 continue
两者都用于控制循环流程,适用于 for、while、do-while:
// break:立即退出当前循环
for (int i = 0; i < 10; i++) {
if (i == 5) break;
cout << i << " "; // 输出 0 1 2 3 4
}
// continue:跳过本次循环剩余部分,进入下一次迭代
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) continue;
cout << i << " "; // 输出 1 3 5 7 9(跳过偶数)
}🔺嵌套循环中,break / continue 只作用于最内层的那个循环:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) break; // 只跳出内层 for
cout << i << "," << j << " ";
}
}
// 输出:0,0 1,0 2,0🔺C++ 有 goto 但几乎不用,要跳出多层嵌套循环可以用标志变量或把循环封装成函数(用 return)。
🔺cin 与循环配合
cin 读取失败(如输入字母却期望 int)会进入失败状态,后续所有读取都被跳过。
int n;
while (cin >> n) { // cin >> n 成功返回 cin 对象(转 bool 为 true),失败为 false
cout << n * 2 << "\n";
}
// 输入 EOF(Windows: Ctrl+Z,Linux: Ctrl+D)或非法字符时退出循环检测并恢复失败状态:
if (!(cin >> n)) {
cin.clear(); // 清除错误标志
cin.ignore(1000, '\n'); // 丢弃缓冲区中残留的一行
}🔺cin >> 遇到 EOF 返回 false,这是读取文件/标准输入到结尾的标准写法:
int sum = 0, val;
while (cin >> val) {
sum += val;
}
cout << "总和:" << sum;🔺嵌套循环与二维数组
二维数组用嵌套循环遍历,外层控制行,内层控制列:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 4; col++) {
cout << matrix[row][col] << "\t";
}
cout << "\n";
}🔺声明二维数组时第二维(列数)不能省略(因为编译器需要知道每行多长来计算偏移):
void print(int arr[][4], int rows); // 正确:列数必须给
void print(int arr[][], int rows); // 错误!🔺sizeof 技巧:
int rows = sizeof(matrix) / sizeof(matrix[0]); // 行数 = 3
int cols = sizeof(matrix[0]) / sizeof(matrix[0][0]); // 列数 = 4🔺循环文本输入
循环文本输入的核心模式:读取 → 判断哨兵 → 处理 → 重复。
1. 用哨兵值(sentinel)结束循环
哨兵值是一个约定的特殊输入,告知程序”停止读取”。
// 哨兵值为特定字符串
string word;
while (cin >> word && word != "quit") {
cout << "你输入了:" << word << "\n";
}
// 输入 quit 时退出// 哨兵值为特定字符(如 #)
char ch;
while (cin.get(ch) && ch != '#') {
cout << ch;
}2. 逐字符读取循环
cin.get(ch) 每次读一个字符,包括空格和换行,成功返回 cin(转 bool 为 true),EOF 时返回 false:
char ch;
int count = 0;
while (cin.get(ch)) { // 读到 EOF 为止(Ctrl+Z/Ctrl+D)
if (ch != '\n')
count++;
}
cout << "非换行字符数:" << count;🔺cin >> ch 会跳过空白字符;cin.get(ch) 不跳过,逐字符处理时用后者。
3. 逐行读取循环(string 版)
string line;
while (getline(cin, line)) { // 读到 EOF 为止
if (line == "quit") break; // 也可以加哨兵行
cout << "行内容:" << line << "\n";
}🔺getline 会读取并丢弃行末 \n,line 中不含换行符。
4. 统计单词 / 字符的完整示例
#include <iostream>
#include <string>
using namespace std;
int main() {
string word;
int count = 0;
cout << "输入若干单词,输入 done 结束:\n";
while (cin >> word && word != "done") {
count++;
}
cout << "共输入了 " << count << " 个单词\n";
}5. 各读取方式对比
| 方式 | 遇空格 | 遇换行 | 返回值 | 适用场景 |
|---|---|---|---|---|
cin >> s | 停止 | 停止 | cin(bool) | 按词读取 |
cin.get(ch) | 读取 | 读取 | cin(bool) | 逐字符处理 |
cin.getline(arr,n) | 读取 | 丢弃 | cin(bool) | C 风格字符串整行 |
getline(cin, s) | 读取 | 丢弃 | cin(bool) | string 整行读取 |
🔺以上四种方式在循环条件中都可直接当 bool 用,EOF 或失败时为 false,是读取到文件末尾的标准写法。
6. 文件尾条件(EOF)
EOF(End Of File)不是一个字符,而是一种状态,由操作系统在输入流结束时设置。
EOF 的触发方式:
| 平台 | 键盘模拟 EOF |
|---|---|
| Windows | 行首按 Ctrl+Z 再回车 |
| Linux / macOS | Ctrl+D |
cin 遇到 EOF 的行为:
int n;
while (cin >> n) { // EOF 或输入失败时,cin 转 bool 为 false,退出循环
cout << n << "\n";
}cin >> n 返回的是 cin 对象本身,cin 对象被转为 bool 时:
- 流正常 →
true - 遇到 EOF 或读取失败 →
false
检查 EOF 状态(cin.eof()):
char ch;
while (cin.get(ch)) {
cout << ch;
}
if (cin.eof()) {
cout << "\n已到达文件末尾\n";
} else {
cout << "\n读取出错\n";
}🔺cin.eof() 返回 true 说明是正常结束;返回 false 说明是其他错误(如类型不匹配),两者都会让 cin 转 bool 为 false。
cin.fail() 与 EOF 的区别:
| 状态函数 | 触发条件 |
|---|---|
cin.eof() | 仅 EOF |
cin.fail() | EOF或类型不匹配等读取失败 |
cin.bad() | 流发生严重错误(磁盘损坏等) |
cin.good() | 以上均未发生,流状态正常 |
// 读取失败后恢复:
cin.clear(); // 清除错误标志,恢复 good 状态
cin.ignore(1000, '\n'); // 丢弃缓冲区残留内容🔺一旦流进入失败状态,后续所有读取都会被跳过,必须先 clear() 才能继续读取。
7. getchar() 与 putchar()(C 风格)
来自 <cstdio>(C 的 <stdio.h>),是 C 语言的字符 I/O 函数,在 C++ 中也可用。
#include <cstdio>
int ch;
while ((ch = getchar()) != EOF) { // getchar 返回 int!
putchar(ch); // 原样输出
}getchar() 返回 int 而不是 char 的原因:
EOF 的值通常是 -1,而 char 在某些平台是无符号的(范围 0~255),无法表示 -1。用 int 接收才能区分”普通字符”和”EOF”:
char ch;
while ((ch = getchar()) != EOF) { ... } // 危险!若 char 无符号,EOF(-1) 会被截断为 255,永远不等于 EOF
int ch;
while ((ch = getchar()) != EOF) { ... } // 正确getchar / putchar vs cin.get / cout:
| 对比 | getchar / putchar | cin.get(ch) / cout |
|---|---|---|
| 来源 | C 标准库 <cstdio> | C++ iostream |
| 返回类型 | int(需手动判断 EOF) | cin 对象(bool 友好) |
| 速度 | 通常更快(无对象开销) | 稍慢(但差异微小) |
| 风格 | C 风格 | C++ 风格(推荐) |
🔺C++ 程序推荐用 cin.get(ch) 和 cout;getchar / putchar 在底层性能敏感或与 C 代码混用时才用。
🔴第 6 章 分支语句和逻辑运算符
🔺if 和 if-else 语句
if (条件) {
语句;
}
if (条件) {
语句A;
} else {
语句B;
}🔺只有一条语句时可以省略花括号,但推荐始终加花括号,防止后续添加语句时出错。
🔺悬空 else:else 与最近的 if 配对,加花括号可消除歧义:
// 看起来 else 属于外层 if,实际属于内层 if
if (a > 0)
if (b > 0)
cout << "both";
else // 属于 if (b > 0),不属于 if (a > 0)
cout << "a only";🔺if-else if 多分支
if (score >= 90) {
cout << "A";
} else if (score >= 80) {
cout << "B";
} else if (score >= 70) {
cout << "C";
} else {
cout << "F";
}🔺条件从上到下依次判断,一旦某个条件为真就执行对应块,后续条件不再判断。因此条件的顺序很重要,范围大的条件要放后面。
🔺逻辑运算符:&& || !
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
&& | 逻辑与 | a>0 && b>0 | 两者都为真才为真 |
|| | 逻辑或 | a>0 || b>0 | 至少一个为真就为真 |
! | 逻辑非 | !(a==b) | 取反 |
优先级:! > && > ||,三者都低于关系运算符,所以 a>0 && b<10 不需要加括号。
🔺短路求值:
&&:左侧为 false,右侧不执行(因为结果已确定为 false)||:左侧为 true,右侧不执行(因为结果已确定为 true)
int arr[] = {1, 2, 3};
int i = 5;
if (i < 3 && arr[i] > 0) { ... } // 安全!i<3 为 false,arr[i] 不执行,不越界🔺C++ 还提供关键字替代写法(可读性更好,但较少用):
| 符号 | 关键字 |
|---|---|
&& | and |
|| | or |
! | not |
🔺cctype 字符函数库
#include <cctype>(C 语言的 <ctype.h>),提供一组判断和转换字符类别的函数,参数和返回值都是 int:
判断函数(条件为真返回非零,否则返回 0):
| 函数 | 含义 |
|---|---|
isalpha(c) | 是字母(a |
isdigit(c) | 是数字(0~9) |
isalnum(c) | 是字母或数字 |
isspace(c) | 是空白字符(空格、制表符、换行) |
isupper(c) | 是大写字母 |
islower(c) | 是小写字母 |
ispunct(c) | 是标点符号 |
isprint(c) | 是可打印字符(含空格) |
转换函数:
toupper('a') // 返回 'A'(已是大写则不变)
tolower('A') // 返回 'a'(已是小写则不变)🔺这些函数比自己写范围判断更可靠,且能处理本地化字符集。常见用法:
string s = "Hello, World! 123";
int letters = 0, digits = 0, spaces = 0;
for (char c : s) {
if (isalpha(c)) letters++;
else if (isdigit(c)) digits++;
else if (isspace(c)) spaces++;
}🔺?: 条件运算符(三目运算符)
条件 ? 表达式A : 表达式B条件为真返回 A,为假返回 B。是 if-else 的紧凑写法,适合简单赋值或输出:
int max = (a > b) ? a : b; // 取较大值
cout << (n % 2 == 0 ? "偶数" : "奇数");🔺?: 是运算符,可以用在表达式中;if-else 是语句,不能直接嵌入表达式里。
🔺嵌套三目运算符可读性差,超过两层建议改用 if-else。
🔺switch 语句
switch 适合对同一变量的多个离散值进行分支,比多层 if-else if 更清晰:
switch (整型表达式) {
case 常量1:
语句;
break;
case 常量2:
语句;
break;
default:
语句;
break;
}int day = 3;
switch (day) {
case 1: cout << "周一"; break;
case 2: cout << "周二"; break;
case 3: cout << "周三"; break;
default: cout << "其他"; break;
}🔺**switch 只能用整型**(int、char、enum),不能用浮点数或字符串。
🔺没有 break 会贯穿(fall-through):执行完一个 case 后继续执行下一个 case,直到遇到 break 或 switch 结尾:
switch (x) {
case 1:
case 2:
cout << "1 或 2"; // 故意利用贯穿,两个 case 共用同一处理
break;
case 3:
cout << "3";
// 没有 break,会继续执行 case 4 的代码!
case 4:
cout << "4";
break;
}🔺**default 可以放在任意位置**(通常放最后),匹配所有未列出的值;不写 default 时无匹配则跳过整个 switch。
🔺switch vs if-else if:
- 相同变量的多个等值判断 → 用
switch,更清晰 - 范围判断(
x > 10)或不同变量 → 只能用if-else if
🔺简单文件 I/O
C++ 用 fstream 库读写文件,接口与 cin / cout 几乎完全相同,只需替换对象名。
写文件(ofstream):
#include <fstream>
using namespace std;
ofstream outFile;
outFile.open("output.txt"); // 打开文件(不存在则创建,存在则清空)
if (!outFile) { // 检查是否打开成功
cerr << "无法打开文件\n";
return 1;
}
outFile << "Hello, file!\n"; // 用法与 cout 完全相同
outFile << 42 << " " << 3.14 << "\n";
outFile.close(); // 关闭文件(析构时也会自动关闭)读文件(ifstream):
#include <fstream>
using namespace std;
ifstream inFile;
inFile.open("input.txt");
if (!inFile) {
cerr << "无法打开文件\n";
return 1;
}
int n;
double d;
inFile >> n >> d; // 用法与 cin 完全相同
string line;
while (getline(inFile, line)) { // 逐行读取
cout << line << "\n";
}
inFile.close();🔺可以在声明时直接传文件名(等价于 open()):
ofstream out("output.txt");
ifstream in("input.txt");🔺ofstream 默认会清空已有文件;追加模式需指定标志:
ofstream out("log.txt", ios::app); // 追加模式,不清空原有内容🔺文件路径:相对路径基于程序运行时的当前目录,不是源文件目录。Windows 路径分隔符用 \\ 或 / 均可:
ifstream in("data/input.txt"); // 相对路径
ifstream in("C:/data/input.txt"); // 绝对路径🔺inFile >> n 和 cin >> n 行为一致,包括 EOF 判断:
int val, sum = 0;
while (inFile >> val) { // 读到文件末尾自动退出
sum += val;
}缓冲区问题:
类似C语言的缓冲区问题,c语言读写切换要fseek重新定位文件指针
C++ 不再使用一个通用的 fseek,而是根据“读”和“写”分成了两个不同的指针函数。你只需要记住 G 和 P :
seekg(Seek Get) :用于 输入流 (ifstream) ,移动“读”指针。seekp(Seek Put) :用于 输出流 (ofstream) ,移动“写”指针。
🔴第7章 函数
🔺函数基础
函数是完成特定任务的具名代码块,C++ 程序由一个或多个函数组成,main() 是程序入口。
函数定义格式:
返回类型 函数名(参数列表) {
函数体;
return 返回值; // 返回类型为 void 时可省略
}double square(double x) {
return x * x;
}
void greet(string name) {
cout << "Hello, " << name << "\n";
// 无返回值,return 可省略
}🔺函数必须先声明(原型)或定义,再调用。把定义放在 main() 后面时,需要在前面写函数原型:
double square(double x); // 函数原型(声明),分号结尾
int main() {
cout << square(3.0); // 可以调用
}
double square(double x) { return x * x; } // 定义放后面🔺函数原型中参数名可以省略,只写类型即可:double square(double);
🔺参数传递:值传递 vs 引用传递
调用函数时,系统 单独为形参分配一块新内存 ,将实参的值复制到形参的内存中
值传递(默认): 函数收到的是实参的副本,修改形参不影响原变量:
void addOne(int n) {
n++; // 只修改副本,调用方的变量不变
}
int x = 5;
addOne(x);
cout << x; // 仍然是 5引用传递(&): 形参是实参的别名,修改形参就是修改原变量:
void addOne(int& n) {
n++; // 直接修改原变量
}
int x = 5;
addOne(x);
cout << x; // 6🔺引用传递的两个主要用途:
- 修改调用方的变量(如 swap)
- 避免大对象拷贝(传
const string&比传string高效)
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
// 传大对象用 const 引用,只读不改
void print(const string& s) {
cout << s;
}🔺**const 引用**:既避免拷贝,又防止函数意外修改实参,是传递大对象的推荐方式。
🔺函数与数组
数组名传入函数时退化为指针,函数收到的是首元素地址,不是整个数组的副本:
// 以下两种写法等价,arr 都是 int*
void print(int arr[], int n) { ... }
void print(int* arr, int n) { ... }🔺因为退化为指针,函数内 sizeof(arr) 得到的是指针大小(4 或 8 字节),而不是数组大小,所以必须额外传入长度 n:
void sum(int arr[], int n) {
int total = 0;
for (int i = 0; i < n; i++)
total += arr[i];
return total;
}用指针范围传递(首尾指针):
// 传入首指针和尾后指针(STL 风格)
int sum(const int* begin, const int* end) {
int total = 0;
for (const int* p = begin; p != end; p++)
total += *p;
return total;
}
int arr[] = {1, 2, 3, 4, 5};
sum(arr, arr + 5); // arr+5 指向最后一个元素的下一位🔺函数内修改数组元素会影响原数组(因为是指针,不是副本)。若不想修改,加 const:
void print(const int* arr, int n); // 承诺不修改数组内容函数的返回值不可以是数组,但有三种变通方式:
① 用结构体包裹(返回副本):
struct ArrWrap { int data[5]; };
ArrWrap getArr() {
ArrWrap w = {{1, 2, 3, 4, 5}};
return w; // 返回结构体副本,数组随之复制
}② 用 std::array(C++11,推荐):
#include <array>
std::array<int, 5> getArr() {
return {1, 2, 3, 4, 5}; // 值语义,安全返回
}③ 用 std::string / std::vector 返回动态数组:
#include <vector>
std::vector<int> getArr() {
return {1, 2, 3, 4, 5}; // 堆上分配,大小任意
}🔺不能返回局部数组的指针——函数返回后局部变量销毁,指针悬空。
std::array 传参的特殊性:
std::array 是值类型,传参时整体拷贝,不会退化为指针,这与 C 数组和 string 不同:
void foo(std::array<int, 5> arr); // 拷贝全部 5 个元素,arr 是副本
void foo(int arr[], int n); // C 数组:退化为指针,无拷贝
void foo(std::string s); // string:拷贝堆上字符数据缺点:数组较大时,拷贝开销大(需逐元素复制,O(N))且占用较多栈空间。推荐传 const std::array<int, 5>& 避免拷贝:
void foo(const std::array<int, 5>& arr); // 引用传递,零拷贝🔺函数与二维数组
传递二维数组时,必须指定列数(编译器需要知道每行的步长):
void print(int arr[][4], int rows) {
for (int i = 0; i < rows; i++)
for (int j = 0; j < 4; j++)
cout << arr[i][j] << " ";
}等价的指针写法:
void print(int (*arr)[4], int rows); // arr 是指向"含4个int的数组"的指针🔺函数与字符串
C 风格字符串(char*)传入函数时同样退化为指针,但不需要传长度,因为 \0 标志结尾:
int myStrlen(const char* s) {
int count = 0;
while (*s != '\0') { count++; s++; }
return count;
}string 类传参推荐用 const string&:
void print(const string& s) { cout << s; }🔺函数与结构体
结构体默认值传递(整个结构体被复制),对大结构体效率低;推荐传引用或指针:
struct Point { double x, y; };
// 值传递:复制整个结构体
double dist(Point p1, Point p2) {
return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
}
// 引用传递:避免复制(推荐)
double dist(const Point& p1, const Point& p2) { ... }
// 指针传递:用 -> 访问成员
void move(Point* p, double dx, double dy) {
p->x += dx;
p->y += dy;
}🔺函数也可以返回结构体(值返回,会复制;现代编译器通常会优化掉这次复制):
Point midpoint(const Point& a, const Point& b) {
return {(a.x + b.x) / 2, (a.y + b.y) / 2};
}🔺函数指针
函数也有地址,可以用函数指针存储和调用:
// 声明函数指针:指向"接受两个int,返回int"的函数
int (*fp)(int, int);
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
fp = add; // 赋值(不需要 &,函数名本身就是地址)
cout << fp(3, 4); // 调用,输出 7
fp = mul;
cout << fp(3, 4); // 输出 12🔺函数指针的典型用途:回调函数(把函数作为参数传给另一个函数):
// 接受一个函数指针作为参数
void apply(int arr[], int n, int (*func)(int)) {
for (int i = 0; i < n; i++)
arr[i] = func(arr[i]);
}
int doubleIt(int x) { return x * 2; }
int arr[] = {1, 2, 3};
apply(arr, 3, doubleIt); // arr 变为 {2, 4, 6}用 auto 或 typedef 简化函数指针类型:
typedef int (*BinaryOp)(int, int); // 给函数指针类型起别名
BinaryOp fp = add;
// C++11 用 using 更清晰
using BinaryOp = int (*)(int, int);
BinaryOp fp = add;
// auto 推导
auto fp = add; // 最简洁🔺递归
函数调用自身称为递归,必须有终止条件(基准情形),否则无限递归导致栈溢出:
int factorial(int n) {
if (n <= 1) return 1; // 基准情形
return n * factorial(n - 1); // 递归调用
}
// factorial(4) = 4 * factorial(3) = 4 * 3 * 2 * 1 = 24🔺递归调用会在栈上不断压入新的栈帧,深度过大会栈溢出(stack overflow)。能用循环解决的问题,通常循环比递归效率更高。
🔺内联函数(inline)
inline 建议编译器将函数调用直接展开为函数体代码,避免函数调用的开销(压栈、跳转、返回):
inline int square(int x) { return x * x; }
// 编译器可能将 square(5) 直接替换为 5 * 5🔺inline 只是建议,编译器可以忽略(如函数太复杂)。适合短小、频繁调用的函数。
🔺与 #define 宏的区别:inline 有类型检查,宏没有;inline 是真正的函数,宏是文本替换(容易出错):
#define SQUARE(x) x * x // 危险:SQUARE(1+2) = 1+2*1+2 = 5,不是 9
inline int square(int x) { return x * x; } // 安全:square(1+2) = 9🔺默认参数
函数参数可以有默认值,调用时可以省略有默认值的参数:
void greet(string name, string greeting = "Hello") {
cout << greeting << ", " << name << "\n";
}
greet("Alice"); // 输出 Hello, Alice
greet("Bob", "Hi"); // 输出 Hi, Bob🔺默认参数必须从右向左设置,不能跳过中间参数:
void func(int a, int b = 2, int c = 3); // OK
void func(int a = 1, int b, int c = 3); // 错误!b 没有默认值但在有默认值的参数之间🔺默认参数通常写在函数原型中,不在定义中重复写(否则编译器报错)。
🔴第 8 章 函数探幽
C++ Primer Plus 第 8 章;内联函数、默认参数已在第 7 章覆盖,本章补充引用变量、函数重载和函数模板。
🔺引用变量是什么
引用是已定义变量的别名(alias),对引用的所有操作等同于直接操作原变量,底层共享同一块内存。
int a = 10;
int& ref = a; // ref 是 a 的别名
ref = 20; // 等同于 a = 20
cout << a; // 输出 20
cout << &ref; // 与 &a 完全相同的地址🔺引用和原变量地址相同,不是副本,不占新内存。
🔺创建引用变量
int a = 10;
int& ref = a; // & 紧跟类型,声明引用,必须同时初始化两条铁律:
-
引用必须在声明时初始化:
int& ref; // 错误! int& ref = a; // 正确 -
引用一旦绑定就不能改绑其他变量:
int a = 1, b = 2; int& ref = a; ref = b; // 不是改绑 b!而是把 b 的值赋给 a,ref 仍是 a 的别名 cout << a; // 输出 2
🔺引用 vs 指针
| 对比项 | 引用 int& r = a | 指针 int* p = &a |
|---|---|---|
| 初始化 | 声明时必须初始化 | 可先声明后赋值 |
| 能否为空 | 不能为 null | 可以为 nullptr |
| 能否重新指向 | 不能,永远绑定初始对象 | 可以随时改指向 |
| 访问方式 | 直接用(透明) | 需要解引用 *p |
sizeof | 和原变量相同 | 指针本身大小(4 或 8字节) |
🔺能用引用就用引用,只有需要”可以为空”或”可以重新指向”时才用指针。
🔺将引用用作函数参数
引用参数的两大用途:修改实参 和 避免大对象拷贝。
修改实参:
void swap(int& a, int& b) {
int tmp = a; a = b; b = tmp;
}
int x = 1, y = 2;
swap(x, y);
cout << x << " " << y; // 2 1避免拷贝(const 引用):
// 每次调用复制整个 string(开销大)
void print(string s);
// 零拷贝,只读(推荐)
void print(const string& s);返回引用(让调用方直接操作内部数据):
int arr[5] = {1, 2, 3, 4, 5};
int& getElem(int* a, int i) { return a[i]; }
getElem(arr, 2) = 99; // 直接修改 arr[2]🔺绝对不能返回局部变量的引用:函数返回后局部变量销毁,引用悬空:
int& bad() {
int local = 10;
return local; // 危险!悬空引用
}🔺临时变量与 const 引用
这是引用最容易踩坑的地方。
什么时候产生临时变量: 当 const 引用 参数收到类型不完全匹配的值时,编译器自动创建临时变量承接转换结果:
void show(const double& d) { cout << d; }
int x = 5;
show(x); // x 是 int,参数是 const double&
// 编译器创建临时 double tmp = 5.0,d 绑定 tmp非 const 引用不能绑定临时变量:
void addOne(double& d) { d++; }
int x = 5;
addOne(x); // 错误!修改临时变量对 x 毫无意义,编译器拒绝🔺只有 const 引用 才能绑定临时变量或字面量:
const int& r = 42; // OK,临时变量生命周期随引用延长
int& r2 = 42; // 错误!这也意味着 const string& 能接受字符串字面量,而 string& 不行:
void show(const string& s); // show("hello") ✅
void modify(string& s); // modify("hello") ❌🔺将引用用于结构体
结构体是引用最典型的应用场景,避免整个结构体的拷贝:
struct Point { double x, y; };
void printPoint(const Point& p) { // 只读,零拷贝
cout << "(" << p.x << ", " << p.y << ")\n";
}
void move(Point& p, double dx, double dy) { // 修改
p.x += dx;
p.y += dy;
}🔺引用结构体成员用 .,指针结构体成员用 ->:
Point& ref = p; ref.x = 3.0; // 引用
Point* ptr = &p; ptr->x = 3.0; // 指针🔺将引用用于类对象
选哪种参数方式,看需不需要修改:
string combine(const string& s1, const string& s2) {
return s1 + " " + s2; // 只读,const 引用
}
void append(string& s, const string& suffix) {
s += suffix; // 需要修改 s,非 const 引用
}🔺何时用引用 vs 指针
| 场景 | 推荐 | 原因 |
|---|---|---|
| 函数修改单个实参 | T& | 语法简洁 |
| 函数只读大对象 | const T& | 零拷贝,防意外修改 |
| 参数可能不存在(optional) | T* | 引用不能为 null |
| 需要动态重绑或动态内存 | T* | 引用不能重新绑定 |
| 运算符重载、返回可赋值左值 | 引用 | a[i] = val 这类语法需要引用 |
🔺经验口诀:能用 const T& 就不用值传递;能用引用就不用指针;需要”可以为空”才用指针。
🔺函数重载
C++ 允许定义同名但参数不同的多个函数,编译器根据调用时的参数类型自动选择:
void print(int n) { cout << "int: " << n; }
void print(double d) { cout << "double: " << d; }
void print(string s) { cout << "string: " << s; }
print(42); // 调用 print(int)
print(3.14); // 调用 print(double)
print("hello"); // 调用 print(string)🔺重载的区分依据是参数列表(类型、个数、顺序),返回类型不同不构成重载:
int func(int x); // OK
void func(int x); // 错误!仅返回类型不同,不是重载,是重定义🔺函数签名:函数名 + 参数列表(不含返回类型)。重载要求签名不同。
🔺编译器选择重载函数的过程称为重载解析,优先选择完全匹配,其次是类型提升匹配,再次是标准转换匹配。若有歧义则报错:
void func(int x) { ... }
void func(double x) { ... }
func(3.14f); // float → 可转为 int 也可转为 double,有歧义,编译报错🔺函数模板
函数模板让你写一份代码,编译器自动为不同类型生成对应的函数,解决的是”逻辑相同、类型不同要重复写”的问题。
// 普通写法:每种类型都要写一遍
void swap(int& a, int& b) { int tmp = a; a = b; b = tmp; }
void swap(double& a, double& b) { double tmp = a; a = b; b = tmp; }
// 模板写法:写一次,编译器自动生成所有版本
template <typename T>
void swap(T& a, T& b) {
T tmp = a;
a = b;
b = tmp;
}🔺template <typename T> 是模板头,T 是类型参数,名字随意(T 只是约定俗成)。也可以写 class T,与 typename T 完全等价。
调用时编译器自动推导类型:
int a = 1, b = 2;
double x = 1.1, y = 2.2;
swap(a, b); // 编译器推导 T = int,生成 swap<int>
swap(x, y); // 编译器推导 T = double,生成 swap<double>
swap<int>(a, b); // 也可以显式指定类型🔺模板本身不是函数,是”生成函数的配方”。只有被调用时编译器才真正生成对应的函数,这个过程叫实例化。
🔺重载的模板
模板也可以重载,用于处理某些类型需要特殊逻辑的情况:
template <typename T>
void show(T val) {
cout << val << "\n";
}
// 重载:专门处理数组(普通模板无法知道数组长度)
template <typename T>
void show(T arr[], int n) {
for (int i = 0; i < n; i++)
cout << arr[i] << " ";
cout << "\n";
}
int x = 42;
int arr[] = {1, 2, 3};
show(x); // 调用第一个模板,T = int
show(arr, 3); // 调用第二个模板,T = int🔺重载解析顺序:精确匹配的普通函数 > 精确匹配的模板 > 需要类型转换的普通函数。
🔺模板的局限性与显式具体化
局限性: 模板里的操作必须对 T 类型有效。比如 T tmp = a 要求 T 可赋值,a > b 要求 T 支持 >。某些类型(如结构体)不满足这些条件:
template <typename T>
T myMax(T a, T b) {
return (a > b) ? a : b; // 若 T 是结构体,> 未定义,编译报错
}显式具体化(Explicit Specialization): 针对某个特定类型,提供一个专门版本覆盖模板:
struct Job { string name; double salary; };
// 通用模板
template <typename T>
void swap(T& a, T& b) {
T tmp = a; a = b; b = tmp;
}
// 显式具体化:只交换 Job 的 salary,不交换 name
template <>
void swap<Job>(Job& a, Job& b) {
double tmp = a.salary;
a.salary = b.salary;
b.salary = tmp;
}🔺语法是 template <> + 函数名后加 <具体类型>。调用时编译器优先使用具体化版本。
🔺实例化与具体化
这两个概念容易混淆:
| 概念 | 含义 | 关键字 |
|---|---|---|
| 隐式实例化 | 调用模板函数时,编译器自动生成对应类型的函数 | 无(自动) |
| 显式实例化 | 主动要求编译器生成某个类型的版本(提前编译) | template |
| 显式具体化 | 为某个类型提供完全不同的实现,覆盖模板 | template <> |
template <typename T>
T add(T a, T b) { return a + b; }
// 隐式实例化:调用时自动生成
add(1, 2); // 编译器生成 add<int>
add(1.0, 2.0); // 编译器生成 add<double>
// 显式实例化:主动生成,不需要调用(常用于分离编译),减少编译的时间
template int add<int>(int, int);
// 显式具体化:为 string 提供拼接而非相加的特殊逻辑
template <>
string add<string>(string a, string b) {
return a + " " + b; // 加空格
}🔺实际开发中显式实例化主要用于跨编译单元共享模板(.cpp 文件里实例化,其他文件直接用),普通使用时不需要关心。