前言

内功学习推荐

  • CSAPP 第七章:链接
  • 程序员的自我修养:第2,3,4,6章

基础知识

形参带默认值

形参带默认值的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
int sum(int a, int b){
return a + b;
}
int sum1(int a=2, int b=4){
return a + b;
}
int main(){
int a = 10;
int b = 20;
int ret = sum(a, b);
cout << "ret:" << ret << endl;
return 0;
}

Note1

若是传入实参,则使用的就是你的实参。没有传入就使用形参的默认值。

1
2
3
4
5
6
7
8
9
10
#include<bits/stdc++.h>

using namespace std;
int fun(int a=1,int b){
return a+b;
}
signed main(){
cout<<fun(2);
return 0;
}

bug

1
4	5	E:\CPP\996.cpp	[Error] default argument missing for parameter 2 of 'int fun(int, int)'

Note:形参给默认值的时候,只能从右向左给。因为压栈的时候是从右向左压的。

Note2

调用形参带默认值的函数,其效率与不带的比较?

  • 效率是增长了,毕竟少了mov指令

汇编查看

  • pre
1
2
3
4
5
01112156  mov         eax,dword ptr [b]  【ebp-8】
01112159 push eax
0111215A mov ecx,dword ptr [a] 【ebp-4】
0111215D push ecx
0111215E call sum (01111406h)
  • now
1
2
3
0111217A  push        14h  
0111217C push 0Ah
0111217E call sum (01111406h)

Note3

函数形参变量给默认值,定义处可以声明也可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int sum(int a = 10, int b = 20);
int main()
{
int a = 10;
int b = 20;

int ret = sum(a, b);

ret = sum(a);

ret = sum();

ret = sum(a, 40);
return 0;
}
int sum(int a , int b )
{
return a + b;
}

Note4

函数声明可以很多次,定义只有一次。但是形参给默认值,不管是在定义处还是声明处给,形参默认值只能出现一次。

内联inline

内联函数和普通函数的区别:

  1. inline函数在编译过程中,没有函数调用的开销。在函数调用点(int ret = sum(a, b);)直接把函数的代码进行展开处理。
  2. inline函数(内联成功)不会在符号表中生成相应的函数符号。
  3. inline只是建议编译器把这个函数处理为内联函数,但是编译器最终决定是否处理为内联函数的。
  4. 可以通过命令查看:objdump -t main.o。就看在符号表里面有没有产生符号,若内联成功肯定没有符号。
  5. 在debug版本上,inline是不起作用的(也有普通函数的调用开销),因为是debug版本需要调试的。

Note:递归不会产生内联。

具体代码规范参考《Google Style C++》

函数重载

pro1:C++为什么支持函数重载,但是C语言不支持?

编译器编译代码产生符号的规则不同。

  1. C++代码产生函数符号的时候,是由 函数名+参数列表类型 组成的。
  2. C语言产生函数符号的时候,是只由函数名来决定的。(在C语言中,函数名相同,所以就会产生链接错误:找到多个函数的定义)

pro2:函数重载需要注意些什么?

函数重载:一组函数,其函数名相同,参数列表的个数或者类型不同,那么这一组函数就可以称作为函数重载。一组函数要称得上重载,一定得先处于同一个作用域当中的。重载函数名字都一样,在编译器编译的过程中会根据函数调用的时候 传入的实参的类型来选择合适的函数重载版本。

注意点

  1. 函数重载定义:一组函数,其函数名相同,参数列表的个数或者类型不同,那么这一组函数就可以称作为函数重载。
  2. 一组函数要称得上重载,一定得先处于同一个作用域当中的(从函数调用点来看,这组函数是处于同一个作用域 。注:在函数中不可以定义函数,可以声明函数,但是需要注意其中的作用域问题)
  3. 当给参数加上const或者volatile(标准的C/C++的关键字)的时候,是怎么影响形参类型的?(下面会进行详解)
  4. 一组函数,函数名相同,参数列表也相同,仅仅是返回值不同?这不是重载
  • 返回值相不相同和函数重载没有任何关系,函数重载的主要原因就是:虽然看起来函数名相同但最终生成的符号是不同的。因为符号是函数名+形参列表。

程序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
#include<bits/stdc++.h>

using namespace std;

bool compare(int a, int b)
{
cout << "compare_int_int" << endl;
return a > b;
}
bool compare(double a, double b)
{
cout << "compare_double_double" << endl;
return a > b;
}
bool compare(const char* a, const char* b)
{
cout << "compare_const char*_const char*" << endl;
return strcmp(a, b) > 0;
}
int main()
{
compare(10, 20);
compare(10.0, 20.0);
compare("aa", "bb");
return 0;
}

运行结果

1
2
3
compare_int_int
compare_double_double
compare_const char*_const char*

程序2:不同作用域

在C和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
#include<bits/stdc++.h>

using namespace std;

bool compare(int a, int b)
{
cout << "compare_int_int" << endl;
return a > b;
}
bool compare(double a, double b)
{
cout << "compare_double_double" << endl;
return a > b;
}
bool compare(const char* a, const char* b)
{
cout << "compare_const char*_const char*" << endl;
return strcmp(a, b) > 0;
}
int main()
{ bool compare(int a, int b);
compare(10, 20);
compare(10.0, 20.0);
compare("aa", "bb");
return 0;
}

报错信息:

1
24	20	E:\CPP\996.cpp	[Error] invalid conversion from 'const char*' to 'int' [-fpermissive]

分析:在C语言和C++中,都是分作用域的。在函数里面也可以定义一个和全局变量同名的局部变量,作用域不同 是可以定义同名变量。在使用这个变量的时候,会优先使用在当前最近的作用域下找这个变量。

程序3:当给参数加上const或者volatile

1
2
3
4
5
6
7
8
9
10
11
#include<bits/stdc++.h>

using namespace std;

int func(int a){}
int func(const int a){}
int main()
{
cout<<fun(1);
return 0;
}

运行结果

1
6	5	E:\CPP\996.cpp	[Error] redefinition of 'int func(int)'

对于编译器而言,两个func的形参类型都是int。相当于函数重定义了

**补充:**C++和C语言代码之间如何相互调用?

在说这个问题之前,需要先了解下两者之间的差异。

最大的差异是C++支持函数重载,而C语言不支持。为了使函数支持重载,C++在C语言的基础上,将函数名添加上返回值和参数的类型信息。例如,int add(int, int)这个函数,通过C++编译器编译后,可能呈现的函数名为int int_add_int_int(int, int)。因此调用过程在链接阶段可能会出现错误。

C调用C++

  1. 将函数用extern "C"声明。
  2. C代码中不要include C++的头文件, 而采用直接在C中增加函数声明的方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*C++ code*/
extern "C" void f(int);
void f(int i)
{
// your code
}

/*C code*/
void f(int); // 不引入, 而只是直接声明
void cc(int i)
{
f(i); //调用
// other code
}

C++调用C

cfun.h

1
2
3
4
5
6
7
8
9
10
11
12
13
// --------------cfun.h
#ifndef __C_FUN_20180228_H__
#define __C_FUN_20180228_H__

#ifdef __cplusplus
extern "C"{
#endif // __cplusplus

void cfun();
#ifdef __cplusplus
}
#endif
#endif

#ifdef __cplusplus, 表示如果是C++来调用该接口,则该函数接口经编译后的函数符号生成规则按照C风格走, 否则没有extern “C” , 这样提供的接口同时支持C和C++两者的调用。

1
2
3
4
5
6
7
8
// --------------cfun.c
#include "cfun.h"
#include <stdio.h>

void cfun()
{
printf("hello world.\n");
}
1
2
3
4
5
6
7
8
9
10
11
// --------------main.cpp
#include <iostream>
#include "cfun.h"


int main()
{
cfun();
system("pause");
return 0;
}

const修饰

const是什么?

const修饰的变量不能再作为左治,初始化完成,值不能修改!

const在C和C++中的区别?

  1. 初始化

    • C++中的const,必须初始化
  2. 编译方式不一样:

    • C中,const就是当作一个变量[常变量]来编译生成指令
  • C++中,所有出现const常量名字的地方,都会常量的初始值代替

const和一级指针

待填坑…

const和二级指针

待填坑…

面向对象

Q:OOP语言的四大特征是什么?

A:抽象 ,封装(隐藏), 继承, 多态。

Q:封装(隐藏)体现在?

A:使用的访问限定符 (public private protected)

Q:对象的内存大小计算

A:对象的内存大小只依赖于 成员变量,与成员方法的多少无关。算成员变量的大小是和结构体变量的内存计算方式一样:先找占用内存最长的成员变量,以其为内存字节对齐的方式。然后计算出总的对象的大小。

Note:静态成员变量不占用对象大小。

类有无数个对象,每个对象都有自己的成员变量,但是共享一套成员方法【方法放在代码段】

Q:当用对象调用类里面的同一套方法时,这个方法是怎么知道处理哪个对象的信息呢?

A:this 指针:this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。

1
2
3
4
5
6
7
class A{
init();// 编译器给加的:init(A *const this,...)
};
int main(){
A a;
a.init();// 编译器给加的: init(&a,...)
}

类的成员方法经过编译,所有方法参数都会加上this指针,接受该方法的对象地址。

Q:对象的构造函数和析构函数的时机?

A:先构造的后析构,后构造的先析构

Q:动态和静态定义对象?

A:静态建立一个类对象, 是由编译器为对象在栈空间中分配内存, 通过直接移动栈顶指针挪出适当的空间, 然后在这片内存空间上调用构造函数形成一个栈对象。 动态建立类对象, 是使用new运算符将对象建立在堆空间中, 在栈中只保留了指向该对象的指针。

Q:深拷贝和浅拷贝?

对象默认的拷贝构造是在做内存的数据拷贝。在new的时候会存在问题。

case1:默认拷贝构造函数

1
A a=b;(等于A a(b))

case2:默认赋值函数

1
2
A a;
a=b;

Q:看看下面代码哪错了

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>
//#include <typeinfo>
//#include <string>
//#include <functional>

#include <bits/stdc++.h>

using namespace std;
class A{
public:
A(int a){
a_=a;
}
int a_;
};
class B{
public:
B(int aa){
tmp=A(aa);
}
A tmp;
};
signed main(){
B b(11);
return 0;
}

构造函数初始化列表的时候,tmp=A(aa)赋值语句本质是

1
2
A tmp
tmp=A(aa)

由于A类中提供了有参构造,因此,不会生成默认的无参构造。所以A tmp报错。

如果修改正确?

方式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
//#include <iostream>
//#include <typeinfo>
//#include <string>
//#include <functional>

