C++学习笔记2

1. 类模板-模板类与友元

模板类的友元函数有三类:
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
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
#include <iostream>
using namespace std;

template<class T1, class T2>
class AA
{
friend void show();



T1 m_x;
T2 m_y;
public:
AA(const T1 x, const T2 y) :m_x(x), m_y(y)
{

}

friend void show(AA<T1, T2>& a)//编译器利用模板参数生成了友元函数,但是,这个函数不是模板函数
{
cout << "x= " << a.m_x << " , y = " << a.m_y.c_str() << endl;
cout << "show(AA<T1, T2>& a)" << endl;
}

//编译器会利用友元函数模板生成实例,和下面这些代码冲突了,所以出现了重定义
//函数模板具体化
//friend void show(AA<int, string>& a);//这样就很麻烦了,要为每一个类型创建一个友元函数
//friend void show(AA<char, string>& a);
};

AA<int, string> aa_1(8,"我是一只快乐鸟");

void show()
{
cout << "x= " << aa_1.m_x << " , y = " << aa_1.m_y.c_str() << endl;
}

//void show(AA<int,string>& a)
//{
// cout << "x= " << a.m_x << " , y = " << a.m_y.c_str() << endl;
// cout << "show(AA<int,string>& a)" << endl;
//}
//
//void show(AA<char, string>& a)
//{
// cout << "x= " << a.m_x << " , y = " << a.m_y.c_str() << endl;
// cout << "show(AA<char, string>& a)" << endl;
//}

int main()
{
AA<int, string> aa_2(8, "我是一只快乐鸟");
AA<char, string> aa_3(8, "我是一只快乐鸟");
AA<double, string> aa_4(8, "我是一只快乐鸟");

show();
show(aa_2);
show(aa_3);
show(aa_4);

}

这种方案的本质是:编译器利用模板参数帮我生成了友元函数。方便使用,注意:编译器利用模板参数生成了友元函数,但是,这个函数不是模板函数,而是友元函数实体
这样对我们写程序有什么影响:如果我们想为某种数据类型创建特别版本的友元函数(具体化)这种方法是无法做到,用该方法生成的友元函数只能用于这个模板类,不能用于其他的模板类.

2)约束模板友元:模板类实例化时,每个实例化的类对应一个友元函数。(最好的友元函数)

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;

template<typename T>
void show(T& a);//第一步:在模板类的定义前面声明友元函数模板

template<class T1, class T2>
class AA
{
friend void show<>(AA<T1, T2>& a);//第二步:在模板类中,再次声明友元函数模板
T1 m_x;
T2 m_y;

public:
AA(const T1 x, const T2 y) :m_x(x), m_y(y) {}
~AA() {}
};

template<typename T> //第三步:友元函数模板定义
void show(T& a) //通用类型函数模板
{
cout << "通用:x = " << a.m_x << " , y = " << a.m_y.c_str() << endl;
}

template<> //第三步:具体化版本
void show(AA<int, string >& a)//具体化函数模板
{
cout << "具体化<int, string >:x = " << a.m_x << " , y = " << a.m_y.c_str() << endl;
}

int main(void)
{
AA<int, string> aa_1(88, "我是小明");
show(aa_1);

AA<char, string> aa_2(88, "我是小婷");
show(aa_2);

return 0;
}

最好的友元方案,该方案的友元函数是函数模板,为模板设置友元分三个步骤
第一步:在模板类的定义前面,声明友元函数模板;目的是为了让模板类AA知道友元函数模板的存在
第二步:在模板类中再次声明友元函数模板;目的是让编译知道需要实例化的友元函数模板 ,类模板与函数模板本来是没有关系的,如下(1)代码让他们有关系,编译器在实例化某种数据类型的模板类时,也会实例化这种数据类型的模板函数

1
(1) friend void show<>(AA<T1, T2>& a);

第三步:友元函数模板的定义,放在模板类的下面;因为友元函数是函数模板,可以有具体化的版本。

这种友元的函数模板可以用于多个模板类

1
2
3
4
5
template<typename T>	//第三步:友元函数模板定义
void show(T& a) //通用类型函数模板
{
cout << "通用:x = " << a.m_x << " , y = " << a.m_y.c_str() << endl;
}
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
#include <iostream>
using namespace std;

template<typename T>
void show(T& a);//第一步:在模板类的定义前面声明友元函数模板

template<class T1, class T2>
class AA
{
friend void show<>(AA<T1, T2>& a);//第二步:在模板类中,再次声明友元函数模板
T1 m_x;
T2 m_y;

public:
AA(const T1 x, const T2 y) :m_x(x), m_y(y) {}
~AA() {}
};

