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

using namespace std;

const int NAME_LENGTH = 50;

class CGoods {

public:
CGoods(const char *name, double price, int amount) {
strcpy(this->_name, name);
this->_price = price;
this->_amount = amount;
}

const char *getName() const {
return this->_name;
}

double getPrice() const {
return this->_price;
}

int getAmount() const {
return this->_amount;
}

void setName(char *name) {
strcpy(this->_name, name);
}

void setPrice(double price) {
this->_price = price;
}

void setAmount(int amount) {
this->_amount = amount;
}

void show() {
cout << "name: " << this->_name << endl;
cout << "price: " << this->_price << endl;
cout << "amount: " << this->_amount << endl;
}

private:
char _name[NAME_LENGTH]; // 静态分配内存
double _price;
int _amount;

};

int main() {
CGoods good("Book", 80, 3);
good.show();
return 0;
}

程序执行后的输出结果:

1
2
3
name: Book
price: 80
amount: 3

提示

类的成员函数一经编译,在所有函数的参数列表中,都会隐式自动添加一个 this 指针,用于接收调用该函数的对象的地址。这样在函数被调用时,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
#include <iostream>

using namespace std;

class Test {

public:
int ma;
static int mb;

};

// 初始化类成员静态变量
int Test::mb = 50;

void test01() {
Test t1; // 栈上分配内存
Test *t2 = new Test(); // 堆上分配内存

// 错误写法
// int * p = &Test::ma;

// 指向类成员变量的指针
int Test::*p = &Test::ma;

// 通过指针访问类成员变量
t1.*p = 20;
cout << t1.ma << ", " << t1.*p << endl;

// 通过指针访问类成员变量
t2->*p = 30;
cout << t2->ma << ", " << t2->*p << endl;

delete t2;
}

void test02() {
// 正确写法, 指向类成员静态变量的指针
int *p1 = &Test::mb;

// 通过指针访问类成员静态变量
*p1 = 60;
cout << Test::mb << ", " << *p1 << endl;
}

int main() {
test01();
test02();
return 0;
}

程序执行后的输出结果:

1
2
3
20, 20
30, 30
60, 60

指向类成员函数的指针

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

using namespace std;

class Test {

public:
void func() {
cout << "call Test::func" << endl;
}

static void static_func() {
cout << "call Test::static_func" << endl;
}
};

void test01() {
Test t1; // 栈上分配内存
Test *t2 = new Test(); // 堆上分配内存

// 错误写法
// void (*pFunc)() = &Test::func;

// 指向类成员函数的指针(函数指针)
void (Test::*pFunc)() = &Test::func;

// 通过指针调用类成员函数
(t1.*pFunc)();

// 通过指针调用类成员函数
(t2->*pFunc)();
}

void test02() {
// 正确写法, 指向类成员静态函数的指针(函数指针)
void (*pStaticFunc)() = &Test::static_func;

// 通过指针调用类成员静态函数
(*pStaticFunc)();
}

int main() {
test01();
test02();
return 0;
}

程序执行后的输出结果:

1
2
3
call Test::func
call Test::func
call Test::static_func

构造函数与析构函数

  • 构造函数
    • 定义对象时,构造函数会自动调用。
    • 构造函数是可以重载的,可以有多个构造函数。
    • 对象构造完成后,对象就产生了。
  • 析构函数
    • 析构函数不带参数,不能重载,有且只有一个析构函数。
    • 对象析构完成后,对象就不存在了。
  • 二者的共同点
    • 当开发者没有自定义构造函数和析构函数时,编译器会自动生成一个默认构造函数(无参构造函数)和默认析构函数。

提示

  • 在栈上分配内存空间的 C++ 对象(比如 SeqStack s;),当该对象出了作用域之后(比如函数执行结束之后),C++ 会自动调用该对象的析构函数来释放内存空间。
  • 在堆上分配内存空间的 C++ 对象(比如 SeqStack *s = new SeqStack();),那么必须在该对象出了作用域之前(比如函数执行结束之前),手动执行 delete 操作来释放内存空间,这样该对象的析构函数才会被调用。

