CPP : C++11、C++14和C++17

文章目录[x]
  1. 1:C++11
  2. 1.1:初始化列表Initializer List
  3. 1.2:类型推导
  4. 1.3:遍历
  5. 1.4:空指针nullptr
  6. 1.5:强类型枚举enum class
  7. 1.6:静态断言 static assert
  8. 1.7:构造函数相互调用
  9. 1.8:禁止重写final
  10. 1.9:定义成员初始值
  11. 1.10:默认构造函数 default
  12. 1.11: 删除构造函数 delete
  13. 1.12:常量表达式 constexpr
  14. 1.13:Lambda函数
  15. 1.14:右值引用
  16. 1.15:智能指针
  17. 2:C++14
  18. 2.1:函数返回值类型推导
  19. 2.2:lambda参数auto
  20. 2.3:变量模板
  21. 2.4:别名模板
  22. 2.5:constexpr的限制
  23. 2.6:[[deprecated]]标记
  24. 2.7:二进制字面量与整形字面量分隔符
  25. 3:C++17
  26. 3.1:构造函数模板推导
  27. 3.2:结构化绑定
  28. 3.3:内联变量
  29. 3.4:折叠表达式
  30. 3.5:constexpr lambda表达式
  31. 3.6:namespace嵌套
  32. 3.7:在lambda表达式用*this捕获对象副本

C++编程语言最初于1998年由ISO标准化即C++98,随后被C++11C++14C++17标准进行了修订。当前的C++ 20标准以新功能和扩展的标准库取代了这些标准。本文将介绍C++20前,C++11C++17每个版本的主要特性。

C++11

初始化列表Initializer List

所有STL容器都支持初始化列表,如下:

std::list<int> list = {1,2,3};
std::vector<int> vector = {1,2,3};
std::set<int> set = {1,2,3};
std::map<int,std::string> map = {{1,"m"},{2,"e"}};

在自定义class上支持初始化列表:

#include <initializer_list>

 

class Model {

 

public:
std::vector<int> m_items;

 

 

public:
Model(const std::initializer_list<int>& items);

 

};

 

Model::Model(const std::initializer_list<int> &items)
{
m_items = items;
}

//使用
Model model = {1,4,5};

类型推导

编译器会自动推导出正确的类型。字面量也可以:

auto i =1; //int
auto d = 1.1; //double
auto s = "hi"; //const char*
auto a = {1,2}; //std::initializer_list<int>

推导对于泛型编程非常方便,比如:

template <typename T,typename M> auto add(T const& t,M const& m){
return t+m;
}

add(1,1); // int add(int,int);
add(1.1,1);// double add(double,int);

遍历

以前遍历vector一般是这么写的:

std::vector<int> vector = {2,4,5} ;
for (std::vector<int>::const_iterator itr = vector.begin(); itr != vector.end(); ++itr) {
printf("value :%d\n",*itr);
}
这样写有两个缺点:
  • 迭代器声明很冗长 (用auto可以部分解决)
  • 循环内部必须对迭代器解引用
可以使用的新的遍历方式:
std::vector<std::string> vector = {"m","e","l","r","o","s","e"} ;

for(auto item:vector){
printf("%s\n",item.c_str());
}
代码立马简洁了许多。但是要注意,这里每次循环,会对item进行一次拷贝。因为string是一个class,就更希望用引用的方式进行遍历,一般写成:
for(auto &item:vector){
printf("%s\n",item.c_str());
}
auto&即可以变成引用方式遍历,甚至还能在循环中改变它的值。也可以使用const auto &

空指针nullptr

以往使用NULL表示空指针。它实际上是个为0的int值。下面的代码会产生岐义:

void f(int i) {} // chose this one
void f(const char* s) {}

f(NULL);

为此C++ 11新增类型nullptr_t,它只有一个值nullptr。上面的调用代码可以写成:

void f(int i) {}
void f(const char* s) {} // chose this one

f(nullptr);

强类型枚举enum class

原来的enum有两个缺点:

  • 容易命名冲突
  • 类型不严格
enum Direction {
Left, Right
};

enum Answer {
Right, Wrong
};
这段代码编译报错,Right重定义。这里使用了单个单词作为名称,很容易出现冲突。所以我们一般加个前缀,变成:
enum Direction {
Direction_Left, Direction_Right
};
enum Answer {
Answer_Right, Answer_Wrong
};
这样写很难看,而且如果这两个枚举是分别从两个第三方库引入的,那就无法自己改名字了。而且改成这样依然有个问题:
auto a = Direction_Left;
auto b = Answer_Right;if (a == b)
printf("a == b\n");
else
printf("a != b\n");
这个代码将输出a == b,因为这两上值都为0。然而允许两个不同类型的值作比较,就是不合理的,容易隐藏一些bug
C++ 11引入了enum class
enum class Direction {
Left, Right
};enum class Answer {
Right, Wrong
};auto a = Direction::Left;
auto b = Answer::Right;if (a == b)
printf("a == b\n");
else
printf("a != b\n");
  • 引用时必须加上枚举名称(Direction_Left变成Direction::Left),似乎写法上差不多,但是这样类型更加严格。下面的a == b编译将会报错,因为它们是不同的类型。
  • 枚举值不再是全局的,而是限定在当前枚举类型的域内。所以使用单个单词作为值的名称也不会出现冲突。

