C++11

1.C++11的发展历史

C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对 C++ 程序员可用的抽象。在它最终由 ISO 在 2011 年 8 月 12 日采纳前,人们曾使⽤名称“C++0x”,因为它曾被期待在 2010 年之前发布。
在这里插入图片描述

2. 列表初始化

C++11 中的列表初始化是一种非常强大和灵活的初始化方式,它为变量、数组、容器、结构体和类等的初始化提供了统一的语法形式。

2.1C++98的{}

C++98中⼀般数组和结构体可以用 {} 进行初始化。

struct Point
{
	int _x;
	int _y;
};
int main()
{
	Point p = {1,2};//对结构体初始化
	int a[] = { 1,2,3,4,5 };//对数组初始化
	return 0;
}

2.2C++11的{}

C++11引入了统一的列表初始化语法,使用花括号 {} 进行初始化, {} 初始化也叫做列表初始化。不仅可以用于数组、结构体,还能用于标准容器、类等。
内置类型支持,自定义类型也⽀持,自定义类型本质是类型转换,中间会产⽣临时对象,最后优化
了以后变成直接构造。
{} 初始化的过程中,可以省略掉 =
C++11列表初始化的本意是想实现⼀个大统⼀的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{} 初始化会很方便。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date& d)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//定义容器的同时初始化多个元素,使代码更加简洁直观
	vector<int> v{ 1,2,3,4,5 };
	map<string, int> Mymap{ {"one",1},{"two",2}};


	Date d1 = { 2025, 1, 1 };
	// 这里d2引用的是{ 2024, 7, 25 }构造的临时对象
	const Date& d2 = { 2024, 7, 25 };
	// 需要注意的是C++98支持单参数时类型转换,也可以不用{}
	Date d3 = { 2025 };
	Date d4 = 2025;
	return 0;
}

3.右值引用和移动语义

C++98的C++语法中就有引⽤的语法,而C++11中新增了的右值引用语法特性,C++11之后我们之前学习的引用就叫做左值引用。⽆论左值引用还是右值引用,都是给对象取别名。

3.1左值和右值