下面将实现一个顺序栈的数据结构,并结合构造函数与析构函数一起使用。

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

using namespace std;

// 顺序栈
class SeqStack {

public:
// 构造函数
SeqStack(int size = 10) {
cout << "call SeqStack()" << endl;
// 分配内存
_pstatck = new int[size];
_top = -1;
_size = size;
}

// 析构函数
~SeqStack() {
cout << "call ~SeqStack()" << endl;
// 释放内存
if (_pstatck != nullptr) {
delete[] _pstatck;
_pstatck = nullptr;
}
}

// 入栈
void push(int val) {
if (full()) {
resize();
}
_pstatck[++_top] = val;
}

// 弹栈
void pop() {
if (empty()) {
return;
}
--_top;
}

// 获取栈顶元素
int top() {
return _pstatck[_top];
}

// 栈是否满了
bool full() {
return _top == _size - 1;
}

// 栈是否为空
bool empty() {
return _top == -1;
}

private:
int *_pstatck; // 动态开辟数组,存储顺序栈的元素
int _top; // 指向栈顶元素的位置
int _size; // 数组的总大小

// 扩容操作
void resize() {
// 分配新的内存空间
int *pnew = new int[_size * 2];
for (int i = 0; i < _size; i++) {
pnew[i] = _pstatck[i];
}
// 释放旧的内存空间
delete[] _pstatck;
// 指向新的内存空间
_pstatck = pnew;
_size *= 2;
}
};

void test01() {
cout << "===== call test01() =====" << endl;

SeqStack s(5);
for (int i = 0; i < 15; i++) {
s.push(rand() % 100);
}

while (!s.empty()) {
cout << s.top() << " ";
s.pop();
}
cout << endl;
}

void test02() {
cout << "===== call test02() =====" << endl;

SeqStack *s = new SeqStack(5);
for (int i = 0; i < 15; i++) {
s->push(rand() % 100);
}

while (!s->empty()) {
cout << s->top() << " ";
s->pop();
}
cout << endl;

delete s;
}

int main() {
// 设置随机数种子
srand(time(nullptr));

test01();
test02();
return 0;
}

程序执行后的输出结果:

1
2
3
4
5
6
7
8
===== call test01() =====
call SeqStack()
20 40 33 74 97 39 83 65 85 16 48 55 89 22 48
call ~SeqStack()
===== call test02() =====
call SeqStack()
15 92 64 83 46 74 70 93 54 69 9 46 88 94 39
call ~SeqStack()

对象的深拷贝和浅拷贝

使用案例一

下面将实现一个顺序栈的数据结构,并结合拷贝构造函数、深拷贝与赋值运算符重载一起使用。

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

using namespace std;

