C++ 学习笔记6

cpp_6

一般 C++ 11 语法

long long 类型

新增了类型long long和unsigned long long,以支持64位(或更宽)的整型。

在VS中,int和long都是4字节,long long是8字节。

在Linux中,int是4字节,long和long long是8字节。

char16_t和char32_t类型

用的少。

新增了类型char16_t和char32_t,以支持16位和32位的字符。

原始字面量

R("12345")

统一的初始化(列表)

也就是所有变量都可以使用大括号进行初始化了。

C++11丰富了大括号的使用范围,用大括号括起来的列表(统一的初始化列表)可以用于所有内置类型和用户自定义类型。使用统一的初始化列表时,可以添加等号(=),也可以不添加:

1
2
3
int x={5};
double y{2.75};
short quar[5]{4,5,2,76,1};

统一的初始化列表也可以用于new表达式中:

1
int *ar=new int[4]{2,4,6,7};

创建对象时,也可以使用大括号(而不是圆括号)来调用构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
class Girl
{
private:
int m_bh;
string m_name;
public:
Girl(int bh,string name) : m_bh(bh),m_name(name) {}
};

Girl g1(3, "西施"); // C++98的风格。
Girl g2={5, "冰冰"}; // C++11的风格。
Girl g3{8, "幂幂"}; // C++11的风格。

initializer_list

STL容器提供了将initializer_list模板类作为参数的构造函数:

1
2
3
vector<int> v1(10);   // 把v1初始化为10个元素。
vector<int> v2{10}; // 把v2初始化为1个元素,这个元素的值是10。
vector<int> v2{3,5,8}; // 把v3初始化为3个元素,值分别是3、5、8。

头文件<initializer_list>提供了对模板类initializer_list的支持,这个类包含成员函数begin()和end()。除了用于构造函数外,还可以将initializer_list用于常规函数的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <initializer_list>

using std::cout;
using std::endl;

int addsum(std::initializer_list<int> i)
{
int sum = 0;
for(auto it = i.begin(); it != i.end(); it++)
{
sum = sum + *it; // 不能用 +=,应该是没重载。
}
return sum;
}

int main()
{
cout << addsum({1,2,3,4,5}) << endl;
return 0;
}

模板别名

pi2nJt1.png

空指针 nullptr

更安全。只表示空指针,即指针类型,不表示 0,不指向有效数据。

空指针是不会指向有效数据的指针。以前,C/C++用0表示空指针,这带来了一些问题,这样的话0既可以表示指针常量,又可以表示整型常量。

C++11新增了关键字nullptr,用于表示空指针;它是指针类型,不是整型类型。

为了向后兼容,C++11仍允许用0来表示空指针,因此表达式nullptr==0为true。

使用nullptr提供了更高的类型安全。例如,可以将0传递给形参为int的函数,但是,如果将nullptr传递给这样的函数,编译器将视为错误。

因此,出于清晰和安全考虑,请使用nullptr。

枚举类(强类型枚举)

传统的C++枚举提供了一种创建常量的方式,但类型检查比较低级。还有,如果在同一作用域内定义的两个枚举,它们的成员不能同名。

针对枚举的缺陷,C++11 标准引入了枚举类,又称强类型枚举。

声明强类型枚举非常简单,只需要在enum后加上关键字 class。

例如∶

1
2
3
enum e1{ red, green };
enum class e2 { red, green, blue };
enum class e3 { red, green, blue, yellow };

使用强类型枚举时,要在枚举成员名前面加枚举名和::,以免发生名称冲突,如:e2::rede3::blue

强类型枚举默认的类型为int,也可以显式地指定类型,具体做法是在枚举名后面加上:type,type可以是除wchar_t以外的任何整型。

例如:

1
enum class e2:char { red, green, blue };

explicit

C++支持对象自动转换,但是,自动类型转换可能导致意外。为了解决这种问题,C++11引入了explicit关键字,用于关闭自动转换的特性。

类内成员初始化

在类的定义中初始化成员变量。

1
2
3
4
5
6
7
8
9
class Girl
{
private:
int m_bh=20; // 年龄。
string m_name="美女"; // 姓名。
char m_xb = 'X'; // 性别。
public:
Girl(int bh, string name) : m_bh(bh), m_name(name) {}
};

final

final关键字用于限制某个类不能被继承,或者某个虚函数不能被重写。