左值是⼀个表示数据的表达式(如变量名或解引用的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
特点: 具有持久的状态,可被赋值和取地址。

int a = 10; // a 是左值,它有明确的内存地址,可以被赋值和取地址
int* ptr = &a; // 可以取 a 的地址
a = 20; // 可以对 a 进行赋值操作

右值: 纯粹的临时对象、字面量值等,不能出现出现在赋值符号的左边,它们不代表任何具有持久状态的对象,右值不能取地址。

int b = 5 + 3; // 5 + 3 是纯右值,它是一个临时计算结果,没有自己的持久内存地址
int c = 10; // 10 是纯右值,它是一个字面量

区分方法:
能否取地址:可以使用&运算符取地址的表达式通常是左值,否则是右值。例如&(a)是合法的(a是左值),而&(5 + 3)是不合法的(5 + 3是右值)
是否可赋值:可以出现在赋值语句左边的表达式是左值。例如a = 10;中a是左值5 = a是错误的,因为5是右值

3.2左值引用和右值引用

左值引用是对左值的引用,使用&符号来声明。它提供了一种为对象起别名的方式,使得多个名称可以指向同一个对象。

int num = 10;
// 声明一个左值引用 ref,它引用了变量 num
int& ref = num; 

左值引用只能绑定到左值,不能直接绑定到右值。因为左值代表一个具有持久存储位置的对象,而右值通常是临时对象,生命周期短暂。
但是const左值引用可以引用右值: 是因为const的特性使得它不会去修改所引用对象的值。这样一来,当引用右值时,就不会出现修改临时对象(右值)的情况,从而保证了程序的安全性。同时,通过这种方式还可以延长右值的生命周期,使其在const左值引用的作用域内保持有效。

左值引用常用于函数参数传递,避免对象的拷贝,提高效率。同时,函数可以通过引用修改调用者提供的对象

右值引用是对右值的引用,使用&&符号来声明。它主要用于实现移动语义和完美转发。

int&& rref = 20; // 声明一个右值引用 rref,绑定到右值 20

右值引用只能绑定到右值,右值引用不能直接引用左值(但是右值引用可以引用move(左值))。这使得右值引用能够区分临时对象和持久对象,从而实现不同的处理逻辑。

// int num = 10;
// int&& rref1 = num; // 错误,num 是左值,不能绑定到右值引用
int&& rref2 = 20; // 正确,20 是右值

右值引用的主要用途之一是在对象所有权转移时避免不必要的拷贝,提高性能。通过右值引用,可以将临时对象的资源(如动态分配的内存)直接转移到新对象中。
右值引用使用场景:
移动构造函数和移动赋值运算符: 右值引用在移动构造函数和移动赋值运算符中起着关键作用,允许对象在所有权转移时高效地释放和获取资源。

需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量变
量表达式的属性是左值。

语法层⾯看,左值引用和右值引用都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1
汇编层实现,底层都是用指针实现的,没什么区别。

3.3移动构造和移动赋值

移动构造函数和移动赋值运算符是 C++11 引入的重要特性,它们主要用于高效地转移资源的所有权,避免不必要的深拷贝,提升程序性能。
移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引
⽤,但是不同的是要求这个参数是右值引引,如果还有其他参数,额外的参数必须有缺省值。

class BigObject {
private:
	int* data;
	size_t size;
public:
	// 构造函数,动态分配内存
	BigObject(size_t s) : size(s) {
		data = new int[size];
		for (size_t i = 0; i < size; ++i) {
			data[i] = i;
		}
	}
	// 拷贝构造函数,进行深拷贝
	BigObject(const BigObject& other) 
		: size(other.size) {
		data = new int[size];
		for (size_t i = 0; i < size; ++i) {
			data[i] = other.data[i];
		}
	  cout << "Copy constructor called" << endl;
	}

	// 移动构造函数,转移资源所有权
	BigObject(BigObject&& other) noexcept 
		: data(other.data)
		, size(other.size) 
	{
		other.data = nullptr;
		other.size = 0;
		cout << "Move constructor called" << endl;
	}

	// 析构函数,释放动态分配的内存
	~BigObject() {
		delete[] data;
	}
};

int main() {
	BigObject obj1(1000);
	// 调用移动构造函数
	BigObject obj2(std::move(obj1));
	return 0;
}

移动构造函数的主要作用是在对象初始化时,将一个临时对象的资源(如动态分配的内存)直接转移到新对象中,避免深拷贝带来的性能开销。

移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引用。
移动赋值运算符是一种特殊的赋值运算符,用于在对象已经存在的情况下,从一个临时对象(右值)中接管资源。

class MyString {
private:
	char* data;
	size_t length;

public:
	// 构造函数
	MyString(const char* str = "") {
		length = strlen(str);
		data = new char[length + 1];
		strcpy(data, str);
	}

	// 移动赋值运算符
	MyString& operator=(MyString&& other) noexcept {
		if (this != &other) {
			delete[] data;
			data = other.data;
			length = other.length;
			other.data = nullptr;
			other.length = 0;
		}
		return *this;
	}

	// 析构函数
	~MyString() {
		delete[] data;
	}

	// 打印字符串
	void print() const {
		cout << data << endl;
	}
};

int main() {
	MyString str1("Hello");
	MyString str2("World");
	// 使用移动赋值运算符将 str2 的资源转移到 str1
	str1 = std::move(str2);
	str1.print();
	return 0;
}

打印结果为World

总结:对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。

3.4引用折叠

引用折叠是 C++ 模板编程和完美转发中的一个重要概念,它主要用于处理模板参数为引用类型时的复杂情况,尤其是在涉及右值引用和万能引用(也叫转发引用)的场景。
C++11给出了⼀个引用折叠的规则:
T& & 折叠为 T&(左值引用 + 左值引用 = 左值引用)
T& && 折叠为 T&(左值引用 + 右值引用 = 左值引用)
T&& & 折叠为 T&(右值引用 + 左值引用 = 左值引用)
T&& && 折叠为 T&&(右值引用 + 右值引用 = 右值引用)
简单来说,右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。

// 用于接收左值引用的函数
template<typename T>
void fun1(T& value) {
	cout << value << endl;
}

// 用于接收右值引用的函数
template<typename T>
void fun2(T&& value) {
	cout << value << endl;
}

 //实现完美转发的函数模板
template<typename T>
void fun3(T&& arg) {
	cout << value << endl;
}

int main() {
	int num = 42;
	fun1(num);
	fun2(20);

	fun3(20);//传右值
	fun3(num);//传左值
	return 0;
}

fun3这样的函数模板中,T&& arg参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地⽅也把这种函数模板的参数叫做万能引用。

3.5完美转发

template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
}

Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化
以后是右值引用的Function函数。
但是结合之前的讲解,变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性,就需要使用完美转发实现。

