C++11新特性

一、语言核心

1.列表初始化

  基础用法

    int i1{10};
    int i2{3+4};
    int a[]{1,2,3,4,5};
    int *pi = new int[3]{10,20,30};
    std::string s1{"Hello C++11"};
    std::string s2{'a','b','c'}; // s2: abc
    std::cout << s2 << std::endl;
    std::vector<int> v1{0,2,4};
    const std::vector<int> &v2{1,3,5}; // 引用常量必须加const
    std::map<std::string,int> m{{"John",25},{"Mark",30},{"Angel",18}};

使自定义类也支持列表初始化 

标准模板库中的容器支持列表初始化语法是因为其定义了使用类模板initializer_list<T>作为参数的构造函数

string (initializer_list<char> il);

vector (initializer_list<value_type> il, const allocator_type& alloc = allocator_type());

map (initializer_list<value_type> il, const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type());

所以自定义类要想支持列表初始化只需定义initializer_list<T>为参数的构造函数即可

#include <map>
#include <iostream>
enum Gender
{
    kBoy,
    kGirl
};
class People
{
public:
    People(std::initializer_list<std::pair<std::string, Gender>> il)
    {
        for (auto &it : il)
            data[it.first] = it.second;
    }
    void print()
    {
        for (auto &it : data)
            std::cout << it.first << ":" << it.second << std::endl;
    }
private:
    std::map<std::string, Gender> data;
};

int main(void)
{
    People group{{"Jack",kBoy},{"Alan",kBoy},{"Ella",kGirl}};
    group.print();
    return 0;
}

 使用列表初始化语法传参或返回值

#include <iostream>
void Fun1(std::initializer_list<int> il)
{
    for (auto& it : il)
        std::cout << it << std::endl;
}
std::vector<int> Fun2(void)
{
    return {11,22,33};
}

int main(void)
{
    Fun1({2,3,7});
    auto v3 = Fun2();
    return 0;
}

// 重载[]操作符,使用列表作索引
// MyClass& MyClass::operator[](std::initializer_list<int> il);
// MyClass obj;
// obj[{1,2,3}];

列表初始化还有一个作用就是防止类型收窄。类型收窄是指使数据变化或精度丢失的一些隐式类型转换,比如

a. 浮点数和整数的互相转换

b. 整型转换为较低长度的整型

const int x{1024};
const int y{10};

char c1{x}; // 编译报错
char c2{y}; // 编译通过

unsigned char c3{-1}; // 编译报错
float f1{7}; // 编译通过
float f2{1.2l}; // 编译通过
int i{2.0f}; // 编译报错

2.引用和指针

引用是给对象起了一个别名,引用本身不是对象!所以引用本身不占内存空间,对引用取地址就是取其绑定的对象的地址。

一般在初始化变量时,初始值会被拷贝到新建的对象中。但是定义引用时,程序会把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。在初始化之后就无法令引用重新绑定到其他对象上,因此引用必须初始化。

定义引用之后,对其进行的所有操作都是作用在与之绑定的对象上

a.对引用赋值,实际上是把值赋给了与引用绑定的对象

b.获取引用的值,实际上是获取了与引用绑定的对象的值

c.以引用作为初始值,实际上是以引用绑定的对象作为初始值

int ival = 1024;
int &refVal = ival;
refVal = 2;            // 把2赋值给refVal指向的对象即ival, ival = 2
int ii = refVal;       // 用与refVal绑定的对象的值初始化变量ii, ii = 2
int &refVal2 = refVal  // 把revVal2也绑定到refVal绑定的对象上即ival
refVal2 = 0;           // ival = 0

引用的类型必须要与绑定的对象类型严格匹配,除了以下两种例外情况

1.const引用(即常量引用)可以绑定到非常量对象上

2.基类引用可以绑定到子类对象上(多态)

默认状态下,const对象只在本文件内有效(文件作用域)

a.当多个文件同时定义一个同名的const变量时,并不会报重复定义的错误。等同于在不同文件分别定义了独立的变量

b.如果想在不同文件共享同一个const变量,那么就要在定义和声明中都加上extern

// file.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file.h声明该变量,可以声明多次
extern const int bufSize;

const引用

const引用既可以绑定到普通对象(左值)也可以绑定到const对象、字面值、临时量(对象),比如表达式、函数返回值、类型转换都有临时量(对象)的创建

int i = 42;
const int j = 50;
const int &r1 = i;      // 将const引用绑定到普通对象上
const int &r2 = j;      // 将const引用绑定到常量对象上
const int &r3 = 42;     // 将const引用绑定到字面值上
const int &r4 = r1 * 2; // 将const引用绑定到临时量上(表达式)
double d = 3.14;
const int &r5 = d; // 将const引用绑定到临时量上,该语句等同于如下代码:
// const int temp = d;
// const int &r5 = temp;

 指针的引用

引用本身不是一个对象,所以不能定义指向引用的指针

int i = 42;
int &* pi = &i; // 错误

但指针是对象,所以存在对指针的引用

int i = 42;
int *p;
int *&r = p; // 正确
r = &i; // 给r赋值就是给p赋值,所以p指向了i
*r = 0; // r解引用就是p解引用,所以i的值改为了0

常量指针

指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身设为常量。常量指针必须初始化,且它的值不能再被改变。

int errNumb = 0;
int *const curErr = &errNumb; // curErr不能再指向其他对象

指针本身是不是常量和指针所指的对象是不是常量是两个独立的问题

顶层const表示指针本身是一个常量

底层const表示指针指向的对象是一个常量

更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等复合类型的基本类型部分有关。指针比较特殊,它既可以是顶层const,又可以是底层const

int i = 0;
int *const p1 = &i; // 顶层const
const int ci = 42;  // 顶层const
const int *p2 = &ci;// 底层const
const int *const p3 = p2; //靠右是顶层const,靠左是底层const
const int &r1 = ci; // 用于声明引用的都是底层const,引用不是对象所以不存在引用的顶层const

// 对象拷贝时,顶层const不受影响
i = ci;
p2 = p3

// 对象拷贝时,底层const的限制就不能忽视
int *p = p3; // 错误;p3包含底层const的定义,而p没有
p2 = &i;     // 正确;int*能转成const int*
int &r2 = ci;// 错误;普通的int&不能绑定到int常量上
const int &r3 = i; // 正确;const int&可以绑定到一个int对象上

3.auto与引用

编程时常常需要把表达式的值赋给变量,这就需要在声明变量的时候清楚地知道表达式的类型。然而这一点有时却并不容易做到,C++11引入了auto类型说明符,用它就能让编译器为我们分析表达式所属的类型。编译器是通过初始值来推导变量的类型的,因此auto定义的变量必须有初始值

使用auto也可以在一条语句中声明多个变量,但是该语句中所有变量的类型都必须一样

auto i = 0, *p = &i;    // 正确
auto sz = 0, pi = 3.14; // 错误,sz和pi的类型不一致

编译器推导出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当的改变结果类型使其更符合初始化规则

首先,使用引用就是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值,此时编译器以引用对象的类型作为auto的类型

int i = 0, &r = i;
auto a = r; // type(a) = int

其次,auto通常会忽略顶层const,保留底层const

const int ci = i, &cr = ci;
auto b = ci; // type(b) = int  顶层const
auto c = cr; // type(c) = int  cr绑定到ci,而ci是顶层const
auto d = &i; // type(d) = int*
auto e = &ci;// type(e) = const int* 对ci取地址是底层const

如果希望推断出的auto类型是一个 顶层const,则需要显示指明

const auto f = ci;

引用的类型设为auto

auto &g = ci; // type(g) = const int &
auto &h = 42; // 错误,不能为非常量引用绑定字面值
              // 用常量和字面值初始化auto型引用是有区别的!
const auto &j = 42; // 正确

4.decltype类型

有时希望从表达式的类型推断出要定义的变量类型,但是不想用表达式的值初始化该变量。C++11提供了decltype关键字,它的作用是选择并返回操作数的类型。在此过程中,编译器分析表达式并得到它的类型,但不实际计算表达式的值

decltype(f()) sum = x; // sum的类型就是f()的返回类型,但编译器并不调用f()

decltype处理顶层const和引用的方式和auto有些许不同,如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)

const int ci = 0, &cj = ci;
decltype(ci) x = 0; // type(x) = const int
decltype(cj) y = ci;// type(y) = const int &
decltype(cj) z; // 错误,decltype(cj)是一个引用,必须初始化

如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型

当表达式返回的结果是一个左值时,decltype返回一个引用类型,比如解引用

int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // type(b) = int
decltype(*p) c; // 错误,type(c) = int &,必须初始化

decltype返回的类型与表达式的形式密切相关。有一种情况特别注意,如果变量名加上一对括号则会被视为返回左值的表达式,因此decltype((varibale))返回的是引用,而decltype(variable)只用当variable本身是引用时才返回引用

decltype((i)) d; // 错误,type(d) = int &,必须初始化
decltype(i) e; //正确,type(i) = int

decltype作用于某个函数时它返回的是函数类型,而非函数指针类型。因此,我们要显示加上*以表明我们需要返回指针。

using F = int (int*, int); // 函数类型
using PF = int (*) (int*, int); // 函数指针

string::size_type = sumLength(const string&, const string&);
string::size_type = largerLength(const string&, const string&);
decltype(sumLength)* getFcn(const string&);

5.constexpr关键字

constexpr变量

constexpr函数

constexpr构造函数  字面值常量类

6.右值引用

左值与右值[C++左值和右值]

C++表达式要么是左值要么是右值,这是表达式的属性

一个表达式是左值还是右值可以从语法语义上来区分

语法上:能否用取地址&运算符

语义上:表达式是持久对象还是临时对象(本质涵义)

const int a = 10; // 左值

(a + 5); // 右值

右值引用也是某个对象的一个别名

对于左值引用,我们不能将其绑定到需要类型转换的表达式、字面值、返回右值的表达式

右值引用则完全相反,我们可以将其绑定到这类表达式上,但不能直接绑定到左值上

int i = 42;
int &lr1 = i;      // 正确,左值引用
int &&rr1 = i;     // 错误,不能将右值引用绑定到左值上
int &lr2 = i * 42; // 错误,不能将左值引用绑定到右值上 
const int &cr = i * 42;// 正确,可以将const引用绑定到右值上
int &&rr2 = i * 42;// 正确

返回左值的表达式:

(1)赋值运算符

(2)内置解引用/迭代器解引用、内置(数组)/string/vector下标运算符

(3)前++/--运算符

(4)返回左值引用的函数调用

返回右值的表达式:

