C++ 基础语法 — 专题笔记

← C++ 知识地图


目录


🔴第 1 章 预备知识

C已经学了

🔴第 2 章 开始学习 C++

🔺用 int main() 不用 void main()void main 逻辑上没问题,但不是标准强制要求,部分系统不支持

🔺使用 C++ 输入输出必须写 #include <iostream>,io = input/output

🔺C++ 头文件无扩展名;C 头文件转 C++ 版本时去掉 .h 并加前缀 c,如 math.hcmath

🔺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{};      // 初始化为 0

C++11 推荐用 {} 大括号初始化,好处是防止窄化转换:把 double 赋给 int 会直接报错,而不是悄悄截断。


🔺整型:short / int / long / long long

保证:shortintlonglong longshort 至少 16 位,long 至少 32 位。

类型最小宽度典型宽度(64 位)
short16 位16 位
int16 位32 位
long32 位32/64 位
long long64 位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 long

cout 输出进制(设置后持续生效直到显式切换):

cout << hex << 255;   // ff
cout << oct << 255;   // 377
cout << dec << 255;   // 255(切回默认十进制)

🔺char 类型与宽字符

char 本质是整型,存储字符对应的编码值(通常 ASCII,0~127)。char 是否有符号由实现决定,需要时显式写 signed charunsigned char

char ch = 'A';       // 存储 65
cout << ch;          // 输出字符 A
cout << (int)ch;     // 输出 65

cout.put(ch):专门输出单个字符,无论参数是什么整型,都按字符显示:

cout.put('A');   // 输出 A
cout.put(65);    // 也输出 A
// 对比:cout << 65 输出的是数字 65

一般的字节是 8 位,但国际编程可能用到更大的字符集(如 Unicode),因此有些实现使用 16 位甚至 32 位。C++ 提供了三种宽字符类型:

类型宽度字面值前缀
wchar_t实现定义(通常 16/32 位)L
char16_t16 位(C++11)u
char32_t32 位(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;
  • 任何非零整数 → true0false
  • true1false0(参与算术时自动转换)
  • true / false 是 C++ 关键字(C 语言需要 <stdbool.h> 才有)

🔺const 限定符

const int MONTHS = 12;   // 必须在声明时初始化,之后不可修改

#define 好:有类型检查、有作用域、调试器能看到名字。


🔺浮点数:float / double / long double

类型有效十进制位数典型范围
float6~7 位±3.4×10³⁸
double15~16 位±1.8×10³⁰⁸
long double18~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_MAXDBL_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++;               // 移到下一个字符
}

指针与字符串函数: strlenstrcpy 等函数的参数本质上都是 char*,传入数组名和传入指针效果相同:

char arr[] = "Hello";
char* p = arr;
 
strlen(arr);  // 5
strlen(p);    // 5,完全等价

🔺cout << pchar* 类型)输出的是整个字符串而不是地址;若想输出地址需强转为 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)

vectorarray 是 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

关系运算符返回 booltruefalse),优先级低于算术运算符,但高于赋值运算符

🔺经典错误:= 写成 ==(反过来也是):

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);  // 输入不合法就重来

🔺适合”先做一次,再验证”的场景,如菜单交互、输入校验。大多数情况下 forwhile 更常用。


🔺基于范围的 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

两者都用于控制循环流程,适用于 forwhiledo-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 会读取并丢弃行末 \nline 中不含换行符。


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 / macOSCtrl+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 / putcharcin.get(ch) / cout
来源C 标准库 <cstdio>C++ iostream
返回类型int(需手动判断 EOF)cin 对象(bool 友好)
速度通常更快(无对象开销)稍慢(但差异微小)
风格C 风格C++ 风格(推荐)

🔺C++ 程序推荐用 cin.get(ch)coutgetchar / putchar 在底层性能敏感或与 C 代码混用时才用。

🔴第 6 章 分支语句和逻辑运算符


🔺if 和 if-else 语句

if (条件) {
    语句;
}
 
if (条件) {
    语句A;
} else {
    语句B;
}

🔺只有一条语句时可以省略花括号,但推荐始终加花括号,防止后续添加语句时出错。

🔺悬空 elseelse 与最近的 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)是字母(az 或 AZ)
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 只能用整型**(intcharenum),不能用浮点数或字符串

🔺没有 break 会贯穿(fall-through):执行完一个 case 后继续执行下一个 case,直到遇到 breakswitch 结尾:

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 >> ncin >> n 行为一致,包括 EOF 判断:

int val, sum = 0;
while (inFile >> val) {  // 读到文件末尾自动退出
    sum += val;
}

缓冲区问题:

类似C语言的缓冲区问题,c语言读写切换要fseek重新定位文件指针

C++ 不再使用一个通用的 fseek,而是根据“读”和“写”分成了两个不同的指针函数。你只需要记住 GP

  • 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

🔺引用传递的两个主要用途:

  1. 修改调用方的变量(如 swap)
  2. 避免大对象拷贝(传 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}

autotypedef 简化函数指针类型:

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;   // & 紧跟类型,声明引用,必须同时初始化

两条铁律:

  1. 引用必须在声明时初始化

    int& ref;        // 错误!
    int& ref = a;    // 正确
  2. 引用一旦绑定就不能改绑其他变量

    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 文件里实例化,其他文件直接用),普通使用时不需要关心。