C++ 学习笔记3

Cpp12 之后的查漏补缺

原始字面量

在字符串前加 R 就可以输出原本的字符串,不需要转义。

语法:R”(string)”

1
cout << R"abc(c/fjk/)abc" << endl; // abc 是标签,需要前后相同,不被输出

通常在代码中很长的一行可以用反斜杠 \ 来进行多行显示,此时用 R 也可以分多行进行显示。

使用不同的分隔符(标签)可以帮助避免在原始字符串中不小心结束了字符串。例如,如果你选择的分隔符在原始字符串中从未出现,那么编译器就能够清楚地知道原始字符串的开始和结束位置。

内存空间

释放堆区内存后,要把指针指向的内存置为空。

内存空间:

pirjcAx.png

void 关键字

  • 函数形参用 void* 代表接受任意数据类型的指针。
  • 在向一个有 void* 形参的函数传入一个指针后,不能直接在函数中解引用,需要在函数中将形参转换为具体的类型,如 int*
  • 当有一个 char 类型变量 a,使用 & 取地址值显示的是乱码,此时用 (void*)& a 转换,即可正常输出。

数组排序

qsort 函数

原型:

1
2
3
4
5
6
7
#include <stdlib.h>
void qsort(
void *base, // 数组名,即起始地址
size_t nmemb, // 数组元素个数
size_t size, // 数组中单元素大小
int (*compar)(const void * p1, const void * p2) // 回调函数,即排序方法,返回 <0 代表升序(p1在p2前面),返回 0 代表不确定,返回 >0 代表降序(p2在p1前面)。
);

使用实例如下:

void* 类型不能直接解引用,需要具体转换后。

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

using namespace std;

int compar(const void* p1, const void* p2)
{
return *static_cast<const int*>(p1) - *static_cast<const int*>(p2);
}

int main()
{
int arr[] = {2, 5, 9, 6, 7, 3, 1, 4, 0};

auto f = [](const void* p1, const void* p2){
return *((int*)p1) - *((int*)p2);
};

qsort(arr, sizeof(arr)/sizeof(int), sizeof(int), compar);

for (auto &x : arr)
cout << x << " ";

cout << endl;

return 0;
}

二分查找

pirzlcD.png

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

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

bool search(int arr[], int len, int num)
{
int low = 0, high = len-1, mid;
while (low <= high)
{
mid = (low+high)/2;
if (arr[mid] == num) // 这里是判断 arr[mid] 的值,所以之后就是判断 mid 前和 mid 后了。
{
return true;
}
else if (arr[mid] < num)
{
low = mid + 1;
}
else
{
high = mid - 1;
}
}
return false;
}

int main()
{
int arr[5] = {1,2,3,4,5};

cout << search(arr, sizeof(arr)/sizeof(arr[0]), 3) << endl;

return 0;
}

单向链表实现

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
#include <iostream>

using namespace std;

struct Node
{
int num;
struct Node* next;
};

int main()
{
struct Node* head = nullptr, *tail = nullptr, *tmp = nullptr;

tmp = new Node({1, nullptr}); // 初始化方法1
head = tail = tmp; // 第一个节点,既是头指针也是尾指针。

tmp = new Node; // 初始化方法 2
*tmp = {2, nullptr};

tail->next = tmp; // 将第二个节点的指针赋值给第一个节点的 next,这里的 tail 是上一个节点的指针。
tail = tmp; // 将当前节点设置为尾指针。

tmp = new Node; // 初始化方法 3
tmp->num = 3;
tmp->next = nullptr;

tail->next = tmp;
tail = tmp;

tmp = new Node({4, nullptr});
tail->next = tmp;
tail = tmp;

// 遍历单向链表
tmp = head;
while (tmp != nullptr)
{
cout << tmp->num << endl;
tmp = tmp->next;
}

// 释放链表
while (head != nullptr)
{
tmp = head;
head = head -> next;
delete tmp;
}

return 0;
}

枚举

枚举也就是除去 #define 和 const 定义常量的另一种方式。

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

using namespace std;

enum Color {blue, yellow, red}; // 默认 0,1,2

int main()
{
Color c1 = blue;
Color c2 = Color(2); // 将整数类型强转为枚举类型。

switch(c1)
{
case blue:
..
break;
}
return 0;
}

引用

如果引用的数据对象类型不匹配,当引用为 const 时,c++ 将创建临时变量,让引用指向临时变量。

1
2
3
4
5
6
void func(const int&, const string&);

int main()
{
func(1, "string"); // 因为是 const 修饰的形参,所以可以创建并指向临时变量,并转换为正确类型
}

何时创建临时变量:

  • 引用是 const
  • 数据对象的类型是正确的,但不是左值
  • 数据对象的类型不正确,但可以转换为正确的类型。

将引用形参声明为 const 的理由:

  • 使用 const 可以避免无意中修改数据的编程错误
  • 使用 const 使函数能够处理 const 和 非 const 实参,否则将只能接受非 const 实参。
  • 使用 const ,函数能正确生成并使用临时变量。

引用用于函数返回值

  • 当函数要返回一个引用时,返回值应该是引用、静态变量、全局变量等,不能是局部变量(因为销毁后是野指针)。
  • 因为返回引用后,依旧可以修改原本传入引用参数的实参的值,因此可以在返回值前加 const 约束,避免被修改。
1
2
3
4
5
6
7
cosnt int &func(int &num);

int main()
{
int a = 3;
const int& b = func(a); // 函数返回值是 const 修饰,那么接收返回值的变量也需要 const 修饰
}

