小言_互联网的博客

06-现代C++有哪些新特性

339人阅读  评论(0)

前言

C++在1978年就由Bjame Sgoustrup在Bell实验室发明出来,但是第一次标准化已经是十多年后的1994年了。发布了第一个国际通用标准C++,C++98。此后十多年里没有更新,直到2011年发布了C++11。从C++11过后C++标准委员会平均3年发布一个版本,今年也是C++20发布。但是他的小弟Java自从1996年Sun公司发布第一个开发工具包JDK1.0开始,平均一年一个小版本,三年一个大版本。这一点上C++还是落后了,但是在C++11之后C++的现代化历程开始了,也带来一些易用性的提升,我们就来看一看这部分提升到底有什么效果。

高级for

在C++11之前,如果我们需要对一个vector或者list进行遍历,我们通常需要使用迭代器。方法如下

for (vector<int>::iterator i = vec_int.begin(); i != vec_int.end(); i++)
{
	cout << *i << " ";
}

但是C++11引入了一个基于范围的for循环,使用方法for(int temp : vec_int)。其中int替换为容器内的类型,temp是一个临时变量,vec_int是一个容器。这样上面这个代码就可以精简为

for (int temp : vec_int)
    {
        cout << temp << " ";
    }

这是两种方法的结果

可以看出来的是原有的for循环方式能够实现的方式在新版for循环方式中都可以实现。所以在C++11的工程中尽量使用新版for循环。

  • 使用条件: 容器必须提供begin()end()函数,并且容器边界确定。其实这两个条件在旧版C++中可以用iterator+for可以实现那么在新版的for循环中也可以实现。

类型推导

auto

很多知道C++是静态类型语言但是这个auto来的就很奇特。下面我们就来看看这个·auto是如何使用的。

int i = 50;
auto temp = i;
cout << "i  ‘s  type : " << typeid(i).name() << endl;
cout << "temp‘s type : " << typeid(temp).name() << endl;

执行结果如下:

这是最普通的auto用法,但是如果原有类型加上了const,&,*这个结果可能就不再这么简单了。

  • const auto cx = x; 此时cx是一个const int(非引用非指针);
  • const auto& rx = x;此时rx是一个const int&;
  • auto&& ur1 = x;此时ur1是一个int&;
  • auto&& ur2 = cx;此时ur2是一个const int&;
  • auto&& ur3 = 27;此时ur3是一个int&&;

但是上面这些auto推导的结果如果使用typeid()得到的结果依然是int 类型,这是由于这个函数局限性导致的。如果要得到更加准确地推导结果,可以使用来自Boost的TypeIndex库。

左值在auto&&推导中,会被直接推导为左值引用,int&&&->int&;右值推导为int,int&&->int&&

可能会有疑问就是,既然C++是一个静态类型语言,那么这个auto怎么能够在运行期才确定类型呢?这并不矛盾,auto变量在编译期就确定了,只不过这个事情由编译器帮你完成了。

decltype

其实这个decltype和auto的功能类似。他有两个基本用法:

  1. decltype(变量名):可以获得变量类型,decltype(a) i = 50,得到i为int类型变量;
  2. decltype(表达式):可以获得表示的引用类型,decltype((a))获得int&;但是如果表达式是一个纯右值则得到一个值类型,decltype(a+a)获得int。

decltype(auto)

C++14新加入,这个表达式的执行顺序就是先用初始化表达式替换的decltype(auto)中的auto,然后根据decltype语法规则确定类型。

可能会说既然有了auto还要什么decltype(auto),虽然auto功能强大,但是有一个事情是无法处理的。auto x = ex;是值类型还是引用类型呢?也有人会说为什么不用decltype(ex) x = ex;?当然可以但是程序员看到这长长的代码不经陷入了沉思,怎么不统一成decltype(auto)

还有一种情况就是在函数返回值的时候,如果我们在编程时不清楚返回值类型或者返回值类型十分复杂。我们可以使用一个auto类型。但是在C++11中auto类型的使用并不好用,

auto foo() -> 返回值类型声明
{
	// code
}

具体用法

//C++11
auto foo() -> decltype(int)
{
	// code
}

在C++14中这个操作可以变得更加简单

//C++14
decltype(auto) foo()
{
	// code
}

大括号妙用

列表初始化

对于一个标准容器vector来说向里面添加数据,在C++98中十分复杂。但是C++11引入了一种全新的初始化方法。

