C++11中较实用特性

列表初始化

{} 初始化:是一种语法结构(语言层面的特性),核心作用是统一初始化语法:无论初始化基本类型、数组还是复杂对象,都可以用 {}

std::initializer_list:是标准库定义的模板类型,核心作用是让函数 / 构造函数能 “批量接收” 初始化列表元素

{}初始化

1.基本类型的初始化
{} 可直接初始化基本数据类型(如 int、double 等)

int a{5};          // 初始化int为5
double b{3.14};    // 初始化double为3.14
char c{'A'};       // 初始化char为'A'

// 传统数组
int arr1[]{1, 2, 3, 4};  // 省略大小,自动推断为4个元素
int arr2[5]{1, 2};       // 部分初始化,剩余元素自动为0(结果:[1,2,0,0,0])

// 动态数组(new 分配)
int* arr3 = new int[3]{10, 20, 30};  // 动态数组初始化

2.结构体 / 聚合类型的初始化
对于聚合类型(如结构体、联合体,没有用户定义的构造函数),{} 可直接按成员顺序初始化,无需调用构造函数

struct Point {
    int x;
    int y;
};

struct Student {
    std::string name;
    int age;
    double score;
};

int main() {
    Point p{10, 20};                // 按顺序初始化x=10, y=20
    Student s{"Alice", 18, 95.5};   // 按顺序初始化name、age、score
    return 0;
}

4.类对象的初始化
对于自定义类,{} 初始化会根据类的构造函数匹配:

  • 若类有普通构造函数,{} 会传递参数给对应的构造函数。
  • 若类有接受 std::initializer_list 的构造函数,{}
    会优先匹配该构造函数(这是 std::initializer_list 与 {} 结合的核心场景)。

匹配普通构造

class Person {
private:
    std::string name;
    int age;
public:
    // 普通构造函数
    Person(std::string n, int a) : name(n), age(a) {}
};

int main() {
    Person p{"Bob", 25};  // 等价于 Person p("Bob", 25); 调用普通构造函数
    return 0;
}

匹配 std::initializer_list 构造函数

#include <initializer_list>
#include <vector>

class MyList {
private:
    std::vector<int> data;
public:
    // 普通构造函数:初始化n个值为val的元素
    MyList(int n, int val) {
        data.resize(n, val);
    }
    
    // initializer_list构造函数:用列表元素初始化
    MyList(std::initializer_list<int> list) {
        for (auto num : list) {
            data.push_back(num);
        }
    }
};

int main() {
    MyList l1{3, 5};  // 调用initializer_list构造函数(元素为3,5)
    MyList l2(3, 5);  // 调用普通构造函数(3个元素,每个为5)
    return 0;
}

initializer_list之前的使用

在 C++11 引入 std::initializer_list 之前,对象和容器的初始化方式相对繁琐,缺乏统一的列表初始化语法,主要通过以下几种方式进行构造:

1.标准容器的初始化
使用默认构造 + 插入元素:先创建空容器,再通过 push_back 等方法逐个添加元素

#include <vector>
#include <map>

int main() {
    // 初始化vector(C++11前的方式)
    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> nums;
    for (int i = 0; i < 5; ++i) {
        nums.push_back(arr[i]); // 逐个插入
    }
    
    // 初始化map(C++11前的方式)
    std::map<std::string, int> ages;
    ages["Alice"] = 25;    // 逐个插入键值对
    ages["Bob"] = 30;
    ages["Charlie"] = 35;
    
    return 0;
}

2.自定义类型的初始化
通过多个参数的构造函数:需要为不同数量的初始化参数重载构造函数

#include <vector>

class MyCollection {
private:
    std::vector<int> data;
public:
    // 无参构造函数
    MyCollection() {}
    
    // 单个元素的构造函数
    MyCollection(int a) {
        data.push_back(a);
    }
    
    // 两个元素的构造函数(需要手动重载)
    MyCollection(int a, int b) {
        data.push_back(a);
        data.push_back(b);
    }
};

int main() {
    // 有限参数的构造
    MyCollection coll1(10);
    MyCollection coll2(10, 20);
  
    return 0;
}
  1. 函数参数传递
    无法直接传递初始化列表,必须先创建临时容器或数组,再传递给函数:
#include <vector>
#include <iostream>

// 计算vector中所有元素的和
int sum(const std::vector<int>& nums) {
    int total = 0;
    for (size_t i = 0; i < nums.size(); ++i) {
        total += nums[i];
    }
    return total;
}