template<class T1, class T2>
class BB
{
friend void show<>(BB<T1, T2>& a);//第二步:在模板类中,再次声明友元函数模板
T1 m_x;
T2 m_y;

public:
BB(const T1 x, const T2 y) :m_x(x), m_y(y) {}
~BB() {}
};

template<typename T> //第三步:友元函数模板定义
void show(T& a)
{
cout << "通用:x = " << a.m_x << " , y = " << a.m_y.c_str() << endl;
}

template<> //第三步:具体化版本
void show(AA<int, string >& a)
{
cout << "具体化<int, string >:x = " << a.m_x << " , y = " << a.m_y.c_str() << endl;
}

template<> //第三步:具体化版本
void show(BB<int, string >& a)
{
cout << "具体化<int, string >:x = " << a.m_x << " , y = " << a.m_y.c_str() << endl;
}

int main(void)
{
AA<int, string> aa_1(88, "我是小明");
show(aa_1);

AA<char, string> aa_2(88, "我是小婷");
show(aa_2);

BB<int, string> bb_1(88, "我是小明");
show(bb_1);

BB<char, string> bb_2(88, "我是小婷");
show(bb_2);

return 0;
}

优点:可以具体化、支持多个模板类

3)非约束模板类友元:模板类实例化时,如果实例化了n个类,也会实例化n个友元函数,每个实例化的类都拥有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
#include <iostream>
using namespace std;

template<class T1, class T2>
class AA
{
template<typename T> friend void show(T& a);
T1 m_x;
T2 m_y;

public:
AA(const T1 x, const T2 y) :m_x(x), m_y(y) {}
~AA() {}
};

template<typename T> //通用的函数模板
void show(T& a)
{
cout << "通用:x = " << a.m_x << " , y = " << a.m_y.c_str() << endl;
}

template<> //函数模板的具体版本
void show(AA<int, string >& a)
{
cout << "具体化<int, string >:x = " << a.m_x << " , y = " << a.m_y.c_str() << endl;
}

int main(void)
{
AA<int, string> aa_1(88, "我是小明");
show(aa_1);

AA<char, string> aa_2(88, "我是小婷");
show(aa_2);

return 0;
}

2. 类模板-模板类的成员模板

意思是在模板类中创建模板类和函数模板,开发中,模板类中有类模板和函数模板的情况很常见;

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
#include <iostream>
using namespace std;

template<class T1,class T2>
class AA
{
public:
T1 m_x;
T2 m_y;

AA(T1 x, T2 y) :m_x(x), m_y(y)
{
}
void show()
{
cout << "m_x=" << m_x << " , m_y=" << m_y << endl;
}

template<class T> //里面这个类模板参数可以和外面的类模板相同,也可以不同
class BB
{
public:
T m_a;
T1 m_b; //可以用AA的模参数T1和T2创建成员变量
T1 m_c;
//BB() {}
BB() {}
~BB() {}
//void show()
//{
// cout << "m_a=" << m_a << " , m_b=" << m_b << endl;
//}

void show();
};

BB<string> m_bb;

template<typename T>
void show(T tt)//是类模板AA的成员函数,也是函数模板
{
cout << "tt=" << tt << endl;
cout << "m_x=" << m_x << " , m_y=" << m_y << endl;
}

};

template<class T1, class T2>
template<class T>
void AA<T1,T2>::BB<T>::show()
{
cout << "m_a=" << m_a << " , m_b=" << m_b << endl;
}

int main()
{
AA<int, string> a(88, "我是小明");
a.show();
a.m_bb.m_a = "小婷";
a.m_bb.m_b = 66;
a.m_bb.show();
a.show("结束行");
}

3. 类模板-将模板类用作参数

链表数组,为了支持任意类型数据,最好的方法是用类模板来实现(物理结构不相同,但是逻辑结构是相同的)
模板的目的就是代码重用。链表和数组的逻辑结构是一样的,是否可以做成一个模板类呢?可以的。
链表这个模板类模板化目的为了兼容各种数据类型
C++支持模板的模板:把模板名当成一种特殊的类型,实例化对象的时候,可以用模板名作为参数,传给模板

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <iostream>
using namespace std;

template<class T1, int len>
class LinkList //链表类模板 被称为容器
{
public:
T1* m_head;
int m_len = len;

public:
LinkList();
~LinkList();
void insert()
{
cout << "向链表中插入一条记录。" << endl;
}
void ddelete()
{
cout << "向链表中删除了一条记录。" << endl;
}
void update()
{
cout << "向链表中更新一条记录" << endl;
}

private:

};

