类和对象
类的基本知识
类的构造函数
构造函数的名字和类名相同,没有返回类型。对象创建时会自动调用,用来保证对象一出生就是“可用状态”。
常见形式:
- 默认构造函数(无参)
- 带参构造函数
- 委托构造函数(构造函数里调用同类其他构造函数,
C++11)
|
|
常见坑:
- 构造函数体内赋值不等于成员初始化,优先使用初始化列表。
- 成员初始化顺序由“声明顺序”决定,不是初始化列表书写顺序。
类的拷贝构造函数
拷贝构造就是“用一个同类型老对象,生成一个新对象”。
典型签名:
|
|
如果类里只有基础类型和标准库对象,编译器默认生成的拷贝构造通常就够用;
如果类管理的是裸资源(如 new 出来的指针),就要自己实现深拷贝,不然容易出现二次释放。
|
|
浅拷贝与深拷贝:
- 浅拷贝:只复制指针值,两个对象指向同一块内存。
- 深拷贝:复制资源本体,每个对象各管各的资源。
类的移动构造函数
移动构造不是“复制资源”,而是“接管资源”。
典型签名:
|
|
当右值对象(临时对象,或被 std::move 转换后的对象)初始化新对象时,移动构造可以避免深拷贝,提高性能。
|
|
常见坑:
- 移动构造建议加
noexcept,容器(如std::vector)扩容时才更愿意走移动路径。 - 被移动对象必须保持“可析构、可赋值”的有效但未指定状态。
类的析构函数
析构函数就是对象的“收尾清理”。
典型签名:
|
|
析构函数会在对象生命周期结束时自动调用:
- 局部对象离开作用域。
delete动态对象。- 程序结束时销毁全局/静态存储期对象。
当类作为基类并且要“通过基类指针删除派生类对象”时,基类析构函数必须是虚函数。
|
|
常见坑:资源管理类通常遵循“规则五(Rule of Five)”,即拷贝构造、拷贝赋值、移动构造、移动赋值、析构这五个函数要整体考虑。
类的继承与多态
多态实现的前提
- 类的成员函数有虚函数
- 公有继承的派生类
- 派生类对这个虚函数进行了重写
- 使用基类的指针或引用指向派生类对象
还需要注意:如果用基类指针指向派生类对象,并通过这个基类指针
delete,那么基类析构函数必须是虚函数,否则只会调用基类析构,不会完整调用派生类析构。
纯虚函数(抽象类)
|
|
不可实例化,需要派生一个类且重写虚函数,实例化派生类
类的进阶知识
override和final限定符
override 和 final 是 C++11 引入的上下文关键字。
在派生类中重写虚函数时,使用 override 可以让编译器检查函数签名是否真的匹配基类,防止“看起来像重写,其实是重载”的错误。
final 可以阻止继续重写:
- 修饰虚函数:禁止更下层派生类再重写。
- 修饰类:禁止该类再被继承。
它们不是完全保留关键字,在普通上下文里仍可作为标识符使用,但实际开发中不建议这么写,避免降低可读性。
|
|
静态成员
静态成员属于类本身,不属于某个对象。
静态成员变量不依赖对象存在,通常用于:
- 统计同类对象数量。
- 维护类级共享配置。
- 单例模式中的实例入口(只是场景之一,不是唯一用途)。
|
|
静态成员函数里没有 this 指针,所以不能直接访问非静态成员;但可以通过传入对象,再间接访问对象成员。
const成员函数
const 成员函数承诺“不会修改对象可观察状态”。
本质上可以理解为:this 的类型变成了 const ClassName*。
|
|
const 对象只能调用 const 成员函数,不能调用非常量成员函数。
|
|
this指针
this 就是“当前对象自己的地址”。
在非静态成员函数中,编译器会隐式传入 this 指针。
- 普通成员函数中:
this类型近似ClassName*。 const成员函数中:this类型近似const ClassName*。
典型用法:
- 解决成员名和参数名冲突。
- 返回
*this以支持链式调用。
|
|
访问父类中的函数
在子类中调用父类版本函数,可以使用作用域解析运算符:
|
|
如果子类定义了同名函数,会隐藏父类的重载集合,可用 using 重新引入:
|
|
友元函数与类
友元是“被特批访问私有成员的外部函数/类”。
友元不属于类成员,但可以访问类的 private/protected。
|
|
注意点:
- 友元关系是单向的、不可继承的。
- 友元是“打洞”机制,能不用就少用,优先保持封装性。
模板
函数模板

