本文主要总结深入理解c++11中的常用新特性
新标准的诞生
保证平稳性和兼容性
通用为本,专用为末
右值引用
在没有右值引用时,对带有指针成员变量的对象进行拷贝,会让指针的内存变得很难管理,甚至会产生错误。这是如果有移动语义,对象不执行拷贝,只执行移动就不会就这种错误。
在介绍移动语义之前,首先得明白左值,右值,和右值引用。
一般而言,可以取地址,有名字的就是左值。反之,不能取地址,没有名字的就是右值。我们称c++98中的引用为“左值引用”,而c ++11中的引用为右值引用。左值引用是一个具名对象的别名,而右值引用是一个不具名对象的别名。
右值引用延长了将亡对象的生命期。对应的,常量左值引用(const T &)也能延长一个将亡对象的生命期,但是付出的代价就是余生只能是只读的。
非常量左值引用只能绑定非常量左值
非常量右值引用只能绑定非常量右值
常量左值引用可以绑定任何值
常量右值引用没有用
右值引用一般的编写方式如下:
class Moveable{
public:
Moveable(const Moveable& m);
Moveable& operate=(const Moveable&);
Moveable(Moveable&& m);
Moveable& operate=(const Moveable&&);
std::move能够将一个对象的属性从左值变成右值,但是需要之一的是并不会改变对象的生命期。
在编写模板的时候,我们经常需要实现完美转发。例如:
template <typename T>
void IamForwording(T&& t)
{
IrunCodeActually(forward(t));
}
使用forward可以实现完美转发,实际上,forward的功能和move一样,只不过在模板里面一般使用forward。它们都等于static_cast<T&&>,具体为什么可以去看“右值引用的模板推导”。
列表初始化
我觉得一般知道可以如下方式去初始化容器就够了。
vector<int> a = {1, 2, 3, 4};
map<int, float> b = {{1, 2.3f}, {2, 3.2f}};
POD类型
在c++11中,POD的概念被分为平凡和标准布局两个概念。平凡强调了类型没有复杂的构造函数,析构函数,多态等看起来不平凡的行为。而标准布局以为着可以通过memcpy进行拷贝。可以通过is_pod
下面是平凡的详细定义:
- 拥有平凡的默认构造函数和析构函数。(就是不定义构造函数和析构函数)
- 拥有平凡的拷贝构造函数和移动构造函数。(不定义)
- 拥有平凡的拷贝复制运算符和移动赋值运算符。(不定义)
- 不能包含虚函数以及虚基类。
标准布局的详细定义
- 所有非静态成员都相同的访问权限。
- 非静态成员不能同时出现在派生类和基类间。
- 第一个非静态成员类型和基类不同。
- 没有虚函数和虚基类。
新手易学,老手易用
- 常用auto和decltype。
auto不能推导的四种情况。
void fun(auto x = 1){} //auto函数参数
struct str{
auto var = 10; //auto非静态成员变量
}
auto z[3] = x; //auto数组
vector<auto> v = {1}; //auto模板参数
- 基于范围的for循环。
vector<int> a = {1, 2, 3, 4};
for(int i : a)
cout << i << endl;
提高类型安全
强类型枚举
enum class Type { General, Light, Medium, Heaby };
它有如下优势:
- 强作用域,强类型枚举的名称不会被输出到其父作用域。
- 转换限制,值不可以和整形发生隐私转换。
- 可以指定底层类型,比如:
enum class Type : char { General, Light, Medium, Heaby };
堆内存管理:智能指针与垃圾回收
智能指针
必用
垃圾回收
c++只实现了最简单的垃圾回收。了解即可。
垃圾回收一般可以分为两大类。
- 基于引用计数
缺点是比较难处理“环形引用”。 - 基于跟踪
- 标记-清除(标记:根据正在使用的对象查找引用的堆空间,并在堆空间上做标记。标记结束后,没有被标记的对象就是垃圾。清除:清除垃圾对象)
- 标记-整理(整理:将活对象向左靠齐,还整理了碎片)
- 标记-拷贝(拷贝:将堆分为FROM, TO,当某一块满了,将其中的活对象拷贝到另一块内存区域)
提高性能及操作硬件的能力
常量表达式
常量表达式的作用,就是在于有时候我们知道某个变量是编译器常量,但是因为不是const或者在函数内,所以编译器就是不给过。下面有个例子:
int main()
{
int n = 3;
int arr[n];
return 0;
}
在我们的教材中,要求定义arr必须使用编译器常量作为数组下标。所以,上面的写法应该是错的。但是!!!居然通过了编译,通过查阅资料,说这是c++的新特性。好吧,换个例子。
int main()
{
int n = 3;
enum { a = 3 }; //错误
switch(cond){
case getConst()://错误
break;
}
return 0;
}
看吧,上面报错了。
常量表达式函数
- 函数体只有单一的return返回语句。
- 函数必须有返回值(不能是void函数)。
- 在使用前必须有定义。
- return语句不能使用非常量表达式函数,全局数据,且必须是一个常量表达式。
常量表达式值
const int i = 1;
constexpr int j = 1;
它们几乎没有区别,区别我就不说了。
用常量表达式进行元编程
因为常量表达式是在编译器展开,所以递归调用就会产生模板元编程类似的效果。
constexpr int Fibonacci( int n ){
return (n == 1) ? 1 : ((n == 2) ? 1 : Fibonacci(n - 1) + Fibonacci(n -2));
}
变长模板
- 变长类模板常用方式
//变长模板的声明
template <typename... Elements> class tuple;
//递归的偏特化定义
template <typename Head, typename... Tail>
class tuple<Head, Tail...> : private tuple<Tail...>{
Head head;
}
//边界条件
template<> class tuple<> {};
- 变长函数模板常用方式
#include <iostream>
using namespace std;
//结束条件
void test()
{
return;
}
template<typename T, typename... Args>
void test(T t, Args... args)
{
cout << t << endl;
test(args...);
}
int main()
{
test(1,2,3,4,5,6,7,8);
}
原子类型
- 理解c++原子操作内存模型。
为了充分利用CPU的多级流水线模型,编译器和处理器都可能将我们所写的代码顺序打乱。这可能对我们编写的进行原子操作的代码的正确性造成影响。因此,在使用原子类型的时候,必须对内存模型有所了解。
一般而言,常用的内存模型有以下几种:
- memory_order_seq_cst : 全部按照顺序执行(原子操作默认的内存模型)。
- memory_order_acquire : 所有后续读操作都在本条操作后面完成。
- memory_order_release : 所有之前的写操作必须在本条之前完成。
通常而言,我们可以通过memory_order_acquire,memory_order_release写出无锁的高性能程序。
atomic<int> a;
atomic<int> b;
int Thread1(int){
int t = 1;
a.store(t, memory_order_relaxed);
b.store(2, memory_order_release); //本操作之前的所有写原子操作必须完成。
}
int Thread2(int){
//本操作之后的读原子操作必须等该条语句执行。
while(b.load(memory_order_acquire) != 2);
cout << a.load(memory_order_relaxed) << endl;
}
线程局部存储
int thread_local errCode;
很简单,不解释。
quick_exit和at_quick_exit
因为exit()需要在函数结束的时候调用全局对象的析构函数,所以如果析构函数里面有加锁,或则delete之类的操作,在多线程的情况下可能对产生错误。
quick_exit与exit的区别就是不执行全局对象的析构函数。
at_quick_exit的作用是注册退出时执行的函数的。
为改变思考方式而改变
指针空值——nullptr
一般情况下,NULL是个宏定义
#define NULL 0
可能会产生二义性。
注意:nullptr不能像bool隐式转换。
if(nullptr)和if(nullpty == 0)之类的用法都是不允许的。
默认函数的控制
=default;使用默认函数版本。
=delete;删除编译器生成的默认函数。
lambda函数
一般格式如下:
auto fun = [=](int a)->int{return a};
- [] : 里面是捕捉列表,能够捕捉父作用域中的变量。(使用值传递,需要注意捕捉列表里面的值不会随外界改变)
- () : 参数,可以省略。
- ->return-type :返回值,能够推导出的情况可以省略。
- {} : 函数体。
lambda函数是仿函数的语法糖,所有lambda函数都能转换为一个仿函数。
融入实际应用
对齐支持
c++11可以获取和指定数据的对齐方式。
alignas(8) char c;//指定为按8为对齐
int n = alignof(double)//获取按多少位数据对齐
//可以将ptr指向的大小为space的内存的对齐方式进行调整,将ptr开始的size大小的数据调整为按alignment对齐。
align(std::size_t alignment, std::size_t size, void *&ptr, std::size_t& space);