C++ 高频易错点

C++ 高频易错点

第一部分:基础语法易错点(入门必避)

一、基础语法核心易错

  1. = 赋值 vs == 判断 :条件里写 if(a=3) 不会报错,但永远为真,正确写法是 if(a==3)
  2. switch 语句缺少 break:不加 break 会穿透执行所有后续 case,导致逻辑错乱。
  3. else 匹配规则 :else 默认匹配最近的 if,缩进不影响语法,多层分支必须加 {} 明确作用域。
  4. void 函数返回值错误 :void 类型函数不能有返回值,写 return 0; 直接编译报错。
  5. 字符与字符串混淆'a' 是 char 类型(单个字符),"a" 是 const char* 类型(字符串常量),混用会导致编译报错。

二、数组与指针基础易错

  1. 数组越界未定义行为a[10] 只能访问下标 0~9,越界不会编译报错,但会导致程序崩溃、数据错乱(未定义行为)。
  2. 数组名与指针的区别 :数组名是地址常量,不能自增(如 a++ 报错),仅在函数传参时会退化为指针。
  3. 野指针风险:未初始化、已释放、越界的指针均为野指针,解引用野指针会直接导致程序崩溃。
  4. NULL 与 nullptr 混用:C++11 推荐使用 nullptr,NULL 本质是 0,容易引发函数重载歧义。
  5. 指针与引用的核心区别:引用必须初始化、不能为空、不能更换指向;指针可以为空、可以修改指向。

三、函数基础易错

  1. 形参修改不影响实参:普通形参是值拷贝,想通过函数修改实参,必须传指针或引用。
  2. 默认参数写法错误 :默认参数必须从右往左连续设置,错误写法 void f(int a=10, int b);,正确写法 void f(int b, int a=10);
  3. 函数声明与定义不匹配 :声明 int f(); 与定义 void f(){} 会编译报错,返回值、参数列表必须完全一致。
  4. 递归缺少终止条件:递归无终止条件会导致栈溢出,程序崩溃。
  5. 内联函数滥用:内联函数不能包含循环、递归,否则编译器会自动忽略内联声明,失去内联意义。

四、内存管理基础易错

  1. 内存释放不配对:new 配 delete、new[] 配 delete[]、malloc 配 free,混用会导致内存泄漏或程序崩溃。
  2. delete 后未置空指针delete p; 后未写 p = nullptr;,指针会变成野指针,后续误用风险极高。
  3. 重复 delete 同一指针:对同一个指针多次 delete,会直接导致程序崩溃。
  4. malloc 与构造函数:malloc 仅分配内存,不会调用类的构造函数;C++ 推荐使用 new(分配+构造)。
  5. 常见内存泄漏场景:new/malloc 后未 delete/free、局部对象指针未释放、函数返回局部变量的指针/引用。

第二部分:进阶易错点(面向对象+进阶语法)

一、引用 & 指针进阶易错

  1. 普通引用不能绑定临时值 :错误写法 int &a = 10;,正确写法 const int &a = 10;(const 引用可绑定临时对象)。
  2. 返回局部变量的引用/指针:局部变量在函数结束后销毁,返回其引用/指针会得到野引用/野指针,属于未定义行为。
  3. 指针数组与数组指针混淆int *p[5]; 是指针数组(存储5个int指针),int (*p)[5]; 是数组指针(指向含5个int的数组)。
  4. 二级指针传参误区:想要修改指针本身的值(如改变指针指向),必须传二级指针或指针引用。
  5. void 指针误用*:void* 可接收任意类型指针,但不能直接解引用,必须强转为具体类型指针才能取值。

二、隐式类型转换易错

  1. 单参数构造函数的隐式转换 :未加 explicit 的单参数构造函数会触发隐式转换(如 A a = 10;,A 类有 A(int a);),加 explicit 可禁止。
  2. 无符号数减法溢出unsigned int a=5, b=10;if(a - b > 0) 永远为真(无符号数溢出会变成极大值)。
  3. 浮点数强转整数 :浮点数强转为整数时直接截断小数部分,不是四舍五入(如 (int)3.9 结果为3)。
  4. 整型提升异常:char、short 类型参与运算时,会先提升为 int 类型,容易导致溢出或判断错误。