(1)取地址运算符

(2)算术、关系、位运算符

(3)后++/--

(4)返回非引用的函数调用

所有变量(包括函数参数)都是左值,不能将右值引用直接绑定到变量上,即使这个变量是右值引用类型也不行

int &&rr1 = 42;  // rr1是变量,类型为int &&
int &&rr2 = rr1; // 错误,表达式rr1是左值!
int &&rr3 = std::move(rr1); // 正确,通过move可以将一个左值转为右值

const引用和右值引用的区别

const引用既可以绑定到普通对象也可以绑定到const对象,右值引用二者都不能绑定

int i = 20;
const int j = 10;
const int &cr1 = i; // 正确
const int &cr2 = j; // 正确
int &&rr1 = i; // 错误
int &&rr2 = j; // 错误

左值引用和右值引用的意义: 

左值引用:

1.用作对象的别名

2.避免对象的复制

右值引用:

1.移动语义[c++11右值引用]

2.完美转发

7.范围for循环

for (declaration : expression)

        statement

expression是一个序列对象,declaration定义了一个循环变量

示例1:统计字符串中的标点符号

std::string s{"Hello World!!!"};
decltype(s.size()) cnt = 0;
for (auto c : s)
    if (ispunct(c))
        ++cnt;

 示例2:如果想改变序列中的元素,则要把循环变量定义成引用类型

              把字符串中的字符转为大写

std::string s{"Hello World!!!"};
for (auto &c : s)
    c = toupper(c);

8.尾置返回类型

在返回值位置用auto代替,在形参列表后面用->指向真正的返回类型

这种形式对返回值类型比较复杂的函数最有效

auto func(int i) -> int (*)[10] // 返回一个指向数组的指针

9.可变参数函数

(1) C语言变参函数

void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);

#include <stdio.h>
#include <stdarg.h>

void foo(const char *fmt, ...)
{
    va_list ap;
    int d;
    char c, *s;

    va_start(ap, fmt);
    while (*fmt)
        switch (*fmt++) {
        case 's':              /* string */
            s = va_arg(ap, char *);
            printf("string %s\n", s);
            break;
        case 'd':              /* int */
            d = va_arg(ap, int);
            printf("int %d\n", d);
            break;
        case 'c':              /* char */
            /* need a cast here since va_arg only
            takes fully promoted types */
            c = (char) va_arg(ap, int);
            printf("char %c\n", c);
            break;
         }
    va_end(ap);
}

(2) C++变参函数

所有实参类型相同:使用initializer_list<T>传参

实参的类型不同:可变参数函数模板

10.enum

枚举类型使我们可以将一组整形常量组织在一起,和类一样枚举类型也定义了一种新的类型。同一作用域的枚举成员不能重复定义

C++11包含两种枚举:

//不限定作用域的枚举类型

enum {kRed, kGreen, kBlue}; // 匿名枚举,只能在定义enum的时候同时定义其对象

enum Season {kSpring, kSummer, kAutumn, kWinter};

//限定作用域的枚举类型

enum class OpenMode {kInput, kOutput, kAppend};

enum Color {kRed, kGreen, kYellow}; // 不限定作用域的枚举类型
enum StopLight {kRed, kGreen, kYellow}; // 错误,枚举成员重复定义
enum class Pepper {kRed, kGreen, kYellow}; // 正确,限定作用域

Color hair = kRed; // 正确,不限定作用域的枚举成员可以直接使用
Color eyes = Color::kGreen; // 正确,显示访问枚举成员
Pepper p1 = kYellow; // 错误,限定作用域的枚举成员不能直接使用
Pepper p2 = Pepper::kRed; // 正确

不能将整数赋值给枚举类型对象

对于不限定作用域的枚举类型,其成员可以直接赋给整型变量,也可以跟整型变量判等或判不等

对于限定作用域的枚举类型,以上操作都不允许

int i = Color::kGreen;   // 正确
if (Color::kRed == 0)    // 正确
int j = Pepper::kYellow; // 错误
if (Pepper::kRed == 0)   // 错误

C++11可以对枚举类型前置声明,对于不限定作用域的枚举必须指定成员大小,限定作用域的枚举默认是int

enum Color : int;

11.命名空间

大型程序往往会使用多个独立的库(自己开发的或第三方的),这些库中又会定义大量的全局名字,如全局变量、全局函数、类等,那么就很有可能出现名字冲突的情况。传统的做法是在全局名字前加上库名之类的前缀,比如:

void *OPENSSL_malloc(size_t num);
lua_State *luaL_newstate (void);
xmlDocPtr xmlReadFile(const char *filename, const char *encoding, int options);

在C++中为防止名字冲突提供了命名空间的机制,能出现在全局作用域的声明都可以放在命名空间内,包括变量、函数、类、模板和其他命名空间

每个命名空间是一个作用域

不要把#include放在命名空间中

命名空间可以是不连续的,所以命名空间中的函数和类也可以使声明和实现分别位于头文件和源文件中

匿名命名空间中定义的变量和函数只在该文件内有效,不能跨文件访问,用来取代C语言的static声明

如果命名空间的名字太长我们可以为其设置一个较短的别名

namespace primer = cplusplus_primer;
namespace Qlib = cplusplus_primer::QueryLib; // 也可以为嵌套的命名空间设置别名
Qlib::Query q;

使用命名空间成员

我们可以通过using声明把命名空间的某个成员引入到某个作用域,这样就能直接使用该名称而不用加::限定符了

using的引入规则跟其他作用域规则一样:它的有效范围从using声明的地方开始,一直到using所在作用域结束为止。在此过程中,外层作用域的同名实体被隐藏,未加::限定符的名称只能在using所在作用域及内层作用域中使用

不要直接使用using namespace ...引入整个命名空间,这样会造成命名空间污染

12.智能指针

程序使用动态内存通常出于以下三种原因:

a.程序不知道自己需要使用多少对象,比如容器类

b.程序不知道所需对象的准确类型

c.程序需要在多个对象间共享数据,希望对象和其分配的资源有独立的生存期,当对象被销毁后,资源还是保留的(比如shared_ptr的引用计数)

class StrBlob {
public:
    StrBlob(std::initializer_list<std::string> il);
private:
    std::shared_ptr<std::vector<std::string>> data; // 资源分配在堆上
};

12.1 shared_ptr/weak_ptr

实现原理介绍

基于gcc 4.8.5 libstdc++库

通过以上类图可知:

shared_ptr是__shared_ptr的派生类,__shared_ptr有一个__shared_count类型的成员变量

__shared_count封装了一个_Sp_counted_base指针,实际指向_Sp_counted_ptr对象,_Sp_counted_ptr继承自_Sp_counted_base,所以会产生多态机制

_Sp_counted_ptr创建在堆上,拷贝__shared_count(也即拷贝shared_ptr)时只是浅拷贝,所以多个__shared_count对象都保存有_Sp_counted_ptr的指针,从而实现了引用计数的共享

 _Sp_counted_base的两个原子类型成员变量_M_use_count和_M_weak_count是底层真正用来表示引用计数的

__shared_count和__weak_count共享引用计数

每构造一个shared_ptr使_M_use_count加1,_M_weak_count(初始为1)不变

每构造一个weak_ptr使_M_weak_count加1,_M_use_count不变

销毁shared_ptr使_M_use_count减1,减为0时销毁其管理的对象,然后使_M_weak_count减1,减为0时(即没有weak_ptr)销毁引用计数

销毁weak_ptr只是_M_weak_count减1,减为0时直接销毁引用计数

通过内存分布理解智能指针

1 #include <memory>
2 class A
3 {
4 private:
5     int i;
6 };
7 
8 int main(void)
9 {
10     using std::shared_ptr;
11     using std::weak_ptr;
12     A* a1 = new A();
13     A* a2 = new A();
14     shared_ptr<A> sp1(a1);  // sp1.use_count() = 1
15     shared_ptr<A> sp2(a2);  // sp2.use_count() = 1
16     shared_ptr<A> sp3(sp2); // sp3.use_count() = 2
17     sp1 = sp2; // sp1.use_count() = 0  对象a1被释放
18                // sp2.use_count() = 3
19     weak_ptr<A> wp1(sp1);
20     return 0;
21 }

程序执行完第16行时,各个对象在内存中的关系如下图

可以看出sp1和sp2的引用计数是同一个对象,即分配在堆上的_M_pi,并且它俩共同管理对象a2,所以shared_ptr称为共享式智能指针

程序执行完第17行时,各对象的关系如下图

 程序执行完第19行时,各对象的关系如下图

用法及注意事项

 接受指针参数的智能指针构造函数是explicit的,所以不能将一个内置指针隐式转换为智能指针,必须使用显示初始化构造一个智能指针

shared_ptr<int> p1 = new int(1024); // 错误
shared_ptr<int> p2(new int(1024));  // 正确

shared_ptr<int> clone(int p) {
    return new int(p); // 错误
}
shared_ptr<int> clone(int p) {
    return shared_ptr<int>(new int(p)); // 正确
}

智能指针使用规范:

不要使用相同的指针初始化或reset多个智能指针

不要delete get()返回的指针

不要使用get()返回的指针初始化或reset另一个智能指针

慎用get()返回的指针,尤其当把它保存在一个指针变量中时,最后一个智能指针销毁后它就变成无效指针了

如果使用智能指针管理的资源不是new分配的内存(比如建立的某种连接等),要传递一个自定义的删除器(默认是delete)

// 使用shared_ptr管理动态数组
shared_ptr<int> sp6(new int[10], [](int* p) { delete[] p; });

12.2 unique_ptr

独占式智能指针,同一时刻只能有一个unique_ptr指向给定的对象,所以它不支持拷贝和赋值操作

unique_ptr支持的操作

unique_ptr<T> u1(p)

