C++11 部分新特性

一、类型推导

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

 下面这张表格可以更加方便记忆左值右值引用。

表 1 C++左值引用和右值引用
引用类型可以引用的值类型使用场景
非常量左值常量左值非常量右值常量右值
非常量左值引用YNNN
常量左值引用YYYY常用于类中构建拷贝构造函数
非常量右值引用NNYN移动语义、完美转发
常量右值引用NNYY无实际用途

六、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​​​​​​​

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值