C++11学习记录:核心语言功能特性

  • 本篇笔记汇总了C++11中的主要新语言功能,根据个人理解与查阅的资料进行记录。
  • 主要参考地址:cppreference

· 模板优化

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关键字的保留情况相同:

  • 当变量不是指针或者引用类型时,推导的结果中不会保留 constvolatile 关键字。
  • 当变量是指针或者引用类型时,推导的结果中会保留 constvolatile 关键字。
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 不允许使用的场景主要有四个:

  1. 不能作为函数参数,因为函数调用时才会传实参,auto 使用要求必须要给修饰的变量赋值,二者矛盾。
  2. 不能用于类的非静态成员变量初始化。原因和上一条一样,因为类的非静态成员变量在没创建对象的时候也是未定义的。
  3. 不能使用 auto 关键字定义数组。int array[] = {...} 后,auto a = array 是被允许的,a 被推导为 int* 类型;而 auto b[] = array 是非法的,因为 auto 无法定义数组。
  4. 无法使用 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修饰。

  1. 如果 表达式 的值类别是亡值,将会 decltype 产生 T&&。
  2. 如果 表达式 的值类别是左值,或者被括号 () 包围,将会 decltype 产生 T&。
  3. 如果 表达式 的值类别是纯右值,将会 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 左值的构造参数,以及相关的 = 操作。这样就可以保证此智能指针的唯一性。我个人感觉还是挺有用的,这样直接删除就不用重载了。
4

· final 与 override

1. final

  其作用为指定某个虚函数不能在派生类中被覆盖,或者某个类不能被派生。也就是说,其可以作用于函数或者类,但是作用于函数时只能是虚函数。此关键字写于虚函数或类的后面。

  1. 作用于虚函数:使用 final 修饰虚函数,阻止子类重写父类的此函数。
...
class Child : public Base
{
public:
	void test() final
	{
		...
	}
};
...
  1. 作用于类:使用 final 修饰类,此类无法被继承。
...
class Child final : public Base
{
public:
	void test()
	{
		...
	}
};
...

2. override

  其作用为指定一个虚函数覆盖另一个虚函数。在成员函数的声明或定义中,override 说明符确保该函数为虚函数并覆盖某个基类中的虚函数。如果不是这样,那么程序会生成编译错误。

override 是在成员函数声明符之后使用时拥有特殊含义的标识符,其他情况下它不是保留的关键词。

...
class Child : public Base
{
public:
	void test() override
	{
		...
	}
};
...

· 尾随返回类型

  尾随返回类型语法为auto 函数名(传参) -> decltype(表达式) { 函数体 },返回值类型为 decltype 推导出的类型。

  这个东西我感觉主要是为模板服务,使用场景主要是:

  1. 返回值随模板类型变化
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
  1. 返回值类型比较复杂
auto fpif(int) -> int(*)(int)

· 右值引用

  C++11中增加了一个新的很好用的类型,右值引用 &&,就是对右值的引用。首先看一下左右值的区别,其实主要就是看能不能取地址:

  • 左值为 locator value,即 lvalue;右值为 read value,即 rvalue
  • 左值:储存在内存中、有明确存储地址的数据(可取地址)
  • 右值:可以提供数据值的数据(不可取地址)

  那么右值引用有什么作用?主要的作用就是延长右值的生命周期,以提高效率。 那么是如何提高效率的呢?比如说如下这个场景:

vector<vector<int>>vt;
vector<int>temp{1, 2, 3, 4, 5};
vt.push_back(temp);

  此段代码中,先声明了二维数组 vt,随后声明一个一维数组 temp 来塞入容器 vt。自此一维数组 temp 使命完成,其储存的右值也没有作用了。在 push_back() 操作中,其首先会将 temp 以左值引用传进去,随后再使用 construct 来创建一个 vector<int> 拷贝储存传进来的值,最后再放入容器(如下图)。这就导致在函数中,此组数据被完整拷贝了一次,降低了效率。
