C++11 新特性总结——全干货(上)

序言

作为一名C/C++程序员,C++11的新特性想必大家多多少少都了解一些。对于实际项目,C++11的很多新特性给每一位C++程序员带来了不少的便利,例如智能指针,正则,自动推导,多线程编程等等;零一方面,对于面试,C++11又是很多面试的高频考点。因此,掌握C++11的新特性真是百利而无一害。对于BOOST库比较熟悉的人应该知道,实际上C++11许多的特性都是从BOOST中参考过来的。BOOST库被誉为是准标准库,有时间学习学习BOOST库也是一个不错的选择。好了,废话不多说,下面一起来看一下C++11到底引入了哪些新的特性。Let’s Go!。

C++11新特性

1.关键字nullptr

关键词nullptr是用来取代NULL的,它表示指针字面量,是一个std::nullptr_t类型的纯右值。一般而言,有些编译器会把NULL直接定义为0,而一些编译器则会把NULL定义为((void*)0)。所以这会导致一些问题,如下:

void fun(int* p);
void fun(int a);

接下来来分析一下下面函数的调用情况:

int *p = NULL;
fun(*p);

在C++中,不允许void*隐式转换到其他类型,所以对于int p = NULL的定义,如果NULL被定义为((void)0),那么NULL只能被定义为0;一旦NULL被定义为0,那fun(*p)将会去调用fun(int a)的函数,这样的代码调用起来很是奇怪,为了解决这样的问题,nullptr关键字就被用来区分0和空指针。
nullptr 存在到任何指针类型及任何成员指针类型的隐式转换,同样的转换对于任何空指针常量也存在,空指针常量包括 std::nullptr_t 的值,以及宏 NULL。
看如下测试代码(来源于https://zh.cppreference.com):

#include <cstddef>
#include <iostream>
 
template<class T>
constexpr T clone(const T& t)
{
    return t;
}
 
void g(int*)
{
    std::cout << "Function g called\n";
}
 
int main()
{
    g(nullptr);        // ok
    g(NULL);           // ok
    g(0);              // ok
 
    g(clone(nullptr)); // ok
//  g(clone(NULL));    // error
//  g(clone(0));       // error
}

最后两个函数的调用是错误的,因为非字面量的0,是不可以作为空指针常量的。总之,为了避免不必要的麻烦,对空指针进行定义是最好采用nullptr来赋值为佳。

2.auto和decltype关键字

1.auto基本用法

C++11引入了auto和decltype关键字,他们可以在编译期间就自动推导出变量或者表达式的类型。或许有些人就纳闷了,在声明变量的时候,一般都是指定了变量的类型,为什么要用auto?好吧,像一些简单类型例如int,double等基本数据类型就没有必要使用auto了,但是对于一些复杂的类型使用auto就真香警告了。下面简单列出auto的基本用法:

auto a = 1;//自动推导 a为int型
auto b = 1.0;//自动推导b为double型

可以看到,auto可以通过=右边的类型推导出变量的类型。
再来看一个迭代器的例子:

for(vector<int>::iterator iter = vec.begin(); iter != vec.end(); ++iter)

传统的迭代器遍历如上使用vector::iterator来定义iter的类型,但是有了auto之后,就可以如下修改上述代码:

for(auto iter = vec.begin(); iter != vec.end(); ++iter)

emmm…这样看起来简洁多了,艾玛,真香(此处读者自行脑补真香表情包)。
虽然auto用起来挺香,但是其也有诸多限制。

2.auto的限制
a.使用auto声明变量时必须马上初始化,否则auto无法推导出变量的类型。
b.auto无法推导模板参数
c.如果在一行使用auto同时定义好几个变量,则这些变量的推导不能出现二义性
d.auto不能用作推导函数参数
e.auto不能定义数组
f.auto不能和其他任何类型说明符一起使用

auto i;//error:i没有被初始化,无法推导类型
auto a = 1, b = 2.0;//error:推导产生二义性
auto int a = 1;//error:不能和任何类型说明符连用
int a[10] = {0};
auto b[10] = a;//error:不能定义数组
vector<int> a;
vector<auto> b = a;//error:无法推导模板参数

3.decltype用法
通过上面的介绍,我们知道auto只能是对变量进行类型推导,为此,decltype关键字的出现就是为了弥补auto这个缺陷而诞生。decltype就是在编译期间对表达式的类型进行推导,decltype语法如下:

decltype(exp)

如上,decltype(exp)的类型和exp相同。如果exp是一个函数,那么decltype(exp)的类型和函数的返回值相同。上栗子:

//例1
int a = 10;
decltype(a) b;// 先推导a的类型为int,所以b也是int。

//例2
int a = 10;
int b = 20;
decltype(a + b) c;

值得注意的是,在表达式类型推导时,并不会实际计算表达式的值;再者如果表达式返回的是一个左值,那么推导出的类型将会是左值的引用:

int a = 10;
int b = 20;
decltype(a += b) c = b; //则c的类型是 int&

3.std::function&std::bind&std::lambda

C++11引入了std::functon,std::bind,lambda表达式来使函数的调用更加丰富,lambda式在此文就不作过多赘述,感兴趣的可以移步我另一篇博客(lambda表达式)。
1.std::function
类模板 std::function 是通用多态函数封装器。 std::function 的实例能存储、复制及调用任何可调用 (Callable) 目标——函数、 lambda 表达式、 bind 表达式或其他函数对象,还有指向成员函数指针和指向数据成员指针。

存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。用法示例如下:

std::function(void <int>) fun;

此时,fun就是一个无返回值,单形参int的函数的封装器,将该类函数实例存储到fun中就可以直接采用fun来调用,如下:

void Printf(int i)
{
	cout << i << endl;
}
std::function(void(int)) fun = Printf;//将函数实例复制给fun
fun(5);//实现调用

上面的栗子只是一个简单的封装示例,std::function()还可以存储lambda表达式,但是当结果类型为引用的 std::function 从无尾随返回类型的 lambda 表达式初始化时需要留心。由于 auto 推导的起效方式,这种 lambda 表达式将返回纯右值。故而结果引用将始终绑定到生命期在std::function::operator() 返回时结束的临时量。
例如:

std::function<const int&()> F([]{ return 42; });
int x = F(); // 未定义行为: F() 的结果是悬垂引用

除了存储普通函数以外,其也可以存储类的成员函数,如下:

class A
{
public:	
	int fun(int i, int j)
	{
		return i + j;
	}
}; 
std::function <int(const A&, int, int)> foo = &A::fun;	

2.std::bind
std::bind的作用是用来绑定调用函数的参数,并且可以使用std::function进行封装,在我们想用的时候再调用,这实际上一种延迟调用的设计思想。bind不仅可以对普通函数进行绑定,也可以对成员函数,函数对象进行绑定,同时还支持占位符,即std::placeholders。使用占位符可以实现绑定部分参数,另外一些参数则在调用时传入。示例:

#include <functional>
#include <iostream>
#include <memory>
using namespace std;
int add(int n1, int n2, int n3)
{
	cout << n1 << "," << n2 << "," << n3 << endl;
	cout << n1 + n2 + n3 << endl;
	return 0;
}

struct fun {
   void Print(int n1, int n2) { std::cout << n1 + n2 << std::endl; }
};

int main() {

   int a = 10;
   auto f1 = std::bind(add, std::placeholders::_2, a,std::placeholders::_1); // n1 绑定占位符_2,n3绑定_1 
   f1(20,40); // 20传给_1,40传给_2 
   
   fun Fun;
   auto f2 = std::bind(&fun::Print, &Fun,std::placeholders::_1,std::placeholders::_2);//绑定成员函数 
   f2(10,20); 
   return 0; 
   
  
}

运行结果如下:
在这里插入图片描述

4.列表初始化

C++11中引入的列表初始化的概念,允许在变量后直接使用初始化列表来初始化变量。如下:

#include <iostream>
using namespace std; 
class A {
public:
   A(int) {}

};
int main() {

   A a{123}; //c++11 采用列表初始化a 
   
   return 0;
} 

除了上述这样在定义的时候采用列表初始化,C++11也允许列表初始化被应用在函数参数中——initializer_list,下面看一下简单的用法:

#include <iostream>
#include <initializer_list> 
using namespace std;
int sum(initializer_list<int> l)
{
	int sum = 0;
	for(auto it = l.begin(); it < l.end(); it++)
	{
		sum += *it;
	}
	return sum;
}
int main() {

  int s = 0;
  s = sum({1,2,3,4,5});
  cout << s << endl;
  return 0;
} 

列表初始化好虽好,但是在某些情况下一些类也无法使用列表初始化,哪些类不能采用列表初始化呢?这里就涉及到了新的名词,聚合类型。只有聚合类型的类才可以使用列表初始化。那么什么是聚合类型呢?聚合类型的类需要满足以下条件:
1.无用户自定义构造函数。
2.无基类
3.无私有或者受保护的非静态数据成员
4.无虚函数
5.没有{}和=直接初始化的非静态数据成员
所以,只有满足以上条件的类才可以使用列表初始化。

5.智能指针

内存泄漏往往是C/C++程序员最为头疼的问题,而该类问题的起因往往是一些指针没有被正确释放导致的。基于C++中类构造和析构的特性,C++11中提供了几个指针的类模板,分别是unique_ptr,shared_ptr和weak_ptr。在C++11之前还有auto_ptr指针,但是现在已经被unique_ptr指针给代替了。shared_ptr和unique_ptr之间最大的区别就是share_ptr引入了计数的方法,而weak_ptr的诞生则是解决shared_ptr相互引用问题的。关于智能指针,我在之前写过两篇相关的博客,介绍了智能指针基本的用法以及一些方法,感兴趣的读者可以去看看。(auto_ptrunique_ptr)。

6.多线程编程

对于多线程编程大伙应该都挺熟悉的,不管是c程序员还是java程序员,特别是从事高并发相关业务的,更是逃不了多线程。在C++11之前,很多linux的程序员在编写多线程的程序时往往采用POSIX接口来创建多线程,但是总感觉太繁琐了,各种接口名称都比较难记。但是在C++11中就推出了thread类模板来简化了多线程编程。同时也提供了其他的一些好东西来支持多线程编程,例如std::mutex、
std::lock、std::atomic、std::call_once、std::condition_variable和std::future等等。一时半会要把C++11多线程的所有知识点都讲完是不可能的,后续我会专门写一篇关于c++11中的多线程。下面我们就先来看一下Thread的简单用法。

#include <iostream>
#include <thread> 
using namespace std;
void Printf()
{
	cout <<"hello world" << endl;
}

void Print1()
{
	cout << "hello C++" << endl;
}
int main() {
	cout << "main" << endl;
    thread t1(Printf);//创建t1对象,并且使用Printf来初始化
    thread t2(Print1);//创建t2对象,并且使用Print1来初始化
    
    t1.detach();//让t1线程和主线程分离
	
	if(t2.joinable())
	{
		t2.join();
	}
	cout << "end" << endl;
	return 0;
} 

在上述代码中创建了两个线程,分别用了两个函数来初始化。当线程对象调用了detach函数后,会使该线程与主线程分离,可以理解为非阻塞,即主线程的结束不用等待线程t1的结束;当你希望线程是阻塞执行的,那么你可以调用join函数,这样主线程会等待t2线程结束后才结束。但是需要注意的是在调用join之前,需要先调用joinable函数判断该线程是否可以join。
随着多线程而来的还有线程安全和资源竞争的问题。c++11提供了一些锁来解决这类问题。

#include <iostream>
#include <thread> 
#include <mutex>

using namespace std;

mutex m;
int i = 10;
int add()
{
	m.lock();
	i = i * 3;
	m.unlock();
	return 0;
}

int jian()
{
	m.lock();
	i = i + 3;
	m.unlock();
	return 0;
}
int main() {
	cout << "main" << endl;
    thread t1(jian);
    thread t2(add);
    if(t1.joinable())
    {
    	t1.join();
	}
	if(t2.joinable())
	{
		t2.join();
	}
	cout << i << endl;
	return 0;
} 

如上,在线程对共享的变量进行操作时,需要加锁,操作完毕后释放锁,这样就能保证一个时间只能有一个线程对共享变量进行修改。

上述关于c++11的新特性如有任何错误,请各位读者指出。谢谢!
下篇文章再和大家一起看一下C++11的其他特性。

参考资料:

  1. https://zh.cppreference.com/w/%E9%A6%96%E9%A1%B5
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值