final关键字放在类名或虚函数名的后面。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class AA
{
public:
virtual void test()
{
cout << "AA class...";
}
};

class BB : public AA
{
public:
void test() final // 如果有其它类继承BB,test()方法将不允许重写。
{
cout << "BB class...";
}
};

class CC : public BB
{
public:
void test() // 错误,BB类中的test()后面有final,不允许重写。
{
cout << "CC class...";
}
};

override

在派生类中,把override放在成员函数的后面,表示重写基类的虚函数,提高代码的可读性。

在派生类中,如果某成员函数不是重写基类的虚函数,随意的加上override关键字,编译器会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AA 
{
public:
virtual void test()
{
cout << "AA class...";
}
};

class BB : public AA
{
public:
void test() override
{
cout << "BB class...";
}
};

数值类型和字符串之间的转换

传统方法用sprintf()和snprintf()函数把数值转换为char*字符串;用atoi()、atol()、atof()把char*字符串转换为数值。

C++11提供了新的方法,在数值类型和string字符串之间转换。

  1. 数值转换为字符串

    使用to_string()函数可以将各种数值类型转换为string字符串类型,这是一个重载函数,在头文件 <string>中声明,函数原型如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    string to_string (int val);
    string to_string (long val);
    string to_string (long long val);
    string to_string (unsigned val);
    string to_string (unsigned long val);
    string to_string (unsigned long long val);
    string to_string (float val);
    string to_string (double val);
    string to_string (long double val);
  2. 字符转换为串数值

    在C++中,数值类型包括整型和浮点型,针对于不同的数值类型提供了不同的函数在头文件 <string>中声明,函数原型如下:

    1
    2
    3
    4
    5
    6
    7
    8
    int                 stoi( const string& str, size_t* pos = nullptr, int base = 10 );
    long stol( const string& str, size_t* pos = nullptr, int base = 10 );
    long long stoll( const string& str, size_t* pos = nullptr, int base = 10 );
    unsigned long stoul( const string& str, size_t* pos = nullptr, int base = 10 );
    unsigned long long stoull( const string& str, size_t* pos = nullptr, int base = 10 );
    float stof( const string& str, size_t* pos = nullptr );
    double stod( const string& str, size_t* pos = nullptr );
    long double stold( const string& str, size_t* pos = nullptr );

    形参说明:

    str:需要要转换的string字符串。

    pos:传出参数,存放从哪个字符开始无法继续解析的位置,例如:123a45, 传出的位置将为3。

    base:若base为0,则自动检测数值进制:若前缀为0,则为八进制,若前缀为0x或0X,则为十六进制,否则为十进制。

    注意:string字符串转换为数值的函数可能会抛出异常

constexpr

const 有两种语义:只读变量(函数参数中)、修饰常量。

constexpr 只表示常量,const 用于表示只读。

C++11 标准中,建议将const和constexpr的功能区分开,表达“只读”语义的场景用const,表达“常量”语义的场景用constexpr。

默认函数控制=default与=delete

在C++中自定义的类,编译器会默认生成一些成员函数:

  • 无参构造函数
  • 拷贝构造函数
  • 拷贝赋值函数
  • 移动构造函数
  • 移动赋值函数
  • 析构函数

=default表示启用默认函数。

=delete表示禁用默认函数。

基本常用语法

auto、decltype、尾置返回类型、智能指针、异常规范、范围 for 循环、新 STL 容器(array、forward_list、unordered_map、unordered_multimap、unordered_set、unordered_multiset(哈希表))、新 STL 成员函数、emplace、移动构造、移动赋值、嵌套模板的尖括号空格、静态断言static_assert、

深入 C++ 11 语法

委托构造

委托构造就是同一个类中,某个构造函数在初始化列表中使用了本类的其它构造函数进行初始化,用于简化代码。

当一个构造函数在初始化列表使用了委托构造,那么就不能同时初始化变量。因为,用类创建对象的时候,初始化只能有一次。

注意:

  • 不要生成环状的构造过程。
  • 一旦使用委托构造,就不能在初始化列表中初始化其它的成员变量。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
using namespace std;