指向类型为T的对象p,使用delete释放对象p
unique_ptr<T,D> u2(p,d)使用类型为D的可调用对象d释放对象p
u = nullptr释放u指向的对象,将u置空
u.release()放弃u的控制权,返回指针,将u置空
u.reset()释放u指向的对象
u.reset(p)令u指向新对象q,释放原对象,对象控制权的转移
using std::string;
using std::unique_ptr;
unique_ptr<string> u1(new string("Hello unique_ptr 111");
//unique_ptr<string> u2(p1); // 错误,unique_ptr不能拷贝
unique_ptr<string> u3;
//u3 = u1; // 错误,unique_ptr不能赋值

unique_ptr<string> u2(new string("Hello unique_ptr 222");
u2.reset(u1.release()); // 将控制权从u1转移给u2,并释放u2原来的对象
unique_ptr<string> u3(u2.release()); // 将控制权从u2转移给u3

// 有一个例外,可以拷贝将要被销毁的或局部的unique_ptr,其实这种是移动语义
unique_ptr<int> clone(int p) {
    return unique_ptr<int>(new int(p));
}
unique_ptr<int> clone(int p) {
    unique_ptr<int> u(new int(p));
    // ...
    return u;
}

// 传递自定义删除器
class connection;
void end_connection(connection* p) { disconnect(*p); }
connection c = connect();
unique_ptr<connection, decltype(end_connection)*> up(&c, end_connection);
// 即使异常退出也可以正确关闭连接

13.lambda表达式

在C++中共有4种可调用对象:

(1) 函数

(2) 函数指针

(3) 重载了函数调用运算符的类对象

(4) lambda表达式

lambda表达式表示一个可调用的代码单元,可以理解为一个未命名的内联函数,其形式如下:

[capture list](parameter list) -> return type {function body}

capture list(捕获列表) 是lambda所在函数定义的局部变量

parameter list、return type和function body和普通函数一样

捕获列表

值捕获

// 输出大于等于给定字符数的单词
void biggies(vector<string> &words, vector<string>::size_type sz)
{
    // 按字符数排序
    // ...

    // 返回满足条件的第一个元素的迭代器
    auto wc = find_if(words.begin(), words.end(),
                        [sz](const string &a) { return a.size() >= sz; });
    for_each(wc, words.end(),
                [](const string &s) { cout << s << " "; });
    cout << endl;
}

引用捕获

// 改写上面的例子
// 输出流和分隔符通过参数传进来
void biggies(vector<string> &words, vector<string>::size_type sz,
              ostream &os = cout, char c = ' ')
{
    // 与上面例子的代码完全一样

    for_each(wc, words.end(),
                [&os, c](const string &s) { os << s << c; });
    os << endl;
}

返回类型

如果lambda包含单一的return语句不需要指定返回类型,编译可以通过return推断出来

如果除return之外还要其他语句则必须指定返回类型,不包含return的lambda返回void

14.异常处理

异常处理(exception)机制允许程序中独立开发的部分能够在运行中就出现的问题进行通信并做出相应的处理。异常使得我们能够将问题的检测与解决过程分离开来

try {

        // 可能抛出异常的代码

}

catch (ExceptType1 &e1)

{}

catch (ExceptType2 &e2)

{}

...

当throw出现在try块内或者直接或间接调用的函数内时,程序就会跳过throw后面的所有代码流程,沿着函数调用栈不断查找异常类型相匹配的catch子句

如果找到了一个匹配的catch子句,则程序就会进入该子句并执行其中的代码,然后跳转到与该try块关联的最后一个catch子句之后的点,并从这里继续执行

catch子句匹配规则

1. 从throw语句开始沿着调用栈向前回溯,所以首先是本函数的catch,其次是上层函数的catch,...

2. 在步骤1的查找过程中,再根据异常类型匹配第一个catch子句,但未必是最佳匹配

3. 找不到任何匹配的catch时,最终调用标准库的terminate函数终止程序

因为catch语句是按照顺序逐一匹配的,所以当程序使用具有继承关系的多个异常类型时,必须对catch语句的顺序进行组织和管理,确保派生类的异常处理代码出现在基类之前

抛出的异常类型和catch声明的类型只允许如下转换:

1. 允许从非常量向常量转换,即非常量对象的throw语句可以匹配接收常量引用的catch语句

2. 允许从派生类向基类转换

3. 数组和函数均被转换成对应类型的指针

重新抛出与捕获所有异常

有时一个单独的catch不能完整地处理某个异常。在执行了某些校正操作后,当前catch可能会决定由调用链更上一层的函数接着处理,此时可以通过单独的throw语句将异常重新抛出

catch (...)语句可以捕获所有异常,通常与重新抛出一起使用。如果catch(...)与其他catch一起出现时,必须在最后的位置

void manip()
{
    try {
        // 这里的操作将引发并抛出一个异常
    }
    catch (...) {
        // 处理异常的某些特殊操作
        throw;
    }
}

局部对象在异常抛出后也能被正确销毁,如果局部对象是类类型,则该对象的析构函数将被自动调用

析构函数不能抛出它自身不处理的异常

释放资源的代码(比如delete、close等)在异常抛出后将被跳过。如果用类来控制资源的分配,就不管函数正常结束还是发生异常,资源都能被释放

在C++11中通过noexcept说明符指定某个函数不会抛出异常,C++98中则通过throw()声明

void recoup(int) noexcept;

void recoup(int) throw();

这些标准库的异常类相对简单,实际编程中我们可以继承它们实现自己的异常类,通过添加成员变量和成员函数来提供更丰富的异常信息

基类exception提供了一个虚函数virtual const char* what() throw(); 子类可以重写它提供自定义的异常信息

15.重载匹配

重载匹配是指把函数调用与一组重载函数中的某一个关联起来的过程,编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较结果决定调用哪个函数

重载匹配有三种可能的结果:

  1. 编译找到一个与实参最佳匹配的函数,并生成调用函数的代码
  2. 找不到任何一个函数与调用实参匹配,则报无匹配的编译错误
  3. 有多个函数可以匹配,但每个都不是明显的最佳选择,则报二义性调用错误

为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下:

  1. 精确匹配,包括数组或函数转换成对应类型的指针、添加或删除实参的顶层const
  2. 通过const转换实现的匹配,0或nullptr能转为任意类型的指针、指向任意非常量的指针能转成void*、指向任意对象的指针能转成const void*、派生类指针能转成基类指针
  3. 通过类型提升实现的匹配,小于int的整数向int转换、带符号向无符号转换、float向double转换
  4. 通过类类型转换实现的匹配,即类中定义了operator type const()成员函数

16.类

1. 构造函数

默认构造函数

如果我们没有为类定义任何构造函数,编译器就会合成一个默认的无参构造函数

一旦我们定义了一些其他的构造函数,编译器将不再合成默认构造函数

如果我们既需要其他形式的构造函数,也需要默认构造函数,可以使用=default要求编译器合成默认构造函数

class Sales_data
{
public:
    Sales_data() = default;
private:
    //数据成员
};

委托构造函数

所谓委托构造函数就是可以调用该类的其他构造函数执行自己的初始化

class Sales_data
{
public:
    Sales_data(std::string s, unsigned cnt, double price);
    Sales_data() : Sales_data("", 0, 0) {}  // 委托构造函数
    Sales_data(std::string s) : Sales_data(s, 0, 0) {} // 委托构造函数
};

转换构造函数

只接受一个实参的构造函数,它实际上定义了参数类型向该类类型的隐式转换机制。在需要类类型的地方可以用参数类型替代,编译器会自动转换为类类型对象

class Sales_data
{
public:
    Sales_data(std::string s, unsigned cnt, double price);
    Sales_data() : Sales_data("", 0, 0) {}
    Sales_data(std::string s) : Sales_data(s, 0, 0) {}
    Sales_data & combine(const Sales_data&);
};
std::string book = "9-999-9999";
Sales_data item;
item.combine(book); // 把std::string隐式转换为Sales_data对象

将构造函数声明为explicit以阻止隐式转换

class Sales_data
{
public:
    // 其他函数与上面相同
    explicit Sales_data(std::istream&);
};
item.combine(cin); // 错误,接受std::istream类型的构造函数是explicit的
item.combine(Sales_data(cin)); // 正确,将std::istream显示转换为Sales_data对象再传给combine

2. 拷贝控制

拷贝控制操作包括以下5种特殊的成员函数:

class Sales_data
{
public:
    // 拷贝构造函数
    Sales_data(const Sales_data &that);
    // 拷贝赋值运算符
    Sales_data & operator=(const Sales_data &rhs);
    // 移动构造函数
    Sales_data(Sales_data &&that) noexcept;
    // 移动赋值运算符
    Sales_data & operator=(Sales_data &&rhs) noexcept;
    // 析构函数
    ~Sales_data();
};

 有些类对象我们不希望其拷贝或赋值,比如io流、线程等。c++11中我们将相关函数声明为=delete来实现这一目的,c++98则通过声明为私有成员来实现

struct NoCopy {
    NoCopy() = default;
    NoCopy(const NoCopy &) = delete;             // 阻止拷贝
    NoCopy & operator=(const NoCopy &) = delete; // 阻止赋值
};

特别地, 如果一个类含有引用或const成员,这个类不能使用拷贝赋值运算符。因为引用或const成员不能在对象创建之后再被赋值

为了定义拷贝构造函数和拷贝赋值运算符,我们必须首先确定此类型对象的拷贝语义。 一般来说,有两种选择:可以定义拷贝操作使类的行为看起来像一个值或者像一个指针

类的行为像一个值,副本和原对象是完全独立的。改变副本不会对原对象有任何影响

类的行为像一个指针,副本和原对象使用相同的底层数据,改变副本原对象也随之改变

移动语义:将一个对象的资源移动到另一个对象,避免不必要的对象拷贝,提高程序效率

移动操作通常不会抛出任何异常,即声明为noexcept

源对象移动后必须保持有效的、可析构的状态,但用户不能对其值进行任何假设

由于对象移动后具有不确定的状态,因此调用std::move()必须绝对确定移动后对象没有其他用户

只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值,编译器才会合成移动构造函数或移动赋值运算符

编译器可以移动内置类型,如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员

struct X {
    X(i, s) : i_(i), s_(s) {}
    int i_; // 内置类型可以移动
    std::string s_; // std::string定义了自己的移动操作
};
X x1(10,"move");
X x2 = std::move(x1); // 调用合成的移动构造函数

如果一个类定义了移动构造函数或移动赋值运算符,则必须定义拷贝构造函数或拷贝赋值运算符,否则它们会被定义为删除的

如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定调用哪一个,赋值操作也类似

规则:移动右值,拷贝左值

StrVec v1, v2;
v1 = v2;    // v2是左值,调用拷贝赋值
StrVec getVec(istream &);
v2 = getVec(cin); // getVec(cin)是一个右值,调用移动赋值
// 虽然v2 = getVec(cin)可以匹配operator(const StrVec&);右值可以绑定到const引用
// 但operator(StrVec&&)是精确匹配

如果没有移动构造函数,右值也会被拷贝,即使通过std::move移动它也是如此

class Foo {
public:
    Foo() = default;
    Foo(const Foo&); // 拷贝构造函数
    // 未定义移动构造函数
};
Foo x;
Foo y(x); // 拷贝构造,x是一个左值
Foo z(std::move(x)); // 拷贝构造,因为未定义移动构造函数

// 此种情形在c++98中很显然,比如c++98的std::string没有定义移动构造函数
// 但我们经常使用如下代码
std::string func(); // 返回一个右值
std::string str(func()); // 拷贝右值到str中

所有五个拷贝控制成员应该看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作

如拷贝构造函数和移动构造函数一样,如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。这种允许移动的成员函数也使用相同的参数模式:

一个接收const的左值引用(拷贝版本),另一个接收右值引用(移动版本)

比如标准库容器的push_back:

void push_back(const X&);  // 拷贝:绑定到任意类型的X

void push_back(X &&);        // 移动:只能绑定到类型X的可修改的右值

我们可以将能转换为类型X的任何对象传递给第一个版本的push_back,此版本从其参数中拷贝数据

我们传递一个可修改的右值编译器会选择第二个版本,此版本从其参数中移动数据

3. 运算符重载

当运算符作用于类类型的运算对象时,可以通过运算符重载重新定义该运算符的含义。当我们定义重载的运算符时,必须首先决定将其声明为类的成员函数还是声明为非成员函数

下面的准则有助于我们在将运算符定义为成员函数还是非成员函数做出抉择: 

  • 赋值(=)、下标([])、函数调用()必须定义为成员函数
  • 复合赋值运算符一般来说应该是成员函数
  • 改变对象状态的运算符、递增、递减、解引用运算符通常应该定义为成员函数
  • 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系等,通常定义为非成员函数
  • 重载输入输出运算符必须是非成员函数

标准库定义的函数对象,下表所列类型定义在functional头文件中

 我们可以用这些函数对象替换算法中的默认运算符,从而让算法按我们自己的意图工作,而不是默认行为

将vector中的字符串按降序排列
sort(svec.begin(), svec.end(), greater<string>());

标准库规定其函数对象对于指针同样适用,关联容器使用less<key_type>对元素排序,因此我们可以定义一个指针set或map中使用指针作为关键值而无须直接声明less

4. 类型转换

我们通过转换构造函数可以将实参类型转换为类类型,同样我们还可以通过类型转换运算符成员函数将类类型转换为其他类型

operator type () const

类型转换运算符没有返回类型,没有形参,通常应该是const

class SmallInt
{
public:
    SmallInt(int i = 0) : val(i) {}
    operator int() const {return val;}
private:
    int val;
};
SmallInt si;
si = 4; // 将4隐式转换成SmallInt,然后调用SmallInt::operator=
si + 3; // 将si隐式转换成int,然后执行int相加

C++11引入了显式的类型转换运算符

class SmallInt
{
public:
    // 编译器不会自动执行这一转换
    explicit operator int() const {return val;}
    // 其他成员与之前版本一致
};
SmallInt si = 3;
si + 3; // 错误,此处需要隐式的类型转换,但类型转换运算符是显式的
static_cast<int>(si) + 3; // 正确,显式转换

explicit的类型转换运算符有一个例外,当对象被用作条件表达式时,编译器会将显式的类型转换自动应用于它,包括下列位置:

  • if、while、do...while语句的条件部分
  • for语句的第二表达式
  • 逻辑&&、||、! 的运算对象
  • 条件表达式 ? :

向bool的类型转换通常用在条件部分,因此operator bool一般定义成explicit的,比如IO流的bool类型转换

explicit std::istream operator bool() const {return _M_ok;}
// 编译器自动将bool类型转换应用于while
while (std::cin >> value)

5. 继承

友元关系不能传递 

class Screen
{
    friend class Window_mgr; // 类Window_mgr可以访问Screen的私有成员
private:
    std::string contents;
};
class Window_mgr
{
    friend void f(); // 函数f()可以访问类Window_mgr的私有成员,但不能访问Screen的私有成员
    // 其他成员
};

派生类的成员函数和友元函数也不能通过基类对象访问私有或保护成员 

class Base
{
protected:
    int prot_mem;
};
class Sneaky : public Base
{
    friend void clobber(Sneaky &);
    friend void clobber(Base &);
public:
    void foo(Base &);
private:
    int j;
};
// 错误,成员prot_mem在基类中是受保护的,不能基类作用域之外访问
void Sneaky::foo(Base &b) {b.prot_mem = 0;}
// 正确,clobber是Sneaky的友元,能访问Sneaky的成员prot_mem(从基类继承而来)
void clobber(Sneaky &s) {s.prot_mem = 0}
// 错误,clobber不是基类的友元,不能访问基类的受保护成员prot_mem
void clobber(Base &b) {b.prot_mem = 0;}

友元关系也不能继承

class Base
{
    friend class Pal;
    // 其他成员与前一个版本一致
};
class Pal
{
public:
    // 正确,Pal是Base的友元
    int f(Base b) {return b.prot_mem;}
    // 错误,Pal不是Sneaky的友元,友元不会继承
    int f2(Sneaky s) {return s.j;}
    // 正确,派生类的基类部分的友元特性由基类自己控制
    // Pal是基类的友元,可以通过派生类对象访问从基类继承过来的成员prot_mem
    int f3(Sneaky s) {return s.prot_mem;}
};

公有继承、私有继承

公有/私有继承对派生类访问基类的成员没有影响,即不管公有还是私有继承,派生类可以访问基类的public和protected成员,不能访问基类的private成员

class Base
{
public:
    void pub_mem();
protected:
    int prot_mem;
private:
    char priv_mem;
};
class Priv_Derv : private Base // 私有继承
{
public:
    // 私有继承不影响派生类对基类成员的访问权限
    int f1() const {return prot_mem;}
};

公有/私有继承控制的是派生类用户对于基类成员的访问权限。对于共有继承,基类成员的访问控制在派生类中不变;对于私有继承,基类的所有成员在派生类中都变成了private的

Priv_Derv d;
d.pub_mem(); // 错误,私有继承,pub_mem在派生类Priv_Derv中变成了私有成员

继承关系中类的作用域

每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中查找该名字

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的

派生类能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字

struct Base {
    Base() : mem(0) {}
protected:
    int mem;
};
struct Derived : Base {
    Derived(int i) : mem(i) {}
    int get_mem() {return mem;} // 返回Derived::mem
protected:
    int mem; // 隐藏基类中的mem
};

除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字 

名字查找与继承

理解函数调用的解析过程对于理解C++的继承至关重要,假设我们调用p->mem()或者obj.mem(),则依次执行以下4个步骤:

  • 首先确定p(或obj)的静态类型
  • 在p(或obj)的静态类型中查找mem。如果找不到,则依次在基类中查找直到继承链的顶端。如果最终都没有找到,则编译报错
  • 一旦找到了mem,就进行常规的类型检查以确认本次调用是否合法
  • 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码

--- 如果mem是虚函数且通过引用或指针调用,则编译器产生的代码将在运行时确定到底执行虚函数的哪个版本,依据是对象的动态类型

--- 如果mem不是虚函数或者通过对象(非引用或指针)进行的调用,则编译器将产生一个常规函数的调用

如果派生类(即内层作用域) 的某个函数与基类(即外层作用域)的同名,则它们不会构成重载关系,而是在派生类中会隐藏该基类的成员。即使它们的形参列表不一致,该基类成员也会被隐藏

struct Base {
    int memfcn();
};
struct Derived : Base {
    int memfcn(int); // 隐藏基类的memfcn
};
Derived d; Base b;
b.memfcn();   // 调用Base::memfcn
d.memfcn(10); // 调用Derived::memfcn
d.memfcn();   // 错误,参数列表为空的memfcn被隐藏了
d.Base::memfcn() // 正确,调用Base::memfcn

移动操作与继承

大多数基类都会定义一个虚析构函数,因此默认情况下,基类类不含有合成的移动操作,而且在它的派生类中也没有合成移动操作。当我们确实需要执行移动操作时应该首先在基类中定义,一旦定义了移动操作,那么也必须定义拷贝操作

class Quote {
public:
    Quote() = default;
    Quote(const Quote &) = default;
    Quote(Quoute &&) = default;
    Quote &operator=(const Quote &) = default;
    Quote &operator=(Quote &&) = default;
    virtual ~Quote() = default;
};

派生类的拷贝和移动构造函数

派生类的拷贝控制成员使用直接基类中对应的操作对对象的基类部分进行初始化、赋值或销毁操作

class Quote {
};
class Disc_quote : public Quote {
};
class Bulk_quote : public Disc_quote {
};

比如上面的继承关系,合成的Bulk_quote拷贝构造函数使用(合成的)Disc_quote拷贝构造函数,后者又使用(合成的)Quote拷贝构造函数

当为派生类定义拷贝或移动构造函数时,我们通常使用对应的基类构造函数初始化对象的基类部分

class Base { /* ... */ };
class D : public Base {
public:
    // 默认情况下,使用基类的默认构造函数初始化对象的基类部分
    // 要想使用拷贝或移动构造函数来拷贝或移动基类部分
    // 必须在派生类的初始化列表中显式地调用
    D(const D &d) : Base(d) { /* ... */ }
    D(D &&d) : Base(std::move(d)) { /* ... */ }
};

派生类的赋值运算符

与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值

D &D::operator=(const D &rhs)
{
    Base::operator=(rhs); // 为基类部分赋值
    // 派生类成员的赋值
    return *this;
}

派生类的析构函数

与构造函数和赋值运算符不同,基类的析构函数会被自动调用,派生类的析构函数只负责销毁自己分配的资源 

class D : public Base {
public:
    //Base::~Base被自动调用
    ~D() { /* 销毁派生类自己的资源 */ }
};

在容器中存放继承体系中的对象

当我们使用容器存放继承体系中的对象时,必须采取间接存储的方式。因为不允许在容器中保存不同类型的元素,所以我们不能把具有继承关系的多种类型的对象直接存放在容器中

class Quote {};
class Bulk_quote : public Quote {};
// 使用智能指针封装基类指针存放于容器中
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(make_shared<Bulk_quote>("0-201-54848-8", 50, 10, 0.25));

6. 虚函数

派生类如果定义了一个与基类虚函数名字相同但形参不同的函数,这仍然是合法的,但编译器认为这是两个独立的函数,派生类的函数不会覆盖基类的虚函数。 这是很不好的编程习惯或者可能弄错了

C++11中我们可以用override来说明派生类中的虚函数,这么做的好处是可以使程序员的意图更加清晰,同时让编译器为我们发现一些错误。如果使用override标记了某个函数,但该函数并没有覆盖基类已存在的虚函数,此时编译报错

struct B {
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};
struct D : public B {
    void f1(int) const override; // 正确,f1与基类的虚函数f1匹配
    void f2(int) override;       // 错误,基类没有f2(int)的虚函数
    void f3() override;          // 错误,基类f3不是虚函数
};

我们还可以把某个虚函数标记为final,那么之后的派生类尝试覆盖该函数将会引发错误

struct D1 : public B {
    void f1(int) const final; // 不允许后续的派生类覆盖f1
};
struct D2 : public D1 {
    void f1(int) const override; // 错误,D1已经将f1声明为final
};

基类以及抽象基类都需要定义一个虚析构函数。虚析构函数将阻止合成移动操作,即移动构造函数和移动赋值运算符

class Quote {
public:
    virtual ~Quote() = default;
};

在构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相对应的虚函数版本(即不会产生多态行为)

17.模板与泛型编程

模板是C++泛型编程的基础。一个模板就是创建类或函数的蓝图或者说公式。当使用一个vector这样的泛型类型,或者find这样的泛型函数时,我们提供足够的信息,将蓝图转换为特定的类或函数。这种转换发生在编译阶段

1. 模板的非类型参数

模板的非类型参数表示一个值而不是一个类型。当模板被实例化时,非类型参数被用户提供的或编译器推断出的值替代。这些值必须是常量表达式,从而使编译器能够实例化模板

// 定义一个用来比较字符串字面量的模板
// 由于我们希望比较不同长度的字符串,因此为模板定义了两个非类型参数
// 分别表示两个字符串数组的长度
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{ return strcmp(p1, p2); }

compare("hi", "mom");
// 当我们使用这个形式调用compare时,编译器会实例化出如下版本
// int compare(const char (&p1)[3], const char (&p2)[4]);
// 数组最后一个元素用来存放\0

非类型参数可以是整数,或者一个指向对象或函数的指针,或者一个左值引用

当非类型参数是指针或引用时,所绑定的对象必须具有静态的生存期,即static对象或全局对象,另外指针也可以使用nullptr或0来初始化

函数模板可以声明为inline或constexpr的

template <typename T> inline T min(const T &, const T &); 

2. 编写类型无关的代码

template <typename T> int compare(const T &v1, const T &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}

以上函数模板体现了泛型编程的两个重要准则:

  • 模板中的函数参数是const引用
  • 函数体中的条件判断只使用<运算符

通过将函数参数设定为const引用,保证了函数可以用于不能拷贝的类型(比如unique_ptr和IO流)。另外如果处理大对象时,也能使函数运行得更快

只使用<运算符降低了compare对要处理类型的要求。模板程序应该尽量减少对实参类型的要求

3. 模板编译

函数模板和类模板成员函数的定义通常放在头文件中

当编译器遇到一个模板定义时,它并不生成代码。只有当我实例化模板的一个特定版本时,编译器才会生成代码,因此大多数编译错误在实例化期间报告

当我们编写模板时,代码不能是针对特定类型的,但模板通常对其使用的类型有一些假设。比如上面compare函数模板就假定实参类型定义了<运算符。调用者在使用模板时需要保证所提供的类型能够支持模板所要求的操作

4. 类模板

在模板中使用其他类模板或函数模板时可以用具体类型也可以用本模板的类型参数

类模板的成员函数具有和模板相同的类型参数,因此定义在类模板之外的成员函数必须以关键字template开始,后接类模板参数列表

类模板的成员函数只有被用到时才会实例化,这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类

5. 类模板与友元

如果类模板的友元是普通类或函数,则该友元是所有模板实例的友元

void f(int);
class A {};
template <typename T> class B {
    friend void f(int); // 函数f是类模板B所有实例化类的友元
    friend class A; // 类A同上
};

如果类模板的友元也是模板(类模板或函数模板),则类可以授权给所有友元模板实例,也可以只授权给特定实例

template <typename T> class BlobPtr;
template <typename T> class Blob; // 运算符==中的参数需要的声明
template <typename T>
    bool operator==(const Blob<T> &, const Blob<T> &);
// 友元用Blob的模板形参作为它们的模板实参
// 因此友元关系被限定在使用相同类型实例化的Blob、BlobPtr和==运算符之间
template <typename T> class Blob {
    friend class BlobPtr<T>;
    friend bool operator==<T> (const Blob<T> &, const Blob<T> &);
};
Blob<char> bc; // BlobPtr<char>和operator==<char>都是该对象的友元
Blob<int> bi;  // BlobPtr<int>和operator==<int>都是该对象的友元
template <typename T> class Pal; // 模板的前置声明
class C { // 普通类
    friend class Pal<C>; // 只有用类C实例化的Pal才是C的友元类,其他Pal的实例不是C的友元
    template <typename T> friend class Pal2; // Pal2的所有实例都是C的友元类,此时不用前置声明
};
template <typename T> class C2 { // 类模板
    friend class Pal<T>; // 相同实参实例化的Pal是C2的友元
    template <typename X> friend class Pal2; // Pal2的所有实例是C2所有实例的友元,此处必须与C2的类型参数不同
    friend class Pal3; // Pal3是C2的所有实例的友元
};

6. 模板参数的类型成员

我们可以通过作用域运算符(::)访问static数据成员类型成员

class C {
public:
    typedef unsigned int u;
    static double d;
};
C::u i; // 访问类型成员
C::d * 0.1; // 访问static数据成员

在普通类(非模板)中,编译器掌握类的定义,所以知道访问的到底是类型还是数据成员。然而在模板中,编译器不掌握T的定义,直至实例化时才知道。当遇到T::mem这样的代码时,又必须要知道其是类型还是变量,C++默认情况下通过作用域运算符访问的是变量而不是类型。因此,当我们希望使用一个模板类型参数的类型成员时,要通过typename显式告诉编译器该名字是一个类型

template <typename T>
typename T::value_type top(const T &c)
{
    if (!c.empty())
        return c.back();
    else
        return typename T::value_type();
}

7. 成员模板

普通类的成员模板函数

类模板的成员模板函数

8. 控制实例化

当模板被使用时才会实例化这一特性意味着,相同的实例可能出现在多个源文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。在大型系统中,多个文件中实例化模板的额外开销可能非常严重。在新标准中,我们可以通过显示实例化来避免这种开销,做法如下:

// 在使用该模板的某个源文件中,比如user.cc

extern template declaration // 实例化声明

// 专门新建一个用于模板特例化的源文件,比如template.cc

template declaration // 实例化定义

当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码,即承诺在其他文件中已经实例化

// templateBuild.cc
template int compare(const int &, const int &);
timplate class Blob<string>; // 实例化所有成员

// Application.cc
extern template int compare(const int &, const int &);
extern template class Blob(string);

Blob<string> sa1, sa2; // 不会在本文件实例化
Blob<int> a1 = {0, 1, 2, 3, 4}; // Blob<int>的构造函数在本文件实例化

9. 返回类型无法推断

显式指定模板实参

template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
int i = 10;
long l = 20;
auto ret = sum<long long>(i, l); // 实例化:long long sum(int, long);

尾置返回类型

假设编写一个函数,接受表示的一对迭代,返回序列中的某个元素的引用

template <typename IT>
auto fcn(IT beg, IT end) -> decltype(*beg) // 解引用结果为左值
{                                          // 所以decltype推断的是beg表示的元素类型的引用
    // 处理序列
    return *beg; // 返回序列中元素的引用
}

通过标准模板库进行类型转换

标准库在头文件<type_traits>中定义了许多用于类型转换的模板,它们通常用于模板元编程

比如用remove_reference来获取引用的元素类型,在remove_reference中有个type类型成员,当我们实例化remove_reference<int&>时,type就表示int;同理用std::string&实例化时,type就表示std::string

更一般的,给定一个迭代器,remove_reference<decltype(*beg)>::type将获得beg指向的元素的类型,decltype(*beg)返回元素类型的引用,remove_reference脱去引用,剩下元素类型本身

template <typename IT>
auto fcn2<IT beg, IT end) -> typename remove_reference<decltype(*beg)>::type
{
    // 处理序列
    return *beg; // 返回序列中元素的拷贝
}

10. 引用型参数的类型推断

从左值引用函数参数推断类型

template <typename T> void f1 (T &);  // 实参必须是一个左值

int i;

const int ci;

f1(i);    // T推断为int

f1(ci);  // T推断为const int

f1(5);  // 调用错误,实参必须是左值

template <typename T> void f2 (const T &);  // 可以接受任意类型的实参

f2(i);    // T推断为int

f2(ci);  // T推断为int

f2(5);  // T推断为int

从右值引用函数参数推断类型

template <typename T> void f3 (T &&);

f3(42);  // T推断为int

11. 引用折叠

问题的提出:

template <typename T> void f3 (T &&);

int i = 10;

f3(i);  // 根据先前介绍的语法,我们可能认为这样的调用是错误的。因为i是一个左值,我们不能将一个右值引用绑定到左值上

         // 但是C++在正常的绑定规则之外还定义了两个例外的规则。对于模板参数的右值引用,允许将其绑定到左值上

         // 这两个例外的规则是std::move能够正确工作的基础

规则1:

当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。因此,对于f3(i)这样的调用,编译器推断T为int&

规则2:

当T被推断为int&时,f3的实例化看起来应该是这样的:void f3(int& &&);这就意味着f3的参数是一个int&的右值引用。通常,我们不能直接定义引用的引用。但是通过类型别名模板参数间接定义是可以的
当我们间接定义一个引用的引用时,这些引用会形成“引用折叠”。对一个给定类型X折叠规则如下:

  • X& &、X& &&、X&& &都折叠成X&
  • X&& &&折叠成X&&

根据引用折叠,对于f3(i)这样的调用,函数模板f3的实例化结果其实为void f3(int &);

需要注意的是,以上两个规则暗示,参数类型为T&&的函数也可以接受任意类型的实参

template <typename T> void f3(T&&);
int i = 10;
const int ci = 20;
f3(42); // 传递右值,实例化为void f3(int &&);
f3(i);  // 传递左值,实例化为void f3(int &);
f3(ci); // 传递const左值,实例化为void f3(const int &);

对引用折叠的进一步理解:

#include <iostream>
int main(void)
{
    typedef int&    LR;
    typedef int&&   RR;

    int i = 10;
    int &lr1 = i;   // 左值引用lr1绑定到变量i
    int &lr2 = lr1; // 用引用作为初始值,实际上是以引用绑定的对象作为初始值
                    // 所以左值引用lr2也绑定到变量i
    std::cout << lr2 << std::endl; // 输出10

    LR & lrlr = i;  // 类型LR是int&的别名,该语句展开会形如 int& & lrr = i
                    // 我们不能直接定义引用的引用,所以该语句看起来是错误的
                    // 但是在C++11中,可以通过类型别名或模板参数间接定义引用的引用
                    // 并且在这种情况下,引用的引用会形成'引用折叠'
                    // int& &会折叠为左值引用,所以这条语句实际上编译为: int & lrr = i;
    lrlr = 20; // 把引用绑定的对象即变量i赋值为20
    std::cout << i << std::endl; // 输出20

    LR && lrrr = i; // int& &&也会折叠为左值引用,即int &
                    // 所以这条语句实际也会编译为: int & lrrr = i;
    lrrr = 30;
    std::cout << i << std::endl; // 输出30

    RR & rrlr = i;  // int&& &同样折叠为左值引用,即int&
                    // 所以这条语句实际也会编译为: int & rrlr = i;
    rrlr = 40;
    std::cout << i << std::endl; // 输出40

//  RR && rrrr = i; // 错误,int&& &&会折叠为右值引用,即int &&
                    // 不能将右值引用绑定到左值上
    return 0;
}

参数为T&&和const T&的函数模板都能接受任意类型的实参,所以通常我们使用在[16.2]节看到的方式进行重载

template <typename T> void f(T&&); // 绑定到右值

template <typename T> void f(const T&); // 绑定到普通左值或const左值

12. 转发

某些函数模板需要将一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括是否是const以及是左值还是右值

template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{ f(t2, t1); }

该模板第一个类型是一个可调用对象,把剩余两个参数“反转”后传给可调用对象

这个模板大部分情况下工作的很好,但是如果可调用对象f接受一个引用型参数就会出问题,比如

void f(int v1, int &v2) { std::cout << v1 << "," << ++v2 << std::endl; }

在这段代码中,v2绑定到了t1,但是不会绑定到调用flip1的实参,比如

int i = 10;

flip1(f, i, 42); // flip1实例化为:void flip1(void (*)(int v1, int &v2), int t1, int t2); 因此变量i不会被改变

通过前一节介绍的引用折叠就可以完美解决这一问题,我们把函数的参数定义为模板类型的右值引用看看会发生什么?

template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{ f(t2, t1); }

flip2(f, i, 42); // 把左值i传给模板类型的右值引用型参数t1,使得T1推断出的类型为int&,通过引用折叠,flip2实例化为:

                     // void flip2(void (*)(int v1, int &v2), int &t1, int &&t2);

                     // t1绑定到了i,v2又绑定到t1;所以当f递增v2时,i也会被改变

到这里好似一切都没问题了,但是对于接受右值引用的可调用对象会编译报错,比如

void g(int &&v1, int &v2) { std::cout << v1 << "," << ++v2 << std::endl; }

由[一.6]节可知,函数参数和变量一样都是左值,不能用左值直接初始化右值引用,即这样的语句:int &&t2 = 42; int &&v1 = t2;是错误的

这里就要用到C++11标准库的新函数std::forward()了,我们再次重写flip()函数如下:

template <typename F, typename T1, typename T2)
void flip(F f, T1 &&t1, T2 &&t2)
{
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

与std::move()不同的是std::forward()必须通过显式模板实参调用

如果仅仅是上述示例中的用法似乎用处不大,只要不把函数参数定义为右值引用就好了,比如void g(int v1, int &v2)

但是这样会存在一个问题,当参数是一个对象并且是右值时,比如string("Hi"),就无法实现潜在的对象移动,只能拷贝

template <class T> T&& forward (typename remove_reference<T>::type& arg) noexcept;

template <class T> T&& forward (typename remove_reference<T>::type&& arg) noexcept;

引用C++网站对forward的解释:

(a) 如果arg是一个非左值引用,该函数返回一个右值引用

(b) 如果arg是一个左值引用,该函数不做任何修改,仍返回左值引用

(c) 该函数将右值引用的参数完美地转发到推导的类型,保留潜在的移动语义

(d) 所有命名值(如函数参数)总是作为左值(即使是声明为右值引用的值),这给在函数模板中将参数转发给其他函数以保留潜在的移动语义带来了困难

#include <utility>
int main(void)
{
    int i = 10;
    int &ri = i;    // 左值引用
    int &rr = i+10; // 右值引用
    int &&rr1 = std::forward<int>(i); 	  // int &&std::forward(int &arg);
    int &&rr2 = std::forward<int>(i+5);   // int &&std::forward(int &&arg);
    int &&rr3 = std::forward<int&&>(rr);  // int &&std::forward(int &&arg);
    int &lr = std::forward<int&>(ri); 	  // int &std::forward(int &arg);
    return 0;
}

13. 重载与模板

函数模板可以被另一个函数模板普通函数重载,构成重载关系的函数必须具有不同数量或类型的参数。涉及函数模板的重载匹配会在以下几方面受到影响:

  • 对一个调用,其候选函数包括类型推断成功的函数模板实例
  • 如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数
  • 如果有多个函数提供同样好的匹配,则
  • (a) 如果同样好的函数中只有一个是非模板函数,则选择此函数
  • (b) 如果同样好的函数中没有非模板函数,而有多个函数模板,则选择更特例化的模板

编写重载模板

我们定义一组在调试中有用的函数,该函数返回给定对象的string表示,传递给函数的对象必须是具备输出运算符的类型

// <版本1> 通用版本
template <typename T> string debug_rep(const T &t)
{
    ostringstream oss;
    oss << t;
    return oss.str();
}
// <版本2> 针对指针版本
// 注意,对于char*指针该版本不会打印指针的值,而是打印p指向的字符串
template <typaname T> string debug_rep(T *p)
{
    ostringstream oss;
    oss << "pointer: " << p; // 打印指针的值
    if (p)
        oss << "," << debug_rep(*p);
    else
        oss << "," << "this is a nullptr";
    return oss.str();
}

string s("hi");
debug_rep(s); // 调用第一个版本;第二个版本要求指针参数,无法从非指针实参实例化期望指针参数的函数模板,实参推断失败
debug_rep(&s);// 此调用两个版本都生成可行的实例
              // string debug_rep(const string* &); T推断为string*
              // string debug_rep(string *); T推断为string
              // 第二个是精确匹配,第一个需要进行普通指针到const指针的转换
const string *sp = &s;
debug_rep(sp);// 此调用两个版本都生成可行的实例,且都是精确匹配
              // string debug_rep(const string* &); T推断为string*
              // string debug_rep(const string*); T推断为const string
              // 根据重载函数模板的特殊规则,此调用会解析为debug_rep(T*),即更特例化的版本
              // debug_rep(const T&)可用于任何类型,包括指针;而debug_rep(T*)只能用于指针

// <版本3> 针对string的非模板版本
string debug_rep(const string &s)
{ return s; }
string s("hi");
debug_rep(s); // 第一个和第三个提供同样好的匹配,则选择第三个非模板版本

// 再考虑下面的调用
debug_rep("hello world");
// 本例中三个版本都是可行的:
//  (1) string debug_rep(const T&); T推断为char[12]
//  (2) string debug_rep(T*); T推断为const char
//  (3) string debug_rep(const string &); 需要const char*到string的转换
// 前两版本都是精确匹配(第三个版本需要类型转换),又因为第二个版本更特例化,所以编译器选择debug_rep(T*)
// 对于字符串字面值我们肯定只希望打印字符串本身即可,不需要打印指针的值
// 而调用debug_rep(T*)不但打印了指针而且只打印了字符串的第一个字符,这与我们的预期大相径庭

// 如果希望字符数组和字符串字面值按string处理,则定义以下两个版本
// <版本4> 针对字符数组和字符串字面值的版本
string debug_req(char *p)
{ return debug_rep(string(p)); } // string debug_rep(const string &s)必须声明在前面,否则会调用不期望的版本
string debug_rep(const char *p)
{ return debug_rep(string(p)); }

14. 可变参数模板

一个可变参数模板就是接受可变数目参数的函数模板或类模板。可变数目的参数称为参数包,存在两种参数包:模板参数包函数参数包

// Args是一个模板参数包
// rest是一个函数参数包
template <typename T, typename... Args>
void foo(const T &t, const Args&... rest);

与往常一样,编译器从函数实参推断模板参数类型;对于可变参数模板,编译器还会推断参数的数目

int i = 0; double d = 3.14; string s = "hello template";
foo(i, s, 42, d); // 包中有3个参数
foo(s, 42, "hi"); // 包中有2个参数
foo(d, s);        // 包中有1个参数
foo("hi");        // 空包

// 对应的实例化版本
void foo(const int &, const string &, const int &, const double &);
void foo(const string &, const int &, const char[3] &);
void foo(const double &, const string &);
void foo(const char[3] &);

当我们需要知道包中有多少元素时,可以使用sizeof...运算符

template <typename ... Args> void g(Args... args)
{
    std::cout << sizeof...(Args) << std::endl; // 类型参数的数目
    std::cout << sizeof...(args) << std::endl; // 函数参数的数目
}

编写可变参数函数模板

可变参数函数通常是递归的,第一步先处理包中的第一个实参,然后用剩余的实参调用自身。为了终止递归,还需要定义一个非可变参数版本

// 负责终止递归
template <typename T>
ostream &print(ostream &os, const T &t)
{ return os << t; }
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... args)
{
    os << t;
    return print(os, args...); // 递归调用
}
假设给定调用print(cout, i, s, 42); 则递归过程如下:
+----------------------+----+--------+
     调用              |  t | args...
