c# 网站开发实例教程,河南郑州新闻头条最新事件,在哪里可以查公司注册信息,下载wordpress建站程序文章目录 1. 自动类型推导1.1 auto1.1.1 推导规则1.1.2 auto的限制1.1.3 auto的应用1.1.4 范围for 1.2 decltype1.2.1 推导规则1.2.2 decltype的应用 1.3 返回类型后置 2.可调用对象包装器、绑定器2.1 可调用对象包装器2.1.1 基本用法2.1.2 作为回调函数使用 2.2 绑定器 3. usi… 文章目录 1. 自动类型推导1.1 auto1.1.1 推导规则1.1.2 auto的限制1.1.3 auto的应用1.1.4 范围for 1.2 decltype1.2.1 推导规则1.2.2 decltype的应用 1.3 返回类型后置 2.可调用对象包装器、绑定器2.1 可调用对象包装器2.1.1 基本用法2.1.2 作为回调函数使用 2.2 绑定器 3. using3.1 定义别名3.2 模板的别名 4. 智能指针的使用4.1 shared_ptr4.1.1 通过构造函数初始化4.1.2 通过拷贝和移动构造函数初始化4.1.3 通过std::make_shared初始化4.1.4 通过 reset方法初始化4.1.5 获取原始指针 4.2 weak_ptr4.2.1 基本使用方式4.2.1.1 初始化4.2.1.2 其他常用方法4.2.1.2.1 use_count()4.2.1.2.2 expired()4.2.1.2.3 lock()4.2.1.2.4 reset() 4.2.2 返回管理this的shared_ptr4.2.3. 解决循环引用问题 5. constexpr5.1 constexpr 介绍5.2 常量表达式函数5.2.1 修饰函数5.2.2 修饰模板函数5.2.3 修饰构造函数 6. 委托构造和继承构造函数6.1 委托构造函数6.2 继承构造函数 7. 原始字面量8. chrono库8.1 时间间隔8.1.1 常用类成员8.1.2 类的使用 8.2 时间点 time point8.3 时钟clocks8.3.1 system_clock8.3.2 steady_clock8.3.3 high_resolution_clock 8.4 转换函数8.4.1 duration_cast8.4.2 time_point_cast 9. 静态断言10. POD类型10.1 POD类型10.2 “平凡”类型10.3 “标准布局”类型10.4 对 POD 类型的判断10.4.1 对“平凡”类型判断10.4.2 对“标准布局”类型的判断 10.5 总结 11. 非受限联合体11.1 什么是非受限联合体11.2 非受限联合体的使用11.2.1 静态类型的成员11.2.2 非POD类型成员 12. 强枚举类型12.1 枚举12.1.1 枚举的使用12.1.2 枚举的缺陷 12.2 强类型枚举12.2.1 优势12.2.2 对原有枚举的扩展 *13. 单列模式13.1 饿汉模式13.2 懒汉模式 1. 自动类型推导
1.1 auto C11之前auto和static是对应的,表示变量是自动存储的但是非static的局部变量默认都是自动存储的因此这个关键字变得非常鸡肋 在C11中他们赋予了新的含义使用这个关键字能够像别的语言一样自动推导出变量的实际类型。 1.1.1 推导规则 使用auto声明的变量必须要进行初始化以让编译器推导出它的实际类型在编译时将auto占位符替换为真正的类型。 当变量不是指针或者引用类型时推导的结果中不会保留const、volatile关键字 当变量是指针或者引用类型时推导的结果中会保留const、volatile关键字
1.1.2 auto的限制
不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参auto要求必须要给修饰的变量赋值因此二者矛盾。
int func(auto a, auto b) // error
{ cout a: a , b: b endl;
}不能使用auto关键字定义数组
int func()
{int array[] {1,2,3,4,5}; // 定义数组auto t1 array; // ok, t1被推导为 int* 类型auto t2[] array; // error, auto无法定义数组auto t3[] {1,2,3,4,5};; // error, auto无法定义数组
}无法使用auto推导出模板参数
template typename T
struct Test{}int func()
{Testdouble t;Testauto t1 t; // error, 无法推导出模板类型return 0;
}1.1.3 auto的应用
下面列举几个比较常用的场景
用于STL的容器遍历。
在C11之前定义了一个stl容器之后遍历的时候常常会写出这样的代码
#include map
int main()
{mapint, string person;mapint, string::iterator it person.begin();for (; it ! person.end(); it){// do something}return 0;
}可以看到在定义迭代器变量 it 的时候代码是很长的写起来就很麻烦使用了auto之后就变得清爽了不少
#include map
int main()
{mapint, string person;// 代码简化for (auto it person.begin(); it ! person.end(); it){// do something}return 0;
}用于泛型编程
在使用模板的时候很多情况下我们不知道变量应该定义为什么类型比如下面的代码
#include #include using namespace std;
class T1
{
public:static int get(){return 10;}
};class T2
{
public:static string get(){return hello, world;}
};template class A
void func(void)
{auto val A::get();cout val: val endl;
}int main()
{funcT1();funcT2();return 0;
}在这个例子中定义了泛型函数func在函数中调用了类A的静态方法 get() 这个函数的返回值是不能确定的如果不使用auto就需要再定义一个模板参数并且在外部调用时手动指定get的返回值类型具体代码如下
#include iostream
#include string
using namespace std;class T1
{
public:static int get(){return 0;}
};class T2
{
public:static string get(){return hello, world;}
};template class A, typename B // 添加了模板参数 B
void func(void)
{B val A::get();cout val: val endl;
}int main()
{funcT1, int(); // 手动指定返回值类型 - intfuncT2, string(); // 手动指定返回值类型 - stringreturn 0;
}1.1.4 范围for
对应基于范围的for循环来说冒号后边的表达式只会被执行一次。在得到遍历对象之后会先确定好迭代的范围基于这个范围直接进行遍历。如果是普通的for循环在每次迭代的时候都需要判断是否已经到了结束边界。
#include iostream
#include vector
using namespace std;vectorint v{ 1,2,3,4,5,6 };
vectorint getRange()
{cout get vector range... endl;return v;
}int main(void)
{for (auto val : getRange()){cout val ;}cout endl;return 0;
}get vector range...
1 2 3 4 5 61.2 decltype 在某些情况下不需要或者不能定义变量但是希望得到某种类型这时候就可以使用C11提供的decltype关键字了它的作用是在编译器编译的时候推导出一个表达式的类型 //语法格式
decltype (表达式)decltype 是“declare type”的缩写意思是“声明类型”。 decltype的推导是在编译期完成的它只是用于表达式类型的推导并不会计算表达式的值。
一组简单的例子
int a 10;
decltype(a) b 99; // b - int
decltype(a3.14) c 52.13; // c - double
decltype(ab*c) d 520.1314; // d - double可以看到decltype推导的表达式可简单可复杂在这一点上auto是做不到的auto只能推导已初始化的变量类型。 1.2.1 推导规则
通过上面的例子我们初步感受了一下 decltype 的用法但不要认为 decltype 就这么简单在它简单的背后隐藏着很多的细节 分三个场景依次讨论一下
表达式为普通变量或者普通表达式或者类表达式在这种情况下使用decltype推导出的类型和表达式的类型是一致的。
#include iostream
#include string
using namespace std;class Test
{
public:string text;static const int value 110;
};int main()
{int x 99;const int y x;decltype(x) a x;decltype(y) b x;decltype(Test::value) c 0;Test t;decltype(t.text) d hello, world;return 0;
}变量a被推导为 int类型变量b被推导为 const int 类型变量c被推导为 const int类型变量d被推导为 string类型
表达式是函数调用使用decltype推导出的类型和函数返回值一致。
class Test{...};
//函数声明
int func_int(); // 返回值为 int
int func_int_r(); // 返回值为 int
int func_int_rr(); // 返回值为 intconst int func_cint(); // 返回值为 const int
const int func_cint_r(); // 返回值为 const int
const int func_cint_rr(); // 返回值为 const intconst Test func_ctest(); // 返回值为 const Test//decltype类型推导
int n 100;
decltype(func_int()) a 0;
decltype(func_int_r()) b n;
decltype(func_int_rr()) c 0;
decltype(func_cint()) d 0;
decltype(func_cint_r()) e n;
decltype(func_cint_rr()) f 0;
decltype(func_ctest()) g Test(); 变量a被推导为 int类型变量b被推导为 int类型变量c被推导为 int类型变量d被推导为 int类型变量e被推导为 const int 类型变量f被推导为 const int 类型变量g被推导为 const Test类型
函数 func_cint() 返回的是一个纯右值在表达式执行结束后不再存在的数据也就是临时性的数据 对于纯右值而言只有类类型可以携带const、volatile限定符除此之外需要忽略掉这两个限定符 因此推导出的变量d的类型为 int 而不是 const int。
表达式是一个左值或者被括号( )包围使用 decltype推导出的是表达式类型的引用如果有const、volatile限定符不能忽略。
#include iostream
#include vector
using namespace std;class Test
{
public:int num;
};int main() {const Test obj;//带有括号的表达式decltype(obj.num) a 0;decltype((obj.num)) b a;//加法表达式int n 0, m 0;decltype(n m) c 0;decltype(n n m) d n;return 0;
}obj.num 为类的成员访问表达式符合场景1因此 a 的类型为intobj.num 带有括号符合场景3因此b 的类型为 const int。nm 得到一个右值符合场景1因此c的类型为 intnnm 得到一个左值 n符合场景3因此d的类型为 int
1.2.2 decltype的应用 关于decltype的应用多出现在泛型编程中。比如我们编写一个类模板在里边添加遍历容器的函数 #include list
using namespace std;template class T
class Container
{
public:void func(T c){for (m_it c.begin(); m_it ! c.end(); m_it){cout *m_it ;}cout endl;}
private:??? m_it; // 这里不能确定迭代器类型
};int main()
{const listint lst;Containerconst listint obj;obj.func(lst);return 0;
}在程序的???行出了问题关于迭代器变量一共有两种类型 只读T::const_iterator和读写T::iterator 有了decltype就可以完美的解决这个问题了 当 T 是一个 非 const 容器得到一个 T::iterator 当 T 是一个 const 容器时就会得到一个 T::const_iterator。
#include list
#include iostream
using namespace std;template class T
class Container
{
public:void func(T c){for (m_it c.begin(); m_it ! c.end(); m_it){cout *m_it ;}cout endl;}
private:decltype(T().begin()) m_it; // 这里不能确定迭代器类型
};int main()
{const listint lst{ 1,2,3,4,5,6,7,8,9 };Containerconst listint obj;obj.func(lst);return 0;
}1.3 返回类型后置 在泛型编程中可能需要通过参数的运算来得到返回值的类型 比如
#include iostream
using namespace std;
// R-返回值类型, T-参数1类型, U-参数2类型
template typename R, typename T, typename U
R add(T t, U u)
{return t u;
}int main()
{int x 520;double y 13.14;// auto z adddecltype(x y), int, double(x, y);auto z adddecltype(x y)(x, y); // 简化之后的写法cout z: z endl;return 0;
}关于返回值从上面的代码可以推断出和表达式tu的结果类型是一样的,因此可以通过decltype进行推导 关于模板函数的参数t和u可以通过实参自动推导出来因此在程序中就也可以不写。 虽然通过上述方式问题被解决了但是解决方案有点过于理想化因为对于调用者来说是不知道函数内部执行了什么样的处理动作的。
因此如果要想解决这个问题就得直接在 add 函数身上做文章先来看第一种写法
template typename T, typename U
decltype(tu) add(T t, U u)
{return t u;
}当我们在编译器中将这几行代码改出来后就直接报错了因为decltype中的 t 和 u 都是函数参数直接这样写相当于变量还没有定义就直接用上了这时候变量还不存在
C11中增加了返回类型后置语法说明白一点就是将decltype和auto结合起来完成返回类型的推导。
// 语法格式
// 符号 - 后边跟随的是函数返回值的类型
auto func(参数1, 参数2, ...) - decltype(参数表达式)auto 会追踪 decltype() 推导出的类型 因此上边的add()函数可以做如下的修改
#include iostream
using namespace std;template typename T, typename U
// 返回类型后置语法
auto add(T t, U u) - decltype(tu)
{return t u;
}int main()
{int x 520;double y 13.14;// auto z addint, double(x, y);auto z add(x, y); // 简化之后的写法cout z: z endl;return 0;
}为了进一步说明再看一个例子
#include iostream
using namespace std;int test(int i)
{return i;
}double test(double d)
{d d 100;return d;
}template typename T
// 返回类型后置语法
auto myFunc(T t) - decltype(test(t))
{return test(t);
}int main()
{int x 520;double y 13.14;// auto z myFuncint(x);auto z myFunc(x); // 简化之后的写法cout z: z endl;// auto z myFuncdouble(y);auto z1 myFunc(y); // 简化之后的写法cout z1: z1 endl;return 0;
}在这个例子中通过decltype结合返回值后置语法很容易推导出来 test(t)函数可能出现的返回值类型并将其作用到了函数myFunc()上。
// 输出结果
z: 520
z1: 113.142.可调用对象包装器、绑定器 C11通过提供std::function 和 std::bind统一了可调用对象的各种操作。 2.1 可调用对象包装器 std::function是可调用对象的包装器。它是一个类模板可以容纳除了类成员函数指针之外的所有可调用对象。 通过指定它的模板参数它可以用统一的方式处理函数、函数对象、函数指针并允许保存和延迟执行它们。 2.1.1 基本用法
// 语法
#include functional
std::function返回值类型(参数类型列表) diy_name 可调用对象;下面的实例代码中演示了可调用对象包装器的基本使用方法
#include iostream
#include functional
using namespace std;int add(int a, int b)
{cout a b a b endl;return a b;
}class T1
{
public:static int sub(int a, int b){cout a - b a - b endl;return a - b;}
};class T2
{
public:int operator()(int a, int b){cout a * b a * b endl;return a * b;}
};int main(void)
{// 绑定一个普通函数functionint(int, int) f1 add;// 绑定以静态类成员函数functionint(int, int) f2 T1::sub;// 绑定一个仿函数T2 t;functionint(int, int) f3 t;// 函数调用f1(9, 3);f2(9, 3);f3(9, 3);return 0;
}输入结果如下:
9 3 12
9 - 3 6
9 * 3 27通过测试代码可以得到结论std::function可以将可调用对象进行包装得到一个统一的格式 包装完成得到的对象相当于一个函数指针和函数指针的使用方式相同通过包装器对象就可以完成对包装的函数的调用了。 2.1.2 作为回调函数使用
因为回调函数本身就是通过函数指针实现的使用对象包装器可以取代函数指针的作用
#include iostream
#include functional
using namespace std;class A
{
public:// 构造函数参数是一个包装器对象A(const functionvoid() f) : callback(f){}void notify(){callback(); // 调用通过构造函数得到的函数指针}
private:functionvoid() callback;
};class B
{
public:void operator()(){cout 我是要成为海贼王的男人!!! endl;}
};
int main(void)
{B b;A a(b); // 仿函数通过包装器对象进行包装a.notify();return 0;
}使用对象包装器std::function可以非常方便的将仿函数转换为一个函数指针 通过进行函数指针的传递在其他函数的合适的位置就可以调用这个包装好的仿函数了。
另外使用std::function作为函数的传入参数可以将定义方式不同的可调用对象进行统一的传递这样大大增加了程序的灵活性。
2.2 绑定器 std::bind用来将可调用对象与其参数一起进行绑定。 绑定后的结果可以使用std::function进行保存并延迟调用到任何我们需要的时候。 通俗来讲有两大作用
将可调用对象与其参数一起绑定成一个仿函数。将多元参数个数为nn1可调用对象转换为一元或者n-1元可调用对象即只绑定部分参数。
// 语法格式
// 绑定非类成员函数/变量
auto f std::bind(可调用对象地址, 绑定的参数/占位符);
// 绑定类成员函/变量
auto f std::bind(类函数/成员地址, 类实例对象地址, 绑定的参数/占位符);一个关于绑定器的实际使用的例子
#include iostream
#include functional
using namespace std;void callFunc(int x, const functionvoid(int) f)
{if (x % 2 0){f(x);}
}void output(int x)
{cout x ;
}void output_add(int x)
{cout x 10 ;
}int main(void)
{// 使用绑定器绑定可调用对象和参数auto f1 bind(output, placeholders::_1);for (int i 0; i 10; i){callFunc(i, f1);}cout endl;auto f2 bind(output_add, placeholders::_1);for (int i 0; i 10; i){callFunc(i, f2);}cout endl;return 0;
}测试代码输出的结果:
0 2 4 6 8
10 12 14 16 18使用std::bind绑定器在函数外部通过绑定不同的函数控制了最后执行的结果。 std::bind绑定器返回的是一个仿函数类型得到的返回值可以直接赋值给一个std::function 在使用的时候我们并不需要关心绑定器的返回值类型使用auto进行自动类型推导就可以了。
placeholders::_1是一个占位符代表这个位置将在函数调用时被传入的第一个参数所替代。同样还有其他的占位符placeholders::_2、placeholders::_3、placeholders::_4、placeholders::_5等……
有了占位符的概念之后使得std::bind的使用变得非常灵活:
#include iostream
#include functional
using namespace std;void output(int x, int y)
{cout x y endl;
}int main(void)
{// 使用绑定器绑定可调用对象和参数, 并调用得到的仿函数bind(output, 1, 2)();bind(output, placeholders::_1, 2)(10);bind(output, 2, placeholders::_1)(10);// error, 调用时没有第二个参数// bind(output, 2, placeholders::_2)(10);// 调用时第一个参数10被吞掉了没有被使用bind(output, 2, placeholders::_2)(10, 20);bind(output, placeholders::_1, placeholders::_2)(10, 20);bind(output, placeholders::_2, placeholders::_1)(10, 20);return 0;
}示例代码执行的结果:
1 2 // bind(output, 1, 2)();
10 2 // bind(output, placeholders::_1, 2)(10);
2 10 // bind(output, 2, placeholders::_1)(10);
2 20 // bind(output, 2, placeholders::_2)(10, 20);
10 20 // bind(output, placeholders::_1, placeholders::_2)(10, 20);
20 10 // bind(output, placeholders::_2, placeholders::_1)(10, 20);通过测试可以看到std::bind可以直接绑定函数的所有参数也可以仅绑定部分参数。在绑定部分参数的时候通过使用std::placeholders来决定空位参数将会属于调用发生时的第几个参数。
可调用对象包装器std::function是不能实现对类成员函数指针或者类成员指针的包装的但是通过绑定器std::bind的配合之后就可以完美的解决这个问题了
一个例子然后解释细节
#include iostream
#include functional
using namespace std;class Test
{
public:void output(int x, int y){cout x: x , y: y endl;}int m_number 100;
};int main(void)
{Test t;// 绑定类成员函数functionvoid(int, int) f1 bind(Test::output, t, placeholders::_1, placeholders::_2);// 绑定类成员变量(公共)functionint(void) f2 bind(Test::m_number, t);// 调用f1(520, 1314);f2() 2333;cout t.m_number: t.m_number endl;return 0;
}示例代码输出的结果:
x: 520, y: 1314
t.m_number: 2333在用绑定器绑定类成员函数或者成员变量的时候需要将它们所属的实例对象一并传递到绑定器函数内部。 f1的类型是functionvoid(int, int)通过使用std::bind将Test的成员函数output的地址和对象t绑定并转化为一个仿函数并存储到对象f1中。
使用绑定器绑定的类成员变量m_number得到的仿函数被存储到了类型为functionint(void)的包装器对象f2中并且可以在需要的时候修改这个成员。其中int是绑定的类成员的类型并且允许修改绑定的变量因此需要指定为变量的引用由于没有参数因此参数列表指定为void。
示例程序中是使用function包装器保存了bind返回的仿函数如果不知道包装器的模板类型如何指定可以直接使用auto进行类型的自动推导这样使用起来会更容易一些。 3. using
3.1 定义别名
// 使用typedef定义函数指针
typedef int(*func_ptr)(int, double);// 使用using定义函数指针
using func_ptr1 int(*)(int, double);效果是一样的,但是使用using更加清晰 3.2 模板的别名
typedef对模板的别名简单并不容易实现
template typename T
typedef mapint, T type; // error, 语法错误#include iostream
#include functional
#include map
using namespace std;template typename T
// 定义外敷类
struct MyMap
{typedef mapint, T type;
};int main(void)
{MyMapstring::type m;m.insert(make_pair(1, luffy));m.insert(make_pair(2, ace));MyMapint::type m1;m1.insert(1, 100);m1.insert(2, 200);return 0;
}在C11中新增了一个特性就是可以通过使用using来为一个模板定义别名 template typename T
using mymap mapint, T;#include iostream
#include functional
#include map
using namespace std;template typename T
using mymap mapint, T;int main(void)
{// map的value指定为string类型mymapstring m;m.insert(make_pair(1, luffy));m.insert(make_pair(2, ace));// map的value指定为int类型mymapint m1;m1.insert(1, 100);m1.insert(2, 200);return 0;
}再次强调,using的语法和typedef是一样的,并不会创建新的类型,只是定义别名, using相较于typedef的优势在于定义函数指针别名时看起来更加直观并可给模板定义别名。 4. 智能指针的使用
4.1 shared_ptr
4.1.1 通过构造函数初始化 如果智能指针被初始化了一块有效内存那么这块内存的引用计数1如果智能指针没有被初始化或者被初始化为nullptr空指针引用计数不会1。另外不要使用一个原始指针初始化多个shared_ptr。 #include iostream
#include memory
using namespace std;int main()
{// 使用智能指针管理一块 int 型的堆内存shared_ptrint ptr1(new int(520));cout ptr1管理的内存引用计数: ptr1.use_count() endl;// 使用智能指针管理一块字符数组对应的堆内存shared_ptrchar ptr2(new char[12]);cout ptr2管理的内存引用计数: ptr2.use_count() endl;// 创建智能指针对象, 不管理任何内存shared_ptrint ptr3;cout ptr3管理的内存引用计数: ptr3.use_count() endl;// 创建智能指针对象, 初始化为空shared_ptrint ptr4(nullptr);cout ptr4管理的内存引用计数: ptr4.use_count() endl;return 0;
}int *p new int;
shared_ptrint p1(p);
shared_ptrint p2(p); // error, 编译不会报错, 运行会出错4.1.2 通过拷贝和移动构造函数初始化
#include iostream
#include memory
using namespace std;int main()
{// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1shared_ptrint ptr1(new int(520));cout ptr1管理的内存引用计数: ptr1.use_count() endl;//调用拷贝构造函数shared_ptrint ptr2(ptr1);cout ptr2管理的内存引用计数: ptr2.use_count() endl;shared_ptrint ptr3 ptr1;cout ptr3管理的内存引用计数: ptr3.use_count() endl;//调用移动构造函数shared_ptrint ptr4(std::move(ptr1));cout ptr4管理的内存引用计数: ptr4.use_count() endl;std::shared_ptrint ptr5 std::move(ptr2);cout ptr5管理的内存引用计数: ptr5.use_count() endl;return 0;
}ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 2
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 3
ptr5管理的内存引用计数: 34.1.3 通过std::make_shared初始化 通过C提供的std::make_shared() 就可以完成内存对象的创建并将其初始化给智能指针 template class T, class... Args
shared_ptrT make_shared( Args... args );T模板参数的数据类型Args... args 要初始化的数据如果是通过make_shared创建对象需按照构造函数的参数列表指定
#include iostream
#include string
#include memory
using namespace std;class Test
{
public:Test() {cout construct Test... endl;}Test(int x) {cout construct Test, x x endl;}Test(string str) {cout construct Test, str str endl;}~Test(){cout destruct Test ... endl;}
};int main()
{// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1shared_ptrint ptr1 make_sharedint(520);cout ptr1管理的内存引用计数: ptr1.use_count() endl;shared_ptrTest ptr2 make_sharedTest();cout ptr2管理的内存引用计数: ptr2.use_count() endl;shared_ptrTest ptr3 make_sharedTest(520);cout ptr3管理的内存引用计数: ptr3.use_count() endl;shared_ptrTest ptr4 make_sharedTest(我是要成为海贼王的男人!!!);cout ptr4管理的内存引用计数: ptr4.use_count() endl;return 0;
}ptr1管理的内存引用计数: 1
construct Test...
ptr2管理的内存引用计数: 1
construct Test, x 520
ptr3管理的内存引用计数: 1
construct Test, str 我是要成为海贼王的男人!!!
ptr4管理的内存引用计数: 1
destruct Test ...
destruct Test ...
destruct Test ...4.1.4 通过 reset方法初始化
//函数原型
void reset() noexcept;template class Y
void reset( Y* ptr );template class Y, class Deleter
void reset( Y* ptr, Deleter d );template class Y, class Deleter, class Alloc
void reset( Y* ptr, Deleter d, Alloc alloc );ptr指向要取得所有权的对象的指针d指向要取得所有权的对象的指针aloc内部存储所用的分配器
#include iostream
#include string
#include memory
using namespace std;int main()
{// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1shared_ptrint ptr1 make_sharedint(520);shared_ptrint ptr2 ptr1;shared_ptrint ptr3 ptr1;shared_ptrint ptr4 ptr1;cout ptr1管理的内存引用计数: ptr1.use_count() endl;cout ptr2管理的内存引用计数: ptr2.use_count() endl;cout ptr3管理的内存引用计数: ptr3.use_count() endl;cout ptr4管理的内存引用计数: ptr4.use_count() endl;ptr4.reset();cout ptr1管理的内存引用计数: ptr1.use_count() endl;cout ptr2管理的内存引用计数: ptr2.use_count() endl;cout ptr3管理的内存引用计数: ptr3.use_count() endl;cout ptr4管理的内存引用计数: ptr4.use_count() endl;shared_ptrint ptr5;ptr5.reset(new int(250));cout ptr5管理的内存引用计数: ptr5.use_count() endl;return 0;
}ptr1管理的内存引用计数: 4
ptr2管理的内存引用计数: 4
ptr3管理的内存引用计数: 4
ptr4管理的内存引用计数: 4ptr1管理的内存引用计数: 3
ptr2管理的内存引用计数: 3
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 0ptr5管理的内存引用计数: 1对于一个未初始化的共享智能指针可以通过reset方法来初始化当智能指针中有值的时候调用reset会使引用计数减1。 4.1.5 获取原始指针
// 获取原始地址
T* get() const noexcept;#include iostream
#include string
#include memory
using namespace std;int main()
{int len 128;shared_ptrchar ptr(new char[len]);// 得到指针的原始地址char* add ptr.get();memset(add, 0, len);strcpy(add, 我是要成为海贼王的男人!!!);cout string: add endl;shared_ptrint p(new int);*p 100;cout p.get() *p endl;return 0;
}string: 我是要成为海贼王的男人!!!
0000026F48FE9410 1004.2 weak_ptr 弱引用智能指针std::weak_ptr可以看做是shared_ptr的助手 它不管理shared_ptr内部的指针。 std::weak_ptr没有重载操作符*和-因为它不共享指针不能操作资源所以它的构造不会增加引用计数析构也不会减少引用计数 它的主要作用就是作为一个旁观者监视shared_ptr中管理的资源是否存在。 4.2.1 基本使用方式
4.2.1.1 初始化
// 默认构造函数
constexpr weak_ptr() noexcept;
// 拷贝构造
weak_ptr (const weak_ptr x) noexcept;
template class U weak_ptr (const weak_ptrU x) noexcept;
// 通过shared_ptr对象构造
template class U weak_ptr (const shared_ptrU x) noexcept;具体使用方法如下
#include iostream
#include memory
using namespace std;int main()
{shared_ptrint sp(new int);weak_ptrint wp1;weak_ptrint wp2(wp1);weak_ptrint wp3(sp);weak_ptrint wp4;wp4 sp;weak_ptrint wp5;wp5 wp3;return 0;
}weak_ptr wp1;构造了一个空weak_ptr对象weak_ptr wp2(wp1);通过一个空weak_ptr对象构造了另一个空weak_ptr对象weak_ptr wp3(sp);通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象wp4 sp;通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象这是一个隐式类型转换wp5 wp3;通过一个weak_ptr对象构造了一个可用的weak_ptr实例对象 4.2.1.2 其他常用方法
4.2.1.2.1 use_count()
通过调用std::weak_ptr类提供的use_count()方法可以获得当前所观测资源的引用计数
// 函数原型
// 函数返回所监测的资源的引用计数
long int use_count() const noexcept;修改一下上面的测试程序添加打印资源引用计数的代码
#include iostream
#include memory
using namespace std;int main()
{shared_ptrint sp(new int);weak_ptrint wp1;weak_ptrint wp2(wp1);weak_ptrint wp3(sp);weak_ptrint wp4;wp4 sp;weak_ptrint wp5;wp5 wp3;cout use_count: endl;cout wp1: wp1.use_count() endl;cout wp2: wp2.use_count() endl;cout wp3: wp3.use_count() endl;cout wp4: wp4.use_count() endl;cout wp5: wp5.use_count() endl;return 0;
}测试程序输出的结果为:
use_count:
wp1: 0
wp2: 0
wp3: 1
wp4: 1
wp5: 1虽然弱引用智能指针wp3、wp4、wp5监测的资源是同一个但是它的引用计数并没有发生任何的变化也进一步证明了weak_ptr只是监测资源并不管理资源。 4.2.1.2.2 expired()
通过调用std::weak_ptr类提供的expired()方法来判断观测的资源是否已经被释放
// 函数原型
// 返回true表示资源已经被释放, 返回false表示资源没有被释放
bool expired() const noexcept;函数的使用方法如下:
#include iostream
#include memory
using namespace std;int main()
{shared_ptrint shared(new int(10));weak_ptrint weak(shared);cout 1. weak (weak.expired() ? is : is not) expired endl;shared.reset();cout 2. weak (weak.expired() ? is : is not) expired endl;return 0;
}测试代码输出的结果:
1. weak is not expired
2. weak is expiredweak_ptr监测的就是shared_ptr管理的资源 当共享智能指针调用shared.reset();之后管理的资源被释放因此weak.expired()函数的结果返回true表示监测的资源已经不存在了。 4.2.1.2.3 lock()
通过调用std::weak_ptr类提供的lock()方法来获取管理所监测资源的shared_ptr对象
// 函数原型
shared_ptrelement_type lock() const noexcept;函数的使用方法如下:
#include iostream
#include memory
using namespace std;int main()
{shared_ptrint sp1, sp2;weak_ptrint wp;sp1 std::make_sharedint(520);wp sp1;sp2 wp.lock();cout use_count: wp.use_count() endl;sp1.reset();cout use_count: wp.use_count() endl;sp1 wp.lock();cout use_count: wp.use_count() endl;cout *sp1: *sp1 endl;cout *sp2: *sp2 endl;return 0;
}测试代码输出的结果为:
use_count: 2
use_count: 1
use_count: 2
*sp1: 520
*sp2: 520sp2 wp.lock();通过调用lock()方法得到一个用于管理weak_ptr对象所监测的资源的共享智能指针对象使用这个对象初始化sp2此时所监测资源的引用计数为2sp1.reset();共享智能指针sp1被重置weak_ptr对象所监测的资源的引用计数减1sp1 wp.lock();sp1重新被初始化并且管理的还是weak_ptr对象所监测的资源因此引用计数加1共享智能指针对象sp1和sp2管理的是同一块内存因此最终打印的内存中的结果是相同的都是520 4.2.1.2.4 reset()
通过调用std::weak_ptr类提供的reset()方法来清空对象使其不监测任何资源
// 函数原型如下
void reset() noexcept;函数的使用非常简单
#include iostream
#include memory
using namespace std;int main()
{shared_ptrint sp(new int(10));weak_ptrint wp(sp);cout 1. wp (wp.expired() ? is : is not) expired endl;wp.reset();cout 2. wp (wp.expired() ? is : is not) expired endl;return 0;
}测试代码输出的结果为:
1. wp is not expired
2. wp is expiredweak_ptr对象sp被重置之后,变成了空对象不再监测任何资源因此wp.expired()返回true 4.2.2 返回管理this的shared_ptr
如果在一个类中编写了一个函数通过这个得到管理当前对象的共享智能指针 我们可能会写出如下代码
#include iostream
#include memory
using namespace std;struct Test
{shared_ptrTest getSharedPtr(){return shared_ptrTest(this);}~Test(){cout class Test is disstruct ... endl;}};int main()
{shared_ptrTest sp1(new Test);cout use_count: sp1.use_count() endl;shared_ptrTest sp2 sp1-getSharedPtr();cout use_count: sp1.use_count() endl;return 0;
}执行上面的测试代码运行中会出现异常在终端还是能看到对应的日志输出
use_count: 1
use_count: 1
class Test is disstruct ...
class Test is disstruct ...通过输出的结果可以看到一个对象被析构了两次 其原因是这样的在这个例子中使用同一个指针this构造了两个智能指针对象sp1和sp2这二者之间是没有任何关系的因为sp2并不是通过sp1初始化得到的实例对象。 在离开作用域之后this将被构造的两个智能指针各自析构导致重复析构的错误。
这个问题可以通过weak_ptr来解决通过wek_ptr返回管理this资源的共享智能指针对象shared_ptr。 C11中为我们提供了一个模板类叫做std::enable_shared_from_thisT这个类中有一个方法叫做shared_from_this()通过这个方法可以返回一个共享智能指针在函数的内部就是使用weak_ptr来监测this对象并通过调用weak_ptr的lock()方法返回一个shared_ptr对象。
修改之后的代码为
#include iostream
#include memory
using namespace std;struct Test : public enable_shared_from_thisTest
{shared_ptrTest getSharedPtr(){return shared_from_this();}~Test(){cout class Test is disstruct ... endl;}
};int main()
{shared_ptrTest sp1(new Test);cout use_count: sp1.use_count() endl;shared_ptrTest sp2 sp1-getSharedPtr();cout use_count: sp1.use_count() endl;return 0;
}测试代码输出的结果为:
use_count: 1
use_count: 2
class Test is disstruct ...注意在调用enable_shared_from_this类的shared_from_this()方法之前 必须要先初始化函数内部weak_ptr对象否则该函数无法返回一个有效的shared_ptr对象 具体处理方法可以参考上面的示例代码。
4.2.3. 解决循环引用问题
智能指针如果循环引用会导致内存泄露比如下面的例子
#include iostream
#include memory
using namespace std;struct TA;
struct TB;struct TA
{shared_ptrTB bptr;~TA(){cout class TA is disstruct ... endl;}
};struct TB
{shared_ptrTA aptr;~TB(){cout class TB is disstruct ... endl;}
};void testPtr()
{shared_ptrTA ap(new TA);shared_ptrTB bp(new TB);cout TA object use_count: ap.use_count() endl;cout TB object use_count: bp.use_count() endl;ap-bptr bp;bp-aptr ap;cout TA object use_count: ap.use_count() endl;cout TB object use_count: bp.use_count() endl;
}int main()
{testPtr();return 0;
}测试程序输出的结果如下:
TA object use_count: 1
TB object use_count: 1
TA object use_count: 2
TB object use_count: 2在测试程序中共享智能指针ap、bp对TA、TB实例对象的引用计数变为2在共享智能指针离开作用域之后引用计数只能减为1 这种情况下不会去删除智能指针管理的内存导致类TA、TB的实例对象不能被析构最终造成内存泄露。 通过使用weak_ptr可以解决这个问题只要将类TA或者TB的任意一个成员改为weak_ptr 修改之后的代码如下
#include iostream
#include memory
using namespace std;struct TA;
struct TB;struct TA
{weak_ptrTB bptr;~TA(){cout class TA is disstruct ... endl;}
};struct TB
{shared_ptrTA aptr;~TB(){cout class TB is disstruct ... endl;}
};void testPtr()
{shared_ptrTA ap(new TA);shared_ptrTB bp(new TB);cout TA object use_count: ap.use_count() endl;cout TB object use_count: bp.use_count() endl;ap-bptr bp;bp-aptr ap;cout TA object use_count: ap.use_count() endl;cout TB object use_count: bp.use_count() endl;
}int main()
{testPtr();return 0;
}程序输出的结果:
TA object use_count: 1
TB object use_count: 1
TA object use_count: 2
TB object use_count: 1
class TB is disstruct ...
class TA is disstruct ...通过输出的结果可以看到类TA或者TB的对象被成功析构了。
上面程序中在对类TA成员赋值时ap-bptr bp;由于bptr是weak_ptr类型这个赋值操作并不会增加引用计数所以bp的引用计数仍然为1在离开作用域之后bp的引用计数减为0类TB的实例对象被析构。
在类TB的实例对象被析构的时候内部的aptr也被析构其对TA对象的管理解除内存的引用计数减为1当共享智能指针ap离开作用域之后对TA对象的管理也解除了内存的引用计数减为0类TA的实例对象被析构。 5. constexpr
5.1 constexpr 介绍 在C11中添加了一个新的关键字constexpr这个关键字是用来修饰常量表达式的。 所谓常量表达式指的就是由多个≥1常量值不会改变组成并且在编译过程中就得到计算结果的表达式。 在介绍gcc/g工作流程的时候说过C 程序从编写完毕到执行分为四个阶段预处理、 编译、汇编和链接4个阶段得到可执行程序之后就可以运行了。 需要额外强调的是常量表达式和非常量表达式的计算时机不同 非常量表达式只能在程序运行阶段计算出结果 常量表达式的计算往往发生在程序的编译阶段这可以极大提高程序的执行效率因为表达式只需要在编译阶段计算一次节省了每次程序运行时都需要计算一次的时间。
那么问题来了编译器如何识别表达式是不是常量表达式呢在C11中添加了constexpr关键字之后就可以在程序中使用它来修饰常量表达式用来提高程序的执行效率。 在使用中建议将 const 和 constexpr 的功能区分开 即凡是表达“只读”语义的场景都使用 const表达“常量”语义的场景都使用 constexpr。
在定义常量时const 和 constexpr 是等价的都可以在程序的编译阶段计算出结果例如
const int m f(); // 不是常量表达式m的值只有在运行时才会获取。
const int i520; // 是一个常量表达式
const int ji1; // 是一个常量表达式constexpr int i520; // 是一个常量表达式
constexpr int ji1; // 是一个常量表达式对于 C 内置类型的数据可以直接用 constexpr 修饰 但如果是自定义的数据类型用 struct 或者 class 实现直接用 constexpr 修饰是不行的。 // 此处的constexpr修饰是无效的
constexpr struct Test
{int id;int num;
};如果要定义一个结构体/类常量对象可以这样写
struct Test
{int id;int num;
};int main()
{constexpr Test t{ 1, 2 };constexpr int id t.id;constexpr int num t.num;// error不能修改常量t.num 100;cout id: id , num: num endl;return 0;
}t.num 100;的操作是错误的对象t是常量因此它的成员也是常量常量是不能被修改的。 5.2 常量表达式函数 为了提高C程序的执行效率 我们可以将程序中值不需要发生变化的变量定义为常量 也可以使用constexpr修饰函数的返回值这种函数被称作常量表达式函数这些函数主要包括以下几种普通函数/类成员函数、类的构造函数、模板函数。 5.2.1 修饰函数
constexpr并不能修改任意函数的返回值使这些函数成为常量表达式函数 必须要满足以下几个条件
函数必须要有返回值并且return 返回的表达式必须是常量表达式。
// error不是常量表达式函数
constexpr void func1()
{int a 100;cout a: a endl;
}// error不是常量表达式函数
constexpr int func1()
{int a 100;return a;
}函数func1()没有返回值不满足常量表达式函数要求函数func2()返回值不是常量表达式不满足常量表达式函数要求 由此可见在更新的C标准里边放宽了对constexpr的语法限制。
函数在使用之前必须有对应的定义语句。
#include iostream
using namespace std;constexpr int func1();
int main()
{constexpr int num func1(); // errorreturn 0;
}constexpr int func1()
{constexpr int a 100;return a;
}在测试程序constexpr int num func1();中还没有定义func1()就直接调用了 应该将func1()函数的定义放到main()函数的上边。
整个函数的函数体中不能出现非常量表达式之外的语句using 指令、typedef 语句以及 static_assert 断言、return语句除外。
// error
constexpr int func1()
{constexpr int a 100;constexpr int b 10;for (int i 0; i b; i){cout i: i endl;}return a b;
}// ok
constexpr int func2()
{using mytype int;constexpr mytype a 100;constexpr mytype b 10;constexpr mytype c a * b;return c - (a b);
}因为func1()是一个常量表达式函数在函数体内部是不允许出现非常量表达式以外的操作因此函数体内部的for循环是一个非法操作。 以上三条规则不仅对应普通函数适用对应类的成员函数也是适用的 class Test
{
public:constexpr int func(){constexpr int var 100;return 5 * var;}
};int main()
{Test t;constexpr int num t.func();cout num: num endl;return 0;
}5.2.2 修饰模板函数 C11 语法中constexpr可以修饰函数模板但由于模板中类型的不确定性因此函数模板实例化后的模板函数是否符合常量表达式函数的要求也是不确定的。 如果constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求则 constexpr 会被自动忽略即该函数就等同于一个普通函数。 #include iostream
using namespace std;struct Person {const char* name;int age;
};// 定义函数模板
templatetypename T
constexpr T dispaly(T t) {return t;
}int main()
{struct Person p { luffy, 19 };//普通函数struct Person ret dispaly(p);cout luffys name: ret.name , age: ret.age endl;//常量表达式函数constexpr int ret1 dispaly(250);cout ret1 endl;constexpr struct Person p1 { luffy, 19 };constexpr struct Person p2 dispaly(p1);cout luffys name: p2.name , age: p2.age endl;return 0;
}在上面示例程序中定义了一个函数模板 display()但由于其返回值类型未定因此在实例化之前无法判断其是否符合常量表达式函数的要求
struct Person ret dispaly( p );由于参数p是变量所以实例化后的函数不是常量表达式函数此时 constexpr 是无效的constexpr int ret1 dispaly(250);参数是常量符合常量表达式函数的要求此时 constexpr 是有效的constexpr struct Person p2 dispaly(p1);参数是常量符合常量表达式函数的要求此时 constexpr 是有效的 5.2.3 修饰构造函数 如果想用直接得到一个常量对象也可以使用constexpr修饰一个构造函数这样就可以得到一个常量构造函数了。 常量构造函数有一个要求构造函数的函数体必须为空并且必须采用初始化列表的方式为各个成员赋值。 #include iostream
using namespace std;struct Person {constexpr Person(const char* p, int age) :name(p), age(age){}const char* name;int age;
};int main()
{constexpr struct Person p1(luffy, 19);cout luffys name: p1.name , age: p1.age endl;return 0;
}6. 委托构造和继承构造函数
6.1 委托构造函数
委托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数从而简化相关变量的初始化。 下面举例说明
#include iostream
using namespace std;class Test
{
public:Test() {};Test(int max){this-m_max max 0 ? max : 100;}Test(int max, int min){this-m_max max 0 ? max : 100; // 冗余代码this-m_min min 0 min max ? min : 1; }Test(int max, int min, int mid){this-m_max max 0 ? max : 100; // 冗余代码this-m_min min 0 min max ? min : 1; // 冗余代码this-m_middle mid max mid min ? mid : 50;}int m_min;int m_max;int m_middle;
};int main()
{Test t(90, 30, 60);cout min: t.m_min , middle: t.m_middle , max: t.m_max endl;return 0;
}在上面的程序中有三个构造函数但是这三个函数中都有重复的代码在C11之前构造函数是不能调用构造函数的加入了委托构造之后我们就可以轻松地完成代码的优化了
#include iostream
using namespace std;class Test
{
public:Test() {};Test(int max){this-m_max max 0 ? max : 100;}Test(int max, int min):Test(max){this-m_min min 0 min max ? min : 1;}Test(int max, int min, int mid):Test(max, min){this-m_middle mid max mid min ? mid : 50;}int m_min;int m_max;int m_middle;
};int main()
{Test t(90, 30, 60);cout min: t.m_min , middle: t.m_middle , max: t.m_max endl;return 0;
}在修改之后的代码中可以看到重复的代码全部没有了并且在一个构造函数中调用了其他的构造函数用于相关数据的初始化相当于是一个链式调用。在使用委托构造函数的时候还需要注意一些几个问题 这种链式的构造函数调用不能形成一个闭环死循环否则会在运行期抛异常。 如果要进行多层构造函数的链式调用建议将构造函数的调用的写在初始列表中而不是函数体内部否则编译器会提示形参的重复定义。
Test(int max)
{this-m_max max 0 ? max : 100;
}Test(int max, int min)
{Test(max); // error, 此处编译器会报错, 提示形参max被重复定义this-m_min min 0 min max ? min : 1;
}在初始化列表中调用了代理构造函数初始化某个类成员变量之后就不能在初始化列表中再次初始化这个变量了。
// 错误, 使用了委托构造函数就不能再次m_max初始化了
Test(int max, int min) : Test(max), m_max(max)
{this-m_min min 0 min max ? min : 1;
}6.2 继承构造函数 C11中提供的继承构造函数可以让派生类直接使用基类的构造函数而无需自己再写构造函数尤其是在基类有很多构造函数的情况下可以极大地简化派生类构造函数的编写。 没有继承构造函数之前的处理方式
#include iostream
#include string
using namespace std;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:Child(int i) :Base(i) {}Child(int i, double j) :Base(i, j) {}Child(int i, double j, string k) :Base(i, j, k) {}
};int main()
{Child c(520, 13.14, i love you);cout int: c.m_i , double: c.m_j , string: c.m_k endl;return 0;
}在子类中初始化从基类继承的类成员需要在子类中重新定义和基类一致的构造函数这是非常繁琐的 C11中通过添加继承构造函数这个新特性完美的解决了这个问题使得代码更加精简。
继承构造函数的使用方法是这样的通过使用using 类名::构造函数名其实类名和构造函数名是一样的来声明使用基类的构造函数这样子类中就可以不定义相同的构造函数了直接使用基类的构造函数来构造派生类对象。
#include iostream
#include string
using namespace std;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;
};int main()
{Child c1(520, 13.14);cout int: c1.m_i , double: c1.m_j endl;Child c2(520, 13.14, i love you);cout int: c2.m_i , double: c2.m_j , string: c2.m_k endl;return 0;
}在修改之后的子类中没有添加任何构造函数而是添加了using Base::Base;这样就可以在子类中直接继承父类的所有的构造函数通过他们去构造子类对象了。
另外如果在子类中隐藏了父类中的同名函数也可以通过using的方式在子类中使用基类中的这些父类函数
#include iostream
#include string
using namespace std;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) {}void func(int i){cout base class: i i endl;}void func(int i, string str){cout base class: i i , str str endl;}int m_i;double m_j;string m_k;
};class Child : public Base
{
public:using Base::Base;using Base::func;void func(){cout child class: iam luffy!!! endl;}
};int main()
{Child c(250);c.func();c.func(19);c.func(19, luffy);return 0;
}上述示例代码输出的结果为
child class: iam luffy!!!
base class: i 19
base class: i 19, str luffy子类中的func()函数隐藏了基类中的两个func()因此默认情况下通过子类对象只能调用无参的func() 在上面的子类代码中添加了using Base::func;之后就可以通过子类对象直接调用父类中被隐藏的带参func()函数了。 7. 原始字面量
R “xxx(原始字符串)xxx”一个例子直接带入
#includeiostream
#includestring
using namespace std;
int main()
{string str D:\hello\world\test.text;cout str endl;string str1 D:\\hello\\world\\test.text;cout str1 endl;string str2 R(D:\hello\world\test.text);cout str2 endl;return 0;
}D:helloworld est.text
D:\hello\world\test.text
D:\hello\world\test.text在R “xxx(raw string)xxx” 中原始字符串必须用括号括起来括号的前后可以加其他字符串所加的字符串会被忽略并且加的字符串必须在括号两边同时出现。
#includeiostream
#includestring
using namespace std;
int main()
{string str1 R(D:\hello\world\test.text);cout str1 endl;string str2 Rluffy(D:\hello\world\test.text)luffy;cout str2 endl;
#if 0string str3 Rluffy(D:\hello\world\test.text)robin; // 语法错误编译不通过cout str3 endl;
#endifreturn 0;
}D:\hello\world\test.text
D:\hello\world\test.text结论:使用原始字面量R “xxx(raw string)xxx”两边的字符串在解析的时候是会被忽略的因此一般不用指定。如果在前后指定了字符串那么前后的字符串必须相同否则会出现语法错误。 8. chrono库 C11中提供了日期和时间相关的库chrono通过chrono库可以很方便地处理日期和时间为程序的开发提供了便利。 chrono库主要包含三种类型的类时间间隔duration、时钟clocks、时间点time point。 8.1 时间间隔
8.1.1 常用类成员 duration表示一段时间间隔用来记录时间长度可以表示几秒、几分钟、几个小时的时间间隔。 //原型
// 定义于头文件 chrono
templateclass Rep,class Period std::ratio1class duration;Rep这是一个数值类型表示时钟数周期的类型默认为整形。 若 Rep 是浮点数则 duration 能使用小数描述时钟周期的数目。 Period表示时钟的周期它的原型如下
// 定义于头文件 ratio
templatestd::intmax_t Num,std::intmax_t Denom 1 class ratio;ratio类表示每个时钟周期的秒数其中第一个模板参数Num代表分子Denom代表分母该分母值默认为1 因此ratio代表的是一个分子除以分母的数值 比如ratio2代表一个时钟周期是2秒ratio60代表一分钟ratio60*60代表一个小时ratio60*60*24代表一天。 而ratio1,1000代表的是1/1000秒也就是1毫秒ratio1,1000000代表一微秒ratio1,1000000000代表一纳秒。 为了方便使用在标准库中定义了一些常用的时间间隔比如时、分、秒、毫秒、微秒、纳秒它们都位于chrono命名空间下定义如下 类型定义纳秒std::chrono::nanosecondsdurationRep*/至少 64 位的有符号整数类型/*, std::nano微秒std::chrono::microsecondsdurationRep*/至少 55 位的有符号整数类型/*, std::micro毫秒std::chrono::millisecondsdurationRep*/至少 45 位的有符号整数类型/*, std::milli秒std::chrono::secondsdurationRep*/至少 35 位的有符号整数类型/*分钟std::chrono::minutesdurationRep*/至少 29 位的有符号整数类型/*, std::ratio60小时std::chrono::hoursdurationRep*/至少 23 位的有符号整数类型/*, std::ratio3600
注意到 hours 为止的每个预定义时长类型至少涵盖 ±292 年的范围。
duration类的构造函数原型如下
// 1. 拷贝构造函数
duration( const duration ) default;
// 2. 通过指定时钟周期的类型来构造对象
template class Rep2
constexpr explicit duration( const Rep2 r );
// 3. 通过指定时钟周期类型和时钟周期长度来构造对象
template class Rep2, class Period2
constexpr duration( const durationRep2,Period2 d );为了更加方便的进行duration对象之间的操作类内部进行了操作符重载 操作符描述operator对应复制内容 (公开成员函数)operator operator-实现一元 和一元 - (公开成员函数)operator operator(int) operator– operator–(int)递增或递减周期计数 (公开成员函数)operator operator- operator* operator/ operator%实现二个时长间的复合赋值 (公开成员函数) duration类还提供了获取时间间隔的时钟周期数的方法count() // 函数原型
constexpr rep count() const;8.1.2 类的使用 通过构造函数构造事件间隔对象示例代码如下 #include chrono
#include iostream
using namespace std;
int main()
{chrono::hours h(1); // 一小时chrono::milliseconds ms{ 3 }; // 3 毫秒 花括号也可以初始化chrono::durationint, ratio1000 ks(3); // 3000 秒// chrono::durationint, ratio1000 d3(3.5); // errorchrono::durationdouble dd(6.6); // 6.6 秒// 使用小数表示时钟周期的次数chrono::durationdouble, std::ratio1, 30 hz(3.5);
}h(1)时钟周期为1小时共有1个时钟周期所以h表示的时间间隔为1小时ms(3)时钟周期为1毫秒共有3个时钟周期所以ms表示的时间间隔为3毫秒ks(3)时钟周期为1000秒一共有三个时钟周期所以ks表示的时间间隔为3000秒d3(3.5)时钟周期为1000秒时钟周期数量只能用整形来表示但是此处指定的是浮点数因此语法错误dd(6.6)时钟周期为默认的1秒共有6.6个时钟周期所以dd表示的时间间隔为6.6秒hz(3.5)时钟周期为1/30秒共有3.5个时钟周期所以hz表示的时间间隔为1/30*3.5秒
chrono库中根据duration类封装了不同长度的时钟周期也可以自定义基于这个时钟周期再进行周期次数的设置就可以得到总的时间间隔了时钟周期 * 周期次数 总的时间间隔。
示例代码如下
#include chrono
#include iostream
int main()
{std::chrono::milliseconds ms{3}; // 3 毫秒std::chrono::microseconds us 2*ms; // 6000 微秒// 时间间隔周期为 1/30 秒std::chrono::durationdouble, std::ratio1, 30 hz(3.5);std::cout 3 ms duration has ms.count() ticks\n 6000 us duration has us.count() ticks\n 3.5 hz duration has hz.count() ticks\n;
}输出的结果为
3 ms duration has 3 ticks
6000 us duration has 6000 ticks
3.5 hz duration has 3.5 ticksms时间单位为毫秒初始化操作ms{3}表示时间间隔为3毫秒一共有3个时间周期每个周期为1毫秒us时间单位为微秒初始化操作2*ms表示时间间隔为6000微秒一共有6000个时间周期每个周期为1微秒hz时间单位为秒初始化操作hz(3.5)表示时间间隔为1/30*3.5秒一共有3.5个时间周期每个周期为1/30秒
由于在duration类内部做了操作符重载因此时间间隔之间可以直接进行算术运算比如我们要计算两个时间间隔的差值就可以在代码中做如下处理
#include iostream
#include chrono
using namespace std;int main()
{chrono::minutes t1(10);chrono::seconds t2(60);chrono::seconds t3 t1 - t2;cout t3.count() second endl;
}程序输出的结果
540 second在上面的测试程序中t1代表10分钟t2代表60秒t3是t1减去t2也就是60*10-60540这个540表示的时钟周期每个时钟周期是1秒因此两个时间间隔之间的差值为540秒。
注意duration的加减运算有一定的规则当两个duration时钟周期不相同的时候会先统一成一种时钟然后再进行算术运算 统一的规则如下假设有ratiox1,y1 和 ratiox2,y2两个时钟周期首先需要求出x1x2的最大公约数X然后求出y1y2的最小公倍数Y统一之后的时钟周期ratio为ratioX,Y。
#include iostream
#include chrono
using namespace std;int main()
{chrono::durationdouble, ratio9, 7 d1(3);chrono::durationdouble, ratio6, 5 d2(1);// d1 和 d2 统一之后的时钟周期chrono::durationdouble, ratio3, 35 d3 d1 - d2;
}对于分子6,、9最大公约数为3对于分母7、5最小公倍数为35因此推导出的时钟周期为ratio3,35 8.2 时间点 time point
chrono库中提供了一个表示时间点的类time_point
类的定义如下
// 定义于头文件 chrono
templateclass Clock,class Duration typename Clock::durationclass time_point;它被实现成如同存储一个 Duration 类型的自 Clock 的纪元起始开始的时间间隔的值通过这个类最终可以得到时间中的某一个时间点。
Clock此时间点在此时钟上计量Duration用于计量从纪元起时间的 std::chrono::duration 类型
time_point类的构造函数原型如下
// 1. 构造一个以新纪元(epoch即1970.1.1)作为值的对象需要和时钟类一起使用不能单独使用该无参构造函数
time_point();
// 2. 构造一个对象表示一个时间点其中d的持续时间从epoch开始需要和时钟类一起使用不能单独使用该构造函数
explicit time_point( const duration d );
// 3. 拷贝构造函数构造与t相同时间点的对象使用的时候需要指定模板参数
template class Duration2
time_point( const time_pointClock,Duration2 t );在这个类中除了构造函数还提供了另外一个time_since_epoch()函数 用来获得1970年1月1日到time_point对象中记录的时间经过的时间间隔duration // 函数原型
duration time_since_epoch() const;除此之外时间点time_point对象和时间段对象duration之间还支持直接进行算术运算即加减运算时间点对象之间可以进行逻辑运算具体细节可以参考下面的表格 其中 tp 和 tp2 是time_point 类型的对象 dtn 是duration类型的对象。 描述操作返回值复合赋值(成员函数) operatortp dtn*this复合赋值(成员函数) operator-tp - dtn*this算术运算符(非成员函数) operatortp dtna time_point value算术运算符(非成员函数) operatordtn tpa time_point value算术运算符(非成员函数) operator-tp - dtna time_point value算术运算符(非成员函数) operator-tp - tp2a duration value关系操作符(非成员函数) operatortp tp2a bool value关系操作符(非成员函数) operator!tp ! tp2a bool value关系操作符(非成员函数) operatortp tp2a bool value关系操作符(非成员函数) operatortp tp2a bool value关系操作符(非成员函数) operatortp tp2a bool value关系操作符(非成员函数) operatortp tp2a bool value
由于该时间点类经常和下面要介绍的时钟类一起使用所以在此先不举例 在时钟类的示例代码中会涉及到时间点类的使用到此为止只需要搞明白时间点类的提供的这几个函数的作用就可以了。 8.3 时钟clocks
chrono库中提供了获取当前的系统时间的时钟类包含的时钟一共有三种
system_clock系统的时钟系统的时钟可以修改甚至可以网络对时因此使用系统时间计算时间差可能不准。steady_clock是固定的时钟相当于秒表。开始计时后时间只会增长并且不能修改适合用于记录程序耗时high_resolution_clock和时钟类 steady_clock 是等价的是它的别名。
在这些时钟类的内部有time_point、duration、Rep、Period等信息基于这些信息来获取当前时间以及实现time_t和time_point之间的相互转换。
时钟类成员类型描述rep表示时钟周期次数的有符号算术类型period表示时钟计次周期的 std::ratio 类型duration时间间隔可以表示负时长time_point表示在当前时钟里边记录的时间点
在使用chrono提供的时钟类的时候不需创建类对象直接调用类的静态方法就可以得到想要的时间。
8.3.1 system_clock 具体来说时钟类system_clock是一个系统范围的实时时钟。 system_clock提供了对当前时间点time_point的访问将得到时间点转换为time_t类型的时间对象就可以基于这个时间对象获取到当前的时间信息了。 system_clock时钟类在底层源码中的定义如下
struct system_clock { // wraps GetSystemTimePreciseAsFileTime/GetSystemTimeAsFileTimeusing rep long long;using period ratio1, 10000000; // 100 nanosecondsusing duration chrono::durationrep, period;using time_point chrono::time_pointsystem_clock;static constexpr bool is_steady false;_NODISCARD static time_point now() noexcept { // get current timereturn time_point(duration(_Xtime_get_ticks()));}_NODISCARD static __time64_t to_time_t(const time_point _Time) noexcept { // convert to __time64_treturn duration_castseconds(_Time.time_since_epoch()).count();}_NODISCARD static time_point from_time_t(__time64_t _Tm) noexcept { // convert from __time64_treturn time_point{seconds{_Tm}};}
};通过以上源码可以了解到在system_clock类中的一些细节信息
rep时钟周期次数是通过整形来记录的long longperiod一个时钟周期是100纳秒ratio1, 10000000duration时间间隔为rep*period纳秒chrono::durationrep, periodtime_point时间点通过系统时钟做了初始化chrono::time_pointsystem_clock里面记录了新纪元时间点
另外还可以看到system_clock类一共提供了三个静态成员函数
// 返回表示当前时间的时间点。
static std::chrono::time_pointstd::chrono::system_clock now() noexcept;
// 将 time_point 时间点类型转换为 std::time_t 类型
static std::time_t to_time_t( const time_point t ) noexcept;
// 将 std::time_t 类型转换为 time_point 时间点类型
static std::chrono::system_clock::time_point from_time_t( std::time_t t ) noexcept;比如我们要获取当前的系统时间并且需要将其以能够识别的方式打印出来示例代码如下
#include chrono
#include iostream
using namespace std;
using namespace std::chrono;
int main()
{// 新纪元1970.1.1时间system_clock::time_point epoch;durationint, ratio60*60*24 day(1);// 新纪元1970.1.1时间 1天system_clock::time_point ppt(day);using dday durationint, ratio60 * 60 * 24;// 新纪元1970.1.1时间 10天time_pointsystem_clock, dday t(dday(10));// 系统当前时间system_clock::time_point today system_clock::now();// 转换为time_t时间类型time_t tm system_clock::to_time_t(today);cout 今天的日期是: ctime(tm);time_t tm1 system_clock::to_time_t(todayday);cout 明天的日期是: ctime(tm1);time_t tm2 system_clock::to_time_t(epoch);cout 新纪元时间: ctime(tm2);time_t tm3 system_clock::to_time_t(ppt);cout 新纪元时间1天: ctime(tm3);time_t tm4 system_clock::to_time_t(t);cout 新纪元时间10天: ctime(tm4);
}示例代码打印的结果为
今天的日期是: Sun Aug 20 02:43:21 2023
明天的日期是: Mon Aug 21 02:43:21 2023
新纪元时间: Thu Jan 1 08:00:00 1970
新纪元时间1天: Fri Jan 2 08:00:00 1970
新纪元时间10天: Sun Jan 11 08:00:00 19708.3.2 steady_clock 如果我们通过时钟不是为了获取当前的系统时间而是进行程序耗时的时长此时使用syetem_clock就不合适了因为这个时间可以跟随系统的设置发生变化。 在C11中提供的时钟类steady_clock相当于秒表只要启动就会进行时间的累加并且不能被修改非常适合于进行耗时的统计。 steady_clock时钟类在底层源码中的定义如下
struct steady_clock { // wraps QueryPerformanceCounterusing rep long long;using period nano;using duration nanoseconds;using time_point chrono::time_pointsteady_clock;static constexpr bool is_steady true;// get current time_NODISCARD static time_point now() noexcept { // doesnt change after system bootconst long long _Freq _Query_perf_frequency(); const long long _Ctr _Query_perf_counter();static_assert(period::num 1, This assumes period::num 1.);const long long _Whole (_Ctr / _Freq) * period::den;const long long _Part (_Ctr % _Freq) * period::den / _Freq;return time_point(duration(_Whole _Part));}
};通过以上源码可以了解到在steady_clock类中的一些细节信息
rep时钟周期次数是通过整形来记录的long longperiod一个时钟周期是1纳秒nanoduration时间间隔为1纳秒nanosecondstime_point时间点通过系统时钟做了初始化chrono::time_pointsteady_clock 另外在这个类中也提供了一个静态的now()方法用于得到当前的时间点 // 函数原型
static std::chrono::time_pointstd::chrono::steady_clock now() noexcept;假设要测试某一段程序的执行效率可以计算它执行期间消耗的总时长
#include chrono
#include iostream
using namespace std;
using namespace std::chrono;
int main()
{// 获取开始时间点steady_clock::time_point start steady_clock::now();// 执行业务流程cout print 1000 stars .... endl;for (int i 0; i 1000; i){cout *;}cout endl;// 获取结束时间点steady_clock::time_point last steady_clock::now();// 计算差值auto dt last - start;cout 总共耗时: dt.count() 纳秒 endl;
}8.3.3 high_resolution_clock high_resolution_clock提供的时钟精度比system_clock要高 它也是不可以修改的。在底层源码中这个类其实是steady_clock类的别名。 using high_resolution_clock steady_clock;因此high_resolution_clock的使用方式和steady_clock是一样的 8.4 转换函数
8.4.1 duration_cast duration_cast是chrono库提供的一个模板函数,这个函数不属于duration类。 通过这个函数可以对duration类对象内部的时钟周期Period和周期次数的类型Rep进行修改 // 函数原型
template class ToDuration, class Rep, class Periodconstexpr ToDuration duration_cast (const durationRep,Period dtn);如果是对时钟周期进行转换源时钟周期必须能整除目的时钟周期比如小时到分钟。如果是对时钟周期次数的类型进行转换低等类型默认可以向高等类型进行转换比如int 转 double。如果时钟周期和时钟周期次数类型都变了,根据第二点推导也就是看时间周期次数类型。以上条件都不满足那么就需要使用 duration_cast 进行显示转换。
我们可以修改一下上面测试程序执行时间的代码在代码中修改duration对象的属性
#include iostream
#include chrono
using namespace std;
using namespace std::chrono;void f()
{cout print 1000 stars .... endl;for (int i 0; i 1000; i){cout *;}cout endl;
}int main()
{auto t1 steady_clock::now();f();auto t2 steady_clock::now();// 整数时长时钟周期纳秒 转 毫秒要求 duration_castauto int_ms duration_castchrono::milliseconds(t2 - t1);// 小数时长不要求 duration_castdurationdouble, ratio1, 1000 fp_ms t2 - t1;cout f() took fp_ms.count() ms, or int_ms.count() whole milliseconds\n;
}示例代码输出的结果
print 1000 stars ....
*************************************************************************************************************
f() took 40.2547 ms, or 40 whole milliseconds8.4.2 time_point_cast time_point_cast也是chrono库提供的一个模板函数这个函数不属于time_point类。 函数的作用是对时间点进行转换因为不同的时间点对象内部的时钟周期Period和周期次数的类型Rep可能也是不同的一般情况下它们之间可以进行隐式类型转换也可以通过该函数显示的进行转换 // 函数原型
template class ToDuration, class Clock, class Duration
time_pointClock, ToDuration time_point_cast(const time_pointClock, Duration t);关于函数的使用示例代码如下
#include chrono
#include iostream
using namespace std;using Clock chrono::high_resolution_clock;
using Ms chrono::milliseconds;
using Sec chrono::seconds;
templateclass Duration
using TimePoint chrono::time_pointClock, Duration;void print_ms(const TimePointMs time_point)
{std::cout time_point.time_since_epoch().count() ms\n;
}int main()
{TimePointSec time_point_sec(Sec(6));// 无精度损失, 可以进行隐式类型转换TimePointMs time_point_ms(time_point_sec);print_ms(time_point_ms); // 6000 mstime_point_ms TimePointMs(Ms(6789));// error会损失精度不允许进行隐式的类型转换(ms类型转换为sec类型)TimePointSec sec(time_point_ms);// 显示类型转换,会损失精度。6789 truncated to 6000time_point_sec std::chrono::time_point_castSec(time_point_ms);print_ms(time_point_sec); // 6000 ms
}注意关于时间点的转换如果没有没有精度的损失可以直接进行隐式类型转换 如果会损失精度只能通过显示类型转换也就是调用time_point_cast函数来完成该操作。 9. 静态断言 静态断言static_assert所谓静态就是在编译时就能够进行检查的断言使用时不需要引用头文件。静态断言的另一个好处是可以自定义违反断言时的错误提示信息。 静态断言使用起来非常简单它接收两个参数
参数1断言表达式这个表达式通常需要返回一个 bool值参数2警告信息它通常就是一段字符串在违反断言表达式为false时提示该信息
一个判断Linux是否为32位平台的小程序
#include iostream
using namespace std;int main()
{static_assert(sizeof(long) 4, 错误, 不是32位平台...);cout 64bit Linux 指针大小: sizeof(char*) endl;cout 64bit Linux long 大小: sizeof(long) endl;return 0;
}g assert.cpp -stdc11
assert.cpp: In function ‘int main()’:
assert.cpp:6:5: error: static assertion failed: 错误, 不是32位平台...
static_assert(sizeof(long) 4, 错误, 不是32位平台...);注意 : 由于静态断言的表达式是在编译阶段进行检测所以在它的表达式中不能出现变量也就是说这个表达式必须是常量表达式。 10. POD类型
10.1 POD类型
POD是英文中 Plain Old Data 的缩写翻译过来就是普通的旧数据 。 POD在C中是非常重要的一个概念通常用于说明一个类型的属性尤其是用户自定义类型的属性。
POD属性在C11中往往是构建其他C概念的基础
Plain 表示是个普通的类型Old 体现了其与C的兼容性支持标准C函数
在C11中将 POD划分为两个基本概念的合集即∶平凡的trivial 和标准布局的standard layout 10.2 “平凡”类型
一个平凡的类或者结构体应该符合以下几点要求
拥有平凡的默认构造函数trivial constructor和析构函数trivial destructor。
平凡的默认构造函数就是说构造函数什么都不干。
通常情况下不定义类的构造函数编译器就会为我们生成一个平凡的默认构造函数。
// 使用默认的构造函数
class Test {};一旦定义了构造函数即使构造函数不包含参数函数体里也没有任何的代码那么该构造函数也不再是平凡的。
class Test1
{Test1(); // 我们定义的构造函数, 非默认构造
};关于析构函数也和上面列举的构造函数类似一旦被定义就不平凡了。 但是这也并非无药可救使用default关键字可以显式地声明默认的构造函数从而使得类型恢复 “平凡化”。
拥有平凡的拷贝构造函数trivial copy constructor和移动构造函数trivial move constructor。
平凡的拷贝构造函数基本上等同于使用 memcpy 进行类型的构造。同平凡的默认构造函数一样不声明拷贝构造函数的话编译器会帮程序员自动地生成。可以显式地使用 default 声明默认拷贝构造函数。而平凡移动构造函数跟平凡的拷贝构造函数类似只不过是用于移动语义。
拥有平凡的拷贝赋值运算符trivial assignment operator和移动赋值运算符trivial move operator。
这基本上与平凡的拷贝构造函数和平凡的移动构造运算符类似。
不包含虚函数以及虚基类。
类中使用 virtual 关键字修饰的函数 叫做虚函数
class Base
{
public:Base() {}virtual void print() {}
};虚基类是在创建子类的时候在继承的基类前加 virtual 关键字 修饰
语法: class 派生类名virtual 继承方式 基类名示例代码
class Base
{
public:Base() {}
};
// 子类Child虚基类Base
class Child : virtual public Base
{Child() {}
};10.3 “标准布局”类型
标准布局类型主要主要指的是类或者结构体的结构或者组合方式。
标准布局类型的类应该符合以下五点定义最重要的是前两条
所有非静态成员有相同 的访问权限publicprivateprotected。
类成员拥有不同的访问权限非标准布局类型
class Base
{
public:Base() {}int a;
protected:int b;
private:int c;
};类成员拥有相同的访问权限标准布局类型
class Base
{
public:Base() {}int a;int b;int c;
};在类或者结构体继承时满足以下两种情况之一∶
派生类中有非静态成员基类中包含静态成员或基类没有变量。基类有非静态成员而派生类没有非静态成员。
struct Base { static int a;};
struct Child: public Base{ int b;}; // ok
struct Base1 { int a;};
struct Child1: public Base1{ static int c;}; // ok
struct Child2:public Base, public Base1 { static int d;); // ok
struct Child3:public Base1{ int d;}; // error 有非静态和静态
struct Child4:public Base1, public Child // error 基类有多个非静态
{static int num;
};结论
非静态成员只要同时出现在派生类和基类间即不属于标准布局。对于多重继承一旦非静态成员出现在多个基类中即使派生类中没有非静态成员变量派生类也不属于标准布局。 子类中第一个非静态成员的类型与其基类不同。
此处基于G编译器如果使用VS的编译器和G编译器得到的结果是不一样的。
struct Parent{};
struct Child : public Parent
{Parent p; // 子类的第一个非静态成员int foo;
};上面的例子中Child不是一个标准布局类型 因为它的第一个非静态成员变量p和父类的类型相同 改成下面这样子类就变成了一个标准布局类型
struct Parent{};
struct Child1 : public Parent
{int foo; // 子类的第一个非静态成员Parent p;
};这条规则对于我们来说是比较特别的这样规定的目的主要是是节约内存提高数据的读取效率。对于上面的两个子类Child和Child1来说它们的内存结构是不一样的 在基类没有成员的情况下 C标准允许标准布局类型Child1派生类的第一个成员foo与基类共享地址 此时基类并没有占据任何的实际空间可以节省一点数据 对于子类Child而言如果子类的第一个成员仍然是基类类型 C标准要求类型相同的对象它们的地址必须不同基类地址不能和子类中的变量 p 类型相同此时需要分配额外的地址空间将二者的地址错开。 没有虚函数和虚基类。 所有非静态数据成员均符合标准布局类型其基类也符合标准布局这是一个递归的定义。
10.4 对 POD 类型的判断 如果我们想要判断某个数据类型是不是属于 POD 类型 可以使用C11给我们提供的相关函数 10.4.1 对“平凡”类型判断
C11提供的类模板叫做 is_trivial其定义如下
template class T struct std::is_trivial;std::is_trivial 的成员value 可以用于判断T的类型是否是一个平凡的类型value 函数返回值为布尔类型。 除了类和结构体外is_trivial还可以对内置的标准类型数据比如int、float都属于平凡类型及数组类型元素是平凡类型的数组总是平凡的进行判断。
关于类型的判断示例程序如下
#include iostream
#include type_traits
using namespace std;class A {};
class B { B() {} };
class C : B {};
class D { virtual void fn() {} };
class E : virtual public A { };int main()
{cout std::boolalpha; //通过这一句可以使value的输出从0/1,转换为false/truecout is_trivial: endl;cout int: is_trivialint::value endl;cout A: is_trivialA::value endl;cout B: is_trivialB::value endl;cout C: is_trivialC::value endl;cout D: is_trivialD::value endl;cout E: is_trivialE::value endl;return 0;
}输出的结果
is_trivial:
int: true
A: true
B: false
C: false
D: false
E: falseint 内置标准数据类型属于 trivial 类型A 拥有默认的构造和析构函数属于 trivial 类型B 自定义了构造函数因此不属于 trivial 类型C 基类中自定义了构造函数因此不属于 trivial 类型(继承了B,B自定义了构造函数)D 类成员函数中有虚函数因此不属于 trivial 类型E 继承关系中有虚基类因此不属于 trivial 类型 10.4.2 对“标准布局”类型的判断
同样在C11中我们可以使用模板类来帮助判断类型是否是一个标准布局的类型
其定义如下
template typename T struct std::is_standard_layout;通过 is_standard_layout模板类的成员 valueis_standard_layoutT∶∶value我们可以在代码中打印出类型的标准布局属性函数返回值为布尔类型。
示例程序:
// pod.cpp
#include iostream
#include type_traits
using namespace std;struct A { };
struct B : A { int j; };
struct C
{
public:int a;
private:int c;
};
struct D1 { static int i; };
struct D2 { int i; };
struct E1 { static int i; };
struct E2 { int i; };
struct D : public D1, public E1 { int a; };
struct E : public D1, public E2 { int a; };
struct F : public D2, public E2 { static int a; };
struct G : public A
{int foo;A a;
};
struct H : public A
{A a;int foo;
};int main()
{cout std::boolalpha;cout is_standard_layout: std::endl;cout A: is_standard_layoutA::value endl;cout B: is_standard_layoutB::value endl;cout C: is_standard_layoutC::value endl;cout D: is_standard_layoutD::value endl;cout D1: is_standard_layoutD1::value endl;cout E: is_standard_layoutE::value endl;cout F: is_standard_layoutF::value endl;cout G: is_standard_layoutG::value endl;cout H: is_standard_layoutH::value endl;return 0;
}VS2019输出的结果
is_standard_layout:
A: true
B: true
C: false
D: true
D1: true
E: false
F: falseG: false
H: falseG 编译输出的结果: 编译命令
$ g pod.cpp -stdc11输出的结果
is_standard_layout:
A: true
B: true
C: false
D: true
D1: true
E: false
F: falseG: true
H: false关于输出的结果
A 没有虚基类和虚函数属于 standard_layout 类型B 没有虚基类和虚函数属于 standard_layout 类型C 所有非静态成员访问权限不一致不属于 standard_layout 类型D 基类和子类没有同时出现非静态成员变量属于 standard_layout 类型D1 没有虚基类和虚函数属于 standard_layout 类型E 基类和子类中同时出现了非静态成员变量不属于 standard_layout 类型F 多重继承中在基类里同时出现了非静态成员变量不属于 standard_layout 类型G 使用的编译器不同得到的结果也不同。H 子类中第一个非静态成员的类型与其基类类型不能相同不属于 standard_layout 类型
10.5 总结
我们使用的很多内置类型默认都是 POD的。 POD 最为复杂的地方还是在类或者结构体的判断。 使用POD有什么好处呢
字节赋值代码中我们可以安全地使用 memset 和 memcpy 对 POD类型进行初始化和拷贝等操作。提供对C内存布局兼容。C程序可以与C 函数进行相互操作 因为POD类型的数据在C与C 间的操作总是安全的。保证了静态初始化的安全有效。静态初始化在很多时候能够提高程序的性能而POD类型的对象初始化往往更加简单。
关于 POD 重在理解,以上 11. 非受限联合体
11.1 什么是非受限联合体
联合体又叫共用体又将其称之为union它的使用方式和结构体类似可以在联合体内部定义多种不同类型的数据成员但是这些数据会共享同一块内存空间也就是如果对多个数据成员同时赋值会发生数据的覆盖。 在某些特定的场景下通过这种特殊的数据结构我们就可以实现内存的复用从而达到节省内存空间的目的。
在C11之前我们使用的联合体是有局限性的主要有以下三点
不允许联合体拥有非POD类型的成员不允许联合体拥有静态成员不允许联合体拥有引用类型的成员
在新的C11标准中取消了关于联合体对于数据成员类型的限定规定任何非引用类型都可以成为联合体的数据成员这样的联合体称之为非受限联合体Unrestricted Union
11.2 非受限联合体的使用
11.2.1 静态类型的成员
对于非受限联合体来说静态成员有两种分别是静态成员变量和静态成员函数
看一下下面代码
union Test
{int age;long id;// int tmp age; // errorstatic char c;static int print(){cout c value: c endl;return 0;}
};
char Test::c;
// char Test::c a;int main()
{Test t;Test t1;t.c b;t1.c c;t1.age 666;cout t.c: t.c endl;cout t1.c: t1.c endl;cout t1.age: t1.age endl;cout t1.id: t1.id endl;t.print();Test::print();return 0;
}执行程序输出的结果如下:
t.c: c
t1.c: c
t1.age: 666
t1.id: 666
c value: c
c value: c接下来我们逐一分析一下上面的代码: 第5行语法错误非受限联合体中不允许出现引用类型 第6行非受限联合体中的静态成员变量 需要在非受限联合体外部声明或者初始化之后才能使用通过打印的结果可以发现t和t1对象共享这个静态成员变量和类 class/struct 中的静态成员变量的使用是一样的。 第7行非受限联合体中的静态成员函数 在静态函数print()只能访问非受限联合体Test中的静态变量对于非静态成员变量age、id是无法访问的。调用这个静态方法可以通过对象也可以通过类名实现。 第24、25、26行通过打印的结果可以得出结论在非受限联合体中静态成员变量和非静态成员变量使用的不是同一块内存。
11.2.2 非POD类型成员
在 C11标准中会默认删除一些非受限联合体的默认函数。 比如非受限联合体有一个非 POD 的成员而该非 POD成员类型拥有 非平凡的构造函数那么非受限联合体的默认构造函数将被编译器删除。 其他的特殊成员函数例如默认拷贝构造函数、拷贝赋值操作符以及析构函数等也将遵从此规则。
举例说明
union Student
{int id;string name;
};int main()
{Student s;return 0;
}编译程序会看到如下的错误提示:
warning C4624: “Student”: 已将析构函数隐式定义为“已删除”
error C2280: “Student::Student(void)”: 尝试引用已删除的函数上面代码中的非受限联合体Student中拥有一个非PDO类型的成员string name string 类中有非平凡构造函数因此Student的构造函数被删除通过警告信息可以得知它的析构函数也被删除了导致对象无法被成功创建出来。 解决这个问题的办法就是由自己为非受限联合体定义构造函数 在定义构造函数的时候我们需要用到定位放置 new 操作。
placement new
一般情况下使用new申请空间时是从系统的堆heap中分配空间申请所得的空间的位置是根据当时的内存的实际使用情况决定的。 但是在某些特殊情况下可能需要在已分配的特定内存创建对象这种操作就叫做placement new即定位放置 new。
定位放置new操作的语法形式不同于普通的new操作 使用new申请内存空间Base* ptr new Base; 使用定位放置new申请内存空间
ClassName* ptr new (定位的内存地址)ClassName;我们来看下面的示例程序:
#include iostream
using namespace std;class Base
{
public:Base() {}~Base() {}void print(){cout number value: number endl;}
private:int number;
};int main()
{int n 100;Base* b new (n)Base;b-print();return 0;
}程序运行输出的结果为:
number value: 100在程序的new部分中使用定位放置的方式为指针b申请了一块内存也就是说此时指针 b 指向的内存地址和变量 n对应的内存地址是同一块栈内存 而在Base类中成员变量 number 的起始地址和Base对象的起始地址是相同的所以打印出 number 的值为100也就是整形变量 n 的值。
最后总结一下关于placement new的一些细节
使用定位放置new操作既可以在栈(stack)上生成对象也可以在堆heap上生成对象这取决于定位时指定的内存地址是在堆还是在栈上。从表面上看定位放置new操作是申请空间其本质是利用已经申请好的空间真正的申请空间的工作是在此之前完成的。使用定位放置new 创建对象时会自动调用对应类的构造函数但是由于对象的空间不会自动释放如果需要释放堆内存必须显示调用类的析构函数。使用定位放置new操作我们可以反复动态申请到同一块堆内存这样可以避免内存的重复创建销毁从而提高程序的执行效率比如网络通信中数据的接收和发送。 自定义非受限联合体构造函数
掌握了placement new的使用通过一段程序演示一下如何在非受限联合体中自定义构造函数
class Base
{
public:void setText(string str){notes str;}void print(){cout Base notes: notes endl;}
private:string notes;
};union Student
{Student(){new (name)string;}~Student() {}int id;Base tmp;string name;
};int main()
{Student s;s.name 蒙奇·D·路飞;s.tmp.setText(我是要成为海贼王的男人!);s.tmp.print();cout Student name: s.name endl;return 0;
}程序打印的结果如下
Base notes: 我是要成为海贼王的男人!
Student name: 我是要成为海贼王的男人!我们在上面的程序里边给非受限制联合体显示的指定了构造函数和析构函数 在程序的第31行 (Student s;) 需要创建一个非受限联合体对象这时便调用了联合体内部的构造函数在构造函数的第20行通过定位放置 new 的方式将构造出的对象地址定位到了联合体的成员string name的地址上了 这样联合体内部其他非静态成员也就可以访问这块地址了通过输出的结果可以看到对联合体内的tmp对象赋值会覆盖name对象中的数据。 匿名的非受限联合体 一般情况下我们使用的非受限联合体都是具名的有名字但是我们也可以定义匿名的非受限联合体一个比较实用的场景就是配合着类的定义使用。设定一个场景
进行村内人口普查人员的登记方式如下- 学生只需要登记所在学校的编号- 本村学生以外的人员需要登记其身份证号码- 本村外来人员需要登记户口所在地联系方式// 外来人口信息
struct Foreigner
{Foreigner(string s, string ph) : addr(s), phone(ph) {}string addr;string phone;
};// 登记人口信息
class Person
{
public:enum class Category : char {Student, Local, Foreign};Person(int num) : number(num), type(Category::Student) {}Person(string id) : idNum(id), type(Category::Local) {}Person(string addr, string phone) : foreign(addr, phone), type(Category::Foreign) {}~Person() {}void print(){cout Person category: (int)type endl;switch (type){case Category::Student:cout Student school number: number endl;break;case Category::Local:cout Local people ID number: idNum endl;break;case Category::Foreign:cout Foreigner address: foreign.addr , phone: foreign.phone endl;break;default:break;}}private:Category type;union{int number;string idNum;Foreigner foreign;};
};int main()
{Person p1(9527);Person p2(1101122022X);Person p3(砂隐村村北, 1301810001);p1.print();p2.print();p3.print();return 0;
}程序输出的结果
Person category: 0
Student school number: 9527
Person category: 1
Local people ID number: 1101122022X
Person category: 2
Foreigner address: 砂隐村村北, phone: 1301810001根据需求我们将木叶村的人口分为了三类并通过枚举记录了下来在Person类中添加了一个匿名的非受限联合体用来存储人口信息仔细分析之后就会发现这种处理方式的优势非常明显 尽可能地节省了内存空间。 Person类可以直接访问匿名非受限联合体内部的数据成员。 不使用匿名非受限联合体申请的内存空间等于 number、 idNum 、 foreign 三者内存之和。 使用匿名非受限联合体之后number、 idNum 、 foreign 三者共用同一块内存。 12. 强枚举类型
12.1 枚举
12.1.1 枚举的使用
枚举类型是C及C中一个基本的内置类型不过也是一个有点”奇怪”的类型。从枚举的本意上来讲就是要定义一个类别并穷举同一类别下的个体以供代码中使用。 由于枚举来源于C所以出于设计上的简单的目的枚举值常常是对应到整型数值的一些名字
比如
// 匿名枚举
enum {Red, Green, Blue};
// 有名枚举
enum Colors{Red, Green, Blue};在枚举类型中的枚举值编译器会默认从0开始赋值而后依次向下递增也就是说 Red0Green1Blue2。 12.1.2 枚举的缺陷
C/C的enum有个很”奇怪” 的设定就是具名有名字的enum类型的名字以及 enum 的成员的名字都是全局可见的 这与 C中具名的 namespace、class/struct 及 union 必须通过名字::成员名的方式访问相比是格格不入的编码过程中一不小心程序员就容易遇到问题。
比如∶
enum China {Shanghai, Dongjing, Beijing, Nanjing};
enum Japan {Dongjing, Daban, Hengbin, Fudao};上面定义的两个枚举在编译的时候编译器会报错具体信息如下
error C2365: “Dongjing”: 重定义以前的定义是“枚举数”错误的原因上面也提到了在这两个具名的枚举中Dongjing是全局可见的所有编译器就会提示其重定义了。
另外由于C中枚举被设计为常量数值的”别名”的本性所以枚举的成员总是可以被隐式地转换为整型但是很多时候我们并不想这样。 12.2 强类型枚举
12.2.1 优势
针对枚举的缺陷C11标准引入了一种新的枚举类型即枚举类又称强类型枚举strong-typed enum
声明强类型枚举非常简单只需要在 enum 后加上关键字 class。
// 定义强类型枚举
enum class Colors{Red, Green, Blue};强类型枚举具有以下几点优势∶ 强作用域强类型枚举成员的名称不会被输出到其父作用域空间。 强类型枚举只能是有名枚举如果是匿名枚举会导致枚举值无法使用因为没有作用域名称。 转换限制强类型枚举成员的值不可以与整型隐式地相互转换。 可以指定底层类型。强类型枚举默认的底层类型为 int但也可以显式地指定底层类型
具体方法为在枚举名称后面加上∶type其中 type 可以是除 wchar_t 以外的任何整型。比如:
enum class Colors :char { Red, Green, Blue };wchar_t 是什么? 双字节类型或宽字符类型是C/C的一种扩展的存储方式一般为16位或32位所能表示的字符数远超char型。 主要用在国际化程序的实现中但它不等同于 unicode 编码。unicode 编码的字符一般以wchar_t类型存储。 了解了强类型枚举的优势之后看一段程序
enum class China { Shanghai, Dongjing, Beijing, Nanjing, };
enum class Japan:char { Dongjing, Daban, Hengbin, Fudao };
int main()
{int m Shanghai; // errorint n China::Shanghai; // errorif ((int)China::Beijing 2){cout ok! endl;}cout size1: sizeof(China::Dongjing) endl;cout size2: sizeof(Japan::Dongjing) endl;return 0;
}第5行该行的代码有两处错误 强类型枚举属于强作用于类型不能直接使用枚举值前必须加枚举类型强类型枚举不会进行隐式类型转换因此枚举值不能直接给int行变量赋值虽然强类型枚举的枚举值默认就是整形但其不能作为整形使用。 第6行语法错误将强类型枚举值作为整形使用此处不会进行隐式类型转换第7行语法正确强类型枚举值在和整数比较之前做了强制类型转换。第11行打印的结果为4强类型枚举底层类型值默认为int因此占用的内存是4个字节第12行打印的结果为1显示指定了强类型枚举值的类型为char因此占用的内存大小为1个字节这样我们就可以节省更多的内存空间了。 12.2.2 对原有枚举的扩展
相比于原来的枚举强类型枚举更像是一个属于C的枚举。 但为了配合新的枚举类型C11还对原有枚举类型进行了扩展
原有枚举类型的底层类型在默认情况下仍然由编译器来具体指定实现。 但也可以跟强类型枚举类一样显式地由我们来指定。 其指定的方式跟强类型枚举一样都是枚举名称后面加上∶type
enum Colors : char { Red, Green, Blue };关于作用域在C11中枚举成员的名字除了会自动输出到父作用域也可以在枚举类型定义的作用域内有效。比如
enum Colors : char { Red, Green, Blue };
int main()
{Colors c1 Green; // C11以前的用法Colors c2 Colors::Green; // C11的扩展语法return 0;
}上面程序中第4、5行的写法都是合法的。
我们在声明强类型枚举的时候也可以使用关键字enum struct。 enum struct 和 enum class 在语法上没有任何区别enum class 的成员没有公有私有之分也不会使用模板来支持泛化的声明 。 *13. 单列模式 这并不属于C11单独涉及到的区域,只是做一个小补充 一个类只能创建一个对象即单例模式该模式可以保证系统中该类只有一个实例并提供一个访问它的全局访问点该实例被所有程序模块共享。 使用样例内存池的申请 13.1 饿汉模式
特点
一main函数前就创造对象程序启动时就创建一个唯一的实例对象
缺点 由于初始化在main函数之前这样的类数据过多会使得启动慢 多个单例类有初始化依赖关系饿汉模式无法控制类的初始化先后关系
class InfoSingleton
{
public:static InfoSingleton GetInstance(){return _sins;}void Insert(string name, int money){_info[name] money;}void Print(){for (auto kv : _info){cout kv.first kv.second endl;}}private:InfoSingleton(){}InfoSingleton(const InfoSingleton info) delete;InfoSingleton operator(const InfoSingleton info) delete;mapstring, int _info;private:static InfoSingleton _sins;
};InfoSingleton InfoSingleton::_sins;int main()
{InfoSingleton::GetInstance().Insert(张三, 1000);InfoSingleton info InfoSingleton::GetInstance();info.Insert(李四, 100);//InfoSingleton copy InfoSingleton::GetInstance(); //拷贝构造//copy.Insert(***, 10000);return 0;
}13.2 懒汉模式 如果单例对象构造十分耗时或者占用很多资源比如加载插件初始化网络连接读取文件等等而有可能该对象程序运行时不会用到那么也要在程序一开始就进行初始化就会导致程序启动时非常的缓慢。 这种情况使用懒汉模式延迟加载更好。 如果类之间存在依赖关系也可以使用懒汉模式延迟加载。 懒汉模式特点 只创建一次并且在main函数调用之后创建。 有线程安全问题C11可以解决。 饿汉模式不需要注意线程安全问题在main调用之前就已存在没有所谓的线程可以创建其他的对象。
templateclass Lock
class LockGuard
{
public:LockGuard(Lock lk):_lk(lk){_lk.lock();}~LockGuard(){_lk.unlock();}private:Lock _lk;
};class _InfoSingleton
{
public://线程安全问题,多线程一起调用创建对象static _InfoSingleton GetInstance(){//双检查增加效率if (_psins nullptr) {LockGuardmutex lock(*_smtx);if (_psins nullptr){_psins new _InfoSingleton;}}return *_psins;}void Insert(string name, int money){_info[name] money;}void Print(){for (auto kv : _info){cout kv.first kv.second endl;}}private:_InfoSingleton(){}_InfoSingleton(const _InfoSingleton info) delete;_InfoSingleton operator(const _InfoSingleton info) delete;mapstring, int _info;private:static _InfoSingleton* _psins;static mutex* _smtx;
};static _InfoSingleton* _psins nullptr;
static mutex* _smtx;懒汉模式需要注意线程安全问题所以我们在类中需要有一个唯一的锁确保判断时是串行访问
每次都先加锁再进行判断是否为空非常低效 所以我们需要双判断第一次判断是为了抛去已经创建过的节省加锁的时间第二次判断是为了创建对象使用的而锁夹在中间确保第二次的判断是串行的。
单例对象释放问题 一般而言单例类不需要释放内存因为单例出现的环境就是全局的它的目的就是陪到进程执行到最后进程结束后操作系统也会将这一部分的资源回收。 如果有一定要求我们可以手写出析构