#include <bits/stdc++.h>

using namespace std;
class A{
public:
A(int a){
a_=a;
}
int a_;
};
class B{
public:
B(int aa):tmp(A(aa)){
}
A tmp;
};
signed main(){
B b(11);
return 0;
}

方式2:

在A中添加无参构造。

Q:构造函数初始化顺序问题?

A:结论:谁先定义,谁先初始化。因为C++的·对象模型在内存中是连续的!

Q:类的成员方法有哪些?区别是什么?

A:

  • 普通方法要用对象调用=》编译会产生this指针
  • 静态方法用类的作用域调用=》没有this指针

模板编程

函数模板

函数模板 : 是不进行编译的,因为(形参a b)类型还不知道。(区分函数模板很简单,前面看有没有加template和模板参数列表)。

模板的实例化:是在函数调用点进行实例化(由用户指定的类型,或实参推演出的类型)。

模板函数: 在函数调用点,编译器用用户指定的类型,从原模板实例化一份函数代码出来。这是真真正正需要代码编译(编译器编译)的函数。

模板的实参推演 :可以根据用户传入的实参的类型,来推导出模板类型参数的具体类型。

模板代码是不能在一个文件中定义,在另外一个文件中使用的。

image-20240503193134169

针对某些类型来说,依赖编译器默认实例化的模板代码,代码处理逻辑是有错误的。

因此,使用模板的特例化的实例化。

下面来看个例子

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
#include <bits/stdc++.h>

using namespace std;

template<typename T>//定义一个模板参数列表

//用T类型 定义具体的形参变量
bool compare(T a, T b){
cout << "template compare" << endl;
return a > b;
}

//针对compare函数模板,提供const char *类型的特例化版本
//不可省略:表示这个特例化也是从上面的模板来的,先有模板才有特例化。
template<>
bool compare<const char*>(const char* a, const char* b){
cout << "compare<const char*>" << endl;
return strcmp(a, b) > 0;
}
////非模板函数
//bool compare(const char* a, const char* b){
// cout << "普通的compare" << endl;
// return strcmp(a, b) > 0;
//}

signed main(){
cout<<compare("11","2");
return 0;
}

Note:模板的非类型参数,都是常量

类模板

类模板的使用实际上是类模板实例化成一个具体的类(而非模板类)。模板参数列表里面一般都是定义参数,用来初始化类型的。即都是定义模板类型参数。模板类型参数:< > 里面可以由typename/class定义 模板 类型参数T (可以定义多个,由,隔开)T用来接收 类型的。

直接来个例子练练手吧~

实现C++ STL向量容器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
#include<bits/stdc++.h>

using namespace std;
template<typename T=int>//定义模板类型参数
class MyVector
{
public:
MyVector(int size = 5)//底层开辟5个元素的空间
{
_first = new T[size];
_last = _first;
_end = _first + size;
}
~MyVector()
{
delete[]_first;
_first = _last = _end = nullptr;
}
MyVector(const MyVector<T>& src)
{
int size = src._end - src._first;//总的空间 个数
_first = new T[size];
_end = _first + size;
int len = src._last - src._first;//有效长度
_last = _first + len;
for (int i = 0; i < len; ++i)
{
_first[i] = src._first[i];//把值赋过来
}
}
MyVector<T>& operator=(const MyVector<T>& src)
{
if (this == &src)
{
return *this;
}

delete[]_first;

int size = src._end - src._first;// 总的空间 个数
_first = new T[size];
_end = _first + size;
int len = src._last - src._first;// 有效长度
_last = _first + len;
for (int i = 0; i < len; ++i)
{
_first[i] = src._first[i];// 把值赋过来
}
return *this;
}
void push_back(const T& val)// 向容器末尾添加元素
{
if (full())
{
resize();
}
*_last++ = val;
}
void pop_back()// 向容器末尾删除元素
{
if (empty())
{
return;
}
*_last--;
}
T getValueBack()const// 返回容器末尾的值
{
return *(_last - 1);
}
bool full()const
{
return _last == _end;
}
bool empty()const
{
return _first == _last;
}
int cursize()const//元素的个数
{
return _last - _first;
}
private:
T* _first;// 数组起始位置
T* _last;// 最后一个有效元素的 后继
T* _end;// 数组最后一个空间位置的 后继

void resize()// 容器的2倍扩容
{
cout << "resize()" << endl;
int size = cursize() * 2;
T* newfirst = new T[size];
for (int i = 0; i < cursize(); ++i)
{
newfirst[i] = _first[i];
}
delete[]_first;
_first = newfirst;
_last = _first + size / 2;
_end = _first + size;
}
};

int main()
{
MyVector<>myvec;
cout << "当前容器的容量是:" << myvec.cursize() << endl;
for (int i = 0; i < 12; ++i)
{
myvec.push_back(rand() % 25);
}
cout << "当前容器的容量是:" << myvec.cursize() << endl;
while (!myvec.empty())
{
cout << myvec.getValueBack() << " ";
myvec.pop_back();
}
cout << endl;
cout << "当前容器的容量是:" << myvec.cursize() << endl;
return 0;
}

特化、偏特化

说到C++模板特化与偏特化,就不得不简要的先说说C++中的模板。我们都知道,强类型的程序设计迫使我们为逻辑结构相同而具体数据类型不同的对象编写模式一致的代码,而无法抽取其中的共性,这样显然不利于程序的扩充和维护。C++模板就应运而生。C++的模板提供了对逻辑结构相同的数据对象通用行为的定义。这些模板运算对象的类型不是实际的数据类型,而是一种参数化的类型。C++中的模板分为类模板和函数模板。

注意:

  • 编译器并不是把函数模板处理成能够处理任何类型的函数;函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译,在声明处对模板代码本身进行编译(检查语法),在调用处对参数替换后的代码进行编译。
  • 函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须有程序员在程序中显式地指定,即 函数模板允许隐式调用和显式调用而类模板只能显示调用。【很累】
  • 注意在继承中,类模板的写法
1
2
3
4
5
template<typename T>
class A{};

template<typename T>
class B:public A<typename T>{};

类模板如下:

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

template <class T>
class TClass
{
public:
// TClass的成员函数

private:
T DateMember;
};

函数模板如下:

1
2
3
4
5
template <class T>
T Max(const T a, const T b)
{
return a > b ? a : b;
}

2、模板特化

有时为了需要,针对特定的类型,需要对模板进行特化,也就是所谓的特殊处理。比如有以下的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

template <class T>
class TClass
{
public:
bool Equal(const T& arg, const T& arg1);
};

template <class T>
bool TClass<T>::Equal(const T& arg, const T& arg1)
{
return (arg == arg1);
}

int main()
{
TClass<int> obj;
cout<<obj.Equal(2, 2)<<endl;
cout<<obj.Equal(2, 4)<<endl;
}

类里面就包括一个Equal方法,用来比较两个参数是否相等;上面的代码运行没有任何问题;但是,你有没有想过,在实际开发中是万万不能这样写的,对于float类型或者double的参数,绝对不能直接使用“==”符号进行判断。所以,对于float或者double类型,我们需要进行特殊处理,处理如下:

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

template <class T>
class Compare
{
public:
bool IsEqual(const T& arg, const T& arg1);
};

// 已经不具有template的意思了,已经明确为float了
template <>
class Compare<float>
{
public:
bool IsEqual(const float& arg, const float& arg1);
};

// 已经不具有template的意思了,已经明确为double了
template <>
class Compare<double>
{
public:
bool IsEqual(const double& arg, const double& arg1);
};

template <class T>
bool Compare<T>::IsEqual(const T& arg, const T& arg1)
{
cout<<"Call Compare<T>::IsEqual"<<endl;
return (arg == arg1);
}

bool Compare<float>::IsEqual(const float& arg, const float& arg1)
{
cout<<"Call Compare<float>::IsEqual"<<endl;
return (abs(arg - arg1) < 10e-3);
}

bool Compare<double>::IsEqual(const double& arg, const double& arg1)
{
cout<<"Call Compare<double>::IsEqual"<<endl;
return (abs(arg - arg1) < 10e-6);
}

int main()
{
Compare<int> obj;
Compare<float> obj1;
Compare<double> obj2;
cout<<obj.IsEqual(2, 2)<<endl;
cout<<obj1.IsEqual(2.003, 2.002)<<endl;
cout<<obj2.IsEqual(3.000002, 3.0000021)<<endl;
}

3、模板偏特化

上面对模板的特化进行了总结。那模板的偏特化呢?所谓的偏特化是指提供另一份template定义式,而其本身仍为templatized;也就是说,针对template参数更进一步的条件限制所设计出来的一个特化版本。这种偏特化的应用在STL中是随处可见的。

与模板特化的区别在于,模板特化以后,实际上其本身已经不是templatized,而偏特化,仍然带有templatized。我们来看一个实际的例子:

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

// 一般化设计
template <class T, class T1>
class TestClass
{
public:
TestClass()
{
cout<<"T, T1"<<endl;
}
};

// 针对普通指针的偏特化设计
template <class T, class T1>
class TestClass<T*, T1*>
{
public:
TestClass()
{
cout<<"T*, T1*"<<endl;
}
};

// 针对const指针的偏特化设计
template <class T, class T1>
class TestClass<const T*, T1*>
{
public:
TestClass()
{
cout<<"const T*, T1*"<<endl;
}
};

int main()
{
TestClass<int, char> obj;
TestClass<int *, char *> obj1;
TestClass<const int *, char *> obj2;

return 0;
}

总结

  • 模板化(Templatized):创建通用的模板代码,可以适用于多种类型或值。
  • 模板特化(Full Specialization):为特定类型提供完全不同的实现,该实现不再是通用的。
  • 偏特化(Partial Specialization):为某些类型模式提供特殊处理,仍然保留一定程度的通用性。

理解容器空间配置器allocator

为什么要提出这个东东呢?

先来看个例子,PS:代码示例虽然长,但是比起文字更有力量!

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
#include<bits/stdc++.h>

using namespace std;

template<typename T>
class A{
public:
A(){
p=new T[5];
}
~A(){
delete[] p;
}
T *p;
};
class Test{
public:
Test(){
cout<<"Test()\n";
}
~Test(){
cout<<"~Test()\n";
}
};
signed main(){
A<Test> a;
return 0;
}

运行结果

1
2
3
4
5
6
7
8
9
10
Test()
Test()
Test()
Test()
Test()
~Test()
~Test()
~Test()
~Test()
~Test()

这一个空的容器,竟然给我构造了5个 Test对象。出了作用域,又析构了5个Test对象。我现在就只是实例化一个这个空的vector容器。

