C++ 高频易错点
第一部分:基础语法易错点(入门必避)
一、基础语法核心易错
- = 赋值 vs == 判断 :条件里写
if(a=3)不会报错,但永远为真,正确写法是if(a==3)。 - switch 语句缺少 break:不加 break 会穿透执行所有后续 case,导致逻辑错乱。
- else 匹配规则 :else 默认匹配最近的 if,缩进不影响语法,多层分支必须加
{}明确作用域。 - void 函数返回值错误 :void 类型函数不能有返回值,写
return 0;直接编译报错。 - 字符与字符串混淆 :
'a'是 char 类型(单个字符),"a"是 const char* 类型(字符串常量),混用会导致编译报错。
二、数组与指针基础易错
- 数组越界未定义行为 :
a[10]只能访问下标 0~9,越界不会编译报错,但会导致程序崩溃、数据错乱(未定义行为)。 - 数组名与指针的区别 :数组名是地址常量,不能自增(如
a++报错),仅在函数传参时会退化为指针。 - 野指针风险:未初始化、已释放、越界的指针均为野指针,解引用野指针会直接导致程序崩溃。
- NULL 与 nullptr 混用:C++11 推荐使用 nullptr,NULL 本质是 0,容易引发函数重载歧义。
- 指针与引用的核心区别:引用必须初始化、不能为空、不能更换指向;指针可以为空、可以修改指向。
三、函数基础易错
- 形参修改不影响实参:普通形参是值拷贝,想通过函数修改实参,必须传指针或引用。
- 默认参数写法错误 :默认参数必须从右往左连续设置,错误写法
void f(int a=10, int b);,正确写法void f(int b, int a=10);。 - 函数声明与定义不匹配 :声明
int f();与定义void f(){}会编译报错,返回值、参数列表必须完全一致。 - 递归缺少终止条件:递归无终止条件会导致栈溢出,程序崩溃。
- 内联函数滥用:内联函数不能包含循环、递归,否则编译器会自动忽略内联声明,失去内联意义。
四、内存管理基础易错
- 内存释放不配对:new 配 delete、new[] 配 delete[]、malloc 配 free,混用会导致内存泄漏或程序崩溃。
- delete 后未置空指针 :
delete p;后未写p = nullptr;,指针会变成野指针,后续误用风险极高。 - 重复 delete 同一指针:对同一个指针多次 delete,会直接导致程序崩溃。
- malloc 与构造函数:malloc 仅分配内存,不会调用类的构造函数;C++ 推荐使用 new(分配+构造)。
- 常见内存泄漏场景:new/malloc 后未 delete/free、局部对象指针未释放、函数返回局部变量的指针/引用。
第二部分:进阶易错点(面向对象+进阶语法)
一、引用 & 指针进阶易错
- 普通引用不能绑定临时值 :错误写法
int &a = 10;,正确写法const int &a = 10;(const 引用可绑定临时对象)。 - 返回局部变量的引用/指针:局部变量在函数结束后销毁,返回其引用/指针会得到野引用/野指针,属于未定义行为。
- 指针数组与数组指针混淆 :
int *p[5];是指针数组(存储5个int指针),int (*p)[5];是数组指针(指向含5个int的数组)。 - 二级指针传参误区:想要修改指针本身的值(如改变指针指向),必须传二级指针或指针引用。
- void 指针误用*:void* 可接收任意类型指针,但不能直接解引用,必须强转为具体类型指针才能取值。
二、隐式类型转换易错
- 单参数构造函数的隐式转换 :未加 explicit 的单参数构造函数会触发隐式转换(如
A a = 10;,A 类有A(int a);),加explicit可禁止。 - 无符号数减法溢出 :
unsigned int a=5, b=10;,if(a - b > 0)永远为真(无符号数溢出会变成极大值)。 - 浮点数强转整数 :浮点数强转为整数时直接截断小数部分,不是四舍五入(如
(int)3.9结果为3)。 - 整型提升异常:char、short 类型参与运算时,会先提升为 int 类型,容易导致溢出或判断错误。
三、类与对象进阶易错
- 构造函数相关 :
- 构造函数不能是虚函数;析构函数在多态场景下必须是虚函数(否则子类析构不执行,导致内存泄漏)。
- 拷贝构造函数必须传引用,传值会导致无限递归。
- 一旦自定义任意一个构造函数,编译器不再生成默认构造函数。
- 构造函数中调用虚函数,不会触发多态(子类未初始化,仅调用当前类版本)。
- 拷贝与赋值重载 :
- 浅拷贝隐患:多个对象共用同一块内存,delete 时会重复释放,导致崩溃,需自定义深拷贝。
- 拷贝构造、赋值重载需判断自赋值(
if(this == &other) return *this;),避免自身赋值崩溃。
- 初始化列表易错 :
- 初始化列表的初始化顺序,按类中成员的声明顺序,而非列表书写顺序。
- const 成员、引用成员,必须在初始化列表中赋值,构造函数体内赋值会编译报错。
- 初始化列表比构造体内赋值效率高(避免二次赋值)。
- this 指针与 const 成员 :
- this 指针是常量指针,不能修改其指向。
- const 成员函数不能修改普通成员变量,想修改需用 mutable 修饰成员。
- const 对象只能调用 const 成员函数,不能调用普通成员函数。
- 静态成员易错 :
- 静态成员变量属于类,不属于对象,必须在类外初始化(不能在构造函数或初始化列表中初始化)。
- 静态成员函数没有 this 指针,不能访问非静态成员变量和非静态成员函数。
- 静态局部变量只初始化一次,生命周期贯穿整个程序,多线程下易引发线程安全问题。
- 其他:友元函数破坏类的封装性,不能滥用;重载、重写、隐藏易混淆(重载:同一作用域、参数不同;重写:虚函数、子类重写;隐藏:子类同名函数隐藏父类)。
四、继承与多态深度易错
- 继承访问权限 :
- public 继承:父类 public→子类 public,protected→子类 protected。
- protected 继承:父类 public/protected 均变为子类 protected。
- private 继承:父类所有成员均变为子类 private。
- 名字隐藏 vs 重写:子类与父类函数名相同、参数不同,属于名字隐藏(非重载、非多态);只有虚函数、参数+返回值+const 完全匹配,才是重写。
- 虚函数重写规则:重写必须满足"参数列表、返回值、const 修饰"完全一致,差一个细节就变成名字隐藏。
- override / final 关键字:override 强制检查是否真的重写虚函数(不匹配则编译报错);final 禁止子类重写该虚函数,或禁止类被继承。
- 菱形继承(钻石继承):多继承时存在相同父类副本,会导致数据冗余和二义性,必须用 virtual public 虚继承解决。
- 抽象类与多态 :含纯虚函数(
virtual void f()=0;)的类是抽象类,不能实例化,但可以定义抽象类指针/引用,指向子类对象实现多态。 - 对象切片问题:父类对象接收子类对象时,会"切掉"子类独有成员和虚函数表,丢失多态特性;实现多态必须用父类指针/引用。
- 析构函数与多态:析构函数中调用虚函数,不会触发多态;多态场景下,父类析构必须是虚函数,否则子类析构不执行。
五、const 进阶易错
- const 修饰指针的三种写法(易混淆) :
const int *p;:不能修改指针指向的值,可修改指针指向。int const *p;:与上一行完全等价。int *const p;:不能修改指针指向,可修改指针指向的值。
- const 与引用结合:const 引用可绑定临时值、常量,普通引用不能;const 引用不能修改绑定的值。
- const 成员函数:const 成员函数内不能修改普通成员变量,若需修改,成员变量需用 mutable 修饰。
六、模板 & 泛型易错
- 模板实现位置:模板类、模板函数的实现必须放在头文件中(或头文件包含实现文件),否则编译链接报错(模板实例化需要完整定义)。
- 模板参数推导:模板参数类型推导不会自动做隐式类型转换,类型必须严格匹配。
- 模板静态成员 :类模板的静态成员,每个实例化版本(如
Template<double>)独享一份,互不干扰。
七、内存管理进阶易错
- placement new(定位 new) :只在已分配的内存上构造对象,不分配新内存;使用后需手动调用析构函数(
p->~T();),不能直接 delete。 - new[] / delete[] 严格配对:自定义类对象数组,new[] 配 delete[] 必须严格对应,否则会导致析构函数未被全部调用,引发内存泄漏或崩溃;内置类型偶尔"侥幸"不出错,但不推荐。
- 内存越界潜伏性:内存越界不一定当场崩溃,属于未定义行为,可能潜伏很久,在随机场景下触发崩溃、数据错乱。
- 智能指针循环引用:shared_ptr 互相引用(如 A 含 B 的 shared_ptr,B 含 A 的 shared_ptr),会导致引用计数无法归 0,引发内存泄漏;解决方案是将其中一方改为 weak_ptr。
第三部分:STL 易错点(高频面试+开发)
- vector 相关 :
- 迭代器失效:扩容、insert、erase 中间位置,会导致所有迭代器失效;erase 后需用返回值更新迭代器(正确:
it = vec.erase(it);,错误:vec.erase(it++);)。 - resize 与 reserve 混淆:reserve 只预分配容量(不创建元素、不改变 size),resize 创建元素(改变 size);reserve 后不能直接用 [] 访问(无有效元素)。
-
\] 与 at() 区别:\[\] 不做越界检查,越界崩溃;at() 会做越界检查,抛异常。
- 迭代器失效:扩容、insert、erase 中间位置,会导致所有迭代器失效;erase 后需用返回值更新迭代器(正确:
- list 相关 :
- 不支持随机访问:不能用 [] 下标访问,不能用
it+5这种写法,只能用 ++/-- 遍历。 - 迭代器失效:erase 仅当前迭代器失效,其他迭代器不受影响。
- 排序:不能用全局 sort(需要随机访问迭代器),需用 list 成员函数
sort()。
- 不支持随机访问:不能用 [] 下标访问,不能用
- set / multiset 相关 :
- 元素不可修改:set/multiset 底层是红黑树,元素有序,修改元素会破坏红黑树结构,需删除后重新插入。
- 排序:插入后自动升序,与插入顺序无关;不能用 std::sort 重新排序。
- 区别:set 元素唯一,multiset 允许重复元素,可用 count() 统计重复个数。
- map / multimap 相关 :
-
\] 运算符陷阱:map 的 \[\] 若 key 不存在,会自动插入该 key 并赋默认值,查找时推荐用 find()。
- 区别:map key 唯一,支持 [];multimap key 可重复,不支持 [],只能用 find() 或遍历取值。
- 迭代器失效:erase 仅被删除的迭代器失效,其他迭代器有效;插入不会导致迭代器失效。
-
- unordered_set / unordered_map 相关 :
- 自定义 key 要求:自定义结构体做 key,必须同时提供哈希函数和 == 运算符(缺一不可)。
- 迭代器失效:负载因子过高触发 rehash 时,所有迭代器全部失效。
- 特性:无序、增删查平均 O(1),适合纯查找场景;不支持有序遍历。
- 容器适配器(stack/queue/priority_queue) :
- stack/queue:无迭代器,不支持遍历,只能访问栈顶/队首/队尾。
- stack 底层:默认用 deque,也可用 vector,但 vector 扩容时会整体拷贝,性能不如 deque。
- queue 底层:不选用 vector(头删效率 O(n)),默认用 deque(头删 O(1))。
- priority_queue:底层是 vector 实现的二叉堆,默认大顶堆;小顶堆需手动指定比较器<int,>`),比较器容易写反。
- STL 算法易错 :
- unique 去重:必须先排序,否则只是逻辑去重(相邻重复元素合并),不会真正删除重复元素。
- sort 排序:默认升序,自定义排序需传仿函数或 lambda;stack/queue 无迭代器,不能直接用 sort。
第四部分:C++11/14 新特性易错点
- auto 推导陷阱 :auto 推导不会保留引用和 const 修饰,若需保留,需手动写
const auto&。 - 范围 for 循环 :遍历容器并修改元素时,必须用引用(
for(auto &x : vec)),不用引用是值拷贝,改不到原元素。 - lambda 表达式捕获 :
- 引用捕获:捕获局部变量时,需注意变量生命周期,离开作用域后引用会悬空。
- 值捕获:捕获的是变量的副本,默认不能修改,若需修改需加 mutable。
- 右值引用(&&)误用:普通函数形参不要随便用右值引用,主要用于移动构造、移动赋值,避免绑定错乱。
- emplace_back 与 push_back:emplace_back 直接在容器内构造对象,省去拷贝/移动开销;push_back 先构造临时对象,再拷贝/移动到容器,新手容易无脑混用。
第五部分:编译 & 隐性语法坑
- 头文件重复包含 :未用
#ifndef/#define/#endif或#pragma once,会导致重复定义报错。 - 宏定义加分号 :如
#define MAX 100;,使用时if(a > MAX)会因多一个分号报错。 - 运算符优先级陷阱:&(位与)低于 ==、|(位或)低于 &&(逻辑与),位运算时不加括号极易导致逻辑错误。
- 默认参数重复定义:函数默认参数只能在声明处设置,定义处不能重复给默认值。
- 命名空间污染 :全局滥用
using namespace std;容易引发名字冲突,大型项目禁止全局使用,可局部使用(如函数内)。 - 未定义行为(UB):数组越界、空指针解引用、重复释放、整数溢出等,程序可能崩溃、乱码或无规律运行,调试难度极大。
第六部分:字符串易错点
- string 与 C 字符串区别:C++ string 末尾没有 '\0',不要用 C 语言的 strlen、strcpy 等函数强行操作 string。
- c_str() 陷阱:c_str() 返回的是临时指针,其生命周期与 string 对象一致,不能长期保存(如 string 销毁后,指针变成野指针)。
- string::npos :是无符号整数(值为 -1 转换后的极大值),用
if(str.find("a") == -1)会判断错误,正确写法if(str.find("a") == string::npos)。
补充:高频面试易错补充
- 浮点数比较:不能用 == 直接比较,需用误差范围判断(如
fabs(a< 1e-6)。 - 多线程易错:共享变量未加互斥锁,会导致数据竞争,程序运行异常。
- 虚函数底层:虚函数表存储虚函数地址,对象包含虚指针指向虚函数表;构造函数中虚指针未初始化,故不触发多态。