int main() {
    // 必须先创建vector再传递
    std::vector<int> temp;
    temp.push_back(1);
    temp.push_back(2);
    temp.push_back(3);
    int result = sum(temp);
    std::cout << "Sum: " << result << std::endl;
    
    return 0;
}

有了initializer_list之后的对比

std::initializer_list 是一个重要的特性,它主要用于支持对象的列表初始化,让代码更加简洁直观。
它的主要体现和应用场景包括:

容器的初始化
标准容器(如vector、map、set等)都支持使用初始化列表进行初始化:

int main() {
    // 初始化vector
    std::vector<int> nums = {1, 2, 3, 4, 5};
    
    // 初始化map
    std::map<std::string, int> ages = {
        {"Alice", 25},
        {"Bob", 30},
        {"Charlie", 35}
    };
    return 0;
}

自定义类型的初始化
可以为自定义类添加接受std::initializer_list参数的构造函数,从而支持列表初始化:

class MyCollection {
private:
    std::vector<int> data;
public:
    // 接受initializer_list的构造函数
    MyCollection(std::initializer_list<int> list) {
        for (auto num : list) {
            data.push_back(num);
        }
    }
};

int main() {
    // 使用列表初始化自定义对象
    MyCollection coll = {10, 20, 30, 40};
    return 0;
}

函数参数
函数可以接受std::initializer_list作为参数,允许调用者传递一个列表:

#include <initializer_list>
#include <iostream>

// 计算列表中所有元素的和
int sum(std::initializer_list<int> list) {
    int total = 0;
    for (auto num : list) {
        total += num;
    }
    return total;
}

int main() {
    // 调用时直接传递列表
    int result = sum({1, 2, 3, 4, 5});
    std::cout << "Sum: " << result << std::endl; // 输出 Sum: 15
    return 0;
}

返回值
函数可以返回std::initializer_list:

#include <initializer_list>
#include <iostream>

std::initializer_list<int> getNumbers() {
    return {1, 2, 3, 4, 5};
}

int main() {
    for (int num : getNumbers()) {
        std::cout << num << " ";
    }
    return 0;
}

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值

总结

C++11理念是一切皆可用列表初始化
简单说:{} 是 “初始化的语法外壳”,std::initializer_list 是 “处理初始化列表元素的工具,让函数或构造函数能统一的列表的去处理元素”

#include <initializer_list>
#include <vector>

class MyList {
private:
    std::vector<int> data;
public:
    // 普通构造函数:初始化n个值为val的元素
    MyList(int n, int val) {
        data.resize(n, val);
    }
    
    // initializer_list构造函数:用列表元素初始化
    MyList(std::initializer_list<int> list) {
        for (auto num : list) {
            data.push_back(num);
        }
    }
};

int main() {
    MyList l1{3, 5};  // 调用initializer_list构造函数(元素为3,5)
    //MyList l1{3, 5}; {}语法的作用:
    //在没有initializer_list构造函数的情况下
    //等价于调用 MyList l2(3, 5);
    MyList l2(3, 5);  // 调用普通构造函数(3个元素,每个为5)
    return 0;
}

STL的变化

array不常用,更常用是直接用vector
forward_list(单向链表)的使用场景不多,单链表可以节省一些空间(但现在这些空间其实也不大重要),只需要头插头删的时候可以使用,双向链表list用的会更多

最有用的是unordered_map和unordered_set(元素无序,因为是哈希表储存,顺序不确定,平均 O (1)的查找效率)
在这里插入图片描述

decltype

类型可以以字符串形式获取到
typeid(变量名).name拿到的是这个类型的字符串,比如拿到int,然后可以打印出int

int i = 1;
cout << typeid(i).name() << endl;

仅仅是个字符串,不能够做类型再去定义出变量

typeid(i).name() j; 错误的使用

可以用auto定义变量,但auto也有不能使用的场景

int i = 1;
double d = 2.2;
auto ret = i * d;
//vector<auto> 这种时候括号里就不能是auto了

用ret的类型去实例化vector
decltype可以推导对象的类型。这个类型是可以用的
用来模板实参,或者再定义对象

vector<decltype(ret)> v;
v.push_back(1);
v.push_back(1.1);

decltype(ret) x;//定义对象也是可以的

左值引用和右值引用

无论左值引用还是右值引用,都是给对象取别名。

左值:
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。

左值引用:
左值引用就是给左值的引用,给左值取别名。

int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;

// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}

右值:
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址