三、类与对象进阶易错

  1. 构造函数相关
    • 构造函数不能是虚函数;析构函数在多态场景下必须是虚函数(否则子类析构不执行,导致内存泄漏)。
    • 拷贝构造函数必须传引用,传值会导致无限递归。
    • 一旦自定义任意一个构造函数,编译器不再生成默认构造函数。
    • 构造函数中调用虚函数,不会触发多态(子类未初始化,仅调用当前类版本)。
  2. 拷贝与赋值重载
    • 浅拷贝隐患:多个对象共用同一块内存,delete 时会重复释放,导致崩溃,需自定义深拷贝。
    • 拷贝构造、赋值重载需判断自赋值(if(this == &other) return *this;),避免自身赋值崩溃。
  3. 初始化列表易错
    • 初始化列表的初始化顺序,按类中成员的声明顺序,而非列表书写顺序。
    • const 成员、引用成员,必须在初始化列表中赋值,构造函数体内赋值会编译报错。
    • 初始化列表比构造体内赋值效率高(避免二次赋值)。
  4. this 指针与 const 成员
    • this 指针是常量指针,不能修改其指向。
    • const 成员函数不能修改普通成员变量,想修改需用 mutable 修饰成员。
    • const 对象只能调用 const 成员函数,不能调用普通成员函数。
  5. 静态成员易错
    • 静态成员变量属于类,不属于对象,必须在类外初始化(不能在构造函数或初始化列表中初始化)。
    • 静态成员函数没有 this 指针,不能访问非静态成员变量和非静态成员函数。
    • 静态局部变量只初始化一次,生命周期贯穿整个程序,多线程下易引发线程安全问题。
  6. 其他:友元函数破坏类的封装性,不能滥用;重载、重写、隐藏易混淆(重载:同一作用域、参数不同;重写:虚函数、子类重写;隐藏:子类同名函数隐藏父类)。

四、继承与多态深度易错

  1. 继承访问权限
    • public 继承:父类 public→子类 public,protected→子类 protected。
    • protected 继承:父类 public/protected 均变为子类 protected。
    • private 继承:父类所有成员均变为子类 private。
  2. 名字隐藏 vs 重写:子类与父类函数名相同、参数不同,属于名字隐藏(非重载、非多态);只有虚函数、参数+返回值+const 完全匹配,才是重写。
  3. 虚函数重写规则:重写必须满足"参数列表、返回值、const 修饰"完全一致,差一个细节就变成名字隐藏。
  4. override / final 关键字:override 强制检查是否真的重写虚函数(不匹配则编译报错);final 禁止子类重写该虚函数,或禁止类被继承。
  5. 菱形继承(钻石继承):多继承时存在相同父类副本,会导致数据冗余和二义性,必须用 virtual public 虚继承解决。
  6. 抽象类与多态 :含纯虚函数(virtual void f()=0;)的类是抽象类,不能实例化,但可以定义抽象类指针/引用,指向子类对象实现多态。
  7. 对象切片问题:父类对象接收子类对象时,会"切掉"子类独有成员和虚函数表,丢失多态特性;实现多态必须用父类指针/引用。
  8. 析构函数与多态:析构函数中调用虚函数,不会触发多态;多态场景下,父类析构必须是虚函数,否则子类析构不执行。

五、const 进阶易错

  1. const 修饰指针的三种写法(易混淆)
    • const int *p;:不能修改指针指向的值,可修改指针指向。
    • int const *p;:与上一行完全等价。
    • int *const p;:不能修改指针指向,可修改指针指向的值。
  2. const 与引用结合:const 引用可绑定临时值、常量,普通引用不能;const 引用不能修改绑定的值。
  3. const 成员函数:const 成员函数内不能修改普通成员变量,若需修改,成员变量需用 mutable 修饰。