+----------------------+----+--------+
 print(cout, i, s, 42) |  i | s,42
+----------------------+----+--------+
 print(cout, s, 42)    |  s | 42
+----------------------+----+--------+
 print(cout, 42)  调用非可变参数版本
+------------------------------------+

// 前两次调用只能匹配可变参数版本,因为这两次调用分别传递三个和四个参数,而非可变参数版本只能接受两个参数
// 最后一次调用print(count, 42),两个版本都是可行的,但是非可变参数版本更加特例化,所以编译器选择此版本

包扩展

对于一个参数包我们唯一能做的就是扩展(expand)它。当扩展一个包时,我们需要提供用于每个元素的扩展模式。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表

template <typename T, typename... Args>
ostream & print(ostream &os, T t, const Args&... rest) // 扩展Args
{
    os << t;
    return print(os, rest...);                         // 扩展rest
}
// 对Args的扩展中,编译器将模式const Args&应用于每个元素,扩展结果是逗号分割的零个或多个类型列表,每个类型都形如const Type&
// print(cout, i, s, 42); // 此调用的实例化结果如下
// ostream & print(ostream &, const int &, const string &, const int &);

// 对rest的扩展中,模式就是参数包中的名字,扩展结果是逗号分割的参数列表,因此print(os, rest...)等价于:
// print(os, s, 42);