若是用户传入了个 10000,那么他只是想 定义一个容器对象。然后就给人家构造了10000个对象,这不是人家想要的。

因此,现在的需求:需要把内存开辟 和 对象的构造分开处理。在定义容器对象的时候,需要做的只是 底层开辟空间(只是给容器这个数组开辟空间,而不去构造对象)。

废话少说,直接看代码

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
#include<bits/stdc++.h>

using namespace std;
// 定义容器的空间配置器,和C++标准库的allocator实现一样
//默认的内存管理都是free malloc(也可以采用内存池的方式)
template<typename T>
struct Allocator
{
T* allocate(size_t size) // 只负责内存开辟
{
return (T*)malloc(sizeof(T) * size);
}
void deallocate(void* p) // 只负责内存释放
{
free(p);
}
void construct(T* p, const T& val) // 只负责对象构造
{
//在我们指定的地址里面构造一个值为val的对象
//在一个已经存在 开辟好的内存上去构造一个值为val的对象
new (p) T(val); // 定位new T类型的拷贝构造
}
void destroy(T* p) // 只负责对象析构
{
p->~T(); // ~T()代表了T类型的析构函数
}
};

/*
容器底层内存开辟,内存释放,对象构造和析构,
都通过allocator空间配置器来实现
*/

template<typename T=int,typename Alloc=Allocator<T>>//定义模板类型参数
class MyVector
{
public:
MyVector(int size = 5)//底层开辟5个元素的空间
{
//需要把内存开辟和对象的构造分开处理
//_first = new T[size];

_first = _allocator.allocate(size);
_last = _first;
_end = _first + size;
}
~MyVector()
{
//析构容器里面有效的元素,再去释放_first指向的堆内存
//delete[]_first;

for (T* p = _first; p != _last; ++p)
{
//把_first指针指向的数组的有效元素进行析构操作
_allocator.destroy(p);
}
_allocator.deallocate(_first);// 释放堆上的数组内存
_first = _last = _end = nullptr;
}
MyVector(const MyVector<T>& src)
{
int size = src._end - src._first;//总的空间 个数
//_first = new T[size];

_first = _allocator.allocate(size);
_end = _first + size;
int len = src._last - src._first;//有效长度
_last = _first + len;
for (int i = 0; i < len; ++i)
{
//_first[i] = src._first[i];//把值赋过来

_allocator.construct(_first + i, src._first[i]);
}
}
MyVector<T>& operator=(const MyVector<T>& src)
{
if (this == &src)
{
return *this;
}

//delete[]_first;

for (T* p = _first; p != _last; ++p)
{
//把_first指针指向的数组的有效元素进行析构操作
_allocator.destory(p);
}
_allocator.deallocate(_first);// 释放堆上的数组内存

int size = src._end - src._first;//总的空间 个数
//_first = new T[size];

_first = _allocator.allocate(size);
_end = _first + size;
int len = src._last - src._first;//有效长度
_last = _first + len;
for (int i = 0; i < len; ++i)
{
//_first[i] = src._first[i];//把值赋过来

_allocator.construct(_first + i, src._first[i]);
}
return *this;
}
void push_back(const T& val)//向容器末尾添加元素
{
if (full())
{
resize();
}
//*_last= val;
//_last++;

//_last指针指向的内存构造一个值为val的对象
_allocator.construct(_last, val);
_last++;
}
void pop_back()//向容器末尾删除元素
{
if (empty())
{
return;
}
//_last--;// 不仅要把_last指针--,还需要析构删除的元素
--_last;
_allocator.destroy(_last);

}
T getValueBack()const//返回容器末尾的值
{
return *(_last - 1);
}
bool full()const
{
return _last == _end;
}
bool empty()const
{
return _first == _last;
}
int cursize()const//元素的个数
{
return _last - _first;
}
private:
T* _first;//数组起始位置
T* _last;//最后一个有效元素的 后继
T* _end;//数组最后一个空间位置的 后继
Alloc _allocator;//定义的容器 空间配置器对象

void resize()//容器的2倍扩容
{
cout << "resize()" << endl;
int size = cursize() * 2;
//T* newfirst = new T[size];

T* newfirst = _allocator.allocate(size);

for (int i = 0; i < cursize(); ++i)
{
_allocator.construct(newfirst + i, _first[i]);
//newfirst[i] = _first[i];
}
//delete[]_first;

for (T* p = _first; p != _last; ++p)
{
//把_first指针指向的数组的有效元素进行析构操作
_allocator.destroy(p);
}
_allocator.deallocate(_first);// 释放堆上的数组内存

_first = newfirst;
_last = _first + size / 2;
_end = _first + size;
}
};
class Test
{
public:
Test() { cout << "Test()" << endl; }
~Test() { cout << "~Test()" << endl; }
Test(const Test&) { cout << "Test(const Test&)" << endl; }
};
int main()
{
Test t1, t2, t3;
cout << "++++++++++++++++++++++++++++++++++++++++++++++" << endl;
//MyVector<>myvec;//定义了一个容器,里面并没有添加元素
MyVector<Test>myvec;//用Test类型 实例化一下这个vector容器
myvec.push_back(t1);
myvec.push_back(t2);
myvec.push_back(t3);
cout << "++++++++++++++++++++++++++++++++++++++++++++++" << endl;
myvec.pop_back();
cout << "++++++++++++++++++++++++++++++++++++++++++++++" << endl;
return 0;
}

通过空间配置器的功能,就可以很好的解决上面的问题。

补充:定位new运算符

定位new(placement new)是C++中的一种new操作符,它允许在已分配的内存中构造对象。这种构造方式不会为对象分配新的内存,而是在已经存在的内存中创建对象。

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

class MyClass {
public:
MyClass() { std::cout << "MyClass constructed\n"; }
MyClass(int a) { std::cout << "MyClass constructed:"<<a<<std::endl; }
~MyClass() { std::cout << "MyClass destroyed\n"; }
};

int main() {
char buffer[50]; // 分配一块内存

MyClass *p = new (buffer) MyClass(10); // 在已分配的内存中构造对象

p->~MyClass(); // 手动调用析构函数

return 0;
}

运算符重载

Note1:

++ --单目运算符
operator++() 前置++
operator++(int) 后置++

String类的实现

实现String类,并支持以下功能:

  1. 支持默认构造
  2. 说明string类 里面有string(const char*参数)的构造函数
  3. 里面有 加法运算符的重载函数
  4. 里面有类内实现的:支持对象+ const char*的
  5. 还要全局实现的:const char*+对象

Note:strlen§表示字符串有效字符个数,但是字符后面存在\0,因此申请大小的时候要加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
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
#include <iostream>
#include <algorithm>
#include <functional>
#include <memory>
#include <queue>
#include <thread>
#include <stdio.h>
#include <string.h>
using namespace std;
class String
{
public:
String(const char *str = nullptr)
{
if (str != nullptr)
{
str_ = new char[strlen(str) + 1];
strcpy(str_, str);
}
else
{
str_ = new char[1];
*str_ = '\0';
}
}
~String()
{
delete[] str_;
str_ = nullptr;
}
// 拷贝构造
String(const String &obj)
{
str_ = new char[strlen(obj.str_) + 1];
strcpy(str_, obj.str_);
}
// 赋值运算,保证可以连续赋值
String &operator=(const String &obj)
{
if (this == &obj)
{
return *this;
}
delete[] str_;
str_ = new char[strlen(obj.str_) + 1];
strcpy(str_, obj.str_);
return *this;
}
bool operator>(const String &obj)
{
return strcmp(str_, obj.str_) > 0;
}
bool operator<(const String &obj)
{
return strcmp(str_, obj.str_) < 0;
}
bool operator==(const String &obj)
{
return strcmp(str_, obj.str_) == 0;
}
int length() const
{
return strlen(str_);
}
const char &operator[](int index) const
{
return str_[index];
}
const char *c_str() const
{
return str_;
}

private:
// 在一个类中指明其他的类(或者)函数能够直接访问该类中的private和protected成员
friend String operator+(const String &des, const String src);
friend istream &operator>>(istream &in, String &src);
friend ostream &operator<<(ostream &out, const String &src);

char *str_;
};
ostream &operator<<(ostream &out, const String &src)
{
out << src.str_;
return out;
}
istream &operator>>(istream &in, String &src)
{
in >> src.str_;
return in;
}
String operator+(const String &des, const String src)
{
char *newstr = new char[strlen(des.str_) + strlen(src.str_) + 1];
strcpy(newstr, des.str_);
newstr = strcat(newstr, src.str_);
return String(newstr);
}

signed main()
{
String a = "abc";
String b = "abc";
String c = a + b;
cout << c << endl;
cout << c.length() << endl;
if (a > b)
{
cout << "a>b" << endl;
}
else
{
cout << "a<b" << endl;
}
return 0;
}

完成以上示例,我们还要继续完善,如果给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
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
#include <iostream>
#include <algorithm>
#include <functional>
#include <memory>
#include <queue>
#include <thread>
#include <stdio.h>
#include <string.h>
using namespace std;
class String
{
public:
String(const char *str = nullptr)
{
if (str != nullptr)
{
str_ = new char[strlen(str) + 1];
strcpy(str_, str);
}
else
{
str_ = new char[1];
*str_ = '\0';
}
}
~String()
{
delete[] str_;
str_ = nullptr;
}
// 拷贝构造
String(const String &obj)
{
str_ = new char[strlen(obj.str_) + 1];
strcpy(str_, obj.str_);
}
// 赋值运算,保证可以连续赋值
String &operator=(const String &obj)
{
if (this == &obj)
{
return *this;
}
delete[] str_;
str_ = new char[strlen(obj.str_) + 1];
strcpy(str_, obj.str_);
return *this;
}
bool operator>(const String &obj)
{
return strcmp(str_, obj.str_) > 0;
}
bool operator<(const String &obj)
{
return strcmp(str_, obj.str_) < 0;
}
bool operator==(const String &obj)
{
return strcmp(str_, obj.str_) == 0;
}
int length() const
{
return strlen(str_);
}
const char &operator[](int index) const
{
return str_[index];
}
const char *c_str() const
{
return str_;
}
class iterator
{
public:
iterator(char *p = nullptr) : p_(p) {}
bool operator!=(const iterator &it)
{
return it.p_ != p_;
}
void operator++()
{
p_++;
}
char &operator*()
{
return *p_;
}

private:
char *p_;
};
iterator begin()
{
return iterator(str_);
}
iterator end()
{
return iterator(str_ + length());
}

private:
// 在一个类中指明其他的类(或者)函数能够直接访问该类中的private和protected成员
friend String
operator+(const String &des, const String src);
friend istream &operator>>(istream &in, String &src);
friend ostream &operator<<(ostream &out, const String &src);

char *str_;
};
ostream &operator<<(ostream &out, const String &src)
{
out << src.str_;
return out;
}
istream &operator>>(istream &in, String &src)
{
in >> src.str_;
return in;
}
String operator+(const String &des, const String src)
{
char *newstr = new char[strlen(des.str_) + strlen(src.str_) + 1];
strcpy(newstr, des.str_);
newstr = strcat(newstr, src.str_);
return String(newstr);
}

