0%

多态

多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
实现方法:多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。

实现过程

在类中用 virtual 关键字声明的函数叫做虚函数;
存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应);
当基类指针指向派生类对象,基类指针调用虚函数时,基类指针指向派生类的虚表指针,由于该虚表指针指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数。

举例

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

class Base
{
public:
virtual void fun() { cout << "Base::fun()" << endl; }

virtual void fun1() { cout << "Base::fun1()" << endl; }

virtual void fun2() { cout << "Base::fun2()" << endl; }
};
class Derive : public Base
{
public:
void fun() { cout << "Derive::fun()" << endl; }

virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }

virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
};
int main()
{
Base *p = new Derive();
p->fun(); // Derive::fun() 调用派生类中的虚函数
return 0;
}

基类的虚函数表如下:

20220215171346

派生类的对象虚函数表如下:

20220215171352

简单解释:当基类的指针指向派生类的对象时,通过派生类的对象的虚表指针找到虚函数表(派生类的对象虚函数表),进而找到相应的虚函数 Derive::f() 进行调用。

03/24

LCP 04. 覆盖

03/18

328.奇偶链表

43.字符串相乘

78.子集

128.最长连续序列

150.逆波兰表达式求值

03/17

21.合并两个有序链表

24.两两交换链表中的节点

82.删除排序链表中的重复元素 II

143.重排链表

148.排序链表

147.对链表进行插入排序

03/14

【2021】阿里巴巴编程题 1.子集

【2021】阿里巴巴编程题 2.小强爱数学

【2021】阿里巴巴编程题 3.二叉树

【2021】阿里巴巴编程题 4.对称飞行器

【2021】阿里巴巴编程题 6.树上最短链

【2021】阿里巴巴编程题 7.牛牛们吃糖果

【2021】阿里巴巴编程题 8.方案数量

【2021】阿里巴巴编程题 9.合法连续子段

03/12

424.替换后的最长重复字符

03/11

25.K 个一组翻转链表

03/10

剑指 Offer 25. 合并两个排序的链表

NC142 最长重复子串

NC73 数组中出现次数超过一半的数字

208.实现 Trie (前缀树)

03/09

120.三角形最小路径和

329.矩阵中的最长递增路径

343.整数拆分

03/08

18.四数之和

03/06

131.分割回文串

132.分割回文串 II

233.数字 1 的个数

264.丑数 II

313.超级丑数

03/04

剑指 Offer 24. 反转链表

160.相交链表

03/03

322.零钱兑换

337.打家劫舍 III

416.分割等和子集

338.比特位计数

72.编辑距离

213.打家劫舍 II

03/02

152.乘积最大子数组

221.最大正方形

279.完全平方数

300.最长递增子序列

121.买卖股票的最佳时机

122.买卖股票的最佳时机 II

123.买卖股票的最佳时机 III

188.买卖股票的最佳时机 IV

309.最佳买卖股票时机含冷冻期

714.买卖股票的最佳时机含手续费

03/01

1091.二进制矩阵中的最短路径

1129.颜色交替的最短路径

102.二叉树的层序遍历

101.对称二叉树

752.打开转盘锁

02/28

105.从前序与中序遍历序列构造二叉树

112.路径总和

98.验证二叉搜索树

494.目标和

547.省份数量

1254.统计封闭岛屿的数目

02/27

47.全排列 II

406.根据身高重建队列

899.有序队列

946.验证栈序列

116.填充每个节点的下一个右侧节点指针

895.最大频率栈 *

61.旋转链表

729.我的日程安排表 I

25.K 个一组翻转链表

554.砖墙

112.路径总和

02/26

剑指 Offer II 051. 节点之和最大的路径

409.最长回文串

5.最长回文子串

214.最短回文串*

02/24

3.无重复字符的最长子串

剑指 Offer II 016. 不含重复字符的最长子字符串

49.字母异位词分组

30.串联所有单词的子串

86.分隔链表

16.最接近的三数之和

732.我的日程安排表 III

731.我的日程安排表 II

729.我的日程安排表 I

02/23

剑指 Offer 14- I. 剪绳子

115.不同的子序列

139.单词拆分

140.单词拆分 II

02/22

面试题 16.24. 数对和

面试题 17.05. 字母与数字

面试题 17.11. 单词距离

面试题 17.16. 按摩师

02/21

面试题 04.01. 节点间通路

面试题 04.05. 合法二叉搜索树

面试题 04.06. 后继者

面试题 04.08. 首个共同祖先

面试题 08.02. 迷路的机器人