template<class T1, int len>
LinkList<T1, len>::LinkList()
{
}

template<class T1, int len>
LinkList<T1, len>::~LinkList()
{
}

template<class T1, int len>
class Array
{
public:
T1* m_data; //数组指针
int m_len = len; //表长
public:
Array();
~Array();
void insert()
{
cout << "向链表中插入一条记录。" << endl;
}
void ddelete()
{
cout << "向链表中删除了一条记录。" << endl;
}
void update()
{
cout << "向链表中更新一条记录" << endl;
}
private:

};

template<class T1, int len>
Array<T1, len>::Array()
{
}

template<class T1, int len>
Array<T1, len>::~Array()
{
}

//线性表模板类:tabletype-线性表类型 datetype-线性表数据类型
//template<class, int>class 模板类
template<template<class, int>class tabletype, class datatype, int len>
class LinearList
{
/*线性表模板类的代码实现,特别地方有:
那就是它的参数
*/
public:
tabletype<datatype, len> m_table; //创建线性表对象

LinearList();
~LinearList();

void insert() //线性表插入操作
{
m_table.insert();
}
void ddelete() { m_table.ddelete(); } //线性表删除操作
void updata() { m_table.update(); } //线性表更新操作
void oper() //按业务要求操作线性表
{
cout << "len=" << m_table.m_len << endl;
m_table.insert();
m_table.update();
}
private:

};

template<template<class, int>class tabletype, class datatype, int len>
LinearList<tabletype, datatype, len>::LinearList()
{
}

template<template<class, int>class tabletype, class datatype, int len>
LinearList<tabletype, datatype, len>::~LinearList()
{
}
int main()
{
//创建线性表对象,容器类型为链表,链表的数据类型为int,表长为20
LinearList<LinkList, int, 20> a;
//LinearList:线性表的类模板名,创建线性表对象
//LinkList:线性表第一个参数是容器的类型,容器都是类模板,所以填写类模板名
//第二个参数用于指定容器的数据类型,意思是容器中存放的是什么类型的数据
//第三个参数是指定容器的大小
a.insert();
a.ddelete();
a.updata();

//创建线性表对象,容器类型为链表,链表的数据类型为string,表长为20
LinearList<Array, string, 20> b;
b.insert();
b.ddelete();
b.updata();

/*他们选择不同容器 LinkList 和 Array 容器,容器可以不同,但是操作数据的方法是相同的*/
}

线性表模板类的代码实现,特别地方有:那就是它的参数
template<class, int>class tabletype的意思:表示 tabletype 不是一个普通的参数,而是模板,意思是这个参数要填写模板名,不要填写普通类型(int、string);
填什么样的模板名呢?填有两个参数的类模板名,一个参数的类模板名是不可以的。并且要求类模板的第一个参数是通用类型,第二个是非通用类型。
在如下代码中 class 也可以用 typename 代替,类模板更习惯用 class 而已

1
2
template<class, int>class tabletype
template<typename, int>class tabletype
1
template<template<class, int>class tabletype, class datatype, int len>

模板名通过参数 tabletype 传入到了类中,类中可以用 tabletype 创建对象;tabletype 是一个模板名, 用tabletype创建对象的时候,还需要指定具体的数据类型;tabletype应该用什么数据类型。
最常见的做法是:用模板参数从外面传进来,例如 把 datatype 填这里,len 填在这里
这个一般用于数据处理中

4. 编译预处理

C++程序编译的过程:预处理 -> 编译(优化、汇编) -> 链接
预处理指令主要有以下三种:
包含头文件:#include
宏定义:#define #undef
条件编译:#if #else #elif #end #ifnedf #if defined

5. 编译和链接