形参使用场景(值传递、地址、引用)

  1. 不需要在函数中修改实参
    • 实参很小,如 c++ 内置数据类型或小型结构体,则按值传递
    • 实参是数组,使用 const 指针(因为数组没有建立引用的说法)
    • 实参是较大的结构,使用 const 指针或 const 引用
    • 数据实参是类,则使用 const 引用,传递类的标准方式是按引用传递
  2. 需要在函数中修改实参
    • 实参是内置数据类型,使用指针。
      因为可读性更好,看到 func(&x) 就知道传递指针,需要修改值。
    • 实参是数组,只能使用指针。
    • 实参是结构体,使用指针或引用。
    • 实参是类,使用引用。

练习

p2:

C 风格字符串二维数组输出月份如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
int month;
cout << "Enter month:";

std::cin >> month;

char out[12][10] = {"January", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
"Sep", "Oct", "Nov", "Dec"};

if (month > 0 && month <13)
cout << out[month-1] << endl;
}

使用 C++ 的 string 则只需要一维数组即可。

p6:

实现 strlen 的方法:

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
#include <iostream>
#include <cstring>

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

// 数组方式仍然要转换为指针
// int length(char arr[])
// {
// int len;

// for(len = 0; arr[len] != '\0'; len++)
// {
// }
// return len;
// }

// // while 指针实现,效率最高
// int length(char* arr)
// {
// int len = 0;
// char* ptr = arr;
// while (*ptr++) // 当前字符不为0,且判断之后递增指针
// {
// ++len;
// }

// return len;
// }

// 递归实现
int length(char* arr)
{
if (*arr == '\0') return 0; // 递归终止条件

arr++;

int len = length(arr) + 1;

return len;

// 可以简化上面三行如下:
// return length(++arr) + 1;
}

int main()
{
char temp[200];
std::memset(temp, 0, sizeof(temp));

cout << "输入字符串:" << endl;
cin >> temp;
cout << length(temp) << endl;

return 0;
}

p7:

逆序输出字符串

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
#include <iostream>
#include <cstring>

using namespace std;

char* absort(char* str) // 0 1 2
{
char* newstr = new char[50];
std::memset(newstr, 0, sizeof(newstr));
int len = strlen(str); // 3

for (int i = 0; len > 0; len--, i++)
{

newstr[i] = str[len-1];
// cout << str[len] << endl;
}

return newstr;
}

int main()
{
char str[50];
std::memset(str, 0, sizeof(str));

cin >> str;

cout << "Normal:" << str << endl;

char* abstr = absort(str);

cout << "Abnormal:" << abstr << endl;

delete [] abstr;

// 直接逆序输出
// for (int i = strlen(str)-1; i >= 0; i--)
// {
// cout << str[i] << " ";
// }

// 不借助字符串
for (int i = 0, len = strlen(str); i < len / 2; i++)
{
int temp = str[i];
str[i] = str[len - 1 - i];
str[len - 1 - i] = temp;
}
cout << str << endl;

return 0;
}

p114:

memcpy(效率更高,但是不能处理内存重叠的问题) 和 memmove(用于处理内存重叠的问题)。

课后作业四

静态顺序表

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#include <iostream>
#include <cstring>

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

#define MAXSIZE 100
typedef int ElemType;

struct SeqList
{
ElemType data[MAXSIZE];
size_t length;
};

void ClearList(SeqList& list)
{
list.length = 0;
memset(list.data, 0, sizeof(list.data)); // ?
}

bool InsertList(SeqList& list, const size_t pos, const ElemType& ee)
{
if (list.length == MAXSIZE)
{
cout << "full list" << endl;
return false;
}

// 判断pos 位置是否合法
if (pos < 1 || pos > list.length + 1)
{
cout << "pos error" << endl;
return false;
}

// 将 pos 及其之后元素后移
// 1 2 3 4 5 6 7 8 9
// 0 1 2 3 4 5 6 7 8
if (pos < list.length + 1)
memmove(&list.data[pos], &list.data[pos - 1], (list.length - pos + 1) * sizeof(ElemType)); // ?

list.data[pos - 1] = ee;

list.length++;
return true;
}

size_t LengthList(const SeqList& list)
{
return list.length;
}

bool GetElem(const SeqList& list, const size_t pos, ElemType& ee)
{
if (pos < 1 || pos > list.length)
{
cout << "get error" << endl;
return false;
}

ee = list.data[pos - 1];
return true;
}

bool PushFront(SeqList& list, const ElemType& ee)
{
if (list.length == MAXSIZE)
{
cout << "full list" << endl;
return false;
}
InsertList(list, 1, ee);
return true;
}

bool PushBack(SeqList& list, const ElemType& ee)
{
if (list.length == MAXSIZE)
{
cout << "full list" << endl;
return false;
}
InsertList(list, list.length + 1, ee);
return true;
}

size_t FindElem(const SeqList& list, const ElemType& ee)
{
for (int i = 0; i < list.length; i++)
{
if (list.data[i] == ee)
return i + 1;
}
return 0;
}

bool DeleteElem(SeqList& list, const size_t pos)
{
// 判断pos 位置是否合法
if (pos < 1 || pos > list.length + 1)
{
cout << "pos error" << endl;
return false;
}

memmove(list.data + pos - 1, list.data + pos, (list.length - pos)* sizeof(ElemType));
list.length--;
return true;
}

bool PopFront(SeqList& list)
{
return DeleteElem(list, 1);
}

