《C++进阶之C++11》【可变参数模板 + emplace接口 + 新的类功能】

【可变参数模板 + empalce接口 + 新的类功能】目录

  • 前言:
  • [------------ 可变参数模板 ------------](#------------ 可变参数模板 ------------)
    • [1. 什么是可变参数模板?](#1. 什么是可变参数模板?)
    • [2. 怎么使用可变参数模板?](#2. 怎么使用可变参数模板?)
    • [3. 参数包的关键特性有哪些?](#3. 参数包的关键特性有哪些?)
    • [4. 参数包可以干什么?](#4. 参数包可以干什么?)
    • [5. 什么是参数包展开?](#5. 什么是参数包展开?)
    • [6. 怎么进行参数包的展开?](#6. 怎么进行参数包的展开?)
      • [6.1:递归展开(C++11 经典方式)](#6.1:递归展开(C++11 经典方式))
      • [6.2:折叠表达式(C++17 简化方式)](#6.2:折叠表达式(C++17 简化方式))
  • [------------ empalce系列接口 ------------](#------------ empalce系列接口 ------------)
    • [1. 什么是emplace系列接口?](#1. 什么是emplace系列接口?)
    • [2. emplace 系列接口是怎么实现的?](#2. emplace 系列接口是怎么实现的?)
    • [3. emplace 系列接口的核心优势是什么?](#3. emplace 系列接口的核心优势是什么?)
    • [4. emplace 系列接口为什么要实现完美转发?](#4. emplace 系列接口为什么要实现完美转发?)
    • [5. emplace 系列接口在list容器种的使用](#5. emplace 系列接口在list容器种的使用)
    • [6. 使用emplace系列接口有什么建议?](#6. 使用emplace系列接口有什么建议?)
  • [------------ 新的类功能 ------------](#------------ 新的类功能 ------------)
  • 一、默认函数
  • 二、缺省值
  • 三、defult和delete
  • 四、final与override
  • 五、STL新变化

往期《C++初阶》回顾:

《C++初阶》目录导航


往期《C++进阶》回顾:

/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】
【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】
【多态:概念 + 实现 + 拓展 + 原理】

/------------ STL ------------/
【二叉搜索树】
【AVL树】
【红黑树】
【set/map 使用介绍】
【set/map 模拟实现】
【哈希表】
【unordered_set/unordered_map 使用介绍】
【unordered_set/unordered_map 模拟实现】

/------------ C++11 ------------/
【列表初始化 + 右值引用】
【移动语义 + 完美转发】

前言:

"hi~小伙伴们大家......"​

评论区就传来一句带着点小委屈的质问(。•́︿•̀。) :"打住打住!你昨天怎么没按时更新呀?" 原来是位蹲点等内容的小伙伴,语气里满是 "没学够" 的着急。​

鼠鼠赶紧把食指凑到嘴边,压低声音 "嘘 ------ 嘘 ------" (。>﹏<。) 了两声,又飞快地从随身的小口袋里掏出一颗包装亮晶晶的水果糖,偷偷塞到这位小伙伴手里,眼睛弯成月牙:"别跟其他人说哦~这是迟到的小补偿,今天咱们把内容补得满满当当的!"​ (。•̀ᴗ-)✧ ʕ•̀ω•́ʔ♡
好啦,言归正传!今天咱们继续深入 C++11 的新特性,重点聊聊 【可变参数模板 + emplace 接口 + 新的类功能】 这三组 "提速小能手"。

鼠鼠一直觉得,C++ 每一次更新迭代,核心思路都离不开 "让代码跑得更快一点、让开发更灵活一点"------ 毕竟在实际开发里,效率和灵活性可是能帮咱们少走很多弯路的关键。(≧ڡ≦*)ゞ

而今天要学的这三个特性,正是把 "快" 和 "灵活" 贯彻得淋漓尽致的典型,咱们一个个拆开揉碎了讲明白! ٩(ˊᗜˋ)و ✧(≖‿‿≖✿) ʕ•̀ω•́ʔ✧

------------ 可变参数模板 ------------

1. 什么是可变参数模板?

可变参数模板(Variadic Templates) :是 C++11 引入的一项强大特性,允许模板(函数模板或类模板)接受数量不确定的参数

  • 这些参数可以是任意类型任意数量,极大地增强了模板的灵活性和表达能力
  • 这些参数被称为 "参数包",可以分为两大类

可变参数模板的核心是 "参数包" 的分类:

  • 模板参数包 :表示零个或多个模板参数类型
    • 例如class... Args
  • 函数参数包 :表示零个或多个函数调用参数
    • 例如Args... args

2. 怎么使用可变参数模板?

可变参数模板的基本语法声明形式

1. 类模板的可变参数声明

cpp 复制代码
//可变参数模板类:Tuple
//       1. 接收任意数量、任意类型的模板参数(如:Tuple<int, string, double>)
//       2. 底层使用 std::tuple 存储数据
template <class... Args>  //注意:Args是模板参数包
class Tuple 
{
private:
    std::tuple<Args...> _data;

    /* 数据成员:使用参数包实例化的 std::tuple
    *
    *  Args... 会被展开为具体类型列表
    *  如:Tuple<int, string> 会实例化为 std::tuple<int, string>
    */
public:
    Tuple(Args... args)   //注意:args是函数参数包
        : _data(args...)  //初始化列表:用参数包 args 初始化 std::tuple
    { }

    /* 构造函数:接收与模板参数匹配的任意数量实参
    *  注意:Args... args:函数参数包,对应模板参数包 Args... 的实例
    *
    *  参数包展开规则:
    *  若模板实例化为 Tuple<int, string>,则构造函数等价于:
    *  Tuple(int arg1, string arg2)
    *       :_data(arg1, arg2) 
    *        {}
    */
};

2. 函数模板的可变参数声明

cpp 复制代码
/*------------------值传递------------------*/
template <class... Args>
void Func(Args... args)  //注意:args是函数参数包

{
    // args 是函数参数包,可接收任意类型、任意数量的参数
}

/*------------------左值引用------------------*/
template <class... Args>
void Func(Args&... args)  //注意:args是函数参数包
{
    // 每个参数都是左值引用,需绑定到左值(如:变量)
}

/*------------------万能引用------------------*/
template <class... Args>
void Func(Args&&... args)  //注意:args是函数参数包
{
    // 每个参数都是万能引用(转发引用),可绑定左值或右值
}
  • class... Args :模板参数包,可接收任意数量的类型(intdoublestring 等)
  • Args... args :函数参数包,可接收与模板参数包类型匹配的任意数量实参
  • Args&... args :函数参数包的每个参数都是 左值引用,需绑定到左值(如:变量)
  • Args&&... args :函数参数包的每个参数都是 万能引用 (转发引用),可绑定左值/右值,结合引用折叠规则适配实参类型

3. 参数包的关键特性有哪些?

1. 编译期类型推导

编译器会根据传入的实参 ,自动推导模板参数包的具体类型

cpp 复制代码
Func(1, 2.3, "hello"); 

// 推导结果:
// Args = int, double, const char* 
// args = 1 (int), 2.3 (double), "hello" (const char*)

2. 引用折叠规则(针对万能引用包)

对于 Args&&... args 形式的万能引用参数包:

  • 传入左值(如:变量 a)→ Args 推导为 int& → 折叠为 int&(左值引用)
  • 传入右值(如:10std::move(a))→ Args 推导为 int → 折叠为 int&&(右值引用)

4. 参数包可以干什么?

1. 用 sizeof... 计算参数个数

  • sizeof...(Args) 或 sizeof...(args) 可在编译期计算参数包的元素数量:
cpp 复制代码
#include <iostream>
using namespace std;

template <class... Args>
void Func(Args... args)
{
    cout << "参数个数:" << sizeof...(args) << endl;
}


int main()
{
    // 调用:
    Func(1, 2.3, "hello");  // 输出:参数个数:3

    return 0;
}

2. 完美转发(std::forward + 可变参数包)

  • 结合std::forward + 可变参数包,精准转发任意参数
cpp 复制代码
#include <iostream>  
#include <string>  
#include <utility>  // 包含 std::move 和 std::forward 的必要头文件
using namespace std;

/*-------------------定义Object类-------------------*/
class Object 
{
private:
    int m_value;             //整型成员变量
    std::string m_str;       //字符串成员变量
    Object* m_ptr = nullptr; //对象指针成员,初始化为nullptr

public:
    //1.构造函数:接收多种类型参数
    Object(int value, const string& str, Object&& other)
        : m_value(value)
        , m_str(str)  //注:初始化m_str(调用string的拷贝构造函数)
    { 
        //1.处理右值参数:转移 other 的资源
        //1.1:接管other的资源
        m_ptr = other.m_ptr;

        //1.2:将other的资源置空,防止双重释放
        other.m_ptr = nullptr;

        cout << "Object 构造完成" << endl;
    }

    // 新增构造函数:支持用 nullptr 初始化 m_ptr
    Object(int value, const string& str, nullptr_t)
        : m_value(value)
        , m_str(str)
        , m_ptr(nullptr)  //注:显式将指针成员初始化为 nullptr
    {
        cout << "Object 构造完成(nullptr 版本)" << endl;
    }

    //2.析构函数
    ~Object() 
    {
        delete m_ptr; //注意:m_str是栈对象,会自动调用其析构函数
    }
};


/*-------------------全局对象指针-------------------*/
Object* obj = nullptr; //实际应用中可能是 "类成员变量"


/*-------------------可变参数模板函数-------------------*/

template <class... Args>
void Emplace(Args&&... args)
{
    obj = new Object(std::forward<Args>(args)...);  //完美转发参数构造 Object 对象

    /* 关键逻辑详解:
    *  1. std::forward<Args>(args)...: 
    *     - 这是一个参数包展开,对每个参数应用std::forward
    *     - 保持参数的原始值类别(左值/右值)
    *     - 展开后相当于:std::forward<Arg1>(arg1), std::forward<Arg2>(arg2), ...
    *  
    *  2. new Object(...):
    *     - 直接在堆上构造Object对象
    *     - 避免了创建临时对象再拷贝的开销
    *     - 完美转发确保调用最匹配的构造函数
    */
}


int main()
{
    //1.准备参数
    Object tempObj(5, "temp", nullptr);

 
    //2.调用 Emplace,参数会按原始属性转发:
    Emplace(10, "hello", move(tempObj));
    /* 参数转发分析:
    *   - 10:                右值(字面量)→ 转发为 int&&
    *   - "hello":           右值(const char[6]转换为临时string)→ 转发为 string&&
    *   - std::move(tempObj):显式转换为右值 → 转发为 Object&&
    *
    * 注意:调用后tempObj的资源已被转移,不应再使用
    */


    //3.使用 obj...

    //4.释放内存
    delete obj;  // 实际应用中建议使用unique_ptr或shared_ptr管理内存

    return 0;
}

5. 什么是参数包展开?

对于 参数包 (模板参数包或函数参数包),最核心的操作是 "展开"

除了用 sizeof... 计算参数个数外,参数包的主要价值体现在按规则分解为单个元素,并对每个元素应用相同逻辑。


参数包展开 :是 C++ 可变参数模板的核心操作,用于将参数包(Args...args...)分解为独立的元素,并对每个元素应用相同的处理逻辑。

在可变参数模板中,参数包 是一个未展开的参数集合 (类似 "黑盒"),必须通过特定语法显式展开才能使用。


参数包展开的本质是:

  1. 分解参数包 :将参数包(如:Args...args...)拆分为独立的类型/值
    • (如:int, double, string1, 2.3, "hello"
  2. 应用统一模式 :为每个分解出的元素定义同的处理逻辑
    • (如:函数调用、运算符操作)

6. 怎么进行参数包的展开?

参数包扩展的过程可拆解为:

  • 分解参数包:将参数包拆分为独立的元素。

    • (如:Args... 拆为 arg1, arg2, ..., argN
  • 应用模式:为每个元素定义统一的 "处理模式"。

    • (如:函数调用、运算符操作 )
  • 触发扩展 :通过在模式右侧添加 ...(省略号),告知编译器对参数包逐个元素应用模式,生成扩展后的代码。


参数包扩展的方式:

  • 递归展开(C++11 经典方式)
  • 折叠表达式(C++17 简化方式)

6.1:递归展开(C++11 经典方式)

(1)递归展开(C++11 经典方式)

  • 通过 递归调用逗号表达式,可逐个处理参数包的元素(C++17 后支持更简洁的折叠表达式):
cpp 复制代码
#include <iostream>
using namespace std;

/*------------------递归终止函数------------------*/
template <class T>
void Print(T arg)
{
    cout << arg << endl;  //注:输出最后一个参数并换行
}

/* 注意事项:处理最后一个参数(仅剩一个参数时调用)
* 
*     1. 模板参数 T:捕获最后一个参数的类型
*     2. 参数 arg:最后一个实参的值
*/



/*------------------递归展开函数------------------*/
template <class T, class... Args>
void Print(T arg, Args... args)
{
    Print(arg);          // 1. 处理当前第一个参数(递归调用终止函数)
    Print(args...);      // 2. 递归处理剩余参数包(展开为 Print(arg1, arg2, ...))
}

/*------------------主函数------------------*/
int main()
{
    Print(1, 2.3, "hello");
    return 0;
}


/* 注意事项:处理多个参数
*
*     1. 模板参数 T:        捕获第一个参数的类型
*     2. 模板参数包 Args...:捕获剩余参数的类型列表
* 
*     3. 参数 arg:          第一个实参的值
*     4. 参数包 args...:    剩余实参的值列表
*/

// 调用示例:Print(1, 2.3, "hello");
// 编译器实例化过程:
// 1. 初始调用:Print<int, double, const char*>(1, 2.3, "hello")
//    - 匹配多参数版本,实例化为:
//      void Print(int arg, double, const char*) 
//     {
//        Print(arg);           // 调用 Print<int>(1)
//        Print(2.3, "hello");  // 递归调用剩余参数
//      }
//
// 2. 第一次递归:Print<double, const char*>(2.3, "hello")
//    - 匹配多参数版本,实例化为:
//      void Print(double arg, const char*) 
//      {
//        Print(arg);            // 调用 Print<double>(2.3)
//        Print("hello");        // 递归调用剩余参数
//      }
//
// 3. 第二次递归:Print<const char*>("hello")
//    - 匹配终止函数,实例化为:
//      void Print(const char* arg) 
//      {
//        cout << arg << endl;  // 输出 "hello"
//      }

实例化流程 :以 Print(1, 2.3, "hello") 为例

递归层级 函数实例化 参数传递 执行动作
1 Print<int, double, const char*> arg=1, args=2.3, "hello" 输出 1, 递归调用 Print(2.3, "hello")
2 Print<double, const char*> arg=2.3, args="hello" 输出 2.3, 递归调用 Print("hello")
3 Print<const char*> arg="hello" 输出 hello,终止递归

代码案例 2:参数包的递归展开

cpp 复制代码
/*----------------------------- 通过递归展开参数包实现任意参数打印 -----------------------------*/
#include <iostream>
#include <string>
using namespace std;

// ====================== 错误示例(说明参数包不能直接遍历) ======================
// 以下代码演示:参数包无法通过运行时循环遍历(编译时特性 vs 运行时逻辑)

/*
*   template <class... args>
*   void print(args... args)
*   {
*       // 错误:sizeof...(args) 是编译期常量,但 args 不是数组,无法用 [] 访问
*       cout << sizeof...(args) << endl; 
*       for (size_t i = 0; i < sizeof...(args); ++i)
*       {
*           cout << args[i] << " ";  // 编译报错:参数包不是容器,无 [] 运算符
*       }
*   }
*/




// ====================== 递归展开:ShowList 系列 ======================
//1. 递归终止条件:参数包为空时调用(匹配无参数的情况)
void ShowList()
{
    cout << endl;  // 换行,结束递归打印
}

//2. 递归展开函数:每次处理参数包的第一个元素,剩余参数继续递归
template <class T, class... Args>
void ShowList(T x, Args... args)
{

    //1.打印当前第一个参数
    cout << x << " ";  

    //2.递归调用:将剩余参数包 args... 继续传递给 ShowList,触发下一层展开
    ShowList(args...);
}


// ====================== 封装 Print 接口 ======================
template <class... Args>
void Print(Args... args)
{
    //1.调用 ShowList 展开参数包,实现任意参数打印
    ShowList(args...);
}


// ====================== main 函数测试 ======================
int main()
{
    // 测试不同参数数量的调用:
    Print();                       // 无参数 → 直接换行
    Print(1);                      // 1个参数 → ShowList(1) → ShowList()
    Print(1, string("xxxxx"));     // 2个参数 → ShowList(1, "xxxxx") → ShowList("xxxxx") → ShowList()
    Print(1, string("xxxxx"), 2.2);// 3个参数 → 逐层展开

    return 0;
}

/*
// ====================== 编译器实例化过程(伪代码演示) ======================
// 当调用 Print(1, string("xxxxx"), 2.2) 时,编译器会自动生成以下展开:

//1. 最外层 Print 调用:
void Print(int, string, double)
{
	ShowList(1, string("xxxxx"), 2.2);
}

//2. 第一次 ShowList 展开(处理 int):
void ShowList(int x, string, double)
{
	cout << x << " ";  // 输出: 1
	ShowList(string("xxxxx"), 2.2);
}

//3. 第二次 ShowList 展开(处理 string):
void ShowList(string x, double)
{
	cout << x << " ";  // 输出: xxxxx
	ShowList(2.2);
}

//4. 第三次 ShowList 展开(处理 double):
void ShowList(double x)
{
	cout << x << " ";  // 输出: 2.2
	ShowList();
}

//5. 递归终止:
void ShowList()
{
	cout << endl;  // 输出换行
}
*/

6.2:折叠表达式(C++17 简化方式)

(2)折叠表达式(C++17 简化方式)

  • 利用 ... 折叠表达式,无需递归即可展开参数包:

    cpp 复制代码
    #include <iostream>
    #include <string>
    using namespace std;
            
    /* 可变参数模板函数:使用折叠表达式展开参数包
    *
    *      1. Args...:模板参数包,接收任意数量、任意类型的参数
    *      2. args...:函数参数包,对应模板参数包的实例
    */
    template <class... Args>
    void Print(Args... args)
    {
        (cout << ... << args) << endl;
            
        /*  折叠表达式:展开参数包并通过 << 运算符连接
        *   展开过程:(cout << arg1 << arg2 << ... << argN) << endl;
        *
        *   注意:需要确保所有参数类型都支持 << 运算符(如:内置类型、string 等)
        */
    }
            
    int main()
    {
        /*----------------混合类型参数----------------*/
        Print(1, " ", 2.3);   // 输出:1 2.3
        /* 调用示例:参数类型包括 int、const char*、double
        *
        *  展开结果:(cout << 1 << " " << 2.3) << endl;
        */
            
        string name = "Alice";
        Print("Name:", name, ", Age:", 20);  // 输出:Name: Alice, Age: 20
          /*----------------空参数调用----------------*/
          Print();  // 输出空行
    
        return 0;
                  }

/---------- 折叠表达式(简单介绍)----------/

1. 折叠表达式语法:

折叠表达式是 C++17 的特性,用于简化可变参数模板的展开:

  • 一元右折叠:

    cpp 复制代码
    (expr1 << ... << exprN)
    • 展开顺序 :从左到右,等价于 ((expr1 << expr2) << ... << exprN)
  • 一元左折叠:

    cpp 复制代码
    (... << expr)
    • 展开顺序 :从右到左,等价于 (expr1 << (expr2 << ... << exprN))

2. 参数包展开过程

Print(1, " ", 2.3) 为例:

  1. 模板参数包 Args 推导为 int, const char*, double

  2. 函数参数包 args 对应为 1, " ", 2.3

  3. 折叠表达式

    cpp 复制代码
    (cout << ... << args)

    展开为:

    cpp 复制代码
    ((cout << 1) << " ") << 2.3
  4. 最终输出:1 2.3


3. 类型要求

折叠表达式要求所有参数类型都支持 << 运算符:

  • 内置类型 (如:intdoublechar*
  • 标准库类型 (如:std::stringstd::vector 等,需重载 <<
  • 自定义类型 (需显式重载 << 运算符)

4. 与递归展开的对比

与传统递归展开方式(C++11/14)相比折叠表达式(C++11)的优势:

  • 代码更简洁 ---> 无需手动编写递归终止函数
  • 展开效率更高 ---> 编译期一次性展开,而非递归调用

------------ empalce系列接口 ------------

1. 什么是emplace系列接口?

emplace系列接口 :是 C++11 为 STL 容器新增的高效插入操作接口,用于直接在容器内部构造元素,而非先构造再拷贝或移动。

  • 核心解决 "就地构造对象" 的问题,避免额外的 拷贝/移动 开销
  • 这些接口提供了更高的效率,特别是在处理复杂对象时

emplace(原位构造)系列接口允许你:

  • 直接在容器内存中构造对象
  • 避免不必要的拷贝或移动操作
  • 完美转发参数给元素的构造函数

2. emplace 系列接口是怎么实现的?

一、核心定义:可变参数模板接口

emplace 系列接口通过可变参数模板 实现,典型声明(以 vector 为例):

cpp 复制代码
// 1. 在容器末尾构造元素(类似 push_back)
template <class... Args> 
void emplace_back (Args&&... args);  

// 2. 在指定位置构造元素(类似 insert)
template <class... Args> 
iterator emplace (const_iterator position, Args&&... args);  
  • Args&&... args:万能引用参数包,接收任意类型、任意数量的参数

核心功能:直接用参数构造容器元素,而非先构造元素再插入。

3. emplace 系列接口的核心优势是什么?

二、emplace 系列的核心优势

emplace 系列接口通过可变参数模板 + 完美转发,实现两大关键特性:

1. 兼容 push/insert 功能并且更高效

  • 功能覆盖emplace_back 可替代 push_backemplace 可替代 insert
  • 效率提升直接在容器内存中构造对象,跳过临时对象的 拷贝/移动

2. 支持 "参数直接构造"

  • 对于容器 container<T>emplace 系列允许直接传入构造 T 对象所需的参数 ,而非 T 类型的对象本身。

    cpp 复制代码
    std::vector<std::string> vec;
            
    // push_back:需先构造临时 string 对象
    vec.push_back(std::string("hello"));  
            
    // emplace_back:直接传入构造参数,容器内直接构造 string
    vec.emplace_back("hello");  

三、高效的本质:就地构造

传统 push/insert 流程:

  • 构造临时对象拷贝/移动到容器销毁临时对象

    cpp 复制代码
    //传统 push_back/insert 流程:
            
    std::vector<std::string> vec;
    vec.push_back(std::string("hello"));  
    /* 步骤:
    *     1. 构造临时 string 对象("hello")
    *     2. 拷贝/移动到容器内存
    *	  3. 销毁临时对象
    */

emplace 系列流程:

  • 直接在容器内存中构造对象(跳过临时对象的额外开销)

    cpp 复制代码
    //emplace_back 流程:
            
    vec.emplace_back("hello");  
    /* 步骤:
    * 	直接在容器内存中构造 string 对象(用 "hello" 作为参数)
    */

差异emplace 跳过 "临时对象构造 → 拷贝 → 销毁" 的额外开销,尤其适合构造复杂对象时提升效率。

4. emplace 系列接口为什么要实现完美转发?

四、完美转发的必要性:

emplace 系列通过 Args&&... args(万能引用参数包)接收参数。

emplace 系列通过 std::forward<Args>(args)... 实现完美转发。确保:

  • 左值参数保持左值属性(调用拷贝构造)
  • 右值参数保持右值属性(调用移动构造)

emplace 系列需配合 std::forward<Args>(args)... 实现完美转发,传递参数包时需用 std::forward 保留值类别,确保右值属性不丢失。

若不完美转发,右值引用参数会退化为左值,导致构造时调用拷贝构造(而非移动构造),失去性能优势。

cpp 复制代码
template <class... Args> 
void emplace_back (Args&&... args) 
{
    // 关键:用 forward 保留参数的左值/右值属性
    construct_in_place(std::forward<Args>(args)...);  
}

5. emplace 系列接口在list容器种的使用

头文件:List.h

cpp 复制代码
#include <iostream>
#include <utility>  // 用于 std::move、std::forward
using namespace std;

namespace mySpace 
{
    /*------------------------"链表节点"模板类------------------------*/
    template<class T>
    struct ListNode 
    {
        /*----------------成员变量----------------*/
        //1.指向下一个节点的指针
        //2.指向前一个节点的指针
        //3.存储节点数据

        ListNode<T>* _next;  
        ListNode<T>* _prev;  
        T _data;            

        /*----------------成员函数----------------*/
        //1.实现:"拷贝构造节点的构造函数"(接收左值引用)
        ListNode(T& data)
            : _next(nullptr)
            , _prev(nullptr)
            , _data(data)  // 直接拷贝数据
        { }

        //2.实现:"移动构造节点的构造函数" (接收右值引用)
        ListNode(T&& data)
            : _next(nullptr)
            , _prev(nullptr)
            , _data(std::move(data))  // 移动语义,减少拷贝
        { }

        //3.实现:"可变参数模板构造函数"(用于 emplace 系列接口,完美转发参数构造数据)
        template <class... Args>
        ListNode(Args&&... args)
            : _next(nullptr)
            , _prev(nullptr)
            , _data(std::forward<Args>(args)...) //注意:完美转发参数包,直接在节点中构造数据对象
        { }
    };

    /*------------------------"链表迭代器"模板类------------------------*/
    template<class T, class Ref, class Ptr>
    struct ListIterator 
    {
        /*----------------类型别名----------------*/
        //1.重命名"链表的节点"的类型:ListNode<T> ---> Node
        using Node = ListNode<T>;               
        //2.重命名"链表的迭代器"的类型:ListIterator<T,Ref,Ptr> ---> Self
        using Self = ListIterator<T, Ref, Ptr>;  


        /*----------------成员变量----------------*/
        //1.指向当前节点的指针
        Node* _node;  


        /*----------------成员函数----------------*/
        //1.实现:"迭代器的构造函数"
        ListIterator(Node* node)
            : _node(node)
        {
        }

        //2.实现:"前置++运算符重载"
        Self& operator++() //目的:用于迭代器向后移动
        {
            _node = _node->_next;
            return *this;
        }

        //3.实现:"前置--运算符重载"
        Self& operator--() //目的:用于迭代器向前移动
        {
            _node = _node->prev;
            return *this;
        }

        //4.实现:"解引用运算符重载"
        Ref operator*()  //注意:返回节点数据的引用(支持 const/非const 场景)
        {
            return _node->_data;
        }

        //5.实现:"箭头运算符"
        Ptr operator->()  //目的:用于通过迭代器访问数据成员(常配合指针类型数据使用)
        {
            return &_node->_data;
        }

        //6.实现:"不等于比较"
        bool operator!=(const Self& it) const  //目的:判断迭代器是否指向不同节点
        {
            return _node != it._node;
        }
    };


    /*------------------------"链表容器"模板类------------------------*/
    template<class T>
    class list 
    {
    private:
        /*------------------类型别名------------------*/
        //1.重命名链表节点的类型:ListNode<T> ---> Node
        using Node = ListNode<T>;

        /*------------------成员变量------------------*/
        Node* _head;  //指向哨兵头节点的指针

    public:
        /*------------------类型别名------------------*/
        //1.重命名链表的"普通迭代器"的类型:ListIterator<T, T&, T*> ---> iterator
        using iterator = ListIterator<T, T&, T*>;

        //2.重命名链表的"常量迭代器"的类型:ListIterator<T, const T&, const T*> ---> const_iterator
        using const_iterator = ListIterator<T, const T&, const T*>;


        /*------------------成员函数(公有)------------------*/

        //1.实现:"获取起始位置的迭代器"
        iterator begin()
        {
            return iterator(_head->_next);//哨兵头节点的 next 指向第一个有效节点
        }

        //2.实现:"获取末尾位置的迭代器"
        iterator end()
        {
            return iterator(_head);      //末尾迭代器指向哨兵头节点
        }


        // 3.实现:"初始化空链表"
        void empty_init()
        {
            //核心:创建哨兵头节点,自己指向自己
            _head = new Node();
            _head->_next = _head;
            _head->_prev = _head;
        }

        //4.实现:"构造函数"
        list()  //初始化空链表(哨兵头节点)
        {
            empty_init();
        }

        //5.实现:"const T& 版本"的尾插
        void push_back(const T& x)  //适配左值,调用拷贝构造
        {
            insert(end(), x);
        }

        //6.实现:"T&& 版本"的尾插
        void push_back(T&& x)  //适配右值,调用移动构造
        {
            insert(end(), move(x));  //注意:显式将"右值"x转为"右值引用",触发移动逻辑
        }

        //7.实现:"const T& 版本"的插入
        iterator insert(iterator pos, const T& x)  //在 pos 位置插入"左值",拷贝构造新节点
        {
            //1.使用cur指针指向"插入的位置"
            Node* cur = pos._node;
            //2.使用newNode指针指向"新创建的节点"
            Node* newNode = new Node(x);  //用拷贝构造创建新节点
            //3.使用prev指针指向"插入位置前面的节点"
            Node* prev = cur->_prev;

            //4.调整指针:将新节点接入链表
            prev->_next = newNode;
            newNode->_prev = prev;

            newNode->_next = cur;
            cur->_prev = newNode;

            //5.返回指向新节点的迭代器
            return iterator(newNode);
        }

        //8.实现:"T&& 版本"的插入
        iterator insert(iterator pos, T&& x)  //在 pos 位置插入"右值",移动构造新节点
        {
            //1.使用cur指针指向"插入的位置"
            Node* cur = pos._node;
            //2.使用newNode指针指向"新创建的节点"
            Node* newNode = new Node(move(x));  //用移动构造创建新节点,转移 x 的资源
            //3.使用prev指针指向"插入位置前面的节点
            Node* prev = cur->_prev;


            //4.调整指针:将新节点接入链表
            prev->_next = newNode;
            newNode->_prev = prev;

            newNode->_next = cur;
            cur->_prev = newNode;


            //5.返回指向新节点的迭代器
            return iterator(newNode);
        }

        //9.实现:"emplace_back接口函数" 
        template <class... Args>
        void emplace_back(Args&&... args)  //核心实现:"可变参数模板" + "完美转发参数直接构造节点"
        {
            //1.调用 insert,在末尾位置用完美转发的参数构造节点
            insert(end(), forward<Args>(args)...);

        }

        //添加特化版本
        template <class K, class V>
        void emplace_back(K&& key, V&& value) 
        {
            insert(end(), std::pair<K, V>( 
                std::forward<K>(key),
                std::forward<V>(value) 
            ));
        }

        //10.实现:"emplace接口函数" 
        // 
        template <class... Args>
        iterator emplace(iterator pos, Args&&... args)  //(简化版,完整 STL 还会有更多重载等,这里演示关键逻辑)
        {
            //1.调用 insert,在指定位置用完美转发的参数构造节点
            return insert(pos, T(forward<Args>(args)...));
        }

    };
}

测试文件:Test.cpp

cpp 复制代码
#include "List.h"  
#include <string>  
using namespace std;

int main() 
{
    // -------------------- 测试 string 类型链表 --------------------
    cout << "-------------------- 测试 string 类型链表 --------------------" << endl;
    mySpace::list<string> lt1;

    // 1. 传左值,类似 push_back 的"拷贝构造"语义
    string s1("1111111111");
    lt1.emplace_back(s1);
    cout << "********** 左值构造(类似 push_back 拷贝) **********" << endl;
    for (auto& e : lt1) 
    {
        cout << e << " ";
    }
    cout << endl << endl;

    // 2. 传右值,类似 push_back 的"移动构造"语义
    lt1.emplace_back(move(s1));
    cout << "********** 右值构造(类似 push_back 移动) **********" << endl;
    for (auto& e : lt1) 
    {
        cout << e << " ";
    }
    cout << endl << endl;

    // 3. 直接传构造 string 的参数包,这是 push_back 做不到的!
    //注意:emplace_back 会直接用 "1111111111" 构造节点里的 string
    lt1.emplace_back("1111111111");
    cout << "********** 直接传参数包构造 string **********" << endl;
    for (auto& e : lt1) 
    {
        cout << e << " ";
    }
    cout << endl << endl;




    // -------------------- 测试 pair<string, int> 类型链表 --------------------
    cout << "-------------------- 测试 pair<string, int> 类型链表 --------------------" << endl;
    mySpace::list<pair<string, int>> lt2;
    
    // 1. 传左值 pair,类似 push_back 的"拷贝构造"语义
    pair<string, int> kv("苹果", 1);
    lt2.emplace_back(kv);
    cout << "********** 左值构造 pair(类似 push_back 拷贝) **********" << endl;
    for (auto& e : lt2) 
    {
        cout << e.first << " : " << e.second << " ";
    }
    cout << endl << endl;

    // 2. 传右值 pair,类似 push_back 的"移动构造"语义
    lt2.emplace_back(move(kv));
    cout << "********** 右值构造 pair(类似 push_back 移动) **********" << endl;
    for (auto& e : lt2)
    {
        cout << e.first << " : " << e.second << " ";
    }
    cout << endl << endl;

    // 3. 直接传构造 pair 的参数包("苹果", 1),push_back 无法直接做到!
    //注意:emplace_back 会直接用这两个参数构造节点里的 pair
    lt2.emplace_back("苹果", 1);
    cout << "********** 直接传参数包构造 pair **********" << endl;
    for (auto& e : lt2) 
    {
        cout << e.first << " : " << e.second << " ";
    }
    cout << endl << endl;

    return 0;
}

6. 使用emplace系列接口有什么建议?

1.优先使用emplace 系列接口

  • emplace_back 替代 push_back
  • emplace 替代 insert

2. 注意完美转发的使用 :传递参数包时需用 std::forward<Args>(args)...


3. 场景适配

  • 对已构造的对象vec.push_back(obj)),pushemplace 效率差异小
  • 对直接传构造参数vec.emplace_back(10, "a")),emplace 优势显著

------------ 新的类功能 ------------

一、默认函数

在 C++ 类体系里,默认成员函数的规则可梳理如下:

一、默认成员函数的基础构成与演进

C++ 类原本包含 4 个默认成员函数

  • 构造函数、析构函数
  • 拷贝构造函数、拷贝赋值重载

到了 C++11 标准,新增 移动构造函数移动赋值运算符重载 ,进一步丰富了类的资源转移能力。


二、默认移动构造函数的生成规则

编译器自动生成默认移动构造函数,需同时满足两个条件:

  1. 类中 没有手动实现移动构造函数
  2. 类中 没有手动实现析构函数拷贝构造函数拷贝赋值重载函数 (这三者只要有一个手动实现,就会抑制默认移动构造的生成 )

默认移动构造函数的行为:

  • 内置类型成员intdouble指针等):直接按字节 "转移"(本质是浅拷贝,若涉及动态资源需注意风险 )
  • 自定义类型成员 :检查该成员是否实现了移动构造
    • 若实现,则调用其移动构造
  • 若未实现,则退而调用其拷贝构造

三、默认移动赋值运算符重载的生成规则

编译器自动生成默认移动赋值运算符重载,条件与移动构造类似:

  1. 类中 没有手动实现移动赋值重载函数
  2. 类中 没有手动实现析构函数拷贝构造函数拷贝赋值重载函数 (任一手动实现,都会抑制默认移动赋值的生成 )

默认移动赋值的行为:

  • 内置类型成员:逐成员按字节 "转移"(浅拷贝逻辑 )

  • 自定义类型成员:检查该成员是否实现了移动赋值

    • 若实现,则调用其移动赋值
  • 若未实现,则调用其拷贝赋值

:整体逻辑与默认移动构造高度相似,只是操作对象从 "构造新对象" 变为 "赋值给已有对象"


四、移动语义与拷贝语义的互斥性

若手动提供了 移动构造函数移动赋值重载函数 ,编译器会认为你要自己控制资源的 "转移逻辑",因此 不会再自动生成默认的拷贝构造函数和拷贝赋值重载函数

(反之,若手动实现了拷贝语义相关函数,默认移动语义函数也不会自动生成,需开发者按需抉择 。)


代码案例:"移动构造"和"移动赋值"的生成条件

cpp 复制代码
#include <iostream>
#include <utility>  // 用于std::move
using namespace std;

namespace mySpace
{
    class string
    {
    private:
        // 存储字符串内容
        const char* _str;

    public:
        // 1. 构造函数(带缺省参数)
        string(const char* str = "")
            : _str(str)
        {
            cout << "mySpace::string 构造函数" << endl;
        }

        // 2. 拷贝构造函数
        string(const string& other)
            : _str(other._str)  // 浅拷贝指针
        {
            cout << "mySpace::string 拷贝构造" << endl;
        }

        // 3. 移动构造函数
        string(string&& other) noexcept
            : _str(move(other._str))
        {
            other._str = nullptr;  // 避免悬挂指针
            cout << "mySpace::string 移动构造" << endl;
        }

        // 4. 拷贝赋值运算符
        string& operator=(const string& other)
        {
            if (this != &other)
            {
                _str = other._str;
                cout << "mySpace::string 拷贝赋值" << endl;
            }
            return *this;
        }

        // 5. 移动赋值运算符
        string& operator=(string&& other) noexcept
        {
            if (this != &other)
            {
                _str = move(other._str);
                other._str = nullptr;  // 避免悬挂指针
                cout << "mySpace::string 移动赋值" << endl;
            }
            return *this;
        }

        // 6. 析构函数
        ~string()
        {
            cout << "mySpace::string 析构函数" << endl;
        }
    };
}

class Person
{
private:
    mySpace::string _name;  // 字符串类型的姓名
    int _age;               // 整型的年龄

public:
    // 1. 构造函数(带缺省参数)
    Person(const char* name = "", int age = 0)
        : _name(name)  // 调用mySpace::string的构造函数
        , _age(age)    // 直接初始化int成员
    {
        cout << "Person 构造函数" << endl;
    }

    // 2. 拷贝构造函数
    //注意:根据规则,手动实现拷贝构造会抑制默认移动构造的生成
    Person(const Person& p)
        : _name(p._name)  // 调用mySpace::string的拷贝构造
        , _age(p._age)    // 拷贝int成员
    {
        cout << "Person 拷贝构造" << endl;
    }

    // 3. 拷贝赋值运算符
    //注意:根据规则,手动实现拷贝赋值会抑制默认移动赋值的生成
    Person& operator=(const Person& p)
    {
        if (this != &p)  // 防止自赋值
        {
            _name = p._name;  // 调用mySpace::string的拷贝赋值
            _age = p._age;    // 赋值int成员
            cout << "Person 拷贝赋值" << endl;
        }
        return *this;
    }

    // 4. 析构函数
    // 注意:根据规则,手动实现析构函数会抑制默认移动构造和移动赋值的生成
    ~Person()
    {
        cout << "Person 析构函数" << endl;
    }

    /*  注意:
    *       1. 由于我们手动实现了"拷贝构造、拷贝赋值和析构函数"
    *       2. 根据C++规则,编译器不会自动生成默认的"移动构造"和"移动赋值"
    *       3. 如果需要移动语义,必须手动实现
    *   所以:如果你想看到默认生成的"移动构造"和"移动赋值",可以将person类中手动实现的"拷贝构造、拷贝赋值和析构函数"注释掉
    */
};

int main()
{
    /*--------------------测试:默认构造函数--------------------*/
    cout << "Person s1;" << endl;
    Person s1;  // 调用Person的构造函数,使用默认参数

    /*--------------------测试:拷贝构造--------------------*/
    cout << "\nPerson s2 = s1;" << endl;
    Person s2 = s1;  // 调用Person的拷贝构造函数

    /*--------------------测试:移动构造--------------------*/
    cout << "\nPerson s3 = move(s1);" << endl;
    Person s3 = move(s1);
    /* 说明:
    *      1. 由于Person类手动实现了拷贝构造和析构函数
    *      2. 编译器不会生成默认移动构造,这里实际会调用拷贝构造
    */

    /*--------------------测试:拷贝赋值--------------------*/
    cout << "\ns2 = s1;" << endl;
    s2 = s1;  // 调用Person的拷贝赋值运算符

    /*--------------------测试:移动赋值--------------------*/
    cout << "\ns3 = move(s1);" << endl;
    s3 = move(s1);
    /* 说明:
    *      1. 由于Person类手动实现了拷贝赋值和析构函数
    *      2. 编译器不会生成默认移动赋值,这里实际会调用拷贝赋值
    */

    cout << "\n程序结束,对象开始析构" << endl;
    return 0;
}

二、缺省值

成员变量声明时设置缺省值,作用是供构造函数的初始化列表使用。

若构造函数未在初始化列表里显式对该成员初始化,初始化列表就会用这个缺省值来初始化成员。

这个我们在《C++初阶之类和对象》部分已经讲过了,可以参考这篇博客:【命名空间 + 输入&输出 + 缺省参数 + 函数重载】

三、defult和delete

一、用 =default 显式生成默认函数

=default主动要求编译器生成默认版本的成员函数

  • 解决因手动实现其他函数导致默认函数被抑制的问题

场景示例:

  • 若手动实现了拷贝构造函数 ,编译器会默认抑制移动构造函数的生成。

  • 此时若仍需要编译器生成默认移动构造,可显式声明:

    cpp 复制代码
    class MyClass
    {
    public:
        //1.手动实现拷贝构造,抑制了默认移动构造
        MyClass(const MyClass& other) 
        { 
            /* 自定义逻辑 */ 
        }
      
        //2.用 =default 显式要求编译器生成默认移动构造
        MyClass(MyClass&& other) = default;
    };

核心价值:
让开发者在 "需要自定义部分函数(如:拷贝构造)" 的同时,保留编译器自动生成的其他默认函数(如:移动构造),避免手动实现所有函数的冗余。


二、用 =delete 显式删除默认函数

=delete主动禁止编译器生成默认版本的成员函数

  • 替代 C++98 中 "将函数声明为 private 且不实现" 的蹩脚写法

C++98 旧写法(不推荐)

  • 若要禁止拷贝构造,需将其声明为 private 且不实现,调用时才会报错(但报错在链接阶段,不直观):

    cpp 复制代码
    class NonCopyable
    {
    private:
        // 声明但不实现,外部调用会触发链接错误
        NonCopyable(const NonCopyable&);
    };

C++11 新写法(推荐)

  • =delete 直接标记函数为 "删除",编译阶段即可报错,更高效且语义清晰:

    cpp 复制代码
    class NonCopyable
    {
    public:
        // 用 =delete 显式禁止编译器生成拷贝构造
        NonCopyable(const NonCopyable&) = delete;
    };
    
    int main()
    {
        NonCopyable a;
        // 编译报错:调用了被删除的函数(拷贝构造)
        NonCopyable b = a;
    }

典型应用场景:

  • 禁止对象拷贝std::unique_ptr 需独占资源
  • 禁止某些无意义的默认函数禁止基本类型的隐式转换构造

三、=default 和 =delete 的对比总结

关键字 作用 优势 典型场景
=default 要求编译器生成默认函数 保留编译器优化 避免手动实现冗余 需自定义部分函数时保留默认函数
=delete 禁止编译器生成默认函数 编译阶段报错 语义清晰,替代旧写法 禁止拷贝 禁止无意义默认函数

通过 =default=delete,C++11 让开发者对 "默认成员函数的生成" 有了更精细的控制

  • =default 用于 "主动保留默认行为"
  • =delete 用于 "主动禁止默认行为"

两者配合可大幅提升代码的可读性和安全性。

四、final与override

关于这两的关键字我们在《C++进阶之继承多态》中已经讲过了,所以就不再过多的赘述了,这里就再简单的介绍一下这两个关键字的作用。


final 关键字的作用:

final 关键字有两个主要用途阻止类的继承 阻止虚函数的重写


1. 阻止类的继承:当在类名后面使用 final 修饰时,这个类就不能被其他类继承。

  • 这在一些场景下非常有用,比如你设计了一个工具类,它的功能已经完整且不需要被扩展

  • 或者你希望明确表示某个类是继承体系的最底层,不允许再有子类

    cpp 复制代码
    class FinalClass final
    {
        // 类的成员和实现
    };
    
    /* 以下代码会编译报错,因为FinalClass不能被继承
    class SubFinalClass : public FinalClass 
    {
        // 类的成员和实现
    }; 
    */

2. 阻止虚函数的重写:当在虚函数声明后面加上 final 时,该虚函数在派生类中不能被重写。

  • 这有助于保证虚函数的行为在继承体系中有明确的定义,避免在后续派生类中意外地改变其行为

    cpp 复制代码
    class Base
    {
    public:
        virtual void func() final
        {
            // 函数实现
        }
    };
    
    class Derived : public Base
    {
        /* 以下代码会编译报错,因为Base中的func函数被声明为final,不能被重写
        void func() override
        {
            // 函数实现
        }
        */
    };

override 关键字的作用:

override 关键字的主要用途:用于显式地表明派生类中的函数是对基类中虚函数的重写。


它主要有以下几个作用:

  • 增强代码的可读性和可维护性:通过 override 关键字,代码的意图更加清晰,能够让阅读代码的人一眼看出这是对基类虚函数的重写。

  • 编译器检查:编译器会检查带有 override 关键字的函数是否真的重写了基类中的虚函数。

    • 如果基类中不存在对应的虚函数,或者函数签名(返回类型、函数名、参数列表)与基类中的虚函数不匹配,编译器会报错
    • 这样可以在编译阶段发现错误,而不是在运行时出现难以调试的多态问题
    cpp 复制代码
    class Base
    {
    public:
        virtual void virtualFunc()
        {
            // 函数实现
        }
    };
    
    class Derived : public Base
    {
        // 正确重写,编译器检查签名与基类虚函数一致
        void virtualFunc() override
        {
            // 函数实现
        }
    
        /* 
        1. 以下代码会编译报错,因为基类中不存在名为wrongFunc的虚函数
        void wrongFunc() override
        {
            // 函数实现
        }
    
    	--------------------------------------------------------
    
        2. 以下代码会编译报错,因为参数列表与基类中virtualFunc函数不匹配
        void virtualFunc(int num) override 
        { 
            // 函数实现
        } 
        */
    };

总的来说finaloverride 关键字在 C++11 中为类的继承和多态提供了更好的控制和检查机制,有助于写出更健壮、更易理解和维护的代码。

五、STL新变化

:下图是STL中的所有容器,其中用红色方框框起来的容器是C++11新添加的容器。

一、新容器

STL 新增了一些容器(如:图中圈出部分 ),其中最实用的是 unordered_setunordered_map

这两个容器我们之前已做过详细讲解,其他新增容器大家简单了解概念即可。


二、新接口

STL 容器新增了不少实用接口,核心聚焦在 C++11 移动语义初始化列表 等特性的支持上,关键改进包括:

  • 移动语义相关接口:

    • push/insert/emplace 系列接口支持右值引用,可直接转移资源

      • :用 emplace 就地构造对象,避免拷贝
    • 容器的移动构造、移动赋值接口,让资源转移更高效

      • vector v2 = move(v1); 直接转移内存,而非深拷贝
  • 初始化列表支持:

    支持用 initializer_list 直接初始化容器(如:vector<int> v = {1,2,3}; ),语法更简洁

  • 次要接口补充:

    cbegin/cend(返回 const 迭代器 )这类接口,实际使用频率低,需要时查文档即可

相关推荐
Pocker_Spades_A2 小时前
C++程序设计上机作业(1)
开发语言·c++
Chen--Xing2 小时前
OpenMP并行化编程指南
c++·密码学·openmp
乱飞的秋天2 小时前
C++中的特殊成员函数
开发语言·c++
Dovis(誓平步青云)3 小时前
《Linux 构建工具核心:make 命令、进度条、Gitee》
linux·运维·学习
聪明的笨猪猪3 小时前
Java SE “核心类:String/Integer/Object”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
聪明的笨猪猪3 小时前
Java SE “语法”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
攻城狮7号3 小时前
【AI时代速通QT】第八节:Visual Studio与Qt-从项目迁移到多版本管理
c++·qt·跨平台·visual studio·qt vs tools
郝学胜-神的一滴3 小时前
QAxios研发笔记(一):在Qt环境下,构建Promise风格的Get请求接口
开发语言·c++·spring boot·qt·ajax·前端框架·软件工程
AA陈超3 小时前
虚幻引擎UE5专用服务器游戏开发-21 连招技能动画蒙太奇播放
c++·游戏·ue5·游戏引擎·虚幻