一、简介
在C++11标准出来之前,一直是C++98/03标准占引领地位,而C++98/03标准是C++98标准在2003年将存在的一些漏洞进行了修复,但并没有核心语法的改动。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能更好的提升开发效率。本文主要对一些常用的新特性进行讲解。
二、列表初始化 {}
2.1 C++98中的{}
C++98标准中,允许使用{}对数组或结构体元素进行统一的列表初始化:
struct Test
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 }; // 数组列表初始化
int array2[5] = { 0 }; // 数组列表初始化
Test t = { 1, 2 }; // 结构体列表初始化
return 0;
}
2.2 C++11中的{}
在新出的C++11标准中,扩大了{}的使用范围,可以对所有的内置类型和用户自定义的类型使用初始化列表{},可以使用或不使用等号(=)
struct Test
{
int _x;
int _y;
};
int main()
{
int array1[5] = { 1, 2, 3, 4, 5 }; //使用等号
int array2[5]{ 1, 2, 3, 4, 5 }; //不使用等号
int x = 1;
int y{ 1 }; // 这种初始化不使用等号,看着感觉怪怪的,所以还是建议使用等号进行初始化
Test t1 = { 1, 2 }; //使用=
Test t2{ 1,2 }; // 不使用=
//STL容器的使用
vector<int> v{1,2,3,4,5};
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
return 0;
}
对于自定义类型的列表初始化:
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << _year<<"-"<<_month<<"-"<<_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1); // 原始的初始化方法
// 使用C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };
return 0;
}
2.3 std::initializer_list
官方文档,点击查看
为什么C++11会支持新的{}的列表初始化呢?
因为以上的所有类型都支持了以std::initializer_list作为参数的构造函数。
比如list,vector, map等:
三、声明
3.1 auto
在之前的C++98标准中,auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,使得auto没有任何的价值体现,所以,在C++11中就废除了原先的auto用法,定义了一个全新的auto。C++11中,将其用于实现自动类型的推导,要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。使用auto,能使程序的书写提供便利及整体代码的简洁性。
int i = 10;
auto p = &i; // 推导出p的类型应该为int*
auto pf = "string"; // pf: char const*
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//使用auto自行推导出迭代器的类型,使代码简洁
map<string, string>::iterator it1 = dict.begin();
auto it2 = dict.begin();
3.2 decltype
auto进行类型推导时必须进行显示初始化,否则auto是无法进行推导,编译器编译不过。而关键字decltype则将变量的类型声明为表达式指定的类型。如下使用:
const int x = 2;
double y = 3.3;
decltype(x * y) ret; // 推出ret的类型是double
decltype(&x) p; // 推出p的类型是int *
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
3.3 nullptr
// C++中NULL的定义
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
// C++中nullptr的定义
#if defined(nullptr)
#define nullptr EMIT WARNING C4005
#error The C++ Standard Library forbids macroizing the keyword "nullptr". \
Enable warning C4005 to find the forbidden define.
#endif // nullptr
在C++中,NULL被定义成字面量0,在使用的过程中可能会带来一些问题,因为0既能表示指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针,使用nullptr更为安全。
四、final 与 override
final的作用:
a. C++11中,将类标记为final后,该类会禁止被继承。
b. 用final修饰虚函数是,该虚函数将不可被重写。
override的作用(用于派生类函数):
a. override用于在派生类中显式地重写基类中的虚函数。b. 它可以确保在派生类中正确地覆盖基类的虚函数,并且如果没有正确地重写,则会产生编译错误。
五、=default 和 =delete
强制生成默认函数关键字default:
C++11可以更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成.
class A
{
public:
A() = default; //让编译器默认生成无参构造函数
A(int num) // 存在有参构造,编译器不会默认生成无参构造,所有需要用default让编译器生成一下
:_num(num)
{}
void fun()
{
cout << "A::func()" << endl;
}
private:
int _num;
};
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
class A
{
public:
A(int a) : _a(a){}
//C++11
A(const A&) = delete; //禁止编译器生成默认的拷贝构造函数
A& operator=(const A&) = delete; //禁止编译器生成赋值运算符重载
private:
int _a;
//C++98,设置成private
A(const A&) = delete;
A& operator=(const A&) = delete;
};
六、可变参数模板
介绍
可变参数模板是指可以定义0到任意个模板参数的模板,它可以用来实现高度泛化的函数或类。可变参数模板的语法是使用省略号(…)来表示一个或多个参数。比如一个我们很熟悉的函数的参数就是可变参数:printf。它不仅可以接受不同数量的参数,还能接受不同类型的参数。
模板结构:
template<class …Args> 返回类型 函数名(Args… args) { //函数体 }
示例:
// Args是一个模板参数包,args是一个函数形参参数包 // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数 template<class ...Args> void ShowList(Args... args) { //函数体 }
作用
函数参数的数量不确定性: 可变参数模板允许函数接受任意数量的参数,可以在函数体内对这些参数进行处理。这在实现一些工具函数、容器类或者模板库时非常有用,比如
printf
风格的输出函数。递归模板展开: 可变参数模板可以通过递归展开,用于在编译时展开模板参数的各个部分。这在实现一些递归算法或数据结构时非常有用,例如元组、可变参数列表的操作等。
多态参数: 可变参数模板使得可以将不同类型的参数传递给同一个函数或类模板,并在函数内部根据参数的类型来执行不同的操作,从而实现更通用的函数。
日志记录和调试: 可变参数模板可以用于实现灵活的日志记录和调试函数,能够根据不同的调用情况输出不同的信息。
类型安全的变参函数: 可变参数模板相对于C语言中的可变参数函数(如
printf
)来说,能够提供更好的类型安全性,因为在编译时就可以对参数类型进行检查。如下例子展示如何使用可变参数模板来实现递归求和:
#include <iostream>
// 递归模板展开,计算可变数量参数的和
template<typename T>
T sum(T value) {
return value;
}
template<typename T, typename... Args>
T sum(T value, Args... args) {
return value + sum(args...);
}
int main() {
int res = sum(1, 2, 3, 4, 5);
std::cout << "sum: " << res << std::endl; // 输出 15
return 0;
}
因为语法不支持使用args[i]这样方式获取可变参数,所以就通过递归的方式将参数包进行展开,然后对参数包里面的每一种参数类型再进行求和。
七、lambda表达式
介绍
Lambda表达式允许在需要函数对象(函数指针、函数符号)的地方使用内联函数,而不需要再单独的去定义一个函数或函数对象来实现某种简单的功能。
格式:
[capture](parameters) mutable -> return_type { CODE; // Lambda函数的代码 }
capture
:捕获列表,用于捕获外部变量。编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文的变量供lambda函数使用。parameters
:与普通函数的参数列表类似,表示传递给Lambda函数的参数,没有参数需要传递就可以空着不写。mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量
性。使用该修饰符时,参数列表不可省略(即使参数为空)return_type
:Lambda函数的返回类型。没有返回值时此部分可省略,返回值类型明确情况下,也可省略,由编译器对返回类型进行推导- CODE:Lambda函数的实际代码。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
lambda的参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 但是该lambda函数不能做任何事情。
捕获列表说明:
- [] 不捕获任何变量
- [var]:表示值传递方式捕捉变量var(传值捕获)
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获foo变量
- [&var]:表示传引用捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量; [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
- 在块作用域以外的lambda函数捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
示例:
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[]{};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c){b = a + c; };
fun1(10);
cout<<a<<" "<<b<<endl; // 3 13
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int{return b += a+ c; };
cout<<fun2(10)<<endl; // 26
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl; // 30
return 0;
}
通过lambda写一个自定义排序:
//商品类
struct Goods
{
string _name; // 名字
double _price; // 价格
int _eval; // 评价
};
//按价格的升序
struct Cmpup
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price < g2._price;
}
};
//按价格降序
struct Cmpdest
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100 }, { "橙子", 2.2, 1000 }, { "菠萝", 1.5, 1 } };
sort(v.begin(), v.end(), Cmpup()); //按价格升序排序
sort(v.begin(), v.end(), Cmpdest()); //按价格降序排序
// 使用lambda表达式自定义排序
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._eval < g2._eval; });
sort(v.begin(), v.end(),
[](const Goods& g1, const Goods& g2) {return g1._eval > g2._eval; });
return 0;
}
八、包装器 std::function
函数包装器 `std::function` 是C++11中的一个重要特性,它提供了一种能够存储和调用可调用对象(函数、函数指针、成员函数、lambda表达式等)的通用方式。std::function 是一个模板类,它可以用来封装各种不同类型的函数或可调用对象,从而实现了一种统一的接口来进行函数调用。
使用的头文件
#include <functional>
格式:
#include <functional> std::function<return_type(parameter_types)> func;
return_type
:函数的返回类型。parameter_types
:函数的参数类型列表。func
:std::function
类型的对象,可以用来存储和调用可调用对象。
示例:
#include <functional>
using namespace std;
// 普通函数
void printHello() {
std::cout << "Hello, ";
}
void printWorld() {
std::cout << "world!" << std::endl;
}
int add(int a, int b) {
return a + b;
}
int main() {
// 存储不同的函数
std::vector<std::function<void()>> tasks;
tasks.push_back(printHello);
tasks.push_back(printWorld);
for (const auto& task : tasks) {
task(); // 调用存储的函数 会分别调用printHello和printWorld两个函数
}
// 存储普通函数
std::function<int(int, int)> func = add;
std::cout << func(3, 4) << std::endl; // 输出 7
// 存储lambda表达式
std::function<int(int)> square = [](int x) -> int {
return x * x;
};
std::cout << square(5) << std::endl; // 输出 25
return 0;
}
九、 右值引用
介绍
在了解右值引用前,需要先知道什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时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;
右值引用与左值引用就差不多相反:右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址,右值不可以直接修改。右值引用又可以分为两类:纯右值、将亡值。右值引用就是对右值的引用,给右值取别名。
所以:无论左值引用还是右值引用,都是给对象取别名。
右值引用语法:&&
// 以下几个都是常见的右值
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;
左、右值引用比较
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
- 左值引用必须进行初始化
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值
- 右值引用必须进行初始化
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
十、完美转发
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 这就需要用到完美转发。它解决了在函数模板中传递参数时丢失信息的问题, 完美转发使得函数模板能够将参数原封不动地传递给其他函数,包括其参数类型和值类型,同时保留参数的左值或右值属性。
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; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::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;
}