- 1:C++11
- 1.1:初始化列表Initializer List
- 1.2:类型推导
- 1.3:遍历
- 1.4:空指针nullptr
- 1.5:强类型枚举enum class
- 1.6:静态断言 static assert
- 1.7:构造函数相互调用
- 1.8:禁止重写final
- 1.9:定义成员初始值
- 1.10:默认构造函数 default
- 1.11: 删除构造函数 delete
- 1.12:常量表达式 constexpr
- 1.13:Lambda函数
- 1.14:右值引用
- 1.15:智能指针
- 2:C++14
- 2.1:函数返回值类型推导
- 2.2:lambda参数auto
- 2.3:变量模板
- 2.4:别名模板
- 2.5:constexpr的限制
- 2.6:[[deprecated]]标记
- 2.7:二进制字面量与整形字面量分隔符
- 3:C++17
- 3.1:构造函数模板推导
- 3.2:结构化绑定
- 3.3:内联变量
- 3.4:折叠表达式
- 3.5:constexpr lambda表达式
- 3.6:namespace嵌套
- 3.7:在lambda表达式用*this捕获对象副本
C++编程语言最初于1998年由ISO标准化即C++98,随后被C++11,C++14和C++17标准进行了修订。当前的C++ 20标准以新功能和扩展的标准库取代了这些标准。本文将介绍C++20前,C++11到C++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());
}
for(auto &item:vector){
printf("%s\n",item.c_str());
}
空指针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
};
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");
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();
};
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));
捕获列表 | 作用 |
[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 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
总结
-
从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
-
右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
-
作为函数形参时,右值引用更灵活。虽然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;
}
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++11中constexpr函数可以使用递归,在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++11中constexpr函数必须必须把所有东西都放在一个单独的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};
结构化绑定
通过结构化绑定,对于tuple、map等类型,获取相应值会方便很多:
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());
}
结构化绑定不止可以绑定pair和tuple,还可以绑定数组和结构体等:
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);
内联变量
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++17前lambda表达式只能在运行时使用,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);};
}
};