bool PopBack(SeqList& list)
{
return DeleteElem(list, list.length);
}

bool IsEmpty(const SeqList& list)
{
if (list.length == 0)
return true;
return false;
}

void PrintList(const SeqList& list)
{
for (int i = 0; i < list.length; i++)
{
cout << list.data[i] << " ";
}
cout << endl;
}

int main()
{
SeqList LL; // 创建顺序表。
ClearList(LL); // 清空顺序表。

ElemType ee; // 创建一个数据元素。

cout << "在表中插入元素(1、2、3、4、5、6、7、8、9、10)。\n";
ee = 1; InsertList(LL, 1, ee);
ee = 2; InsertList(LL, 1, ee);
ee = 3; InsertList(LL, 1, ee);
ee = 4; InsertList(LL, 1, ee);
ee = 5; InsertList(LL, 1, ee);
ee = 6; InsertList(LL, 1, ee);
ee = 7; InsertList(LL, 1, ee);
ee = 8; InsertList(LL, 1, ee);
ee = 9; InsertList(LL, 1, ee);
ee = 10; InsertList(LL, 1, ee);

PrintList(LL);

cout << "在表头插入元素(11),表尾插入元素(12)。\n";
ee = 11; PushFront(LL, ee);
ee = 12; PushBack(LL, ee);

PrintList(LL);

cout << "在第5个位置插入元素(13)。\n";
ee = 13; InsertList(LL, 5, ee);

PrintList(LL);

cout << "删除表中第7个元素。\n";
DeleteElem(LL, 7); PrintList(LL);

cout << "删除表头元素。\n";
PopFront(LL); PrintList(LL);

cout << "删除表尾元素。\n";
PopBack(LL); PrintList(LL);

GetElem(LL, 5, ee);
cout << "第5个元素的值是" << ee << "。\n";

ee = 8;
cout << "元素值为8的位置是=" << FindElem(LL, ee) << endl;

return 0;
}

简单对象模型

  • 类对象的大小是内存对齐后的非静态成员变量大小之和。成员函数和静态成员不算在类大小中,分散在内存的其它区域,相当于类有一个内存表将各种成员整合起来。
  • 类没有非静态成员变量时,编译器会分配一个占位变量,大小为 1 字节。类的对象的地址是第一个非静态成员变量的地址。
  • 将一个类的对象初始化为 nullptr ,该空指针对象可以调用没有用到 this 指针的非静态成员变量,如果用到 this 指针,则意味着访问了空指针,程序将崩溃。

重载运算符

重载 new 和 delete 运算符,默认是 static 存储类的,所以定义内部不能使用非静态成员变量。

类的自动类型转换

构造函数自动类型转换:

  • 有参构造可以将赋给类对象的值通过该构造函数转换。
  • 使用 explicit 关闭隐式类型转换,必须显式类型转换。

将类类型转换为默认类型:

仅仅是将类类型中的某个成员返回,可以用运算符重载,也可以单独定义一个函数。

c11 也可以使用 explicit。

piyRfdH.png

继承

构造函数和析构函数不能被继承。

派生类中的using

在派生类中使用 public 继承基类的 public 或 protected 成员,可以在派生类中使用 using 改变继承的成员权限。

piyf1g0.png

内存模型

A 是基类,B 是派生类,16 字节代表着 A 中的 12 字节 + B 中新增的 4 字节。

piyfwCR.png

派生类和基类的构造函数

  1. 当新建一个派生类对象,该对象对于基类的成员变量会使用基类的构造函数(因为基类中的私有成员不能在派生类初始化),对于派生类的成员变量会使用派生类的成员函数。
  2. 可以在派生类的构造函数中,委托构造给基类的构造函数。
  3. 在C++中,当创建派生类的对象时,基类的构造函数会被自动调用以初始化基类部分的成员变量。如果派生类的构造函数没有显式地调用基类的构造函数,那么基类的默认构造函数(无参数的构造函数)会被自动调用。如果基类没有默认构造函数,或者你需要调用一个带参数的基类构造函数,那么你必须在派生类的构造函数初始化列表中显式地调用它。

piyhQde.png

基类和派生类变量及函数重名

重名变量和重名函数(也不会触发重载,也就是说基类同名函数有参数,派生类同名函数无参数,那么派生类对象调用该函数就算加了参数,编译器也会报错。)会使用派生类的,也就是基类的成员被遮蔽。

解决方案:可以通过 类名+作用域解析符 指定调用哪个变量或函数

如下:C 是 孙类,B 是子类,A 是父类。

piyhwdg.png

基类的指针可以指向派生类对象

  1. 基类指针可以在不进行显式转换的情况下指向派生类对象:
1
2
3
4
5
6
7
8
9
10
class A;
class B:public A
{};

int main()
{
B b;
A* ptr = &b;
// 这里的 ptr 只能调用A类的成员。
}
  1. 基类引用可以在不进行显式转换的情况下引用派生类对象。