//C++98
vector<int> vec_int_1;
vec_int.push_back(1);
vec_int.push_back(2);
vec_int.push_back(3);

//C++11
vector<int> vec_int_2{1, 2, 3};

大家可能会问这个是不是只有在标准容器里面才可以使用呢?

当然不是,这是通过initializer_list<int>实现的,只要你的类里面有initializer_list<int>初始化类型那么所有的{}都是调用这个重载函数。 看看下面这个例子:

class A {
public:
    A(initializer_list<int> list){
        for (int temp : list)
        {
            cout << temp << " ";
        }
    }
};

int main() {
    A a{9,8,7,6,5,4,3,2,1};
}

执行结果如下:

可以看到自定义的类型可以实现{}初始化。

这里还有一个问题就是,当存在多个重载构造函数时,如果使用{}会通过强制转换使用含有initializer_list的构造函数,如果转换失败就直接报错。

统一初始化

前面讲的是带有initializer_list的构造函数,但是如果我们的构造函数中不包含这重载函数。那么这个情况就会发生改变,一旦使用{}则不允许进行隐形转换。下面这个例子:

class B {
public:
    B(int a) {
        cout << "B create : " << a << endl;
    }
};

//main
B b1{5};
B b2(5);

两者执行结果完全一致:

但是如果将主函数改为:

B b1{5.0};
B b2(5.0);

此时就会报错:

针对前面两个{}使用可以总结的是:

  • 如果构造函数没有使用初始化列表,初始化可以全部使用统一初始化语法;
  • 如果构造函数使用了初始化列表,则{}只能使用初始化列表的情况。

类成员默认初始化

在C++98中由于类在执行期才能才能分配内存空间,所以不允许对C++类进行默认初始化。但是在C++11中,引入了这个功能,允许在声明数据成员时直接给予一个初始化表达式。

class C {
public:
    C(){}
    void print(){ cout << " C -> x_ : " << x_ << endl; }
private:
    int x_{50};
};

//main
C c1;
c1.print();

执行结果:

静态断言

这个功能的语法很简单:

static_assert(编译期条件,可选输出信息);

//例子
static_assert((alignment & (alignment - 1)) == 0," error ");

静态断言的限制是,断言本身必须是常量表达式,如果这样的i不是常量,静态断言是不符合语法的。

特种成员函数

在C++中有六个特殊的成员函数:默认构造函数,析构函数,拷贝构造函数,移动构造函数,复制赋值运算符,移动赋值运算符。 这六种函数有一些能够在没有用户声明的情况下自动生成。但是其中一些函数一旦被用户声明,则会影响另外的一些函数的自动生成。我们就来看一下有哪些规则:

图中有箭头的地方就表明存在抑制作用,比如如果用户声明了析构函数,那么编译器就不会自动生成拷贝函数和移动函数了。

在C++98中,不允许复制对象采用的方法是将拷贝构造函数和拷贝赋值运算符声明为private。但是在C++11中引入了一个新的关键字,delete。可以直接声明为:

A(A &a)=delete;

override和final

在Java中对于父类的函数进行重写需要声明为

@Override
public void onCreate(Bundle savedInstanceState)

但是C++在C++11之前并没这个功能,override显示声明了成员函数是一个虚函数且覆盖了基类中的该虚函数。他的主要作用在于:

  • 让开发人员明白,这个函数覆写了基类的成员函数;
  • 让编译器进行检查,防止没有正确覆盖父类函数。

其实这个final关键字在Java中也是由来已久了,final

  • 声明该成员函数是一个虚函数,并且该虚函数不可在派生类中被覆盖;
  • 声明这个类不可被继承,这是放于类名称后方。
class A {
	virtual void foo1();
	virtual void foo() final;
}

class B final : public A {
	virtual void foo1() override;
}

class B由于final修饰导致不能继承;class A中的foo()由于final的修饰不能覆盖;class B中由于override修饰声明foo1()是一个重写函数。

但是为了和旧版C++兼容,所以final和override只有在特定位置时才认为是关键字,其余位置时则为普通的变量名。

总结

C++11发布已经有将近10年了,但是他给C++带来易用性提升确实现在依然还在使用学习的。C++11带来的易用性提升还有很多比如,nullptr,using,noexcept等等,学习的道路依然艰辛!希望我的这部分拙见能够帮助到大家!谢谢!


转载:https://blog.csdn.net/deng821776892/article/details/105928107
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场