class AA
{
private:
int m_a;
int m_b;
double m_c;
public:
// 有一个参数的构造函数,初始化m_c
AA(double c) {
m_c = c + 3; // 初始化m_c
cout << " AA(double c)" << endl;
}
// 有两个参数的构造函数,初始化m_a和m_b
AA(int a, int b) {
m_a = a + 1; // 初始化m_a
m_b = b + 2; // 初始化m_b
cout << " AA(int a, int b)" << endl;
}
// 构造函数委托AA(int a, int b)初始化m_a和m_b
AA(int a, int b, const string& str) : AA(a, b) {
cout << "m_a=" << m_a << ",m_b=" << m_b << ",str=" << str << endl;
}
// 构造函数委托AA(double c)初始化m_c
AA(double c, const string& str) : AA(c) {
cout << "m_c=" << m_c << ",str=" << str << endl;
}
};

int main()
{
AA a1(10, 20, "我是一只傻傻鸟。");

AA a2(3.8, "我有一只小小鸟。");
}

继承构造

C++11 继承构造(Inheriting Constructor),在派生类中使用using来声明继承基类的构造函数。

C++11之前,派生类如果要使用基类的构造函数,可以在派生类构造函数的初始化列表中指定。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
using namespace std;

class AA // 基类。
{
public:
int m_a;
int m_b;
// 有一个参数的构造函数,初始化m_a
AA(int a) : m_a(a) { cout << " AA(int a)" << endl; }
// 有两个参数的构造函数,初始化m_a和m_b
AA(int a, int b) : m_a(a), m_b(b) { cout << " AA(int a, int b)" << endl; }
};

class BB :public AA // 派生类。
{
public:
double m_c;
using AA::AA; // 使用基类的构造函数。
// 有三个参数的构造函数,调用A(a,b)初始化m_a和m_b,同时初始化m_c
BB(int a, int b, double c) : AA(a, b), m_c(c) {
cout << " BB(int a, int b, double c)" << endl;
}
void show() { cout << "m_a=" << m_a << ",m_b=" << m_b << ",m_c=" << m_c << endl; }
};

int main()
{
// 将使用基类有一个参数的构造函数,初始化m_a
BB b1(10);
b1.show();

// 将使用基类有两个参数的构造函数,初始化m_a和m_b
BB b2(10,20);
b2.show();

// 将使用派生类自己有三个参数的构造函数,调用A(a,b)初始化m_a和m_b,同时初始化m_c
BB b3(10,20,10.58);
b3.show();
}

lambda 表达式

lambda函数是C++11标准新增的语法糖,也称为 lambda 表达式或匿名函数。

lambda函数的特点是:距离近、简洁、高效和功能强大。

注意点:

  1. 使用 & 引用捕获的变量,在调用 lambda 函数后外部也会被修改。
  2. 使用值捕获的变量,同时使用 mutable 关键字,表示仅能在 lambda 函数体内修改变量,函数体外变量不会被修改。

语法及示例:

[](const int& no) mutable/noexcept -> void { cout << no << "号" << endl; };

piR1zTS.png

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>

using std::cout;
using std::endl;

int main()
{
int num = 0;
int num2 = 1;

auto f = [&]() mutable noexcept -> void { // 这里的mutable因为是引用捕获,所以没什么用
num = 4;
num2 = 5;
// throw "exception";
cout << num << " " << num2 << endl; // 4 5
};

// try
// {
// f();
// }
// catch(...)
// {
// cout << "处理完毕..." << endl;
// }

f(); // 需要手动调用。

cout << num << " " << num2 << endl; // 4 5

return 0;
}

捕获列表

通过捕获列表,lambda函数可以访问父作用域中的非静态局部变量(静态局部变量可以直接访问,不能访问全局变量)。