02/20

面试题 17.23. 最大黑方阵

面试题 17.24. 最大子矩阵

面试题 03.05. 栈排序

面试题 10.10. 数字流的秩

02/19

面试题 17.19. 消失的两个数字

面试题 17.20. 连续中值

02/18

面试题 17.09. 第 k 个数

面试题 17.18. 最短超串

面试题 17.21. 直方图的水量

02/17

面试题 03.03. 堆盘子

面试题 08.11. 硬币

面试题 10.11. 峰与谷

面试题 17.14. 最小K个数

面试题 17.08. 马戏团人塔

02/16

剑指 Offer 34. 二叉树中和为某一值的路径

剑指 Offer 33. 二叉搜索树的后序遍历序列

面试题32 - I. 从上到下打印二叉树

剑指 Offer 44. 数字序列中某一位的数字

84.柱状图中最大的矩形

02/15

剑指Offer26.树的子结构

剑指Offer16.数值的整数次方

剑指Offer35.复杂链表的复制

剑指Offer36.二叉搜索树与双向链表

剑指Offer31.栈的压入、弹出序列

剑指Offer38.字符串的排列

剑指Offer32-III.从上到下打印二叉树III

641.设计循环双端队列

02/14

剑指Offer04.二维数组中的查找

剑指Offer12.矩阵中的路径

剑指Offer13.机器人的运动范围

剑指Offer07.重建二叉树

剑指Offer14-I.剪绳子

面试题16.25.LRU缓存

02/13

剑指Offer48.最长不含重复字符的子字符串

剑指Offer49.丑数

剑指Offer59-I.滑动窗口的最大值

剑指Offer59-II.队列的最大值

剑指Offer66.构建乘积数组

剑指Offer60.n个骰子的点数

剑指Offer67.把字符串转换成整数

剑指Offer63.股票的最大利润

剑指Offer64.求1+2+…+n

剑指Offer56-I.数组中数字出现的次数

02/12

剑指Offer45.把数组排成最小的数

剑指Offer46.把数字翻译成字符串

剑指Offer47.礼物的最大价值

12/30

112.最长递增路径

114.外星文字典

12/29

632.最小区间

094.最少回文分割

12/28

543.二叉树的直径

435.无重叠区间

51.数组中的逆序对

12/27

206.反转链表

1312.让字符串成为回文串的最少插入次数

12/23

5.最长回文子串

647.回文子串

409.最长回文串

12/18

121.买卖股票的最佳时机

122.买卖股票的最佳时机II

123.买卖股票的最佳时机III

188.买卖股票的最佳时机IV

12/16

049.从根节点到叶节点的路径数字之和

050.向下的路径节点之和

051.节点之和最大的路径

052.展平二叉搜索树

055.二叉搜索树迭代器

056.二叉搜索树中两个节点之和

012.左右两边子数组的和相等

12/15

043.往完全二叉树添加节点

044.二叉树每层的最大值

045.二叉树最底层最左边的值

046.二叉树的右侧视图

047.二叉树剪枝

039.直方图最大矩形面积

040.矩阵中最大的矩形

12/14

033.变位词组

12/13

095.最长公共子序列

096.字符串交织

036.后缀表达式

030.插入、删除和随机访问都是O(1)的容器

029.排序的循环链表

099.最小路径之和

106.二分图

093.最长斐波那契数列*

107.矩阵中的距离

113.课程顺序

12/12

767.重构字符串

153.寻找旋转排序数组中的最小值

969.煎饼排序

12/11

092.翻转字符

083.没有重复元素集合的全排列

084.含有重复元素集合的全排列

085.生成匹配的括号

007.数组中和为0的三个数

014.字符串中的变位词

031.最近最少使用缓存

12/10

090.环形房屋偷盗

035.最小时间差

010.和为k的子数组前缀和+哈希优化

12/9

057.值和下标之差都在给定的范围内桶

058.日程表

059.数据流的第K大数值

060.出现频率最高的k个数字

061.和最小的k个数对

073.狒狒吃香蕉

116.省份数量

071.按权重生成随机数

12/8

038.每日温度

100.三角形中最小路径之和

102.加减的目标值

103.最少的硬币数目

105.岛屿的最大面积

109.开密码锁

292.Nim游戏

118.多余的边

12/7

027.回文链表

072.求平方根

081.允许重复选择元素的组合

003.前n个数字二进制中1的个数

082.含有重复元素集合的组合

005.单词长度的最大乘积

008.和大于等于target的最短子数组

088.爬楼梯的最少成本