2
  那么,既然 temp 只在此处有用,可否直接把 temp 放入 vt 中来减少那次拷贝呢?右值引用就是为了这个场景而出现的。例如上面这个问题的本质为:temp 的右值生命周期到此为止,想要将其生命周期延长给另一个变量 vt,来避免对 temp 右值的复制。这时就可以传入右值来提高 vector::push_back() 的效率。 std::move源码分析

vector<vector<int>>vt;
vector<int>temp{1, 2, 3, 4, 5};
vt.push_back(move(temp));//这里的std::move的作用是将左值转为右值

  C++11中,STL中已经重载了很多函数的传右值引用版本,比如下图中 vector::push_back() 的右值引用版本,其只调用了 emplace_back 函数来延长生命周期,从而避免使用 construct 重新拷贝创建。
3
  所以呢,在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;
}t{123, 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
5

· nullptr

  在C语言中,空指针普遍使用 NULL 来表示,其实际定义为 (void *)0。但在C++中,NULL 的实际定义为 0,这是因为C++中不能将void *类型的指针隐式转换成其他指针类型。C++是一门强类型的语言,这样将0当成空指针很明显不符合语言的特性,因为 NULL 往往会被推导成 long int 类型而不是指针类型,于是在C++11中推出了 nullptr 来定义各个类型的空指针。

//C++与C中NULL的定义
#undef NULL
#if defined(__cplusplus)
#define NULL 0
#else
#define NULL ((void *)0)
#endif

  nullptr 实际是 std::nullptr_t 类型的纯右值,它可以转换成任意指针类型。这样就可以解决C++中不能将(void *)类型的指针隐式转换成其他指针类型的问题,从而避免 NULL 歧义出现。

#include <cstddef>
#include <iostream>
 
void f(int*)
{
   std::cout << "Pointer to integer overload\n";
}
 
void f(double*)
{
   std::cout << "Pointer to double overload\n";
}
 
void f(std::nullptr_t)
{
   std::cout << "null pointer overload\n";
}
 
int main()
{
    int* pi {}; double* pd {};
 
    f(pi);
    f(pd);
    f(nullptr); // 无 void f(nullptr_t) 可能有歧义
    // f(0);    // 歧义调用:三个函数全部为候选
    // f(NULL); // 若 NULL 是整数空指针常量则为歧义
                // (如在大部分实现中的情况)
}

结果:
Pointer to integer overload
Pointer to double overload
null pointer overload

· long long

  就是C++11里新增的一组基础整数类型,简单来说其核心就是保证至少 64 位的宽度(8个字节),我觉得没什么好说的。
图ll
图ll2

· char16_t 与 char32_t

  这两个是C++11里新增的字符类型,主要是为了服务UTF编码的。其与普通 char 的差别就是位宽不一样。具体定义如下:

  • char16_t: UTF-16 字符表示的类型,要求大到足以表示任何 UTF-16 编码单元( 16 位)。它与 std::uint_least16_t 具有相同的大小、符号性和对齐,但它是独立的类型。使用u来表示utf-16字符: char16_t c{ u'a' };
  • char32_t: UTF-32 字符表示的类型,要求大到足以表示任何 UTF-32 编码单元( 32 位)。它与 std::uint_least32_t 具有相同的大小、符号性和对齐,但它是独立的类型。使用U来表示utf-32字符:char32_t c{ U'a' };

· 类型别名

  C++11中,给关键字 using 添加了一个新的功能:定义类型的别名。就我的使用经验来看,其和 typedef 是一样的效果,没有区别。虽然 using 看起来更简洁一点,但是我感觉已经用 typedef 用习惯了…

  • 使用场景一,和基础类型和函数指针搭配
//语法
using 新类型 = 旧类型;

//下两者相同
using func = void (*) (int, int); 
typedef void (*func)(int, int);

//使用
void test(int, int) {}
func f = test;
  • 使用场景二,和模板搭配,在STL源码里经常见
template <typename T>
struct MyMap
{
	typedef map<int, T> mapType;
};
MyMap<int>::mapType m;

template <typename T>
using MyMap = map<int, T>;
MyMap<int> m;

· 变参数模板

形参包:模板形参包是接受零或更多模板实参(非类型、类型或模板)的模板形参。函数形参包是接受零或更多函数实参的函数形参,至少有一个形参包的模板被称作变参模板。

  在C++11中,新出现了变参数模板,即可将任意数量的模板实参实例化。语法为:template<class|typename ... Types>。其弥补了C++模板不能灵活定义参数数量的不足。

1. --------------------------------------------------------------------
void test()//基础函数
{
    cout << endl;
}

template<typename T, typename ... Args>
void test(const T& t, const Args&... a)//递归变参函数
{
    cout << t << endl;
    test(a...);//递归调用
}

int main()
{
    test(string("hello"), "world1", 1, 3);
    return 0;
}

2. --------------------------------------------------------------------
void tprintf(const char* format)//基础函数
{
    std::cout << format;
}
 
template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs)//递归变参函数
{
    for ( ; *format != '\0'; format++ ) {
        if ( *format == '%' ) {
           std::cout << value;
           tprintf(format+1, Fargs...);//递归调用
           return;
        }
        std::cout << *format;
    }
}
 