右值引用:
在 C++ 中,右值引用是 C++11 引入的一种新的引用类型,专门用于绑定到右值(临时对象或即将销毁的对象),其语法是使用 && 符号
右值引用就是对右值的引用,给右值取别名。

int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}

左值引用和右值引用比较

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值

右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。
  3. std::move 是 C++11 引入的一个标准库函数,它的作用是将一个左值转换为对应的右值引用。
    注意:std::move 本身并不会移动任何东西,它只是进行了一个类型转换。
    比如:int i = 10;
    int&& rr3 = move(i);

左值被move之后返回的属性是右值,但std::move只是转换类型,不会修改原左值的内容或内存。

int&& b = std::move(a);

但是后续原左值的资源会不会被窃取,要取决于这个右值怎么用

左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用

左值引用的优缺点

左值引用的使用场景:
做参数和做返回值都可以提高效率。
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。

在这里插入图片描述
传值返回的过程中会导致空间的浪费
在这里插入图片描述

函数返回值是右值
右值分为:
1.纯右值 内置类型右值
2.将亡值 自定义的右值

这种场景下ret是将亡值,右值可以被const接受,但我们可以设计出一个更适合右值的函数:移动构造

// 拷贝构造
string(const string& s)
{
	cout << "string(const string& s) -- 深拷贝" << endl;

	string tmp(s._str);
	swap(tmp);
}

// 移动构造
string(string&& s)
{
	cout << "string(string&& s) -- 移动拷贝" << endl;

	swap(s);
}

拷贝构造和移动构造的区别

拷贝构造:
const 修饰导致无法修改原对象
不能直接swap(s)交换两个对象的资源,因为const的存在,我们只能读取 s 的资源(如 s._str、s._size 等),但不能修改 s 的任何成员。
所以我们只能读取了s的资源后构造出tmp —— string tmp(s._str);
再和tmp交换资源swap(tmp);

// 拷贝构造
string(const string& s)
{
	cout << "string(const string& s) -- 深拷贝" << endl;

	string tmp(s._str);
	swap(tmp);
}

移动构造:
对于一个临时对象,本来就是要挂了的
直接窃取它的资源,让它带着我的空资源离开
没有了const的限制,直接swap(s)

// 移动构造
string(string&& s)
{
	cout << "string(string&& s) -- 移动拷贝" << endl;

	swap(s);
}

那么对于赋值重载也是一样的,对于一个要挂的临时对象,肯定是移动赋值效率更高

// 赋值重载
string& operator=(const string& s)
{
	cout << "string& operator=(const string& s) -- 深拷贝" << endl;
	/*string tmp(s);
	swap(tmp);*/
	if (this != &s)
	{
		char* tmp = new char[s._capacity+1];
		strcpy(tmp, s._str);

		delete[] _str;
		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
	}

	return *this;
}

// 移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s)-- 移动赋值" << endl;

	swap(s);
	return *this;
}

拷贝(构造,赋值)和 移动(构造,赋值)图解

在这里插入图片描述
局部变量会消耗,所以不能传引用返回,得传值返回
临时对象开一块一样大的空间接收ret的资源,然后ret指向的空间销毁
临时对象拷贝赋值给s,s接收了临时对象的资源后,临时对象指向的空间销毁

对比移动构造和移动赋值:
在这里插入图片描述
省去了临时对象再去开空间

当然这是编译器没有优化时的情况,如果我们让编译器进行优化,右值也还是可以发挥很大作用
两个构造优化成直接构造:
浪费会少一些,但是还是有
在这里插入图片描述

在这里插入图片描述

右值引用后属性的变化

结论:右值被右值引用后的属性是左值

右值是不能被修改的,但右值引用后是需要去修改,因为我们swap(s)是需要修改s的
所以编译器将右值引用接受对应类型之后,该类型就变为左值(可修改)了
在这里插入图片描述

万能引用和完美转发

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }

void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

// 函数模板:万能引用
template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}

int main()
{
	PerfectForward(10);           // 右值

	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值

	const int b = 8;
	PerfectForward(b);		      // const 左值
	PerfectForward(std::move(b)); // const 右值

	return 0;
}
  • 打印结果全是左值引用

在非模板场景中:&& 一定表示右值引用,只能绑定到右值
在模版场景中:
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。

引用类型的 “筛选作用”:

  • 当你调用PerfectForward(10)时,万能引用T&&会推导出右值引用类型,成功接收右值 10
  • 当你调用PerfectForward(a)时,万能引用会推导出左值引用类型,成功接收左值 a
  • 这里的左值 / 右值引用仅用于"匹配并接收" 对应的类型