基本概念
|
|
模板函数描述的是“生成函数的规则”,编译器在看到具体类型后进行实例化,才会生成真正的函数。
|
|
补充:class 和 typename 在模板参数列表中等价,二者都可以用。
显式实例化
显式实例化是“我提前告诉编译器:这个类型版本一定要生成”。
|
|
如果不显式实例化,通常由编译器根据调用点进行隐式实例化。
实例化与特化的区别
- 实例化(Instantiation):按模板规则“生成代码”。
- 特化(Specialization):为某些特定类型“单独写实现”。
特化(Specialization)
特化分为全特化和偏特化:
- 函数模板:只支持全特化,不支持偏特化(可用重载替代)。
- 类模板:支持全特化和偏特化。
|
|
类模板
基本概念
类模板就是“按类型生成类”的模具。
|
|
如果类模板成员函数在类外定义,要重复模板声明:
|
|
类模板的特化
类模板全特化后,本质是一个全新的类定义,不会自动继承主模板实现。
此外类模板还支持偏特化(部分特化):
|
|
非类型模板参数
模板参数不一定是类型,也可以是常量值。
|
|
常见非类型模板参数:整数、枚举、指针、引用、std::nullptr_t(不同标准版本支持范围略有差异)。
模板嵌套
类模板的参数还可以是一个模板
|
|
补充说明:
- 这种写法常见于“策略类/容器适配器”。
- 模板是后面泛型 lambda、完美转发的基础能力,后面章节会用到这里的类型推导思想。
lambda表达式
一起来学C++ 32.Lambda表达式_哔哩哔哩_bilibili
C++14以后引入了泛型lambda
定义方式
|
|
若省略返回类型,编译器会自动推导
例子
|
|
本质
lambda表达式的本质是函数对象的一种快捷定义方式,同时会有的捕获变量的成员变量
|
|
本质形式如下
|
|
按值捕获&按引用捕获
捕获变量有两种方式,按值捕获 和 按引用捕获
按值捕获,初始化时只是复制了x,y的值
|
|
按引用捕获,匿名函数对象内部的成员变量会被初始化为变量x,y的引用
|
|
lambda函数所对应的函数调用运算符默认是const函数,也就是说函数内部不能修改按值捕获的成员变量,如果捕获的是对象,也不能调用这个对象的非常量函数,若是按引用捕获,则不会受此限制
如果给lambda函数加上mutable限制符,那么对应的调用函数就不再是const函数,捕获的成员都是可以修改的
在捕获字句中,还可以使用默认捕获
|
|
下面的例子中,用到的外层变量x,y都会被捕获,并生成对应的拷贝成员
|
|
同样可以进行修改,使特定变量以显式形式被捕获
|
|
需要注意的是,默认捕获与显式捕获不能是同类型的
|
|
还需要注意,使用引用捕获时,若调用lambda对象时,引用变量以及失效,则程序运行的时候会出现错误或者异常
如果在一个类中定义lambda对象时,可以用以下方式来捕获this指针或者*this解引用
|
|
泛型lambda
在C++14标准以后,引入了泛型lambda,实现的方法是在函数的参数类型中使用auto关键字
|
|
这样其本质就是如下所示,用以简化lambda的泛化方式
|
|
右值引用与移动语义
左值与右值
左值能放在表达式的左侧,能够代表一块存储区域,判断左值的方式一般是 能否获得这个表达式的引用或者取地址
计算中间结果等存放在临时中间对象中的是右值(纯右值)
|
|
在C++11之后,有glvalue(泛左值)和prvalue(纯右值),泛左值有被分为lvalue(左值)和xvalue(eXpiring value)(亡值)
右值引用
使用&&来定义右值的引用,右值的引用只能绑定右值
|
|
有了右值引用就可以在函数调用时将左值参数和有值参数分开
基于此还会引申出 移动构造函数 和 移动赋值运算符重载函数(右值引用的参数) 这两个函数与 拷贝构造函数 和 拷贝赋值运算符重载函数(左值引用的参数) 类似
在实际代码中
|
|
使用一个右值来初始化buff对象,应该会调用 移动构造函数,但实际上调用的是 普通构造函数(连拷贝构造函数也不是),
其原因是 拷贝省略(Copy Elison)的优化,直接省略临时对象的创建,并直接在目标的存储位置构造对象
在函数返回等场景也会有优化,都是省略临时对象的创建,例如返回值优化(Return Value Optimization(RVO)),具名返回值优化(Named Return Value Optimization(NRVO))等
移动语义
|
|
此时buff1调用的是 普通构造函数,buff2调用的是 拷贝构造函数
将代码改为以下
|
|
此时buff1调用的是 普通构造函数,buff2调用的是 移动构造函数,在此场景中std::move()等价于static_cast<CharBuffer&&>
|
|
上面的move函数的作用是将 传入的参数转换为 右值引用 并返回,在上面的场景中,std::move的返回的右值引用就是一个亡值(xvalue即将消亡的值)
移动语义常用的场景是智能指针的unique_ptr的控制权转移,另一个常见场景是在通用模板库中,例如std::vector就支持移动语义,方便将右值的指针等转移到容器里
完美转发
|
|
输出的结果为
|
|
注意右值引用前面不要加const
右值引用的核心目的是“窃取资源”(Move)。如果加上 const,就无法修改它,也就无法把它的内部资源(如指针)置空,这就退化成了普通的拷贝,失去了移动的意义。
原因在于不管实参的类型是什么,在函数体内这个函数是具有名称的局部变量(可以获取地址),即s是一个左值,要实现调用想要的右值版本函数,需要加上参数转换
|
|
这样写在参数多的时候会引起重载函数几何式的增长,正确的最佳实践应该如下
|
|
这种模板参数的右值引用形式被称为 万能引用或者转发引用(Universal Reference),这并不是真正的右值引用,它既可以绑定左值,也可以绑定右值(只有当这种形式作为函数形参的类型时才能被叫做万能引用,若出现在函数体代码中,则不是万能引用)
函数模板的类型参数会在实例化时根据传入的实参类型被推导出相应的类型,而模板参数的右值引用形式遵循特殊的类型推导规则:
在传入的参数为右值时,推导出的函数参数为右值引用(T&& = 右值引用),模板参数T会被推导为实参类型(T=实参类型)即T为原始类型
在传入的参数为左值时,模板参数T会被推导为实参类型的引用(T=实参类型引用)
这段代码在传入的参数为右值时,推导出的函数参数为右值引用(T&& = 右值引用),T=string即模板参数T会被推导为实参类型
|
|
当传入的参数为左值时,模板参数T会被推导为实参类型的引用,即T=string&
在C++语法中引用的引用是不被允许的,由此引出的会是 引用折叠规则
引用折叠规则
| T & & | T & |
|---|---|
| T & && | T & |
| T && & | T & |
| T && && | T && |
|
|
再来看forward函数
|
|
对于forward,当传入的T是string时,实例化会是
|
|
此时返回的是传入值t原本参数类型的右值引用(将t强制转换为右值),实现的效果是将右值引用转换为右值(需要注意,右值引用是一个左值)
若传入的T是左值引用string&(根据引用折叠化简),实例化会是
|
|
也就是此时forward函数返回了传入值t的左值引用
总结,通过使用模板的万能引用结合std::forward函数实现参数在调用过程中按照参数原有类型进行传递,这就是完美转发的大致原理
左值右值与完美转发的总结
1. 左值 (Lvalue) vs 右值 (Rvalue)
判断左值和右值,最简单的方法是看**“它是否有名字”以及“你能否取它的地址”**。
- 左值 (Lvalue):
- 特征:有名字、在内存中有固定地址、生命周期长(超过当前语句)。
- 比喻:永久居民。你叫得名字,找得到家。
- 例子:int a = 10; 中的 a。
- 右值 (Rvalue):
- 特征:没名字(或是临时的)、通常不能取地址、生命周期短(用完即弃)。
- 比喻:过客。用完就销毁的临时工。
- 例子:10、x + y 的结果、string(“hello”) 构造出的临时对象。
2. 左值引用 vs 右值引用
引用就是别名。
- 左值引用 (T&):只能绑定到左值。
- 右值引用 (T&&):只能绑定到右值。它的存在是为了给将死的临时对象续命,或者为了窃取它的资源。
3.完美转发
完美转发要解决的问题是:当函数作为“中间商”传递参数时,参数的“左/右值属性”会丢失。
|
|
结论:无论你传给 wrapper 的是左值还是右值,只要到了 wrapper 内部,参数 arg 永远是左值。如果不用完美转发,永远调用的都是 real_func 的左值版本。
4. 万能引用 (Universal Reference / Forwarding Reference)
C++11 规定:如果一个模板参数的类型是 T&&,且 T 是需要推导的,那么它就不是单纯的“右值引用”,而是万能引用。 推导规则(牢记):
- 传入左值:T 被推导为 Type&。
- 传入右值:T 被推导为 Type (即原始类型)。
5. 引用折叠 (Reference Collapsing)
既然 T 可能被推导为引用,那 T&& 就会出现 Type& && 这种奇怪的东西。C++ 编译器会进行“折叠”:
- 只要遇到一个 & (左值引用),结果就是左值引用。
- 只有两个都是 && (右值引用),结果才是右值引用。
| 推导出的 T | 代码中的 T&& | 折叠后的真实类型 | 这里的 arg 是左值还是右值引用? |
| string& (传入左值) | string& && | string& | 左值引用 |
| string (传入右值) | string && | string&& | 右值引用 |
6. std::forward
即使通过万能引用和引用折叠,我们得到了正确的类型(是 string& 还是 string&&),但在 wrapper 函数体内,变量 arg 本身依然是左值(因为它有名字)。
我们需要一种机制:
- 如果 T 是左值引用,把 arg 保持为左值。
- 如果 T 是非引用(即推导自右值),把 arg 强制转换为右值(等价于 std::move)。
这就是 std::forward<T> 做的事情。
std::forward 的实现逻辑(伪代码):
|
|
场景演练 1:传入左值 s
- wrapper(s) 被调用。
- T 推导为 string&。
- std::forward<string&>(arg) 被调用。
- 返回 static_cast<string& &&>(arg) -> 折叠为 static_cast<string&>(arg)。
- 结果:还是左值。
场景演练 2:传入右值 move(s) 或临时对象
- wrapper(move(s)) 被调用。
- T 推导为 string。
std::forward<string>(arg)被调用。- 返回
static_cast<string&&>(arg)。 - 结果:强制转换为右值引用(即右值)。
总结
完美转发
- 万能引用 (T&&):负责根据实参,把 T 推导成“包含左/右值信息”的类型(Type& 或 Type)。
- 引用折叠:负责处理 & 和 && 的冲突,确定参数 arg 的最终类型。
std::forward<T>:负责在最后一步,根据T的类型,决定是否要对arg进行static_cast<Type&&>(即move)。如果T是左值引用,它就不转;如果T是普通类型,它就强转为右值。
函数封装与绑定
std::function是一个通用的多态函数封装器,可将函数指针,函数对象,lambda函数等进行封装
在std中std::function用到的是定义是类模板的部分特化,如下
|
|
实际使用如下
|
|
注意封装类的成员函数(同样也针对于封装 类的普通成员)时,需要传入类的引用
|
|
std::function实现了 类型擦除 的模式,将不同类型的函数实现统一的封装接口
对于封装类的成员,可以使用std::mem_fn
|
|
参数是指向类成员的指针,返回值是一个可调用的包装器,使用方式如下
|
|
std::bind是一个函数模板,用来生成一个函数调用的转发包装器,即一个函数对象,调用该包装器时,就相当于调用它所包装的函数或者对象,并使用args作为函数的参数
|
|
注意,这里用的是右值引用
使用例子如下
|
|
调用时,用的是指定的参数,也可以用占位符来代替未被指定的参数(占位符为std::placeholder)
|
|
需要注意,此处的绑定的是绑定的右值引用,实现的效果是传递值
一句话总结,就是 与参数绑定,形成一个新的可调用对象