// 顺序栈
class SeqStack {

public:
// 构造函数
SeqStack(int size = 10) {
cout << "call SeqStack()" << endl;
// 分配内存
_pstatck = new int[size];
_top = -1;
_size = size;
}

// 析构函数
~SeqStack() {
cout << "call ~SeqStack()" << endl;
// 释放内存
if (_pstatck != nullptr) {
delete[] _pstatck;
_pstatck = nullptr;
}
}

// 拷贝构造函数
SeqStack(const SeqStack &stack) {
cout << "call SeqStack(const SeqStack &stack)" << endl;
// 深拷贝(重新分配内存)
_pstatck = new int[stack._size];
for (int i = 0; i < stack._size; i++) {
_pstatck[i] = stack._pstatck[i];
}
_top = stack._top;
_size = stack._size;
}

// 运算符重载
SeqStack &operator=(const SeqStack &stack) {
cout << "call operator=(const SeqStack &stack)" << endl;
// 防止自赋值
if (this == &stack) {
return *this;
}

// 释放原来占用的内存空间
if (_pstatck != nullptr) {
delete[]_pstatck;
}

// 深拷贝(重新分配内存)
_pstatck = new int[stack._size];
for (int i = 0; i < stack._size; i++) {
_pstatck[i] = stack._pstatck[i];
}
_top = stack._top;
_size = stack._size;

return *this;
}

// 入栈
void push(int val) {
if (full()) {
resize();
}
_pstatck[++_top] = val;
}

// 弹栈
void pop() {
if (empty()) {
return;
}
--_top;
}

// 获取栈顶元素
int top() {
return _pstatck[_top];
}

// 栈是否满了
bool full() {
return _top == _size - 1;
}

// 栈是否为空
bool empty() {
return _top == -1;
}

// 打印所有元素
void print() {
for (int i = 0; i < _size; i++) {
cout << _pstatck[i] << " ";
}
cout << endl;
}

// 获取元素数量
int size() {
return _size;
}

private:
int *_pstatck; // 动态开辟数组,存储顺序栈的元素
int _top; // 指向栈顶元素的位置
int _size; // 数组的总大小

// 扩容操作
void resize() {
// 分配新的内存空间
int *pnew = new int[_size * 2];
for (int i = 0; i < _size; i++) {
pnew[i] = _pstatck[i];
}
// 释放旧的内存空间
delete[] _pstatck;
// 指向新的内存空间
_pstatck = pnew;
_size *= 2;
}
};

int main() {
// 设置随机数种子
srand(time(nullptr));

SeqStack s1(10);
for (int i = 0; i < s1.size(); i++) {
s1.push(rand() % 100);
}
s1.print();

SeqStack s2 = s1; // 默认会调用拷贝构造函数
s2.print();

SeqStack s3(s1); // 默认会调用拷贝构造函数
s3.print();

s2 = s3; // 赋值运算,不会调用拷贝构造函数,默认是浅拷贝,会发生内存泄漏(内存没有被正确释放),解决办法是通过运算符重载来实现深拷贝
return 0;
}

程序执行后的输出结果:

1
2
3
4
5
6
7
8
9
10
call SeqStack()
88 95 94 93 13 7 9 86 43 10
call SeqStack(const SeqStack &stack)
88 95 94 93 13 7 9 86 43 10
call SeqStack(const SeqStack &stack)
88 95 94 93 13 7 9 86 43 10
call operator=(const SeqStack &stack)
call ~SeqStack()
call ~SeqStack()
call ~SeqStack()

使用案例二

下面将实现一个循环队列的数据结构,并结合拷贝构造函数、深拷贝与赋值运算符重载一起使用。

  • 循环队列的关键特性:

    • 队列特性: 循环队列仍然遵循 “先进先出”(FIFO)的原则。
    • 循环特性: 当队尾指针到达数组末尾时,如果队列未满,则可以循环到数组开头继续插入新元素。
    • 队空与队满条件: 为了区分队列是空还是满,循环队列通常会牺牲一个数组元素的存储空间:
    • 队列为空的条件:front == rear
    • 队列为满的条件:(rear + 1) % capacity == front
  • 循环队列的应用场景:

    • 缓冲区:在生产者 / 消费者模型中用作环形缓冲区。
    • 流量控制:如网络数据包的接收缓冲区。
    • 任务调度:在任务管理系统中,存储循环调度任务。
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
#include <iostream>

using namespace std;