后续使用时退化为左值

  • 当参数传递给t后,无论它最初是左值引用还是右值引用,t本身成为了一个有名字的变量
  • 在 C++ 中,所有有名字的引用变量都会被当作左值处理
  • 所以直接调用Fun(t)时,无论t的原始类型是什么,都会匹配左值引用版本的Fun

如果Fun(t)那么调用的都是打印左值引用
如果Fun(move(t))那么调用的都是打印右值引用

期望保持实参的属性呢?

完美转发
std::forward 完美转发在传参的过程中保留对象原生类型属性

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }

void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

// 函数模板:万能引用
template<typename T>
void PerfectForward(T&& t)
{
	Fun(forward<T>(t));
}

int main()
{
	PerfectForward(10);           // 右值

	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值

	const int b = 8;
	PerfectForward(b);		      // const 左值
	PerfectForward(std::move(b)); // const 右值

	return 0;
}

打印结果

右值引用        // PerfectForward(10)
左值引用        // PerfectForward(a)
右值引用        // PerfectForward(std::move(a))
const 左值引用  // PerfectForward(b)
const 右值引用  // PerfectForward(std::move(b))

新的类功能

原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

看起来条件很严格,其实也是应该的:
既然实现了析构,那么说明有资源需要释放,那么就需要深拷贝
所以析构函数 、拷贝构造、拷贝赋值重载基本不会单独出现
如果你的析构函数 、拷贝构造、拷贝赋值重载都自己实现了,那么移动构造和移动赋值也应该自己实现,内部有哪些资源需要移动自己决定

default关键字和delete关键字

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

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;
private:
	 Adz::string _name;
	 int _age;
};
int main()
{
	 Person s1;
	 Person s2 = s1;
	 Person s3 = std::move(s1);
	 return 0;
}

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
Person(Person&& p) = delete;

可变参数模板

在这里插入图片描述
回顾一下printf函数的可变参数
底层是用一个指针数组实现的,传多个参数就把多个参数存到数组中,然后把数组中的值一个个读取

C++11的新特性可变参数模板能够创建可以接受可变参数的函数模板和类模板

模板参数包是多个模板参数,函数参数包是多个实参,一个是类型,一个是变量

template <class …Args>:声明了一个模板参数包Args,它可以包含 0 到多个不同的类型。
(Args… args):声明了一个函数参数包args,它对应传入的实际参数,类型由Args推导而来。
这里的…是可变参数模板的核心语法,用于表示 “参数包”(可以包含多个元素)
两者是绑定关系,函数参数包args依赖模板参数包而存在,二者总是成对出现

template <class ...Args>
void ShowList(Args... args)
{
	cout << sizeof...(args) << endl;
	//sizeof可以获取参数包中有几个参数,但是使用起来会有点怪
}
int main()
{
	ShowList(1);               // 参数包大小:1(int)
	ShowList(1, 2);            // 参数包大小:2(int, int)
	ShowList(1, 2, 3);         // 参数包大小:3(int, int, int)
	ShowList(1, 2.2, 'x', 3.3);// 参数包大小:4(int, double, char, double)

	return 0;
}

输出结果

1
2
3
4

如ShowList(1, 2.2, ‘x’, 3.3)中,Args会被推导为int, double, char, double,args则包含这 4 个实参
语法不支持使用args[i]这样方式获取可变参数,所以用其他方式获取
递归函数方式展开参数包

void _ShowList()
{
	cout << endl;
}
// 编译时的递归推演
// 第一个模板参数依次解析获取参数值
template <class T, class ...Args>
void _ShowList(const T& val, Args... args)
{
	cout << val << " ";
	_ShowList(args...);
}

template <class ...Args>
void ShowList(Args... args)
{
	_ShowList(args...);//参数包往下传需要这样写
}

int main()
{
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	ShowList(1, 2.2, 'x', 3.3);

	return 0;
}

通过ShowList(1, 2, 3);理解:
第一步:调用ShowList(1, 2, 3)

  • ShowList接收参数包(1, 2, 3),调用_ShowList(1, 2, 3)。

第二步:第一次进入_ShowList

  • 函数原型匹配:_ShowList(const T& val, Args… args)。
  • 拆包:第一个参数1赋值给val,剩余参数(2, 3)作为新的参数包args。
  • 推导类型:T = int,Args包含int, int。
  • 执行:cout << val << " " → 打印 1
  • 递归调用:_ShowList(2, 3)(传递剩余参数包)。