signed main()
{
String a = "abc";
String b = "abc";
String c = a + b;
cout << c << endl;
cout << c.length() << endl;
if (a > b)
{
cout << "a>b" << endl;
}
else
{
cout << "a<b" << endl;
}
for (auto it = c.begin(); it != c.end(); ++it)
{
cout << *it << endl;
}
return 0;
}

接下来,我们讨论个常见的问题:容器的迭代器 iterator失效

迭代器失效通常发生在对容器进行修改操作后。下面是一些常见的情况:

  1. 向容器中添加元素:如果容器的内部存储空间不足以容纳新的元素,容器可能需要分配新的内存空间,将所有元素移动到新的位置。在这种情况下,指向容器中元素的所有迭代器都会失效。
  2. 从容器中删除元素:删除元素会导致容器中后面的元素向前移动,填补空出的位置。在这种情况下,指向被删除元素和它之后的元素的迭代器都会失效。
  3. 对容器进行排序或重新排列:这些操作会改变元素的位置,导致指向容器中元素的迭代器失效。
  4. 清空容器:清空容器后,指向容器中任何元素的迭代器都会失效。

Vector类的实现

【重点】在上述的vector代码继续完善insert erase的实现。

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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
#include<bits/stdc++.h>

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(p)T(val);
}
void destroy(T* p)//负责对象析构
{
p->~T();//~T()代表了T类型的析构函数
}
};
/*
容器底层内存开辟,内存释放,对象构造和析构
都通过allocator空间配置器来实现
*/
template<typename T=int,typename Allo= MyAllocator<T>>
class MyVector
{
public:
MyVector(int size = 2)
{
cout << "构造函数" << endl;
//需要把内存开辟 和 对象构造分开,不能用new
_first = _allocator.allocate(size);
_last = _first;
_end = _first + size;
}
~MyVector()
{
cout << "析构函数" << endl;
//先析构容器里面的有效的元素
for (T* p = _first; p != _last; ++p)
{
// 把_first指针指向的数组的有效元素进行析构操作
_allocator.destroy(p);
}
//然后释放_first 指向的堆内存
_allocator.deallocate(_first);
_first = _last = _end = nullptr;
}
MyVector(const MyVector<T>& src)
{
cout << "拷贝构造" << endl;
int size = src._last - src._first;//有效元素的个数
int len = src._end - src._first;//空间总长度
_first = _allocator.allocate(len);
_last = _first + size;
for (int i=0;i<size;++i)
{
_allocator.construct(_first + i, src._first[i]);
}
_end = _first + len;
}

MyVector<T>operator=(const MyVector<T>& src)
{
cout << "= 运算符重载" << endl;
if (this == &src)
{
return *this;
}
//先析构容器里面的有效的元素
for (T* p = _first; p != _last; ++p)
{
// 把_first指针指向的数组的有效元素进行析构操作
_allocator.destroy(p);
}
//然后释放_first 指向的堆内存
_allocator.deallocate(_first);

int size = src._last - src._first;//有效元素的个数
_first = _allocator.allocate(size);
_last = _first + size;
for (int i = 0; i < size; ++i)
{
_allocator.construct(_first + i, src._first[i]);
}

int len = src._end - src._first;//空间总长度
_end = _first + len;
return *this;
}
void push_back(const T& val)// 向容器末尾添加元素
{
if (full())
{
resize();
}

//_last指针指向的内存构造一个值为val的对象
_allocator.construct(_last, val);
_last++;
}
T getBack()const// 返回容器末尾的元素的值
{
return *(_last - 1);
}
void pop_back()// 从容器末尾删除元素
{
if (empty())
{
return;
}
//是从当前末尾元素进行删除的 需要把那个 失效一下
checkIterator(_last - 1, _last);
// 不仅要把_last指针--,还需要析构删除的元素
_allocator.destroy(--_last);
}
bool full()const
{
return _last == _end;
}
bool empty()const
{
return _last == _first;
}
int getSize()const
{
return _last - _first;
}
T& operator[](int index)//myv[4] 下标运算符的重载
{
//cout << "下标运算符的重载" << endl;
if (index < 0 || index >= getSize())
throw "index 不合法";
return _first[index];
}
//迭代器一般实现成容器类的嵌套类
// 给vector类型提供迭代器的实现
//作为vector的嵌套类,为了迭代向量的底层数组
class iterator
{
public:
friend class MyVector<T, Allo>;
iterator(MyVector<T, Allo>* ptrVec=nullptr,T*_p=nullptr)
:_p(_p),_ptrVec(ptrVec)
{
Iterator_Base* newIter =
new Iterator_Base(this, _ptrVec->_head._next);
_ptrVec->_head._next = newIter;
}
// 两个对于字符串的迭代器的不等于
// 迭代器的不等于就是底层指针的不等于
// 迭代器的指向 就是 vector数组底层指针的指向
bool operator!=(const iterator& it)const
{
// 检查迭代器的有效性
if (_ptrVec == nullptr || _ptrVec != it._ptrVec)
{
throw "iterator 不匹配!";
}
return _p != it._p;
}
//前置++ 不会产生临时量,就是修改当前对象的值 再返回
//后置++ 需要把原对象的值返回 再修改当前对象的值
void operator++()
{
// 检查迭代器的有效性
if (_ptrVec == nullptr )
{
throw "iterator 无效的!";
}
++_p;//给迭代器 成员变量++
}
//常方法:普通对象 常对象都可以调用
const T& operator*()const//只读 常方法
{
// 检查迭代器的有效性
if (_ptrVec == nullptr)
{
throw "iterator 无效的!";
}
return *_p;
}
T& operator*() //可读可写
{
// 检查迭代器的有效性
if (_ptrVec == nullptr)
{
throw "iterator 无效的!";
}
return *_p;
}
private:
T* _p;
//当前迭代器需要知道 它在迭代哪种容器对象
MyVector<T, Allo>* _ptrVec;
};

//需要给容器提供begin end方法
// begin返回的是容器底层 首元素 的迭代器的表示
iterator begin()
{
// 用_first这个指向数组首元素地址的 指针
// 构造一个iterator
return iterator(this,_first);
}
// end返回的是容器末尾(有效的)元素后继位置的迭代器的表示
iterator end()
{
return iterator(this,_last);
}
void checkIterator(T* first, T* last)// 检查迭代器失效
{
Iterator_Base* preIter = &this->_head;// 指向头结点
Iterator_Base* curIter = this->_head._next;// 指向第一个节点

while (curIter != nullptr)
{//迭代的地址 在这个失效区间里面 则把这个迭代器失效一下
if (curIter->_curiterator->_p > first &&
curIter->_curiterator->_p <= last)

{
// 迭代器失效,把iterator持有的容器指针置nullptr
curIter->_curiterator->_ptrVec = nullptr;
// 删除当前迭代器节点,继续判断后面的迭代器节点是否失效
preIter->_next = curIter->_next;
delete curIter;
curIter = preIter->_next;
}
else
{
preIter = curIter;
curIter = curIter->_next;
}
}
}


//自定义实现vector容器的insert方法
iterator insert(iterator it, const T& val)
{
//1.不考虑扩容 verify(_first - 1, _last);
//2.不考虑it._ptr的指针合法性
checkIterator(it._p - 1, _last);
T* p = _last;
while (p > it._p)//元素依次后移
{
_allocator.construct(p, *(p - 1));
_allocator.destroy(p - 1);
--p;
}
_allocator.construct(p, val);
_last++;

//最后返回一个当前位置的新的迭代器
//把当前对象 this传进去
}

// 自定义实现vector容器的erase方法
iterator erase(iterator it)
{
// 1.不考虑扩容 verify(_first - 1, _last);
// 2.不考虑it._ptr的指针合法性
checkIterator(it._p - 1, _last);
T* p = it._p;
while ( p<_last-1)//元素依次前移
{
_allocator.destroy(p);
_allocator.construct(p, *(p + 1));
++p;
}
_allocator.destroy(p);
_last--;
return iterator(this, it._p);
// 最后返回一个当前位置的新的迭代器
// 把当前对象 this传进去
}
private:
T* _first;// 指向数组起始的位置
T* _last;// 指向数组中有效元素的后继位置
T* _end;// 指向数组空间的后继位置
Allo _allocator;// 定义容器的空间配置器对象

void resize()
{
cout << "resize()" << endl;
int size = _last - _first;

int newsize = size * 2;
T* newfirst = _allocator.allocate(newsize);

for (int i = 0; i < size; ++i)
{
_allocator.construct(newfirst + i, _first[i]);
}
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p);
}
_allocator.deallocate(_first);
_first = newfirst;
_last = _first + size;
_end = _first + newsize;
}

struct Iterator_Base // 容器迭代器失效增加
{
Iterator_Base(iterator* curiterator=nullptr,
Iterator_Base* next=nullptr)
{
_curiterator = curiterator;
_next = next;
}
iterator* _curiterator;
Iterator_Base* _next;
};
Iterator_Base _head;
};

