C++ 可变参数模板(Variadic Template)
一、基础概念
普通模板只能固定参数个数,可变参数模板允许接收任意数量、任意类型的参数,核心由两部分组成:
- 参数包
Args...:省略号...代表一堆类型 / 参数集合- 解包
args...:展开参数包里所有参数语法格式
cpptemplate<typename... Args> // Args... 类型参数包 void func(Args... args) // args... 函数参数包 { // 解包使用 args... }
二、两种经典解包方式
有参数包
Args.../args...,书写pattern...,编译器会把pattern针对包中每一个元素复制一份,逗号分隔展开,这个过程叫包扩展。**方式 1:**递归拆解(传统写法,C++11 起)
思路:每次取出第一个参数,剩下的参数包递归调用,写终止函数处理空包。
cpp#include <iostream> // 递归终止:空参数包 void print() { cout << "\n"; } // 可变模板 template<typename T, typename... Args> void print(T first, Args... rest) { cout << first << " "; print(rest...); // 递归拆解剩余参数 } int main() { print(1, 3.14, "hello", 'a'); return 0; }执行流程:
**
print(1,3.14,"hello",'a')→ 输出 1 →print(3.14,"hello",'a')→****输出 3.14 →print("hello",'a')→ 输出 hello →print('a')→**输出 a →print()换行结束**方式 2:**折叠表达式(C++17,最简推荐)
无需递归终止函数,一行展开所有参数,分一元折叠、二元折叠。
1. 一元右折叠
(pack op ...)从右往左运算
cpptemplate<typename... Args> void print(Args... args) { (cout << ... << args) << "\n"; } print(10, 20, 30); // 等价:std::cout << 10 << 20 << 302. 一元左折叠
(... op pack)
cpptemplate<typename... Args> auto sum(Args... args) { return (... + args); } sum(1,2,3,4); // 1+2+3+4=103. 二元折叠(带初始值)
cpptemplate<typename... Args> auto sum_init(Args... args) { return (0 + ... + args); } sum_init(); // 空包返回0,不会报错
三、参数包操作工具
1. sizeof... 求参数个数
cpptemplate<typename... Args> void count(Args&&... args) { // sizeof... 专门获取参数包元素数量 cout << sizeof...(args) << "\n"; } count(1, 2, "abc"); // 输出3、32. 配合万能引用 + 完美转发(万能构造函数)
cpp#include <utility> template<typename T> struct Test { T data; // 可变参数转发构造 template<typename... Args> Test(Args&&... args) : data(forward<Args>(args)...) {} }; Test<std::string> t("hello");
std::forward<Args>(args)...批量转发所有参数,保留左右值属性。
四、参数包进阶使用
1. 初始化列表展开
cpptemplate<typename... Args> void push_all(vector<int>& v, Args... args) { int arr[] = { (v.push_back(args), 0)... }; }逗号表达式批量执行每个参数的逻辑。
2. 元组拆解 get + sizeof...
cpp#include <tuple> template<size_t... Idx, typename Tuple> void print_tuple(index_sequence<Idx...>, Tuple t) { ((cout << get<Idx>(t) << " "), ...); } template<typename... Args> void print_t(tuple<Args...> t) { print_tuple(index_sequence_for<Args...>{}, t); }
五、关键区分易错点
Args...:类型参数包;args...:函数参数包;...位置决定是打包还是解包- C++11/14 只能递归解包;C++17 折叠表达式大幅简化代码
sizeof...()是运算符,和普通sizeof完全不同,只用于参数包- 可变模板不能单独使用,必须配合模板参数包声明
typename...- 折叠表达式空包限制:一元折叠空包编译报错;二元折叠有初始值则安全
emplace 系列接口完整详解 (emplace/emplace_back/emplace_front)
一、核心定义与语法
1. 三个核心接口(C++11)
emplace_back(Args&&... args)容器尾部就地构造元素,对应push_back
cpptemplate <class... Args> void emplace_back(Args&&... args);
emplace(const_iterator pos, Args&&... args)指定迭代器位置就地构造,对应insert
cpptemplate <class... Args> iterator emplace(const_iterator position, Args&&... args);
emplace_front(Args&&... args)头部就地构造(仅 deque/list/forward_list 支持),对应push_front共性:
- 全部是可变参数模板 + 万能引用,结合引用折叠、完美转发;
- 参数不是容器元素对象,而是构造元素所需的参数。
二、emplace 和 push/insert 本质区别
push_back 逻辑:先构造,再拷贝 / 移动
cppvector<Person> v; // 1. 临时对象 Person("张三", 20) 构造(栈/临时内存) // 2. 将临时对象移动拷贝到容器内存 v.push_back(Person("张三", 20));两步:临时创建 → 拷贝 / 移动存入容器,存在额外临时对象开销。
emplace_back 逻辑:直接在容器内存原地构造
cpp// 直接把 "张三",20 转发到容器内部内存,调用 Person 构造函数 v.emplace_back("张三", 20);一步:容器分配好内存,直接在这块内存上构造对象,无临时、无拷贝 / 移动。
关键对比表格
接口 传参形式 执行流程 性能损耗 push_back 传已存在对象 / 临时对象 构造临时 → 移动 / 拷贝进容器 存在拷贝 / 移动开销 emplace_back 传元素构造参数包 容器内存****原地直接构造
⚠️****注释说明:
lt1.emplace_back({"苹果", 1})不支持,因为
{}初始化列表无法推导模板参数包类型。**核心根源:**模板参数推导 VS 隐式类型转换
(1)emplace_back 是模板可变参数函数
cpptemplate<class... Args> iterator emplace_back(Args&&... args);
{...}是纯初始化列表,没有类型,编译器无法从无类型的花括号里推导出模板参数包Args...,直接推导失败,编译报错。
lt1.emplace_back("苹果", 1):两个独立实参,类型明确const char*、int,能正常推导Args = const char*, int,合法。lt1.emplace_back({"苹果", 1}):单参数{...}无显式类型,模板推导机制识别不出这是pair<string, int>,推导失败。(2)push_back 不是模板推导,靠类隐式转换
void push_back(const T& val); void push_back(T&& val);容器
list<pair<string,int>>的T已经固定为pair<string,int>。{"苹果",1}会触发pair的隐式构造转换:编译器自动用初始化列表构造临时pair<string,int>对象,再传入push_back,不需要推导模板参数,所以合法。
C++11 默认移动构造 / 默认移动赋值 完整规则解析
一、旧版(C++98)6 个默认成员函数
不手写时编译器自动生成:
- 默认构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符重载
- 普通取地址运算符
operator&()- const 取地址运算符
operator&() const核心常用:前 4 个;后两个几乎不用。
二、C++11 新增两个默认成员函数
- 默认移动构造函数
T(T&&)- 默认移动赋值运算符
T& operator=(T&&)1. 编译器何时自动生成「默认移动构造」
满足全部条件,编译器才会合成默认移动构造:
- 类没有用户自定义移动构造函数;
- 类没有用户自定义析构函数;
- 类没有用户自定义拷贝构造函数;
- 类没有用户自定义拷贝赋值运算符。
只要你手动实现了析构 / 拷贝构造 / 拷贝赋值任意一个,编译器不再生成默认移动构造。
默认移动构造的行为
逐成员移动(逐成员引用折叠):
- 内置类型(int、double、指针等):直接值拷贝(移动对内置无优化,等价浅拷贝);
- 自定义类成员:调用该成员自身的移动构造函数;若成员无移动构造,则降级调用该成员的拷贝构造。
三、关键补充规则(易考点)
规则 1:拷贝、移动互抑制
- 只要你写了拷贝构造 / 拷贝赋值,编译器不会生成默认移动构造 / 移动赋值;
- 只要你写了移动构造 / 移动赋值,编译器不会生成默认拷贝构造 / 拷贝赋值。
规则 2:有析构函数直接禁用默认移动
只要手动写了析构(哪怕是空析构),编译器直接放弃生成移动构造、移动赋值。
设计逻辑:手写析构通常代表类管理堆内存 / 资源,默认逐成员浅移动会导致双重释放 bug,编译器主动不生成,强制用户手动实现移动语义。
规则 3:默认移动是「逐成员移动」,浅移动风险
示例:动态数组类
cppclass Arr { public: int* _a; size_t _sz; Arr(size_t n) { _a = new int[n]; _sz = n; } // 未手写析构、拷贝、移动 → 编译器生成默认移动构造 ~Arr() = default; // 一旦改成自定义析构 ~Arr(){delete[]_a;},默认移动直接消失 };默认移动构造只会拷贝
_a指针值,不会转移资源所有权,两个对象指向同一块堆内存,析构时重复释放崩溃。👉 管理堆资源的类,必须手动实现移动构造 + 移动赋值,或禁用移动。规则 4:无移动函数时,右值会降级走拷贝
若类没有移动构造,传入临时
move右值对象,编译器自动调用拷贝构造替代。
Arr f() { return Arr(10); } Arr a = f(); // 没有移动构造 → 调用拷贝构造
四、默认成员函数生成总览表
手动实现了以下函数 是否生成默认移动构造 / 移动赋值 析构 / 拷贝构造 / 拷贝赋值 任意一个 ❌ 不生成 仅默认构造,无其他自定义 ✅ 自动生成默认移动 手写移动构造 / 移动赋值 ❌ 不生成默认拷贝构造、
C++ =default 与 =delete 完整详解
一、基础作用定位
=default:显式告诉编译器生成默认版本的默认成员函数=delete:显式告诉编译器禁用该函数,调用直接编译报错适用对象:构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值、运算符重载等成员函数。
二、
=default显式默认1. 核心功能
即使你类内写了其他自定义构造,仍强制编译器生成编译器默认版本的成员函数。
场景 1:自定义构造后,保留默认无参构造
cppclass Person { public: // 自定义带参构造,编译器原本不会生成默认无参构造 Person(string name) { _name = name; } // 强制生成默认无参构造 Person() = default; private: string _name; }; Person p1; // 合法,有default默认构造 Person p2("张三");场景 2:手动控制移动 / 拷贝函数生成
根据之前移动构造生成规则:只要手写析构 / 拷贝,编译器不会合成默认移动。用
=default强制生成:
cppclass Arr { public: int* _a; Arr(int n) { _a = new int[n]; } ~Arr() { delete[] _a; } // 手写了析构,编译器原本不生成移动构造,default强制生成 Arr(Arr&&) = default; Arr& operator=(Arr&&) = default; };场景 3:类内声明,类外 default
class Test { public: Test(); }; // 在类外指定生成默认版本 Test::Test() = default;2. 特性
=default只能用于编译器可自动合成的 6 大默认成员函数;普通自定义函数不能用。- 函数体为空、逻辑和编译器自动生成的完全一致:逐成员拷贝 / 移动。
- 相比手动写空函数
Test(){}有区别:
Test() = default:属于平凡函数 (trivial),编译器可做优化、内存零初始化;Test(){}:用户自定义函数,不再是平凡类型,优化受限。
三、
=delete删除函数(禁用)1. 核心功能
将函数定义为 "已删除",任何地方调用该函数直接触发编译错误,用于限制语法行为。
场景 1:禁用拷贝(只允许移动,或禁止复制对象)
cppclass UniqueFile { public: UniqueFile() = default; // 删除拷贝构造、拷贝赋值,禁止对象拷贝 UniqueFile(const UniqueFile&) = delete; UniqueFile& operator=(const UniqueFile&) = delete; }; UniqueFile f1; UniqueFile f2 = f1; // 编译报错:拷贝构造已被delete场景 2:禁用默认无参构造,强制必须带参创建
cppclass Person { public: // 删除无参构造,不允许 Person p; Person() = delete; Person(string n) {} }; Person p; // 报错 Person p2("李四"); // 合法场景 3:禁用移动语义(禁止 move 转移对象)
cppPerson(Person&&) = delete; Person& operator=(Person&&) = delete;场景 4:限制函数入参类型(拦截隐式转换)
cppvoid func(int x) { cout << "int" << endl; } // 禁用double版本,传浮点数直接报错 void func(double) = delete; func(10); // 正常 func(3.14); // 编译报错,double重载被delete场景 5:禁用取地址运算符(极少用)
cppclass Test { public: Test* operator&() = delete; const Test* operator&() const = delete; }; Test t; &t; // 编译报错,禁止取对象地址2. 关键规则
=delete可用于任意函数(普通成员、全局函数、重载运算符),不局限于 6 个默认成员;- 函数声明后加
=delete,整个程序任何调用都会报错,从编译期杜绝非法操作;- 替代旧方案:将拷贝构造私有化(
private:),delete报错信息更清晰,可读性更强。旧写法(C++98):
cppclass A { private: A(const A&); // 私有,外部调用报错,但友元仍能调用 };现代 C++ 推荐:
cppA(const A&) = delete; // 全局彻底禁用,所有场景调用都报错
四、
=defaultvs=delete对比表
语法 作用 使用范围 典型用途 func() = default;强制编译器生成默认内置版本 仅 6 大默认成员函数 保留默认构造、强制生成移动函数、保持类型平凡 func() = delete;删除函数,禁止调用 所有函数(构造 / 重载 / 普通
**💡**应试核心考点
=default只能用于编译器可合成的默认成员,普通函数不能使用;- 手写析构 / 拷贝会抑制默认移动,可用
=default强制生成移动函数;=delete可以彻底禁用函数,优于私有化拦截;=default空构造和手动(){}有差异:前者是平凡类型,内存优化更好;- 禁用拷贝 + 显式 default 移动,是实现独占资源类(类似 unique_ptr)的标准写法。
C++ final 与 override 关键字完整详解
一、
override:重写校验(修饰虚函数)1. 作用
明确标记该函数是重写父类虚函数,让编译器强制校验:如果子类函数签名和父类虚函数不匹配(无法构成重写),直接报编译错误,避免手写失误导致的隐蔽 bug。
2. 使用语法
只能写在虚函数声明末尾:
cppstruct Base { virtual void func() {} }; struct Son : Base { // 正确:签名完全匹配父类虚函数 void func() override {} };3. 典型错误场景(override 拦截问题)
错误 1:函数名拼写错误
cppstruct Son : Base { void fun() override {} // 编译报错:父类无fun虚函数,重写失败 };错误 2:参数列表不匹配
cppstruct Base { virtual void func(int x) {} }; struct Son : Base { void func() override {} // 报错:参数不一致,不构成重写 };错误 3:缺少 const 限定符
cppstruct Base { virtual void func() const {} }; struct Son : Base { void func() override {} // 报错:少const,签名不同 };错误 4:父类不是虚函数
cppstruct Base { void func() {} // 无virtual }; struct Son : Base { void func() override {} // 报错:没有可重写的虚函数 };4. 关键特性
- 仅修饰成员虚函数,不能修饰类、普通函数、构造 / 析构;
- 仅做编译期检查,不改变程序运行逻辑,纯语法安全工具;
- 建议所有子类重写虚函数时强制加上
override,是工程规范。
二、
final:两种用法(修饰类 / 修饰虚函数)用法 1:修饰虚函数 ------ 禁止子类继续重写该虚函数
父类虚函数加
final,派生类不能重写此函数,否则编译报错。
struct Base { virtual void func() final {} // 该函数锁死,后代不可重写 }; struct Son : Base { void func() override {} // 编译报错:func被final禁止重写 };用法 2:修饰类 ------ 禁止该类被继承
类名后加
final,任何类不能继承它,阻断继承链。
struct Base final {}; struct Son : Base {}; // 编译报错:Base是final类,禁止继承3. 使用限制
final修饰函数:仅能用于virtual虚函数末尾;
final修饰类:写在类定义的标识符后方;
final和override可同时修饰同一个虚函数(先 override,后 final):struct Base {
virtual void func() {}
};
struct Son : Base {
// 校验重写 + 禁止孙类再重写
void func() override final {}
};
struct GrandSon : Son {
void func() override {} // 报错:Son的func是final
};
三、
overridevsfinal核心对比表
关键字 修饰对象 核心作用 报错触发场景 override仅虚成员函数 校验是否合法重写父类虚函数 函数签名与父类虚函数不匹配 final1. 虚函数2. 整个类 1. 函数:禁止子类重写2. 类:禁止被继承 子类尝试重写 final 虚函数 / 尝试继承 final 类