注意:

  1. 基类指针或引用只能调用基类的方法,不能调用派生类的方法。(即数据类型决定了操作数据的方法

多态

析构派生类

派生类的析构函数执行完后,c++ 编译器会强制执行基类的析构函数。(手动 delete 情况)

解决方案:将基类的析构函数声明为 virtual,即虚析构函数。

对于基类,即使不需要析构函数,也应该提供一个空的虚析构函数。

在销毁基类指针指向的派生类对象时,不会调用派生类的析构函数,因为基类指针不能调用派生类成员。

纯虚类

  • 普通函数设置为纯虚函数可以给代码实现也可以不给。

  • 而析构函数,调用派生类的析构函数后,也会调用基类的析构函数。

    因此将基类的析构函数设置为纯虚析构后,要加上代码实现(类外定义给空实现)

  • 纯虚析构函数作用:为了使一个类中没有任何纯虚函数的类变成抽象类,此时只需要声明一个纯虚析构函数。

dynamic_cast

用于将基类指针转换为派生类指针,用于使基类指针能够调用派生类的成员。类似 c 语言的强转,但是更安全。用于判断是否可以安全的将对象的地址赋值给特定类型的指针。

成功则返回对象的地址,失败返回空指针。

  1. dynamic_cast 只适用于包含虚函数的类。(因为需要查虚函数表)
  2. dynamic_cast 可以将派生类指针转换为基类指针,但是没有意义,因为派生类指针可以直接赋值给基类指针。
  3. dynamic_cast 可以用于引用,但是,没有与空指针对应的引用值,如果转换请求不正确,会出现 bad_cast 异常。

typeid 运算符

用于判断两个数据类型是否相同。

需要包含头文件 #include <typeinfo>

  1. type_info 类构造函数时 private,只能由编译器内部实例化。
  2. 不建议使用 typeid().name() 返回的字符串判断数据类型,因为不同编译器结果不同。
  3. typeid 运算符可以用于多态场景,可以比较基类对象和派生类对象,基类对象和派生类对象是相同类型的,但是指向派生类对象的基类指针和派生类指针的比较结果是不同的。
  4. typeid(*ptr) ,当 ptr 是空指针,如果 ptr 是多态类型,将引发 bad_typeid 异常。

函数模板

模板要求明确数据类型,可以用 swap<int> (); 尖括号的形式。

注意事项

  1. 可以为类的成员函数创建模板,但不能是虚函数和析构函数。

  2. 使用函数模板时,必须明确数据类型,确保实参与函数模板能匹配上。也就是说函数不能没有使用模板类型。

  3. 使用函数模板时,推导的数据类型必须适应函数模板中的代码。假如一个类使用了一个函数模板进行加的功能,会报错。

  4. 使用函数模板时,如果是自动类型推导,不会发生隐式类型转换,如果显式指定了函数模板的数据类型,可以发生隐式类型转换。也就是在尖括号中指定类型。

  5. 函数模板支持多个通用数据类型的参数。

    1
    template <typename T1, typename T2>
  6. 函数模板支持重载,可以有非通用数据类型的参数。

模板具体化

语法:

1
2
3
4
template<> 与模板相同的返回类型 函数模板名 (参数列表)
{
// 函数体。
}

函数模板可以将函数声明与定义分离:

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
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。

class CGirl // 超女类。
{
public:
int m_bh; // 编号。
string m_name; // 姓名。
int m_rank; // 排名。
};

template <typename T>
void Swap(T& a, T& b); // 交换两个变量的值函数模板。

template<>
void Swap<CGirl>(CGirl& g1, CGirl& g2); // 交换两个超女对象的排名。
// template<>
// void Swap(CGirl& g1, CGirl& g2); // 交换两个超女对象的排名。

int main()
{
int a = 10, b = 20;
Swap(a, b); // 使用了函数模板。
cout << "a=" << a << ",b=" << b << endl;

CGirl g1, g2;
g1.m_rank = 1; g2.m_rank = 2;
Swap(g1, g2); // 使用了超女类的具体化函数。
cout << "g1.m_rank=" << g1.m_rank << ",g2.m_rank=" << g2.m_rank << endl;
}

template <typename T>
void Swap(T& a, T& b) // 交换两个变量的值函数模板。
{
T tmp = a;
a = b;
b = tmp;
cout << "调用了Swap(T& a, T& b)\n";
}

template<>
void Swap<CGirl>(CGirl& g1, CGirl& g2) // 交换两个超女对象的排名。
// template<>
// void Swap(CGirl& g1, CGirl& g2) // 交换两个超女对象的排名。
{
int tmp = g1.m_rank;
g1.m_rank = g2.m_rank;
g2.m_rank = tmp;
cout << "调用了Swap(CGirl& g1, CGirl& g2)\n";
}

编译器使用各种函数的规则

  1. 具体化优先于常规模板,普通函数优先于具体化和常规模板。

  2. 如果希望使用函数模板,可以用空模板参数强制使用函数模板。
    Swap<> (1, 2);

  3. 如果函数模板能产生更好的匹配,将优先于普通函数。

    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
    #include <iostream>         // 包含头文件。
    using namespace std; // 指定缺省的命名空间。

    void Swap(int a, int b) // 普通函数。
    {
    cout << "使用了普通函数。\n";
    }

    template <typename T>
    void Swap(T a, T b) // 函数模板。
    {
    cout << "使用了函数模板。\n";
    }

    template <>
    void Swap(int a, int b) // 函数模板的具体化版本。
    {
    cout << "使用了具体化的函数模板。\n";
    }

    int main()
    {
    Swap(1, 2); // 使用了普通函数
    Swap('c', 'd'); // 使用了函数模板,因为函数模板可以更匹配。
    }

函数模板的分文件编写

  1. 函数模板只是函数的描述,没有实体,创建函数模板的声明和定义放在头文件中。
  2. 函数模板的具体化有实体,编译的原理和普通函数一样,所以,函数模板具体化的声明放在头文件中,定义放在源文件中

public.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma once
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。

void Swap(int a, int b); // 普通函数。

template <typename T>
void Swap(T a, T b) // 函数模板。
{
cout << "使用了函数模板。\n";
}

template <>
void Swap(int a, int b); // 函数模板的具体化版本。

public.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include "public.h"

void Swap(int a, int b) // 普通函数。
{
cout << "使用了普通函数。\n";
}

template <>
void Swap(int a, int b) // 函数模板的具体化版本。
{
cout << "使用了具体化的函数模板。\n";
}

demo.cpp

1
2
3
4
5
6
7
8
#include "public.h"

int main()
{
Swap(1,2); // 将使用普通函数。
Swap(1.3, 3.5); // 将使用具体化的函数模板。
Swap('c', 'd'); // 将使用函数模板。
}

C++ 11 关于函数模板的拓展

decltype 关键字

decltype 不会执行表达式,也不会调用函数。

auto 会执行右边的表达式或者函数。

如果需要多次使用decltype,可以结合typedef和using。

简单来说,decltype 的类型要么是表达式的类型,要么是表达式的引用。用错了编译器会报错。

decltype推导规则(按步骤):

  1. 如果expression是一个没有用括号括起来的标识符,则var的类型与该标识符的类型相同,包括const等限定符。

  2. 如果expression是一个函数调用(也就是 func() 带小括号的),则var的类型与函数的返回值类型相同(函数不能返回void,但可以返回void *)。
    如果expression是一个函数名(不带小括号,也就是函数指针),decltype(func) *ptr = func; ptr(); ,这里通过 ptr() 就调用了 func() 函数。

  3. 如果expression是一个左值(能取地址)(要排除第一种情况)、或者用括号括起来的标识符,那么var的类型是expression的引用。

    1
    2
    3
    4
    5
    6
    7
    short a = 5;
    short b = 10;
    short &ra = a;
    decltype(++a) da = b; // 这里的da类型是 short&
    decltype(a) da; // 这里的da类型是 short
    decltype((a)) da = b; // 这里的da类型是 short&
    decltype((func)) da = func; // 这里的da类型是 函数的引用,da()也可以调用函数
  4. 如果上面的条件都不满足,则var的类型与expression的类型相同。

函数后置返回类型(c++14 可以用auto)

语法:auto func(T1 x, T2 y) -> decltype(x + y)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。

template <typename T1, typename T2>
// auto func(T1 x, T2 y) // c++ 14的写法
auto func(T1 x, T2 y) -> decltype(x + y)
{
// 其它的代码。

decltype(x+y) tmp = x + y;
cout << "tmp=" << tmp << endl;

return tmp;
}

int main()
{
func(3, 5.8);
}

类模板

语法:

1
2
3
template <class T1, class T2 = std::string> // 可以指定默认类型
class Tclass
{};

注意:

  1. 在创建对象的时候,必须指明具体的数据类型(在尖括号内)。

    Tclass<int, string> t1;

  2. 使用类模板时,数据类型必须适应类模板中的代码。
    如:将整型赋值给 string 类型的代码就会报错。

  3. 类模板可以为通用数据类型指定缺省的数据类型(C++11标准的函数模板也可以)。

  4. 模板类的成员函数可以在类外实现。
    类外实现时,函数上一行加模板声明(不含默认参数),函数声明则要加尖括号以及作用域:T1 Tclass<T1, T2>::get_a(){}

  5. 可以用new创建模板类对象。
    Tclass<int, string>* ptr = new Tclass<int, string>();

  6. 在程序中,模板类的成员函数使用了才会创建。
    若是只是 Tclass<int, string>* ptr; ,则都不会调用构造函数,构造函数中就算有错误代码也不会报错,运行时也不会报错。

创建模板类

  1. 先写一个普通类,用具体的数据类型,可以 typedef 定义一个名称,方便后续修改。
  2. 调试普通类
  3. 将普通类改为模板类

栈模板类示例:

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
#include <iostream>

using namespace std;

template <class DataType>
class Stack
{
private:
DataType* items;
int stacksize;
int top;
public:
Stack(int size);
~Stack();
DataType& operator[](int index);
bool isEmpty() const;
int size() const;
bool push(const DataType& item);
bool pop(DataType& item);
};

template <class DataType>
Stack<DataType>::Stack(int size): stacksize(size), top(0)
{
items = new DataType[size];
}

template <class DataType>
Stack<DataType>::~Stack()
{
delete[] items;
items = nullptr;
}

template <class DataType>
DataType& Stack<DataType>::operator[](int index)
{
return items[index];
}

template <class DataType>
bool Stack<DataType>::isEmpty() const
{
return top == 0;
}

template <class DataType>
int Stack<DataType>::size() const
{
return stacksize;
}

template <class DataType>
bool Stack<DataType>::push(const DataType& item)
{
if (top < stacksize)
{
items[top++] = item;
return true;
}
return false;
}

template <class DataType>
bool Stack<DataType>::pop(DataType& item)
{
if (top > 0)
{
item = items[--top]; // top 比数组下标大1,这里的item也就是弹出栈的值。
return true;
}
return false;
}

int main()
{
Stack<int> st(5);
for (int i = 0; i < st.size(); i++)
{
st.push(i+1);
}
int num;

st.pop(num);

cout << num << endl;

for (size_t i = 0; i < st.size() - 1; i++)
{
cout << st[i] << " ";
}

return 0;
}

类模板注意事项

类模板可以有非通用类型参数:

template <class T, int len = 10>

  1. 通常是整型(C++20标准可以用其它的类型);

  2. 实例化模板时必须用常量表达式;

  3. 模板中不能修改参数的值;

  4. 可以为非通用类型参数提供默认值。

C++ 11 之前,嵌套使用模板类的时候,>> 前要加空格。
如:vector<stack<string> >

类模板

具体化的模板类,成员函数类外实现的代码应该放在源文件中。

类的具体化分为:

  • 完全具体化
  • 部分具体化
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
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。

// 类模板
template<class T1, class T2>
class AA { // 类模板。
public:
T1 m_x;
T2 m_y;

AA(const T1 x, const T2 y) :m_x(x), m_y(y) { cout << "类模板:构造函数。\n"; }
void show() const;
};

template<class T1, class T2>
void AA<T1, T2>::show() const { // 成员函数类外实现。
cout << "类模板:x = " << m_x << ", y = " << m_y << endl;
}
/////////////////////////////////////////////////////////////////////////////////////////

// 类模板完全具体化
template<>
class AA<int, string> {
public:
int m_x;
string m_y;

AA(const int x, const string y) :m_x(x), m_y(y) { cout << "完全具体化:构造函数。\n"; }
void show() const;
};

void AA<int, string>::show() const { // 成员函数类外实现。
cout << "完全具体化:x = " << m_x << ", y = " << m_y << endl;
}
/////////////////////////////////////////////////////////////////////////////////////////

// 类模板部分具体化
template<class T1>
class AA<T1, string> {
public:
T1 m_x;
string m_y;

AA(const T1 x, const string y) :m_x(x), m_y(y) { cout << "部分具体化:构造函数。\n"; }
void show() const;
};

template<class T1>
void AA<T1, string>::show() const { // 成员函数类外实现。
cout << "部分具体化:x = " << m_x << ", y = " << m_y << endl;
}
/////////////////////////////////////////////////////////////////////////////////////////

int main()
{
// 具体化程度高的类优先于具体化程度低的类,具体化的类优先于没有具体化的类。
AA<int, string> aa1(8, "我是一只傻傻鸟。"); // 将使用完全具体化的类。
AA<char, string> aa2(8, "我是一只傻傻鸟。"); // 将使用部分具体化的类。
AA<int, double> aa3(8, 999999); // 将使用模板类。
}

模板类与继承

  1. 模板类继承普通类
    正常继承即可,注意在派生类调用基类的构造函数(委托构造)

  2. 普通类继承模板类的实例化版本
    pi6sycD.png

  3. 普通类继承模板类:普通类前加模板声明,派生类使用基类构造函数时要写成模板类型的基类构造形式。创建 AA 的实例化对象时,也需要给出模板类型,如 AA<int,string> aa(1, "2", 3);
    pi6s6je.png

  4. 模板类继承模板类:在派生类中增加基类的通用参数即可。
    pi6so38.png

  5. 模板类继承模板参数给出的基类(不能是模板类)

    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
    #include <iostream>         // 包含头文件。
    using namespace std; // 指定缺省的命名空间。

    class AA {
    public:
    AA() { cout << "调用了AA的构造函数AA()。\n"; }
    AA(int a) { cout << "调用了AA的构造函数AA(int a)。\n"; }
    };

    class BB {
    public:
    BB() { cout << "调用了BB的构造函数BB()。\n"; }
    BB(int a) { cout << "调用了BB的构造函数BB(int a)。\n"; }
    };

    class CC {
    public:
    CC() { cout << "调用了CC的构造函数CC()。\n"; }
    CC(int a) { cout << "调用了CC的构造函数CC(int a)。\n"; }
    };

    template<class T>
    class DD {
    public:
    DD() { cout << "调用了DD的构造函数DD()。\n"; }
    DD(int a) { cout << "调用了DD的构造函数DD(int a)。\n"; }
    };

    template<class T>
    class EE : public T { // 模板类继承模板参数给出的基类。
    public:
    EE() :T() { cout << "调用了EE的构造函数EE()。\n"; } // 可以使用继承的模板参数的类的构造函数
    EE(int a) :T(a) { cout << "调用了EE的构造函数EE(int a)。\n"; }
    };

    int main()
    {
    EE<AA> ea1; // AA作为基类。
    EE<BB> eb1; // BB作为基类。
    EE<CC> ec1; // CC作为基类。
    EE<DD<int>> ed1; // EE<int>作为基类。 因为 DD 是模板类,需要给出具体类型。
    // EE<DD> ed1; // DD作为基类,错误。
    }

模板类与函数

传入的数据类型是什么不重要,只要适应模板中的代码就行。

模板类可以用于函数的参数和返回值,有三种形式:

  1. 普通函数,参数和返回值是模板类的实例化版本。

  2. 函数模板,参数和返回值是某种的模板类。不推荐,因为仅限于一种模板类。

  3. 函数模板,参数和返回值是任意类型(支持普通类和模板类和其它类型)。推荐,可传入任意类型(包括任意模板类型)。

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
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。

template<class T1, class T2>
class AA // 模板类AA。
{
public:
T1 m_x;
T2 m_y;
AA(const T1 x, const T2 y) : m_x(x), m_y(y) { }
void show() const { cout << "show() x = " << m_x << ", y = " << m_y << endl; }
};

// 采用普通函数,参数和返回值是模板类AA的实例化版本。
AA<int, string> func(AA<int, string>& aa)
{
aa.show();
cout << "调用了func(AA<int, string> &aa)函数。\n";
return aa;
}

// 函数模板,参数和返回值是的模板类AA。
template <typename T1,typename T2>
AA<T1, T2> func(AA<T1, T2>& aa)
{
aa.show();
cout << "调用了func(AA<T1, T2> &aa)函数。\n";
return aa;
}

// 函数模板,参数和返回值是任意类型。
template <typename T>
T func(T &aa)
{
aa.show();
cout << "调用了func(AA<T> &aa)函数。\n";
return aa;
}

int main()
{
AA<int, string> aa(3, "我是一只傻傻鸟。");
func(aa);
}

模板类与友元

模板类的友元函数有三类:

  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
    #include <iostream>         // 包含头文件。
    using namespace std; // 指定缺省的命名空间。

    template<class T1, class T2>
    class AA
    {
    T1 m_x;
    T2 m_y;
    public:
    AA(const T1 x, const T2 y) : m_x(x), m_y(y) { }
    // 非模板友元:友元函数不是模板函数,而是利用模板类参数生成的函数,只能在类内实现。
    friend void show(const AA<T1, T2>& a)
    {
    cout << "x = " << a.m_x << ", y = " << a.m_y << endl;
    }
    /* friend void show(const AA<int, string>& a);
    friend void show(const AA<char, string>& a);*/
    };

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

    int main()
    {
    AA<int, string> a(88, "我是一只傻傻鸟。");
    show(a);

    AA<char, string> b(88, "我是一只傻傻鸟。");
    show(b);
    }
  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 <typename T>
    void show(T& a); // 第一步:在模板类的定义前面,声明友元函数模板。

    template<class T1, class T2>
    class AA // 模板类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) { }
    };

    template<class T1, class T2>
    class BB // 模板类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) { }
    };

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

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

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

    int main()
    {
    AA<int, string> a1(88, "我是一只傻傻鸟。");
    show(a1); // 将使用具体化的版本。

    AA<char, string> a2(88, "我是一只傻傻鸟。");
    show(a2); // 将使用通用的版本。

    BB<int, string> b1(88, "我是一只傻傻鸟。");
    show(b1); // 将使用具体化的版本。

    BB<char, string> b2(88, "我是一只傻傻鸟。");
    show(b2); // 将使用通用的版本。
    }
  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
    #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) { }
    };

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

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

    int main()
    {
    AA<int, string> a(88, "我是一只傻傻鸟。");
    show(a); // 将使用具体化的版本。

    AA<char, string> b(88, "我是一只傻傻鸟。");
    show(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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。

template<class T1, class T2>
class AA // 类模板AA。
{
public:
T1 m_x;
T2 m_y;

AA(const T1 x, const 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;
BB() {}
void show();
};
BB<string> m_bb;

template<typename T>
void show(T tt); // 和上面的 show 参数不同,形成重载。
};

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;
}

template<class T1, class T2>
template<typename T>
void AA<T1,T2>::show(T tt) {
cout << "tt=" << tt << endl;
cout << "m_x=" << m_x << ",m_y=" << m_y << endl;
m_bb.show();
}

int main()
{
AA<int, string> a(88, "我是一只傻傻鸟。");
a.show();
a.m_bb.m_a = "我有一只小小鸟。";
a.m_bb.show();
a.show("你是一只什么鸟?");
}

将模板类用作参数

  1. 函数模板不支持模板的模板参数。

  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
#include <iostream>         // 包含头文件。
using namespace std; // 指定缺省的命名空间。

template <class T, int len>
class LinkList // 链表类模板。
{
public:
T* m_head; // 链表头结点。
int m_len = len; // 表长。
void insert() { cout << "向链表中插入了一条记录。\n"; }
void ddelete() { cout << "向链表中删除了一条记录。\n"; }
void update() { cout << "向链表中更新了一条记录。\n"; }
};

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

// 线性表模板类:tabletype-线性表类型,datatype-线性表的数据类型。
// 这里的 template<class, int >class tabletype 是类模板类型,即 Array 或 Linklist 模板类类型(不能是内置类型),第一个 class 代表通用类型,第二个就是Array 或 Linklist指定非通用类型。
template<template<class, int >class tabletype, class datatype, int len>
class LinearList
{
public:
tabletype<datatype, len> m_table; // 创建线性表对象。 tabletype 也就是 Array 或 Linklist 模板类类型。datatype 指定传入给 Array 或 Linklist 模板类类型的具体类型。

void insert() { m_table.insert(); } // 线性表插入操作。
void ddelete() { m_table.ddelete(); } // 线性表删除操作。
void update() { m_table.update(); } // 线性表更新操作。

void oper() // 按业务要求操作线性表。
{
cout << "len=" << m_table.m_len << endl;
m_table.insert();
m_table.update();
}
};

int main()
{
// 创建线性表对象,容器类型为链表,链表的数据类型为int,表长为20。
LinearList<LinkList, int, 20> a;
a.insert();
a.ddelete();
a.update();

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

编译、预处理、链接和命名空间

头文件(*.h):#include头文件、函数的声明、结构体的声明、类的声明、模板的声明、内联函数、#define和const定义的常量等。

源文件(*.cpp):函数的定义、类的定义、模板具体化的定义。

主程序(main函数所在的程序):主程序负责实现框架和核心流程,把需要用到的头文件用#include包含进来。

预处理

#include、#ifdef、#ifndef、#endif 等,进行文本复制。

预处理的包括以下方面:

1)处理#include头文件包含指令。

2)处理#ifdef #else #endif、#ifndef #else #endif条件编译指令。

