文章目录
可变参数模板
基本语法及原理
C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
cpp
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
用省略号来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class...或typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板⼀样,每个参数实例化时遵循引用折叠规则。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
这里可以使用sizeof...运算符去计算参数包中参数的个数。
cpp
#include<iostream>
using namespace std;
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print();
Print(1);
Print(1, string("xxxxx"));
// 包⾥有0个参数
// 包⾥有1个参数
// 包⾥有2个参数
Print(1.1, string("xxxxx"), x);
// 包⾥有3个参数
return 0;
}
// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
//
//
void Print();
template <class T1>
void Print(T1&& arg1);
// 原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能⽀持这⾥的功能,
// 有了可变参数模板,我们进⼀步被解放,他是类型泛化基础
// 上叠加数量变化,让我们泛型编程更灵活。
template <class T1, class T2>
void Print(T1 && arg1, T2 && arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
包扩展
对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供用于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层的实现细节如图所示。
C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处)

empalce系列接
cpp
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position, Args&&... args);
C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效⼀些,可以直接在容器空间上构造T对象。
emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列
第二个程序中我们模拟实现了list的emplace和emplace_back接口,这里把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前面说的empalce支持直接插入构造T对象的参数,这样有些场景会更高效⼀些,可以直接在容器空间上构造T对象。
传递参数包过程中,如果是Args&&... args 的参数包,要用完美转发参数包,⽅式如下std::forward(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左值。
vector 容器的 emplace_back vs push_back
cpp
#include <iostream>
#include <list>
#include <string>
class Student {
public:
std::string id;
std::string name;
Student(const std::string& id, const std::string& name) : id(id), name(name) {
std::cout << "构造: " << id << " " << name << std::endl;
}
};
int main() {
std::list<Student> students;
students.emplace_back("2023001", "小明");
students.emplace_back("2023003", "小红");
// 在 "2023001" 和 "2023003" 之间插入 "2023002 小刚"
auto it = students.begin();
++it; // 指向第二个元素(2023003)的位置
students.emplace(it, "2023002", "小刚"); // 直接传入构造参数
// 遍历输出
std::cout << "\n遍历结果:" << std::endl;
for (const auto& s : students) {
std::cout << s.id << " " << s.name << std::endl;
}
return 0;
}
emplace 示例:在指定位置构造对象
cpp
#include <iostream>
#include <list>
#include <string>
class Student {
public:
std::string id;
std::string name;
Student(const std::string& id, const std::string& name) : id(id), name(name) {
std::cout << "构造: " << id << " " << name << std::endl;
}
};
int main() {
std::list<Student> students;
students.emplace_back("2023001", "小明");
students.emplace_back("2023003", "小红");
// 在 "2023001" 和 "2023003" 之间插入 "2023002 小刚"
auto it = students.begin();
++it; // 指向第二个元素(2023003)的位置
students.emplace(it, "2023002", "小刚"); // 直接传入构造参数
// 遍历输出
std::cout << "\n遍历结果:" << std::endl;
for (const auto& s : students) {
std::cout << s.id << " " << s.name << std::endl;
}
return 0;
}
完美转发示例:实现一个简易的 emplace_back
cpp
#include <iostream>
#include <utility> // std::forward
#include <string>
template <class T>
class MyVector {
private:
struct Node {
T data;
template <class... Args>
Node(Args&&... args) : data(std::forward<Args>(args)...) {}
};
std::vector<Node*> nodes;
public:
// 模拟 emplace_back,使用完美转发参数包
template <class... Args>
void emplace_back(Args&&... args) {
// 把参数包转发给 Node 的构造函数,直接在堆上构造对象
nodes.push_back(new Node(std::forward<Args>(args)...));
}
~MyVector() {
for (auto node : nodes) delete node;
}
};
int main() {
MyVector<std::pair<int, std::string>> vec;
// 直接构造 pair<int, string>,无需创建临时对象
vec.emplace_back(1, "apple");
vec.emplace_back(2, "banana");
return 0;
}

新的类功能
默认的移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成⼀个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意⼀个。那么编译器会自动生成⼀个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意⼀个,那么编译器会自动生成⼀个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷贝构造和拷贝赋值。
默认生成移动构造 / 赋值的情况
cpp
#include <iostream>
#include <vector>
#include <string>
class Person {
public:
std::string name;
int age;
// 我们只写了构造函数,没写析构/拷贝/移动相关函数
Person(const std::string& name, int age) : name(name), age(age) {
std::cout << "构造函数: " << name << "\n";
}
// 析构函数也不写,让编译器默认生成
};
int main() {
Person p1("张三", 20);
std::cout << "--- 调用移动构造 ---\n";
Person p2 = std::move(p1); // 触发默认移动构造
std::cout << "p1.name: " << p1.name << "\n"; // 此时p1.name已被移动,为空
std::cout << "p2.name: " << p2.name << "\n";
Person p3("李四", 25);
std::cout << "--- 调用移动赋值 ---\n";
p2 = std::move(p3); // 触发默认移动赋值
std::cout << "p3.name: " << p3.name << "\n";
std::cout << "p2.name: " << p2.name << "\n";
return 0;
}
自定义析构后,编译器不再生成默认移动
cpp
#include <iostream>
#include <string>
class Person {
public:
std::string name;
int age;
Person(const std::string& name, int age) : name(name), age(age) {
std::cout << "构造函数: " << name << "\n";
}
// 自定义析构函数,触发规则:不再自动生成移动构造/赋值
~Person() {
std::cout << "析构函数: " << name << "\n";
}
};
int main() {
Person p1("张三", 20);
std::cout << "--- 尝试调用移动构造 ---\n";
Person p2 = std::move(p1);
// 此时不会生成默认移动构造,只能调用拷贝构造
std::cout << "p1.name: " << p1.name << "\n"; // p1.name 仍保留,是拷贝而非移动
std::cout << "p2.name: " << p2.name << "\n";
return 0;
}
自定义移动构造后,编译器不再生成拷贝构造
cpp
#include <iostream>
#include <string>
class Person {
public:
std::string name;
int age;
Person(const std::string& name, int age) : name(name), age(age) {
std::cout << "构造函数: " << name << "\n";
}
// 自定义移动构造
Person(Person&& other) noexcept : name(std::move(other.name)), age(other.age) {
std::cout << "自定义移动构造: " << name << "\n";
}
// 此时编译器不会再生成默认拷贝构造,需要我们手动写
Person(const Person& other) = delete; // 显式删除拷贝构造
};
int main() {
Person p1("张三", 20);
Person p2 = std::move(p1); // 调用自定义移动构造
// Person p3 = p2; // 编译错误:拷贝构造已被删除
return 0;
}
自定义移动构造 + 赋值,验证默认行为
cpp
#include <iostream>
#include <string>
class Person {
public:
std::string name;
int age;
Person(const std::string& name, int age) : name(name), age(age) {
std::cout << "构造: " << name << "\n";
}
// 自定义移动构造
Person(Person&& other) noexcept : name(std::move(other.name)), age(other.age) {
std::cout << "移动构造: " << name << "\n";
}
// 自定义移动赋值
Person& operator=(Person&& other) noexcept {
if (this != &other) {
name = std::move(other.name);
age = other.age;
std::cout << "移动赋值: " << name << "\n";
}
return *this;
}
// 手动写拷贝构造(否则编译器不会生成)
Person(const Person& other) : name(other.name), age(other.age) {
std::cout << "拷贝构造: " << name << "\n";
}
};
int main() {
Person p1("张三", 20);
Person p2 = std::move(p1); // 移动构造
Person p3("李四", 25);
p2 = std::move(p3); // 移动赋值
Person p4 = p2; // 拷贝构造(手动实现的)
return 0;
}

成员变量声明时给缺省值
C++11 及以后,成员变量可以直接在声明时给缺省值,这是一种非常方便的初始化方式
声明时直接赋值
cpp
#include <iostream>
#include <string>
class Person {
public:
// 声明时直接给缺省值
std::string name = "未知姓名";
int age = 18;
double score = 0.0;
};
int main() {
// 直接用默认构造创建对象,成员会自动使用缺省值
Person p;
std::cout << "姓名: " << p.name << "\n";
std::cout << "年龄: " << p.age << "\n";
std::cout << "分数: " << p.score << "\n";
return 0;
}
构造函数与缺省值的优先级
如果构造函数初始化列表也给了值,会覆盖声明时的缺省值。
cpp
#include <iostream>
#include <string>
class Person {
public:
// 声明时缺省值
std::string name = "未知姓名";
int age = 18;
// 构造函数:初始化列表的优先级更高
Person() : name("默认用户") {}
Person(std::string n) : name(n), age(20) {}
};
int main() {
Person p1; // 用无参构造,name="默认用户",age=18
Person p2("张三"); // 用带参构造,name="张三",age=20
std::cout << "p1: " << p1.name << ", " << p1.age << "\n";
std::cout << "p2: " << p2.name << ", " << p2.age << "\n";
return 0;
}
复杂类型 / 容器的缺省值
除了内置类型和 std::string,vector、自定义类等也支持声明时初始化
cpp
#include <iostream>
#include <vector>
#include <string>
class Student {
public:
// 容器直接初始化
std::vector<int> scores = {0, 0, 0};
std::string name = "匿名学生";
int id = 0;
// 自定义构造函数
Student() = default;
Student(std::string n, int i) : name(n), id(i) {}
};
int main() {
Student s1;
Student s2("小明", 1001);
std::cout << "s1: " << s1.name << ", id=" << s1.id << ", scores[0]=" << s1.scores[0] << "\n";
std::cout << "s2: " << s2.name << ", id=" << s2.id << ", scores[0]=" << s2.scores[0] << "\n";
return 0;
}
适用版本:C++11 及以上支持,老版本编译器可能不兼容。
静态成员:静态成员变量不能在声明时直接初始化(除了 const static 整型 / 枚举类型),必须在类外定义时初始化。
cpp
class A {
public:
// 合法:const static 整型可以声明时初始化
const static int MAX = 100;
// 不合法:非const静态成员
// static int count = 0;
static int count;
};
// 类外初始化静态成员
int A::count = 0;
defult和delete
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为⼀些原因这个函数没有默认生成。如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造⽣成。
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{
}
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{
}
Person(Person&& p) = default;
//Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
final与override
override:强制重写,编译期校验
作用:告诉编译器「我要重写基类的虚函数,请帮我检查」,如果签名不匹配就直接报错,避免写 bug。
示例:正确与错误用法
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual void func(int x) {
cout << "Base::func(int)\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
// 正确:签名和基类完全一致,override 校验通过
void func(int x) override {
cout << "Derived::func(int)\n";
}
// 错误:签名不匹配(参数从int变成了double),override 会直接报错
// void func(double x) override { }
};
int main() {
Derived d;
Base& b = d;
b.func(10); // 输出 Derived::func(int)
}
final:禁止重写 / 禁止继承
final 有两种用法:
- 修饰虚函数:禁止派生类重写
cpp
class Base {
public:
// 这个虚函数被 final 修饰,派生类不能再重写
virtual void func() final {
cout << "Base::func()\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
// 编译错误:func 被 final 禁止重写
// void func() override { }
};
修饰类:禁止被继承
cpp
// 这个类被 final 修饰,不能被任何类继承
class FinalClass final {
public:
void hello() { cout << "Hello\n"; }
};
// 编译错误:无法继承 final 类
// class Derived : public FinalClass { };
override + final 组合使用
最常见的场景:在派生类中重写虚函数,同时标记为 final,防止后续派生类继续重写。
cpp
class Base {
public:
virtual void func() { cout << "Base\n"; }
virtual ~Base() = default;
};
class Derived : public Base {
public:
// 用 override 确保正确重写,同时用 final 禁止后续派生类重写
void func() override final {
cout << "Derived\n";
}
};
class Derived2 : public Derived {
public:
// 编译错误:func 被 final 禁止重写
// void func() override { }
};

override 只能用于虚函数,非虚函数不能用。
final 修饰虚函数时,该函数的后续所有重写都会被禁止。
final 修饰类时,该类的所有成员都不能被继承,包括构造 / 析构函数
STL中一些变化
下图圈起来的就是STL中的新容器,但是实际最有用的是unordered_map和unordered_set。这两个前面已经进行了非常详细的讲解
stl unordered_map&set
STL中容器的新接口也不少,最重要的就是右值引用和移动语义相关的push/insert/emplace系列接口和移动构造和移动赋值,还有initializer_list版本的构造等,这些前面都讲过了,还有⼀些⽆关痛痒的如cbegin/cend等需要时查查⽂档即可。

总结
C++11 引入可变参数模板,通过模板参数包与函数参数包实现任意数量、类型的参数传递,结合sizeof...与包扩展完成递归处理,并依托std::forward完美转发,支撑emplace系列接口直接在容器内构造对象,提升效率。新增默认移动构造与移动赋值,未自定义拷贝、析构函数时自动生成,实现资源转移。支持成员变量声明赋缺省值,简化初始化。default显式要求生成默认函数,delete禁用函数。override校验虚函数重写,final禁止继承与重写。STL 新增右值引用、移动语义及unordered系列容器,大幅优化性能与编程效率。