静态断言 static assert

static_assert可在编译时作判断。

static_assert( size_of(int) == 4 );

构造函数相互调用

class Person{
private:
std::string m_name;
int m_age;

public:

Person(std::string const& name,int age)
{
m_name = name;
m_age = age;
}

Person(std::string const&name):Person(name,25)
{

}
};

禁止重写final

禁止虚函数被重写

class A {
public:
virtual void f1() final {}
};

class B : public A {
virtual void f1() {}
};

此代码编译报错,提示不能重写f1。虽然f1是虚函数,但是因为有final关键字,保证它不会被重写。你可能会说,那不声明virtual不就完了。但是如果A本身也有基类,f1是继承下来的,那virtual就是隐含的了。

禁止类被继承

class A final {
};

class B : public A {
};

此代码编译报错,提示不能继承A

定义成员初始值

class Mat{
private:
int m_cols;
int m_rows;
public:
Mat():m_cols(100),m_rows(100)
{

}
};

默认构造函数 default

当一个class有自定义构造函数时,编译器就不会自动生成一个无参构造函数。现在可以通过default关键字强制要求生成这个构造函数。

class A {
public:
A(int i) {}
A() = default;
};
//or
class A {
public:
A(int i) {}
A(){}
};

 删除构造函数 delete

以往,当需要隐藏构造函数时,可以把它声明为private成员:

class A {
private:
A();
};
现在可以使用delete关键字:
class A
public:
A() = delete;
};

常量表达式 constexpr

int size() { return 5; }
int arr[size()];

上面的代码编译失败,因为静态数组的大小必须在编译期确定。改成:

constexpr int size() { return 5; }
int arr[size()];

加上了constexpr,函数size变成在编译期计算,返回值被看成一个常量。

Lambda函数

这是个非常强大的重量级功能。简单地讲,就是可以用它定义一个临时的函数对象,它像其它对象一样可以传递和保存。更为强大的是,它甚至可以访问当前函数的上下文。

定义

定义方式如下:

auto func = [](int a,int b){return a+b;};

printf("result:%d\n",func(4,5));

作为参数传递

#include <functional>

void print_result(int a,int b,std::function<int(int,int)> const&func)
{
printf("result: %d\n",func(a,b));
}

print_result(3,4,[](int a,int b){return a-b;});

作为参数传递时,需要先导入头文件functional,使用function类来定义需要传入Lambda类型。

访问当前函数的上下文

int m = 10;
auto func = [=](int a,int b){return a+b+m;};
printf("result:%d\n",func(4,5));
可以看到前面的[]改成了[=],这表示Lambda使用值传递的方式捕获外部变量。[]表示捕获列表,用来描述Lambda访问外部变量的方式。如下:
捕获列表 作用
[a] a为值传递
[a, &b] a为值传递,b为引用传递
[&] 所有变量都用引用传递。当前对象(即this指针)也用引用传递。
[=] 所有变量都用值传递。当前对象用引用传递。

右值引用

什么是左值、右值

首先不考虑引用以减少干扰,可以从2个角度判断:左值可以取地址、位于等号左边;而右值没法取地址,位于等号右边。

int a =5;
  •  a可以通过 & 取地址,位于等号左边,所以a是左值。
  • 5位于等号右边,5没法通过 & 取地址,所以5是个右值。

可见左右值的概念很清晰,有地址的变量就是左值,没有地址的字面值、临时值就是右值。

什么是左值引用和右值引用

引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝,其实现原理和指针类似。 个人认为,引用出现的本意是为了降低C语言指针的使用难度,但现在指针+左右值引用共同存在,反而大大增加了学习和理解成本。

左值引用

左值引用大家都很熟悉,能指向左值,不能指向右值的就是左值引用:

int a =5;
int &ref_a = a;
引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。但是,const左值引用是可以指向右值的:
const int &ref_a = 5; // 编译通过
右值引用

再看下右值引用,右值引用的标志是&&,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值:

int &&ref_a_right = 5; // ok

int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
ref_a_right = 6; // 右值引用的用途:可以修改右值

右值引用指向左值

int a = 5; // a是个左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向

cout << a; // 打印结果:5

总结

  1. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。

智能指针

c++11的标准库包含智能指针,有了智能指针,我们便不需要手动去delete释放资源。智能指针用于帮助确保程序没有内存和资源泄漏。

智能指针主要分为unique_ptr、shared_ptr和weak_ptr。会在之后的文章中详细介绍。

 

C++14

函数返回值类型推导

C++14对函数返回类型推导规则做了优化:

