欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++11的新特性

程序员文章站 2022-04-05 08:50:53
...

本文全部参考自:http://www.cnblogs.com/wangqiguo/p/5635441.html,特此感谢。
开发环境:
1) C++shell 这个在线编译系统可以选择C++98、C++11、C++14特性。
2) Ubuntu gcc5.4版本以上,可以支持最新C++17特性,当然需要添加选项,比如:
g++ test.cpp -o test -std=c++17

其他文章:
https://en.cppreference.com/w/
http://www.gnu.org/prep/ftp
https://blog.csdn.net/FX677588/article/details/70157088
https://www.cnblogs.com/feng-sc/p/5710724.html
https://blog.csdn.net/wangshubo1989/article/details/49823083
https://msdn.microsoft.com/zh-cn/library/hh567368.aspx#C++11 功能列表

一、列表初始化

C++11中全面加入了列表初始化的功能,包括对vector,map,值类型,struct等等都可以使用列表初始化,还可以在函数中返回一个花括号括起来的列表,而在这之前我们只能对数组进行列表初始化:

//数组列表初始化
int xx[5]={1,2,3,4,5};
int yy[]={6,7,8,9,0};
 
//值类型进行初始化
int a{10};
int b={10};
int c={10.123}; // 编译器报错,g++ 5.3.1当列表初始化用于值类型的时候,如果有精度损失,编译器会报错。
 
//列表初始化还可以用结构体
typedef struct Str{
   int x;
   int y;
}Str;
Str s = {10,20};
 
//列表初始化类,必须是public成员,如果含有私有成员会失败
class Cls{
public:
   int x;
   int y;
};
Cls c  = {10,20};
 
//vector不仅可以使用列表初始化,还可以使用列表进行赋值,数组不能用列表赋值
vector<int>v1={1,2,3,4,5,6,7,8,9}; // 初始化
vector<int>v2;
v2={3,4,5,6,7}; //赋值
 
//map列表初始化
map<string ,int> m = {
      {"x",1},
      {"y",2},
      {"z",3}
};
 
//用函数返回初始化列表只展示关键代码,相关头文件自行添加
//同理结构体,类,map的返回也可以使用初始化列表返回
vector<int> getVector()
{
  return {1,2,3,4,5};
}
 
int main()
{
  vector<int> v = getVector();
  cout<<v[0]<<v[1]<<v.size()<<endl;
  return 0 ;
}

二、 nullptr 空指针

C++11中新加入的字面值表示不指向任何对象的空指针,以前我们常常用一个预定义的宏NULL来表示空指针,实际上NULL的值是0,新标准推荐使用nullptr而不是NULL

三、constexpr

constexpr变量

我们在定义常量的时候一般使用const来定义,一个常量必须在定义的时候进行初始化,并且之后不可更改。一个常量必须使用一个常量表达式进行初始化,并且在编译期间就可以得到常量的值,但是如何确定一个表达式就是常量表达式呢,这个通常是由程序员自己确定的,例如:

const int a =20;
//20是一个字面值,当然也是一个常量表达式,所以用20来为a赋值是没有问题的
//然而下面的代码也可以通过编译,g++ 5.3.1
int a = 20 ;
const int x =  a;
int b[x]={0};

为常量x赋值的是一个变量a,这样做应该是不合理的,但是编译器没有报告任何错误,当然这种错误是显而易见的,但是在复杂的系统中如何判断一个表达式是否是常量表达式是很困难的,例如这里的a我们一眼就可以判断其并不是一个常量表达式。为此C++11提供了一个新的关键字constexpr,使用该关键字定义的常量,由编译器检查为其赋值的表达式是否是常量表达式,例如上面的代码改成:

int a = 20 ;
constexpr int x =  a;

编译器编译的时候就会报错说a并不是常量。显然constexpr关键字将常量表达式的检查转交给编译器处理,而不是程序员自己,所以使用constexpr定义常量要比const安全。

