C++11(二)

文章目录

可变参数模板

基本语法及原理

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 有两种用法:

  1. 修饰虚函数:禁止派生类重写
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系列容器,大幅优化性能与编程效率。

相关推荐
阿Y加油吧2 小时前
两道经典子序列 / 子数组 DP 题:最长递增子序列 & 乘积最大子数组
算法
混凝土拌意大利面2 小时前
量子退相干提升区块链安全新范式
算法·安全·区块链·共识算法
EverestVIP2 小时前
C++成员指针在库设计中的实际案例
c++
落羽的落羽2 小时前
【Linux系统】深入线程:多线程的互斥与同步原理,封装实现两种生产者消费者模型
java·linux·运维·服务器·c++·人工智能·python
小则又沐风a2 小时前
STL库(vector)逐步分析vector( 包含常用的接口的使用讲解)
开发语言·c++
故事和你9112 小时前
洛谷-数据结构1-1-线性表1
开发语言·数据结构·c++·算法·leetcode·动态规划·图论
脱氧核糖核酸__12 小时前
LeetCode热题100——53.最大子数组和(题解+答案+要点)
数据结构·c++·算法·leetcode
脱氧核糖核酸__12 小时前
LeetCode 热题100——42.接雨水(题目+题解+答案)
数据结构·c++·算法·leetcode
王老师青少年编程13 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【线性扫描贪心】:数列分段 Section I
c++·算法·编程·贪心·csp·信奥赛·线性扫描贪心