Unite 2 (变量和基本类型)

因为引用本身不是一个对象,所以不能定义引用的引用

1
2
3
int ival=1024;
int &refVal=ival;
int &refVal2=refVal;

refVal初始化之后,无论何时你提到它的名字,它的行为就像它所引用的变量ival一样—它的”引用性”不能再被检测到(除非通过decltype).因此refVal2也被简单初始化为引用ival.

const的引用

  • 常量引用

    对”const的引用简称为 “常量引用””
    严格来说并不存在常量引用, 因为引用不是一个对象, 所以我们没法让引用本身恒定不变. 事实上 C++不允许随意改变引用所绑定的对象, 所以从这层意思上理解所有的引用又都算是常量. 引用的对象是常量还是非常量可以决定其所能参与的操作, 却无论如何都不会影响到引用和对象的绑定关系本身

  • 初始化和对const的引用
    引用的类型必须和其所引用对象的类型一致,但是有两个例外,第一种例外情况是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。允许为一个常量引用绑定非常量的对象, 字面值, 甚至是一个一般表达式

    1
    2
    3
    4
    5
    int i = 42;
    const int &r1 = i;
    const int &r2 = 42;
    const int &r3 = r1 * 2;
    int &r4 = 41 * 2; //错误,r4是一个普通的非常量引用
    1
    2
    3
    4
    5
    double dval = 3.14;
    const int &ri = dval;
    //等价于
    const int temp = dval;
    const int &ri = temp;

类型别名type alias

  • 传统的方法是使用关键字typedef

    1
    2
    typedef dobule wages; 
    typedef wages base, *p;
  • 在新标准中可以使用别名声明(alias declaration)来定义类型的别名

    1
    using wages = double;
  • 如果某个类型别名指代的是复合类型或常量, 那么不能把它直接用到声明语句中

    1
    2
    3
    4
    5
    typedef char* pstring;
    const pstring cstr = 0; //cstr是指向char的常量指针
    // const char* cstr = 0 这种理解是错误的, 声明语句用到 pstring时,其基本数据类型是指针

    const pstring *ps; //ps是一个指针, 它的对象是指向char的常量指针

auto
auto 一般会忽略掉顶层const,同时底层const会保留下来

1
2
3
4
5
const int ci = i, &cr = ci;
auto b = ci; //b是一个整数
auto c = cr; //c是一个整数(cr是ci的别名,ci本身是一个顶层const)
auto d = &i; //d是一个整型指针
auto e = &ci //e是一个指向整数常量的指针

指针本身是一个对象,它又可以指向另外一个对象.因此指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题.
顶层 const(top-level const)表示指针本身是个常量
底层 const(low-level const)表示指针所指的对象是一个常量

decltype

  • 如果decltype使用的是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)
    1
    2
    3
    4
    const int ci = 0, &cj = ci;
    decltype(ci) x = 0; // x的类型是const int
    decltype(cj) y = 0; // y的类型是const int&, y绑定到变量x
    decltype(cj) z; // 错误 z是一个引用必须初始化
  • 若使用的是一个表达式, 且结果类型是一个左值,则 decltype(exp)exp类型的左值引用,否则和exp类型一致
    1
    2
    3
    4
    5
    6
    int n = 0, m = 0;
    int *p = &n;
    decltype(n + m) c = 0; // c -> int
    decltype(n += m) d = c; // d -> int&
    decltype(*p) x //错误 x是 int&,必须初始化
    // 解引用指针可以得到指针所指的对象
  • decltype的表达式如果是加上了括号的变量,结果将是引用
    如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式. 变量是一种可以作为赋值语句左值的特殊表达式
    1
    2
    3
    int i = 0;
    decltype((i)) d; // 错误, d是int&, 必须初始化
    decltype(i) e; //正确, e是一个int

    decltype((variable)) 的结果永远是引用
    decltype(variable)结果只有当 variable 本身就是一个引用时才是引用


Unite 3 (字符串,向量和数组)

size_type

  • string类及其它多数标准库类型都定义了几种配套的类型,这些配套类型体现了标准库类型与机器无关的特性
  • string::size_type是一个无符号整数, 当n是一个具有复制的int,这表达式s.size() < n的判断结果几乎肯定是true,这是因为复制n会自动转换成一个比较大的无符号值
    1
    2
    3
    4
    5
    6
    7
    	string str = "abcd";
    cout << str.size() << endl;
    if (str.size() < -1)
    cout << "233" << endl;
    //输出
    //4
    //233

因为引用不是对象,所以不存在包含引用的vector

列表初始值还是元素数量?

1
2
3
4
vector<int> v1(10);     // v1有10个元素,每个的值都是0
vector<int> v2{10}; // v2有1个元素, 该元素值为10
vector<int> v3(10, 1); // v3有10个元素,每个的值都是1
vector<int> v4{10, 1}; // v4有两个元素
  • 如果使用圆括号, 可以说提供的值用来构造(construct)该对象

  • 如果使用花括号, 可以表达成我们想要列表初始化(list initialize)该对象

如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化, 就要考虑用这样的值来构造对象

1
2
3
vector<string> v1{"hi"}      //有一个元素
vector<string> v1{10}; //有10个默认初始化的元素
vector<string> v2{10, "hi"}; //包含10个值为 "hi"的元素

指针和数组

在大多数的表达式中,使用数组类型得对象其实是使用一个指向该数组首元素得指针

1
2
int ia[] = {0,1,2,3,4,5,6};
auto ia2(ia); //ia2是一个整型指针,指向ia的第一个元素

当使用decltype时不会发生上述转换

1
2
decltype(ia) ia3 = {0,1,2,3,4,5,6}; //ia3是一个含有7个整型的数组
ia3[4] = 233

C++11标准引入beginend函数,功能与容器中的两个同名成员功能类似

1
2
int int_arr[] = {0, 1, 2, ,3, 4, 5};
vector<int> ivec(begin(int_arr),, end(int_arr)) //利用数组初始化vector

可以使用类型别名和auto来简化多维数组的指针

1
2
3
4
5
using int_array = int[4];
typedef int int_array [4];
int ia[3][4];
for (int_array *p = ia; p != ia + 3; ++p)
for(int *q = *p; q != *p + 4; ++q)

Unite 4 (表达式)

除非必须,否者不用递增减运算符的后置版本

  • 后置版本需要将原始值储存下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费
  • 对于整型和指针类型来说,编译器可能对这种额外的工作进行一定的优化;但是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了