constexpr函数

普通的函数一般是不能用来为constexpr常量赋值的,但是C++11允许定义一种constexpr的函数,这种函数在编译期间就可以计算出结果,这样的函数是可以用来为constexpr赋值的。定义constexpr函数需要遵守一些约定,函数的返回类型以及所有形参的类型都应该是字面值,一般情况下函数体中必须有且只有一条return语句。

constexpr int size()
{
    return 42;
}
constexpr int si = size();

执行初始化的时候编译器将函数的调用替换成结果值,constexpr函数体中也可以出现除了return之外的其他语句,但是这些语句在运行时不应该执行任何操作,例如空语句,using声明等。constexpr函数允许其返回值并非是一个字面值,例如:

constexpr int size(int s)
{
    return s*4;
}
 
int a = 20;
const int b = 30;
constexpr int c = 40;
constexpr int si = size(a);  //error a是一个变量所以函数返回的是一个可变的值
constexpr int si1 = size(20); //ok 函数返回的实际上是一个常量
constexpr int si2 = size(b);  //ok
constexpr int si3 = size(c);  //ok

由上可知constexpr函数并不一定返回常量,如果应用于函数的参数是一个常量表达式则返回常量,否则返回变量,而该函数调用到底是一个常量表达式还是非常量表达式则由编译器来判断。这就是constexpr的好处。

四、auto类型指示符

我们定义一个变量的时候首先必须确定该变量的类型,而很多时候并不是我们先需要一个变量然后为该变量赋值合适的数据,而是我们有一个值但是我们却不知道该用什么类型的变量存储它,特别是C++的模版使用的非常广泛,有时候要定义一个变量,其类型是很复杂的会带有模版的类型参数,例如一个最常见的例子:

map<string ,int> m ;
map<string,int>::iterator it = m.begin();

C++11为我们定义了一个新的关键字 auto 用来定义变量,而变量的类型由编译器自动根据赋值的表达式推导出来,不需要我们显示定义了。因为auto定义的变量的类型由编译器根据赋值的表达式推导,所以auto定义的变量必须有初始值,否则编译器没法确定该变量的类型。

auto x = 20; // x 是int
auto y = 3.14; // y 是double
map<string ,int> m ;
auto it = m.begin(); //it 是map<string,int>::iterator

这样是不是方便了不少,而且程序看起来更加简洁了
auto可以在一条语句中声明多个变量,但是要保证语句中的基础数据类型只有一个,例如:

auto i=10,*p=&i; // OK i是int,p是int*
auto a=10,b=3.14; // Error 类型是int还是double ?

五、decltype类型指示符

有时候会有这样的需求,我们需要知道一个表达式的类型,并使用该类型去定义一个变量,例如:

int a = 10;
int b = 20;
auto c = a + b; // OK a+b的类型是int,此时c的类型是int,并且c的值是 a+b

auto可以解决部分问题,例如我们定义的变量的类型就是表达式 a+b 的类型,但是如果我们仅仅需要定义一个与表达式 a+b 的类型相同的变量,但是我们又不希望将表达式a+b的值赋值给刚刚定义的变量,我们希望赋另外一个值或者是仅仅定义变量而不赋值呢。 这就需要用到C++11 提供的另一个类型说明符 decltype了。decltype作用于一个表达式,并且返回该表达式的类型,在此过程中编译器分析表达式的类型,并不会计算表达式的值。例如

int a = 10;
int b = 20;
decltype(a+b) c = 50; // OK c的类型就是 a+b 的类型int

对于引用类型decltype有一些特别的地方:

int a = 20 ;
int &b = a;
decltype(b) c ;  // Error c是引用类型必须赋值
decltype(b) d = a; // OK  d是引用类型,指向a

可以看到decltype如果作用于一个引用类型,其得到的还是一个引用类型。我们知道一个引用类型在使用的时候一般会当作其关联的那个变量的同义词处理,例如如果使用 cout<<b<<endl; 其中b实际上相当于a,但是decltype作用于引用类型的时候会保留引用性质。