六、模板 & 泛型易错

  1. 模板实现位置:模板类、模板函数的实现必须放在头文件中(或头文件包含实现文件),否则编译链接报错(模板实例化需要完整定义)。
  2. 模板参数推导:模板参数类型推导不会自动做隐式类型转换,类型必须严格匹配。
  3. 模板静态成员 :类模板的静态成员,每个实例化版本(如 Template<double>)独享一份,互不干扰。

七、内存管理进阶易错

  1. placement new(定位 new) :只在已分配的内存上构造对象,不分配新内存;使用后需手动调用析构函数(p->~T();),不能直接 delete。
  2. new[] / delete[] 严格配对:自定义类对象数组,new[] 配 delete[] 必须严格对应,否则会导致析构函数未被全部调用,引发内存泄漏或崩溃;内置类型偶尔"侥幸"不出错,但不推荐。
  3. 内存越界潜伏性:内存越界不一定当场崩溃,属于未定义行为,可能潜伏很久,在随机场景下触发崩溃、数据错乱。
  4. 智能指针循环引用:shared_ptr 互相引用(如 A 含 B 的 shared_ptr,B 含 A 的 shared_ptr),会导致引用计数无法归 0,引发内存泄漏;解决方案是将其中一方改为 weak_ptr。

第三部分:STL 易错点(高频面试+开发)

  1. vector 相关
    • 迭代器失效:扩容、insert、erase 中间位置,会导致所有迭代器失效;erase 后需用返回值更新迭代器(正确:it = vec.erase(it);,错误:vec.erase(it++);)。
    • resize 与 reserve 混淆:reserve 只预分配容量(不创建元素、不改变 size),resize 创建元素(改变 size);reserve 后不能直接用 [] 访问(无有效元素)。
    • \] 与 at() 区别:\[\] 不做越界检查,越界崩溃;at() 会做越界检查,抛异常。

  2. list 相关
    • 不支持随机访问:不能用 [] 下标访问,不能用 it+5 这种写法,只能用 ++/-- 遍历。
    • 迭代器失效:erase 仅当前迭代器失效,其他迭代器不受影响。
    • 排序:不能用全局 sort(需要随机访问迭代器),需用 list 成员函数 sort()
  3. set / multiset 相关
    • 元素不可修改:set/multiset 底层是红黑树,元素有序,修改元素会破坏红黑树结构,需删除后重新插入。
    • 排序:插入后自动升序,与插入顺序无关;不能用 std::sort 重新排序。
    • 区别:set 元素唯一,multiset 允许重复元素,可用 count() 统计重复个数。
  4. map / multimap 相关
    • \] 运算符陷阱:map 的 \[\] 若 key 不存在,会自动插入该 key 并赋默认值,查找时推荐用 find()。

    • 区别:map key 唯一,支持 [];multimap key 可重复,不支持 [],只能用 find() 或遍历取值。
    • 迭代器失效:erase 仅被删除的迭代器失效,其他迭代器有效;插入不会导致迭代器失效。
  5. unordered_set / unordered_map 相关
    • 自定义 key 要求:自定义结构体做 key,必须同时提供哈希函数和 == 运算符(缺一不可)。
    • 迭代器失效:负载因子过高触发 rehash 时,所有迭代器全部失效。
    • 特性:无序、增删查平均 O(1),适合纯查找场景;不支持有序遍历。
  6. 容器适配器(stack/queue/priority_queue)
    • stack/queue:无迭代器,不支持遍历,只能访问栈顶/队首/队尾。
    • stack 底层:默认用 deque,也可用 vector,但 vector 扩容时会整体拷贝,性能不如 deque。
    • queue 底层:不选用 vector(头删效率 O(n)),默认用 deque(头删 O(1))。
    • priority_queue:底层是 vector 实现的二叉堆,默认大顶堆;小顶堆需手动指定比较器<int,>`),比较器容易写反。
  7. STL 算法易错
    • unique 去重:必须先排序,否则只是逻辑去重(相邻重复元素合并),不会真正删除重复元素。
    • sort 排序:默认升序,自定义排序需传仿函数或 lambda;stack/queue 无迭代器,不能直接用 sort。

第四部分:C++11/14 新特性易错点

  1. auto 推导陷阱 :auto 推导不会保留引用和 const 修饰,若需保留,需手动写 const auto&
  2. 范围 for 循环 :遍历容器并修改元素时,必须用引用(for(auto &x : vec)),不用引用是值拷贝,改不到原元素。
  3. lambda 表达式捕获
    • 引用捕获:捕获局部变量时,需注意变量生命周期,离开作用域后引用会悬空。
    • 值捕获:捕获的是变量的副本,默认不能修改,若需修改需加 mutable。
  4. 右值引用(&&)误用:普通函数形参不要随便用右值引用,主要用于移动构造、移动赋值,避免绑定错乱。
  5. emplace_back 与 push_back:emplace_back 直接在容器内构造对象,省去拷贝/移动开销;push_back 先构造临时对象,再拷贝/移动到容器,新手容易无脑混用。

第五部分:编译 & 隐性语法坑

  1. 头文件重复包含 :未用 #ifndef/#define/#endif#pragma once,会导致重复定义报错。
  2. 宏定义加分号 :如 #define MAX 100;,使用时 if(a > MAX) 会因多一个分号报错。
  3. 运算符优先级陷阱:&(位与)低于 ==、|(位或)低于 &&(逻辑与),位运算时不加括号极易导致逻辑错误。
  4. 默认参数重复定义:函数默认参数只能在声明处设置,定义处不能重复给默认值。
  5. 命名空间污染 :全局滥用 using namespace std; 容易引发名字冲突,大型项目禁止全局使用,可局部使用(如函数内)。
  6. 未定义行为(UB):数组越界、空指针解引用、重复释放、整数溢出等,程序可能崩溃、乱码或无规律运行,调试难度极大。

第六部分:字符串易错点

  1. string 与 C 字符串区别:C++ string 末尾没有 '\0',不要用 C 语言的 strlen、strcpy 等函数强行操作 string。
  2. c_str() 陷阱:c_str() 返回的是临时指针,其生命周期与 string 对象一致,不能长期保存(如 string 销毁后,指针变成野指针)。
  3. string::npos :是无符号整数(值为 -1 转换后的极大值),用 if(str.find("a") == -1) 会判断错误,正确写法 if(str.find("a") == string::npos)

补充:高频面试易错补充

  1. 浮点数比较:不能用 == 直接比较,需用误差范围判断(如 fabs(a< 1e-6)。
  2. 多线程易错:共享变量未加互斥锁,会导致数据竞争,程序运行异常。
  3. 虚函数底层:虚函数表存储虚函数地址,对象包含虚指针指向虚函数表;构造函数中虚指针未初始化,故不触发多态。
相关推荐
java1234_小锋5 小时前
Spring AI 2.0 开发Java Agent智能体 - 新建 HelloWorld 项目
java·人工智能·spring·spring ai
yue2004035 小时前
Spring IoC 与 DI 核心概念与原理笔记
java·笔记·spring
wuminyu5 小时前
专家视角看Java多态性的底层基石vtable(虚函数表)构建过程解析
java·linux·c语言·jvm·c++
phltxy5 小时前
Spring Cloud 服务注册与发现:Eureka 从原理到实战
java·spring cloud·eureka
charlie1145141916 小时前
现代Qt开发教程(新手篇)1.10——进程
开发语言·c++·qt·学习
测试那点事儿6 小时前
零基础API 接口自动化框架源代码:结构、功能与运行时序
java·servlet·自动化
AI人工智能+电脑小能手6 小时前
【大白话说Java面试题】【Java基础篇】第23题:ConcurrentHashMap的底层原理是什么
java·开发语言·算法·哈希算法·散列表·hash
爱怪笑的小杰杰6 小时前
优化 UniApp 日历组件的多语言切换:告别 setLocale 引起的 App 重启
java·前端·uni-app
solicitous6 小时前
JAVA系统复习(基础语法-类、接口)
java·开发语言