3)处理#define宏定义。

4)为代码添加行号、文件名和函数名。

5)删除注释。

6)保留部分#pragma编译指令(编译的时候会用到)。

编译、链接

编译:将预处理生成的文件,经过词法分析、语法分析、语义分析以及优化和汇编后,编译成若干个目标文件(二进制文件)。

链接:将编译后的目标文件,以及它们所需要的库文件链接在一起,形成一个整体。

使用全局变量时,最好在 .cpp 文件中定义,并在对应的 .h 中 extern 声明。然后 include 就可以使用了。 不能将定义放在头文件中,因为会重复包含,导致多个定义。

其它关键知识点

  1. 分开编译的好处:每次只编译修改过的源文件,然后再链接,效率最高。

  2. 编译单个*.cpp文件的时候,必须要让编译器知道名称的存在,否则会出现找不到标识符的错误。(直接和间接包含头文件都可以)

  3. 编译单个*.cpp文件的时候,编译器只需要知道名称的存在,不会把它们的定义一起编译

  4. 如果函数和类的定义不存在,编译不会报错,但链接会出现无法解析的外部命令。

  5. 链接的时候,变量、函数和类的定义只能有一个,否则会出现重定义的错误。(如果把变量、函数和类的定义放在*.h文件中,*.h会被多次包含,链接前可能存在多个副本;如果放在*.cpp文件中,*.cpp文件不会被包含,只会被编译一次,链接前只存在一个版本)

  6. 把变量、函数和类的定义放在*.h中是不规范的做法,如果*.h被多个*.cpp包含,会出现重定义。

  7. 用#include包含*.cpp也是不规范的做法,原理同上。

  8. 尽可能不使用全局变量,如果一定要用,要在*.h文件中声明(需要加extern关键字),在*.cpp文件中定义。

  9. 全局的const常量在头文件中定义(const常量仅在单个文件内有效)。在头文件中extern声明,源文件中定义,并在需要使用的文件中extern后,使用可行。

  10. .h文件重复包含的处理方法只对单个的.cpp文件有效,不是整个项目。

    如果你的项目包含多个源文件(.cpp 文件),并且这些源文件都包含了同一个头文件,那么头文件的包含保护只对单个源文件有效。这是因为每个源文件是独立编译的单元,它们在编译时是相互独立的。

  11. 函数模板和类模板的声明和定义可以分开书写,但它们的定义并不是真实的定义,只能放在*.h文件中;函数模板和类模板的具体化版本的代码是真实的定义,所以放在*.cpp文件中。

  12. Linux下C++编译和链接的原理与VS一样。