1)分开编译的好处:每次只编译修改过的源文件,然后在再链接,效率最高。
2)编译单个*.cpp 文件的时候,必须要让编译器知道名称的存在,否则会出现找不到标识符的错误。
3)编译单个*.cpp 文件的时候,编译器只需要知道名称的存在,不会把他们的定义一起编译。
4)如果函数和类的定义不存在,编译不会报错,但链接会出现无法解析的外部命令
5)链接的时候、变量、函数和类的定义只能有一个,否则会出现重定义错误。(如果把变量、函数和类的定义放在*.h 文件中,*.h 会被多次包含,链接前会存在多个副本,如果放在*.cpp 文件中,*.cpp 文件不会被包含,只会被编译一次,链接前只存在一个版本)(如果加上 static 可以避免被重复包含)
6)把变量、函数和类的定义放在*.h中是不规范的做法,如果*.h被多个*.cpp包含,会出现重定义。头文件加上防止重定义编译的宏定义
7)用 #include 包含 *.cpp 是不规范的做饭,原理同上
8)尽可能不使用全局变量,如果一定要用,要在 *.h 文件中声明(需要加 extern), *.cpp 中定义。
9)全局的const常量在头文件中定义 (const常量仅在文件内有效)
10)*.h 重复包含的处理方法只对单个的*.cpp文件有效,不是整个项目.
11)函数模板和类模板的声明和定义可以分开书写,但它们的定义并不是真实的定义,只能放在*.h文件中;函数模板和类模板的具体化版本的代码是真实的定义,所以放在*.cpp 文件中。
12) Linux下C++编译和链接的原理与VS一样。

6. C++命名空间

实际开发中,较大型的项目会使用大量的全局名字,如类、函数、模板、变量等,很容易出现名字冲突的情况。
命名空间分割了全局空间,每个命名空间是一个作用域,防止名字冲突
语法
创建命名空间:

1
2
3
4
namespace 命名空间的名字
{
//类、函数、模板、变量的声明和定义
}

创建命名空间的别名
namespace 别名 = 原名;

要使用命名空间的数据类型有三种方法:
第一种方法:在名字前面加上命名空间的名字和两个冒号,这种方法简单明了,不会造成任何冲突,但是使用起来比较繁琐,

1
2
3
4
5
6
7
namespace AA//自定义命名空间
{
int aa = 1;
}

//如果想使用命名空间中的变量
cout<<AA::aa<<endl;

第二种方法:用using声明,使用using声明要注意这样一个问题,如果再同一个区域出现了相同的名字,那么编译会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace AA//自定义命名空间
{
int aa = 1;
void func()
{

}
}

int main()
{
//如果想用 AA 命名空间的 aa ,但是在下面代码中不能再出现 aa 变量名
using AA::aa;
using AA::func;

cout<<aa<<endl;
func();
}

第三种方法:用using编译指令,使用using编译指令要注意这样一个问题,如果再同一个区域出现了相同的名字,虽然编译不会报错,但是会屏蔽之前创建的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace AA//自定义命名空间
{
int aa = 1;
void func()
{

}
}

int main()
{
using namespace AA;//有了这行代码后,main函数中,AA命名空间中的全部名字都可以用
}

注意事项
1)命名空间是全局的,可以分布再多个文件中(有命名空间和没有命名空间代码组织一样的,函数和类的声明在头文件中,函数和类的定义在源文件中;全局变量在头文件中声明,在源文件中定义)

namespace_new.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
#include "namespace_new.h"

namespace aa
{
int ab = 1; //全局变量

void A1::show()//类成员函数的类外实现
{
cout << "调用了A1::show()函数" << endl;
}

void func1()//调用了全局函数定义
{
cout << "调用了func()函数" << endl;
}
}
namespace_new.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma once

namespace aa
{
extern int ab;

void func1(); //全局函数声明

class A1
{
public:
void show(); //类的成员函数

};
}
main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "namespace_new.h"
#include<iostream>
using namespace std;
using namespace aa;

int main()
{
A1 a1;
a1.show();
func1();
cout << ab << endl;

}

还有同一个命名空间的代码可以分散在不同的文件中,例如把命名空间AA代码分开,如下所示

namespace_new.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
using namespace std;
#include "namespace_new.h"

namespace aa
{
void A1::show()//类成员函数的类外实现
{
cout << "调用了A1::show()函数" << endl;
}

void func1()//调用了全局函数定义
{
cout << "调用了func()函数" << endl;
}
}
namespace_new.h
1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once

namespace aa
{
void func1(); //全局函数声明

class A1
{
public:
void show(); //类的成员函数

};
}
namespace_new2.cpp
1
2
3
4
5
6
7
8
#include<iostream>
using namespace std;
#include "namespace_new2.h"

namespace aa
{
int ab = 1; //全局变量
}
namespace_new2.h
1
2
3
4
5
6
#pragma once

namespace aa
{
extern int ab;
}

总的来说,组织代码的方法,和没有命名空间相比,只是在代码外面,套了一个命名空间而已

2)命名空间可以嵌套。
3)在命名空间中声明变量,而不是使用外部全局变量和静态全局变量(应用经验。静态变量不适用原因:例如上面全局变量 ab 在源文件中定义,头文件中声明,不在头文件中声明,那么外部就不知道有该变量,效果和静态变量一样)
4)对于using声明,首选将其作用域设置为局部而不是全局 ;将这个代码放在main函数作用域中using AA::aa;,不要放置外面
5)不要在头文件中使用using编译指令,如果非要使用,应将它放在所有的 #include 之后。
6)匿名的命名空间,在当前文件中从创建的位置到文件结束有效。