piR3eTU.png

  1. 值捕获

    与传递参数类似,采用值捕获的前提是变量可以拷贝。

    与传递参数不同,变量的值是在lambda函数创建时拷贝,而不是调用时拷贝。

    由于被捕获的值是在lambda函数创建时拷贝,因此在随后对其修改不会影响到lambda内部的值。

    1
    2
    3
    4
    size_t v1 = 42;
    auto f = [ v1 ] { return v1; }; // 使用了值捕获,将v1拷贝到名为f的可调用对象。
    v1 = 0;
    auto j = f(); // j为42,f保存了我们创建它是v1的拷贝。
  2. 引用捕获
    和函数引用参数一样,引用变量的值在lambda函数体中改变时,将影响被引用的对象。
    如果采用引用方式捕获变量,就必须保证被引用的对象在lambda执行的时候是存在的。

  3. 隐式捕获

    让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。
    隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。

  4. 混合方式捕获

    lambda函数还支持混合方式捕获,即同时使用显式捕获和隐式捕获。

    混合捕获时,捕获列表中的第一个元素必须是 = 或 &,此符号指定了默认捕获的方式是值捕获或引用捕获。
    显式捕获的变量必须使用和默认捕获不同的方式捕获。

    1
    2
    3
    4
    5
    int i = 10;
    int j = 20;
    auto f1 = [ =, &i] () { return j + i; }; // 正确,默认值捕获,显式是引用捕获
    auto f2 = [ =, i] () { return i + j; }; // 编译出错,默认值捕获,显式值捕获,冲突了
    auto f3 = [ &, &i] () { return i +j; }; // 编译出错,默认引用捕获,显式引用捕获,冲突了
  5. 修改值捕获变量的值
    当使用传值捕获变量时,不能在 lambda 函数体内部修改值,但是可以通过指定 mutable 关键字使得能在函数体内部修改值,但是外部变量的值仍然不会被修改。

  6. 异常说明
    lambda可以抛出异常,用throw(…)指示异常的类型,用noexcept指示不抛出任何异常。

与普通函数的不同点

  • lambda函数不能有默认参数。
  • 所有参数必须有参数名。
  • 不支持可变参数。

lambda 函数本质

当编写了一个lambda函数之后,编译器将它翻译成一个类,该类中有一个重载了()的函数。

  1. 采用值捕获
    采用值捕获时,lambda函数生成的类用捕获变量的值初始化自己的成员变量。
    默认情况下,由lambda函数生成的类是const成员函数,所以变量的值不能修改。如果加上mutable,相当于去掉const。这样上面的限制就能讲通了。

  2. 采用引用捕获

    如果lambda函数采用引用捕获的方式,编译器直接引用就行了。

    唯一需要注意的是,lambda函数执行时,程序必须保证引用的对象有效。

左值和右值、左值引用和右值引用

左值和右值

在C++中,所有的值不是左值,就是右值。 左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不再存在的临时对象。

右值有了名字(右值引用)就变成了左值,可以像普通变量(左值)一样使用。

区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值

示例:

1
2
3
4
5
6
7
8
9
10
11
12
class AA {
int m_a;
};

AA getTemp()
{
return AA();
}

int ii = 3; // ii是左值,3是右值。
int jj = ii+8; // jj是左值,ii+8是右值。
AA aa = getTemp(); // aa是左值 ,getTemp()的返回值是右值(临时变量)。
右值(C11)
  • 纯右值

    1. 非引用返回的临时变量
    2. 运算表达式产生的结果
    3. 字面常量(C风格字符串除外,它是地址)。
  • 将亡值

    与右值引用相关的表达式。例如:将要被移动的对象、T&&函数返回的值、std::move()的返回值、转换成T&&的类型的转换函数的返回值。

左值引用和右值引用(C11)

引入右值引用的主要目的是实现移动语义。

右值引用就是给右值取个名字。

临时值等右值通过右值引用获得新生,使得右值将与右值引用类型变量的生命周期相同,只要右值引用变量还存活,该右值临时变量就会一直存在。

左值引用只能绑定(关联、指向)左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。

语法:

1
数据类型&& 变量名=右值;

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

class AA {
public:
int m_a=9;
};

AA getTemp()
{
return AA();
}

int main()
{
int&& a = 3; // 3是右值。

int b = 8; // b是左值。
int&& c = b + 5; // b+5是右值。

AA&& aa = getTemp(); // getTemp()的返回值是右值(临时变量)。

cout << "a=" << a << endl;
cout << "c=" << c << endl;
cout << "aa.m_a=" << aa.m_a << endl;
}

getTemp()的返回值本来在表达式语句结束后其生命也就该终结了(因为是临时变量),而通过右值引用重获了新生,其生命周期将与右值引用类型变量aa的生命周期一样,只要aa还活着,该右值临时变量将会一直存活下去。

常量左值引用

常量左值引用可以算是一个万能的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。

如函数中的参数定义为常量左值引用,就可以接受字面常量值,也就是右值。

示例:

1
2
3
4
5
6
7
int a = 1;        
const int& ra = a; // a是非常量左值。

const int b = 1;
const int& rb = b; // b是常量左值。

const int& rc = 1; // 1是右值。

总结

T是一个具体类型:

  1. 左值引用, 使用 T&, 只能绑定左值。

  2. 右值引用, 使用 T&&, 只能绑定右值。

  3. 已命名的右值引用是左值。

  4. 常量左值,使用 const T&,既可以绑定左值又可以绑定右值。