// 循环队列
class MyQueue {

public:
MyQueue(int size = 20) {
_pQueue = new int[size];
_front = 0;
_rear = 0;
_size = size;
}

// 删除拷贝构造函数,不让用户调用
// MyQueue(const MyQueue &other) = delete;

// 拷贝构造函数
MyQueue(const MyQueue &other) {
_front = other._front;
_rear = other._rear;
_size = other._size;
_pQueue = new int[_size];
for (int i = _front; i != _rear; i = (i + 1) % _size) {
_pQueue[i] = other._pQueue[i];
}
}

// 删除赋值运算符,不让用户调用
// MyQueue &operator=(const MyQueue &other) = delete;

// 赋值运算符重载
MyQueue &operator=(const MyQueue &other) {
if (this == &other) {
return *this;
}

if (_pQueue != nullptr) {
delete[]_pQueue;
}

_front = other._front;
_rear = other._rear;
_size = other._size;
_pQueue = new int[_size];
for (int i = _front; i != _rear; i = (i + 1) % _size) {
_pQueue[i] = other._pQueue[i];
}

return *this;
}

~MyQueue() {
if (_pQueue != nullptr) {
delete[] _pQueue;
_pQueue = nullptr;
}
}

// 入队操作
void push(int value) {
if (full()) {
resize();
}
_pQueue[_rear] = value;
// 循环队列
_rear = (_rear + 1) % _size;
}

// 出队操作
void poll() {
if (empty()) {
return;
}
// 循环队列
_front = (_front + 1) % _size;
}

// 返回队头元素
int front() {
return _pQueue[_front];
}

// 队列是否已满
bool full() {
return (_rear + 1) % _size == _front;
}

// 队列是否为空
bool empty() {
return _front == _rear;
}

private:
int *_pQueue; // 队列的内存空间
int _front; // 队头的位置
int _rear; // 队尾的位置
int _size; // 队列的大小

// 扩容操作
void resize() {
int index = 0;
int *pTemp = new int[_size * 2];
// 循环队列
for (int i = _front; i != _rear; i = (i + 1) % _size) {
pTemp[index++] = _pQueue[i];
}
delete[] _pQueue;
_pQueue = pTemp;
_size *= 2;
_front = 0;
_rear = index;
}
};

int main() {
// 设置随机数种子
srand(time(nullptr));

// 调用普通构造函数
MyQueue q1;
for (int i = 0; i < 20; i++) {
q1.push(rand() % 100);
}

// 调用拷贝构造函数
MyQueue q2(q1);

// 赋值运算符重载
MyQueue q3;
q3 = q1;

while (!q3.empty()) {
cout << q3.front() << " ";
q3.poll();
}
cout << endl;
return 0;
}

程序执行后的输出结果:

1
44 71 16 21 11 75 28 29 40 81 86 28 35 43 99 37 45 66 81 53

构造函数的初始化列表

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

using namespace std;

class CDate {

public:
// 自定义一个构造函数,编译器不会再自动生成一个默认构造函数(无参构造函数)
CDate(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}

void show() {
cout << "year: " << _year << ", month: " << _month << ", day: " << _day << endl;
}

private:
int _year;
int _month;
int _day;
};

class CGoods {

public:
// 使用构造函数的初始化列表,可以指定当前对象的成员变量的初始化方式
CGoods(const char *name, int amount, double price, int year, int month, int day) : _amount(amount), _price(price), _date(year, month, day) {
strcpy(_name, name);
}

void show() {
cout << "name: " << _name << ", amount: " << _amount << ", price: " << _price << endl;
_date.show();
}

private :
char _name[20]; // 静态分配内存
int _amount;
double _price;
CDate _date; // 成员对象

};

int main() {
CGoods goods("Book", 100, 59.9, 1949, 12, 22);
goods.show();
return 0;
}

程序执行后的输出结果:

1
2
name: Book, amount: 100, price: 59.9
year: 1949, month: 12, day: 22

高频面试题

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

using namespace std;

class Test {

public:
Test(int data = 10) : mb(data), ma(mb) {

}

void show() {
cout << "ma: " << ma << ", mb: " << mb << endl;
}

private:
int ma;
int mb;

};

int main() {
Test test;
test.show();
return 0;
}

程序执行后的输出结果:

1
ma: -858993460, mb: 10

特别注意

类成员变量的初始化顺序和它们定义的顺序有关,和构造函数初始化列表中定义的先后顺序无关,更多关于构造函数初始化列表的使用教程请看 这里