print中的函数参数包仅仅扩展为其构成元素,C++还允许更复杂的扩展模式。比如我们编写一个可变参数函数,对其每个实参调用debug_rep

template <typename... Args>
std::ostream & errMsg(std::ostream &os, const Args&... args)
{
    return print(os, "[", __FILE__, ":", __LINE__, "]", debug_rep(args)...);
    // 此调用使用了模式debug_rep(args),表示我们希望对函数参数包中的每个元素调用debug_rep,即
    // print(os, ... , debug_rep(a1), debug_rep(a2), ... , debug_rep(an))
}
errMsg(std::cout , "string:", "Hello World", ",", "int:", 10, ",", "double:", 3.14, "\n");

转发参数包

可变参数函数通常将参数转发给其他函数,这种函数通常具有与标准库emplace_back函数一样的形式

template <typename... Args>

void fun(Args&&... args) // 将Args扩展为一个右值引用列表,以保持实参的类型信息

{

        // 既扩展Args又扩展args,使用std::forward转发每个实参以保证潜在的移动语义

        work(std::forward<Args>(args)...);

}

15. 模板特例化

编写单一模板,使之对任何可能的模板实参都是最适合的,都能实例化,这并不总是能办到。在某些情况下,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或做得不正确,也有时候我们想用某些特定知识编写更高效的代码。当我们不能(或不希望)使用模板版本时,可以定义模板的一个特例化版本

