C++11新特性(语法糖,新容器)

简介: C++11新特性(语法糖,新容器)

距离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这种就快快用起来吧

目录
相关文章
|
2月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
111 59
|
2月前
|
存储 搜索推荐 C++
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
53 2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
|
2月前
|
存储 C++ 容器
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器1
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
55 5
|
2月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
57 2
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
2月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
36 0
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(三)
【C++】面向对象编程的三大特性:深入解析继承机制