int main()
{
    tprintf("% world% %\n","Hello",'!',123);
    return 0;
}

  如上两个例子所示,变参数模板一般都是拥有一个基础函数,以及一个变参模板函数。在变参模板函数里处理定量的传参后递归调用,直至参数处理完毕。总体而言我感觉它的思路和一般的递归函数是相似的,其基础函数即为边界。

· 推广的(非平凡)联合体

  

· 推广的 POD (平凡类型与标准布局类型)

  

· Unicode 字符串字面量

  在C++11之前,C++中只有通常字符串字面量:"内容" 以及宽字符串字面量:L"内容"。其中前者是最常用的一种,每个字符占一个字节;而后者则是代表每个字符占用两个字节。

  上文中提到,C++11中提供了两个新字符类型 char16_tchar32_t,于是C++也更新了相对应的字符串字面量 u"内容"U"内容"。此外还提供了另外新的两种字符串字面量 u8"内容"R"xxx(内容)xxx",这四种新的字面量的具体解释如下:

  1. u8"内容"UTF-8 字符串字面量。 字符串字面量的类型是 const char[N](C++20 前) const char8_t[N](C++20 起),其中 N 是以 UTF-8 编码单元计的字符串的大小,包含空终止符。
  2. u"内容"UTF-16 字符串字面量。 字符串字面量的类型是 const char16_t[N],其中 N 是以 UTF-16 编码单元计的字符串的大小,包含空终止符。
  3. U"内容"UTF-32 字符串字面量。 字符串字面量的类型是 const char32_t[N],其中 N 是以 UTF-32 编码单元计的字符串的大小,包含空终止符。
  4. R"xxx(内容)xxx"原始字符串字面量。 用于避免转义任何字符。这个我感觉还是挺有用的,在这里记录一下用法:
	在原始字符串字面量的定义内容中,将不存在任何转义。
	比如常见的路径字符串 "C:\\demo\\test.txt",其中使用"\\"的原因是避免"\"进行转义。
	而当使用原始字符串字面量定义时,就可以直接 R"(C:\demo\test.txt)",因为字符串中不会进行任何转义,所以就不需要使用"\\"了。
eg:
	样例输入:
	cout << R"(hello world \n)";
	样例输出:
	hello world \n

	另外有些时候,字符串太长或者说需要分段,常规来讲是通过"\n"和"\"来实现的,比如说下面这个例子,通过连接符和回车来实现分段。
eg:	
	样例输入:
	cout << "1\n\
    2\n\
    3" << endl;
	样例输出:
	1
    2
    3
    
    而当使用原始字符串字面量定义时,直接按位置输入即可,字符串中会根据位置自动换行。
eg:
    样例输入:
    cout << R"(1
    2
    3)" << endl;
    样例输出:
    1
    2
    3
   
	另外,原始字符串字面量的那个"xxx"部分我个人认为与注释相似,没有实际影响,而且得注意前后必须一致。
eg:
	样例输入:
	cout << R"hello(hello world \n)hello" << endl;
	样例输出:
	hello world \n