类模板与函数模板

函数模板使用

在 C++ 中,与函数模板相关的专业术语(知识点)有以下几个:

  • 函数模板
  • 模板的实例化
  • 模板函数
  • 模板的类型参数
  • 模板的非类型参数
  • 模板的实参推演
  • 模板的特例化(专用化)
  • 非模板函数的重载关系

特别注意

  • 模板代码是不能在一个 .cpp 源文件中定义,然后在另一个 .cpp 源文件中使用的。
  • 模板代码在调用之前,一定要看到模板定义的地方,这样模板才能够进行正常的实例化,产生能够被编译器编译的代码。所以,模板代码一般都是写在 .h 头文件中的,然后在 .cpp 源文件中使用 #include 指令将头文件包含进来。
  • 另一种解决办法是,在调用模板函数之前,通过 template bool compare<int>(int, int) 告诉编译器,提前进行指定类型的模板实例化。

使用案例一

函数模板的使用

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

using namespace std;

// 定义一个模板参数列表
template<typename T>
// 定义一个函数模板
bool compare(T a, T b) {
cout << "call compare(T a, T b)" << endl;
return a > b;
}

// 针对 compare 函数模板,提供 const char* 类型的特例化版本
template<>
bool compare<const char *>(const char *a, const char *b) {
cout << "call compare(const char *a, const char *b)" << endl;
return strcmp(a, b);
}

// 普通函数(非模板函数)
bool compare(const char *a, const char *b) {
cout << "call normal compare()" << endl;
return strcmp(a, b);
}

/**
在函数调用点,编译器会使用用户指定的类型,从原函数模板实例化一份函数代码出来(称为模板函数),如下所示:

bool compare<int> (int a, int b) {

}

bool compare<double> (double a, double b) {

}
*/

int main() {
// 函数的调用点
compare<int>(10, 30);
compare<double>(1.3, 4.5);

// 函数模板的实参推演
compare(20, 40);

// 编译器优先将 compare 处理成普通函数,如果函数不存在,才会去找 compare 模板函数
compare("abc", "efg");
compare<const char *>("abc", "efg");

return 0;
}

程序执行后的输出结果:

1
2
3
4
5
call compare(T a, T b)
call compare(T a, T b)
call compare(T a, T b)
call normal compare()
call compare(const char *a, const char *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
#include <iostream>

using namespace std;

// 函数模板
// 使用模板的非类型参数(必须是整数类型,整数或者地址/引用都可以),非类型参数都是常量,只能使用,不能修改
template<typename T, int SIZE>
void sort(T *array) {
// 冒泡排序
for (int i = 0; i < SIZE - 1; i++) {
for (int j = 0; j < SIZE - 1 - i; j++) {
if (array[j] > array[j + 1]) {
int tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
}
}
}
}

int main() {
int array[] = {12, 4, 15, 3, 9, 23, 63};
const int size = sizeof(array) / sizeof(array[0]);

sort<int, size>(array);

for (int item: array) {
cout << item << " ";
}
cout << endl;

return 0;
}

程序执行后的输出结果:

1
3 4 9 12 15 23 63 

类模板的使用

使用案例一

类模板的使用

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

using namespace std;

// 类模板
template<typename T>
// 顺序栈
class SeqStack {

public:

// 建议构造和析构函数的名称不加 <T>,而且其他使用模板的地方都加上类型参数类列表,比如 <T>

// 构造函数
SeqStack(int size = 10) : _pstatck(new T[size]), _top(-1), _size(size) {

}

// 析构函数
~SeqStack() {
if (_pstatck != nullptr) {
delete[]_pstatck;
_pstatck = nullptr;
}
};

// 拷贝构造函数
SeqStack(const SeqStack<T> &stack) : _top(stack._top), _size(stack._size) {
// 实现深拷贝
_pstatck = new T[_size];
for (int i = 0; i < _top; i++) {
_pstatck[i] = stack._pstatck[i];
}
}

// 赋值运算符重载
SeqStack<T> &operator=(const SeqStack<T> &stack) {
if (this == stack) {
return *this;
}

// 释放原来的内存空间
if (_pstatck != nullptr) {
delete[]_pstatck;
}

// 实现深拷贝
_top = stack._top;
_size = stack._size;
_pstatck = new T[_size];
for (int i = 0; i < _top; i++) {
_pstatck[i] = stack._pstatck[i];
}

return *this;
}

// 入栈
void push(const T &val) {
if (full()) {
resize();
}
_pstatck[++_top] = val;
}

// 弹栈
void pop() {
if (!empty()) {
--_top;
}
}

// 获取栈顶元素
T top() const {
if (empty()) {
throw "stack is empty!";
}
return _pstatck[_top];
}

// 栈是否满了
bool full() const {
return _top == _size - 1;
}

// 栈是否为空
bool empty() const {
return _top == -1;
}

private :
T *_pstatck; // 动态开辟数组,存储顺序栈的元素
int _top; // 指向栈顶元素的位置
int _size; // 数组的总大小

// 扩容操作
void resize() {
T *ptmp = new T[_size * 2];
for (int i = 0; i < _top; i++) {
ptmp[i] = _pstatck[i];
}
delete[]_pstatck;
_pstatck = ptmp;
_size *= 2;
}

};

int main() {
// 设置随机数种子
srand(time(nullptr));

// 类模板的选择性实例化
// 实例化后的模板类 class SeqStack<int> { };
SeqStack<int> stack;
for (int i = 0; i < 20; i++) {
stack.push(rand() % 100);
}

while (!stack.empty()) {
cout << stack.top() << " ";
stack.pop();
}
return 0;
}

程序执行后的输出结果:

1
6 68 25 5 53 1 20 71 3 7 0 99 2 74 78 99 92 30 24 40

使用案例二

类模板的默认类型参数使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 类模板(使用默认类型参数)
template<typename T=int>
// 顺序栈
class SeqStack {

......

}

int main() {
// 使用默认类型参数
SeqStack<> stack;

return 0;
}

使用案例三

使用类模板实现向量容器(Vector)

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

using namespace std;

// 类模板
template<typename T>
// 向量容器
class MyVector {

public:
// 构造函数
MyVector(int size = 10) {
_first = new T[size];
_last = _first;
_end = _first + size;
}

// 析构函数
~MyVector() {
if (_first != nullptr) {
delete[] _first;
_first = _last = _end = nullptr;
}
}

// 拷贝构造函数
MyVector(const MyVector<T> &v) {
// 容器的总大小
int size = v._end - v._first;
// 有效元素的个数
int length = v._last - v._first;
// 深拷贝
_first = new T[size];
for (int i = 0; i < length; i++) {
_first[i] = v._first[i];
}
_last = _first + length;
_end = _first + size;
}

// 赋值运算符重载
MyVector<T> &operator=(const MyVector<T> &v) {
if (this == v) {
return *this;
}

// 释放原来的内存空间
if (_first != nullptr) {
delete[] _first;
}

// 容器的总大小
int size = v._end - v._first;
// 有效元素的个数
int length = v._last - v._first;
// 深拷贝
_first = new T[size];
for (int i = 0; i < length; i++) {
_first[i] = v._first[i];
}
_last = _first + length;
_end = _first + size;

return *this;
}

// 往容器尾部添加元素
void push_back(const T &val) {
if (full()) {
resize();
}
*_last++ = val;
}

// 从容器尾部删除元素
void pop_back() {
if (!empty()) {
--_last;
}
}

// 返回容器尾部的元素
T back() const {
if (empty()) {
throw "MyVector is empty!";
}
return *(_last - 1);
}

// 容器是否满了
bool full() const {
return _last == _end;
}

// 容器是否为空
bool empty() const {
return _first == _last;
}

// 返回有效元素的个数
int size() const {
return _last - _first;
}

private:
T *_first; // 指向数组起始的位置
T *_last; // 指向数组中有效元素的后继位置
T *_end; // 指向数组空间的后继位置

// 扩容操作
void resize() {
int size = _end - _first;
T *_ptemp = new T[size * 2];

for (int i = 0; i < size; i++) {
_ptemp[i] = _first[i];
}

// 释放原来的内存空间
delete[] _first;

_first = _ptemp;
_last = _first + size;
_end = _first + size * 2;
}
};

int main() {
// 设置随机数种子
srand(time(nullptr));

MyVector<int> v;
for (int i = 0; i < 20; i++) {
int val = random() % 100;
v.push_back(val);
cout << val << " ";
}
cout << endl;

cout << "size: " << v.size() << endl;
cout << "full: " << (v.full() ? "true" : " false") << endl;
cout << "empty: " << (v.empty() ? "true" : " false") << endl;

while (!v.empty()) {
cout << v.back() << " ";
v.pop_back();
}

return 0;
}

程序执行后的输出结果:

1
2
3
4
5
49 32 50 26 17 87 26 65 49 83 36 57 97 61 25 44 84 23 41 35 
size: 20
full: true
empty: false
35 41 23 84 44 25 61 97 57 36 83 49 65 26 87 17 26 50 32 49

特别注意

  • 上述代码实现的 vector 容器,如果存放的是 Peson 类的对象,那么在容器初始化的时候,默认会调用 10 次 Peson 类的构造函数,因为在容器的构造函数中使用了 new 操作,比如 _first = new T[size]
  • 上述代码实现的 vector 容器,如果存放的是 Peson 类的对象,当调用 pop_back() 函数来删除容器尾部的元素时,Person 对象所占用的内存空间并没有被释放,这存在内存安全隐患。
  • 解决方法可以参考 C++ STL 中的 vector 容器的实现,也就是使用空间配置器(allocator)来解决,空间配置器负责做四件事情,包括内存开辟、内存释放、对象构造、对象析构。

使用空间分配器优化后的代码(重点知识)

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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#include <iostream>
#include <cstring>

using namespace std;

// 类模板
template<typename T>
// 空间配置器(负责内存开辟、内存释放、对象构造、对象析构)
struct MyAllocator {

// 数组的内存开辟
T *allocate(size_t size) {
return (T *) malloc(sizeof(T) * size);
}

// 数组的内存释放
void deallocate(void *p) {
free(p);
}

// 对象构造
void construct(T *p, const T &val) {
// 在指定的内存上构造对象(定位 new)
new (p)T(val);
}

// 对象析构
void destroy(T *p) {
// ~T() 代表了 T 类型对象的析构函数
p->~T();
}

};

// 类模板
template<typename T, typename Alloc = MyAllocator<T>>
// 向量容器
class MyVector {

public:
// 构造函数
MyVector(int size = 10) {
// 开辟数组的内存空间
_first = _allocator.allocate(size);
_last = _first;
_end = _first + size;
}

// 析构函数(先析构容器内的有效元素,然后再释放 _first 指针指向的堆内存)
~MyVector() {
// 析构容器内的有效元素
for (T *p = _first; p != _last; p++) {
_allocator.destroy(p);
}

// 释放堆上的数组内存
_allocator.deallocate(_first);
_first = _last = _end = nullptr;
}

// 拷贝构造函数
MyVector(const MyVector<T> &v) {
// 容器的总大小
int size = v._end - v._first;
// 有效元素的个数
int length = v._last - v._first;

// 开辟数组的内存空间
_first = _allocator.allocate(size);

// 在指定的内存空间中构造对象
for (int i = 0; i < length; i++) {
_allocator.construct(_first + i, v._first[i]);
}
_last = _first + length;
_end = _first + size;
}

// 赋值运算符重载
MyVector<T> &operator=(const MyVector<T> &v) {
if (this == v) {
return *this;
}

// 析构容器内的有效元素
for (T *p = _first; p != _last; p++) {
_allocator.destroy(p);
}

// 释放堆上的数组内存
_allocator.deallocate(_first);

// 容器的总大小
int size = v._end - v._first;
// 有效元素的个数
int length = v._last - v._first;

// 开辟数组的内存空间
_first = _allocator.allocate(size);

// 在指定的内存空间中构造对象
for (int i = 0; i < length; i++) {
_allocator.construct(_first + i, v._first[i]);
}
_last = _first + length;
_end = _first + size;

return *this;
}

// 往容器尾部添加元素
void push_back(const T &val) {
if (full()) {
resize();
}
// 在指定的内存空间中构造对象
_allocator.construct(_last, val);
_last++;
}

// 从容器尾部删除元素(需要将对象的析构和内存释放分开处理)
void pop_back() {
if (!empty()) {
_last--;
// 在指定的内存空间中析构对象
_allocator.destroy(_last);
}
}

// 返回容器尾部的元素
T back() const {
if (empty()) {
throw "MyVector is empty!";
}
return *(_last - 1);
}

// 容器是否满了
bool full() const {
return _last == _end;
}

// 容器是否为空
bool empty() const {
return _first == _last;
}

// 返回有效元素的个数
int size() const {
return _last - _first;
}

private:
T *_first; // 指向数组起始的位置
T *_last; // 指向数组中有效元素的后继位置
T *_end; // 指向数组空间的后继位置
Alloc _allocator; // 定义容器空间配置器的对象

// 扩容操作
void resize() {
int size = _end - _first;
// 开辟数组的内存空间
T *_ptemp = _allocator.allocate(size * 2);
// 在指定的内存空间中构造对象
for (int i = 0; i < size; i++) {
_allocator.construct(_ptemp + i, _first[i]);
}

// 析构原来容器内的有效元素
for (T *p = _first; p != _last; p++) {
_allocator.destroy(p);
}

// 释放原来的数组内存
_allocator.deallocate(_first);

_first = _ptemp;
_last = _first + size;
_end = _first + size * 2;
}
};

class Person {

public:
Person() {
cout << "call Person()" << endl;
}

Person(const Person &p) {
cout << "call Person(const Person &p)" << endl;
}

~Person() {
cout << "call ~Person()" << endl;
}

};

void test01() {
cout << "============= test01() =============" << endl;

MyVector<int> v;
for (int i = 0; i < 20; i++) {
int val = random() % 100;
v.push_back(val);
cout << val << " ";
}
cout << endl;

cout << "size: " << v.size() << endl;
cout << "full: " << (v.full() ? "true" : " false") << endl;
cout << "empty: " << (v.empty() ? "true" : " false") << endl;

while (!v.empty()) {
cout << v.back() << " ";
v.pop_back();
}
}

void test02() {
cout << "\n\n============= test02() =============" << endl;

Person p1, p2, p3;
cout << "------------------------------------------" << endl;
MyVector<Person> v;
v.push_back(p1);
v.push_back(p2);
v.push_back(p3);
cout << "------------------------------------------" << endl;
v.pop_back();
cout << "------------------------------------------" << endl;
}

int main() {
// 设置随机数种子
srand(time(nullptr));

test01();
test02();
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
============= test01() =============
44 60 71 5 8 44 58 52 73 97 20 21 0 10 28 44 94 81 54 82
size: 20
full: true
empty: false
82 54 81 94 44 28 10 0 21 20 97 73 52 58 44 8 5 71 60 44

============= test02() =============
call Person()
call Person()
call Person()
------------------------------------------
call Person(const Person &p)
call Person(const Person &p)
call Person(const Person &p)
------------------------------------------
call ~Person()
------------------------------------------
call ~Person()
call ~Person()
call ~Person()
call ~Person()
call ~Person()