// 版本1
template <typename T> int compare(const T &v1, const T &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}
// 版本2
template <size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M])
{ return strcmp(p1, p2); }

// 只有给compare传递字符数组或字符串字面值时,才调用版本2
// 如果传递字符指针,就会调用版本1,因为无法将一个指针转换为数组的引用
// 这样就有问题,本意是比较指针指向的字符串,实际上却比较的是指针的大小
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2); // 调用版本1
compare("hi", "mom"); // 调用版本2
// 为了处理字符指针,我们可以为版本1定义一个模板特例化版本
// 特例化一个函数模板时,必须为每个类型参数都指定实参,并使用template <>表明正在为模板提供一个特例化版本
template <>
int compare(const char* const &p1, const char* const &p2)
{ return strcmp(p1, p2); }
// 因为原模板参数类型为const T&即const引用,所以特例化版本中的参数也为const &p1
// 又因为指针指向字符串,所以指针类型为const char*即T为const char*

重载与模板特例化

特例化的本质是原模板的一个实例,而不是重载版本。因此特例化不影响函数匹配

模板及其特例化版本应该声明在同一个头文件中。所有同名模板(构成重载关系)的声明放在最前面,然后是这些模板的特例化版本

类模板特例化

比如我们把自定义类型作为无序关联容器的key使用,必须定义针对自定义类型的std::hash模板的特例化版本