int main()
{
MyVector<> myv(20);
for (int i = 0; i < 9; ++i)
{
myv.push_back(rand() % 15);
}
#if 0
//要使用iterator
MyVector<>::iterator it = myv.begin();
for (; it != myv.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
// 使用 iterator 是通用做法,提供[]只适合内存连续 有意义的
// 使用[]运算符重载
for (int i = 0; i < myv.getSize(); ++i)
{
cout << myv[i] << " ";
}
cout << endl;

//使用C++11 的foeach
//其底层原理就是通过iterator来实现容器遍历的
for (int val : myv)
{
cout << val << " ";
}
cout << endl;

while (!myv.empty())//进行打印 OK的
{
cout << myv.getBack() << " ";
myv.pop_back();
}
cout << endl;
#endif
for (int val : myv)
{
cout << val << " ";
}
cout << endl;
//在容器里面,在容器的奇数前面 都添加上一个小于1的偶数
MyVector<int>::iterator it1 = myv.begin();
while (it1 != myv.end())
{
if ((*it1) % 2 == 1)
{
it1 = myv.insert(it1, (*it1) - 1);
++it1;
}
++it1;
}
for (int val : myv)
{
cout << val << " ";
}
cout << endl;
//把容器中的奇数都删掉
MyVector<int>::iterator it2 = myv.begin();
while (it2 != myv.end())
{
if ((*it2) % 2 == 1)
it2 = myv.erase(it2);
else ++it2;//没有删除
}

for (int val : myv)
{
cout << val << " ";
}
cout << endl;

return 0;
}

new和delete

new delete两个运算符的调用,实质上也是两个运算符重载函数的调用。

1
2
new -> operator new
delete -> operator delete

new和malloc的区别:

  1. malloc是按字节开辟内存的:malloc(sizeof(int)10);它不管内存上放什么数据的。开辟完内存之后返回的是void 需要人为的类型强转。而new开辟内存时需要指定类型,并直接给出开辟元素的个数。new int[10];实质上调用operator new(),返回的类型就是自动的就是你给定的类型指针。int* 。
  2. malloc只负责开辟内存,而new不仅仅具有malloc的功能,还具有数据的初始化操作。
  3. malloc开辟内存失败 返回nullptr;new的则是抛出bad_alloc类型的异常(不可以进行与nullptr的比较),需要把new开辟内存的代码放在 try catch里面。
  4. malloc开辟单个元素和数组的内存都是一样的(给字节数就行);new的 对于单个字符不需要加上[]的,对于数组需要加[]并指定个数就可以了。

free与delete的区别:

  1. delete 需要先进行调用析构函数,再free内存。但是对于 delete (int*)p,和free(p)是一样的。因为int 类型没有什么析构函数,只剩下内存释放。

继承与多态

继承

继承的本质:

  1. 代码复用
  2. 在基类中提供统一的虚函数接口,让派生类重写,然后就可以使用多态

类和类之间的关系:

  1. 组合: a part of … …一部分的关系
  2. 继承: a kind of … …一种的关系

访问限定符权限

继承方式 基类的访问限定 派生类的访问限定 (main)外部的访问限定
public (class B : public A) public public 派生类里面可以访问 外部可以访问
protected protected 派生类里面可以访问 外部不可以访问
private 不可见 派生类里面不可以访问 外部不可以访问
protected(class B: protected A) public protected 相当于降级为 protected 外部不可以访问
protected protected 派生类里面可以访问 外部不可以访问
private 不可见 派生类里面不可以访问 外部不可以访问
protected(class B: private A) public private 相当于降级为 private 派生类里面可以访问 外部不可以访问
protected private 相当于降级为 private 派生类里面可以访问 外部不可以访问
private 不可见 派生类里面不可以访问 外部不可以访问

总结:

  1. 外部只能访问对象 public 成员, protected 和 private 的成员无法直接访问
  2. 在继承结构中, 派生类从基类可以继承过来 private 的成员, 但是派生类无法直接访问
  3. protected 和 private 的区别? 在基类中定义的成员, 想被派生类访问, 但是不想被外部访问, 那么在基类中, 把相关成员定义成 protected 保护的; 如果派生类和外部都不打算访问, 那么在基类中, 就把相关成员定义成 private 私有的。

protected 主要用于继承。

默认的继承方式是什么?

要看 派生类是用 class 定义的, 还是 struct 定义的。

  • class 定义派生类, 默认继承方式是 private 私有的。class 的成员默认是 private 权限。
  • struct 定义派生类, 默认继承方式是 public 公有的。 struct 默认是 public 权限。

派生类的构造过程

派生类从基类可以继承来所有的成员(变量和方法),除构造函数和析构函数

派生类怎么初始化从基类继承来的成员变量呢?通过调用基类相应的构造函数来初始化

派生类的构造函数和析构函数,负责初始化和清理派生类部分

派生类从基类继承来的成员由基类的构造函数和析构函数负责。

派生类对象构造和析构的过程

  1. 派生类调用基类的构造函数,初始化从基类继承来的成员
  2. 调用派生类自己的构造函数,初始化派生类自己特有的成员
  3. … 派生类对象的作用域到期了
  4. 调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆内存,文件)
  5. 调用基类的析构函数,释放派生类内存中,从基类继承来的成员可能占用的外部资源(堆内存,文件)

派生类和基类赋值问题

  • 派生类对象可以赋值给基类对象, 派生类对象中含有基类部分【多可以到少】
  • 基类对象不可以赋值给派生类对象, 因为基类中不含派生类部分【多可以到少】
  • 基类指针 ( 引用) 可以指向派生类, 但是只能访问派生类的基类部分
  • 派生类指针 ( 引用) 不可以指向基类, 因为派生类的指针会超出基类对象的范围, 不合法【类型强转】

重载、隐藏、覆盖

重载关系

一组函数要重载,必须处在同一作用域中;而且函数名字相同,参数列表不同。

隐藏关系

在继承结构中,派生类的同名成员把基类的同名成员给隐藏了,也就是调用的时候调用的是派生类的成员函数。要调用基类那就加作用域,(比如 Base::show)

把继承结构也说成从上(基类)到下(派生类)的结构

1
2
3
4
5
6
7
Base(10);
Derive(20);
b = d; // 基类对象b <- 派生类对象d 类型从下到上的转换 允许
d = b; // 派生类对象d <- 基类对象b 类型从上到下的转换 不允许
Base *pb = &d; // 基类指针(引用) <- 派生类对象 类型从下到上的转换 允许
Derive *pd = &b; // 派生类指针(引用) <- 基类对象。 类型从上到下的转换 不允许
// 在继承结构中进行上下的类型转换,默认只支持从下到上的类型转换

虚函数、静态绑定和动态绑定

静态绑定和动态绑定的概念

  • 静态绑定:静态–编译时期;绑定–函数的调用
  • 动态绑定:动态–运行时期;绑定–函数的调用

虚函数 virtual

  1. 一个类里面定义了虚函数,那么编译阶段,编译器会给这个类类型产生一个唯一的 vftable 虚函数表,虚函数表中主要存储的内容就是 RTTI (run-time type information)指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的 .rodata 区(read only data)。

    image-20240504104928494

  2. 一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个 vfptr 虚函数指针,指向相应类型的虚函数表 vftable。一个类型定义的 n 个对象,它们的 vfptr 指向的都是同一张虚函数表。

    image-20240504105018672

  3. 一个类里面虚函数的个数,不影响对象内存大小(vfptr),影响的是虚函数表的大小。

  4. 如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是 virtual 虚函数,那么派生类的这个方法,自动处理成虚函数。

  5. 在派生类中的虚函数表, 如果重写了方法, 那么虚函数表中原本基类的虚函数地址, 被派生类的虚函数地址覆盖调用过程: 基类指针指向派生类对象, 调用函数, 先去基类查看函数的类型, 如果是普通函数, 那么就是静态绑定, 直接调用父类函数。 如果发现是虚函数, 那么进行动态绑定, 首先查看对象的前四个字节 ( 虚函数表) 找到虚函数中的虚函数地址。

    ​ 重写《=》覆盖

    覆盖:虚函数表中虚函数地址的覆盖

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

    class Base
    {
    public:
    virtual void show() {}
    virtual void show(int num) {}
    int ma;
    };
    class Drive
    {
    public:
    void show() {} // virtual void show(),因为这个函数和基类同名,自动处理成虚函数
    int mb;
    };

    image-20240504110035559

看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
class Base
{
public:
virtual void show() { cout << "Base\n"; }
virtual void show(int num) {}
int ma;
};
class Drive : public Base
{
public:
void show() { cout << "Drive\n"; } // virtual void show(),因为这个函数和基类同名,自动处理成虚函数
int mb;
};
signed main()
{
Drive d;
Base *p = (Base *)&d;
p->show();
return 0;
}

p是Base类,当p调用的,p.show先去Base类中查看show是不是virtual:

  • 不是,则静态绑定,直接调用Base中的show方法。
  • 是,则运行时期从代码看,运行时期指向子类Drive类,Drive类中存在从父类继承的虚函数表,那么就从中调用。

如果这样呢?

1
p.show(int)

也是动态绑定,调用子类的虚函数表中查找。

Q:哪些函数不能实现成虚函数?

虚函数依赖:

  1. 虚函数能产生地址,存储在 vftable 当中

  2. 对象必须存在,(vfptr -> vftable -> 虚函数地址)

构造函数:

  1. 构造函数前面不能加 virtual
  2. 构造函数中(调用的任何函数都是静态绑定)调用虚函数,也不会进行动态绑定

派生类对象构造过程 先调用基类的构造函数,然后才调用派生类的构造函数

  1. static 静态成员方法。对象都没有,也就不能 static 前面加 virtual

再谈动态绑定

虚函数和动态绑定的问题:是不是虚函数的调用一定就是动态绑定? 肯定不是的!

在类的构造函数中,调用虚函数,也是静态绑定(构造函数中调用其他函数(虚),不会发生动态绑定)

用对象本身调用虚函数,属于静态绑定

动态绑定,必须由指针调用虚函数(Base *pb1 = &b;),或者必须由引用变量调用虚函数(Base &rb1 = b;)

虚函数通过指针或者引用变量调用,才发生动态绑定

多态

  • 静态(编译时期)的多态:函数重载、模板(函数模板和类模板)

  • 动态(运行时期)多态:在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数)。基类指针指向哪个派生类对象,就会调用哪个派生类对象的同名覆盖方法,称为多态。多态底层是通过动态绑定来实现的。

抽象类

抽象类:有纯虚函数的类【为什么要有抽象类?属实不知道这个类抽象成什么实体】

抽象类不能再实例化对象了,到那时可以定义指针和引用变量。

菱形继承

待填坑。

STL库

对象优化

对象使用过程中调用了哪些方法

**case1:**C++编译器对于对象构造的优化。用临时对象生成新对象时,临时对象就不产生了。直接构造新对象即可。

1
Test t4=Test(20) // Test t4(20);没有区别 《-拷贝构造

**case2:**3个效果引用,都是赋值运算。

1
2
3
1.t4=Test(20)  // 
2.t4=(Test)20 // 显式 【显式生成临时对象生命周期:所在语句】
3.t4=30 // 隐式

case3:

1
Test *p=&Test(40) // p指向的是一个已经析构的临时对象【之后就会有野指针了】
1
const Test &p = Test(40) //ok

解释:

Test *p = &Test(40)这行代码有问题,是因为Test(40)创建了一个临时对象,这个对象在该行代码执行完后就会被销毁,所以p指向的内存区域是未定义的,这样是非法的。