运算对象可按任意顺序求值

1
2
3
4
5
vector<int> arr{1,2,3,4,5};
auto it = arr.begin();
while(it != arr.end())
*it = *it++ * 2; //错误,将产生未定义行为
// 先求左侧的值: *it = *it * 2 or 先求右侧的值: *(it + 1) = *it * 2

sizeof运算符

  • 对引用类型执行sizeof运算得到被引用对象所占空间的大小
  • 对指针执行sizeof运算得到指针本身所占空间的大小
  • 对解引用指针指向sizeof运算得到指针指向对象的对象所占空间的大小,指针不需要有效
  • 对数组执行sizeof运算得到整个数组所占空间的大小
  • 对于string,vector等容器对象执行sizeof运算只返回该类型固定部分大小,不会计算对象中的元素

显示转换
cast-name<type>(expression)

  • type是转换的目标类型
  • expression是要转换的值
  • 如果type是引用类型那么结果是左值
  • cast-name
    • static_cast
      任何具有明确定义的类型转换,只要不包含底层const都可以使用static_cast
      1
      2
      int j = 10;
      double slope = static_cast<double>(j)
    • const_cast
      • 只能改变运算对象的底层const, 如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为,然而如果对象是一个常量,执行写操作就会产生未定义的后果
      • 不能使用const_cast改变表达式类型
        1
        2
        3
        const char *p;
        static_cast<string>(p); //正确,字符串字面值转换成string类型
        const_cast<string>(p); //错误,const_cast只改变常量属性
    • dynamic_cast
    • reinterpret_cast
      通过为运算对象的位模式提供较低层次上的重新解释
      1
      2
      int *ip;
      char *pc = reinterpret_cast<char*>(ip);

旧式的强制类型转换
* type(expr) 函数形式的强制类型转换
* (type) expr C语言风格的强制类型转换
根据所涉及的类型不同,旧式的强制类型转换分别具有与const_cast, static_cast,reinterpret_cast相似的行为


Unite 5 (语句)

switch内部定义的变量
c++语言规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个范围

1
2
3
4
5
6
7
8
9
10
11
12
case true:
string file_name; //错误,控制流绕过一个隐式初始化的变量
int ival = 0; //错误,控制流绕过一个显示初始化的变量
int jval; //正确,因为 jval没有初始化
break
case false:
jval = next_num(); //正确, jval被赋一个值, 但是它并没有初始化
//如果需要为某个`case`分支定义并初始化一个变量,我们应该把变量定义在块内
case true:
{
string file_name = get_file_name();
}

for(init-statement; condition; expresion)

和其他声明一样, init-statement也可以定义多个对象,但是只能有一条声明语句, 因此所有变量的基础类型必须相同

范围for语句

1
2
3
4
5
6
7
8
vector<int> v = {0,1,2,3,4,5};
for(auto &r : v)
r *= 2
//等价于
for(auto beg = v.begin(), end = v.end(); beg != end; ++beg){
auto &r = *beg
r *= 2
}

for语句中预存了end()的值,一旦在序列中添加(删除)元素,end()的值可能会变得无效


Unite 6 (函数)

尽量使用常量引用

如果函数无需改变引用实参的值,最好将其声明为常量引用.
把函数不会改变的形参定义成 (普通的) 引用会给函数调用者一种误导,即函数可以修改它的实参的值,此外还会极大地限制函数所能接收的实参类型,我们不能将const对象,字面值或者需要类型转换的对象传递给普通的引用形参

1
2
3
4
5
6
7
8
9
10
11
void reset(int &i);
void reset(int *ip);
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i);
reset(&ci); //错误,不能用指向const int对象的指针初始化 int*
reset(i);
reset(ci); //错误, 不能把普通引用绑定到const对象ci上
reset(42); //错误, 不能把普通引用绑定到字面值上
reset(ctr); //错误, 类型不匹配

数组形参

  • 当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针

    1
    2
    3
    4
    void print(const int*);
    void print(const int[]);
    void print(const int[10]); //这里的维度表示我们期望数组含有多少元素,实际不一定
    //上面三个函数是等价的, 每个函数的唯一形参都是 const int*
  • c++允许将变量定义成数组的引用

    1
    2
    3
    4
    5
    void print(int (&arr) [10])
    {
    for(auto elem : arr)
    cout << elem << endl;
    }
  • 传递多维数组
    所谓多维数组就是数组的数组,因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针. 数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略

    1
    2
    3
    4
    void print(int (*matrix)[10], int rowsize){};
    //等价于
    void print(int matrix[][10], int rowSize){};
    //matrix的声明看起来是一个二维数组,实际上形参是一个含有10个整型的数组的指针

含有可变参数的函数

initializer_list是一种标准库类型,用于表示某种特定类型的值的数组,initializer_list对象中的元素永远是常量

1
2
3
4
5
6
7
int sum(initializer_list<int> const &il)
{
int sum = 0;
for(auto i : il) sum += i;
return sum;
}
sum({1,2,3,4})

返回数组指针

  • Type (*function(parameter_list)) [dimension]

    1
    int (*func(int i))[10];
  • 使用别名

    1
    2
    3
    4
    typedef int arrT[10];
    //or
    using arrT = int[10];
    arrT* func(int i);
  • 使用尾置返回类型

    1
    auto func(int i) -> int(*)[10];
  • 使用decltype

    1
    2
    3
    4
    int odd[] = {1,3,5,7,9};
    int even[] = {0,2,4,6,8};
    decltype(odd)* func(int i);
    // decltype并不负责把数组类型转换成对应的指针

函数重载

不允许两个函数除了返回类型外其他所有的要素都相同

重载和const形参

  • 顶层const不影响传入函数的对象
    1
    2
    3
    4
    5
    Record lookup(phone); 
    Record lookup(const Phone); //重复声明

    Record lookup(Phone*);
    Record lookup(Phone* const); //重复声明
  • 如果形参是某种类型的指针或引用,则可以通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的
    1
    2
    3
    4
    5
    Record lookup(Account)
    Record lookup(const Account&) //作用于常量引用

    Record lookup(Account*)
    Record lookup(const Account*)

    顶层const只会影响函数形参是否可以修改, 调用时会发生二义性(ambiguous),
    底层const会影响到传入的对象是否可以修改, 编译器可以通过实参是否是常量来推断应该调用哪个函数,因此不会发生二义性

const_cast和重载