如果一个表达式是一个解指针引用的操作,decltype得到的也是一个引用类型:

int a = 20 ;
int *p = &a;
decltype(*p) c = a;  // c的类型是int&
c = 50;
cout<<a<<endl;  // 输出50

当decltype作用于一个变量的时候,变量加不加括号是有区别的,例如:

int a = 20;
decltype(a) b = 30; //ok b的类型是 int
decltype((a)) c = a ; // ok c的类型是int& 其关联变量 a

加上括号之后编译器会把(a)当作是一个表达式处理,而变量是一种可以作为赋值语句左值的表达式,所以会解释成引用类型。

六、范围for语句(range-based for statement)

C++11 引入了一种更为简单的for语句,这种for语句可以很方便的遍历容器或其他序列的所有元素

vector<int> vec = {1,2,3,4,5,6};
for(int elem: vec)
{
    cout<<elem<<endl;
}

七、lambda表达式与bind函数

lambda表达式是一个可以被调用的代码单元,相当于一个内联函数,有参数和返回值以及函数体。但是跟函数不同的是,lambda表达式可以定义在函数的内部,一个完整的lambda表达式具有如下形式:

[捕获列表](参数列表) mutable -> 返回类型 {函数体}

附:
下图为侯捷老师视频中的讲解,当三个opt选项任何一个存在时,必须要有()。
Lambdas主要有三个作用:声明返回类型;metaprograming;用在仿函数中。
C++11的新特性

int x = 10;
int y = 20;
auto f = [x,&y](int a ,int b){++y;return a+b+x+y;};
cout<<f(1,2)<<endl; //34
cout<<y<<endl;      //21

lambda可以省略参数列表(如果没有参数的话),可以省略返回类型,但是不能省略捕获部分与函数体部分,即使捕获列表为空,也要有一个空的[],lambda有两种捕获,一种是值捕获,一种是引用捕获。如果是值捕获那么lambda中获得的是捕获的变量的副本,如果是引用捕获则获得的是引用,可以在lambda内部修改引用的变量的值,如上x是值捕获,y是引用捕获,lambda中默认是值捕获,如果变量前面添加&则是引用捕获,另外lambda中还有两种形式的引用捕获,例如[=]表示值捕获所有可见的变量,而[&]则表示引用捕获所有可见变量。如果希望值捕获所有可见变量,但是又有个别变量采用引用捕获呢,[=,&x]表示值捕获所有可见变量,同时引用捕获x。而[&,x]则表示引用捕获所有可见变量,x采用值捕获的方式。

有关bind函数,在很多地方我们可以使用函数替换lambda表达式,毕竟如果很多地方需要用到同一个lambda表达式,而且这个lambda表达式比较长的话,将其定义成函数应该是最好的。对于没有捕获列表的lambda表达式我们可以直接使用函数替代,例如:

int f(int x,int y)
{
   return x+y;
}
 
void main()
{
    f();
}

我们可以用下面的方式替代:

void main()
{
    auto f=[](int x,int y){return x+y};
    f();
}

与上面的lambda是等价的,但是对于有捕获列表的lambda表达式应该怎么处理呢,例如:

void main()
{
    int x = 10;
    int y = 20;
    auto f = [x,&y](int a ,int b){return a+b+x+y;}; //一个值捕获,一个引用捕获
    f(33,44);
}

如果转换成函数的形式:

int x = 10;
int y = 20;
int f(int a,int b)
{
  return a+b+x+y;
}
 
void main()
{
    f(33,44);
}

这是一种可行的方法,但是总不能把所有的捕获变量定义成全局变量吧。现在的关键问题是lambda的捕获表达式中的内容转换成函数不可行,C++11提供了bind函数来完成这样的操作。

