C++11学习记录:核心语言功能特性
Posted 河边小咸鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11学习记录:核心语言功能特性相关的知识,希望对你有一定的参考价值。
- 本篇笔记汇总了C++11中的主要新语言功能,根据个人理解与查阅的资料进行记录。
- 主要参考地址:cppreference
目录
- · 模板优化
- · auto 与 decltype
- · 预置与弃置的函数
- · final 与 override
- · 尾随返回类型
- · 右值引用
- · 移动构造函数与移动赋值运算符
- · 有作用域枚举
- · constexpr 与字面类型
- · 列表初始化
- · 委托与继承的构造函数
- · 花括号或等号初始化器
- · nullptr
- · long long
- · char16_t 与 char32_t
- · 类型别名
- · 变参数模板
- · 推广的(非平凡)联合体
- · 推广的 POD (平凡类型与标准布局类型)
- · Unicode 字符串字面量
- · 用户定义字面量
- · 属性
- · lambda 表达式
- · noexcept 说明符与 noexcept 运算符
- · alignof 与 alignas
- · 多线程内存模型
- · 线程局部存储
- · GC 接口
- · 范围 for (基于 Boost 库)
- · static_assert (基于 Boost 库)
· 模板优化
1. 对右尖括号的优化
简单来讲,就是在C++11以前,当在模板使用中出现双右尖括号的时候,编译器会解析为右移符号 >>
。这就导致模板嵌套写起来不太方便,右括号之间需要用空格来空开。
//此句在C++11前是错误的,因为 >> 会解释为右移符号
vector<vector<int>> test;
//在C++11前,一般都加个空格给空开
vector< vector<int> > test;
在C++11中其改进了编译器的解析规则,尽可能多的将多个右尖括号 >
解析成模板参数结束符,从而方便代码编写。
2. 默认模板参数
在C++11中,模板参数支持设定默认值。当未设定模板类型时,编译器首先会根据传参进行类型推导,当推导失败时,就会使用模板参数的默认值(默认值没有的话就会报错)。
template <typename T = long, typename U = int>
void test(T t = 'a', U u = 'b')
cout << t << " - " << u << endl;
//test<char, char>
test('a', 'b'); //a - b
//test<int, char>
test<int>('a', 'b'); //97 - b
//test<char, char>
test<char>('a', 'b'); //a - b
//test<int, char>
test<int, char>('a', 'b'); //97 - b
//test<char, int>
test<char, int>('a', 'b'); //a - 98
//test<long, int> 无法推导
test(); //97 - 98
· auto 与 decltype
1. auto
C++11中,出现了一个非常有用的关键字 auto
,即占位类型说明符,它可以自动推导出占位处的类型。在C++11中,其只能服务于变量;在C++14中,其可以服务于函数返回值;在C++17中,它可以服务于非模板形参 template<auto I> struct A;
;在C++20中,其也可以服务于函数形参 void f(auto);
。下面单就C++11中的 auto
用法(变量)进行一定的总结。
首先,其基本用法为 auto x = expr;
,此时编译器会从初始化器推导类型,具体规则参考模板实参推导的规则。所以在使用 auto
的时候,必须要指定初始化内容,这样才可以正确的推导出类型进行初始化。
auto a = 3.14; //double
auto b = 520; //int
auto c = 'a'; //char
auto d; //error未初始化
由于推导规则与模板实参推导规则一致,所以cv关键字的保留情况相同:
- 当变量不是指针或者引用类型时,推导的结果中不会保留
const
、volatile
关键字。 - 当变量是指针或者引用类型时,推导的结果中会保留
const
、volatile
关键字。
int temp = 222;
auto* a = &temp; //auto = int -> a : int*
auto b = &temp; //auto = int* -> b : int*
auto& c = temp; //auto = int -> c : int&
auto d = temp; //auto = int -> d : int
const auto e = temp; //auto = int -> e : const int
auto f = e; //auto = int -> f : int (忽略const)
const auto& g = temp; //auto = int -> g : const int&
auto& h = g; //auto = const int -> h : const int&
auto* i = &e; //auto = const int -> i : const int*
另外在C++11中,auto
不允许使用的场景主要有四个:
- 不能作为函数参数,因为函数调用时才会传实参,
auto
使用要求必须要给修饰的变量赋值,二者矛盾。 - 不能用于类的非静态成员变量初始化。原因和上一条一样,因为类的非静态成员变量在没创建对象的时候也是未定义的。
- 不能使用
auto
关键字定义数组。int array[] = ...
后,auto a = array
是被允许的,a 被推导为int*
类型;而auto b[] = array
是非法的,因为auto
无法定义数组。 - 无法使用
auto
推导函数模板。Test<double> t;
后,Test<auto> t1 = t
是不被允许的,因为auto
不算是一个类型,是没办法传进去的。
2. decltype
即 declare type
的缩写。其作用也是推导类型,其推导和 auto
一样都是在编译期完成的。语法为 decltype(表达式)
,其仅用于表达式类型的推导,不会理会表达式的值。但是有一点,auto
只能推导已初始化的变量类型,而 decltype
的可以推导比较复杂的表达式。
int a = 10;
decltype(a) b = 20; //b : int
decltype(a * 2 + 3.14) c = 13.14; //c : double
decltype
的主要规则如下,简单来说就是当表达式为纯右值时推导出来会剔除cv修饰(因为纯右值不能被cv修饰),其余都会都会保存cv修饰。
- 如果 表达式 的值类别是亡值,将会
decltype
产生 T&&。 - 如果 表达式 的值类别是左值,或者被括号
()
包围,将会decltype
产生 T&。 - 如果 表达式 的值类别是纯右值,将会
decltype
产生 T。
· 预置与弃置的函数
1. 预置
语法为 函数 = default;
。通过将函数体定义为 default
来显式预置函数定义。
在声明类或者结构体的时候,如果未创建构造参数,则编译器会自动帮你创建一个空参空函数体的默认构造函数。例子如下:
class Test
int x;
int y;
;
Test t();//可以编译
编译器生成默认构造函数的Test类:
class Test
Test()
int x;
int y;
;
所以上面才可以调用空参构造
但是,一旦添加了其他有参数的构造函数,编译器就不再生成缺省的构造函数了。而在C++11中,其允许我们使用 = default
来要求编译器生成一个默认构造函数:Test() = default
,这样就可以在使用其余带参构造函数时也能使用默认构造函数了。
2. 弃置
语法为 函数 = delete;
。通过将函数体定义为 delete
来显式弃置函数定义。其可以删除特殊成员函数以及普通成员函数和非成员函数,以阻止定义或调用它们。函数的弃置定义必须是翻译单元中的首条声明,已经声明过的函数不能声明为弃置的。
在 std::unique_ptr
里删除了传参为 unique_ptr
左值的构造参数,以及相关的 =
操作。这样就可以保证此智能指针的唯一性。我个人感觉还是挺有用的,这样直接删除就不用重载了。
· final 与 override
1. final
其作用为指定某个虚函数不能在派生类中被覆盖,或者某个类不能被派生。也就是说,其可以作用于函数或者类,但是作用于函数时只能是虚函数。此关键字写于虚函数或类的后面。
- 作用于虚函数:使用
final
修饰虚函数,阻止子类重写父类的此函数。
...
class Child : public Base
public:
void test() final
...
;
...
- 作用于类:使用
final
修饰类,此类无法被继承。
...
class Child final : public Base
public:
void test()
...
;
...
2. override
其作用为指定一个虚函数覆盖另一个虚函数。在成员函数的声明或定义中,override
说明符确保该函数为虚函数并覆盖某个基类中的虚函数。如果不是这样,那么程序会生成编译错误。
override
是在成员函数声明符之后使用时拥有特殊含义的标识符,其他情况下它不是保留的关键词。
...
class Child : public Base
public:
void test() override
...
;
...
· 尾随返回类型
尾随返回类型语法为auto 函数名(传参) -> decltype(表达式) 函数体
,返回值类型为 decltype
推导出的类型。
这个东西我感觉主要是为模板服务,使用场景主要是:
- 返回值随模板类型变化
template <typename T, typename U>
auto add(T x, U y) -> decltype(x + y)//这里说一嘴,此处的decltype不能填函数体内新声明的变量,比如z
auto z = x + y;
return z;
int a = 1;
double b = 3.14;
auto ret = add(a, b);
cout << ret << endl; //4.14
- 返回值类型比较复杂
auto fpif(int) -> int(*)(int)
· 右值引用
C++11中增加了一个新的很好用的类型,右值引用 &&
,就是对右值的引用。首先看一下左右值的区别,其实主要就是看能不能取地址:
- 左值为
locator value
,即lvalue
;右值为read value
,即rvalue
。 - 左值:储存在内存中、有明确存储地址的数据(可取地址)
- 右值:可以提供数据值的数据(不可取地址)
那么右值引用有什么作用?主要的作用就是延长右值的生命周期,以提高效率。 那么是如何提高效率的呢?比如说如下这个场景:
vector<vector<int>>vt;
vector<int>temp1, 2, 3, 4, 5;
vt.push_back(temp);
此段代码中,先声明了二维数组 vt
,随后声明一个一维数组 temp
来塞入容器 vt
。自此一维数组 temp
使命完成,其储存的右值也没有作用了。在 push_back()
操作中,其首先会将 temp
以左值引用传进去,随后再使用 construct
来创建一个 vector<int>
拷贝储存传进来的值,最后再放入容器(如下图)。这就导致在函数中,此组数据被完整拷贝了一次,降低了效率。
那么,既然 temp
只在此处有用,可否直接把 temp
放入 vt
中来减少那次拷贝呢?右值引用就是为了这个场景而出现的。例如上面这个问题的本质为:temp
的右值生命周期到此为止,想要将其生命周期延长给另一个变量 vt
,来避免对 temp
右值的复制。这时就可以传入右值来提高 vector::push_back()
的效率。 std::move源码分析
vector<vector<int>>vt;
vector<int>temp1, 2, 3, 4, 5;
vt.push_back(move(temp));//这里的std::move的作用是将左值转为右值
C++11中,STL中已经重载了很多函数的传右值引用版本,比如下图中 vector::push_back()
的右值引用版本,其只调用了 emplace_back
函数来延长生命周期,从而避免使用 construct
重新拷贝创建。
所以呢,在C++11以前,右值引用没出现时,实际面临的问题是分辨传入的是右值还是左值。 当右值引用出现后,函数就可以判断传入的是右值还是左值,从而做出更优的选择。例如 vector::push_back()
在接收到右值的时候,它就知道可以直接给这个右值改变"所有者",从而提高效率。
· 移动构造函数与移动赋值运算符
1. 移动构造函数
移动构造函数其实就是传参为本类右值的构造参数,来实现将传入右值拥有的内存资源"移为已用",这部分内容的实现被叫做移动语义。上面右值引用中举得 vector::push_back(value_type&&)
例子中,其实就是移动语义的一种实现,它延长了传入右值的存活时间。
移动构造函数在检测到传入内容为右值时,会将右值内容赋予新建的对象,并且删除右值原属主的内容,来实现移动语义。此类将内容"移为已用"的构造参数即可被称为移动构造参数。下面就是一个移动构造参数的例子:
class Test
public:
Test(int n) : num(new int(n))
cout<<"copy construct"<<endl;
//移动构造函数
Test(Test&& t) : num(t.num)
t.num = nullptr;
cout<<"move construct"<<endl;
private:
int* num;
;
Test t(2);
Test t1(move(t));
输出:
copy construct
move construct
2. 移动赋值运算符
移动赋值函数和上面的移动构造函数相似,只不过移动构造函数是在构造函数里接收右值操作,而移动赋值运算符是重载了 operator =
操作,使其接收一个右值,从而类可以进行 类名 = 类右值
这样的移动语义操作。下面是一个例子,其中移动构造函数和移动赋值运算符都有定义:
struct A
std::string s;
A() : s("测试")
A(const A& o) : s(o.s) std::cout << "移动失败!\\n";
A(A&& o) : s(std::move(o.s))
A& operator=(const A& other)
s = other.s;
std::cout << "复制赋值\\n";
return *this;
A& operator=(A&& other)
s = std::move(other.s);
std::cout << "移动赋值\\n";
return *this;
;
int main()
A a1, a2;
std::cout << "尝试从右值临时量移动赋值 A\\n";
a1 = f(A()); // 从右值临时量移动赋值
std::cout << "尝试从亡值移动赋值 A\\n";
a2 = std::move(a1); // 从亡值移动赋值
· 有作用域枚举
在C++11之前,枚举类型可能会出现一个问题:枚举值的重复。比如说下面这种情况:
//三原色
enum LightColor
red,//note: previous declaration ‘LightColor red’
green,
blue//note: previous declaration ‘LightColor blue’
;
//三基色
enum PaintColor
red,//‘red’ conflicts with a previous declaration
yellow,
blue//‘blue’ conflicts with a previous declaration
;
//如上这样定义就会出现枚举值重复情况 无法正常编译
在C++11之前为了解决这种情况都是将其放入另一个作用域(类或命名空间)中,比如下面就是放入别的命名空间:
//三原色
namespace Light
enum Color
red,
green,
blue
;
//三基色
namespace Paint
enum Color
red,
yellow,
blue
;
//定义
Light::Color c1 = Light::red;
Paint::Color c2 = Paint::red;
但是使用命名空间或类这个解法明显有点繁琐以及浪费,于是在C++11中推出了有作用域枚举。如下:
//三原色
enum class LightColor
red,
green,
blue
;
//三基色
enum class PaintColor
red,
yellow,
blue
;
//定义
LightColor c1 = LightColor::red;
PaintColor c2 = PaintColor::red;
这样,既解决了常规枚举值重复的问题,也让整体定义和使用变得没那么繁琐。
· constexpr 与字面类型
1. constexpr
在C语言中,const
关键字只有"只读"这一语义,但在C++中其引入了"常量"语义。在C++中,所谓"只读"和"常量"的区别大致在编译期间能不能直接确定初始值,若不能则被作为"只读变量"处理,若可以确定则被作为"常量"处理。 在 constexpr
出现之前,const
一直同时承担两种语义,故 constexpr
出现的意义便是承担"常量"这一语义。
所以,constexpr
表示在编译期就可以确定的内容,而 const
只保证运行时不直接被修改。我记得官方是建议凡是"常量"语义的场景都使用 constexpr
,只对"只读"语义使用 const
。另外在C++11中,constexpr
函数必须把一切放在单条 return 语句中,而在C++14后就无此要求了。
constexpr
可以修饰变量和函数。可以看到,C++标准库里的模板元编程内容都加上了 constexpr
修饰,因为这部分内容都是在编译期里可以被推出的。通过关键字 constexpr
的修饰,可以让编译器更好的优化、替换相关的常量,从而提高执行效率。当然,给一段不是常量返回值的函数加上关键字 constexpr
是无效的,编译器会在判定后忽略关键字。
另外存在 noexcept
运算符始终对常量表达式返回 true,所以它可以用于检查具体特定的 constexpr
函数返回是否采用常量表达式。
constexpr int f();
constexpr bool b1 = noexcept(f()); // false,constexpr 函数未定义
constexpr int f() return 0;
constexpr bool b2 = noexcept(f()); // true,f() 是常量表达式
2. 字面类型
指明一个类型为字面类型。字面类型是 constexpr
变量所拥有的类型,且能通过 constexpr
函数构造、操作及返回它们。简单来说就是一个为了配合 constexpr
的理论概念。
注意:标准中并没有定义具有这个名字的具名要求。这是核心语言所定义的一种类型类别。将它作为具名要求包含于此只是为了保持一致性。
· 列表初始化
在C++11之前,变量、数组、对象等都有不同的初始化方法。而在C++11中出现了一种新的初始化方式,其统一了初始化方式并且让初始化行为具有确定的效果,即列表初始化。
列表初始化的语法就是在要初始化的内容后加上一个大括号(括号前可以加等号),其中写上初始化的内容即可。
Test t(520); //普通构造
Test t = 520; //隐式转换
Test t = 520; //列表初始化
Test t 520; //列表初始化
//以下均为列表初始化
int i = 1314;
int i 1314;
int ii[] = 1, 2, 3;
int ii[] 1, 2, 3;
int* p = new int 5201314;
double b = double 13.14;
int* array = new int[3] 1, 2, 3;
注意:类中的私有成员或者静态成员无法进行列表初始化。这个官方一点的总结应该是只有聚合类型才可以无条件使用列表初始化。 如果一个非聚合类也想使用列表初始化,那它必须得拥有相对应的构造函数。cppreference解释链接
struct test
int x;
int y;
protected:
static int z;
t123, 321;//accept
//静态成员初始化
int test::z = 222;
· 委托与继承的构造函数
1. 委托构造函数
委托构造函数允许使用同一个类中的一个构造函数调用其他的构造函数,从而简化相关变量的初始化。我感觉这部分内容没什么好讲的,简单说就是可以通过 :
来调用其他的构造函数来简化操作。
class Test
public:
Test(int max)
max = max > 0 ? max : 100;
Test(int max, int min) : Test(max)
min = min > 0 && min < max ? min : 1;
Test(int max, int min, int mid) : Test(max, min)
mid = mid < max && mid > min ? mid : 50;
private:
int _max;
int _min;
int _middle;
;
2. 继承构造函数
继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大的简化派生类构造函数的编写。
比如下面这个例子,如果想要使用基类的构造函数,挨个重写 Child(int i) : Base(i)
明显很麻烦,但是直接使用 using Base::Base;
就可以直接继承基类的构造函数,方便很多。
class Base
public:
Base(int i) : m_i(i)
Base(int i, double j) : m_i(i), m_j(j)
Base(int i, double j, string k) : m_i(i), m_j(j), m_k(k)
int m_i;
double m_j;
string m_k;
;
class Child : public Base
public:
using Base::Base;
//甚至可以 using Base::func 这样来继承父类的成员函数func()
;
· 花括号或等号初始化器
如下,直接摘自cppreference。
· nullptr
在C语言中,空指针普遍使用 NULL
来表示,其实际定义为 (void *)0
。但在C++中,NULL
的实际定义为 0
,这是因为C++中不能将void *类型的指针隐式转换成其他指针类型。C++是一门强类型的语言,这样将0当成空指针很明显不符合语言的特性,
以上是关于C++11学习记录:核心语言功能特性的主要内容,如果未能解决你的问题,请参考以下文章