· 用户定义字面量

  C++11新标准中引入了用户自定义字面量,也叫自定义后缀操作符,即通过实现一个后缀操作符,将申明了该后缀标识的字面量转化为需要的类型。比如如下代码:

long double operator"" _mm(long double x) { return x / 1000; }
long double operator"" _m(long double x)  { return x; }
long double operator"" _km(long double x) { return x * 1000; }

int main()
{
    cout << 1.0_mm << endl; //0.001
    cout << 1.0_m  << endl; //1
    cout << 1.0_km << endl; //1000

    return 0;
}

输出结果:
0.001
1
1000

  我个人而言这块内容是没有使用过,然后去网上找了一下相关的资料,感觉这东西是为了用户自定义类型的字面量解析输出…比如说下面这个例子:

//一个自定义的rgb类型
struct RGBA
{
	uint8_t r, g, b, a;
	RGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a):r(r),g(g),b(b),a(a){}
};
//自定义字面量后缀
RGBA operator"" _RGBA(const char* str, size_t size)
{
	const char* r = nullptr, *g = nullptr, *b = nullptr, *a = nullptr;
	for (const char* p = str; p != str + size; ++p)
	{
		if (*p == 'r') r = p + 1;
		if (*p == 'g') g = p + 1;
		if (*p == 'b') b = p + 1;
		if (*p == 'a') a = p + 1;
	}
	if (r == nullptr || g == nullptr || b == nullptr) throw;
	if (a == nullptr)
	{
		return RGBA(atoi(r),atoi(g),atoi(b),0);
	}
	else
	{
		return RGBA(atoi(r), atoi(g), atoi(b),atoi(a));
	}
}
//输出运算符重载
ostream& operator<<(ostream& os,const RGBA& color)
{
	return os<<"r="<< (int)color.r<<" g="<< (int)color.g<<" b="<< (int)color.b<<" a="<< (int)color.a<<endl;
}
//main
int main()
{
	//自定义字面量来表示RGBA对象
	cout << "r255 g255 b255 a40"_RGBA << endl;
	return 0;
}

输出结果:
r=255 g=255 b=255 a=40

  值得注意的是,用户定义字面量中,只有下面的7种参数列表才是合法的,而且后面四种会自动计算出字符串的长度,挺好用的。具体可以看下面:

char const *
unsigned long long
long double
char const *, size_t
wchar_t const *, size_t
char16_t const *, size_t
char32_t const *, size_t

//例子
size_t operator"" _len(char const * str, size_t size)
{
    return size;
}

int main()
{
    cout << "hello"_len <<endl; //结果为5
    return 0;
}

· 属性

  C++11中新增了一个概念:属性(attributes),其功能为为类型、对象、代码等引入由实现定义的属性。在C++11中,其语法仅为[[ 属性列表 ]],标准属性也只有下图的前两个:
7

[[gnu::always_inline]] [[gnu::hot]] [[gnu::const]] [[nodiscard]]
inline int f(); // 声明 f 带四个属性
 
[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]]
int f(); // 同上,但使用含有四个属性的单个属性说明符

· lambda 表达式

  即匿名函数表达式,其主要语法为:[捕获列表] (参数列表) 函数选项 -> 返回值类型 { 函数体 };,就是中、小、大三种括号来一遍。

  • 捕获列表可以指定需要"捕获"哪些变量,以及按什么方式"捕获"。简单来说,就是lambda表达式的函数体是独立的区域,如果想使用外部的变量,就必须先给它"捕获"进来。具体方式如下:
[]			不捕获任何变量
[&]			捕获外部作用域中的所有变量,并作为引用在函数体内使用(按引用捕获)
[=]			捕获外部作用域中的所有变量,并作为副本在函数体内使用(按值捕获)
[=, &foo]	按值捕获外部作用域中所有变量,并按照引用捕获外部变量foo
[bar]		按值捕获bar变量,同时不捕获其他变量
[&bar]		按引用捕获bar变量,同时不捕获其他变量
[this]		捕获当前类中的this指针
	- 让lambda表达式拥有和当前类成员函数同样的访问权限
	- 如果以及使用了&=,则默认添加此选项