auto  func(int a, int b)
{
return a+b;
}
返回值类型推导也可以用在模板中:
template <typename T> auto func2(T const&a,T const&b)
{
return a+b;
}
如果return语句返回初始化列表,返回值类型推导也会失败:
auto func4(){
//error : cannot deduce return type from initializer list .
return {1,3,4};
}
如果函数是虚函数,不能使用返回值类型推导:
class Test{
//error: virtual function cannot have deduced return type
virtual auto func(){
return 1;
}
};

lambda参数auto

C++11中,lambda表达式参数需要使用具体的类型声明:

auto i = 1;
auto func = [&](int a){return a+i;};

C++14中,对此进行优化,lambda表达式参数可以直接是auto

auto i = 1;
auto func = [&](auto a){return a+i;};

变量模板

C++14支持变量模板:

template<class T> constexpr T pi = T(3.14156);

printf("%d\n",pi<int>); // 3
printf("%lf\n",pi<double>); // 3.141560

别名模板

C++14也支持别名模板:

template<class T,class M>
class Test{
public:
T t;
M m;
};

 

template <typename T>
using Alias = Test<T,int>;

 

Alias<double> t;

constexpr的限制

C++11constexpr函数可以使用递归,在C++14中可以使用局部变量和循环:

constexpr int factorial(int n) { // C++14 和 C++11均可
return n <= 1 ? 1 : (n * factorial(n - 1));
}

C++14中可以这样做:

constexpr int factorial(int n) { // C++11中不可,C++14中可以
int ret = 0;
for (int i = 0; i < n; ++i) {
ret += i;
}
return ret;
}

C++11constexpr函数必须必须把所有东西都放在一个单独的return语句中,而constexpr则无此限制:

constexpr int func(bool flag) { // C++11中不可,C++14中可以
if (flag) return 1;
else return 0;
}

[[deprecated]]标记

C++14中增加了deprecated标记,修饰类、变、函数等,当程序中使用到了被其修饰的代码时,编译时被产生警告,用户提示开发者该标记修饰的内容将来可能会被丢弃,尽量不要使用。

class [[deprecated]]Deprecated{

};

当编译时,会出现警告。

warning: 'Deprecated' is deprecated [-Wdeprecated-declarations]
Deprecated deprecated;

二进制字面量与整形字面量分隔符

int a = 0b0001'0011;
double pi = 3.14'1592'6;

C++17

构造函数模板推导

C++17前构造一个模板类对象需要指明类型:

template <class Data> class Cache{
Data m_data;
public:
Cache(Data const& data):m_data(data){}
};

 

Cache<int> cache(1);

C++17就不需要特殊指定,直接可以推导出类型,代码如下:

Cache cache(1);
std::vector vector = {1,2,3};

结构化绑定

通过结构化绑定,对于tuplemap等类型,获取相应值会方便很多:

std::tuple<int, double> func() {
return std::tuple(1, 2.2);
}

auto[a,b] = func();
printf("a :%d ; b : %lf",a,b);

//map
std::map<int,std::string>map = {{1,"mel"},{2,"rose"}};
for(auto const& [key,value]:map){
printf("key:%d value:%s \n",key,value.c_str());
}

结构化绑定不止可以绑定pairtuple,还可以绑定数组和结构体等:

struct Point{
int x;
int y;
};

Point p;
p.x = 5;
p.y = 6;
auto [x,y] = p;
printf("x %d y %d",x,y);

内联变量

C++17前只有内联函数,现在有了内联变量,印象中C++类的静态成员变量在头文件中是不能初始化的,但是有了内联变量,就可以达到此目的:
class A{
const static int a ;
}; 

inline int const A::a = 10;

折叠表达式

C++17引入了折叠表达式使可变参数模板编程更方便:

template <typename ...T>auto add(T ...t)
{
return (t+ ...);
}

printf("sum :%d",add(1,2,3,4,5));

constexpr lambda表达式

C++17lambda表达式只能在运行时使用,C++17引入了constexpr lambda表达式,可以用于在编译期进行计算。

constexpr auto func = [](auto i){return i*i;};
static_assert(func(2) == 4);

注意:函数体不能包含汇编语句、goto语句、label、try块、静态变量、线程局部存储、没有初始化的普通变量,不能动态分配内存,不能有new delete等,不能虚函数。

namespace嵌套

namespace A
{
namespace B
{
namespace C
{
void func(){}
}
}
}
A::B::C::func();

在lambda表达式用*this捕获对象副本

正常情况下,lambda表达式中访问类的对象成员变量需要捕获this,但是这里捕获的是this指针,指向的是对象的引用,正常情况下可能没问题,但是如果多线程情况下,函数的作用域超过了对象的作用域,对象已经被析构了,还访问了成员变量,就会有问题。

class Test{
int a ;

void test(){
auto func = [this](){printf("%d",a);};
}

};

所以C++17增加了新特性,捕获*this,不持有this指针,而是持有对象的拷贝,这样生命周期就与对象的生命周期不相关啦。

class Test{
int a ;

 

void test(){
auto func = [*this](){printf("%d",a);};
}

};

 

 

 

点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

Title - Artist
0:00