009.乘积小于K的子数组

089.房屋偷盗

011.0和1个数相同的子数组

013.二维子矩阵的和

091.粉刷房子

037.小行星碰撞

12/6

815.公交路线

909.蛇梯棋

529.扫雷游戏

934.最短的桥

146.LRU缓存机制

12/4

1293.网格中的最短路径三维BFS

141.环形链表

142.环形链表II

12/3

80.删除有序数组中的重复项II

1162.地图分析BFS

743.网络延迟时间Dijkstra

12/2

77.组合

70.爬楼梯

79.单词搜索

89.格雷编码

90.子集II

91.解码方法

92.反转链表II

93.复原IP地址

96.不同的二叉搜索树

97.交错字符串dp*********

12/1

214.最短回文串哈希/KMP

312.戳气球区间dp

7.整数反转

8.字符串转换整数(atoi)

12.整数转罗马数字

63.不同路径II

38.外观数列

40.组合总和II

11/30

149.直线上最多的点数

174.地下城游戏逆向dp

20.有效的括号栈

27.移除元素

124.二叉树中的最大路径和树上dp

底层const和顶层const

164.最大间距计数排序

11/29

123.买卖股票的最佳时机IIIdp

135.分发糖果

11/26

57插图区间什么破题???闭区间还是开区间都没说清楚。。

45跳跃游戏经典dp

50Pow经典快速幂

11/25

44通配符匹配

32最长有效括号

51+52N皇后

60排列序列**********

65有效数字

84柱状图中最大的矩形

11/24

301?????剪枝就愣是剪不过去???

41缺失的第一个正数有点意思

42接雨水

11/23

1.数之和

2.两数相加

3.无重复字符的最长子串

4.寻找两个正序数组的中位数

5.最长回文子串

15.三数之和

72.编辑距离