namespace std {
template <>
struct hash<Sales_data> {
    typedef size_t result_type;
    typedef Sales_data argument_type;
    restult_type operator()(const argument_type &s) const
    {
        return hash<string>()(s.bookNo) ^
               hash<unsigned>()(s.units_sold) ^
               hash<double>()(s.revenue);
    }
};
}
// 自定义类型作为无序关联容器的key,还需要该类型支持operator==()
// std::hash<Sales_data>使用了Sales_data的私有成员,所以需要将它声明为Sales_data的友元
template <typename T> class std::hash;
class Sales_data {
friend class std::hash<Sales_data>;
    // 其他成员
};

类模板部分特例化(偏特化)

与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性

二、标准库

1.IO库

ios_base是io库最顶层的基类,位于头文件ios_base.h中,很多io标志位都定义在该类中

格式化标志:fmtflagsios_base::boolalpha
ios_base::hex
ios_base::fixed
ios_base::left
ios_base::right
...
io状态标志:iostateios_base::badbit
ios_base::eofbit
ios_base::failbit
ios_base::goodbit
打开模式:openmodeios_base::in
ios_base::out
ios_base::binary
ios_base::app
ios_base::trunc
ios_base::ate
文件指针标志:seekdirios_base::beg(SEEK_SET)
ios_base::cur(SEEK_CUR)
ios_base::end(SEEK_END)

basic_ios<_CharT, _Traits> 是个模板类,继承了ios_base,位于头文件basic_ios.h中,是所有io流类的基类

  template<typename _CharT, typename _Traits>
    class basic_ios : public ios_base
    {
    };

ios是basic_ios<char>的类型别名,位于头文件iosfwd中

typedef basic_ios<char>               ios;

类型ifstream和istringstream都继承自istream。因此,我们可以像使用istream对象一样来使用ifstream和istringstream对象,比如调用std::genline函数

(1) getline函数

istream& getline (istream& is, string& str, char delim);

istream& getline (istream& is, string& str);

从输入流种读取字符直到遇到指定的分隔符或换行符(\n)为止,返回的输入流可以直接作为判断条件

while (std::getline(cin, line))
    cout << line << std::endl;

 (2) 条件状态

如果badbit、failbit、eofbit任一个被置位,则检查流状态的条件都会失败

// 复位流的状态

void clear(iostate state = goodbit);

cin.clear(); // 复位所有错误状态

cin.clear(cin.rdstate() & ~cin.failbit); // 复位failbit位

2.顺序容器

3.泛型算法

标准库提供了超过100个算法,这些算法都有一致的结构,理解此结构可以帮助我们更容易地学习和使用这些算法

除了少数例外,标准库算法都对一个范围内的元素进行操作。我们将此范围称为"输入范围。接受输入范围的算法总是以前两个参数表示此范围,分别指向要处理的第一个元素和最后一个元素之后位置的迭代器

理解算法最基本的方法就是了解它们是否读取元素改变元素或者重排元素

3.1 不改变序列元素的算法

一些算法只会读取输入范围内的元素,而不改变元素。像find、count、equal、accumulate就属于这类算法

// accumulate算法对输入序列中的元素进行求和
// 第三个参数表示求和的初始值,比如对string序列就行求和(即字符串连接)
string sum = accumulate(v.cbegin(), v.cend(), string(""));

// equal用于判断两个序列是否相等,前两个参数表示第一个序列的范围,第三个参数表示第二个序列的首元素
int myints[] = {20,40,60,80,100};
std::vector<int> myvector(myints,myints+5);
if (std::equal(myvector.begin(), myvector.end(), myints))

// 只接受单一元素表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长
// 如果第二个序列除了第一个序列的元素之外后面还有元素,也被视为相等

3.2 改变序列元素的算法

一些算法将新值赋于序列中的元素,注意这些算法不会改变容器的大小,比如fill、copy、move、remove、replace、transform、unique、reverse、rotate

int myints[]={10,20,30,40,50,60,70};
std::vector<int> myvector(7);
std::fill(myvector.begin(), myvector.end(), 5); // myvector: 5,5,5,5,5,5,5
std::copy(myints, myints+7, myvector.begin()); // myvector: 10,20,30,40,50,60,70

// 移动一个序列中的元素到另一个序列
std::vector<std::string> foo = {"air","water","fire","earth"};
std::vector<std::string> bar;
std::move(foo.begin(), foo.end(), bar.begin()); // 可以移动不同序列,比如vector和list
bar = std::move(foo); // 通过移动赋值运算符移动整个容器,只能移动相同容器
                      // std::move()返回右值引用,所以会调用vector的operator=(std::vector &&);
// 以上两种移动方式都会导致foo中的元素处于未定义状态(容器状态是有效的),不要再使用它们
int myints[] = {10,20,30,30,20,10,10,20};      // 10 20 30 30 20 10 10 20
int* pbegin = myints;                          // ^
int* pend = myints+sizeof(myints)/sizeof(int); // ^                       ^
// 删除序列中指定的值
pend = std::remove(pbegin, pend, 20);          // 10 30 30 10 10 ?  ?  ?
std::replace(pbegin, pend, 10, 99);            // 99 30 30 99 99 ?  ?  ?
// 大小写转换
std::string str = "Hello World";
std::transform(str.begin(), str.end(), str.begin(), ::toupper);
// 必须用全局名字空间的toupper,即定义在cctype中的版本;如果用std::toupper会编译报错

3.3 排序算法

sort默认按照元素类型的<运算符进行排序

unique重新排序序列,将相邻的重复元素去除,返回一个指向不重复范围末尾的迭代器,调用unique后序列的大小不回改变

int myints[] = {10,20,20,20,30,30,20,20,10};
std::vector<int> myvec(std::begin(myints), std::end(myints));
std::sort(myvec.begin(), myvec.end()); // 10,10,20,20,20,20,20,30,30
auto it = std::unique(myvec.begin(), myvec.end()); // 10,20,30,?,?,?,?,?,?
myvec.erase(it, myvec.end()); // 10,20,300                    ^it

3.4 for_each算法

用于对序列中的每个元素执行指定的操作

std::vector<int> myvec = {10,20,30};
std::for_each(myvec.begin(), myvec.end(), [](int &i) {i++;});
std::for_each(myvec.begin(), myvec.end(), [](int i) {std::cout << i << " ";}); // 11 21 31
std::string str = "Hello World";
std::for_each(str.begin(), str.end(), [](char &c) {c = ::toupper(c);}); // str转换为大写

3.5 参数绑定

对于只有一两个地方使用的简单算法,lambda表达式是最有用的。如果需要在很多地方使用的操作,应定义为一个函数;如果一个操作需要很多语句才能完成,通常使用函数更好

对于需要捕获局部变量的lambda表达式,用函数替换它不是那么容易,比如编写一个查找大于指定长度的字符串的函数

void query(vector<string> words, size_t sz)
{
    // 按字符串长度排序
    stable_sort(words.begin(), words.end(), [](const string &a, const string &b)
                                                {return a.size() < b.size();});
    // 获取大于给定长度的元素位置
    auto wc = find_if(words.begin(), words.end(), [sz](const string &a)
                                                    {return a.size() >=sz;});
    for_each(wc, words.end(), [](const string &s) {cout << s << ' ';});
    cout <<  endl;
}

我们很容易编写一个同样功能的函数

bool check_size(const string &s, size_t sz) { return s.size() >= sz; }

但是不能把该函数作为find_if的第三个参数,因为其接受一个一元谓词,传给find_if的可调用对象必须接受单一参数。为了用check_size代替lambda,必须解决如何向sz传参的问题

对于这个问题,可以用标准库的std::bind函数实现。它接受一个可调用对象,生成一个新的可调用对象来适配原对象的参数列表

// 其调用的一般形式为