第三步:第二次进入_ShowList

  • 拆包:第一个参数2赋值给val,剩余参数(3)作为新的参数包args。
  • 推导类型:T = int,Args包含int。
  • 执行:cout << val << " " → 打印 2
  • 递归调用:_ShowList(3)。

第四步:第三次进入_ShowList

  • 拆包:第一个参数3赋值给val,剩余参数为空(())。
  • 推导类型:T = int,Args为空。
  • 执行:cout << val << " " → 打印 3 。
  • 递归调用:_ShowList()(传递空参数包)。

第五步:调用无参_ShowList

  • 匹配原型:void _ShowList()。
  • 执行:cout << endl → 打印换行。
  • 递归结束。

本质是利用了编译时的递归推演,利用第一个参数依次解析参数包中内容

初始化数组展开参数包
对比递归展开,代码更简洁

template <class T>
int PrintArg(T&& t)
{
	cout << t << " ";
	return 0;
}

//展开函数
template <class ...Args>
void ShowList(Args&&... args)
{
	// 要初始化arr,强行让解析参数包,参数包有几个参数,PrintArg就依次推演生成几个
	int arr[] = { PrintArg(args)... };
	cout << endl;
}

int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));

	return 0;
}

核心原理
C++ 中,当用初始化列表{…}初始化数组时,会按顺序解析列表中的每个元素。这里利用这一特性,将参数包args…展开为一系列PrintArg(args)的调用,从而逐个处理每个参数。

关键语法是{ PrintArg(args)… }中的…,它会将参数包args中的每个元素依次传递给PrintArg,形成类似{ PrintArg(arg1), PrintArg(arg2), …, PrintArg(argN) }的效果。

整体流程

template <class ...Args>
void ShowList(Args&&... args)

万能引用接收参数

int arr[] = { PrintArg(args)... };

通过初始化列表强行解析参数包,{PrintArg(1),PrintArg(‘A’),PrintArg(std::string(“sort”)) }

逐个调用PrintArg:
PrintArg(1):打印 1 ,返回0
PrintArg(‘A’):打印 A ,返回0
PrintArg(std::string(“sort”)):打印 sort ,返回0
数组arr最终被初始化为{0, 0, 0}(arr值无关紧要,后续可能也不会使用,仅为了使用初始化列表)

empalce接口

template <class... Args>
void emplace_back (Args&&... args);

emplace系列的接口,支持模板的可变参数,并且万能引用。

以 emplace_back 为例,其核心作用是在容器中直接构造元素,避免不必要的拷贝或移动操作,跟push_back有什么区别呢?

std::pair 是 C++ 标准库中的一个模板类,用于将两个数据值组合成一个单一的对象
一个节点需要存储两个独立的值时(如 val 和 n),可以将这两个值封装为一个 std::pair
两个节点里就有两个自己的pair对象

在这里插入图片描述
执行流程:

  • emplace_back 接收参数包 (“2222”, 2)。
  • 在链表内部,调用节点构造函数 list_node(Args&&… args),将参数包传递给节点。
  • 节点构造函数中,_data(args…) 触发 std::pair<bit::string, int> 的构造,直接使用参数 “2222” 和 2。
  • 最终在节点的内存中构造 pair 对象,无需额外拷贝或移动。

对比push_back

list<pair<string, int>> lt;
// push_back 需要先构造 pair 对象
lt.push_back(make_pair("2222", 2)); // 拷贝 pair
// emplace_back 直接传递参数,原地构造 pair
lt.emplace_back("2222", 2); // 更高效

push_back 需要先通过 make_pair 构造 pair 对象,再将其拷贝 / 移动到容器中。
emplace_back 直接将 “2222” 和 2 传递给 pair 的构造函数,在容器内直接构造 pair,省去了中间对象的开销。

lambda

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _evaluate; // 评价
	...

	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};


struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};

struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());

	return 0;
}

每次为了实现一个算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类(按价格比较、按评价比较),特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式

lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

mutable一般省略
参数列表和返回值类型写不写看需不需要参数和返回值类型是否明确
捕捉列表和函数体一般都写

auto f1 = [](int x)->int {cout << x << endl; return 0; };
f1(1);

函数体中语句多时,也可以想下面这样写:

auto f2 = [](int x)
{
	cout << x << endl;
	return 0;
};
//整体是一个赋值表达式,语句结束记得给分号