而const Test &p = Test(40)则是合法的,这是因为const引用延长了临时对象的生命周期。在这种情况下,临时对象会一直存在,直到对应的const引用p不再使用。所以,p引用的对象在其生命周期内始终是有效的。

总的来说,这两种情况的区别在于const引用对临时对象的生命周期进行了延长,而普通指针则没有这个特性。

image-20240504112620253

总结:3条对象优化规则

  • 函数参数传递过程中,对象优先按引用传递。【防止对象切片】

  • 函数返回的时候,应优先返回一个临时对象,而不要返回一个定义的对象。

    image-20240504112419307

  • 接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按照赋值方式接受

    image-20240504112507471

【精华】用临时对象生成新对象,临时对象就不产生了。

智能指针

为什么要有智能指针这个东西?裸指针不好吗?

裸指针到底有什么不好,比如下面的原因:

  1. 有写free或者delete,忘记释放资源,导致资源泄露(发生了内存泄漏)
  2. 同一资源释放多次,导致释放野指针,程序崩溃
  3. 明明代码的后面写了释放资源的代码,但是由于程序逻辑满足条件,从中间return掉了,导致释放资源的代码未被执行到。
  4. 代码运行过程中一旦发生异常,随着异常栈展开,导致释放资源的代码未被执行到。

那有没有什么办法自动负责管理这些资源。答案就是智能指针。

智能指针的特点:主要体现在用户可以不关注资源的释放,因为智能指针会自动完全管理资源的释放,它会保证无论程序逻辑如何进行,正常执行还是发生异常,资源在到期的情况下,一定会进行资源的自动释放。智能指针就是对普通指针(裸指针)的一层封装(面向对象的),在构造函数中初始化资源地址,在析构函数中负责释放资源。利用栈上的对象出作用域会自动析构这么一个特点,把资源释放的代码全部放在这个析构函数中执行。

示例:

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
#include <iostream>
#include <algorithm>
#include <functional>
#include <memory>
#include <queue>

using namespace std;
template <typename T>
class MySmartptr
{

public:
MySmartptr(T *ptr = nullptr) : ptr_(ptr) {}
~MySmartptr()
{
delete ptr_;
ptr_ = nullptr;
}
T *operator->()
{
return ptr_;
}
T &operator*()
{
return *ptr_;
}

private:
T *ptr_;
};
class Test
{
public:
void show()
{
cout << "show" << endl;
}
};
int main()
{
MySmartptr<Test> p1(new Test);
p1->show();
(*p1).show();
return 0;
}

有个问题:智能指针可不可以定义到堆上?

1
shared_ptr<int> *p = new shared_ptr<int>(new int);

但是最后不是得手动·delete,那和裸指针没有区别了。

不带引用计数的智能指针

如果在最开始的示例中,改成如下语句,会发送什么事情?

1
2
3
MySmartptr<Test> p1(new Test);
p1->show();
MySmartptr<Test> p2(p1);

pro:在智能指针对象析构的时候,同一份资源释放了两次。第二次 是在释放野指针。也就是,我们常说的,默认拷贝构造函数:发生浅拷贝

1
2
3
4
5
6
7
8
9
class MySmartptr
{
public:
MySmartptr(T *ptr = nullptr) : ptr_(ptr) {}
MySmartptr(const MySmartptr<T> &obj)
{
ptr_ = new T(obj.ptr_);
}
};

因此,需要加上拷贝构造函数。

那智能指针这块是怎么解决这个浅拷贝问题呢?

不带引用计数的智能指针

  1. auto_ptr
  2. scoped_ptr
  3. unique_ptr

auto_ptr

1
2
3
4
auto_ptr<int> p1(new int);
auto_ptr<int> p2(p1);
*p2=20;
cout<<*p1<<endl;

程序运行崩溃!

查看源码就可以知道了。

1
2
template<typename _Tp1>
auto_ptr(auto_ptr<_Tp1>& __a) throw() : _M_ptr(__a.release()) { }

release方法:把ptr1的 _ mptr值给ptr2的_ mptr,意思就是ptr2的_ mptr指向了原来ptr1的堆内存。然后把ptr1的_mptr置为nullptr,意思是ptr1放弃了堆内存的指向。

总结:auto_ptr解决浅拷贝的方法如下:
首先其成员变量只有一个裸指针,没有引用计数。永远让最后一个智能指针对象去 管理资源。其余前面的智能指针对象的_mptr置为nullptr,意思是他们放弃了堆内存的指向。

scoped_ptr

scoped_ptr:不能拷贝构造和赋值

1
2
scoped_ptr(scoped_ptr const &)=delete;
scoped_ptr & operator=(scoped_ptr const &)=delete;

unique_ptr

底层提供了移动拷贝和移动赋值。

1
2
unique_ptr(unique_ptr const &)=delete;
unique_ptr & operator=(unique_ptr const &)=delete;
1
2
3
4
5
6
7
8
9
10
11
int main()
{

unique_ptr<int> p1(new int);
unique_ptr<int> p2(move(p1));
if (p1 == nullptr)
{
cout << "null";
}
return 0;
}

带引用计数的智能指针

引用计数的智能指针:多个智能指针可以管理同一份资源。

什么是带引用计数的智能指针?当允许多个智能指针对象指向同一个资源的时候,每一个智能指针对象都会给资源的引用计数加1,当一个智能指针对象析构时,同样会使资源的引用计数减1,这样最后一个智能指针把资源的引用计数从1减到0时,就说明该资源可以释放了,由最后一个智能指针对象的析构函数来处理资源的释放问题,这就是很好的解决了 智能指针的浅拷贝问题。

shared_ptr和weak_ptr都是线程安全。

来看个例子:

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<bits/stdc++.h>

using namespace std;
//对资源进行引用计数的类
template<typename T>
class ResouseCount
{
public:
ResouseCount(T* mReptr = nullptr) :_mReptr(mReptr)
{
cout << "ResouseCount()" << endl;
if (_mReptr != nullptr)
{
_mcount = 1;
}
else _mcount = 0;
}
#if 0
~ResouseCount()
{
cout << "~ResouseCount()" << endl;
delete _mReptr;
_mReptr = nullptr;
}
#endif
void addResouseCont()//引用计数+1,增加资源的引用计数
{
_mcount++;
}
int delResouseCont()//引用计数-1,减少资源的引用计数
{
_mcount--;
return _mcount;
}
private:
T* _mReptr;//记录资源的地址
int _mcount;//资源的引用计数
};
//智能指针 保证可以做到资源的自动释放
//利用栈上对象出作用域自动析构的特点,保证可以做到资源的自动释放
template<typename T>
class MySmartptr
{
public:
// 在生成这个智能指针对象的时候,生成一个资源的引用计数对象
MySmartptr(T* mptr = nullptr) :_mptr(mptr)
{
cout << "MySmartptr()" << endl;
myRes = new ResouseCount<T>(_mptr);
//把_mptr传进去,即给这个资源建立了一个引用计数对象
}
~MySmartptr()
{
cout << "~MySmartptr()" << endl;
if (myRes->delResouseCont() == 0)
{
delete _mptr;
_mptr = nullptr;
}
}
//看似是在对 智能指针对象进行解引用,但是实际上是对底层的成员变量进行解引用
//返回值是引用,因为我们要改变指针指向的内存本身的值
T& operator *()
{
return *_mptr;
}
/*
给指向符 提供运算符重载函数 其实是:
(ptr2.operator->())->show();
即:智能指针对象调用指向符运算符重载函数,然后返回的结
果(返回智能指针对象底层管理的指针)再去调用后面的方法。
*/
T* operator->()//指向 运算符重载函数
{
return _mptr;
}

//拷贝构造函数,当前的智能指针对象也要指向这个资源
MySmartptr(const MySmartptr<T>& src)
:_mptr(src._mptr), myRes(src.myRes)
{
cout << "MySmartptr(const &)" << endl;
//引用了一个有效的资源
//相当于 两个智能指针对象都指向了这块资源,需要增加资源的引用计数
if (_mptr != nullptr)
{
myRes->addResouseCont();
}
}
MySmartptr<T>& operator=(const MySmartptr<T>& src)
{
cout << "MySmartptr<T>& operator=(const MySmartptr<T>& src)" << endl;
if (this == &src)
return *this;
if (myRes->delResouseCont() == 0)
{
delete _mptr;
}
_mptr = src._mptr;
myRes = src.myRes;//当前智能指针也指向了这块资源

myRes->addResouseCont();//当前资源的引用计数+1
return *this;
}
/*
拷贝构造函数 和 赋值运算符重载函数,并没有去开辟新的资源
而是给这个资源添加了引用计数。有效的避免了 多个智能指针引用同一块资源
然后对同一个资源进行多次的释放。
*/
private:
T* _mptr;//指向资源的指针
ResouseCount<T> * myRes;//指向该资源引用计数对象的指针
};
int main()
{
MySmartptr<int> ptr1(new int);
MySmartptr<int> ptr2(ptr1);
MySmartptr<int> ptr3;
ptr3 = ptr2;

*ptr1 = 20;
cout << "*ptr2:" << *ptr2 << " *ptr3:" << *ptr3 << endl;//20 20
return 0;
}

myRes->delResouseCont()语句解释:

在这个函数中,myRes->delResouseCont()的作用是减少当前智能指针所指向资源的引用计数。这是因为在赋值操作中,我们假设当前的智能指针将不再指向原来的资源,因此需要减少原来资源的引用计数。接着,我们检查引用计数是否为0,如果是0,那么说明没有任何智能指针再指向这个资源了,因此我们可以安全地删除这个资源,这就是if (myRes->delResouseCont() == 0) { delete _mptr; }的作用。

shared_ptr 的交叉引用问题

  • shared_ptr:强智能指针,可以改变资源的引用计数
  • weak_ptr:弱智能指针,不会改变资源的引用计数

weak_ptr来观察shared_ptr,而shared_ptr来观察资源。

看个资源泄露的问题

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>
#include <algorithm>
#include <functional>
#include <memory>
#include <queue>

using namespace std;

class B;
class A
{
public:
A() { cout << "A()" << endl; } // 构造函数是给成员变量做初始化的
~A() { cout << "~A()" << endl; }
// 析构函数是在对象内存释放之前,把对象占有的外部资源进行释放
shared_ptr<B> ptr_b; // 指向B类型对象的智能指针
};
class B
{
public:
B() { cout << "B()" << endl; } // 构造函数是给成员变量做初始化的
~B() { cout << "~B()" << endl; }
// 析构函数是在对象内存释放之前,把对象占有的外部资源进行释放
shared_ptr<A> ptr_a; // 指向A类型对象的智能指针
};