struct S2 { void f(int i); };
void S2::f(int i)
{
    [&]{};          // OK:默认以引用捕获
    [&, i]{};       // OK:以引用捕获,但 i 以值捕获
    [&, &i] {};     // 错误:以引用捕获为默认时后续不能以引用捕获
    [&, this] {};   // OK:等价于 [&]
    [&, this, i]{}; // OK:等价于 [&, i]
}

struct S2 { void f(int i); };
void S2::f(int i)
{
    [=]{};          // OK:默认以复制捕获
    [=, &i]{};      // OK:以复制捕获,但 i 以引用捕获
    [=, *this]{};   // C++17 前:错误:无效语法
                    // C++17 起:OK:以复制捕获外围的 S2
    [=, this] {};   // C++20 前:错误:= 为默认时的 this
                    // C++20 起:OK:同 [=]
}

struct S2 { void f(int i); };
void S2::f(int i)
{
    [i, i] {};        // 错误:i 重复
    [this, *this] {}; // 错误:"this" 重复 (C++17)
}
  • 参数列表和普通函数的相似,就是小括号里写上接收什么类型的参数之类的。当没有传参的时候,可以直接写空括号 (),或者干脆省略括号 auto f = []{return 1;};,但是不推荐不写因为我感觉会影响可读性。

  • 函数选项主要有两个,mutableexception:前者 mutable 含义为可以修改按值传递进来的拷贝(修改拷贝,不是值本身);后者 exception 含义为指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw();。这部分内容不需要可以直接省略不写,说实话我也没用过…

  • 返回值类型是典型的"尾随返回类型",但是一般是不写它的,因为会自动推导返回值类型。不过有一些情况下编译器无法推导返回类型,比如说返回一个初始化列表 return {1, 2};,此时就必须指定返回值类型了。

  lambda的本质其实是一个仿函数,当以值捕获变量时,其默认是 const 的,所以无法更改,而选项 mutable 的功能就是去掉 const 修饰。而当一个lambda表达式未捕获任何变量时,其还可以转换成一个普通的函数指针。

· noexcept 说明符与 noexcept 运算符

1. noexcept 说明符

  指定函数是否抛出异常。在函数后添加 noexcept 即可,也可以指定 false,默认即为 noexcept(false)

void f() noexcept;
void f(); // 错误:不同的异常说明
void g() noexcept(false);
void g(); // OK: g 的两个声明均为潜在抛出

2. noexcept 运算符

  noexcept 运算符进行编译时检查,若表达式声明为不抛出任何异常则返回 true。它可用于函数模板的 noexcept 说明符中,以声明函数将对某些类型抛出异常,但不对其他类型抛出。其语法为 noexcept(表达式)

  noexcept 运算符不对表达式求值。若表达式的潜在异常集合为空,则结果为 true,否则结果为 false。

#include <iostream>
#include <utility>
#include <vector>
 
void may_throw();
void no_throw() noexcept;
auto lmay_throw = []{};
auto lno_throw = []() noexcept {};
class T{
public:
  ~T(){} // 析构函数妨碍了移动构造函数
         // 复制构造函数为 noexcept
};
class U{
public:
  ~U(){} // 析构函数妨碍了移动构造函数
         // 复制构造函数为 noexcept(false)
  std::vector<int> v;
};
class V{
public:
  std::vector<int> v;
};
 
int main()
{
  T t;
  U u;
  V v;
 
  std::cout << std::boolalpha
           << "Is may_throw() noexcept? " << noexcept(may_throw()) << '\n'
           << "Is no_throw() noexcept? " << noexcept(no_throw()) << '\n'
           << "Is lmay_throw() noexcept? " << noexcept(lmay_throw()) << '\n'
           << "Is lno_throw() noexcept? " << noexcept(lno_throw()) << '\n'
           << "Is ~T() noexcept? " << noexcept(std::declval<T>().~T()) << '\n'
           // 注:以下各项测试也要求 ~T() 为 noexcept
           // 因为 noexccept 中的表达式构造并销毁了临时量
           << "Is T(rvalue T) noexcept? " << noexcept(T(std::declval<T>())) << '\n'
           << "Is T(lvalue T) noexcept? " << noexcept(T(t)) << '\n'
           << "Is U(rvalue U) noexcept? " << noexcept(U(std::declval<U>())) << '\n'
           << "Is U(lvalue U) noexcept? " << noexcept(U(u)) << '\n'  
           << "Is V(rvalue V) noexcept? " << noexcept(V(std::declval<V>())) << '\n'
           << "Is V(lvalue V) noexcept? " << noexcept(V(v)) << '\n';  
}