移动语义

转移资源的操作叫移动语义,也就是避免深拷贝的复制操作,而是直接将原有资源转移到现有资源。

如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。

深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++11新增加的移动语义就能够做到这一点。

实现移动语义要增加两个函数:移动构造函数和移动赋值函数。

移动构造函数的语法:

1
类名(类名&& 源对象){......}

移动赋值函数的语法:

1
类名& operator=(类名&& 源对象){……}

注意点:

  1. 对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?
    • C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。
    • 左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
  2. 如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数。
  3. C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
  4. 移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>
using namespace std;

class AA
{
public:
int* m_data = nullptr; // 数据成员,指向堆区资源的指针。

AA() = default; // 启用默认构造函数。

void alloc() { // 给数据成员m_data分配内存。
m_data = new int; // 分配内存。
memset(m_data, 0, sizeof(int)); // 初始化已分配的内存。
}

AA(const AA& a) { // 拷贝构造函数。
cout << "调用了拷贝构造函数。\n"; // 显示自己被调用的日志。
if (m_data == nullptr) alloc(); // 如果没有分配内存,就分配。
memcpy(m_data, a.m_data, sizeof(int)); // 把数据从源对象中拷贝过来。
}

AA(AA&& a) { // 移动构造函数。
cout << "调用了移动构造函数。\n"; // 显示自己被调用的日志。
if (m_data != nullptr) delete m_data; // 如果已分配内存,先释放掉。
m_data = a.m_data; // 把资源从源对象中转移过来。
a.m_data = nullptr; // 把源对象中的指针置空。
}

AA& operator=(const AA& a) { // 赋值函数。
cout << "调用了赋值函数。\n"; // 显示自己被调用的日志。
if (this == &a) return *this; // 避免自我赋值。
if (m_data == nullptr) alloc(); // 如果没有分配内存,就分配。
memcpy(m_data, a.m_data, sizeof(int)); // 把数据从源对象中拷贝过来。
return *this;
}

AA& operator=(AA&& a) { // 移动赋值函数。
cout << "调用了移动赋值函数。\n"; // 显示自己被调用的日志。
if (this == &a) return *this; // 避免自我赋值。
if (m_data != nullptr) delete m_data; // 如果已分配内存,先释放掉。
m_data = a.m_data; // 把资源从源对象中转移过来。
a.m_data = nullptr; // 把源对象中的指针置空。
return *this;
}

~AA() { // 析构函数。
if (m_data != nullptr) {
delete m_data; m_data = nullptr;
}
}
};

int main()
{
AA a1; // 创建对象a1。
a1.alloc(); // 分配堆区资源。
*a1.m_data = 3; // 给堆区内存赋值。
cout << "a1.m_data=" << *a1.m_data << endl;

AA a2 = a1; // 将调用拷贝构造函数。
cout << "a2.m_data=" << *a2.m_data << endl;

AA a3;
a3 = a1; // 将调用赋值函数。
cout << "a3.m_data=" << *a3.m_data << endl;

auto f = [] { AA aa; aa.alloc(); *aa.m_data = 8; return aa; }; // 返回AA类对象的lambda函数。
AA a4 = f(); // lambda函数返回临时对象,是右值,将调用移动构造函数。
cout << "a4.m_data=" << *a4.m_data << endl;

AA a6;
a6 = f(); // lambda函数返回临时对象,是右值,将调用移动赋值函数。
cout << "a6.m_data=" << *a6.m_data << endl;
}

完美转发

模板中,用于保持传入函数的参数的左、右值属性以及 const/volatile 限定符不变。

在函数模板中,可以将参数“完美”的转发给其它函数。所谓完美,即不仅能准确的转发参数的值,还能保证被转发参数的左、右值属性不变。

为了支持完美转发,C++11提供了以下方案(下面两种结合起来):

  1. 如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名,那么,函数既可以接受左值引用,又可以接受右值引用。

  2. 提供了模板函数std::forward<T>(参数) ,用于转发参数,如果 参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

void func1(int& ii) { // 如果参数是左值,调用此函数。
cout << "参数是左值=" << ii << endl;
}

void func1(int&& ii) { // 如果参数是右值,调用此函数。
cout << "参数是右值=" << ii << endl;
}

