C++11学习笔记
C++11 笔记
一、auto 类型推导
1.定义:auto 让编译器自动推导变量类型,编译期确定类型,不是动态类型。
2.通俗讲解:不用手动写 int、string 这些类型,编译器根据等号右边的值自动判断变量是什么类型。
3.不适用auto原生写法
vector<int>::iterator it = v.begin();
使用auto
auto it = v.begin();
好处:
- 简化长类型,像容器迭代器、复杂模板类型,不用手写一长串;
- 代码简洁,修改容器类型时,左边类型不用同步改动,减少出错;
- 编译期推导,无运行时开销。
4.示例
auto a = 10; // 推导成 int
auto b = 3.14; // double
auto str = "abc"; // const char*
5.坑
- auto 推导会默认去掉顶层 const 和引用;
- auto 不能单独声明变量,必须初始化;auto x;直接编译报错;
- auto 配合引用auto &ref = a,会保留底层 const。
二、decltype 类型推导
1.定义:decltype (表达式) 获取表达式对应的类型,不会计算表达式的值。
2.通俗讲解:auto 是根据变量初始化值推类型;decltype 直接拿括号内表达式的类型,可以不初始化变量。
3.原生写法:需要手动写返回值 / 变量类型,复杂表达式类型很难手写。
auto 局限:auto 必须给初始化值;decltype 不需要变量初始化,只提取类型。
好处:拿到表达式真实类型,保留 const、引用属性;适合函数后置返回类型场景。
4.示例
int x = 10;
decltype(x) y = 20; // y类型就是int
5.坑
-
如果表达式是变量加括号
decltype((x)),推导结果变成引用; -
decltype 不会执行括号里代码,只做类型分析
三、auto + decltype :decltype (auto) (C++14)
1.定义:保留表达式原本的引用、const 属性,不用手动写 decltype。
2.通俗讲解:auto 会丢失引用,decltype (auto) 完整保留类型属性,常用于函数返回值推导。
3.普通auto:丢失引用属性;decltype (auto) 保留引用、const。
好处:写函数返回值时,不用手写冗长 decltype,自动保留值类别。
4.坑:容易和 auto 混淆,decltype(auto)会推引用。
四、范围 for 循环(range-based for)
1.定义:C++11 新增,直接遍历容器 / 数组,不用写迭代器下标。
2.通俗讲解:遍历数组、vector 的时候,不用 i 下标,直接取出里面每一个元素。
3.旧写法(迭代器遍历)
for(vector<int>::iterator it = v.begin(); it != v.end(); ++it)
cout << *it;
范围for写法
for(auto num : v){
cout << num;
}
好处:代码简短、可读性强;不用手动维护迭代器、判断 end (),减少边界错误。
4.示例
vector<int> v = {1,2,3};
for(auto num : v){
cout << num;
}
5.坑
-
默认是值拷贝,修改不会改动原容器;想要修改元素必须用
auto &num;只读用const auto &避免拷贝; -
遍历的时候不能增删容器元素,会导致迭代器失效(vector 最常考);
-
仅支持有 begin () end () 的容器 / 原生数组。
五、初始化列表 initializer_list
1.定义:统一初始化语法,花括号 {} 初始化,initializer_list 是接收一组同类型参数的模板类。
2.通俗讲解:C++11 之前初始化写法很乱;C++11 统一用 {},还可以让函数接收一串同类型数字。
3.旧写法:逐个push_back
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
C++11初始化列表
vector<int> v{1,2,3,4};
void func(initializer_list<int> list);
func({10,20,30});
好处:一行完成批量初始化,代码简洁;统一所有类型初始化语法;initializer_list 支持函数一次性接收多个同类型参数。
4.示例
vector<int> v{1,2,3,4};
void func(initializer_list<int> list);
func({10,20,30});
5.坑
-
{} 初始化会禁止隐式窄化转换:int {3.14} 编译报错;
-
initializer_list 底层是常量引用,里面元素不能修改。
六、右值引用
1.定义:右值引用T&&只能绑定到右值,用来区分临时对象,实现移动语义。
2.通俗讲解
左值:能取地址、生命周期长的变量;
右值:临时对象、表达式结果,用完马上销毁,不能取地址;
&&就是专门绑定这种临时对象,不用拷贝数据。
3.新旧对比:C++之前只有普通引用 T&,只能绑定左值,临时对象只能触发拷贝构造;
C++11 T&& 右值引用,可以捕获临时对象。
好处:开启移动语义,避免大对象深拷贝,大幅提升性能(string、vector 这种容器效果明显)。
4.示例
int a = 10;
int &&r = 20; // 20是右值,可以绑定
// int &&r2 = a; 报错,a是左值
5.坑
- 万能引用:模板里T&&不是单纯右值引用,会发生引用折叠,既能接左值也能接右值;
- 引用折叠规则:T& + & → &;T& + && → &;T&& + & → &;T&& + && → &&(笔试选择题高频)。
七、std::move
1.定义:move 不是移动,只是强制把左值转为右值引用,不移动任何内存。
2.通俗讲解:只是类型转换标记,告诉编译器这个对象的资源可以被抢夺,原对象进入有效但未定义状态。
3.新旧对比:C++之前无法主动触发移动;std::move 手动将左值转为右值,主动触发移动构造。
好处:复用已有对象堆资源,省去深拷贝开销。
4.示例
string s = "hello";
string s2 = move(s); // 把s转为右值,触发移动构造
5.坑:
move 之后不要继续使用原变量 s,只能赋值或销毁。
八、移动构造函数 / 移动赋值
1.定义:C++11 新增,接收 T&& 参数,转移另一个对象堆内存资源,代替深拷贝。
2.通俗讲解:拷贝构造是复制一份数据;移动构造直接抢临时对象的堆内存,效率高,没有拷贝开销。
3.新旧对比:C++之前只有拷贝构造,临时对象会完整复制堆上全部数据,开销巨大;C++11 移动构造只拷贝指针、大小等少量成员,堆内存所有权转移。
4.触发时机:返回局部对象、临时对象赋值。
5.思考:
- 如果自己不写移动构造,编译器会自动生成(满足条件);
- 移动构造 / 移动赋值如果抛异常,会破坏容器,所以引出 noexcept。
九、noexcept
1.定义:声明函数不会抛出异常,如果抛异常程序直接终止。
2.通俗讲解:标记这个函数安全,不会抛异常,STL 容器会优先选用带 noexcept 的移动构造。
3.新旧对比:C++之前没有 noexcept,无法标记函数是否抛异常;STL 容器扩容时,不知道移动构造安不安全,会回退拷贝。
好处:告诉标准库该移动操作不会抛异常,vector 扩容优先使用移动,而不是拷贝,提升性能。
4.思考:vector 扩容的时候,如果移动构造带 noexcept,会直接移动;否则回退到拷贝。
十、lambda 表达式
1.定义:可以在函数内部定义匿名可调用对象。
2.通俗讲解:不用单独写全局函数 / 仿函数,临时写一个小函数,给 sort、线程用。
3.新旧对比:C++之前排序要单独写全局比较函数,或者手写仿函数类,代码分散;
lambda 直接写在调用处。
好处:回调逻辑就近写,代码紧凑;可以直接捕获外部变量,不用全局传参。
基础语法:[捕获](参数) -> 返回类型 {函数体}
捕获规则
[] 不捕获任何变量
[=] 值捕获,拷贝外部变量,不能修改
[&] 引用捕获,可以修改外部变量
[this] 捕获类内 this 指针
4.示例
vector<int> v = {3,1,2};
sort(v.begin(),v.end(),[](int a,int b){
return a < b;
});
5.坑
- 值捕获默认 const,想要修改要加 mutable;
- lambda 捕获是定义时捕获,不是调用时;循环内 lambda 很容易出现引用捕获悬空问题;
- lambda 本质是编译器生成仿函数类。
十一、std::function 可调用对象包装器
1.定义:统一包装普通函数、lambda、函数指针、仿函数。
2.通俗讲解:不管是什么类型可调用对象,统一包装成同一个 function 类型,方便存容器、回调
3.新旧对比:C++之前函数指针只能包装普通函数,仿函数、lambda 无法统一存放;
std::function 统一封装所有可调用对象。
好处:回调接口统一,可以放到 vector 中保存一批回调函数,写事件驱动代码很方便。
4.示例
function<void(int)> f = [](int x){cout << x;};
5.坑:
空 function 调用会抛异常。
十二、std::bind
1.定义:绑定函数参数,固定部分参数,调整参数顺序。
2.通俗讲解:给函数预先填好一部分参数,生成新的可调用对象
3.新旧对比:C++之前要封装一个包装类才能固定参数;bind 一行完成参数绑定。
好处:适配回调接口,修改参数个数、顺序,适配接口参数要求。
4.思考:placeholders::_1 占位符
十三、智能指针
shared_ptr
1.定义:共享所有权智能指针,引用计数,多个 shared_ptr 管理同一块堆内存。
2.通俗讲解:有计数器,每多一个 shared_ptr 指向这块内存,计数 + 1;析构计数 - 1,计数到 0 释放内存。
3.新旧对比:
裸指针:手动 new、delete,忘记释放就内存泄漏,多次 delete 程序崩溃;
shared_ptr 自动管理生命周期。
好处:自动释放堆内存,减少内存泄漏风险;多个对象共享同一块内存。
4.坑:循环引用!两个对象互相存 shared_ptr,引用计数永远不为 0,内存泄漏。
weak_ptr
1.定义:弱指针,不增加引用计数,专门解决 shared_ptr 循环引用。
2.通俗讲解:只是观察者,不持有所有权,要使用必须调用 lock () 转成 shared_ptr。
3.新旧对比:
单独 shared_ptr 无法解决循环引用;weak_ptr 不增加计数,打破循环。
好处:解决 shared_ptr 循环引用内存泄漏问题;可以判断对象是否已经销毁。
4.思考:weak_ptr 不能直接解引用。
unique_ptr
1.定义:独占式智能指针,同一时间只能有一个 unique_ptr 持有内存。
2.通俗讲解:不允许拷贝,只能 move 转移所有权,开销比 shared_ptr 小。
3.新旧对比:
shared_ptr 有引用计数开销;unique_ptr 无额外计数开销。
好处:独占资源,性能接近裸指针;禁止拷贝,避免多份指针乱释放。
一句话对比:unique_ptr 独占;shared_ptr 共享(计数);weak_ptr 弱引用,解决循环引用。
十四、nullptr
1.定义:C++11 空指针常量,类型 nullptr_t。
2.通俗讲解:代替原来 NULL(NULL 本质是 0),解决函数重载歧义。
3.新旧对比:
旧:#define NULL 0,本质整数;传进重载函数会匹配 int 版本。
nullptr 是专门空指针类型。
好处:类型安全,重载场景不会匹配 int,专门匹配指针参数。
4.坑:
NULL是宏 0,传给重载函数会匹配 int 版本;nullptr 会匹配指针版本
十五、enum class 强类型枚举
1.定义:强类型枚举,enum class,枚举值不会隐式转为 int,有独立作用域。
2.通俗讲解:老 enum 可以直接当成整数用,容易类型混乱;强类型 enum 必须写枚举名::枚举值,不能隐转 int。
3.新旧对比:
普通 enum:隐式转 int,不同枚举值可以互相比较,容易混淆;
enum class 强类型,有命名空间隔离。
好处:类型安全,枚举常量不会泄露到外层作用域,避免名字冲突。
4.示例
enum class Color{Red,Green};
Color c = Color::Red;
5.思考:
必须强制类型转换才能转 int。
十六、constexpr
1.定义:编译期常量,constexpr 修饰变量 / 函数,求值发生在编译阶段。
2.通俗讲解:const 只是只读;constexpr 要求值编译期就能算出来,运行期不用计算。
3.新旧对比:
const 只是运行期只读;constexpr 强制编译期求值。
好处:计算在编译阶段完成,运行时直接用结果,节省运行开销;可作为数组大小、模板参数。
4.坑:
C++11 的 constexpr 函数限制很多,C++14 放宽。
十七、类的默认函数控制:default /delete
1.default:让编译器生成默认版本的函数(构造、拷贝构造等)。
2.delete:禁用这个函数,禁止调用。
3.通俗讲解
A() = default; 强制编译器生成默认构造;
A(const A&) = delete; 直接禁用拷贝构造,这个类不能拷贝。
4.新旧对比:
C++03:想要禁用拷贝,需要把拷贝构造写为 private;
C++11 delete 直接禁用,语义清晰。
好处:default 显式声明,可读性高;delete 简洁禁用函数,不需要私有 hack 写法。
5.思考:
常用场景禁止拷贝、禁止移动。
十八、委托构造函数
1.定义:同一个类里面,一个构造函数调用本类另一个构造。
2.通俗讲解:多个构造函数重复逻辑,复用其他构造,减少重复代码。
3.新旧对比:
C++之前多个构造函数重复初始化代码,冗余;委托构造复用逻辑。
好处:代码复用,统一初始化逻辑,减少重复代码。
4.坑:
不能循环委托(A 构造委托 B,B 又委托 A,编译报错)。
十九、继承:override、final
1.override:显式声明这个虚函数是重写父类虚函数,如果签名不一致直接编译报错。
2.final:修饰虚函数 → 禁止子类重写;修饰类 → 这个类不能被继承。
3.通俗讲解:
override 帮编译器检查有没有写重虚函数;final 上锁,不让继续继承 / 重写。
4.新旧对比:
C++之前重写虚函数,签名写错编译器不会报错,运行期异常很难排查;override 编译期校验。
好处:override 编译期捕获虚函数签名错误;final 防止不必要继承,保护类。
5.思考:
override 只用于虚函数,普通函数写 override 编译报错。
二十、线程库 thread /mutex
1.std::thread:C++ 原生线程类
1.通俗讲解:创建 C++ 线程,传入可调用对象(lambda 最常用)
2.新旧对比:
C++之前 需要调用系统 API(pthread),跨平台差;std::thread 标准库,跨平台统一接口。
好处:C++ 标准库自带,不用依赖平台原生线程 API。
3.坑:
thread 对象析构时,如果线程还没 join (),程序直接崩溃;两个方案:join () 等待结束 /detach 分离线程。
2.std::mutex 互斥锁:保护共享资源,同一时间只允许一个线程访问
3.std::lock_guard:RAII 自动锁,构造加锁,析构自动解锁,避免忘记 unlock 死锁
1.新旧对比:
手动 mutex.lock ()/unlock:中途 return、抛异常会忘记解锁,死锁;lock_guard RAII 自动解锁。
好处:自动释放锁,异常场景也不会死锁。
RAII:资源获取即初始化,C++ 核心思想,智能指针、lock_guard 全是 RAII。
4.std::condition_variable 条件变量:线程等待 + 唤醒,生产者消费者模型
二十一、std::atomic 原子类型
1.定义:无锁原子操作,多线程下读写这个变量不需要 mutex,不会出现数据竞争。
2.通俗讲解:普通变量多线程读写会出现脏数据;atomic 封装的变量操作是不可分割的原子操作。
3.新旧对比:
多线程普通变量需要 mutex 加锁保护;atomic 无锁。
好处:相比 mutex 开销更低,适合计数器这类简单变量。
4.思考:
适合计数器场景,开销小于 mutex。
二十二、typedef 替代:using 类型别名(C++11)
1.using别名语法,比typedef更直观,支持模板别名。
using IntPtr = int*;
// 模板别名(typedef做不到)
template<typename T>
using Vec = vector<T>;
2.新旧对比:
typedef 语法容易出错,不支持模板别名;using 语法顺序自然,支持模板别名。
好处:可读性更强;支持模板别名,typedef 无法实现模板别名。
3.思考:
using 支持模板别名,typedef 不支持,选择题经常对比两者。
浙公网安备 33010602011771号