输出:
Is may_throw() noexcept? false
Is no_throw() noexcept? true
Is lmay_throw() noexcept? false
Is lno_throw() noexcept? true
Is ~T() noexcept? true
Is T(rvalue T) noexcept? true
Is T(lvalue T) noexcept? true
Is U(rvalue U) noexcept? false
Is U(lvalue U) noexcept? false
Is V(rvalue V) noexcept? true
Is V(lvalue V) noexcept? false

· alignof 与 alignas

1. alignof

  查询类型的对齐要求。其语法为 alignof(类型标识),返回 std::size_t 类型的值。

#include <iostream>
struct Foo {
    int   i;
    float f;
    char  c;
};
// 注:下面的 `alignas(alignof(long double))` 如果需要可以简化为 
// `alignas(long double)`
struct alignas(alignof(long double)) Foo2 {
    // Foo2 成员的定义...
};
struct Empty {};
struct alignas(64) Empty64 {};
int main()
{
    std::cout << "对齐字节数"  "\n"
        "- char             :" << alignof(char)    << "\n"
        "- 指针             :" << alignof(int*)    << "\n"
        "- Foo 类           :" << alignof(Foo)     << "\n"
        "- Foo2 类          :" << alignof(Foo2)     << "\n"
        "- 空类             :" << alignof(Empty)   << "\n"
        "- alignas(64) Empty:" << alignof(Empty64) << "\n";
}

可能的输出:
对齐字节数
- char1
- 指针             :8
- Foo 类           :4
- Foo2 类          :16
- 空类             :1
- alignas(64) Empty:64

2. alignas

  指定类型或对象的对齐要求。语法为 (1)alignas(表达式)、(2)alignas(类型标识)、(3)alignas(包 ...)

  1. alignas(表达式) 必须是求值为零或合法的对齐或扩展对齐的整型常量表达式。
  2. 等价于 alignas(alignof(类型))
  3. 等价于对同一说明应用多个 alignas 说明符,逐个对应于形参包的各个成员,形参包可以是类型或非类型形参包。
// 每个 struct_float 类型对象都将被对齐到 alignof(float) 边界
// (通常为 4):
struct alignas(alignof(float)) struct_float
{
    // 定义在此
};
// sse_t 类型的每个对象将对齐到 32 字节边界
struct alignas(32) sse_t
{
  float sse_data[4];
};
// 数组 "cacheline" 将对齐到 64 字节边界
alignas(64) char cacheline[64];

#include <iostream>
int main()
{
    struct default_aligned { float data[4]; } a, b, c;
    sse_t x, y, z;
 
    std::cout
        << "alignof(struct_float) = " << alignof(struct_float) << '\n'
        << "sizeof(sse_t) = " << sizeof(sse_t) << '\n'
        << "alignof(sse_t) = " << alignof(sse_t) << '\n'
        << "alignof(cacheline) = " << alignof(alignas(64) char[64]) << '\n'
        << std::hex << std::showbase
        << "&a: " << &a << '\n'
        << "&b: " << &b << '\n'
        << "&c: " << &c << '\n'
        << "&x: " << &x << '\n'
        << "&y: " << &y << '\n'
        << "&z: " << &z << '\n';
}

可能的输出:
alignof(struct_float) = 4
sizeof(sse_t) = 32
alignof(sse_t) = 32
alignof(cacheline) = 64
&a: 0x7ffc835270d0
&b: 0x7ffc835270e0
&c: 0x7ffc835270f0
&x: 0x7ffc83527100
&y: 0x7ffc83527120
&z: 0x7ffc83527140

