距离C++11版本发布已经过去那么多年了,为什么还称为新特性呢?因为笔者前面探讨的内容,除了auto,范围for这些常用的,基本上是用着C++98的内容,虽说C++11已经发布很多年,却是目前被使用最广泛的版本。因为新版本的发布,编译器要很长时间才能支持新语法新特性,有一句话说得挺好,C/C++之所以强大,是因为它们的编译器很强大。C++23版本已经发布,但真想普遍使用不知道还得多少年呢?尤其是C/C++这种基本用于底层架构的语言,底层代码不敢改,也不愿意改。但不管怎么说,日后的开发是离不开C++11的,C++11可以说是C++历年来最大的版本更新,多了很多非常强大的东西,但也多了很多鸡肋,内容太多太多,不可能全部提完。故而笔者着重讲述必须要掌握的,用处一般的就简单提提,剩下的,大家碰到了再查,C++11新特性部分将分为多篇内容,让我们开始吧
array & forward_list
C++11新更新了两个容器,分别是array,forward_list
array本质上是对C语言的数组进行了一层封装,加上了模板,迭代器等功能
array的定义如上,它也是一个静态数组,声明时需要指定类型和大小,整体使用下来并不比C语言的数组强多少,唯一比C语言数组好用的就是会检查下标,我们知道C语言的数组越界读是不会报错的,越界写也仅是抽查,所以不小心写错下标可能会造成一些问题
但是vector也能检查下标呀,我为何不使用vector呢,况且vector功能比array强大多了,所以array算是C++11里比较鸡肋的一个
forward_list是新推出的单链表,在C++推出STL库时,只有list,list的底层实现我们前面也探讨过了,本质是双链表。forward_list算是list的青春版,因为双链表的节点要链接前后,所以会比单链表多存储一个指针大小,如果在内存空间特别紧凑,并且单链表足够满足使用场景,可以考虑使用forward_list,其基础功能如下,只能头插和尾插
auto,typeid,decltype
auto,typeid,decltype都是C++11推出的和类型相关的特性功能
auto是语法糖,为什么叫语法糖,因为让人用的舒服,auto是很好用的,在一些场景下,有的类型被层层封装,导致其类型名很长,有了auto就不用我们自己去推断类型
这仅是auto应用场景之一,后面我们会见到大量使用auto的场景,例如范围for就是使用auto来推导要迭代的类型,auto是根据初始值来进行类型推导的,也就是说你要使用auto推导必须给出一个初始值,否则没有意义
auto test; //没有初始值,无意义的推导
如果你想把某个变量或者表达式的类型给打印出来看看,那么就可以使用typeid,具体用法如下图,typeid是一个类,把想知道类型的变量或者表达式传过去,调用name
调用name之后,会返回一个字符串,字符串的内容即是推导出的类型
如果现在提一个过分一点的要求,我不仅要你推导出类型,还要用你推导的结果再声明出同类型的对象,使用typeid是做不到了,因为它返回的是字符串,不可能作为类型声明符,但是decltype可以做到,看看decltype是如何使用的
上图用decltype推导出test_1的类型,并用推导结果声明了变量test_2
这里的应用场景看着比较傻,类型推导在模板里应用比较广泛,如下
按照常规方法是不好解决的,因为我们不明确 T1 和 T2的类型到底是什么,就没有办法给变量ret_val确定类型,但是现在我们有了auto 和 decltype就很容易解决这个问题
范围for
范围for也是C++11中使用体验不错的语法糖,范围for本质就是循环遍历对象,范围for的出现帮我们节省了不少时间,如下列程序
不仅内置类型可以用,容器也是可以使用范围for的,如下图程序
只要容器支持迭代器,那么它就可以使用范围for,因为容器使用范围for本质还是在调用容器中实现的迭代器 ,但是范围for使用起来方便不少,使用汇编可以一窥细节
//测试代码 int main() { vector<int> test; for (int i = 1; i <= 10; i++) { test.push_back(i); } //迭代器 auto it = test.begin(); while (it != test.end()) { cout << *it << " "; it++; } cout << endl; //范围for for (auto& t : test) { cout << t << " "; } return 0; }
可以看出两者都是在调用迭代器,我们手动调用的begin()和end()是经过封装过的,范围for则直接调用迭代器的底层实现,在容器中范围for本质和迭代器没区别,但是用的更省心
使用范围for时,如果auto推导类型后跟上&,就是引用调用,可以读写原数据
如果没有跟上&,那么就是传值调用,修改并不会影响原数据
如果只想读不想写的话推荐 const auto & 这种写法,减少拷贝消耗
统一的列表初始化
平时我们给C语言的数组进行初始化操作时,可以使用一对花括号进行赋值" { } "
//使用花括号给数组进行初始化赋值 int arr[] = {1, 2, 3, 4, 5, 6};
其实这样的初始化还挺好用的,我们平时使用vector进行赋值时就没那么方便,如果赋值有顺序还好,没顺序的话还要自己手动push_back,于是C++11提出了统一初始化列表,也就是说让STL中的容器也够支持使用" { } "来进行赋值,如下图
可不仅仅是只有vector能使用,其它容器也是支持的,如下图的list和map
你甚至可以直接不写赋值符号 "=" ,同样能完成初始化,不仅可以用于数组,容器,对于单个内置类型也是可以使用{}来初始化的,如下图
除了内置类类型和STL库中的容器支持这种初始化,自定义类型也是支持的
需要注意的是,使用{}初始化自定义类型,是去调用自定义类型的构造函数,由此看来,C++11之后确实可以统一使用{ }来初始化,这也是为什么叫统一的列表初始化
统一的列表初始化原理
像数组可以使用{}进行初始化可以理解,毕竟原生的编译器就支持,但是容器也支持{}初始化是怎么做到的呢? 其实实现原理也不难,我们以vector为例,既然是初始化,那我们就紧盯构造函数,打开资料库查查C++11的构造函数有没有发生变化
果然,我们发现,构造函数中多了一个initializer list,而这就是统一列表初始化实现的秘密,可以看出该构造函数使用的是initializer_list,我们查一下这是个什么东西
我们大概能理解,使用{ }时,会将里面的内容放到initializer_list容器中存放起来,然后把该容器内的值拷贝给vector,如此就完成了初始化操作,知道原理了,我们可以自己尝试给之前写的vector也添加这么个功能
vector(std::initializer_list<T> _lt) { _begin = new T[_lt.size()]; _finish = _begin + _lt.size(); _end = _begin + _lt.size(); auto _vtp = _begin; auto _ltp = _lt.begin(); while (_ltp != _lt.end()) { *_vtp = *_ltp; _vtp++; _ltp++; } }
这个只能构造,若想拷贝赋值的话,可以重载一个operator=( std::initializer_list<T> _lt)
具体定义如下,别忘了包含头文件initializer_list
vector<T>& operator=(std::initializer_list<T> _lt) { vector<T> tmp(_lt); std::swap(_start, tmp._start); std::swap(_finish, tmp._finish); std::swap(_endofstorage, tmp._endofstorage); return *this; }
nullptr
出现nullptr是因为C++错误的将库中的NULL定义为0,这就会导致很多问题,因为NULL不仅表示一个空指针,还是一个字面常量值0,如下述代码
简单写个代码验证其中的危害性
int main() { int p = NULL; cout << p << endl; int t = 0; if (t == NULL) cout << "t是一个空指针" << endl; return 0; }
运行结果如上,t被错误的判断为一个空指针,事实上,t连指针都不是。错已经错了,直接改NULL会影响原先的代码,为了解决这个问题,C++又推出了nullptr来代替NULL
nullptr则是正确的定义 (void*)0,所以C++中请使用nullptr来表示空指针
至此,本篇文章就结束了,总体下来还算轻松,很多内容都是见过多次的老朋友了,其中也多了不少好用的特性,像统一初始化列表,范围for这种就快快用起来吧