本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本文是我在掘金平台发表于2022年4月份的,从发表最初累计第8篇博客,希望大家关注我,我将会持续在后端和大数据等领域进行书写更多的文章。
摘要
C++11是一个使用非常广泛的C++版本,目前很多公司依然在内部使用C++11版本。C++11相比于C++99,首次出现了很多新的特性,本文就一些比较常用的特性进行快速的介绍,使用这些特性可以很明显增强编程的效率,提高编程乐趣,然而这些特性有的时候也非常的费解和容易犯错误,本文就这些特性进行清晰而简单的讲解,并且通过一些用例来展示这些特性的用法。
引言
C++在不断的迭代着,特性也在不断的增加。然而哪些特性是常用的呢?这个没有统一的标准,但是根据实际编程,我们可以发现有的特性是非常实用的,而有的特性则不然,基本上无人问津,并且因为少数人的使用降低了其代码可阅读性。因此有的特性建议使用,而有的特性则不建议使用。根据网上的文章,本文挑选了一些经典的特性进行逐一介绍。
C++11是一个使用非常广泛的C++版本,目前很多公司依然在内部使用C++11版本。而C++14,C++17,C++20等都远不如C++11使用的广泛。
本文工作有:
(1)在参考[2]的基础上,进行了相当工作量的扩充和矫正,力图说明新特性的本质,并且提高了阅读的可理解性。
(2)对C++11的特性进行了辛辣的点评和批评,诸如垃圾语言,毁我青春之类的话!
本文的结构比较简单,首先列举目录,然后对一些实用的特性进行了逐一的列举,并且针对有的进行了举例,列举过程并没有和目录顺序完全一致,希望可以提高阅读的乐趣和惊喜度。
文章目录
- 1、long long 类型
- 2、列表初始化
- 3、nullptr 空指针
- 4、constexpr变量
- 5、constexpr函数
- 6、using类型别名
- 7、auto类型指示符
- 8、decltype类型指示符
- 9、范围for语句
- 10、尾置返回类型
- 11、强枚举类型
- 12、=default 生成默认构造函数
- 13、类对象成员的类内初始化
- 14、lambda表达式与bind函数
- 15、可调用对象与function
- 16、move语义与右值引用
- 17、智能指针share_ptr,unique_ptr
- 18、STL容器
- 19、其他内容
C++11包括大量的新特性:主要特征像lambda表达式和移动语义,实用的类型推导关键字auto,更简单的容器遍历方法,和大量使模板更容易使用的改进。这一系列教程将包含所以以上特性。
很明显,C++11为C++带来了大量的新特性。C++11将修复大量缺陷和降低代码拖沓,比如lambda表达式的支持将使代码更简洁。像移动语义这种特性会提高语言内核的基础效率,使你可以写出更快的代码。对模板系统的优化可以使你更容易写出泛型的代码。
C++11中的新特性列举
3、nullptr 空指针
C++11中新加入的字面值表示不指向任何对象的空指针,以前我们常常用一个预定义的宏NULL来表示空指针,实际上NULL的值是0,新标准推荐使用nullptr而不是NULL。
6、using类型别名
类型别名其实早在C语言中就有了,一般情况下我们使用关键字typedef来声明一个类型的别名,在C++11中增加了另一种声明类型别名的方法就是使用using关键字,using关键字在C++11以前一般用来引用命名空间。
typedef int INT; // 右侧的符号代表左侧,翻译过来就是给int类型起个别名叫做INT
using INT2 = int; // 左侧符号代表右侧
INT a = 20;
INT2 b = 30;
7、auto类型指示符
如果编译器在定义一个变量的时候可以推断出变量的类型,不用写变量的类型,你只需写auto即可:
auto str = "sissie";
assert(typeid(str) == typeid(const char *));
在迭代器中使用auto,简化代码:
std::vector<std::string> vs{"sissie", "robin", "playjokes", "sky", "hatlonely"};
for (auto it = vs.begin(); it != vs.end(); ++it) {
std::cout << *it << ", ";
}
在模板中使用auto:
template <typename BuiltType, typename Builder>
void makeAndProcessObject(const Builder& builder)
{
BuiltType val = builder.makeObject();
// do stuff with val
}
MyObjBuilder builder;
makeAndProcessObject<MyObj>(builder);//第一个参数类型指定,第二个让自动推倒,可以看到一个语法,使用模板类型的函数,FuncName<TypeA, TypeB, TypeC, ...>(Param...),如果<>内不指定完整,后面的不指定,就会引起自动推倒
// 使用auto只需要一个模板参数,让编译器自动推导
template <typename Builder>
void makeAndProcessObject(const Builder& builder)
{
auto val = builder.makeObject();
// do stuff with val
}
MyObjBuilder builder;
makeAndProcessObject(builder);//自动推倒
1、long long 类型
long long 类型实际上没有在C++ 98中存在,而之后被C99标准收录,其实现在市面上大多数编译器是支持 long long 的,但是这个类型正式成为C++的标准类型是在C++11中。标准要求long long至少是64位也就是8个字节。一个字面常量使用LL后缀表示long 。而无符号long类型,使用ULL后缀表示unsigned long long 类型。
**为什么是两个long?那么一个long呢?**long int 即long,用法是 long a = 1;, 给人的感觉好像是长整型,但实际上,它和int一样,只有32位。
既然long int与int相同,那么为什么还有long int这种尴尬的类型呢?
原因是早期的C编译器定义了long int占用4个字节,int占用2个字节,long int是名副其实的长整型。在ANSI C的标准中,对长整型的定义也是long int应该至少和int一样长,而不是long int 一定要比int占用存储字节长。所以,正确的关系应该是 long >= int >= short,新版的C/C++标准兼容了早期的这一设定。
long long则不同,long long是C++的64位整型的基本类型,“现任”长整型,从C99开始引入这个概念,在后续的标准中完善概念和定义。long long占用8个字节,数据表示范围也从int的[ − 2^31 , 2^31 − 1 ]升级到[ − 2^63 , 2^63 −1] 。详情见参考[1]。
2、列表初始化
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 v1={1,2,3,4,5,6,7,8,9}; // 初始化
vector 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 ;
}
9、范围for语句(for each)
std::vector<std::string> vs{"sissie", "robin", "playjokes", "sky", "hatlonely"};
for (const auto& name: vs) { // 注意,这里使用了 &, 也就是引用类型的遍历,否则会进行复制的
std::cout << name << ", ";
}
举一个例子, 如果使用for each语句,不使用引用的话,会逐个拷贝,因此这里要注意一下!
#include <iostream>
#include <fstream>
using namespace std;
class X{
public: int a;
X():a(0){
cout << "new X" << endl;
}
X(int a_):a(a_){
cout << "new X with param" << endl;
}
X(const X &xp){
a = xp.a;
cout << "copy constructor X" << endl;
}
};
int main(){
X x1(2), x2;
cout << "##############" << endl;
vector<X> xarr{x1,x2};
cout << "##############" << endl;
for(auto x: xarr){
cout << x.a << endl;
}
}
输出的结果为
new X with param
new X
##############
copy constructor X
copy constructor X
copy constructor X
copy constructor X
// 注:为什么调用了4次构造函数还需要调查,这里我暂时没有搞清楚!
##############
copy constructor X
2
copy constructor X
0
Process finished with exit code 0
此外,还可以使用 for each 遍历 map,我们依然使用上面的类X。
int main(){
X x1(2), x2;
cout << "##############" << endl;
map<int,X> mm{{1,x1},{2, x2}}; // 调用
cout << "##############" << endl;
for(auto x: mm){ //标记【1】
cout << x.first << ", " << x.second.a << endl;
}
}
输出结果为
new X with param
new X
##############
copy constructor X
copy constructor X
copy constructor X
copy constructor X
##############
copy constructor X
1, 2
copy constructor X
2, 0
因此上,如果把标记【1】处改为 for(auto& x: mm){那么就会避免拷贝行为。本质上,map也是一个数组,只不过下标更加自由而已。
12、=default 生成默认构造函数
在C++的类中,如果我们没有定义构造函数,编译器会为我们合成默认的无参构造函数,如果我们定义了构造函数,则编译器就不生成默认构造函数了,但是如果我们定义构造函数同时也希望编译器生成默认构造函数呢?
C++11中可以通过在构造函数的声明中直接 =default的方式要求编译器生成构造函数。
class ClassName{
public:
ClassName(int x);
ClassName()=default; // 显示要求编译器生成构造函数
};
4、constexpr变量
我们在定义常量的时候一般使用const来定义,一个常量必须在定义的时候进行初始化,并且之后不可更改。一个常量必须使用一个常量表达式进行初始化,并且在编译期间就可以得到常量的值,但是如何确定一个表达式就是常量表达式呢,C++11提供了一个新的关键字constexpr,使用该关键字定义的常量,由编译器检查为其赋值的表达式是否是常量表达式,例如:
int a = 20 ;
constexpr int x = a;
编译器编译的时候就会报错说a并不是常量。显然constexpr关键字将常量表达式的检查转交给编译器处理,而不是程序员自己,所以使用constexpr定义常量要比const安全。
常量表达式要求表达式中的符号都是常量的。
张三思评:
这里的意思是 x 被顺便声明为常量吗? constexpr 是更强大的 const? 其目的是将运算尽量放在编译阶段,而不是运行阶段。这个从字面上也好理解,const是常量的意思,也就是后面不会发生改变,因此当然可以将计算的过程放在编译过程。
所以,正确的使用 constexpr 的方法如下:
#define b 10
const int a = 20 ;
constexpr int x = a+1;
constexpr int x2 = b+1;
5、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的好处。
所以 constexpr的参数也应该是常量表达式,如果是变量就不行,因为要在编译器执行。
所以,这种东西在什么场景使用呢?由此可见C++奇怪臃肿的特点,增加一些很奇怪的特性没人用,增加学习成本。
13、类对象成员的类内初始化
class ClassName
{
public:
int x = 10; //C++11 之前是不允许的
};
张三思评:
这个比列表初始化好用!!优先使用这个吧!!
最起码,它能节省好几个字母呢.
14、lambda表达式与bind函数
C++11中得lambda表达式用来定义和创建匿名函数,lambda表达式语法形式如下:
[ capture ] ( params ) mutable exception attribute -> ret { body } // 完整的 lambda 表达式形式
[ capture ] ( params ) -> ret { body } // const 类型的 lambda 表达式,该类型的表达式不能改捕获("capture")列表中的值
[ capture ] ( params ) { body } // 省略了返回值类型的 lambda 表达式
[ capture ] { body } // 省略了参数列表,类似于无参函数 f()
lambda表达式是一个可以被调用的代码单元,相当于一个内联函数,有参数和返回值以及函数体。但是跟函数不同的是,lambda表达式可以定义在函数的内部,一个完整的lambda表达式具有如下形式:
mutable 意思是可变的,也就是说这个函数名称是一个变量,可以赋给其他的函数。
[捕获列表](参数列表) mutable -> 返回类型 {函数体}
int main()
{
auto add= [](int a, int b)->int{
return a + b;
};
int ret = add(1,2);
std::cout << "ret:" << ret << std::endl;
return 0;
}
lambda可以省略参数列表(如果没有参数的话),可以省略返回类型,但是不能省略捕获部分与函数体部分,即使捕获列表为空,也要有一个空的[],lambda有两种捕获,一种是值捕获,一种是引用捕获。如果是值捕获那么lambda中获得的是捕获的变量的副本,如果是引用捕获则获得的是引用,可以在lambda内部修改引用的变量的值,如上x是值捕获,y是引用捕获,lambda中默认是值捕获,如果变量前面添加&则是引用捕获,另外lambda中还有两种形式的引用捕获,例如[=]表示值捕获所有可见的变量,而[&]则表示引用捕获所有可见变量。如果希望值捕获所有可见变量,但是又有个别变量采用引用捕获呢,[=,&x]表示值捕获所有可见变量,同时引用捕获x。而[&,x]则表示引用捕获所有可见变量,x采用值捕获的方式。
在成员函数中的 Lambda 表达式可以捕获当前对象的 this 指针,让 Lambda 表达式拥有和当前类成员同样的访问权限,可以修改类的成员变量,使用类的成员函数。 最后,this 指针只能按值捕获 [this] ,不能按引用捕获 [&this] 。
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;
}
int 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)
}
8、decltype类型指示符
decltype 返回操作数的类型,可以对基本上任何类型使用decltype,包括函数的返回值:
int ia[10];
decltype(ia) ib; // int ib[10];
新的函数返回值声明语法,把返回类型放在函数声明的后面,用auto代替前面的返回类型:
// 这两种函数声明方式等效
int multiply(int x, int y);
auto multiply(int x, int y) -> int;
// 返回auto
template <typename Builder>
auto makeAndProcessObject(const Builder& builder) -> decltype(builder.makeObject())
{
auto val = builder.makeObject();
// do stuff with val
return val;
}
张三思评:
为什么要有这种花里胡哨的东西?
对于引用类型decltype有一些特别的地方:
(1)decltype如果作用于一个引用类型,其得到的还是一个引用类型:
int a = 20 ;
int &b = a;
decltype(b) c ; // Error c是引用类型必须赋值
decltype(b) d = a; // OK d是引用类型,指向a
(2)如果一个表达式是一个解指针引用的操作,decltype得到的也是一个引用类型:
int a = 20 ;
int *p = &a;// p是一个指针,指向a
decltype(*p) c = a; //decltype(*p)表示获取*p的值的类型,c的类型是int&
c = 50;
cout<<a<<endl; // 输出50
(3)当decltype作用于一个变量的时候,变量加不加括号是有区别的,例如:
int a = 20;
decltype(a) b = 30; //ok b的类型是 int
decltype((a)) c = a ; // ok c的类型是int& 其关联变量 a
张三思评:
这么多复杂的东西引入进来, 难怪C++越来越臃肿了…请问这些东西的存在意义是什么, 可以用简答的其他语法形式来实现, 为什么要引入无关的东西? 真他妈垃圾语言.
可以认为, decltype 的全程是 declare type of a variable, 拷贝一个已经生命的变量的类型,作用类似于 auto 。
15、可调用对象与function
C++语言中有几种可调用对象:函数、函数指针、lambda表达式、bind创建的对象以及重载子函数调用运算符的类。
不同类型可能具有相同的调用形式,所以C++11中提供了名为function的标准库类型,定义在头文件中,该类型用来存储一个可调用的对象,统一了所有可以像函数一样调用的调用形式,例如:
#include <functional>
#include <iostream>
using namespace std;
int add(int x,int y)
{
return x+y;
}
class Add
{
public:
int operator()(int x,int y)
{
return x+y;
}
};
void main()
{
//function模版的参数是函数调用形式,包括返回类型,以及参数列表个数和类型
function<int(int,int)> f1 = add; //函数指针
function<int(int,int)> f2 = Add(); // 可调用类对象
function<int(int,int)> f3 = [](int x,int y){return x+y;}; //lambda表达式
cout<<f1(10,20)<<" "<<f2(30,40)<<" "<<f3(50,60)<<endl;
}
16、move语义与右值引用
左值和右值是针对表达式而言,表达式之后依然存在的对象是左值,表达式之后不再存在的临时对象为右值,左值可以对其取地址,右值不能。
int i = 0;
std::string hello = "hello";
std::string world = "world";
const int& ri = 1;
// 左值:i, ++i, hello, world
// 右值:i++, hello + world, ri
为了支持移动操作,C++11中使用了一种称为右值引用的类型。移动操作是什么呢,一般情况下我们将一个对象赋值给另一个对象的时候会调用对象到拷贝构造函数或者拷贝赋值运算符。而移动构造函数和移动赋值运算符则用来将数据从一个对象移动到另一个对象。在很多情况下对象拷贝之后需要被销毁,此时使用移动操作会大幅提高性能。右值引用被使用之后,其关联的对象会被销毁。右值引用使用两个&&表示,例如
int && 表示右值引用,而int &则是左值。通过C++11标准库提供的函数 std::move()可以得到某一对象的右值引用。
TestClass::TestClass(TestClass &&t) noexcept //移动构造函数不应该抛出任何异常
:x(t.x),y(t.y),z(t.z),p(t.p)
{
t.p=nullptr; // 实现移动操作之后需要保证被移动的对象的析构是安全的,所以源对象的指针成员置为空,随后t的析构函数将会被自动调用,t被销毁。
}
std::vector<TestClass> v;
TestClass tc;
v.push_back(std::move(tc));
10、尾置返回类型
要想引入尾置类型,我们还得从复杂的类型声明说起。
int arr[10] = {0}; //定义一个含有10个int元素的数组。
int (*p_arr)[10] = &arr; //注意:int *p_arr[10] 表示一个数组,有10个元素,元素类型是int*
int (*func(char x))[10]; //要定义一个函数,这个函数接受一个char类型的参数,并返回一个指向10个int类型数组的指针呢,不得不说,这个声明非常难以读懂。容易理解为返回一个数组,这个数组每个元素都是函数。
C++11的另外一个特性,尾置返回类型,任何函数都可以使用尾置返回类型, 这种形式对于返回类型比较复杂的函数最有效,比如上面的函数可以使用如下方式:
auto func(char x) -> int(*) [10];
这种形式将函数的返回类型写在函数声明的最后面,并且在函数形参列表后面加上 ->
符号,然后紧接着是函数需要返回的类型,由于函数的返回类型被放在了形参列表之后,所以在函数名前面使用一个 auto替代。
11、强枚举类型
在标准C++中,枚举类型不是类型安全的。枚举类型被视为整数,这使得两种不同的枚举类型之间可以进行比较。C++03
唯一提供的安全机制是一个整数或一个枚举型值不能隐式转换到另一个枚举别型。 此外,枚举所使用整数类型及其大小都由实现方法定义,皆无法明确指定。
最后,枚举的名称全数暴露于一般范围中,因此C++03两个不同的枚举,不可以有相同的枚举名。 (好比 enum Side{ Right, Left }; 和
enum Thing{ Wrong, Right }; 不能一起使用。)
C++11 引进了一种特别的 “枚举类”,可以避免上述的问题。使用 enum class 的语法来声明:
enum class Enumeration
{
Val1,
Val2,
Val3 = 100,
Val4 /* = 101 */,
};
此种枚举为类型安全的。枚举类型不能隐式地转换为整数;也无法与整数数值做比较。 (表示式Enumeration::Val4 == 101 会触发编译期错误)。
枚举类型所使用类型必须显式指定。在上面的示例中,使用的是默认类型 int,但也可以指定其他类型:
enum class Enum2 : unsigned int {Val1, Val2};
这个枚举类的核心意义在于,相当于给枚举加了命名空间。
Enum2 x = Enum2::Val1;
// cout << x << endl; //这个值不能打印出来,会报错
17、智能指针share_ptr,unique_ptr
简单地说,智能指针只是用对象去管理一个资源指针,同时用一个计数器计算当前指针引用对象的个数,当管理指针的对象增加或减少时,计数器也相应加1或减1,当最后一个指针管理对象销毁时,计数器为1,此时在销毁指针管理对象的同时,也把指针管理对象所管理的指针进行delete操作。
unique_ptr:作用域结束之后自动释放资源,不可复制,可以移动
shared_ptr:通过引用计数共享资源,当引用计数为0时,自动释放资源
weak_ptr: 一个shared_ptr的弱引用,不修改引用计数,为了解决循环引用问题而引入。
#include <cassert>
#include <memory>
int main() {
{
std::unique_ptr<int> upi(new int(6));
}
{
// 用make_shared来初始化shared_ptr
std::shared_ptr<int> spi = std::make_shared<int>(6);
// use_count获取引用计数
assert(spi.use_count() == 1);
{
std::shared_ptr<int> spi_shared(spi);
assert(spi.use_count() == 2);
}
assert(spi.use_count() == 1);
}
{
std::shared_ptr<int> spi = std::make_shared<int>(6);
assert(spi.use_count() == 1);
// 通过shared_ptr来构造weak_ptr
std::weak_ptr<int> wpi(spi);
// weak_ptr不改变引用计数
assert(spi.use_count() == 1);
assert(wpi.use_count() == 1);
// lock() 获取weak_ptr引用的shared_ptr
assert(*wpi.lock() == 6);
// expired() 返回引用的对象是否已经释放
assert(!wpi.expired());
}
return 0;
}
18、STL容器
(1)std::array :
array是C++11新引入的数组类型,和std::vector不同的是array的长度是固定的,不能动态拓展。
template <class T, std::size_t N> struct array
std::array<int, 3> a1{{1, 2, 3}};
std::sort(a1.begin(), a1.end());
张三思评:
那么 array 的意义在哪? 是为了完全替代内置数组吗?可是语法并不简洁啊!! 直接用const vector不就行了吗?
(2)std::forward_list :
C++11引入的新类型,forward_list是单链表(std::list是双链表),只需要顺序遍历的场合,forward_list能更加节省内存,插入和删除的性能高于list。
std::forward_list<int> fli{{1, 2, 3, 4}};
(3)unordered :
std::set
std::multiset
std::map
std::multimap
用平衡树实现的有序的容器,插入、删除和查找的时间复杂度都是O(nlogn)。
std::unordered_set
std::unordered_multiset
std::unordered_map
std::unordered_multimap
C++11引入的新类型,用hash实现的无序的容器,插入、删除和查找的时间复杂度都是O(1),在不关注容器内元素顺序的场合,使用unordered的容器能获得更高的性能。
19、其他内容
(1)static_assert:做编译期间的断言,因此叫做静态断言,其语法:static_assert(常量表达式,提示字符串)。
(2)tuple:适用于存储不同类型和不同数量的元素。 (3)bitset:位图,适用于进制操作和海量数据排序。
(4)正则表达式:regex则是C++11中新增的正则表达式库。
相关工作
大山喵在他的文章里描述了long和long lonng的区别[1]。
青萍之末在他的文章里详细列举了C++11相对于之前版本而言增加的新特性[2],本文已基本以此为核心进行拓展。
编程网的文章中简单列举了C++版本和编译标准可选项[4]。
参考
[1]大山喵.C++中long 和long long的区别.blog.csdn.net/CV_Jason/ar…
[2]青萍之末. C++11特性总结. blog.csdn.net/daaikuaichu…
[3]decltype cppReference.en.cppreference.com/w/cpp/langu…
[4]编程网.GCC -std编译标准一览表, c.biancheng.net/view/8053.h…