#include <iostream>
#include <utility>

void process(int& val) {
    std::cout << "Lvalue: " << val << std::endl;
}

void process(int&& val) {
    std::cout << "Rvalue: " << val << std::endl;
}

template<typename T>
void forwardValue(T&& arg) {
    process(forward<T>(arg));//通过函数模板和 forward 实现了完美转发,确保参数在传递过程中保持原有的左值或右值属性。
}

int main() {
    int num = 10;
    forwardValue(num); // 传递左值
    forwardValue(20);  // 传递右值
    return 0;
}

4.可变参数模板

C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。

template <class ...Args> void Func(Args... args) {}
//可变参数函数模板template <class ...Args> void Func(Args&... args) {}
//可变参数函数模板(左值引用)template <class ...Args> void Func(Args&&... args) {}
////可变参数函数模板(右值引用)
// 可变参数函数模板
template<typename... Args>
void printArgs(Args... args) {
    // 函数体
}

typename... Args:这是一个模板参数包,Args 是一个模板参数包的名称,… 表示它可以包含零个或多个模板参数。
Args... args:这是一个函数参数包,args 是函数参数包的名称,它对应于模板参数包 Args,可以包含零个或多个函数参数。

可以使⽤sizeof...运算符去计算参数包中参数的个数。

template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;
}
int main()
{
	double x = 2.2;
	Print(); // 包里有0个参数
	Print(1); // 包里有1个参数
	Print(1, string("xxxxx")); // 包里有2个参数
	Print(1.1, string("xxxxx"), x); // 包里有3个参数
	return 0;
}

在这里插入图片描述
对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供用于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。

void ShowList()
{
	// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
	cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	cout << x << " ";
	// args是N个参数的参数包
	// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
	ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
	ShowList(args...);
}
int main()
{
	Print();
	Print(1);
	Print(1, string("xxxxx"));
	Print(1, string("xxxxx"), 2.2);
	return 0;
}

上面代码就可以对参数包进行扩展
运行结果:在这里插入图片描述
代码原理:
在这里插入图片描述
总结:可变参数模板为 C++ 编程带来了更高的灵活性和通用性,允许模板处理任意数量和类型的参数。通过递归展开或折叠表达式,可以方便地使用参数包中的参数。在实现通用容器、日志记录函数等场景中,可变参数模板发挥着重要作用。

5.lambda表达式

在传统 C++ 中,如果需要一个简单的函数,通常要定义一个具名函数或函数对象。Lambda 表达式则允许在需要的地方直接定义一个匿名的函数对象,避免了额外的命名和定义,使代码更加简洁和灵活。
Lambda 表达式的基本语法如下:

[capture list] (parameter list) mutable(可选) exception(可选) -> return type(可选) { function body }

捕获列表(capture list): 用于指定 Lambda 表达式可以访问的外部变量。可以是值捕获、引用捕获或混合捕获。
参数列表(parameter list): 与普通函数的参数列表类似,用于传递参数给 Lambda 表达式。
mutable(可选): 如果使用了 mutable,则可以修改通过值捕获的变量。
exception(可选): 用于指定 Lambda 表达式可能抛出的异常。
返回类型(return type): 可以显式指定返回类型,若省略,编译器会自动推导。
函数体(function body): 包含 Lambda 表达式的具体实现代码。
其中:
1.捕捉为空也不能省略
2.参数为空可以省略
3.返回值可以省略,可以通过返回对象自动推导
4.函数题不能省略

int main()
{
	// 一个简单的lambda表达式
	auto add1 = [](int x, int y)->int {return x + y; };
	cout << add1(1, 2) << endl;
	// 1、捕捉为空也不能省略
	// 2、参数为空可以省略
	// 3、返回值可以省略,可以通过返回对象⾃动推导
	// 4、函数体不能省略
	auto func1 = []
		{
			cout << "hello bit" << endl;
			return 0;
		};
	func1();
	return 0;
}

5.1捕捉列表

捕获列表决定了 Lambda 表达式如何访问外部变量,常见的捕获方式有以下几种:
第⼀种捕捉方式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,y, &z] 表示x和y值捕捉,z引用捕捉。

值捕捉:

#include <iostream>
using namespace std;
int main() {
    int x = 10;
    auto lambda = [x]() {
        cout << "Value of x: " << x << endl;
    };
    lambda();
    return 0;
}

[x] 表示通过值捕获外部变量 x,Lambda 表达式内部使用的是 x 的副本,对副本的修改不会影响外部的 x