// 1)如果模板中(包括类模板和函数模板)函数的参数书写成为T&& 参数名,
// 那么,函数既可以接受左值引用,又可以接受右值引用。
// 2)提供了模板函数std::forward<T>(参数) ,用于转发参数,
// 如果参数是一个右值,转发之后仍是右值引用;如果 参数是一个左值,转发之后仍是左值引用。
template<typename TT>
void func(TT&& ii)
{
func1(forward<TT>(ii));
}

int main()
{
int ii = 3;
func(ii); // 实参是左值。
func(8); // 实参是右值。
}

可变参数模板

可变参数模版是C++11新增的最强大的特性之一,它对参数进行了泛化,能支持任意个数、任意数据类型的参数。

使用方式:

  1. 定义可变参数模板(同时要有一个同名同返回类型的无参数的用于递归终止的函数)

    这里的第一个模板参数 T first 是第二个可变模板参数每次展开出来的参数。
    参数在全部展开之后会调用递归终止函数(实现可以为空,但必须要有)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 基本情况
    void printArgs() {}

    // 递归展开
    template <typename T, typename... Args>
    void printArgs(T first, Args... rest) {
    std::cout << first << " ";
    printArgs(rest...);
    }

    // 调用
    printArgs(1, 2.5, "Hello"); // 输出: 1 2.5 Hello
  2. 递归展开实现通用函数

    在定义一个递归展开参数的模板函数后,再额外定义一个模板参数为可变参数,但是在函数参数中添加指定类型的参数。如下:

    1
    2
    3
    4
    5
    6
    7
    template <typename...Args>
    void func(const string& str, Args...args) // 除了可变参数,还可以有其它常规参数。
    {
    cout << str << endl; // 表白之前,喊句口号。
    print(args...); // 展开可变参数包。
    cout << "表白完成。\n";
    }

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <thread>
using namespace std;

template <typename T>
void show(T girl) // 向超女表白的函数,参数可能是超女编号,也可能是姓名,所以用T。
{
cout << "亲爱的" << girl << ",我是一只傻傻鸟。\n";
}

// 递归终止时调用的非模板函数,函数名要与展开参数包的递归函数模板相同。
void print()
{
cout << "递归终止。\n";
}

// 展开参数包的递归函数模板。
template <typename T, typename ...Args>
void print(T arg, Args... args)
{
//cout << "参数: " << arg << endl; // 显示本次展开的参数。

show(arg); // 把参数用于表白。

//cout << "还有" << sizeof...(args) << "个参数未展开。" << endl; // 显示未展开变参的个数。

print(args...); // 继续展开参数。
}

template <typename...Args>
void func(const string& str, Args...args) // 除了可变参数,还可以有其它常规参数。
{
cout << str << endl; // 表白之前,喊句口号。

print(args...); // 展开可变参数包。

cout << "表白完成。\n";
}

int main(void)
{
//print("金莲", 4, "西施");
//print("冰冰", 8, "西施", 3);
func("我是绝世帅歌。", "冰冰", 8, "西施", 3); // "我是绝世帅歌。"不是可变参数,其它的都是。
}

时间操作 chrono 库

C++11提供了chrono模版库,实现了一系列时间相关的操作(时间长度、系统时间和计时器)。

头文件:#include <chrono>

命名空间:std::chrono

时间长度

duration模板类用于表示一段时间(时间长度、时钟周期),如:1小时、8分钟、5秒。

duration的定义如下:

1
2
3
4
5
template<class Rep, class Period = std::ratio<1, 1>>
class duration
{
……
};

为了方便使用,定义了一些常用的时间长度,比如:时、分、秒、毫秒、微秒、纳秒,它们都位于std::chrono命名空间下,定义如下:

时间长度可以加减时间,以秒为单位。

duration模板类重载了各种算术运算符,用于操作duration对象。

duration模板类提供了count()方法,获取duration对象的值(int 类型)。

1
2
3
4
5
6
using hours			= duration<Rep, std::ratio<3600>>	// 小时
using minutes = duration<Rep, std::ratio<60>> // 分钟
using seconds = duration<Rep> // 秒
using milliseconds = duration<Rep, std::milli> // 毫秒
using microseconds = duration<Rep, std::micro> // 微秒
using nanoseconds = duration<Rep, std::nano> // 纳秒

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <chrono> // chrono库的头文件。
using namespace std;