#include <functional> //bind()
#include <iostream>
using namespace std;
using namespace std::placeholders; // _1,_2所在的命名空间
int f(int x,int y,int a,int b)
{
  return a+b+x+y;
}
 
void main()
{
  int x = 10;
  int y = 20;
 
  auto f_wrap = bind(f,x,y,_1,_2);
  cout<<f_wrap(33,44)<<endl; // _1,_2是占位符,表示调用f_wrap的时候_1是第一个参数,_2是第二个参数。最终会被替换成调用  f(10,20,33,44)
}

如果引用类型的捕获怎么做呢,看下面的例子,用lambda是这样的:

#include <iostream>
#include <functional>
using namespace std;
using namespace std::placeholders;
void main()
{
  int x = 10;
  ostream &o = cout;
  auto f =[&o](int a){o<<a<<endl;}; // 注意这里的输出对象是用的引用捕获
  f(x);
}

使用bind是这样的:

#include <iostream>
#include <functional>
using namespace std;
using namespace std::placeholders;
void f(ostream &o,int x)
{
  o<<x<<endl;
}
int main()
{
  int x = 10;
  auto f_wrap = bind(f,ref(cout),_1); //将变量的引用传递到bind中是个问题,为此C++11提供了ref()函数用于获得引用
  f_wrap(x);
  return 0 ;
}

这里一个关于lambda的小栗子。

//功能测试1:使用lambda在main函数之前运行
#include<iostream>
using namespace std;

auto lamadatest = []()
{
	cout << "before main" << endl;
	return 0;
}();
int main()
{
	cout << "main" << endl;
	return 0;
}
输出:
before main
main
//功能测试2:捕获外部变量时采用值传递
 		int id = 0;
        auto f=[id]() mutable{
			cout<<"id:"<<id<<endl;
			++id;
		};
		
		id = 42;
		f();
		f();
		f();
		cout<<id<<endl; 
输出:
id:0
id:1
id:2
42

//功能测试3:捕获外部变量时采用引用传递
 		int id = 0;
        auto f=[&id]() mutable{
			cout<<"id:"<<id<<endl;
			++id;
		};
		
		id = 42;
		f();
		f();
		f();
		cout<<id<<endl; 
输出:
id:42
id:43
id:44
45
当采用引用传递时,在函数调用时id的值被修改为42,故输出如上。

//功能测试4:在仿函数中使用
	vector<int> vi {5,28,50,83,70,590,245,59,24};
		int x = 30;
		int y = 100;
		//去除vi中x和y之间的元素 
		vi.erase(remove_if(vi.begin(),
						   vi.end(),
							[x,y](int n){ return x<n && n<y; }
						  ),
				vi.end()
				);
		for(auto i :vi)
			cout<<i<<" ";
		cout<<endl;
输出:
5 28 590 245 24

八、智能指针share_ptr,unique_ptr

C++11中引入了几种智能指针,智能指针能够自动释放所指向的对象,其中shared_ptr允许多个指针指向同一个对象,unique_ptr则独占所指向的对象,我们主要说明shared_ptr的使用。通过使用make_shared()函数产生智能指针对象。

shared_ptr<int> p = make_shared<int>(40); // p指向一个值为40的int对象
shared_ptr<string> p2 = make_shared<string>(10,'c'); //指向值为'cccccccccc'的string对象

make_shared()函数中传递的值要与对应的type的构造函数相匹配,实际上应该就是直接调用的对应type的构造函数。

我们可以使用new初始化的指针来初始化智能指针:

share_ptr<int> p (new int(40));
p.get(); // 使用share_ptr<type>的get()函数来获得其关联的原始指针。

shared_ptr对象在离开其作用域(例如一个函数体),会自动释放其关联的指针指向的动态内存,就像局部变量那样。另外多个shared_ptr可以指向一个对象,当最后一个shared_ptr对象销毁的时候才会销毁其关联的那个对象的动态内存。这里使用了引用记数。

相关标签: C++11