命名空间

创建命名空间语法:

1
2
3
4
namespace Test
{
int a;
}

在 main 函数中可以使用 using Test::a; ,但是这样 main 中再定义同名变量就会导致重定义。使用 using namespace Test 的话是可以再定义同名变量的,但是会遮蔽命名空间的变量。如果要使用命名空间的变量,则要加 Test::a 作用域和解析符。

注意点

  1. 命名空间是全局的,可以分布在多个文件中。

  2. 命名空间可以嵌套。

  3. 在命名空间中声明全局变量,而不是使用外部全局变量和静态变量。

  4. 对于using声明,首选将其作用域设置为局部而不是全局。

  5. 不要在头文件中使用using编译指令,如果非要使用,应将它放在所有的#include之后。

  6. 匿名的命名空间,从创建的位置到文件结束有效(也就是当前文件有效,因为没有名字,其它地方也调用不了)。

    1
    2
    3
    4
    namespace
    {
    int b;
    }

c++强制转换语法

static_cast<目标类型>(表达式)、reinterpret_cast、const_cast、dynamic_cast

static_cast

static_cast不能用于转换不同类型的指针(引用)(不考虑有继承关系的情况),必须借助 void*

一般用于向函数中传递一个指针,函数参数是 void*,然后转为 void* 后再在函数内部使用 static_cast 转换为其它类型指针。

  1. 内置普通类型转换,与 c 语法强转没有区别。
  2. 指针类型转换。如 int* 转换为 double* ,需要先将 int* 类型赋值给 void* 变量,再通过 static_cast 将这个 void* 变量转换为 double*。C 风格可以直接强转。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