1
2
3
4
5
6
7
8
9
namespace //匿名的命名空间
{
int ii = 2;
}

int main()
{
cout<<ii<<endl;
}

7. C++类型转换 static_cast

风格的类型转换很容易理解:
C语法:(目标类型)表达式或目标类型(表达式);
C++认为C风格的类型转换过于散,可能会带来隐患,不够安全。
C++推出了新的类型转换来替代C风格的类型转换,采用更严格的语法检查,降低使用风险。
C++新增四个关键字static_cast、const_cast、reinterpret_cast、dynamic_cast,用于支持C++风格的类型转换。
C++的类型转换只是语法上的解释本质上与C风格的类型转换没什么不同,C语言做不到事情的C++也做不到
语法
static_cast<目标类型>(表达式);实际开发中,这个用的多
const_cast<目标类型>(表达式);
reinterpret_cast<目标类型>(表达式);
dynamic_cast<目标类型>(表达式);

static_cast
1)用于内置数据类型之间的转换,除了语法不同,C和C++没有区别

1
2
3
double dd = 1.23;
long ll ;
ll = static_cast<long>(dd);\\C++风格

2)用于指针之间转换C风格可以把不同类型的指针进行转换。C++不可以,需要借助 void *

1
2
3
int ii = 10;
void *pv = &ii;
double * pd4 = static_cast<double*>(pv);

reinterpret_cast
static_cast不能用于转换不同类型的指针(引用) (不考虑有继承关系的情况),reinterpret_cast可以。
reinterpret_cast的意思是重新解释,能够将一种对象类型转换为另一种类型,不管他们是否有关系。(就是想怎么转就怎么转)
语法:reinterpret_cast<目标类型>(表达式);
<目标类型>(表达式)中必须有一个是指针(引用)类型。为什么会这么要求,因为它只为转换指针的应用场景而设计的
reinterpret_cast不能丢掉(表达式)constvolitale属性。

static_cast转换指针受到一些限制,C++设计的目标是:让static_cast满是程序员普通的需求;非常规的、特别的应用场景用reinterpret_cast;
应用场景
1)reinterpret_cast的第一种用途是改变指针(引用)的类型.
例如:把int*型指针直接转换成double*型指针,中间不借助void*

1
2
int ii= 10;
double * pd = reinterpret_cast<double*>(&ii);
1
2
3
4
5
6
double aa = 100.23;
int *aa_int = reinterpret_cast<int *>(&aa);
double * aa_double = reinterpret_cast<double*>(aa_int);

printf("%f\n",*aa_double);
cout << *aa_double << endl;

2)reinterpret_cast的第二种用途是将指针(引用)转换成整型变量。整型与指针占用的字节数必须一致,否则转换可能损失精度。

1
2
3
4
5
6
7
8
9
10
11
void func(void * ptr)
{
long long ii = reinterpret_cast<long long>(ptr);
cout << "ii=" << ii << endl;
}

int main()
{
long long ii = 10;
func(reinterpret_cast<void *>(ii));
}

3)reinterpret_cast 的第三种用途是将一个整型变量转换成指针 (引用)

const_cast
static_cast不能丢掉指针(引用)的constvolitale属性const_cast可以。

1
2
const int * aa = nullptr;
int *cc = const_cast<int *>(aa);

在实际开发中为什么要丢掉指针const呢?
如下定义一个函数funca(),形参用整形指针,那就用const_cast转换,把指针aaconst丢掉

1
2
3
4
5
6
7
8
9
void funca(int *ii)
{
}

int main()
{
const int * aa = nullptr;
funca(const_cast<int*>(aa));
}

7. C++的String容器

string是字符容器,内部维护了一个动态的字符数组。
与普通的字符数组相比,string 容器有三个优点:
1)使用的时候,不必考虑内存分配和释放的问题
2)动态管理内存(可扩展)
3)提供了大量操作容器的API。缺点是效率略有降低。

string类是std:basic_string类模板的一个具体化版本的别名。
using std:string=std::basic_string<char, std::char_traits<char>, std::allocator<char>>

构造和析构
静态常量成员string::npos为字符数组的最大长度(通常为unsigned int 的最大值);
NBTS (null-terminated string): C风格的字符串 (以空字符0结束的字符串)
string 类有七个构造函数(C++11新增了两个):
1)string():创建一个长度为0的string对象(默认构造函数)