auto newCallable = std::bind(callable, arg_list);

newCallable是一个新的可调用对象,当调用newCallable时,newCallable会调用callable,并传给它arg_list的参数

arg_list是一个逗号分隔的参数列表,对应callable的参数,arg_list可能包含形如_n的参数占位符

// 使用check_size代替lambda表达式调用find_if
auto wc = find_if(words.size(), words.end(),
                    std::bind(check_size, std::placeholders::_1, sz));
// _1表示find_if传给新的可调用对象的参数,即words中的每个字符串元素
// _n都定义在placeholders名字空间中

std::bind可以适配参数的个数,也可以适配参数的顺序

using namespace std::placeholders;
auto g = std::bind(f, a, b, _2, c, _1);
// g的第一个参数绑定到f的第五个参数上,第二个参数绑定到第三个参数上
// 例如: 调用g(X,Y),会调用f(a,b,Y,c,X)

std::bind绑定引用参数,默认情况下非占位符参数会拷贝到新的可调用对象中,对于不能拷贝的类型就无法直接绑定

for_each(words.begin(), words.end(), [&os](const string &s){os << s << " ";});
// 编写一个函数代替上面的lambda
ostream &print(ostream &os, const string &s)
{ return os << s << " "; }
// 错误,ostream对象不能拷贝
for_each(words.begin(), words.end(), std::bind(print, os, _1)); // 错误
// 如果希望给bind传递一个不能拷贝的对象,必须使用标准库ref函数
for_each(words.begin(), words.end(), std::bind(print, std::ref(os), _1));
// ref返回一个可拷贝对象,封装了给定对象的引用,标准库还有一个cref函数用于生成包含const引用的类

3.6 迭代器

除了每个容器定义的迭代器之外,头文件iterator(只是包含了定义真正迭代器的内部头文件)还定义了几种额外的迭代器,包括:

  • 插入迭代器 (定义在bits/stl_iterator.h)
  • 流迭代器 (bits/stream_iterator.h)
  • 反向迭代器
  • 移动迭代器

插入迭代器接受一个容器,生成一个迭代器。向此类迭代器赋值时会向容器插入一个元素,插入迭代器有三种类型,区别在于元素插入的位置

  • back_inserter 创建一个使用push_back的迭代器
  • front_inserter 创建一个使用push_front的迭代器
  • inserter 创建一个使用insert的迭代器,该函数接受第二个参数,该参数必须是一个指向给定容器的迭代器,元素被插入到给定迭代器之前
// 调用front_inserter时,元素总是被插入到容器的第一个元素之前
list<int> lst1 = {1,2,3,4};
list<int> lst2, lst3;
copy(lst1.begin(), lst1.end(), front_inserter(lst2)); // 对每个元素调用push_front,lst2: 4,3,2,1
copy(lst1.begin(), lst1.end(), inserter(lst3, lst3.begin()); // '1,2,3,4'整体插入到begin()前面,lst3: 1,2,3,4 

 流迭代器

istream_iterator输入流迭代器,用于读取特定类型的对象,该类型必须定义了>>操作符

istream_iterator的操作
istream_iterator<T> it(is);流迭代器it从输入流读取类型为T的值
istream_iterator<T> end;尾后迭代器,相当于容器的end()
*it返回从输入流读取的值
it->member访问读取的类型为T的对象的某个成员
++it 或 it++从输入流读取下一个值
/*
[jianjunyang@~/R2.2.3.X]$ cat a.txt
aaa bbb
ccc
ddd
*/
std::ifstream ifs("a.txt");
std::istream_iterator<std::string> isit(ifs), isend;
// 用法1
//while (isit != isend)
//    std::cout << *isit++ << " "; // 使用输入流迭代器读取文件
// 用法2
// for_each(isit, isend, [](std::string s){std::cout << s << " ";}); // 输入流迭代器用作for_each的参数
// std::cout << std::endl;
// 输出结果一样:aaa bbb ccc ddd
std::vector<std::string> strvec(isit, isend); // 使用输入流迭代器读取文件内容到vector中
for_each(strvec.begin(), strvec.end(), [](std::string s){std::cout << s << " ";});

// 流迭代器作为标准库算法的参数
std::string nums = "1 2 3 4 5 6 7 8 9 10";
std::istringstream iss(nums);
std::istream_iterator<int> isit(iss), isend;
std::cout << std::accumulate(isit, isend, 0) << std::endl; // 55

C++输入流的>>操作符默认是以空格、tab、换行分隔的内容作为一个输入单元

ostream_iterator输出流迭代器,任何定义了<<操作符的类型都可以赋值给ostream_iterator,相当于对ostream_iterator绑定的输出流直接使用<<操作符

ostream_iterator的操作
ostream_iterator<T> it(os);将类型为T的值写到输出流os中
ostream_iterator<T> it(os, str);每个值后面都输出一个\0结尾的字符串
it = val调用<<操作符将val写入到it绑定的输出流
std::ofstream ofs("a.txt");
T val;
std::ostream_iterator<T> osit(ofs);
osit = val; // 把val写入文件a.txt,相当于ofs << val

// 对于单个对象的输出没有意义,ostream_iterator最大的用处是可以作为标准库算法的参数
std::vector<int> nums = {1,2,3,4,5,6,7,8,9,10};
std::ostream_iterator<int> osit(std::cout, ", ");
std::copy(nums.begin(), nums.end(), osit);
std::cout << std::endl;
// 输出结果:1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

内置数组、array、deque、vector、string的迭代器都是随机迭代器。标准库算法sort要求随机迭代器,因此不适合用于list

std::advance(iterator, n); // 使迭代器向前移动n个元素位置

std::distance(first, last);  // 计算[first,last)之间有多少个元素 

3.7 算法命名规范

_if版本的算法

接受一个元素值的算法通常还提供另一个_if的版本,该版本接受一个谓词代替元素值,比如

find(beg, end, val);    // 查找输入范围中第一次出现val的位置

find(beg, end, pred);  // 查找第一个使pred返回真的元素

_copy版本的算法

默认情况下重排元素的算法将重排后的元素写回给定的输入序列中,这些算法通常还提供另一个_copy的版本,将元素写到指定的输出位置,比如

reverse(beg, end); // 反转输入序列中的元素顺序

reverse_copy(beg, end, dest); // 将元素按逆序拷贝到dest

remove_copy_if(beg, end, dest, pred); // 拷贝元素到dest,除了使pred为真的元素

3.8 特定容器算法

与其他容器不同,链表类型容器list和forward_list定义了一些成员函数形式的算法,比如sort、merge、remove、reverse和unique,这两个容器只提供了前向迭代器双向迭代器

list的底层数据结构为带头双向循环链表

对于list和forward_list应该优先使用成员函数版本的算法而不是通用算法

splice成员,此算法是list特有的,用于将一个链表的元素搬迁到另一个链表

void splice (iterator position, list& x);  // 将链表x的元素转移到position之前,x变为空链表,不能是同一个链表
void splice (iterator position, list& x, iterator i);  // 将链表x位置为i的元素转移到position之前,该元素在x中被删除,可以是同一个链表
void splice (iterator position, list& x, iterator first, iterator last);  // 将链表x范围为[first,last)的元素转移到position之前,并在x中删除对应元素,可以是同一个链表

4.关联容器

对于有序关联容器,关键字key的类型必须定义了<操作符,或提供自定义的比较操作

bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{ return lhs.isbn < rhs.isbn; }
multiset<Sales_data, decltype(compareIsbn)*> bookstore(&compareIsbn);

multimapmultiset中查找元素,对于允许重复关键字的容器,查找给定关键字的元素稍微复杂,这里介绍两种方法:

<方法1>

lower_bound返回的迭代器,指向第一个具有给定关键字的元素

upper_bound返回的迭代器,指向最后一个匹配给定关键字的元素之后的位置

如果不存在给定关键字的元素,lower_bound和upper_bound返回相同的迭代器,指向不影响排序的关键字插入位置

multimap authors;
// 元素插入
for (auto beg = authors.lower_bound(search_item),
          end = authors.upper_bound(search_item);
      beg != end; beg++)
    cout << beg->second << endl;

<方法2>

equal_range接受一个关键字,返回一个迭代器pair,first和second成员的含义与upper_bound以及upper_bound相同

for (auto pos = authors.equal_range(search_item);
     pos.first != pos.second; ++pos.first)
    cout << pos.first->second << endl;

5. begin和end

我们知道每个标准库容器内部都定义了begin和end成员函数,此外还在std名字空间中定义了全局的begin和end,全局形式定义为函数模板,其参数可以是容器、string、内置数组

当全局begin和end的参数是容器时,最终会执行对应容器的begin和end成员函数,返回相应的容器迭代器

当全局begin和end的参数是内置数组时,分别返回指向第一个元素和最后一个元素之后的指针

全局形式定在bits/range_access.h中,每个容器的头文件都包含了该文件

root@localhost:~ # grep 'range_access' /usr/include/c++/4.8.2 -R
/usr/include/c++/4.8.2/array:#include <bits/range_access.h>
/usr/include/c++/4.8.2/unordered_map:#include <bits/range_access.h>
/usr/include/c++/4.8.2/unordered_set:#include <bits/range_access.h>
/usr/include/c++/4.8.2/vector:#include <bits/range_access.h>
/usr/include/c++/4.8.2/deque:#include <bits/range_access.h>
/usr/include/c++/4.8.2/forward_list:#include <bits/range_access.h>
/usr/include/c++/4.8.2/list:#include <bits/range_access.h>
/usr/include/c++/4.8.2/map:#include <bits/range_access.h>
/usr/include/c++/4.8.2/set:#include <bits/range_access.h>
/usr/include/c++/4.8.2/string:#include <bits/range_access.h>

// 全局版本的定义如下:

// 用于标准库容器
template<class _Container>
    inline auto
    begin(_Container& __cont) -> decltype(__cont.begin())
    { return __cont.begin(); }

template<class _Container>
    inline auto
    end(_Container& __cont) -> decltype(__cont.end())
    { return __cont.end(); }

// 用于内置数组
template<class _Tp, size_t _Nm>
    inline _Tp*
    begin(_Tp (&__arr)[_Nm])
    { return __arr; }

template<class _Tp, size_t _Nm>
    inline _Tp*
    end(_Tp (&__arr)[_Nm])
    { return __arr + _Nm; }
int myint[] = {10,20,30};
std::vector<int> myvec(std::begin(myint), std::end(myint)); // 用于内置数组
for (auto it = std::begin(myvec); it != std::end(myvec); ++it) // 用于vector
    std::cout << *it << std::endl;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值