1
2
3
4
5
6
7
8
9
10
11
12
13
cosnt string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
//我们可以对两个非常量的string实参调用这个函数, 但返回的结果仍然是const string的引用

string &shoterString(string &s1, string &s)
{
auto &r = shorterString(const_cast<const stirng&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}
//const版本返回对const string的引用,这个引用事实上绑定在了某个初始的非常量实参上
//我们可以再将其转换为一个普通的string&, 这显然是安全的

当调用重载函数时有三种可能的结果:

  • 编译器找到一个与实参最佳匹配 best match 的函数,并生成调用
  • 找不到匹配的函数,编译器发出无匹配 no match 的错误信息
  • 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择,此时也发生错误,称为二义性调用ambiguous call

内联函数和constexpr函数

内联函数

调用函数一般比求等价表达式的值要慢一些. 在大多数机器上, 一次函数调用其实包含着一系列工作:调用前要先保存寄存器, 并在返回时恢复, 可能需要拷贝实参, 程序转向一个新的位置继续执行.

1
2
3
4
5
6
7
inline cosnt string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
cout << shorterString(s1, s2) << endl;
//将在编译过程中展开成类似下面的形式
cout << (s1.size() < s2.size() ? s1 : s2) << endl;

将函数指定为内联函数(inline), 通常就是将它在每个调用点上 “内联地” 展开, 在函数返回类型前面加上关键字inline,这样就可以将它声明成内联函数了

内联函数只是向编译器发出一个请求,编译器可以选择忽略这个请求.
一般来说内联机制用于优化规模较小,流程直接,频繁调用地函数,很多编译器不支持内联递归函数

constexpr函数

constexpr函数是指能用于常量表达式的函数, 需要遵循以下约定

  • 函数的返回类型及所有形参都必须为字面值类型
  • 函数体必须有且只有一条return语句
    1
    2
    3
    constexpr int new_sz() {return 233;}
    constexpr int num = new_sz();
    //执行该初始化任务时,编译器把对constexpr函数地调用替换成其结果值, 为了能在编译过程中随时展开, constexpr函数被隐式地指定为内联函数

允许constexpr函数的返回值并非一个常量

1
2
3
4
constexpr size_t scale(size_t cnt) {return new_sz() * cnt;}
int arr[scale(2)]; //正确, scale(2)是常量表达式
int i = 2;
int a2[scale(i)]; //错误,scale(i)不是常量表达式

constexpr函数不一定返回常量表达式

函数指针

函数指针指向的是函数而非对象,和其它指针一样,函数指针指向某种特定类型.
函数的类型由它的返回类型和形参类型共同决定,与函数名无关

1
2
3
bool lengthCompare(const string&, const string&);

bool (*pf)(const string&, const string&);

使用指针

  • 把函数名作为一个值时,该函数自动地转换成指针
    1
    2
    3
    pf = lengthCompare;
    //等价于
    pf = &lengthCompare;
  • 可以直接使用指向函数地指针调用该函数
    1
    2
    3
    bool b1 = pf("hello", "goodbye");
    //等价于
    bool b2 = (*pf)("hello", "goodbye");
  • 在指向不同函数类型的指针间不存在转换规则
  • 重载函数的指针
    1
    2
    3
    void ff(int*);
    void ff(unsigned int);
    void (*pf1)(unsigned int) = ff;
    指针类型必须与重载函数中的某一个精确匹配

函数指针形参

1
2
3
4
void useBigger(bool pf(const string&, const string&));
//等价于
void useBigger(bool (*pf)(const string&, const string&)); //显示的将形参定义成指向函数的指针
//我们可以直接把函数作为实参使用,此时它会自动转换成指针

使用别名

1
2
3
4
5
6
7
8
9
10
//func 和 func2 是函数类型
typedef bool func(const string&, const string&);
typedef decltype(useBigger) func2;

//funcP 和 funcP2 是指向函数的指针
typedef bool (*funcP)(const string&, const string&);
typedef decltype(useBigger)* funcP2;

void useBigger(const string&, const string&, func)
void useBigger(const string&, const string&, funcP2)

返回指向函数的指针
1
2
3
4
5
6
7
8
9
10
11
using F = int(int*, int);
using PF = int(*)(int*, int);

PF f1(int);

F* f1(int);

int (*f1(int)) (int*, int);

//尾置指针
auto f1(int) -> int (*) (int*, int);

autodecltype 用于函数指针类型

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
// 定义一个函数
int add(int a, int b) {
return a + b;
}

int sub(int a, int b){
return a - b;
}

// 返回指向函数的指针
auto getAddFunc() -> int (*)(int, int) {
return add;
}

auto getSubFunc() -> int (*)(int, int) {
return add;
}


int main() {
// 获取函数指针
auto func = getAddFunc();

// 使用函数指针调用函数
std::cout << "Result: " << func(2, 3) << std::endl;

decltype(func) func2 = getSubFunc();

std::cout << "Result: " << func2(2, 3) << std::endl;
return 0;
}


Unite 7 (类)

  • 成员函数通过名为 this 的额外的隐式参数来访问调用它的那个对象,当我们调用一个成员函数时,用请求该函数的对象初始化 this

    1
    2
    3
    4
    5
    class A;
    A a;
    a.func();
    等价于伪代码
    A::func(&a);

    默认情况下 this 是一个指向非常量类型的常量指针, 这意味着我们不能把 this 绑定到一个常量对象上. 这一情况下使得我们不能在一个常量对象上调用普通的成员函数
    C++允许把 const 放在成员函数列表之后表示 this 是一个指向常量的指针, 这样的成员函数被称作为常量成员函数(const member function).

  • 构造函数不能被声明为const, 当我们创建类的一个 const对象时,直到构造函数完成初始化过程后对象才能获取其”常量”属性.

  • 可变数据成员(mutable data member)

    一个可变数据成员永远不会是const,即使它是const对象的成员

    1
    mutable size_t access_ctr;
  • 一个 const 成员函数如果以引用的形式返回 *this, 那么它的返回类型将是常量引用

  • 类的声明

    • 可以像函数一样仅声明类而不定义它,这种声明有时被称为向前声明(forward declaration), 在它声明之后定义之前是一个不完全类型(incomplete type). 可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数

    • 在创建它的对象之前该类必须被定义过, 否则编译器就无法了解这样的对象需要多少存储空间

    • 直到类被定义之后数据成员才能被声明成这种类型,一个类的成员类型不能是该类自己,但可以是指向它自身类型的引用或指针

  • 友元的声明和作用域

    类和非成员函数的声明不是必须在他们的友元声明之前,当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的,然而友元本身不一定真的在当前作用域中.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct X{
    friend void f(){/*友元函数可以定义在类的内部*/}
    X() {f();} //错误, f 还没有被声明
    void g();
    void h();
    };
    void X::g(){return f();} //错误, f还没有被声明
    void f();
    void x::h(){return f();}
  • 类的作用域

    一旦遇到了类名, 定义的剩余部分就在类的作用域之内

  • 构造函数初始化列表

    当我们定义变量时习惯立即对其进行初始化

    1
    2
    3
    string foo = "Hello World!";    //定义并初始化
    string bar; //默认初始化成空的 string 对象
    bar = "Hello World"; //对 bar 赋一个新值

    对于对象的数据成员而言, 如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化

    构造函数的初始值有时必不可少

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class ConstRef{
    public:
    ConstRef(int ii);
    private:
    int i;
    const int ci;
    int &ri;
    }

    //ci和ri必须被初始化
    ConstRef::ConstRef(int ii){
    i = ii;
    ci = ii; //错误, 不能给const赋值
    ri = i; //错误, ri没被初始化
    }

    //显式的初始化1引用和const成员
    ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i){}

    建议使用构造函数初始值.
    在很多类中,初始化和赋值的区别事关底层效率问题,前者直接初始化数据成员,后者则先初始化再赋值.
    一些数据成员必须被初始化.

    成员初始化的顺序

    成员的初始化顺序与它们在类定义的出现顺序一致

    1
    2
    3
    4
    5
    6
    7
    class X{
    int i;
    int j;
    public:
    //为定义的: i在j之前被初始化
    X(int val): j(val), i(j){}
    }

    最好令构造函数初始值的顺序与成员声明的顺序保持一致. 尽量避免使用某些成员初始化其他成员

  • 隐式的类类型转换

    • 如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制.(编译器只会自动地执行一步类型转换)

    • 关键字 explicit抑制构造函数定义的隐式转换(只能在类内部声明构造函数时使用,在类外部定义时不因重复)

  • 聚合类

    • 所有成员都是public
    • 没有定义任何构造函数
    • 没有类内初始值
    • 没有基类
    1
    2
    3
    4
    5
    6
    struct Data{
    int ival;
    string s;
    }

    Data val = {0, "Anna"};
  • 类的静态成员

    • 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据
      静态成员函数也不与任何对象绑定在一起,它们不包含this指针, 所以静态成员函数不能声明成const
    • 当在类的外部定义静态成员时, 不能重复 static 关键字,该关键字只能出现在类内部
    • 静态数据成员的类型可以是它所属的类类型(非静态数据成员只能声明成它所属类的指针或引用)