using namespace std;

double* func(void* ptr)
{
return static_cast<double*>(ptr);
}

int main()
{
int i = 10;
int* ii = &i;

double* dp = func(ii);

return 0;
}

reinterpret_cast

reinterpret_cast的意思是重新解释,能够将一种对象类型转换为另一种,不管它们是否有关系。

转换指针类型时不需要借助 void*。

reinterpret_cast<目标类型>(表达式); <目标类型>和(表达式)中必须有一个是指针(引用)类型。

不能丢掉 const 和 volitale 属性。

应用场景:

  1. reinterpret_cast的第一种用途是改变指针(引用)的类型。引用本质还是指针。

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

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

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

using namespace std;

void func(void* ptr)
{
long long ii = reinterpret_cast<long long>(ptr);
cout << ii << endl;
}

int main()
{
long long i = 10; // 如果是 int 类型,则编译器会警告精度问题。因为 int 是4字节,void* 指针是8字节。

func(reinterpret_cast<void*>(i)); // 用于线程和网络通讯。

return 0;
}

const_cast

用于丢掉 const 和 volitale 属性。

static_cast不能丢掉指针(引用)的const和volitale属性,const_cast可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

void func(int *ii)
{}

int main(int argc, char* argv[])
{
const int *aa=nullptr;
int *bb = (int *)aa; // C风格,强制转换,丢掉const限定符。
int* cc = const_cast<int*>(aa); // C++风格,强制转换,丢掉const限定符。

func(const_cast<int *>(aa));
}