1
2
3
4
string s1;//创建一个长度为0的string对象
cout << "s1=" << s1 << endl;
cout << "s1.capacity()=" << s1.capacity() << endl;//返回当前容器容量,可以存放字符的总数
cout << "s1.size() = " << s1.size() << endl; //返回当前容器中数据的大小

2)string(const char *s): 将string对象初始化为s指的NBTS(转换函数)

1

3)string(const string &str): 将string对象初始化为str(拷贝构造函数)。

1

4)string(const char *s,size_t n): 将string对象初始化为s指的NBTS的前n个字符,即使超过了(C风格)NBTS结尾,这里不判断结尾标志(这里的第一个参数不要理解为字符串,理解为一个地址string(const void *s,size_t n)

1
2
3
4
string s6("hello world", 5);
cout << "s6=" << s6.c_str() << endl;
string s7("hello world", 50);//会从这条字符串向后复制50个字符
cout << "s7=" << s7.c_str() << endl;

5)string(const string &str,size_t pos=0,size_t n=npos):sring对象初始化为str 从位置pos开始到结尾的字符,或从位置pos开始的n个字符。会判断字符串结尾标志

1
2
3
4
5
6
7
string s1 = "hello world";
string s8(s1, 3, 5);
cout << "s8=" << s8.c_str() << endl;
string s9(s1, 3);//从第三个位置开始,截取后面全部内容 lo world
cout << "s9=" << s9.c_str() << endl;
cout << "s9.capacity()=" << s9.capacity() << endl;
cout << "s9.size()=" << s9.size() << endl;

6)template<class T> string(T begin,T end):将string对象初始化为区间[begin,end]内的字符,其中 begin 和 end 的行为就像指针,用于指定位置,范围包括 begin 在内,但不包括 end
7)string(size_t n,char c): 创建一个由 n 个字符 c 组成的 string 对象。

C++11新增的构造函数:
1)string(string && str) noexcept: 它将一个 string 对象初始化为 string 对象 str,并可能修改str(移动构造函数)。(要学习了右值引用之后才知道它)
2)string(initializer_list<char> il): 它将一个string对象初始化为初始化列表il中的字符。例如: string ss ={‘h’,’e’,’l’,’l’,’o’};

8. string容器设置目标

弄清楚string容器设计目标
char cc[8];//是使用了连续的8个存储空间,
是以字节为最小存储单元的动态容器
用于存储放过字符串(不存放空字符0,空字符0是C风格字符串)
用于存放数据的内存空间(缓冲器),在这种情况下,什么样的数据都可以放到容器中

string内部的三个指针
char *start_;动态分配内存块开始的地址
char *end_;动态分配内存块最后的地址
char *finish_;已使用空间的最后的地址
因为有这三个指针,所以,用它存放字符串的时候,不需要空字符0

9. string容器设置目标

10. vector 容器

vector容器封装了动态数组
包含头文件:#include<vector>
vector类模板的声明:

1
2
3
4
5
6
7
8
template<class T, class Alloc = allocator<T>>
class vector
{
private:
T *start_;
T *finish_;
T *end_;
}

分配器
各种STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存如果省略该模板参数的值,将默认使用allocator<T>,用newdelete分配和释放内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

#include <iostream>
#include <vector>
using namespace std;


int main()
{
//创建一个空的 vector 容器
vector<int> v1;
cout << "v1.capacity()=" << v1.capacity() << " , v1.size()=" << v1.size() << endl;

//创建 vector 容器,元素个数为8(容量和实际大小都是n)
vector<int> v2(8);
cout << "v2.capacity()=" << v2.capacity() << " , v2.size()=" << v2.size() << endl;
}


11. 迭代器(其实就是像数组一样的访问他的元素)

为什么要使用迭代器
迭代器是访问容器中元素的通用方法。
如果使用迭代器,不同的容器,访问元素的方法是相同的。
迭代器支持的基本操作:赋值 (=)、解引用(*)、比较 (==和!=)、从左向右遍历 (++)
一般情况下,迭代器是指针和移动指针的方法

迭代器有五种分类:
1)正向迭代器
只能使用++运算符来遍历容器,每次沿容器向右移动一个元素。
容器名<元素类型>::iterator 迭代器名;// 正向迭代器
容器名<元素类型>::const_iterator 迭代器名; // 常正向选代器

迭代器失效问题