f2(2);

lambda 表达式的本质是创建了一个匿名的函数对象(可调用对象)。这个对象可以像函数一样被调用
也就是可以理解为 lambda 表达式是简化版的仿函数

把 lambda 表达式看作一个 “一次性的迷你函数”也是可以的

适合新手的使用场景:

sort,或者find查找之类的

  • 排序的逻辑(如a.age < b.age)直接写在排序函数旁边,不需要跳转到其他地方看比较函数的定义。
  • 想临时修改排序规则(比如从升序改降序),只需修改 lambda 内部的逻辑,无需新增或修改外部函数。
  • 避免为了简单的比较逻辑定义多个函数(如compareByAge、compareByNameLength),减少命名负担。
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool {return g1._price < g2._price; });

一个语句的结束要给分号,
但 [](const Goods& g1, const Goods& g2)->bool {return g1._price < g2._price; } 是把lambda作为一个对象传递给sort

sort可以传仿函数对象,也可以传lambda,甚至可以传函数指针,因为有模板的存在
在这里插入图片描述

auto f1 = [](int x)->int {cout << x << endl; return 0; };
f1(1);
cout << typeid(f1).name() << endl;
//class <lambda_ba37a3eb9b8e2495e3aae6ef76d9eed2>

auto f2 = [](int x)
{
	cout << x << endl;
	return 0;
};
f2(2);
cout << typeid(f2).name() << endl;
//class <lambda_8c5f46db939004e569f343c4f7d08c27>

可以看到lambda打印出来的类型名有点奇怪
其实是<lambda_uuid>

UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,是一种软件建构的标准,亦为开放软件基金会组织在分布式计算环境领域的一部分。其目的,是让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的UUID。在这样的情况下,就不需考虑数据库创建时的名称重复问题。最广泛应用的UUID,是微软公司的全局唯一标识符(GUID),而其他重要的应用,则有Linux ext2/ext3文件系统、LUKS加密分区、GNOME、KDE、Mac OS X等等。另外我们也可以在e2fsprogs包中的UUID库找到实现。

看到的是lambda,编译器语法编译时其实是没有lambda的,就像范围for一样,编译器在语法编译时没有范围for,只有迭代器。在语法编译之前lambda会被生成对应的类,每个lambda都会生成一个类,这个类可以去调用operator(),一个类重载了operator(),这个类就可以叫仿函数

auto f1 = [](int x)->int {cout << x << endl; return 0; };
f1(1);

operator()的参数是int x,返回值是int,函数体就是cout << x << endl; return 0;
写的参数函数体这些是给operator()的,所以可以像operator()一样使用

总结:
每个lambda生成一个仿函数,不同仿函数类型不一样,因为uuid不一样
以前需要自己写仿函数,现在不需要自己写了

捕获列表说明

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针

注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割
比如:
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同(其实不同)

当我们想写交换函数
一般传参写法:

int x = 0, y = 1;
cout << x << " " << y << endl;

auto f1 = [](int& r1, int& r2) {
    int tmp = r1;
    r1 = r2;
    r2 = tmp;
};

f1(x, y);
cout << x << " " << y << endl << endl;

不传参写法:

//错误示范:传值捕捉
int x = 0, y = 1;
cout << x << " " << y << endl;

auto f2 = [x, y]() mutable {
    cout << &x << ":" << &y << endl;

    int tmp = x;
    x = y;
    y = tmp;
};

f2(x, y);
cout << x << " " << y << endl << endl;

捕捉列表捕捉来的变量可以看成这个lambda仿函数类的成员变量,所以可以不需要参数直接用,但成员变量是const修饰的,想让捕捉来的变量可以修改,就加上mutable

捕捉x,y成为了< lambda_uuid>的成员变量,这是传值捕捉
外面的x,y传给< lambda_uuid>初始化< lambda_uuid>的成员变量x,y
< lambda_uuid>类里的x,y和外面的x,y不是同一个
语法没说错,但是没有达到交换函数该有的效果

//正确写法:引用捕捉
成员变量是引用,用x和y初始化成员变量
可以不加mutable,本来引用捕捉就是为了修改
可以认为特殊处理不用加
如果一个是引用捕捉一个是传值捕捉,还是要加mutable
int x = 0, y = 1;
cout << x << " " << y << endl;

auto f2 = [&x, &y]() {
    cout << &x << ":" << &y << endl;

    int tmp = x;
    x = y;
    y = tmp;
};

f2(x, y);
cout << x << " " << y << endl << endl;