引用捕捉:

#include <iostream>
using namespace std;
int main() {
    int x = 10;
    auto lambda = [&x]() {
        x = 20;
     cout << "New value of x: " << x << endl;
    };
    lambda();
    cout << "Value of x outside lambda: " << x << endl;
    return 0;
}

[&x] 表示通过引用捕获外部变量 x,Lambda 表达式内部对 x 的修改会影响外部的 x
第⼆种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会自动捕捉那些变量。

#include <iostream>
using namespace std;
int main() {
    int x = 10;
    int y = 20;
    auto lambda = [=]() {
        cout << "Value of x: " << x << endl;
        cout << "Value of y: " << y <<endl;
    };
    
	// 隐式值捕捉
	// ⽤了哪些变量就捕捉哪些变量
	auto func2 = [=]
		{
			int ret = a + b + c;
			return ret;
		};
	cout << func2() << endl;
	// 隐式引⽤捕捉
	// ⽤了哪些变量就捕捉哪些变量
	auto func3 = [&]
		{
			a++;
			c++;
			d++};
	func3();
	cout << a << " " << b << " " << c << " " << d << endl;
    lambda();
    return 0;
}

第三种捕捉⽅式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表⽰其他变量引用捕捉,xy值捕捉。当使用混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引用捕捉。

#include <iostream>
using namespace std;
int main() {
    int x = 10;
    int y = 20;
    auto lambda = [x, &y]() {
        cout << "Value of x: " << x << endl;
        y = 30;
        cout << "New value of y: " << y << endl;
    };
    lambda();
    cout << "Value of y outside lambda: " << y << endl;
    return 0;
}

5.2lambda的应用

在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义⼀个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单⼜方便。

#include<algorithm>
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 } };
	// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中
	// 不同项的比较,相对还是比较麻烦,那么这⾥lambda就很好⽤了
	sort(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price < g2._price;
		});
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price > g2._price;
		});
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate < g2._evaluate;
		});
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate > g2._evaluate;
		});
	return 0;
}

在这里例子中我们可以发现与仿函数相比,Lambda 表达式的优势在于它的简洁性和灵活性。使用仿函数需要定义一个结构体或类,并重载 () 运算符,代码相对繁琐。而 Lambda 表达式可以在需要的地方直接定义,无需额外的命名和定义,使代码更加简洁易读。特别是在需要临时定义一个简单的比较规则时,Lambda 表达式的优势更加明显。

Lambda的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有 Lambda和范围for这样的东西。范围for底层是迭代器,而Lambda底层是仿函数对象,也就说我们写了⼀个Lambda以后,编译器会生成⼀个对应的仿函数的类。

6.包装器

在 C++ 里,包装器指的是能够对可调用对象(像函数、函数指针、成员函数指针、仿函数、Lambda 表达式等)进行封装,从而让它们能够以统一的方式被调用和管理的工具

6.1function

template <class T>
class function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;

std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为 std::function 的⽬标。若 std::function 不含⽬标,则称它为空。调用空std::function 的目标导致抛出 std::bad_function_call 异常。

#include <iostream>
#include <functional>

// 普通函数
int add(int a, int b) {
    return a + b;
}

// 仿函数
struct Subtract {
    int operator()(int a, int b) {
        return a - b;
    }
};

int main() {
    // 包装普通函数
    std::function<int(int, int)> func1 = add;
    std::cout << "Add result: " << func1(3, 2) << std::endl;

    // 包装仿函数
    Subtract sub;
    std::function<int(int, int)> func2 = sub;
    std::cout << "Subtract result: " << func2(3, 2) << std::endl;

    // 包装Lambda表达式
    std::function<int(int, int)> func3 = [](int a, int b) {
        return a * b;
    };
    std::cout << "Multiply result: " << func3(3, 2) << std::endl;

    return 0;
}

6.2bind

std::bind 也在 <functional> 头文件中定义,它能够将可调用对象与其参数进行绑定,生成一个新的可调用对象。借助 std::bind,可以预先绑定部分参数,或者调整参数的顺序。

#include <iostream>
#include <functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
// 普通函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 绑定部分参数
    auto addFive = bind(add, _1, 5);
    cout << "Add five result: " << addFive(3) <<endl;

    // 调整参数顺序
    auto reverseAdd = std::bind(add, _2, _1);
    cout << "Reverse add result: " << reverseAdd(3, 2) << endl;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值