迭代器的实现

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//自定义一个数组类型,实现元素的添加,修改,移除,遍历
#include<iostream>
using namespace std;
//迭代器 :遍历容器,获取容器中的元素数据 ,进行某些操作 * ++ -- !=
class IntIterator
{
public:
IntIterator(int*p)
{
ptr = p;
}
int& operator*()
{
return *ptr;
}
void operator++()//前置
{
++ptr;
}
bool operator!=(const IntIterator&it)
{
return ptr != it.ptr;
}

private:
int*ptr;
};

//在不考虑任何意外的情况下实现
class Array
{
public:
//行为
Array(int capacity = 1)
{
this->capacity = capacity;
_size = 0;
data = new int[this->capacity];
}
~Array()
{
delete []data;
}

void append(int value)//从后面添加一个元素
{
data[_size++] = value;
}
void pop_back()//从后面移除一个元素
{
_size--;
}
void replace(int index,int value)//修改指定位置的值
{
//不能越界
data[index] = value;
}
int size()const
{
return _size;
}

IntIterator begin()//返回首元素的地址
{
return IntIterator(data);//创建了一个迭代器
}
IntIterator end()//返回最后一个元素的下一个位置的地址
{
return IntIterator(data+_size);//创建了一个迭代器
}

private:
int capacity;//容量,最多能够容纳的元素个数
int*data;
int _size;//已经存储的元素个数
};

int main()
{
Array arr{10};

arr.append(10);
arr.append(20);
arr.append(30);

//cout << arr << endl;
arr.pop_back();

//用迭代器对数组进行遍历
for(IntIterator it = arr.begin();it != arr.end();++it)
{
cout << "*it = " << *it << endl;//
}
//auto :根据对象的值,自动去推导其类型
for(auto it = arr.begin();it != arr.end();++it)
{
cout << "*it = " << *it << endl;
}
}

12.基于范围的for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
vector<int> vv = {1,2,3,4,5,6,7,8,9,10};

for(auto it = vv.begin(); it!= vv.end(); it++)//迭代器遍历容器vv
{
cout<< *it <<" ";
}
cout<< endl;

for(auto val:vv) //用基于范围的for循环遍历容器vv.
{
cout<< val <<" ";
}
cout<< endl;

for(int val:vv) 相当于把容器vv中的元素逐个赋值给变量val
对于一个有范围的集合来说,在程序代码中指定循环的范围有时候是多余的,还可能犯错误。
C++11中引入了基于范围的for 循环。
语法:

1
2
3
4
for(迭代的变量:迭代的范围)
{
//循环体
}

1)迭代的范围:可以填数组名、容器名、统一初始化列表或者其它可迭代的对象(支持begin()、end0、++、==)
迭代的变量:一般用auto关键字声明
2)数组名传入函数后,已退化成指针,不能作为容器名。
3)如果容器中的元素是结构体和类,迭代器变量应该申明为引用,加const约束表示只读。
4)注意迭代器失效的问题。

1
2
3
4
5
for(const auto &val:vv)	//用基于范围的for循环遍历容器vv.
{
cout<< val <<" ";
}
cout<< endl;

13.list容器

list容器封装了双链表。
包含头文件 #include <list>
统一初始化列表的语法:构造函数的形参是这个initializer list<T> il,完整语法list(initializer list<T> il)就有三种初始化写法

1
2
3
4
使用统一初始化列表创建list容器
list<int> l2({1,2,3,4,5,6,7,8,9,10});
list<int> l3 = {1,2,3,4,5,6,7,8,9,10};
list<int> l4 {1,2,3,4,5,6,7,8,9,10}

14.list容器的操作

15.pair键值对

pair是类模板,一般用于表示key/value数据,其实现是结构体
pair结构模板的定义如下:(C++中类和模板是同一种,类可以做成模板,结构体也可以做成模板)

1
2
3
4
5
6
7
8
9
10
11
12
teamplate<class T1, class T2>
struct pair
{
T1 first; //第一个成员,一般表示KEY.
T2 second; //第二个成员,一般表示value

pair(); //默认构造函数
pair(const T1 &val,const T2 &val2);//有两个参数的构造函数

pair(const pair<T1,T2> &p); // 拷贝构造函数
void swap(pair<T1,T2> &p); // 交换两个 pair。
};

make_pair函数模板的定义如下

1
2
3
4
5
teamplate<class T1, class T2>
make_pair(const T1 &first,const T2 &second)
{
return pair<T1,T2>(first,second);
}

STL定义了pair结构体模板和make_pair函数模板

下面两个函数,每个只会调用一次构造函数