239.滑动窗口最大值

  1. 重载:是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class A
    {
    public:
    void fun(int tmp);
    void fun(float tmp); // 重载 参数类型不同(相对于上一个函数)
    void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
    void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
    int fun(int tmp); // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型
    };
  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;

    class Base
    {
    public:
    void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; }
    };

    class Derive : public Base
    {
    public:
    void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
    };

    int main()
    {
    Derive ex;
    ex.fun(1); // Derive::fun(int tmp)
    ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided
    return 0;
    }

    说明:上述代码中 ex.fun(1, 0.01); 出现错误,说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数,可以加上类型名指明 ex.Base::fun(1, 0.01);,这样就可以调用基类中的同名函数。

  3. 重写(覆盖):虚函数的应用。是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。

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

    class Base
    {
    public:
    virtual void fun(int tmp) { cout << "Base::fun(int tmp) : " << tmp << endl; }
    };

    class Derived : public Base
    {
    public:
    virtual void fun(int tmp) { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
    };
    int main()
    {
    Base *p = new Derived();
    p->fun(3); // Derived::fun(int) : 3
    return 0;
    }

重写和重载的区别:

  1. 范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。

  2. 参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。

  3. virtual 关键字:重写的函数基类中必须有 virtual关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。

隐藏和重写,重载的区别:

  1. 范围区别:隐藏与重载范围不同,隐藏发生在不同类中。

  2. 参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。

智能指针

智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C++11 中封装在了 <memory> 头文件中。

C++11 中智能指针包括以下三种:

  1. 共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。

  2. 独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用 move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。

  3. 弱指针(weak_ptr):指向 share_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
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

#include <iostream>
#include <memory>

template <typename T>
class SmartPtr
{
private :
T *_ptr;
size_t *_count;

public:
SmartPtr(T *ptr = nullptr) : _ptr(ptr)
{
if (_ptr)
{
_count = new size_t(1);
}
else
{
_count = new size_t(0);
}
}

~SmartPtr()
{
(*this->_count)--;
if (*this->_count == 0)
{
delete this->_ptr;
delete this->_count;
}
}

SmartPtr(const SmartPtr &ptr) // 拷贝构造:计数 +1
{
if (this != &ptr)
{
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
}
}

SmartPtr &operator=(const SmartPtr &ptr) // 赋值运算符重载
{
if (this->_ptr == ptr._ptr)
{
return *this;
}
if (this->_ptr) // 将当前的 ptr 指向的原来的空间的计数 -1
{
(*this->_count)--;
if (this->_count == 0)
{
delete this->_ptr;
delete this->_count;
}
}
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++; // 此时 ptr 指向了新赋值的空间,该空间的计数 +1
return *this;
}

T &operator*()
{
assert(this->_ptr == nullptr);
return *(this->_ptr);
}

T *operator->()
{
assert(this->_ptr == nullptr);
return this->_ptr;
}

size_t use_count()
{
return *this->count;
}
};

存在的问题

智能指针可能出现的问题:循环引用

在如下例子中定义了两个类 Parent、Child,在两个类中分别定义另一个类的对象的共享指针,由于在程序结束后,两个指针相互指向对方的内存空间,导致内存无法释放。

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

using namespace std;

class Child;
class Parent;

class Parent {
private:
shared_ptr<Child> ChildPtr;
public:
void setChild(shared_ptr<Child> child) {
this->ChildPtr = child;
}

void doSomething() {
if (this->ChildPtr.use_count()) {

}
}

~Parent() {
}
};

class Child {
private:
shared_ptr<Parent> ParentPtr;
public:
void setPartent(shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {

}
}
~Child() {
}
};

int main() {
weak_ptr<Parent> wpp;
weak_ptr<Child> wpc;
{
shared_ptr<Parent> p(new Parent);
shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
cout << p.use_count() << endl; // 2
cout << c.use_count() << endl; // 2
}
cout << wpp.use_count() << endl; // 1
cout << wpc.use_count() << endl; // 1
return 0;
}

解决方法

循环引用的解决方法: weak_ptr

循环引用:该被调用的析构函数没有被调用,从而出现了内存泄漏。

  1. weak_ptr 对被 shared_ptr 管理的对象存在 非拥有性(弱)引用,在访问所引用的对象前必须先转化为 shared_ptr;

  2. weak_ptr 用来打断 shared_ptr 所管理对象的循环引用问题,若这种环被孤立(没有指向环中的外部共享指针),shared_ptr 引用计数无法抵达 0,内存被泄露;令环中的指针之一为弱指针可以避免该情况;

  3. weak_ptr 用来表达临时所有权的概念,当某个对象只有存在时才需要被访问,而且随时可能被他人删除,可以用 weak_ptr 跟踪该对象;需要获得所有权时将其转化为 shared_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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>
#include <memory>

using namespace std;

class Child;
class Parent;

class Parent {
private:
//shared_ptr<Child> ChildPtr;
weak_ptr<Child> ChildPtr;
public:
void setChild(shared_ptr<Child> child) {
this->ChildPtr = child;
}

void doSomething() {
//new shared_ptr
if (this->ChildPtr.lock()) {

}
}

~Parent() {
}
};

class Child {
private:
shared_ptr<Parent> ParentPtr;
public:
void setPartent(shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {

}
}
~Child() {
}
};

int main() {
weak_ptr<Parent> wpp;
weak_ptr<Child> wpc;
{
shared_ptr<Parent> p(new Parent);
shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
cout << p.use_count() << endl; // 2
cout << c.use_count() << endl; // 1
}
cout << wpp.use_count() << endl; // 0
cout << wpc.use_count() << endl; // 0
return 0;
}

C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。

  1. 栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。

  2. 堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。

  3. 全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。

  4. 常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。

  5. 代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

说明:

从操作系统的本身来讲,以上存储区在内存中的分布是如下形式(从低地址到高地址):.text 段 –> .data 段 –> .bss 段 –> 堆 –> unused –> 栈 –> env

程序实例:

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

/*
说明:C++ 中不再区分初始化和未初始化的全局变量、静态变量的存储区,如果非要区分下述程序标注在了括号中
*/

int g_var = 0; // g_var 在全局区(.data 段)
char *gp_var; // gp_var 在全局区(.bss 段)

int main()
{
int var; // var 在栈区
char *p_var; // p_var 在栈区
char arr[] = "abc"; // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
char *p_var1 = "123456"; // p_var1 在栈区;"123456"为字符串常量,存储在常量区
static int s_var = 0; // s_var 为静态变量,存在静态存储区(.data 段)
p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
free(p_var);
return 0;
}

补充总结:

在理解C/C++内存分区时,常会碰到如下术语:数据区,堆,栈,静态存储区,静态区,常量区,常变量区,全局区,字符串常量区,静态常量区,静态变量区,文字常量区,代码区等等,初学者被搞得云里雾里。在这里,尝试捋清楚以上分区的关系。

数据区包括:堆,栈,静态存储区。

静态存储区包括:常量区(静态常量区),全局区(全局变量区)和静态变量区(静态区)。

常量区包括:字符串常量区和常变量区。

代码区:存放程序编译后的二进制代码,不可寻址区。