· 多线程内存模型

  应该指的是对原子操作那个库的相关支持吧,深入一点说实话我也不太了解。可以参考一下这位大佬的文:C++11多线程-内存模型

· 线程局部存储

  线程局部存储在其它语言中都是以库的形式提供的(库函数或类)。但在C++11中以关键字的形式,做为一种存储类型出现,由此可见C++11对线程局部存储的重视。C++11中有如下几种存储类型:

序号类型备注
1auto该关键字用于两种情况:
1. 声明变量时,根据初始化表达式自动推断变量类型。
2. 声明函数作为函数返回值的占位符。
2staticstatic变量只初始化一次,除此之外它还有可见性的属性:
1. static修饰函数内的“局部”变量时,表明它不需要在进入或离开函数时创建或销毁。且仅在函数内可见。
2. static修饰全局变量时,表明该变量仅在当前(声明它的)文件内可见。
3. static修饰类的成员变量时,则该变量被该类的所有实例共享。
3register寄存器变量。该变量存储在CPU寄存器中,而不是RAM(栈或堆)中。
该变量的最大尺寸等于寄存器的大小。由于是存储于寄存器中,因此不能对该变量进行取地址操作。
4extern引用一个全局变量。当在一个文件中定义了一个全局变量时,就可以在其它文件中使用extern来声明并引用该变量。
5mutable仅适用于类成员变量。以mutable修饰的成员变量可以在const成员函数中修改。
6thread_local线程周期

thread_local 修饰的变量具有如下特性:

  • 变量在线程创建时生成(不同编译器实现略有差异,但在线程内变量第一次使用前必然已构造完毕)。
  • 线程结束时被销毁(析构,利用析构特性,thread_local 变量可以感知线程销毁事件)。
  • 每个线程都拥有其自己的变量副本。
  • thread_local 可以和 staticextern 联合使用,这将会影响变量的链接属性。
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
 
thread_local unsigned int rage = 1; 
std::mutex cout_mutex;
 
void increase_rage(const std::string& thread_name)
{
    ++rage; // 在锁外修改 OK;这是线程局部变量
    std::lock_guard<std::mutex> lock(cout_mutex);
    std::cout << thread_name << " 的愤怒计数:" << rage << '\n';
}
 
int main()
{
    std::thread a(increase_rage, "a"), b(increase_rage, "b");
 
    {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "main 的愤怒计数:" << rage << '\n';
    }
 
    a.join();
    b.join();
}

输出:
a 的愤怒计数:2
main 的愤怒计数:1
b 的愤怒计数:2

· GC 接口

  C++11中新增了对 GC(垃圾回收) 的支持,但是好像在C++23的目标里要进行删除…这部分感觉用的也挺少的,我也没怎么研究,就简单贴个图过了吧。
6

· 范围 for (基于 Boost 库)

  在C++11中新增了一种范围for,这东西用的蛮多的,使用场景基本都是为了遍历各种容器,搭配 auto 使用非常方便。

  注意:此类范围for循环在遍历过程中只会访问一次容器。 在第一次也是唯一一次访问中,其会确认边界,随后根据边界进行遍历。因此其不会每次遍历都判定条件,这也可能造成在遍历中增减元素会出现问题。

vector<int>test{1, 2, 3, 4, 5, 6};
//此处i为拷贝,无法修改原值
for(auto i : test)
{
	cout << i << endl;
}
//此处i为引用,可以修改原值
for(auto& i : test)
{
	cout << i++ << endl;
}

· static_assert (基于 Boost 库)

  其功能为编译时进行断言检查,即静态断言。语法为 static_assert(布尔常量表达式, 字符串字面量),当布尔常量表达式为 false 时,则会出现后面指定的字符串字面量,同时编译失败。

  静态断言的好处就是可以在编译期就更早的发现错误,以及减少运行时开销。我看了一小部分C++标准库源码,发现静态断言出现的频率还挺高的,感觉标准委员会是挺推崇这东西的。
1

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值