引用和取地址符号会混,以前引用前面要加类型,但这里引用捕捉没有加类型,记作一种特殊情况

为什么要捕捉this指针
a1 和 a2 是类 AA 的私有成员变量,只能通过 AA 类的对象来访问
在成员函数 func() 中,隐式存在 this 指针,指向当前调用该函数的对象,所以可以直接使用 a1、a2
但 lambda 表达式是一个独立的函数对象,它本身并没有 this 指针。所以要捕捉this指针才能用a1、a2

捕捉this指针
a1,a2不是父作用域,捕捉a1,a2会报错
捕捉了this就可以用a1,a2了,应该是this->a1这样访问,但做了优化,可以不显示写this

  • 使用 [=] 捕获[=] 表示值捕获,捕获 lambda 所在作用域中所有被 lambda 使用的非静态局部变量(按值拷贝)。同时会隐式捕获 this 指针(这是 C++ 的规则)
  • 使用显式捕获 [this]
class AA
{
public:
	void func()
	{
		/*auto f1 = [this]() {
			cout << a1 << endl;
			cout << a2 << endl;
		};*/
		auto f1 = [=]() {
			cout << a1 << endl;
			cout << a2 << endl;
		};

		f1();
	}
private:
	int a1 = 1;
	int a2 = 1;
};

包装器

函数指针缺点:类型不好用
仿函数缺点:得在外面单独定义一个类,哪怕只是一个简单的比较逻辑
lambda缺点:类型写不了,匿名

#include <vector>
#include <iostream>

// 模板类:用模板参数F接收处理逻辑(函数/仿函数/lambda)
template <typename T, typename F>
class DataProcessor {
private:
    std::vector<T> data;
    F processor;  // 存储处理逻辑(模板参数带来的通用性)
public:
    DataProcessor(const std::vector<T>& d, F f) : data(d), processor(f) {}
    
    // 执行处理逻辑(编译器可内联processor,无调用开销)
    void process() {
        for (auto& item : data) {
            processor(item);  // 核心:用模板参数传递的逻辑处理数据
        }
    }
};

int main() {
    // 场景1:处理int数据,将每个元素乘以2
    std::vector<int> ints = {1, 2, 3, 4};
    DataProcessor intProcessor(ints, [](int& x) { x *= 2; });
    intProcessor.process();  // 处理后:2,4,6,8
    
    // 场景2:处理double数据,保留2位小数
    std::vector<double> doubles = {1.234, 5.678, 9.012};
    DataProcessor doubleProcessor(doubles, [](double& x) { 
        x = round(x * 100) / 100;  // 保留2位小数
    });
    doubleProcessor.process();  // 处理后:1.23,5.68,9.01
}

对比两种使用,很明显vector< int > 类型声明对象/变量的方式更符合直觉

std::vector<int> ints = {1, 2, 3, 4};
DataProcessor intProcessor(ints, [](int& x) { x *= 2; });

如果想用这样的显式指定完整类型来声明变量,lambda是做不到的,因为lambda 的类型是编译器生成的匿名类型(比如可能叫 lambda_123456),我们无法在代码中写出这个类型名

// 理想状态:显式指定所有模板参数(包括lambda类型)
DataProcessor<int, 具体的lambda类型> intProcessor(ints, [](int& x) { x *= 2; });

所以就出现了function

std::function在头文件<functional>
// 类模板原型如下
template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参

为什么 std::function 能解决这个问题?
std::function 本质是一个 “类型擦除” 的包装器。无论内部包装的是函数指针、仿函数还是 lambda(它们的类型各不相同),std::function 本身的类型是明确的(如 std::function<int(int, int)>)。

//包装器
void swap_func(int& r1, int& r2)
{
	int tmp = r1;
	r1 = r2;
	r2 = tmp;
}

struct Swap
{
	void operator()(int& r1, int& r2)
	{
		int tmp = r1;
		r1 = r2;
		r2 = tmp;
	}
};

