目录
C++11简介:
1、统一的列表初始化:
std::initializer_list
2、自动类型推导:
auto:
decltype:
3、final 和 override
final:
override:
4、默认成员函数控制:
显示缺省函数:
删除默认函数:
5、左值和右值:
概念补充:
代码示例:
左值引用与右值引用:
两者的联系和区别:
右值引用的意义:
6、完美转发:
7、lambda表达式:
8、可变参数列表:
9、包装器:
function包装器:
bind(绑定):
C++11简介:
C++11,也被称为C++0x,是C++编程语言的一个重要更新版本,它于2011年正式被ISO标准委员会批准。相比于C++98,C++11带来了大量的新特性和改进,其中包含了约140个新特性,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。本篇主要介绍一些使用的多且实用的一些语法:
1、统一的列表初始化:
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。例如:
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自 定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
struct Student
{
int _x;
int _y;
};
int main()
{
// 两种写法一样的
int x1 = 1;
int x2{ 2 };
// 可以省去 =
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Student s{ 1, 2 };
return 0;
}
创建对象时也可以使用列表初始化方式调用构造函数初始化
class A
{
public:
// 如果不想让隐式类型转换发生可以加关键字: explicit
// explicit A(int x, int y)
A(int x, int y)
:_x(x)
, _y(y)
{}
// 单参数
A(int x)
:_x(x)
, _y(x)
{}
private:
int _x;
int _y;
};
int main()
{
// 多参数的隐式类型转换
// 本质是构造一个A对象,然后用这个对象去拷贝构造(编译器会进行优化, 就变成了直接构造)
A a1 = { 1,2 };
// 例如: A& a3 = {2, 2}; 这样子的就不行,但转换成下面的就可以
const A& a3 = { 2,2 }; // 因为右边会产生临时变量,但临时变量具有常性
// 当然也可以不加 = ,如 A a1{ 1,2 }; 但是不太推荐这么写
return 0;
}
std::initializer_list
initializer_list
是 C++11 引入的一个特性,用于表示某种类型对象的数组。它允许使用花括号 {} 包围的初始值列表来初始化对象或函数参数。initializer_list
通常与构造函数结合使用,以提供一种灵活的方式来初始化集合(如数组、向量等)或执行基于多个值的初始化。
(本质就是一个常量数组)
是个容器,但是没有新开空间,里面有两个指针,一个指向第一个元素(常量数组的开始),一个指向最后一个元素的后一个位置(常量数组的结束),所以是8/16字节,因为是两个指针(first, last)
2、自动类型推导:
auto:
auto 关键字用于自动类型推导。编译器会根据初始化表达式自动推断出变量的类型。使用 auto 可以让代码更加简洁,例如STL容器迭代器、函数返回类型等。
缺陷:
auto虽然用起来非常方便,但也不能滥用哦,例如在层层嵌套的函数当中,用起来是爽了,但是代码的可读性会大大下降,这时维护起来也会特别麻烦,一个变量可能得剥好几层才知道类型,还有在涉及到函数重载以及模板元编程时,容易引发错误和意外的行为。
decltype:
decltype 关键字用于查询表达式的类型。与 auto 不同,decltype 在编译时解析表达式并得到其类型,但不实际计算表达式的值。这意味着可以使用 decltype 来获得几乎任何表达式的类型,包括那些没有定义(或不可计算)的表达式。
缺陷:
decltype的语法相对复杂一点,且在某些情况下,decltype导出的类型可能非常冗长,特别是当表达式涉及到模板类型或复杂函数时。如果在函数声明中使用decltype时,如果函数的返回类型依赖于模板参数或函数参数的类型,那么可能需要使用尾置返回类型(trailing return type)语法,这可能会使函数声明的复杂性增加。
3、final 和 override
final:
final修饰类的时候,表示这个类无法被继承、修饰虚函数时,表示这个虚函数不能被重写。
修饰类:
修饰虚函数:
override:
override 关键字用于检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写编译报错。
4、默认成员函数控制:
显示缺省函数:
当你不为类定义任何构造函数时,编译器会为你生成一个默认构造函数。如果你显式地定义了其他构造函数(例如拷贝构造、移动构造还是其他自定义构造函数),但没有定义默认构造函数,编译器不会自动生成默认构造函数。如果还想编译器生成默认构造函数的话,可以在类定义中显式地使用 default 关键字来请求编译器生成默认构造函数。
删除默认函数:
如果想要一个类禁止被拷贝,在C++98当中的做法是将这个类的拷贝构造以及赋值重载直接声明为私有(private),在C++11中则更为简单一些,只需在该函数声明加上delete关键字即可:
5、左值和右值:
概念补充:
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用的返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
简记:左值能出现在右边,右值不能出现在左边。
代码示例:
左值引用与右值引用:
可以先简短的记着:左值引用就是给左值取别名,右值引用就是给右值取别名!
先来谈谈简单的:左值引用:
int main()
{
int a = 10;
int& b = a; // b就是a的别名,其实就是a
// 属于同一块地址空间:
cout << &a << endl;
cout << &b << endl;
return 0;
}
再来谈谈右值引用:
int main()
{
int x = 1, y = 2;
int&& z = (x + y);
string&& s1 = string("111111");
string&& s2 = to_string(1234);
int&& a = 10;
return 0;
}
两者的联系和区别:
右值引用的本身是左值,因为只有这样才能实现移动构造和移动赋值,以此来实现资源转移,因为普通左值是没法引用右值的,必须加const,但是加了又没法实现资源的转移。
所以右值引用是有地址的:
string&& s1 = string("111111");
cout << &s1 << endl;
如果右值引用的属性是右值,那么移动构造和移动赋值,要转移资源的语法逻辑是矛盾的,右值是不能被改变的,可以理解为右值带有const属性。
诶?那再回头来看看,既然右值引用是给右值取别名,那么右值引用有地址,那么是不是右值本身是有地址的???
其实还真是,像刚才的那种右值的底层是有地址的,但是只有编译器知道,为了语法的逻辑自洽,他不会让你取得到这个地址,右值引用s1其实就是变相获取到这个地址,所以能不能取到地址不是关键,关键是资源的转移(匿名对象和临时对象都带有const属性)
一般情况下左值引用只能引用左值,不能引用右值。但是加了const的左值引用既能引用左值也能引用右值。
一般情况下右值引用只能引用右值,不能引用左值。但是右值引用可以引用move以后的左值。
move函数:将一个左值对象转化为右值引用,从而允许使用移动语义来优化资源的管理和程序的性能。
右值引用的意义:
右值引用的意义主要体现在两个方面:移动语义、完美转发。
移动语义:
1、资源所有权转移:在C++中,类的右值通常是一个临时对象,如果在表达式结束时没有被绑定到引用,就会被废弃。通过右值引用,可以在对象被废弃之前移走其资源,实现资源的再利用,再需要时,就可以避免无意义的拷贝复制操作,提高效率。
2、减少开销:被移走资源的右值在废弃时已经成为空壳,其析构的开销会大大降低。这有助于提升程序的性能,特别是在处理大型对象或资源密集型操作时。
完美转发:
这里简单讲一下,下一标题再详细叙述。
完美转发的实现依赖于右值引用。右值引用(T&&)用于绑定到即将被销毁的对象上,从而允许资源的移动而非拷贝。
在模板函数中,通过使用 T&& 作为参数类型,能够接受左值和右值作为参数。
6、完美转发:
完美转发(Perfect Forwarding)是C++11中引入的一种编程技巧,其目的是在编写泛型函数时能够保留参数的类型和值类别(左值或右值),从而实现更为高效且准确地传递参数。
完美转发允许函数模板将其接收到的参数“完美”地转发给内部调用的其他函数,这里的“完美”指的是不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
省流:
完美转发就是在函数模板中,将参数转发给其他函数时,可以锁定参数的左右值属性和值类别。
因为右值引用的对象在作为实参传递时,属性会退化为左值,会直接匹配左值引用,使用完美转发可以保持它的右值属性。
这里来认识两个东西:forward函数和万能引用(T &&)
forward函数:是一个模板函数,用于实现完美转发
万能引用(T &&):既能接收左值也能接收右值。
直接上一段代码来直观感受一下:
7、lambda表达式:
lambda表达式从C++11标准开始引入的一种定义匿名函数对象的简洁方式。它可以捕获它所在作用域的变量,并可以在需要函数对象的任何地方使用,包括作为算法的一部分或作为回调函数。
基本语法:
[capture](parameters) mutable -> return_type { // 函数体 }
- capture:捕获列表,指定哪些外部变量在lambda函数体内是可用的。捕获列表可以是按值捕获(例如
[x]
)或按引用捕获(例如[&x]
),也可以混合使用(例如[&, x = this->x]
),其中&
表示按引用捕获所有外部变量,除了显式按值捕获的。=
表示按值捕获所有外部变量,除了显式按引用捕获的。- parameters:参数列表,与普通函数相同。如果lambda不接受任何参数,则可以省略括号。
- mutable:这是一个可选的说明符,表示lambda函数体内的代码可以修改按值捕获的变量。默认情况下,这些变量是只读的。
- return_type:返回类型,也是可选的。如果lambda体只有一个返回语句,且编译器能够从该语句推断出返回类型,则可以省略。如果lambda没有返回语句,则其返回类型为
void
。
也可以就简单的记为:[](){}
其实本质就是仿函数,编译器在编译的时候就会转成仿函数,原理类似范围for
举个小例子:我给这个lambda表达式传两个值,让它返回这两个值的和给我:
补充:
int main()
{
int a = 1, b = 2;
// 捕捉列表
auto swap1 = [a, b]() mutable // 还是传值捕捉
{
int tmp = a;
a = b;
b = tmp;
};
swap1();
// 如果不加mutable的话a, b捕捉到的默认是const的,无法对其做出修改操作,而且·此ab非局部的那俩ab
// 也就是就算带mutable修改内部也并不会改变外部
printf("%d %d\n", a, b);
// 引用捕捉可修改:
auto swap2 = [&a, &b]() {
int tmp = a;
a = b;
b = tmp;
};
swap2();
printf("%d %d", a, b);
// 捕捉方式还有 =, &
// = 为传值捕捉所有父作用域中的变量,包括this
// & 为引用捕捉所有父作用域中的变量,包括this
// 还可以混合捕捉: 表示为&捕捉全局,唯独b为传值捕捉(也可以反着来)
auto func = [&, b]() {};
return 0;
}
8、可变参数列表:
省流:可变参数列表一种特殊的函数参数机制,允许函数接收数量不确定的参数。
先来段简单的代码看看使用方法:
既然可变参数列表可以接收不确定数量的参数,那么仔细想想,好像printf函数也是如此,那么是不是就可以简单的模拟实现一下printf函数呢?
总结:可变参数列表提供了极高的灵活性和类型安全性,且支持泛型编程,但复杂性较高,可能会导致编译时间的增加和额外的性能开销。
9、包装器:
function包装器:
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。于头文件#include <functional>
中
其实包装器的本质就是函数指针,可调用对象有:函数、函数对象(仿函数)、lambda表达式、绑定表达式等。
先来看一个例子:来看看下面这段代码中的函数模板会被实例化几次?
template<class F, class T>
T Test(F f, T t)
{
static int x = 0;
cout << "x: " << ++x << endl;
cout << "x: " << &x << endl;
return f(t);
}
double f(double i)
{
return i / 2;
}
// 仿函数
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数:
cout << Test(f, 11.11) << endl;
// 函数对象(仿函数):
cout << Test(Functor(), 11.11) << endl;
// lambda表达式:
cout << Test([](double d) {return d / 4; }, 11.11) << endl;
return 0;
}
通过结果我们可以看出来,函数模板实例化了三次(静态变量x的地址都不一样)
对于同一个模板类或模板函数,使用多种不同的类型参数去实例化它时。每次使用不同的类型参数实例化模板,编译器都会生成一份独立的模板代码。这可能导致编译时间增加、二进制文件大小膨胀,以及潜在的运行时性能问题(如指令缓存未命中增加)。
但是我只想让这个函数模板实例化一次怎么办呢?
这时就可以掏出function包装器来解决:
function用法解析:
代码结果:
这样实例化出来的就是同一份模板了(x依次递增,且为同一地址),这样通过function包装器就可以减少不必要的模板实例化,因为模板实例化多了容易导致代码膨胀,降低效率。
最重要的是:好用(滑稽)
bind(绑定):
省流:调整可调用对象的参数个数或者顺序:
完整概念:
std::bind
用于生成一个新的可调用实体(function object),这个新的可调用实体可以把它的某些(或全部)参数绑定到给定的值上。这样,当你调用这个新的可调用实体时,就不需要再为那些已经被绑定的参数提供值了。(头文件:#include <functional>
)
基本语法:
auto newCallable = bind(callable, arg_list, placeholders::_1, placeholders::_2, ...);
- callable:你想要调用的函数、函数对象、成员函数指针或成员对象指针。
- arg_list:你希望预先绑定到 callable 的参数列表。这些参数在后续调用 newCallable 时将不再需要。
- placeholders::_1, placeholders::_2, ...:占位符,用于表示 newCallable 被调用时,需要接收的参数位置。允许保留一些参数位置以供将来调用时提供。
上个简单的代码看看:
再来看一组:也可以绑定类中的函数:
注意:
绑定成员函数时,需要传递成员函数的地址(使用&
运算符),并且还需要传递一个指向对象实例的指针(对于非静态成员函数)或对象的引用(但通常期望指针,因为bind需要能够存储这个引用所指向的对象的状态,而局部引用在函数返回后可能不再有效)。