int main()
{
shared_ptr<A> pa(new A()); // A 类型实例化的智能指针pa 来管理A对象
shared_ptr<B> pb(new B()); // B 类型实例化的智能指针pb 来管理B对象

pa->ptr_b = pb;
pb->ptr_a = pa;
cout << pa.use_count() << endl; // 打印它所引用资源的引用计数
cout << pb.use_count() << endl;
return 0;
}

例子

1
2
3
4
A()
B()
2
2

new出来的对象,没有释放,资源泄漏了。

这个问题又该怎么解决呢?

方法:定义对象的时候,用强智能指针;引用对象的地方,使用弱智能指针

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>
#include <algorithm>
#include <functional>
#include <memory>
#include <queue>

using namespace std;

class B;
class A
{
public:
A() { cout << "A()" << endl; } // 构造函数是给成员变量做初始化的
~A() { cout << "~A()" << endl; }
// 析构函数是在对象内存释放之前,把对象占有的外部资源进行释放
weak_ptr<B> ptr_b; // 指向B类型对象的智能指针
};
class B
{
public:
B() { cout << "B()" << endl; } // 构造函数是给成员变量做初始化的
~B() { cout << "~B()" << endl; }
// 析构函数是在对象内存释放之前,把对象占有的外部资源进行释放
weak_ptr<A> ptr_a; // 指向A类型对象的智能指针
};

int main()
{
shared_ptr<A> pa(new A()); // A 类型实例化的智能指针pa 来管理A对象
shared_ptr<B> pb(new B()); // B 类型实例化的智能指针pb 来管理B对象

pa->ptr_b = pb;
pb->ptr_a = pa;
cout << pa.use_count() << endl; // 打印它所引用资源的引用计数
cout << pb.use_count() << endl;
return 0;
}
1
2
3
4
5
6
7
 penge@penge-virtual-machine  ~/Desktop/MordenCpp  ./main                       
A()
B()
1
1
~B()
~A()

Note:弱智能指针只会观察资源,不可以去使用和访问资源。弱智能指针 这个类就没有提供 * 运算符重载 和 -> 运算符重载 函数。因此需要将弱智能指针升级成强智能指针。

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
class B;
class A
{
public:
A() { cout << "A()" << endl; }//构造函数是给成员变量做初始化的
~A() { cout << "~A()" << endl; }
//析构函数是在对象内存释放之前,把对象占有的外部资源进行释放
void fun_A() { cout << "类A的成员方法" << endl; }
//shared_ptr<B>ptr_b;//指向B类型对象的智能指针
weak_ptr<B>ptr_b;//指向B类型对象的智能指针
};
class B
{
public:
B() { cout << "B()" << endl; }//构造函数是给成员变量做初始化的
~B() { cout << "~B()" << endl; }
//析构函数是在对象内存释放之前,把对象占有的外部资源进行释放
//shared_ptr<A>ptr_a;//指向A类型对象的智能指针
weak_ptr<A>ptr_a;//指向A类型对象的智能指针

void fun()
{
//ptr_a->fun_A();//不可以进行访问
shared_ptr<A> strongPtr = ptr_a.lock();
//把ptr_a提升成一个强智能指针
if (strongPtr != nullptr)//提升成功
strongPtr->fun_A();//这样就可以访问了
}
/*
提升成功 strongPtr就是一个强智能指针了
*/
};

int main()
{
shared_ptr<A>pa(new A());//A 类型实例化的智能指针pa 来管理A对象
shared_ptr<B>pb(new B());//B 类型实例化的智能指针pb 来管理B对象

pa->ptr_b = pb;
pb->ptr_a = pa;

pb->fun();
cout << pa.use_count() << endl;//打印它所引用资源的引用计数
cout << pb.use_count() << endl;
return 0;
}

多线程访问共享对象的线程安全问题

例子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
#include <iostream>
#include <algorithm>
#include <functional>
#include <memory>
#include <queue>
#include <thread>
using namespace std;

class B;
class A
{
public:
A() { cout << "A\n"; }
~A() { cout << "~A\n"; }
void fun_A()
{
cout << "类A的方法\n";
}
weak_ptr<B> ptr_b;
};
void handler(weak_ptr<A> q)
{
this_thread::sleep_for(chrono::seconds(2));
shared_ptr<A> check = q.lock();
if (check)
{
check->fun_A();
}
else
{
cout << "object is delete\n";
}
}
signed main()
{
{
shared_ptr<A> p(new A());
thread t1(handler, weak_ptr<A>(p));
t1.detach();
}
this_thread::sleep_for(chrono::seconds(20));
return 0;
}
1
2
3
4
penge@penge-virtual-machine  ~/Desktop/MordenCpp  ./main                       
A
~A
object is 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
#include <iostream>
#include <algorithm>
#include <functional>
#include <memory>
#include <queue>
#include <thread>
using namespace std;

class B;
class A
{
public:
A() { cout << "A\n"; }
~A() { cout << "~A\n"; }
void fun_A()
{
cout << "类A的方法\n";
}
weak_ptr<B> ptr_b;
};
void handler(weak_ptr<A> q)
{
this_thread::sleep_for(chrono::seconds(2));
shared_ptr<A> check = q.lock();
if (check)
{
check->fun_A();
}
else
{
cout << "object is delete\n";
}
}
signed main()
{
{
shared_ptr<A> p(new A());
thread t1(handler, weak_ptr<A>(p));
t1.detach();
this_thread::sleep_for(chrono::seconds(5));
}
this_thread::sleep_for(chrono::seconds(3));
return 0;
}

运行结果

1
2
3
4
penge@penge-virtual-machine  ~/Desktop/MordenCpp  ./main                       
A
类A的方法
~A

总结:解决多线程访问共享对象的线程安全问题,强弱智能指针shared_ptr和weak_ptr 可以在线程里面 通过资源计数来检测对象还活着吗

自定义删除器

C++11智能指针std::shared_ptr和std::unique_ptr都支持自定义删除器,本文将介绍自定义删除器的使用场景和使用方法。智能指针模板参数的第二个类型是删除器,一般是一个函数指针类型或者是一个函数对象类型。通常情况下,删除器的类型是std::default_delete< T >,它是一个函数对象类型,用于调用delete来释放所管理的对象。

1
2
3
4
5
template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr;

template <typename T, typename Deleter = std::default_delete<T>>
class shared_ptr;

使用场景

自定义删除器的作用是在智能指针释放所管理的对象时,执行一些特殊的操作,比如:

  • 内存释放时打印一些日志。
  • 管理除内存以外的其它资源,例如文件句柄、数据库连接等。
  • 与自定义分配器(Allocator)配合使用,将资源释放给自定义分配器。
  • 在C++17之前,std::shared_ptr用于管理数组时需要自定义删除器来释放数组内存,因为默认使用delete来释放所管理的对象,而delete不能正确释放分配的数组,需要在自定义删除器delete[]释放数组。

这部分可以参考Effective书籍。

绑定器和function函数对象

绑定器

函数对象: 拥有operator()运算符重载函数的对象,且这个对象的使用 类似于函数调用。

STL中提供了的绑定器函数bind1st和bind2nd,它们将一个二元函数对象转化为一个一元函数对象。

  • bind1st()是绑定第一个参数。
  • bind2nd()是绑定第二个参数。

看个示例:

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

using namespace std;

int main()
{
int numbers[] = { 10,20,30,40,50,10 };
int cx;
cx = count_if(numbers, numbers + 6, bind2nd(less<int>(), 40));
cout << "There are " << cx << " elements that are less than 40.\n";

cx = count_if(numbers, numbers + 6, bind1st(less<int>(), 40));
cout << "There are " << cx << " elements that are not less than 40.\n";

system("pause");
return 0;
}

输出结果

1
2
3
There are 4 elements that are less than 40.
There are 1 elements that are not less than 40.
请按任意键继续. . .

解释:

  • bind1st将参数绑定到二元函数对象的第一个参数,例如bind1st(less< int >(), 40)将创建一个新的一元函数对象,这个函数对象将检查给定的参数是否大于40。
  • bind2nd将参数绑定到二元函数对象的第二个参数,例如bind2nd(less< int >(), 40)将创建一个新的一元函数对象,这个函数对象将检查给定的参数是否小于40。

bind2nd(less< int >(), 40)用于计算数组中小于40的元素的数量,而bind1st(less< int >(), 40)用于计算数组中不小于40的元素的数量。因此,bind1st和bind2nd的区别在于它们将参数绑定到二元函数对象的不同位置。

通过上面的示例我们对于绑定器有了基本的了解。先记个结论:绑定器+二元函数对象 = 一元函数对象

再来看个示例

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
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>

using namespace std;
template <typename T>
void show(T v)
{
// C++11
// for (auto it : v)
// {
// cout << it << " ";
// }
// cout << endl;
typename T::iterator it = v.begin();
// typename T::iterator it = v.begin();
for (; it != v.end(); it++)
{
cout << *it << " ";
}
cout << endl;
}
signed main()
{
vector<int> v;
for (int i = 0; i < 20; i++)
{
v.push_back(rand() % 40 + 1);
}
show<vector<int> >(v);
// greater<int>()函数对象 二元函数对象:需要两个元素一一进行比较
// 一次从容器里面拿出来2个元素
// 其operator() 有两个参数 比较的是 >
// 而库里面 与排序有关的默认 从小到大 < less
sort(v.begin(), v.end(), greater<int>());
show<vector<int>>(v);
// 需求1:把14插入容器其中
// 30 < x
vector<int>::iterator it = find_if(v.begin(), v.end(), bind1st(less<int>(), 30));
if (it != v.end())
{
v.insert(it, 30);
}
show<vector<int>>(v);
// 需求2:把14插入容器其中
// x < 10
it = find_if(v.begin(), v.end(), bind2nd(less<int>(), 10));
if (it != v.end())
{
v.insert(it, 10);
}
show<vector<int>>(v);
return 0;
}

接下来,我们学习下这个bind()以及find_if底层的实现。

find_if

find_if的底层实现:第三个参数需要的是一个 一元函数对象。

思路:遍历 iterator区间的元素,如果满足函数对象的运算 则返回当前iterator 。否则返回end 当然这里需要一个一元的函数对象 其operator()只需要接收一个参数即可 。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename Iterator, typename Compare>
//Compare:函数对象
Iterator myfind_if(Iterator st, Iterator ed, Compare compare)
{
for (; st != ed; st++)
{
if (compare(*st))
{
return st;
}
}
return ed;
}

bind1st 和 bind2nd

先来了解下这个函数对象是怎么调用的。