Unite 8 (IO库)

  • io
头文件 类型
iostream istream, wistream 从流读取数据
ostream, wostream 向流写入数据
iostreawm,wiostream 读写流
fstream ifstream, wifstream 从文件读取数据
ofstream, wofstream 向文件写入数据
fstreawm,wfstream 读写文件
sstream istringstream, wistringstreamstring读取数据
ostringstream, wostringstreamstring写入数据
stringstreawm,wstringstream 读写string
  • 不能拷贝或对IO对象赋值,所以不能将形参或返回类型设置为流类型,读写一个IO对象会改变其状态,因此传递和返回引用不能是const

Unite 9 (顺序容器)

  • 为了创建一个容器的拷贝,两个容器的类型以及其元素及类型必须匹配。当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了,而且元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可

  • push or insert成员函数会先创建这个元素,然后再将这个元素拷贝或移动到容器中(如果是拷贝,那么事后会销毁先前创建的元素)
    emplace成员使用这些参数在容器管理的内存中直接构造元素,省去拷贝或移动的过程

  • 容器适配器
    适配器(adaptor)是一种机制,能使某种事物的行为看起来像另外一种事物一样,一个容器适配器接收一种已有的容器类型使其行为看起来像一种不同的类型, 如(stack, queue, priority_queue)


Unite 10(泛型算法)

  • bind
    标准库bind函数定义在头文件functional中, 可以将bind函数看作一个通用的函数适配器. 一般形式为:

    1
    auto newCallable = bind(callable, arg_list); 

    arg_list的参数可能就包括形如_n,其中n是一个整数,这些参数是占位符,表示newCallable的参数.

    名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间.
    using std::placeholders::_1; or using namespace std::placeholders;

    默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中. 标准库ref函数返回一个对象,包含给定的引用, cref生成一个保存const引用的类

    1
    for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' ' ));
  • 五种迭代器

类型 功能
输入迭代器 只读, 不写, 单遍扫描, 只能递增
输出迭代器 只写, 不读, 单遍扫描, 只能递增
前向迭代器 可读写, 多遍扫描, 只能递增
双向迭代器 可读写, 多遍扫描, 可递增递减
随机访问迭代器 可读写, 多遍扫描, 支持全部迭代器运算

Unite 11(关联容器)

  • 按关键字有序保存: map, set, multimap, multiset

  • 无序集合: unordered_map, unordered_set, unordered_multimap, unordered_multiset

  • 对于有序容器,关键字类型必须定义元素比较的方法,默认情况下使用关键字类型的<运算符来比较两个关键字

  • 关联容器额外的类型别名

    • key_type 容器关键字类型

    • mapped_type 与关键字关联的类型,只适用于map

    • value_type 对于setkey_type相同, 对于mappair<const key_type, mapped_type> (我们可以改变pair的值,但是不能改变关键字成员的值)

    • set的迭代器是const

  • 无序容器对关键字类型的要求
    默认情况下,无序容器使用关键字类型的 == 运算符来比较元素,还使用 hash<key_type>类型的对象来生成每个元素的哈希值.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    size_t hasher(const Obj &o){
    return hash<string>()(o.str);
    }
    bool eq(const Obj &lhs, const Obj &rhs){
    return lhs.str == rhs.str;
    }

    // arg 桶的大小,哈希函数指针,相等性判断运算符指针
    unordered_set<Obj, decltype(hasher)*, decltype(eq)*> st(10, hasher, eq);

Unite 12(动态内存)

