一、类型推导
1.auto
auto可以让编译器在编译期就推导出变量的类型
(1)auto的使⽤必须⻢上初始化,否则⽆法推导出类型
(2)auto在⼀⾏定义多个变量时,各个变量的推导不能产⽣⼆义性,否则编译失败
auto a = 2, b = 3;
auto a = 2, b = 3, c = 's';//编译错误,前面推导出的类型是int,c的类型是char
(3)auto不能⽤作函数参数
(4)在类中auto不能⽤作⾮静态成员变量
(5)auto不能定义数组,可以定义指针
(6)auto⽆法推导出模板参数
(7)在不声明为引⽤或指针时,auto会忽略等号右边的引⽤类型和cv限定
(8)在声明为引⽤或者指针时,auto会保留等号右边的引⽤和cv属性
cv推导规则,cv是指const 和volatile。
const int a = 3;
auto pp = a; //int
auto* ppp = &a; //const int*
auto& p = a; //const int&
(9)auto被视作是一个占位符,并不是一个他自己的类型,因此不能用于类型转换或其他一些操作,如sizeof和typeid
对于一些简单的变量类型来说,不建议使用auto,应该直接写出变量类型,这样更加清晰易懂。
auto适用于类型冗长复杂、使用范围专一的变量,如:
std::vector<int> v;
for (auto i = v.begin(); i < v.end(); i++)//i的类型为std::vector<int>::iterator
{
cout << *i << endl;
}
2.decltype
decltype则⽤于推导表达式类型,这⾥只⽤于编译器分析表达式的类型,表达式实际不会进⾏运算。
decltype不会像auto⼀样忽略引⽤和cv属性,decltype会保留表达式的引⽤和cv属性。
对于decltype(exp)有:
1. exp是表达式,decltype(exp)和exp类型相同。
2. exp是函数调⽤,decltype(exp)和函数返回值类型相同。
3. 其它情况,若exp是左值,decltype(exp)是exp类型的左值引⽤。
int i;
decltype(i) j; //int
decltype(1 + 5) jj; //int
decltype(add(1,2.2)) jjj; //float
auto和decltype的配合使用:返回值类型后置
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。
int a = 4;
const int* b = &a; // b是底层const
decltype(auto) c = b;//c的类型是const int* 并且指向的是a
二、返回值类型后置
在泛型编程中,可能需要通过参数的运算来得到返回值的类型。考虑下面这个场景:
template <typename R, typename T, typename U>
R add(T t, U u)
{
return t+u;
}
int a = 1; float b = 2.0;
auto c = add<decltype(a + b)>(a, b);
我们并不关心 a+b 的类型是什么,因此,只需要通过 decltype(a+b) 直接得到返回值类型即可。但是像上面这样使用十分不方便,因为外部其实并不知道参数之间应该如何运算,只有 add 函数才知道返回值应当如何推导。
因此,在 C++11 中增加了返回类型后置(trailing-return-type,又称跟踪返回类型)语法,将 decltype 和 auto 结合起来完成返回值类型的推导。
返回类型后置语法是通过 auto 和 decltype 结合起来使用的。上面的 add 函数,使用新的语法可以写成:
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}
三、lambda匿名函数
所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式。
定义一个 lambda 匿名函数很简单,可以套用如下的语法格式:
[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
函数体;
};
其中各部分的含义分别为:
1) [外部变量方位方式说明符](必写)
[ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些“外部变量”。
所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。
2) (参数)
和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;
3) mutable
此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。
注意,对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量;
4) noexcept/throw()
可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。
值得一提的是,如果 lambda 函数标有 noexcept 而函数体内抛出了异常,又或者使用 throw() 限定了异常类型而函数体内抛出了非指定类型的异常,这些异常无法使用 try-catch 捕获,会导致程序执行失败(本节后续会给出实例)。
5) -> 返回值类型
指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略 "->返回值类型 "。
6) 函数体
和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。
需要注意的是,外部变量会受到以值传递还是以引用传递方式引入的影响,而全局变量则不会。换句话说,在 lambda 表达式内可以使用任意一个全局变量,必要时还可以直接修改它们的值。
标红部分为lambda表达式必写项。
比如,如下就定义了一个最简单的 lambda 匿名函数:
[]{} //没有任何功能的lambda函数
lambda匿名函数中的[外部变量]:写法如下表所示。
格式 | 功能 |
---|---|
[] | 空方括号表示当前 lambda 匿名函数中不导入任何外部变量。 |
[=] | 只有一个 = 等号,表示以值传递的方式导入所有外部变量; |
[&] | 只有一个 & 符号,表示以引用传递的方式导入所有外部变量; |
[val1,val2,...] | 表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序; |
[&val1,&val2,...] | 表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序; |
[val,&val2,...] | 以上 2 种方式还可以混合使用,变量之间没有前后次序。 |
[=,&val1,...] | 表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。 |
[this] | 表示以值传递的方式导入当前的 this 指针。 |
注意,单个外部变量不允许以相同的传递方式导入多次。例如 [=,val1] 中,val1 先后被以值传递的方式导入了 2 次,这是非法的。
lambda函数使用示例:
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int num[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行排序
sort(num, num+4); //默认升序
sort(num, num+4, [=](int x, int y) -> bool{ return x < y; }); //作用同上
sort(num, num+4, [=](int x, int y) -> bool{ return x > y; }); //降序排序
for(int n : num){
cout << n << " ";
}
return 0;
}
以上代码是使用sort()函数对数组进行升序排序。其中第二个sort的lambda函数和sort函数内置的sort_up() 函数功能完全相同。而第三个sort中的lambda函数可以实现数组的降序排序。由此可见lambda函数十分灵活。
除此之外,虽然 lambda 匿名函数没有函数名称,但我们仍可以为其手动设置一个名称,比如:
//print 即为 lambda 匿名函数的函数名
auto print = [](int a,int b) -> void{cout << a << " " << b;};
//调用 lambda 函数
print(1,2); //运行结果: 1 2
值传递和引用传递的区别:
#include <iostream>
using namespace std;
//全局变量
int all_num = 0;
int main()
{
//局部变量
int num_1 = 1;
int num_2 = 2;
int num_3 = 3;
cout << "lambda1:\n";
auto lambda1 = [=]{
//全局变量可以访问甚至修改
all_num = 10;
//函数体内只能使用外部变量,而无法对它们进行修改
cout << num_1 << " "
<< num_2 << " "
<< num_3 << endl;
};
lambda1(); //1 2 3
cout << all_num <<endl; //10
cout << "lambda2:\n";
auto lambda2 = [&]{
all_num = 100;
num_1 = 10;
num_2 = 20;
num_3 = 30;
cout << num_1 << " "
<< num_2 << " "
<< num_3 << endl;
};
lambda2(); //10 20 30
cout << all_num << endl; //100
return 0;
}
其中,lambda1 匿名函数是以 [=] 值传递的方式导入的局部变量,这意味着默认情况下,此函数内部无法修改这 3 个局部变量的值,但全局变量 all_num 除外。相对地,lambda2 匿名函数以 [&] 引用传递的方式导入这 3 个局部变量,因此在该函数的内部不就可以访问这 3 个局部变量,还可以任意修改它们。同样,也可以访问甚至修改全局变量。
当然,如果我们想在 lambda1 匿名函数的基础上修改外部变量的值,可以借助 mutable 关键字,例如:
auto lambda1 = [=]() mutable{
num_1 = 10;
num_2 = 20;
num_3 = 30;
//函数体内只能使用外部变量,而无法对它们进行修改
cout << num_1 << " "
<< num_2 << " "
<< num_3 << endl;
};
由此,就可以在 lambda1 匿名函数中修改外部变量的值。但需要注意的是,这里修改的仅是 num_1、num_2、num_3 拷贝的那一份的值,真正外部变量的值并不会发生改变。
四、智能指针
C++ 智能指针底层是采用引用计数的方式实现的。简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为 1),每当有新对象使用此堆内存时,该整形值 +1;反之,每当使用此堆内存的对象被释放时,该整形值减 1。当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。
1.shared_ptr智能指针
实际上,每种智能指针都是以类模板的方式实现的,shared_ptr 也不例外。shared_ptr<T>(其中 T 表示指针指向的具体数据类型)的定义位于<memory>
头文件,并位于 std 命名空间中。
值得一提的是,和 unique_ptr、weak_ptr 不同之处在于,多个 shared_ptr 智能指针可以共同使用同一块堆内存。并且,由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针(只有引用计数为 0 时,堆内存才会被自动释放)。
①创建
std::shared_ptr<int> p1; //不传入任何实参
std::shared_ptr<int> p2(nullptr); //传入空指针 nullptr
std::shared_ptr<int> p3(new int(10)); //也可以明确指向
std::shared_ptr<int> p3 = std::make_shared<int>(10); //同上
注意,空的 shared_ptr 指针,其初始引用计数为 0,而不是 1。
②使用示例
#include <iostream>
#include <memory>
using namespace std;
int main()
{
//构建 2 个智能指针
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2(p1);
//输出 p2 指向的数据
cout << *p2 << endl;
p1.reset();//引用计数减 1,p1为空指针
if (p1) {
cout << "p1 不为空" << endl;
}
else {
cout << "p1 为空" << endl;
}
//以上操作,并不会影响 p2
cout << *p2 << endl;
//判断当前和 p2 同指向的智能指针有多少个
cout << p2.use_count() << endl;
return 0;
}
2.unique_ptr
①unique_ptr”唯⼀”拥有其所指对象,同⼀时刻只能有⼀个unique_ptr指向给定对象,离开作⽤域时,若其指向对象,则将其所指对象销毁(默认 delete)。
②定义unique_ptr时 需要将其绑定到⼀个new返回的指针上。
③unique_ptr不⽀持普通的拷⻉和赋值(因为拥有指向的对象) 但是可以拷贝和赋值⼀个将要被销毁的unique_ptr;可以通过release或者reset将指针所有权从⼀个(⾮const) unique_ptr转移到另⼀个unique。
3. weak_ptr
①weak_ptr是为了配合shared_ptr⽽引⼊的⼀种智能指针 它的最⼤作⽤在于协助shared_ptr⼯作,像旁观者那样观测资源的使⽤情况,但weak_ptr没有共享资源,它的构造不会引起指针引⽤计数的增加。
②和shared_ptr指向相同内存 shared_ptr析构之后内存释放,在使用之前使⽤函数lock()检查weak_ptr是否为空指针。
③weak_ptr<T>模板类中没有重载 * 和 -> 运算符,这就意味着,weak_ptr类型指针只能访问所指向的堆内存,而不能对其进行修改。
五、右值引用
左值右值:
左值: 可以放在等号左边,可以取地址并有名字。
右值: 不可以放在等号左边,不能取地址,没有名字。
字符串字⾯值"abcd"也是左值,不是右值。
++i、--i是左值,i++、i--是右值。
我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的。
为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 "&&" 表示。
需要注意的,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;
和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:
int && a = 10;
a = 100;
cout << a << endl; //100
下面这张表格可以更加方便记忆左值右值引用。
引用类型 | 可以引用的值类型 | 使用场景 | |||
---|---|---|---|---|---|
非常量左值 | 常量左值 | 非常量右值 | 常量右值 | ||
非常量左值引用 | Y | N | N | N | 无 |
常量左值引用 | Y | Y | Y | Y | 常用于类中构建拷贝构造函数 |
非常量右值引用 | N | N | Y | N | 移动语义、完美转发 |
常量右值引用 | N | N | Y | Y | 无实际用途 |
六、nullptr
nullptr是⽤来代替NULL,⼀般C++会把NULL、0视为同⼀种东西,这取决去编译器如何定义NULL,有的定义为 ((void*)0),有的定义为0 。编译器一般对其实际定义如下:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
C++不允许直接将void* 隐式转换到其他类型,在进⾏C++重载时会发⽣混乱 。
例如:
void foo(char*);
void foo(int);
当传入参数为NULL,即foo(NULL)时,此时NULL为0,会去调⽤foo(int),从⽽发⽣混乱。 为解决这个问题,需要使⽤NULL时,⽤nullptr代替: C++11引⼊nullptr关键字来区分空指针和0。
nullptr 的类型为 nullptr_t,能够转换为任何指针或成员指针的类型, 也可以进⾏相等或不等的⽐较。
七、范围for循环
基于范围的迭代写法,for(变量:对象)表达式。
如:
//字符串
string str = "hello world";
for (char i : str)
{
cout << i;
}
//vector数组
vector<int> num { 1,2,3,4,5 };
//使用迭代器
for (auto i = num.begin(); i < num.end(); i++)
{
cout << *i << endl;
}
//范围for循环
for (int i : num)
{
cout << i << endl;
}
八、列表初始化
C++定义了⼏种初始化⽅式,例如对⼀个int变量 x 初始化为0:
int x = 0;
int x = { 0 };
int x(0);
int x{ 0 };
//数组初始化也可以用
int arr[] = { 1,2,3 }; //之前的数组初始化
int arr[]{ 1,2,3 };
采⽤花括号来进⾏初始化称为列表初始化,⽆论是初始化对象还是为对象赋新值。 ⽤于对内置类型变量时,如果使⽤列表初始化,且初始值存在丢失信息⻛险时,编译器会报错。
double a = 1.222333;
int b{ a }; //编译错误,从“double”转换到“int”需要收缩转换
//即会发生信息丢失,转换不会执行
int c = a; //警告,但是转换依旧执行,结果为 1
cout << c << endl;
九、常量表达式
这个在我的另一篇博客中有详细介绍,详见:关于const和constexpr