int main()
{
	int x = 0, y = 1;
	cout << x << " " << y << endl;

	auto swaplambda = [](int& r1, int& r2) {
		int tmp = r1;
		r1 = r2;
		r2 = tmp;
	};

	function<void(int&, int&)> f1 = swap_func;
	f1(x, y);
	cout << x << " " << y << endl << endl;

	function<void(int&, int&)> f2 = Swap();
	f2(x, y);
	cout << x << " " << y << endl << endl;

	function<void(int&, int&)> f3 = swaplambda;
	f3(x, y);
	cout << x << " " << y << endl << endl;

	map<string, function<void(int&, int&)>> cmdOP = {
		{"函数指针", swap_func},
		{"仿函数", Swap()},
		{"lambda", swaplambda},
	};

	cmdOP["函数指针"](x, y);
	cout << x << " " << y << endl << endl;

	cmdOP["仿函数"](x, y);
	cout << x << " " << y << endl << endl;

	cmdOP["lambda"](x, y);
	cout << x << " " << y << endl << endl;

	return 0;
}
class Plus {
public:
    // 静态成员函数
    static int plusi(int a, int b) {
        return a + b;
    }

    // 非静态成员函数(普通成员函数)
    double plusd(double a, double b) {
        return a + b;
    }
};

静态成员函数

function<int(int, int)> f1 = &Plus::plusi;
cout << f1(1, 2) << endl; // 输出3
  • 静态成员函数的地址获取:&类名::函数名(&Plus::plusi)
  • 静态成员函数不依赖对象,因此std::function的参数列表与函数本身一致(int(int, int))
  • 调用时直接传参即可,无需对象

非静态成员函数的函数指针绑定

function<double(Plus*, double, double)> f2 = &Plus::plusd;
Plus ps;
cout << f2(&ps, 1.1, 2.2) << endl; // 输出3.3
  • 非静态成员函数的地址获取同样需要&类名::函数名(&Plus::plusd)
  • 非静态成员函数隐含一个this指针作为第一个参数(指向当前对象),因此std::function的参数列表需要额外增加一个对象指针(Plus*)
  • 调用时需传入对象地址(&ps),本质是通过指针调用成员函数(ps.plusd(1.1, 2.2)的等价操作)

另一种方式绑定对象本身

function<double(Plus, double, double)> f3 = &Plus::plusd;
cout << f3(Plus(), 1.11, 2.22) << endl; // 输出3.33
  • 这里std::function的第一个参数是Plus对象(值传递),而非指针
  • 调用时传入临时对象Plus(),会通过该对象调用plusd函数
  • 本质是通过对象值调用成员函数,相当于Plus().plusd(1.11, 2.22)

总结

  1. 静态成员函数:获取地址用&类名::函数名,绑定后直接调用(无对象依赖)

  2. 非静态成员函数:
    必须通过&类名::函数名获取地址
    绑定到std::function时,需在参数列表最前面增加对象指针(类名*)或对象(类名)

  3. 非静态成员函数的调用必须依赖对象(通过指针、引用或对象本身),因为其内部隐含this指针指向当前对象

其他参数会变,第一个参数总是固定的,那么可不可以不写第一个参数:

bind

std::bind函数定义在头文件< functional >中,是一个函数模板,它就像一个函数包装器(适配器)

bind可以调整参数顺序,可以让固定参数写死,调整参数个数

function<double(double, double)> f4 = bind(
    &Plus::plusd, 
    Plus(),          // 绑定一个临时对象作为this
    std::placeholders::_1, 
    std::placeholders::_2
);
cout << f4(1.11, 2.22) << endl; // 输出3.33 函数:1.11+2.22
  • std::bind将对象(这里是临时对象Plus())与成员函数绑定,消除了对显式传递对象的依赖
  • std::placeholders::_1和std::placeholders::_2表示调用f4时的第一个和第二个参数,对应plusd的a和b
  • 此时f4的参数列表与plusd原本的参数列表一致(double(double, double))
先通过using namespace std::placeholders;引入命名空间,之后直接写_1、_2、_3可以简化代码
如何调整参数顺序
function<double(double, double)> f4 = bind(
    &Plus::plusd, 
    Plus(),          // 绑定一个临时对象作为this
    _2,              // 占位符_2表示:新函数的第二个参数,传给原函数的a
    _1				 // 占位符_1表示:新函数的第一个参数,传给原函数的b
);
cout << f4(1.11, 2.22) << endl; // 函数内部就变成了:2.22+1.11,如果是减法就更明显

总结

  1. 静态成员函数获取地址用&类名::函数名,绑定后直接调用(无对象依赖)

  2. 非静态成员函数
    必须通过&类名::函数名获取地址
    绑定到std::function时,需在参数列表最前面增加对象指针(类名*)或对象(类名)
    通过std::bind预先绑定对象,简化调用方式

  3. 非静态成员函数的调用必须依赖对象(通过指针、引用或对象本身),因为其内部隐含this指针指向当前对象

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值