直接内存管理

  • 在自由空间分配的内存是无名的,因此new无法为其分配的内存对象命名,而是返回一个指向该对象的指针.

    默认情况下,动态分配得对象是默认初始化的, 内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化

    1
    2
    3
    4
    string *ps1 = new string; //默认初始化为空string
    string *ps2 = new string(); //值初始化为空string
    int *pi1 = new int; //默认初始化,*pi1的值未定义
    int *pi2 = new int(); //值初始化为0, *pi2为0
  • 只有当括号中仅有单一初始化器时才可以使用auto

    1
    2
    auto p1 = new auto(obj); //p指向一个与 obj类型相同的对象,该对象用obj进行初始化
    auto p2 = new auto{a,b,c}; //错误,括号中只能有单个初始化器
  • 动态分配的 const 对象

    1
    const int *pci = new const int(1024);

    类似其它const对象,一个动态分配的const对象必须进行初始化。new 返回一个指向const的指针

  • delete之后重置指针值
    当我们delete一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上仍然保存着(已经释放了的)动态内存的地址, 此时指针变成了空悬指针(dangling pointer)。可以在delete之后将nullptr赋予指针

智能指针(smart pointer)

  • 定义在memory

    • shared_ptr 允许多个指针指向同一个对象
    • unique_ptr 独占所指向的对象
    • weak_ptr 伴随类,一种弱引用,指向shared_ptr所管理的对象
  • shared_ptrnew 结合使用
    如果我们不初始化一个智能指针它会被初始化为一个空指针, 我们还可以使用new返回来的指针来初始化智能指针,但接收指针参数智能指针构造函数时explict的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化的方式.

    1
    2
    shared_ptr<int> p1 = new int(1024); // false
    shared_ptr<int> p2(new int(1024)); //true

    一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针

    1
    2
    3
    4
    5
    6
    7
    shared_ptr<int> clone(int p){
    return new int(p);//false
    }

    shared_ptr<int> clone(int p){
    return shared_ptr<int>(new int(p));//true
    }
  • unique_ptr

    • 当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,类似shared_ptr,初始化时必须采用直接初始化形式
    • unique_ptr不支持普通的拷贝或赋值操作
    • 传递unique_ptr和返回unique_ptr
      不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要销毁的unique_ptr.
      1
      2
      3
      4
      5
      6
      7
      8
      unique_ptr<int> clone(int p){
      return unique_ptr<int>(new int(p));
      }

      unique_ptr<int> clone(int p){
      unique_ptr<int> ret(new int(p));
      return ret;
      }
  • weak_ptr

    • weak_ptr是一种不控制对象生存期的智能指针,它指向由一个shared_ptr管理的对象. 将weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数.
  • w.expried()w.use_count 为 0 则返回 true,否则返回false

  • w.lock()expriedtrue,则返回一个空的shared_ptr,否则返回一个指向w的对象的 shared_ptr


Unite 13 (控制拷贝)

当定义一个类时,我们显式地或隐式地指定在此类型的对象拷贝,移动,赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作

  • 拷贝构造函数(copy constructor)
  • 拷贝赋值运算符(copy-assignment operator)
  • 移动构造函数(move constructor)
  • 移动赋值运算符(move-assignment operator)
  • 析构函数(destructor)

我们称这些操作为拷贝控制操作(copy control)

拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的应用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数

  • 与合成默认构造函数不同,即使我们定义了其它构造函数,编译器也会为我们合成一个拷贝构造函数

  • 拷贝初始化

    1
    2
    3
    4
    5
    string dos(10, ',');  //直接初始化
    string s(dots); //直接初始化
    string s2 = dots; //拷贝初始化
    string null_book = "99-999-9999"; //拷贝初始化
    string nines = string(100, '9'); //拷贝初始化
    • 当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择我们提供的参数最匹配的构造函数
    • 当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换

    拷贝初始化不仅在我们用=定义变量时会发生,在下列情况下也会发生

    • 将一个对象作为实参传递给一个非引用类型的新参
    • 将一个返回类型为非引用类型的函数返回一个对象
    • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
  • 编译器可以绕过拷贝构造函数
    在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象

    1
    2
    3
    string null_book = "2333";
    //将上述代码改写为
    string null_book("2333");

    即使是这样,在这个程序点上,拷贝/移动构造函数必须是存在且可访问的

拷贝赋值运算符

  • 为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用

  • 与拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符(synthesized copy-assignment operator),对于某些类,合成拷贝赋值运算用来禁止该类型对象赋值。如果不是出于此目的,它会将右侧运算对象的非static成员赋予左侧运算对象的对应成员,对于数组类型成员,逐个赋值数组元素

析构函数

  • 析构函数释放对象使用的资源,并销毁对象的非static数据成员
  • 因为析构函数不接受参数,因此它不能被重载,对于一个给定类,只会有唯一一个析构函数
  • 隐式销毁一个内置指针类型的成员不会delete它所指向的对象

三/五法则

  • 需要析构函数的类也需要拷贝和赋值操作

  • 需要拷贝操作的类也需要赋值操作,反之亦然

  • 使用=default
    在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的, 如果我们不希望合成的成员函数是内联函数,应该只对成员的类使用=default(我们只能对具有合成版本的成员函数使用=default)

  • 阻止拷贝
    我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝

    1
    2
    3
    4
    5
    6
    struct NoCopy{
    NoCopy() = default;
    NoCopy(const NoCopy&) = delete;
    NoCopy& operator=(const NoCopy&) = delete;
    ~NoCopy() = default;
    }
    • 析构函数不能是删除的成员
      对于删除了析构函数的类型,虽然我们不能定义这种类型的变量或成员,但可以动态分配这种类型的对象,但是不能释放这些对象

      1
      2
      3
      4
      5
      6
      7
      struct NoDtor{
      NoDtor() = default;
      ~NoDtor() = delete;
      };
      NoDtor nd; //error
      NoDtor *p = new NoDtor(); //正确
      delete p; //error

      对于析构函数已经删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针

      • 如果一个类有数据成员不能默认构造,拷贝,复制或销毁,则对应的成员函数将定义为删除的
        • 一个成员有删除的或不可访问的析构函数会导致合成的默认和拷贝构造函数被定义为删除的,如果没有这条规则,我们可以会创建出无法销毁的对象
        • 对于具有引用成员或无法默认构造的const成员的类,编译器不会为其合成默认构造函数.对于一个类有const成员,则它不能使用合成的拷贝赋值运算符。虽然我们可以将一个新值赋予一个有引用成员,但这样做改变的是引用指向的对象的值,而不是引用本身,因此对于有引用成员的类,合成拷贝赋值被定义为删除的