1
2
3
4
5
6
7
8
9
#include <bits/stdc++.h>
using namespace std;
int main()
{
auto a = greater<int>();
cout<<a(1,3);
// greater<int>()(1,3)
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
//函数模板 bind1st的实现
/*
第一个参数 是一个二元函数对象
第二个参数 是一个 元素的类型 val(要绑定的值)
*/
template <typename Compare, typename T>
class mybind1stClass
{
public:
mybind1stClass(Compare compare, T val) : compare_(compare), val_(val) {}
// 既然是函数对象 需要有operator()的实现
bool operator()(const T &val)
{
// 底层还是二元函数对象的实现
return compare_(val_, val); // 第1个值被绑定了 所以传入第2个值
}
private:
Compare compare_;
T val_;
};
template <typename Compare, typename T>
auto mybind1st(Compare compare, T val)
{
// //构造一个 一元函数对象出来
return mybind1stClass<Compare, T>(compare, val);
}

bind1st 是一个函数模板,里面封装了一个 一元函数对象的产生。 bind1st 就是函数对象的应用。绑定器+二元函数对象 = 一元函数对象。

bind2nd也很容易了。

Note:在C++模板编程中,typename关键字的使用是为了帮助编译器区分类型名称和其他名称。具体来说,当我们在模板中使用依赖于模板参数的类型时,编译器需要明确地知道我们是在引用一个类型。这里的typename关键字就是用来告诉编译器,T::iterator是一个类型。

比如:编译器在解析模板代码时,不知道T::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
100
101
102
103
104
105
106
107
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>

using namespace std;
template <typename T>
void show(T v)
{
// C++11
// for (auto it : v)
// {
// cout << it << " ";
// }
// cout << endl;
typename T::iterator it = v.begin();
// typename T::iterator it = v.begin();
for (; it != v.end(); it++)
{
cout << *it << " ";
}
cout << endl;
}
template <typename Iterator, typename Compare>
Iterator myfind_if(Iterator st, Iterator ed, Compare compare)
{
for (; st != ed; st++)
{
if (compare(*st))
{
return st;
}
}
return ed;
}

template <typename Compare, typename T>
class mybind1stClass
{
public:
mybind1stClass(Compare compare, T val) : compare_(compare), val_(val) {}
bool operator()(const T &val)
{
return compare_(val_, val);
}

private:
Compare compare_;
T val_;
};
template <typename Compare, typename T>
auto mybind1st(Compare compare, T val)
{
return mybind1stClass<Compare, T>(compare, val);
}
template <typename Compare, typename T>
class mybind2ndClass
{
public:
mybind2ndClass(Compare compare, T val) : compare_(compare), val_(val) {}
bool operator()(const T &val)
{
return compare_(val, val_);
}

private:
Compare compare_;
T val_;
};
template <typename Compare, typename T>
auto mybind2nd(Compare compare, T val)
{
return mybind2ndClass<Compare, T>(compare, val);
}

signed main()
{
vector<int> v;
for (int i = 0; i < 20; i++)
{
v.push_back(rand() % 40 + 1);
}
show<vector<int>>(v);
// greater<int>()函数对象 二元函数对象:需要两个元素一一进行比较
// 一次从容器里面拿出来2个元素
// 其operator() 有两个参数 比较的是 >
// 而库里面 与排序有关的默认 从小到大 < less
sort(v.begin(), v.end(), greater<int>());
show<vector<int>>(v);
// 需求1:把14插入容器其中
// 30 < x
vector<int>::iterator it = myfind_if(v.begin(), v.end(), mybind1st(less<int>(), 30));
if (it != v.end())
{
v.insert(it, 30);
}
show<vector<int>>(v);
// 需求2:把14插入容器其中
// x < 10
it = myfind_if(v.begin(), v.end(), mybind2nd(less<int>(), 10));
if (it != v.end())
{
v.insert(it, 10);
}
show<vector<int>>(v);
return 0;
}

结果是比较随机的。

1
2
3
4
5
6
 penge@penge-virtual-machine  ~/Desktop/MordenCpp  g++ main.cpp -o main -pthread
penge@penge-virtual-machine  ~/Desktop/MordenCpp  ./main
24 7 18 36 34 16 27 13 10 22 3 28 11 20 4 7 21 27 13 17
36 34 28 27 27 24 22 21 20 18 17 16 13 13 11 10 7 7 4 3
30 36 34 28 27 27 24 22 21 20 18 17 16 13 13 11 10 7 7 4 3
30 36 34 28 27 27 24 22 21 20 18 17 16 13 13 11 10 10 7 7 4 3

现在,bind1st() 和 bind2nd(),在 C++11 里已经 deprecated 了.bind()可以替代他们,且用法更灵活更方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <algorithm>
#include <functional>
using namespace std;
int main(){
int numbers[] = { 10,20,30,40,50,10 };
int cx;
cx = count_if(numbers, numbers + 6, bind(less<int>(), std::placeholders::_1, 40));
cout << "There are " << cx << " elements that are less than 40.\n";

cx = count_if(numbers, numbers + 6, bind(less<int>(), 40, std::placeholders::_1));
cout << "There are " << cx << " elements that are not less than 40.\n";

system("pause");
return 0;
}

function函数对象

C++中的std::function是一个强大而灵活的工具,它允许我们将可调用对象(函数、函数指针、Lambda表达式等)包装成一个对象,使得我们可以像操作其他对象一样操作和传递可调用对象。

基本概念

std::function是C++11引入的标准库组件,位于< functional >头文件中。它的主要作用是将可调用对象封装为一个函数对象,提供一种统一的方式来处理各种类型的可调用对象。

1
std::function<返回类型(参数类型1, 参数类型2, ...)> func;

使用示例

1.封装函数指针

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <functional>

void greet() {
std::cout << "Hello, World!" << std::endl;
}

int main() {
std::function<void()> func = greet;
func(); // 调用封装的函数指针
return 0;
}

2.封装Lambda表达式

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <functional>

int main() {
std::function<void()> func = []() {
std::cout << "Lambda says hi!" << std::endl;
};
func(); // 调用封装的Lambda表达式
return 0;
}

3.封装可调用对象

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

class Greeter {
public:
void operator()() const {
std::cout << "Class Greeter says hello!" << std::endl;
}
};

int main() {
std::function<void()> func = Greeter();
func(); // 调用封装的可调用对象
return 0;
}

4.与bind结合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <functional>
#include <bits/stdc++.h>

using namespace std;
void func(string a,int b){
cout<<a<<" "<<b<<endl;
return ;
}
signed main(){
auto printHello = std::bind(func,"Hello",std::placeholders::_1);
function<void(int)> tmp=printHello;
tmp(666);
return 0;
}

OK,介绍完应用,我们来看看具体的原理

如果我们想使用function封装下面的函数,如果实现呢?

1
void(string str)

使用模板和函数对象的知识实现代码

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
#include <iostream>
#include <vector>
#include <functional> //C++库的函数对象
#include <algorithm> //C++库的泛型算法

using namespace std;

template <typename T> //这两行相当于 提供模板
class MyFunction
{
};
template <typename R, typename A>// 返回值、形参类型 属于部分特例化
class MyFunction<R(A)> // //因为这是一个函数类型(1返回值 1参数)
{
public:
using pFun = R (*)(A);// 定义这个函数指针类型
MyFunction(pFun pFun) { pFun_ = pFun; }
/* operator()重载函数
封装的这个函数需要什么参数,
operator()就需要什么函数
需要接收1个参数 类型为A
*/
R operator()(A val)
{
return pFun_(val);
}

private:
pFun pFun_;
};
void show(string a)
{
cout << a << endl;
return;
}
signed main()
{
MyFunction<void(string)> f(show);
// MyFunction<void(string)> f=show;
f("hello");
return 0;
}

如果传入的参数不确定呢?

1
2
3
4
void(string str)
void(string str,int a)
void(string str,int a,int b)
...

由C++11里面的 可变参(形参个数不定)的类型参数 完美解决

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
#include <iostream>
#include <vector>
#include <functional> //C++库的函数对象
#include <algorithm> //C++库的泛型算法

using namespace std;

template <typename T>
class MyFunction
{
};
template <typename R, typename... A>
class MyFunction<R(A...)>
{
public:
using pFun = R (*)(A...);
MyFunction(pFun pFun) { pFun_ = pFun; }
R operator()(A... val)
{
return pFun_(val...);
}

private:
pFun pFun_;
};
void show(string a, int b)
{
cout << a << " " << b << endl;
return;
}
signed main()
{
MyFunction<void(string, int)> f(show);
f("hello", 111);
return 0;
}

lambda表达式

lambda :函数对象的升级版,原理就是更高级的函数对象的实现。

函数对象的缺点:虽然在使用中不会单独使用函数对象,而且也都是使用在 泛型算法的参数传递 或者 带有比较 或者优先级队列等自定义类型的元素比较方式 或者 智能指针的删除器 之中(就可以传递一个函数对象进去)。但是 但是 但是 函数对象 是需要自己先去定义一个函数对象类型 出来啊!!!! 而且这个类型定义出来以后,这个类型定义的对象只是使用在 (例如:优先级队列的定义处),之后这个函数对象类型 可能再也不用了。

老样子,看个例子先

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 <algorithm>
#include <functional>

using namespace std;
int main()
{
auto myfun1 = []()->void
{
cout << "Lambda: I Love Libaibai" << endl;
};
myfun1();

auto myfun2 = [](int a, int b)->int
{
return a + b;
};
cout << myfun2(20, 30) << endl;

int a = 10, b = 20;
auto myfun3 = [&a,&b]()
{
int temp = a;
a = b;
b = temp;
};
myfun3();
cout << a << " " << b << endl;
return 0;
}

现代C++11对此有比较深入的介绍。

再来看个常见的例子,也就是优先队列对自定义对象排序。

优先队列中使用lambda表达式。

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 <algorithm>
#include <functional>
#include <memory>
#include <queue>
using namespace std;
class Test
{
public:
Test(int ma = 10, int mb = 10) : _ma(ma), _mb(mb) {}
int _ma;
int _mb;
};
int main()
{
priority_queue<Test, vector<Test>, function<bool(Test &, Test &)>>
myqueue([](Test &t1, Test &t2) -> bool
{ return t1._ma > t2._ma; });
myqueue.push(Test(10, 20));
myqueue.push(Test(15, 15));
myqueue.push(Test(20, 10));
auto cur = myqueue.top();
myqueue.pop();
cout << cur._ma;
/*
auto func = [](Test &t1, Test &t2) -> bool
{ return t1._ma > t2._ma; };
priority_queue<Test, vector<Test>, function<bool(Test &, Test &)>> myqueue(func);
*/
return 0;
}

补充: