【C++】 C++11 知识点梳理(中)


C++ 可变参数模板(Variadic Template)

一、基础概念

普通模板只能固定参数个数,可变参数模板允许接收任意数量、任意类型的参数,核心由两部分组成:

  1. 参数包 Args...:省略号 ... 代表一堆类型 / 参数集合
  2. 解包 args...:展开参数包里所有参数

语法格式

cpp 复制代码
template<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 ...)

从右往左运算

cpp 复制代码
template<typename... Args>
void print(Args... args) {
    (cout << ... << args) << "\n";
}
print(10, 20, 30);
// 等价:std::cout << 10 << 20 << 30
2. 一元左折叠 (... op pack)
cpp 复制代码
template<typename... Args>
auto sum(Args... args) {
    return (... + args);
}
sum(1,2,3,4); // 1+2+3+4=10
3. 二元折叠(带初始值)
cpp 复制代码
template<typename... Args>
auto sum_init(Args... args) {
    return (0 + ... + args);
}
sum_init(); // 空包返回0,不会报错

三、参数包操作工具

1. sizeof... 求参数个数

cpp 复制代码
template<typename... Args>
void count(Args&&... args) {
    // sizeof... 专门获取参数包元素数量
    cout << sizeof...(args) << "\n";
}
count(1, 2, "abc"); // 输出3、3

2. 配合万能引用 + 完美转发(万能构造函数)

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. 初始化列表展开

cpp 复制代码
template<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);
}

五、关键区分易错点

  1. Args...:类型参数包;args...:函数参数包;... 位置决定是打包还是解包
  2. C++11/14 只能递归解包;C++17 折叠表达式大幅简化代码
  3. sizeof...() 是运算符,和普通 sizeof 完全不同,只用于参数包
  4. 可变模板不能单独使用,必须配合模板参数包声明 typename...
  5. 折叠表达式空包限制:一元折叠空包编译报错;二元折叠有初始值则安全

emplace 系列接口完整详解 (emplace/emplace_back/emplace_front)

一、核心定义与语法

1. 三个核心接口(C++11)

emplace_back(Args&&... args)容器尾部就地构造元素,对应 push_back

cpp 复制代码
template <class... Args>
void emplace_back(Args&&... args);

emplace(const_iterator pos, Args&&... args)指定迭代器位置就地构造,对应 insert

cpp 复制代码
template <class... Args>
iterator emplace(const_iterator position, Args&&... args);

emplace_front(Args&&... args)头部就地构造(仅 deque/list/forward_list 支持),对应 push_front

共性:

  • 全部是可变参数模板 + 万能引用,结合引用折叠、完美转发;
  • 参数不是容器元素对象,而是构造元素所需的参数。

二、emplace 和 push/insert 本质区别

push_back 逻辑:先构造,再拷贝 / 移动

cpp 复制代码
vector<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 是模板可变参数函数

cpp 复制代码
template<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 个默认成员函数

不手写时编译器自动生成:

  1. 默认构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值运算符重载
  5. 普通取地址运算符 operator&()
  6. const 取地址运算符 operator&() const核心常用:前 4 个;后两个几乎不用。

二、C++11 新增两个默认成员函数

  • 默认移动构造函数 T(T&&)
  • 默认移动赋值运算符 T& operator=(T&&)

1. 编译器何时自动生成「默认移动构造」

满足全部条件,编译器才会合成默认移动构造:

  1. 类没有用户自定义移动构造函数;
  2. 类没有用户自定义析构函数;
  3. 类没有用户自定义拷贝构造函数;
  4. 类没有用户自定义拷贝赋值运算符。

只要你手动实现了析构 / 拷贝构造 / 拷贝赋值任意一个,编译器不再生成默认移动构造。

默认移动构造的行为

逐成员移动(逐成员引用折叠):

  1. 内置类型(int、double、指针等):直接值拷贝(移动对内置无优化,等价浅拷贝);
  2. 自定义类成员:调用该成员自身的移动构造函数;若成员无移动构造,则降级调用该成员的拷贝构造。

三、关键补充规则(易考点)

规则 1:拷贝、移动互抑制

  1. 只要你写了拷贝构造 / 拷贝赋值,编译器不会生成默认移动构造 / 移动赋值;
  2. 只要你写了移动构造 / 移动赋值,编译器不会生成默认拷贝构造 / 拷贝赋值。

规则 2:有析构函数直接禁用默认移动

只要手动写了析构(哪怕是空析构),编译器直接放弃生成移动构造、移动赋值。

设计逻辑:手写析构通常代表类管理堆内存 / 资源,默认逐成员浅移动会导致双重释放 bug,编译器主动不生成,强制用户手动实现移动语义。

规则 3:默认移动是「逐成员移动」,浅移动风险

示例:动态数组类

cpp 复制代码
class 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:自定义构造后,保留默认无参构造
cpp 复制代码
class Person
{
public:
    // 自定义带参构造,编译器原本不会生成默认无参构造
    Person(string name) { _name = name; }
    // 强制生成默认无参构造
    Person() = default;
private:
    string _name;
};

Person p1;       // 合法,有default默认构造
Person p2("张三");
场景 2:手动控制移动 / 拷贝函数生成

根据之前移动构造生成规则:只要手写析构 / 拷贝,编译器不会合成默认移动。用 =default 强制生成:

cpp 复制代码
class 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. 特性

  1. =default 只能用于编译器可自动合成的 6 大默认成员函数;普通自定义函数不能用。
  2. 函数体为空、逻辑和编译器自动生成的完全一致:逐成员拷贝 / 移动。
  3. 相比手动写空函数 Test(){} 有区别:
    • Test() = default:属于平凡函数 (trivial),编译器可做优化、内存零初始化;
    • Test(){}:用户自定义函数,不再是平凡类型,优化受限。

三、=delete 删除函数(禁用)

1. 核心功能

将函数定义为 "已删除",任何地方调用该函数直接触发编译错误,用于限制语法行为。

场景 1:禁用拷贝(只允许移动,或禁止复制对象)

cpp 复制代码
class UniqueFile
{
public:
    UniqueFile() = default;
    // 删除拷贝构造、拷贝赋值,禁止对象拷贝
    UniqueFile(const UniqueFile&) = delete;
    UniqueFile& operator=(const UniqueFile&) = delete;
};

UniqueFile f1;
UniqueFile f2 = f1; // 编译报错:拷贝构造已被delete

场景 2:禁用默认无参构造,强制必须带参创建

cpp 复制代码
class Person
{
public:
    // 删除无参构造,不允许 Person p;
    Person() = delete;
    Person(string n) {}
};
Person p;    // 报错
Person p2("李四"); // 合法

场景 3:禁用移动语义(禁止 move 转移对象)

cpp 复制代码
Person(Person&&) = delete;
Person& operator=(Person&&) = delete;

场景 4:限制函数入参类型(拦截隐式转换)

cpp 复制代码
void func(int x) { cout << "int" << endl; }
// 禁用double版本,传浮点数直接报错
void func(double) = delete;

func(10);    // 正常
func(3.14);  // 编译报错,double重载被delete

场景 5:禁用取地址运算符(极少用)

cpp 复制代码
class Test
{
public:
    Test* operator&() = delete;
    const Test* operator&() const = delete;
};
Test t;
&t; // 编译报错,禁止取对象地址

2. 关键规则

  1. =delete 可用于任意函数(普通成员、全局函数、重载运算符),不局限于 6 个默认成员;
  2. 函数声明后加 =delete,整个程序任何调用都会报错,从编译期杜绝非法操作;
  3. 替代旧方案:将拷贝构造私有化(private:),delete 报错信息更清晰,可读性更强。

旧写法(C++98):

cpp 复制代码
class A
{
private:
    A(const A&); // 私有,外部调用报错,但友元仍能调用
};

现代 C++ 推荐:

cpp 复制代码
A(const A&) = delete; // 全局彻底禁用,所有场景调用都报错

四、=default vs =delete 对比表

语法 作用 使用范围 典型用途
func() = default; 强制编译器生成默认内置版本 仅 6 大默认成员函数 保留默认构造、强制生成移动函数、保持类型平凡
func() = delete; 删除函数,禁止调用 所有函数(构造 / 重载 / 普通

**💡**应试核心考点

  1. =default 只能用于编译器可合成的默认成员,普通函数不能使用;
  2. 手写析构 / 拷贝会抑制默认移动,可用 =default 强制生成移动函数;
  3. =delete 可以彻底禁用函数,优于私有化拦截;
  4. =default 空构造和手动 (){} 有差异:前者是平凡类型,内存优化更好;
  5. 禁用拷贝 + 显式 default 移动,是实现独占资源类(类似 unique_ptr)的标准写法。

C++ final 与 override 关键字完整详解

一、override:重写校验(修饰虚函数)

1. 作用

明确标记该函数是重写父类虚函数,让编译器强制校验:如果子类函数签名和父类虚函数不匹配(无法构成重写),直接报编译错误,避免手写失误导致的隐蔽 bug。

2. 使用语法

只能写在虚函数声明末尾:

cpp 复制代码
struct Base {
    virtual void func() {}
};

struct Son : Base {
    // 正确:签名完全匹配父类虚函数
    void func() override {}
};

3. 典型错误场景(override 拦截问题)

错误 1:函数名拼写错误
cpp 复制代码
struct Son : Base {
    void fun() override {} // 编译报错:父类无fun虚函数,重写失败
};
错误 2:参数列表不匹配
cpp 复制代码
struct Base {
    virtual void func(int x) {}
};
struct Son : Base {
    void func() override {} // 报错:参数不一致,不构成重写
};
错误 3:缺少 const 限定符
cpp 复制代码
struct Base {
    virtual void func() const {}
};
struct Son : Base {
    void func() override {} // 报错:少const,签名不同
};
错误 4:父类不是虚函数
cpp 复制代码
struct 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. 使用限制

  1. final 修饰函数:仅能用于 virtual 虚函数末尾;

  2. final 修饰类:写在类定义的标识符后方;

  3. finaloverride 可同时修饰同一个虚函数(先 override,后 final):

    struct Base {
    virtual void func() {}
    };
    struct Son : Base {
    // 校验重写 + 禁止孙类再重写
    void func() override final {}
    };
    struct GrandSon : Son {
    void func() override {} // 报错:Son的func是final
    };


三、override vs final 核心对比表

关键字 修饰对象 核心作用 报错触发场景
override 仅虚成员函数 校验是否合法重写父类虚函数 函数签名与父类虚函数不匹配
final 1. 虚函数2. 整个类 1. 函数:禁止子类重写2. 类:禁止被继承 子类尝试重写 final 虚函数 / 尝试继承 final 类

相关推荐
我不是懒洋洋1 小时前
【C++】string(string的成员变量、auto和范围for、string常用接口的说明、OJ题目、string的模拟实现)
c语言·开发语言·c++·visual studio
j7~1 小时前
【C++】STL--Vector容器--拆析解剖Vector的实现以及Vector的底层详解(2)
开发语言·c++·动态二维数组·vector深度剖析·vector的实现·杨辉三角形
旖-旎2 小时前
《LeetCode 130 被围绕的区域 FloodFill DFS 解法》
c++·算法·深度优先·力扣·floodfill
三品吉他手会点灯8 小时前
C语言学习笔记 - 50.流程控制4 - 流程控制为什么非常非常重要
c语言·开发语言·笔记·学习
一只旭宝10 小时前
【C++入门精讲22】常见设计模式
c++·设计模式
在放️11 小时前
Python 爬虫 · 第三方代理接入与合规使用
开发语言·爬虫·python
KANGBboy11 小时前
java知识五(继承)
java·开发语言
c++之路11 小时前
Bazel C++ 构建系列文档(三):构建第一个 C++ 项目
开发语言·c++
AI人工智能+电脑小能手11 小时前
【大白话说Java面试题 第117题】【并发篇】第17题:线程有几种状态,之间如何转换?
java·开发语言·面试