拷贝控制和资源管理

  • 赋值运算符

    • 如果将一个对象赋予它自身,赋值运算符必须能正常工作
    • 大多数赋值运算符组合了析构函数和拷贝构造函数的工作

    一个好的模式是先将右侧运算对象拷贝到一个局部临时对象中,当拷贝完成后,销毁左侧运算对象的现有成员就是安全的了,一旦左侧运算对象的资源被销毁就可以将数据从临时对象拷贝到左侧运算对象的成员中了

  • 定义行为像值的类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <string>
    class HasPtr {
    public:
    HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) { }
    HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
    HasPtr& operator=(const HasPtr &hp) {
    auto new_p = new std::string(*hp.ps);
    delete ps;
    ps = new_p;
    i = hp.i;
    return *this;
    }
    ~HasPtr() {
    delete ps;
    }
    private:
    std::string *ps;
    int i;
    };
  • 定义行为像指针的类

    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>
    #include<string>
    class HasPtr{
    public:
    HasPtr(const std::string &s = std::string()) : ps(new std::string),
    i(0), use(new size_t(1)){}
    HasPtr(const HasPtr &other): ps(other.ps), i(other.i), use(other.use){
    ++*other.use;
    }
    HasPtr& operator=(const HasPtr &rhs){
    ++*rhs.use;
    if(--*this->use == 0){
    delete ps;
    delete use;
    }
    ps = rhs.ps;
    i = rhs.i;
    use = rhs.use;
    return *this;
    }
    ~HasPtr(){
    if(--*use == 0){
    delete ps;
    delete use;
    }
    }

    private:
    std::string *ps;
    int i;
    std::size_t *use;
    };

交换操作

  • 与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段

  • 在赋值运算符中使用swap

    1
    2
    3
    4
    HasPtr& HasPtr::operator=(HasPtr rhs){
    swap(*this);
    return *this;
    }

对象移动

  • 右值引用(rvalue reference)

    • 只能绑定到一个将要销毁的对象
    • 返回左值引用的函数,连同赋值,下标,解引用和前置递增/递减运算符,都是返回左值表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上
    • 返回非引用类型的函数,连同算术,关系,位以及后置递增/递减,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到着类表达式上

    变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行

    • move
      我们可以显示的将一个左值引用转换为对应的右值引用类型
      1
      2
      3
      int &&rr1 = 24;
      int &&rr2 = rr1; //error
      int &&rr3 = std::move(rr1); //true

      我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值

  • 移动构造函数和移动赋值运算符

    • 与拷贝构造函数一样,任何额外得实参都必须有默认实参

    • 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept

      在STL中,许多容器在调整大小(resize)时会调用元素的移动构造函数来移动资源。但是,为了保证内存安全,STL在大多数情况下只会调用被标记为不会抛出异常的移动构造函数,否则会调用其拷贝构造函数来作为替代。这是因为在资源的移动过程中如果抛出了异常,那么那些正在被处理的原始对象数据可能因为异常而丢失,而在拷贝构造函数中,由于进行的是资源拷贝操作,原始数据不会被更改,因此即使抛出异常也不会影响数据最终的正确性。

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

  • 只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符(编译器可以移动内置类型,如果一个成员是类类型,且该类有对应的移动操作, 编译器也能移动这个成员)

  • 与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。但是如果我们显示地要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。除了一个重要的例外,什么时候将合成的移动操作定义为删除的函数遵循与定义删除的合成拷贝操作类似的原则

    • 有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或有类成员为定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符类似
    • 有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的
    • 该类的析构函数被定义为删除或不可访问的, 则类的移动构造函数被定义为删除的
    • 如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的
    • 定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认地被定义为删除的
  • 如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来”移动”的。拷贝赋值运算符和移动赋值运算符的情况类似

  • 移动迭代器(move iterator)
    移动迭代器的解引用运算符生成一个右值引用,我们通过标准库 make_move_iterator函数将一个普通的迭代器转换为一个移动迭代器,此函数接收一个迭代器参数,返回一个移动迭代器

  • 右值引用和成员函数

    1
    2
    void push_back(const X&);
    void push_back(X&&);

    区分移动和拷贝的重载函数通常有一个版本接收一个const T&, 而另一个版本接收一个T&&

    • 右值和左值引用成员函数
      1
      2
      3
      string s1 = "a value", s2 = "another";
      auto n = (s1 + s2).find('a');
      s1 + s2 = "wow!";

      在旧标准中,我们没有办法阻止这种使用方式,为了维持向后兼容性,新标准库仍然允许向右值赋值,但是我们可能希望在自己的类中阻止这种用法,可以在参数列表后放置放一个引用限定符 (reference qualifier) &&&,分别指出this可以指向一个左值或右值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Foo{
      public:
      Foo& operator=(const Foo&) &; //只能向可修改的左值赋值
      };
      Foo &Foo::operator=(const Foo &rhs)&{
      //
      return *this;
      }
      //引用限定符只能用于(非static)成员函数,且必须出现在函数的声明和定义中

      一个函数可以同时用const和引用限定,引用限定符必须跟在const限定符之后

      1
      Foo anotherMem() const &;
  • 如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Foo{
    public:
    Foo sorted() &&;
    Foo sorted() const; //error

    using Comp = bool(const int&, const int&);
    Foo sorted(Comp*);
    Foo sorted(Comp*) const;
    }

Unite 14 (重载运算与类型转换)

基本概念

  • 如果一个运算符是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上

  • 对于一个运算符函数来说,它或者是类的成员,或者至少包含一个类类型的参数

    1
    2
    int operator+(int,int); //error 
    //这一约定意味着当运算符作用于内置类型的运算对象时,我们无法改变运算符的含义
  • 通常情况下不应该重载逗号,取地址,逻辑与和逻辑或运算符

  • 选择作为成员或者非成员

    • 赋值=, 下标[], 调用()->运算符必须是成员
    • 复合赋值运算符一般来说应该是成员,但并非必须
    • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增,递减,解引用运算符,通常应该是成员
    • 具有对称性的运算符可能转换任意一端的运算对象,例如算术,相等性,关系和位运算符等等,因此它们通常应该是普通的非成员函数

重载运算符

  • 输出运算符尽量减少格式化操作

  • 输入输出运算符必须是非成员函数

    • 如果是类的成员函数,它的左侧运算对象将是一个类对象
      1
      2
      String str;
      str << cout; //很抽象的写法
  • 假设输入输出运算符是某个类的成员,则它们也必须是 istreamostream的成员,然而,这两个类属于标准库,并且我们无法给标准库中的类添加任何成员

  • 输入运算符必须处理输入可能失败的情况,当读取发生错误时,输入运算符应该负责从错误中恢复

    1
    2
    3
    4
    5
    6
    7
    8
    9
    istream& operator>>(istream &is, Sales_data &item){
    double price;
    is >> item.bookNo >> item.units_sold >> price;
    if(is)
    item.renenue = item.units_sold >> price;
    else
    item = Sales_data();
    return is;
    }
  • 算术和关系运算符

    • 如果类同时定义了算术运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符

    • 关系运算符

      • 定义顺序关系(严格弱序)
      • 如果类同时也含有==运算符德胡啊, 如果两个对象是!=的,那么一个对象应该<另外一个
  • 赋值运算符

    • 除了拷贝赋值和移动赋值运算符,类还可以定义其它赋值运算符以使用别的类型作为右侧运算对象(赋值运算符必须定义为成员函数)

    • 复合赋值运算符通常情况下也应该定义为类的成员函数,这两类运算符都应该返回左侧运算对象的引用

  • 下标运算符

    • 下标运算符必须是成员函数
    • 它通常定义两个版本,一个返回普通引用,另一个是类的常量成员并且返回常量引用
  • 递增和递减运算符

    • 定义递增和递减运算符的类应该同时定义前置版本和后置版本。这些运算符通常应该被定义成类的成员
    • 前置运算符应该返回递增或递减后对象的引用,后置运算符返回对象的原值而非引用
    • 后置版本接受一个额外的(不被使用)int类型的形参
      1
      2
      3
      4
      5
      class StrBlobPtr{
      public:
      StrBlobPtr operator++(int);
      StrBlobPtr& operator++();
      }
  • 成员访问运算符

    • 箭头运算符必须是类的成员。解引用运算符通常也是类的成员。
    • 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象

函数调用运算符

  • 函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算对象符,相互之间应该在参数数量或类型上有所区别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class PrintString{
    public:
    PrintString(ostream &o = cout , char c = ' '):
    os(o), sep(c){ }
    void operator()(const string &s) const {os << s << sep;}

    private:
    ostream &os;
    char sep;
    }
  • lambda是一个函数对象

    • 当我们编写一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符

    • 通过值捕获的变量被拷贝到lambda中,因此这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      auto wc = find_if(words.begin(), words.end() [sz](const string&a)
      {return a.size() >= sz;});
      //该lambda表达式产生的类如下

      class sizeComp{
      public:
      SizeComp(size_t n): sz(n) { }
      bool operator() (const string &s) const
      {return s.size() >= sz;}
      private:
      size_t sz;
      };

      这种合成的类不含有默认构造函数,赋值运算符及默认析构函数

  • 标准库定义的函数对象

    • 定义在functional头文件中
      • 算术: plub<Type> minus<Type> multiplies<Types> divieds<Types> modulus<Type> negate<Type>
      • 关系: equal_to<Type> not_equal_to<Type> greater<Type less_euqal<Type
      • 逻辑: logical_and<Type> locical_or<Type> logical_not<Type>
    1
    2
    3
    4
    5
    6
    vector<string *> nameTable;
    sort(nameTable.begin(), nameTable.end(), [](string *a, string *b){
    return a < b;
    }); //错误 nameTable中的指针彼此之间没有关系,所以<将产生未定义的行为
    sort(nameTable.begin(), nameTable.end(), less<string*>()); //true

    • 关联容器使用less<key_type> 对元素排序
  • 可调用对象与function

    • C++语言有几种可调用的函数对象: 函数 函数指针 lambda表达式 bind创建的对象以及重载了函数调用运算符的类
    • 不同类型可能具有相同的调用形式
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      //普通函数
      int add(int i, int j){return i + j;}
      // lambda, 其产生一个未命名的函数对象类
      auto mod = [](int i, int j) {return i % j;};
      //函数对象类
      struct divide{
      int operator()(int denominator, int divisor){
      return denominator / divisor;
      }
      };
    • 标准库function类型
      1
      2
      3
      4
      5
      function<T> f;
      function<T> f(nullptr);
      function<T> f(obj);
      function<T> f;
      f; //将 f 作为条件: 当 f 含有一个可调用对象时为真
      function 是一个模板,当创建一个具体的function类型时我们必须提供额外的信息
    • function<T>的成员类型
      • result_type 返回对象的类型
      • argument_type
      • first_argument_type
      • second_argument_type
    • 重载的函数与function
      我们不能直接将重载函数的名字存入function类型的对象中
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      int add(int i, int j){return i + j;}
      Sales_data add(const Sales_data&, const Sales_data&);
      map<string, function<int(int,int)>> binops;
      binops.insert({"+", add}); //error

      //可以存储函数指针
      int (*ps)(int,int) = add;
      binops.insert({"+", fp});

      //可以使用 lambda来消除二义性
      binops.insert({"+", [](int a, int b){return add(a, b);}});
    • 简单应用
      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
      #include <iostream>
      #include <string>
      #include <map>
      #include <functional>

      int add(int i, int j) { return i + j; }
      auto mod = [](int i, int j) { return i % j; };
      struct Div { int operator ()(int i, int j) const { return i / j; } };

      auto binops = std::map<std::string, std::function<int(int, int)>>
      {
      { "+", add }, // function pointer
      { "-", std::minus<int>() }, // library functor
      { "/", Div() }, // user-defined functor
      { "*", [](int i, int j) { return i*j; } }, // unnamed lambda
      { "%", mod } // named lambda object
      };


      int main()
      {
      while (std::cout << "Pls enter as: num operator num :\n", true)
      {
      int lhs, rhs; std::string op;
      std::cin >> lhs >> op >> rhs;
      std::cout << binops[op](lhs, rhs) << std::endl;
      }

      return 0;
      }

重载 类型转换与运算符

  • 类类型转换运算符(conversion operator)
    1
    operator type() const;

    一个类型转换函数必须是类的成员函数,它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class
    class SmallInt{
    public:
    SmallInt(int i = 0): val(i){
    if(i < 0 || i > 255)
    throw std::out_of_range("Bad SmallInt value");
    }
    operator int() const { return val; }
    private:
    std::size_t val;
    }

    SmallInt si;
    si = 4; // 首先将4隐式转换成SmallInt,然后再调用SmallInt::operator=
    si + 3; // 首先将si隐式地转换成int, 然后执行整数的加法
  • 显示的类型转换运算符

    类型转换运算符可能产生意外结果

    1
    2
    3
    int i = 42;
    cin << i;
    // istream 的 bool类型转换运算符将 cin转换成bool,然后提升成int并用作内置的左移运算符的左侧运算对象

    C++11 新标准引入了显示的类型转换运算符

    1
    2
    3
    4
    5
    class
    class SmallInt{
    public:
    explicit operator int() const { return val; }
    };

    该规定存在一个例外,即如果表达式被用作条件,则编译器会将显示的类型转换自动应用于它

1
2
//无论我们什么时候在条件中使用流对象,都会使用为 IO 类型定义的 operator bool
while(std::cin >> value)

bool 类型转换通常用在条件部分,因此operator bool 一般定义成explicit

避免有二义性的类型转换 && 函数匹配与重载运算符 (>-<)

私认为避免有二义性的类型转换的最好方法就是避免有二义性,
重载 运算符重载函数 不要玩的太花
具体细节(P517)


Unite 15 (面向对象程序设计)

定义基类和派生类

  • 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
  • 任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义
  • 一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数
  • 派生类地声明
    1
    2
    3
    class Bulk_quote : public Quote; //error
    class Bulk_quote;
    //派生列表以及与定义有关的其他细节必须与类的主体一起出现
  • 从派生类向基类的类型转换只对指针或引用有效
  • 基类向派生类不存在隐式类型转换
  • 继承体系中的大多数类仍然(显示或隐式地)定义了拷贝控制成员,因此我们通常能够将一个派生类对象拷贝 移动或赋值给一个基类对象.
    1
    2
    3
    Bulk_quote bulk; //派生类对象
    Quote item(bulk); //使用 Quote::Quote(const Quote&) 构造函数
    item = bulk; //调用 Quote::operator(const Quote&)

虚函数

  • 动态绑定只有当我们通过指针或引用调用虚函数才会发生
  • 一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数
  • 一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与它覆盖的基类函数完全相同。 返回类型也必须与基类匹配(存在一个例外,当类的虚函数返回类型是类本身的指针或引用时),但是这样的返回类型要求从派生类到基类的类型转换是可访问的
  • 我们可以使用override说明符标记某个函数,当该函数没有福覆盖己存在的虚函数时编译器将报错
  • 我们可以使用final说明符将某个函数指定为 final,任何尝试覆盖该函数的操作都将引发错误
  • 如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定

    如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致

抽象基类

  • 纯虚函数
    我们通过在函数体位置添加=0就可以将一个虚函数说明为纯虚函数,其中=0只能出现在类内部的虚函数声明语句处
  • 含有纯虚函数的类是抽象基类,我们不能创建含有抽象基类的对象

访问控制与继承

  • 派生类的成员和友元只能通过派生类对象来访问基类的受保护成员
  • 如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行
  • 友元关系不能继承,每个类负责控制各自成员的访问权限
  • class关键字定义的派生类是私有继承的; struct关键字是公有继承的
  • 一个对象 引用或指针的静态类型决定了该对象的哪些成员是可见的
  • 派生类的成员将隐藏同名的基类成员
  • 假如基类与派生类的虚函数接收的实参不同,则我们就无法通过基类的引用或指针调用派生类的虚函数了
  • 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为

Unite 16 (模板与泛型编程)

定义模板

函数模板

  • 当使用模板时,我们(隐式地或显式地)指定模板实参,将其绑定到模板参数上

  • 模板定义以关键字template开始,后跟一个模板参数列表

  • 非类型模板参数
    一个非类型参数表示一个值而非一个类型,当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替

    1
    2
    3
    4
    template<unsigned N, unsigned N>
    int Compare(const char (&p1)[N], const char (&p2)[M]){
    return strcmp(p1, p2);
    }

    一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(右值)引用。(整型参数的实参必须是一个常量表达式,指针或引用必须具有静态的生存期)

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

类模板

  • 与函数模板不同,编译器不能为类模板推断模板参数类型,我们必须在模板名后的尖括号中提供额外信息
  • 定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表
  • 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化
  • 在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板参数
  • 类与友元各自是否是模板是相互无关的
  • 新标准允许我们为类模板定义一个类型别名
    1
    template<typename T> using twin = pair<T, T>;
  • 通过类来直接访问static成员,我们必须引用一个特定的实例

模板实参推断

  • 类型转换与模板类型参数

    • 将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换
    • 如果函数参数类型不是模板参数,则对实参进行正常的类型转换
  • 函数模板显示实参

    • 指定显示模板实参
      1
      2
      3
      4
      5
      template<typename T1, typename T2, typename T3>
      T1 sum(T2, T3);
      long long lng;
      int i;
      auto val = sum<long long>(i, lng); // long long sum(int, long)
      1
      2
      3
      int a = 1;
      double b = 2;
      std::max<double>(a, b);
    • 正常类型转换应用于显示指定的实参
  • 模板实参推断和引用

    • 引用折叠
      • 当一个左值引用引用一个左值引用(T& &)或者一个右值引用引用一个左值引用(T&& &),
        或左值引用的一个右值引用(T& &&), 它们会折叠成一个左值引用(T&)
      • 当一个右值引用引用一个右值引用(T&& &&),它会折叠成一个右值引用(T&&)

        引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数

    • 使右边值引用的函数模板通常进行重载
      1
      2
      template<typename T> void f(T&&); //绑定到非const右值
      template<typename T> void f(const T&); //左值和const右值

可变模板参数

  • 我们用一个省略号来指出一个模板参数或函数参数表示一个包,在一个模板参数列表中,class... or typename... 指出接下来的参数表示零个或多个类型的列表; 一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。 在函数列表参数中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包.例如:
    1
    2
    3
    4
    5
    6
    7
    8
    template<typename T, typename... Args>;
    void foo(const T &t, const Args& ... rest);

    // sizeof... 运算符
    template<typename ... Args> void g(Args ... args){
    cout << sizeof...(Args) << endl;
    cout << sizeof...(args) << endl;
    }

模板特例化

  • 为了指出我们正在实例化一个模板,应使用 template<>
  • 特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配
  • 我们只能部分特例化类模板而不能部分特例化函数模板






(左值 右值 左值引用 右值引用 右值引用引用一个左值引用. . . . . . 式姐能否一刀斩死右值 >_<)