C++11详解
文章目录
- 前言
- 一、列表初始化
- 1.1 {} 初始化
- 1.2 initializer_list 类型
- 三、声明
- 3.1 auto
- 3.2 decltype
- 四、右值引用和移动语义
- 4.1 左值引用和右值引用
- 4.2 移动语义
- 五、可变参数模板
- 六、lambda表达式
- 各部分详细解释
- 示例代码
- 代码解释
- 七、包装器
- 八、bind
- 注意事项
前言
C++11在系统开发和库开发中表现出色,它简化了语法,让代码编写更加灵活,同时增强了稳定性和安全性。凭借丰富的新特性,C++11不仅功能更强大,还能显著提升开发效率。接下来,我们就一起学习C++11中那些好用的新增语法。
一、列表初始化
1.1 {} 初始化
在C++98的标准中,允许我们使用{}
对数组或结构体元素进行进行统一的列表初始值设定。
struct Point
{int _x;int _y;
};
int main()
{int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };//聚合初始化,感兴趣可以搜一下return 0;
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };//将申请的空间全部初始化为0//创建对象时也可以使用列表初始化方式调用构造函数初始化
class Date
{
public:Date(int year, int month, int day):_year(year),_month(month),_day(day){cout << "Date(int year, int month, int day)" << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2022, 1, 1); // C++98风格// C++11支持的列表初始化,这里会调用构造函数初始化,与聚合初始化区分开Date d2{ 2022, 1, 2 };Date d3 = { 2022, 1, 3 };return 0;
}
1.2 initializer_list 类型
std::initializer_list
是 C++11
引入的一个标准库类型,它提供了一种方便的方式来处理初始化列表。std::initializer_list
允许函数或构造函数接受任意数量的同类型参数,这在需要初始化一组值时非常有用。
int main()
{vector<int> v = { 1,2,3,4 };list<int> lt = { 1,2 };// 这里{"sort", "排序"}会先初始化构造一个pair对象map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };return 0;}
initializer_list
类型的引入后C++11的容器都提供了一个指持,使用该类型初始化的构造函数,这使得我们可以像上述方式传参。
三、声明
3.1 auto
C++11中auto,将其用于实现自动类型推断。这样就要求定义对象必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型,在定义一些类型较复杂的变量时,我们可以使用这种方法快速定义,如:
vector<int>::iterator it1 = v.begin();auto it2 = v.begin();
3.2 decltype
在C++中,auto
关键字的使用存在特定限制:必须对其声明的变量进行初始化,否则编译器将无法推导出auto
所代表的实际类型。这是由于auto
类型推导依赖于初始化表达式,而在编译期间,代码尚未执行,无法获取运行时的结果,因此对于那些需要基于运行时表达式结果进行类型推导的场景,auto
就显得力不从心。
此时,decltype
关键字应运而生。decltype
的核心功能是根据表达式的实际类型,推导出定义变量时所需的类型。例如,在定义变量时,decltype
能够精准地获取表达式的类型,并将其作为变量的定义类型,有效弥补了auto
在编译期类型推导方面的局限性,如:
四、右值引用和移动语义
4.1 左值引用和右值引用
在C++语言的发展历程中,引用机制始终是其重要特性之一。在传统C++语法中,引用概念早已存在,而随着C++11标准的发布,右值引用作为一项全新的语法特性被引入。为了区分这两种不同类型的引用,我们将此前学习的引用正式命名为左值引用。无论是左值引用还是右值引用,其本质都是为对象赋予别名。
左值、左值引用
在C++中,左值是一类具有明确内存存储位置的表达式,像变量名、解引用后的指针等都属于左值范畴。它们不仅允许获取地址,还能进行赋值操作,正因如此,左值可以出现在赋值运算符的左侧。与之相对,右值则不具备这种特性,无法置于赋值符号左边。当左值被const
修饰,虽然丧失了赋值权限,但仍保留取地址的能力。从本质上讲,能够执行取地址操作,正是左值的核心特征。而左值引用,便是专门针对左值设计的引用类型,其作用在于为左值创建别名。
// 以下的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;
右值、右值引用
在C++中,右值同样是用于表示数据的表达式,涵盖字面常量(如10
、"hello"
)、表达式运算结果以及函数返回值(排除以左值引用形式返回的情况)等。与左值不同,右值仅能出现在赋值运算符右侧,无法作为赋值目标置于等号左边,并且由于右值通常不具备固定存储位置,因此不允许对其执行取地址操作。右值引用就是对右值的引用,给右值取别名,一般右值引用会增加右值的生命周期。
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
在 C++ 里,右值引用的一些特性使得右值原本不能取地址的规则在特定情况下有所变化。
右值不能直接取地址
右值通常是临时对象,没有固定的存储位置,所以按常规不能直接对其取地址。比如,字面常量 10 就是一个右值,若尝试对它取地址,像 &10
这样的操作,编译器会报错。这是因为 10 作为一个临时存在的值,并没有被分配具体的内存空间供我们获取其地址。
右值引用使右值可被存储和取地址
当使用右值引用给右值取别名时,右值会被存储到一个特定的位置,此时就可以获取该位置的地址。示例代码如下:
int main() {int&& rr1 = 10; std::cout << "Address of rr1: " << &rr1 << std::endl; rr1 = 20; std::cout << "Value of rr1 after modification: " << rr1 << std::endl;return 0;
}
在这个例子中,rr1 是对右值 10 的右值引用。通过右值引用,10 被存储到了一个特定位置,我们可以用 &rr1
获取其地址。同时,由于 rr1 没有被const
修饰,我们还能对它进行修改,将其值从 10 改成 20。如果不希望右值引用所引用的值被修改,可以使用 const
修饰右值引用。
move
在一些特殊情况下我们需要使用右值引用来引用一个左值,这是后就可以使用move
,将表达式由左值属性强制转化为右值属性:
void func(int&& x)
{return;}
int main()
{int a = 0;func(move(a));int&& r = std::move(a);return 0;
}
万能引用
这块大家可看一下,这个使用涉及了两个问题
template<class T>
void func(T&& x)//使用模板的这种形式,被称为万能引用(转发引用)
{//既可以引用左值也可以引用右值,编译器会根据你传递的参数进行推导int&& r1 = x;//这里是的编译是无法通过的,需要再对x movereturn;
}
int main()
{int a = 0;func(move(a));return 0;
}
在函数func
内部,x
虽然在函数参数列表中是右值引用形式,但它们本身是左值。这是因为它们有名字,在函数作用域内有具体的存储位置,所以int&& r1 = x;
编译无法通过,需要对x
再次进行move
,这种方法太过于局限,一般我们使用forward<T>(x)
完美转发的形式来解决。
完美转发:允许我们在函数模板中精确地将参数传递给其他函数,同时保留参数的左值 / 右值属性和 const 属性。
#include <iostream>
#include <utility>// 辅助函数,用于接收左值
void printValue(int& value) {std::cout << "Lvalue: " << value << std::endl;
}// 辅助函数,用于接收右值
void printValue(int&& value) {std::cout << "Rvalue: " << value << std::endl;
}// 函数模板,使用完美转发
template <typename T>
void forwardValue(T&& value) {// 使用 std::forward 保持参数的原始值类别printValue(std::forward<T>(value));
}int main() {int a = 42;// 传递左值forwardValue(a);// 传递右值forwardValue(123);return 0;
}
无论是右值引用还是完美转发,都是为了给后面的移动语义做铺垫。
4.2 移动语义
左值引用的短板:
当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回,对于一些需要进行深拷贝的容器,这个代价是比较大的:
对于上述这种情况to_string
函数返回后str
所拥有的资源就会被释放,中间还要生成一份同样的临时资源用来初始化ret2
对象,而移动语义的出现就很完美的避免了资源频繁拷贝的问题。当str
被返回并准备调用拷贝构造,去拷贝ret2
对象时,编译器会识别出str
是将亡值,转而去调用,通过右值引用实现的移动构造,将str
的资源直接转移给ret2
对象。
在C++中,右值表达式按类型可分为两类:内置类型的右值称为纯右值,如字面常量、算术表达式结果;自定义类型的右值则称为将亡值,常见于函数返回的临时对象或移动操作前的对象。
string(string&& s):_str(nullptr),_size(0),_capacity(0)
{cout << "string(string&& s) -- 移动语义" << endl;swap(s);//转移资源
}
移动语义这种机制很好的避免深拷贝时间的开销,移动构造中没有开辟新开空间,拷贝数据,效率得到了显著提高。
不仅仅有移动构造,还有移动赋值:
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{bit::string ret1;ret1 = bit::to_string(1234);//编译器识别到to_string的返回值为将亡值,调用移动赋值转移资源return 0;
}
该处和移动构造实现原理是一样的,可以结合上面的一起理解,这部分知识还是很好理解的,这里就不画图展示了。
可以看出无论是右引用还是完美转发本质都是为了服务于移动语义。
五、可变参数模板
C++11 引入的可变参数模板是一项极为重要的新特性,它允许开发者创建能够接受可变数量参数的函数模板和类模板。在 C++98/03 标准里,类模板和函数模板所能容纳的模板参数数量是固定的,可变参数模板的出现无疑是一个重大突破,极大地增强了模板的灵活性和通用性。
不过,可变参数模板相对抽象,其使用需要掌握特定的技巧,具有一定的理解难度。在本篇内容中,我们将聚焦于可变参数模板的简单使用方法。
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template<class ...Args>
void func(Args ...args)
{}int main()
{func(1,2.1, 'a', "abc");//可以传递任意数量,任意类型的参数return 0;
}
在 C++ 可变参数模板的运用中,当参数 args
前面带有省略号时,它便成为了可变模板参数。我们将这种带省略号的参数称作“参数包”,其特点是能够容纳从 0 到 N(N 大于等于 0)个模板参数,极大地提升了模板的灵活性。
然而,我们无法直接访问参数包 args
里的每个参数,必须通过展开参数包的方式才能逐一获取其中的参数,这也正是可变模板参数使用的核心特点与最大挑战所在。具体而言,关键就在于如何巧妙地展开可变模板参数。由于语法层面并不支持采用 args[i]
这样直观的方式来获取可变参数,所以我们不得不借助一些特别的技巧,才能逐个获取参数包中的值。
template<class T>
void func(T&x)
{cout << x << " ";return;
}
template<class T,class ...Args>
void func(T x,Args ...args)
{cout << x << " ";func(args...);//此处传参比较特殊
}
int main()
{func(1,2.1, 'a', "abc");//编译器推导生成模板return 0;
}
在这段代码里,函数调用的执行流程如下:在主函数中,有 func(1, 2.1, 'a', "abc")
这样的调用语句。编译器会依据 void func(T x, Args ...args)
这个函数模板进行类型推导。具体来说,它会把实参列表中的第一个实参存储到形参 x
里,接着继续递归调用 func
函数。在递归调用时,会将剩余的参数组成的参数包 args
(这里是 2.1
, 'a'
, "abc"
)传递给下一次调用。这个过程会持续进行,直到参数包中仅剩下一个形参。此时,编译器会匹配到 void func(T& x)
这个函数,从而完成整个参数包的解析过程。
逗号表达式展开参数包
template <class T>
void PrintArg(T t)
{cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... };cout << endl;
}
int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));r
int arr[] = { (PrintArg(args), 0)... };
这行代码是核心部分。(PrintArg(args), 0)
是一个逗号表达式,逗号表达式会先计算左边的表达式 PrintArg(args)
,也就是调用 PrintArg
函数打印参数,然后返回右边的值 0。...
是参数包展开的语法,它会将参数包 args 中的每个参数依次展开,形成多个逗号表达式。具体来说,假设 args 包含参数 a, b, c,
那么 (PrintArg(args), 0)...
会展开为 (PrintArg(a), 0), (PrintArg(b), 0), (PrintArg(c), 0)
。
六、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());
}
这种方式比较笨重,在C++11中给出了lambda
的方法,简化了这一逻辑。
lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type {statement }
Lambda 表达式是 C++11 引入的一种匿名函数对象,它允许你在代码中快速定义一个轻量级的内联函数。下面详细解释其书写格式的各个部分:
各部分详细解释
[capture-list]
(捕获列表)- 捕获列表用于指定 lambda 表达式能够访问的外部变量。捕获列表可以为空,也可以包含一个或多个捕获项,不同捕获项之间用逗号分隔。
- 捕获方式主要有以下几种:
- 值捕获:使用变量名进行捕获,例如
[x]
表示按值捕获变量x
,lambda 表达式内部会创建一个x
的副本,在 lambda 表达式内部修改该副本不会影响外部的x
。 - 引用捕获:在变量名前加
&
进行捕获,例如[&x]
表示按引用捕获变量x
,lambda 表达式内部对x
的修改会影响外部的x
。 - 隐式捕获:可以使用
[=]
表示按值捕获所有外部变量,[&]
表示按引用捕获所有外部变量。也可以混合使用隐式捕获和显式捕获,例如[=, &x]
表示按值捕获所有外部变量,但按引用捕获x
。
- 值捕获:使用变量名进行捕获,例如
(parameters)
(参数列表)- 参数列表与普通函数的参数列表类似,用于指定 lambda 表达式接受的参数。参数列表可以为空,也可以包含一个或多个参数,不同参数之间用逗号分隔。
- 例如
(int a, double b)
表示 lambda 表达式接受一个int
类型的参数a
和一个double
类型的参数b
。
mutable
(可变修饰符,可选)- 默认情况下,按值捕获的变量在 lambda 表达式内部是只读的,不能修改。如果需要修改按值捕获的变量,可以使用
mutable
关键字。 - 例如
[x] mutable { x++; }
表示可以在 lambda 表达式内部修改按值捕获的变量x
,再次强调,此时依然不会修改外部的x
。
- 默认情况下,按值捕获的变量在 lambda 表达式内部是只读的,不能修改。如果需要修改按值捕获的变量,可以使用
-> return-type
(返回类型,可选)- 返回类型用于指定 lambda 表达式的返回值类型。如果 lambda 表达式的返回类型可以由编译器自动推导,则可以省略返回类型。
- 例如
-> int
表示 lambda 表达式的返回值类型为int
。
{ statement }
(函数体)- 函数体包含了 lambda 表达式的具体实现代码,可以包含任意的语句。
- 例如
{ return a + b; }
表示 lambda 表达式的功能是返回两个参数的和。
示例代码
#include <iostream>int main() {int x = 10;int y = 20;// 按值捕获 x 和 y,返回它们的和auto add = [x, y]() -> int { return x + y; };std::cout << "add result: " << add() << std::endl;// 按引用捕获 x 和 y,修改 x 的值auto modify = [&x, &y]() { x = 30; y = 40; };modify();std::cout << "x: " << x << ", y: " << y << std::endl;// 使用 mutable 关键字修改按值捕获的变量auto mutableTest = [x]() mutable { x = 50; std::cout << "mutable x: " << x << std::endl; };mutableTest();std::cout << "original x: " << x << std::endl;return 0;
}
代码解释
add
lambda 表达式:按值捕获x
和y
,返回它们的和。modify
lambda 表达式:按引用捕获x
和y
,在 lambda 表达式内部修改x
和y
的值,会影响外部的x
和y
。mutableTest
lambda 表达式:使用mutable
关键字修改按值捕获的变量x
,在 lambda 表达式内部修改x
的值不会影响外部的x
。
当掌握了lambda
表达式我们就可以使用下面的方式,编写比较逻辑了:
int main()
{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) {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; });
}
其实lambda
表达式的底层实现依然采用的是仿函数的形式,由于C11
的内容太多这里就不验证了,感兴趣的可以自行看看汇编逻辑。
七、包装器
在 C++ 中,function
是 <functional>
头文件中定义的一个类模板,它是一种通用、多态的函数包装器。function
可以存储、复制和调用任何可调用对象(如函数、Lambda 表达式、函数指针、成员函数指针、仿函数等)。
function
的模板参数是函数类型,指定了可调用对象的参数列表和返回类型。例如,function<int(int, int)>
表示可以存储接受两个 int
类型参数并返回bool
类型值的可调用对象:
struct Less {bool operator()(int x, int y)//仿函数{return x < y;}
};bool compare(int x, int y)//函数
{return x < y;
}
int main()
{auto f=[](int x, int y) {return x < y; };//lambda表达式function<bool(int, int)>func1 = f;function<bool(int, int)>func2 = Less();function<bool(int, int)>func3 = compare;cout << func1(1, 2) << endl;cout << func2(3, 2) << endl;cout << func3(2, 2) << endl;return 0;
}
借助包装器,我们成功地把这三种不同形式的比较操作,整合为统一的调用形式。如此一来,无论是哪种比较方式,在实际调用时都遵循相同的规则和流程,极大地提升了代码的一致性与可维护性。
在使用包装器进行统一时,必须保障他们的参数类型及返回值类型相同。
八、bind
std::bind
函数定义于 <functional>
头文件,它是一个功能强大的函数模板,灵活的函数包装器(也可称作适配器)。它能够接收任意可调用对象,如普通函数、成员函数、函数指针、Lambda 表达式等,然后生成一个全新的可调用对象。
通常情况下,我们可以利用 std::bind
对一个原本需要接收 N 个参数的函数进行处理。通过绑定部分参数,它会返回一个新函数,这个新函数接收 M 个参数(理论上 M 可以大于 N,但在实际应用中,这种做法的意义不大)。除此之外,std::bind
还具备调整参数顺序的能力。
// 原型如下:
template <class Fn, class... Args>
bind (Fn&& fn, Args&&... args);
我们可以通过bind
来个获得一个新的可调用对象,调整可调用对象个数,满足统一包装的条件。
int main()
{auto f=[](int x, int y) {return x < y; };function<bool(int, int)>func1 = f;function<bool(int, int)>func2 = Less();function<bool(int, int)>func3 = compare;function<bool(int, int)>func4 = bind(sum,2, placeholders::_1,placeholders::_2);func4(1, 2);return 0;
}
绑定成员函数:
#include <iostream>
#include <functional>class Calculator {
public:int multiply(int a, int b) {return a * b;}
};int main() {Calculator calc;// 绑定成员函数auto multiplyFunc = std::bind(&Calculator::multiply, &calc, std::placeholders::_1, std::placeholders::_2);int result = multiplyFunc(4, 6);std::cout << "Result: " << result << std::endl;return 0;
}
对于成员函数的绑定,需要传入对象的指针(或者引用),并且要使用取地址符 &
获取成员函数的地址。这里bind
把Calculator
类的 multiply
成员函数和对象 calc
的指针以及两个占位符绑定,生成了新的可调用对象 multiplyFunc
。
调整参数顺序
#include <iostream>
#include <functional>// 普通函数
int subtract(int a, int b) {return a - b;
}int main() {// 调整参数顺序auto reversedSubtract = std::bind(subtract, std::placeholders::_2, std::placeholders::_1);int result = reversedSubtract(8, 3);std::cout << "Result: " << result << std::endl;return 0;
}
注意事项
- 占位符的使用:占位符
std::placeholders::_n
中的n
表示在调用新的可调用对象时,第n
个参数的位置。 - 对象生命周期:如果绑定的是成员函数,要确保对象在新的可调用对象被调用时仍然有效。
- 性能开销:
std::bind
会带来一定的性能开销。
通过使用 std::bind
,可以让代码更加灵活,特别是在需要对函数的参数进行预绑定或者调整参数顺序的场景中非常有用。
``