int main()
{
chrono::hours t1(1); // 1小时
chrono::minutes t2(60); // 60分钟
chrono::seconds t3(60 * 60); // 60*60秒
chrono::milliseconds t4(60 * 60 * 1000); // 60*60*1000毫秒
chrono::microseconds t5(60 * 60 * 1000 * 1000); // 警告:整数溢出。
chrono::nanoseconds t6(60 * 60 * 1000 * 1000*1000); // 警告:整数溢出。

if (t1 == t2) cout << "t1==t2\n";
if (t1 == t3) cout << "t1==t3\n";
if (t1 == t4) cout << "t1==t4\n";

// 获取时钟周期的值,返回的是int整数。
cout << "t1=" << t1.count() << endl;
cout << "t2=" << t2.count() << endl;
cout << "t3=" << t3.count() << endl;
cout << "t4=" << t4.count() << endl;

chrono::seconds t7(1); // 1秒
chrono::milliseconds t8(1000); // 1000毫秒
chrono::microseconds t9(1000 * 1000); // 1000*1000微秒
chrono::nanoseconds t10(1000 * 1000 * 1000); // 1000*1000*1000纳秒

if (t7 == t8) cout << "t7==t8\n";
if (t7 == t9) cout << "t7==t9\n";
if (t7 == t10) cout << "t7==t10\n";

// 获取时钟周期的值。
cout << "t7=" << t7.count() << endl;
cout << "t8=" << t8.count() << endl;
cout << "t9=" << t9.count() << endl;
cout << "t10=" << t10.count() << endl;
}

系统时间

system_clock 类支持了对系统时钟的访问,提供了三个静态成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 返回当前时间的时间点。
static std::chrono::time_point<std::chrono::system_clock> now() noexcept;

// 将时间点time_point类型转换为std::time_t 类型,UTC 时间。
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;
// 返回当前时间的时间点。
static std::chrono::time_point<std::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;

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#define _CRT_SECURE_NO_WARNINGS  // localtime()需要这个宏。
#include <iostream>
#include <chrono>
#include <iomanip> // put_time()函数需要包含的头文件。
#include <sstream>
using namespace std;

int main()
{
// 1)静态成员函数chrono::system_clock::now()用于获取系统时间。(C++时间)
auto now = chrono::system_clock::now();

// 2)静态成员函数chrono::system_clock::to_time_t()把系统时间转换为time_t。(UTC时间)
auto t_now = chrono::system_clock::to_time_t(now);

// t_now = t_now + 24*60*60; // 把当前时间加1天。
// t_now = t_now + -1*60*60; // 把当前时间减1小时。
// t_now = t_now + 120; // 把当前时间加120秒。

// 3)std::localtime()函数把time_t转换成本地时间。(北京时)
// localtime()不是线程安全的,VS用localtime_s()代替,Linux用localtime_r()代替。
auto tm_now = std::localtime(&t_now);

// 4)格式化输出tm结构体中的成员。
std::cout << std::put_time(tm_now, "%Y-%m-%d %H:%M:%S") << std::endl;
std::cout << std::put_time(tm_now, "%Y-%m-%d") << std::endl;
std::cout << std::put_time(tm_now, "%H:%M:%S") << std::endl;
std::cout << std::put_time(tm_now, "%Y%m%d%H%M%S") << std::endl;

stringstream ss; // 创建stringstream对象ss,需要包含<sstream>头文件。
ss << std::put_time(tm_now, "%Y-%m-%d %H:%M:%S"); // 把时间输出到对象ss中。
string timestr = ss.str(); // 把ss转换成string的对象。
cout << timestr << endl;
}

计时器

steady_clock类相当于秒表,操作系统只要启动就会进行时间的累加,常用于耗时的统计(精确到纳秒)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <chrono>
using namespace std;

int main()
{
// 静态成员函数chrono::steady_clock::now()获取开始的时间点。
auto start = chrono::steady_clock::now();

// 执行一些代码,让它消耗一些时间。
cout << "计时开始 ...... \n";
for (int ii = 0; ii < 1000000; ii++) {
// cout << "我是一只傻傻鸟。\n";
}
cout << "计时完成 ...... \n";

// 静态成员函数chrono::steady_clock::now()获取结束的时间点。
auto end = chrono::steady_clock::now();

// 计算消耗的时间,单位是纳秒。
auto dt = end - start;
cout << "耗时: " << dt.count() << "纳秒("<<(double)dt.count()/(1000*1000*1000)<<"秒)";
}