1
2
3
4
auto p4 = make_pair<int,string>(6,"西施");	//make_pair返回临时对象	
auto p5 = pair<int,string>(5,"西施"); //匿名对象
auto p6 = make_pair(6,"西施"); //慎用,make_pair()函数自动推导,再调用拷贝构造,再调用拷贝构造
pair<int,string> p7 = make_pair(7,"西施"); //慎用,make_pair()函数自动推导,再调用拷贝构造,在转换

注意:
auto p5 = pair<int,string>(5,"西施");这行代码不要理解为先创建匿名对象,然后再调用拷贝构造函数创建对象p5;可以理解为先创建匿名对象,然后用了p5的名字;或者直接理解为:创建对象p5时,显式的调用了构造函数。
auto p4 = make_pair<int,string>(6,"西施");:不要理解为,make_pair创建了临时对象,函数返回后,P4使用拷贝函数;可以理解为和上面一样,是编译器做了优化处理
不同编译器可能会有点不同

pair键值对得value还可以用结构体和类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class T>
struct data_info
{
T data1;
T data2;
T data3;

data_info(T data1, T data2, T data3) :data1(data1), data2(data2), data3(data3)
{}
};

int main()
{
pair<data_info<int>, string>p4({43,9,03},"西施");
cout << p4.first.data1 << p4.first.data2 << p4.first.data3 << p4.second << endl;
return 0;
}

15.红黑树

二叉链表

1
2
3
4
5
6
7
struct BTNode
{
pair<K,V>P; //键值对。
BTNode*parent; //父节点
BTNode*lchirld; //左子树
BTNode*rchild; //右子树
}

16.map容器

map容器封装了红黑树(平衡二排序树),用于查找。
包含头文件: #ihclude<map>
map容器的元素是pair键值对。
map类模板的声明:

1
2
3
4
5
template <class K, class V, class P = less<K>, class Alloc = allocator<pair<const K, V >>>
class map : public _Tree<Tmap_traits< K, V, P,_ Alloc, false>>
{

}

第一个模板参数K:key的数据类型(pair.first)
第二个模板参数V: value 的数据类型 (pair.second)。
第三个模板参数P:排序方法,缺省按 key升序。
第四个模板参数 _Alloc:分配器,缺省用 new和 delete。
红黑树使用二叉链表,map可以提供双向迭代器

示例

1
2
3
4
5
6
7
8
9
10
11
//map() 创建一个空的map容器
map<int, string> m1;

//map(initializer_list<pair<k,v>>il) 使用统一初始化列表
map<int, string> m2({ {8,"西施"},{3,"冰冰"},{4,"晶晶"} ,{5,"欢欢"} });
//map<int, string> m2 = { {8,"西施"},{3,"冰冰"},{4,"晶晶"} ,{5,"欢欢"}};
//map<int, string> m2{ {8,"西施"},{3,"冰冰"},{4,"晶晶"} ,{5,"欢欢"}};
for ( auto &val:m2)//map正向遍历
{
cout << val.first << "=" << val.second << endl;
}

容器一般有的操作:
构造函数、特性操作、元素操作、复制操作、交换操作、比较操作、查找操作、插入和删除操作

16.哈希/散列表

哈希表:数组+链表
哈希表长(桶的个数):数组的长度
哈希函数:size_t hash(const T&key)
{
//key%小于哈希表长的最大质数
}
装填因子:元素总数/表长,其值越大,效率越低。

17.unordered_map容器(C++11标准新增加的) 无序的容器

实际开发中数据量只有几万,用红黑树也不错。如果数据量达到上千万,必须用哈希表
unordered_map 容器封装了哈希表,查找、插入和删除元素时,只需要比较几次 key 的值。
包含头文件: #include<unordered_map>
unordered_map 容器的元素是 pair键值对。
unordered_map 类模板的声明:

1
2
3
4
5
template <class K, class V, class _Hasher = hash<K>, class _Keyeq = equal_to<K>,class _Alloc = allocator<pair<const K, V>>>
class unordered_map : public _Hash< Umap_traits<K, V,_Uhash_compare<K,_Hasher,_Keyeq>,_Alloc,false>>
{

}

第一个模板参数K:key的数据类型(pair.first)。
第二个模板参数V: value 的数据类型(pair.second)
第三个模板参数_Hasher: 哈希函数,默认值为 std::hash<K>
第四个模板参数_Keyeq:比较函数,用于判断两个 key 是否相等,默认值是 std:equal to<K>
第五个模板参数_Alloc: 分配器,缺省用 new 和delete。

创建std::unordered_map 类模板的别名

1
2
template<class K,class V>
using umap = std::unordered_map<K, V>;