• 【C++学习】日期类和内存管理


    🐱作者:一只大喵咪1201
    🐱专栏:《C++学习》
    🔥格言:你只管努力,剩下的交给时间!
    图

    🏬日期类的实现

    在前面学习完整个类和对象后,接下来本喵带大家写一个日期类来练练手。

    这个日期类的内容包括,四大默认函数,日期+=天数,日期+天数,日期-天数,日期-=天数,前置++,后置++,后置–,前置–,>运算符重载,==运算符重载,>=运算符重载,<运算符重载,<=运算符重载,!=运算符重载,日期-日期 返回天数。

    日期类的实现由于比较简单,仅是一些基础的知识的运用,所以本喵这里就不进行详细讲解了,在代码的注释中也有相应的解释。

    Date.h中的代码:

    #include 
    using namespace std;
    
    class Date
    {
    public:
    	friend ostream& operator<<(ostream& out, const Date& d);
    	//构造函数
    	Date(int year = 1970, int month = 1, int day = 1)
    		:_year(year)
    		, _month(month)
    		, _day(day)
    	{}
    	//析构函数,对于日期类来说,并没有什么用
    	~Date()
    	{
    		_year = 0;
    		_month = 0;
    		_day = 0;
    	}
    	//拷贝构造函数
    	Date(const Date& d)
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    	//复制运算符重载
    	Date& operator=(const Date& d)
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    		return *this;
    	}
    	
    	//获取每个月的天数
    	int GetMonthDay(int year, int month)
    	{
    		int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
    		if (month == 2)
    		{
    			if ((year % 4 == 0 && year % 100) || (year % 400))
    				return 29;
    		}
    		return arr[month];
    	}
    	
    	//日期+=天数
    	Date& operator+=(int day);
    	//日期+天数
    	Date operator+(int day);
    	//日期-=天数
    	Date& operator-=(int day);
    	//日期-天数
    	Date operator-(int day);
    	//前置++
    	Date& operator++();
    	//后置++
    	Date operator++(int);
    	//前置--
    	Date& operator--();
    	//后置--
    	Date operator--(int);
    	//==运算符重载
    	bool operator==(const Date& d) const;
    	//!=运算符重载
    	bool operator!=(const Date& d) const;
    	//>运算符重载
    	bool operator>(const Date& d) const;
    	//<=运算符重载
    	bool operator<=(const Date& d) const;
    	//<运算符重载
    	bool operator<(const Date& d) const;
    	//>=运算符重载
    	bool operator>=(const Date& d) const;
    	//日期-日期 返回天数
    	int operator-(const Date& d);
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    //流插入运算符重载
    ostream& operator<<(ostream& out, const Date& d);
    
    • 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
    • 在头文件中,有的函数是有定义的,想构造函数,析构函数,拷贝构造函数,赋值运算符重载函数,以及获取天数的函数。
    • 因为这些函数会频繁的调用,而定义在类中的函数,编译器会把它当作内联函数处理,因为这些函数调用比较频繁,所以放在类中以减少系统的开销。
    • 而对于那些不常调用的成员函数,在类中只写函数的声明。

    Date.cpp中的代码:

    #include "Date.h"
    
    //日期+=天数
    Date& Date::operator+=(int day)
    {
    	_day += day;
    	while (_day > GetMonthDay(_year, _month))
    	{
    		int tmp = GetMonthDay(_year, _month);
    		_day -= tmp;
    		if (_month == 12)
    		{
    			_year++;
    			_month = 1;
    		}
    		else
    		{
    			_month++;
    		}
    	}
    	return *this;//日期本身被修改了
    }
    
    //日期+天数
    Date Date::operator+(int day)
    {
    	Date ret(*this);
    	ret._day += day;
    	while (ret._day > GetMonthDay(_year, _month))
    	{
    		int tmp = GetMonthDay(ret._year, ret._month);
    		ret._day -= tmp;
    		if (ret._month == 12)
    		{
    			ret._year++;
    			ret._month = 1;
    		}
    		else
    		{
    			ret._month++;
    		}
    	}
    	return ret;//日期本身没有被修改
    }
    
    //日期-=天数
    Date& Date::operator-=(int day)
    {
    	_day -= day;
    	while (_day <= 0)
    	{
    		if (_month == 1)
    		{
    			_year--;
    			_month = 12;
    		}
    		else
    		{
    			_month--;
    		}
    		int tmp = GetMonthDay(_year, _month);
    		_day += tmp;
    	}
    	return *this;
    }
    
    //日期-天数
    Date Date::operator-(int day)
    {
    	Date ret(*this);
    	ret._day -= day;
    	while (ret._day <= 0)
    	{
    		if (ret._month == 1)
    		{
    			ret._year--;
    			ret._month = 12;
    		}
    		else
    		{
    			ret._month--;
    		}
    		int tmp = GetMonthDay(ret._year, ret._month);
    		ret._day += tmp;
    	}
    	return ret;
    }
    
    //前置++
    Date& Date::operator++()
    {
    	*this += 1;
    	return *this;
    }
    
    //后置++
    Date Date::operator++(int)
    {
    	Date ret(*this);
    	*this += 1;
    	return ret;
    }
    
    //前置--
    Date& Date::operator--()
    {
    	*this -= 1;
    	return *this;
    }
    
    //后置--
    Date Date::operator--(int)
    {
    	Date ret(*this);
    	*this -= 1;
    	return ret;
    }
    
    //==运算符重载
    bool Date::operator==(const Date& d) const
    {
    	return (_year == d._year) && (_month == d._month) && (_day == d._day);
    }
    
    //!=运算符重载
    bool Date::operator!=(const Date& d) const
    {
    	return !((_year == d._year) && (_month == d._month) && (_day == d._day));
    }
    
    //>运算符重载
    bool Date::operator>(const Date& d) const
    {
    	if (_year > d._year)
    	{
    		return true;
    	}
    	else if (_year == d._year && _month > d._month)
    	{
    		return true;
    	}
    	else if (_year == d._year && _month == d._month && _day > d._day)
    	{
    		return true;
    	}
    	return false;
    }
    
    //<=运算符重载
    bool Date::operator<=(const Date& d) const
    {
    	return (!(*this > d));
    }
    //<运算符重载
    bool Date::operator<(const Date& d) const
    {
    	return (!(*this >= d));
    }
    
    //>=运算符重载
    bool Date::operator>=(const Date& d) const
    {
    	return (*this > d || *this == d);
    }
    //日期减日期
    int Date::operator-(const Date& d)
    {
    	int count = 0;
    	int flag = 1;
    	Date max = *this;
    	Date min = d;
    	int ret = max < min;
    	if (ret)
    	{
    		max = d;
    		min = *this;
    		flag = -1;
    	}
    	while (min < max)
    	{
    		++min;
    		count++;
    	}
    	return flag * count;
    }
    
    ostream& operator<<(ostream& out, const Date& d)
    {
    	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
    	return out;
    }
    
    • 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
    • 成员函数不在类中定义,而在cpp源文件中定义时,需要在函数名的前面加上类名和域作用限制符::(Date::函数名)。
    • 凡是不会修改this指针指向内容的成员函数,都应该使用const修饰。

    test.cpp中的测试代码:

    #include "Date.h"
    
    int main()
    {
    	//测试日期加天数
    	Date d1(2022, 10, 31);
    	cout << d1 + 100 << endl;
    
    	//测试日期减天数
    	Date d2(2022, 11, 6);
    	cout << d2 - 10 << endl;
    
    	//测试前后置加加
    	Date d3;
    	cout << d3++ << endl;
    	cout << ++d3 << endl;
    
    	//测试日期相减
    	Date d4 = d2;
    	Date d5 = 2022;
    	cout << d4 - d5 << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    图
    部分测试结果,大家可以自己下去试试。

    🏬C/C++内存分布

    在学习C语言的时候,我们知道内存是分为很多个区的,有栈区,堆区,静态区,常量区等等,这是站在C语言的角度来看的.

    C++是在建立在C语言的基础上的,所以C++和C语言的内存管理的方式是一样的,但是此时并不站在语言本身的角度去看内存,而是站在系统的角度去看内存。

    图
    上图就是将内存划分的几个区,其中数据段就是C语言中的静态区,代码段就是C语言中的常量区。不同区中的数据有不同的性质,比如生命周期,作用域等等性质。

    • 内核空间:是用来跑操作系统的,系统级别的数据都是在这个区上的,而且这个区我们普通用户是无法进行读写的,它从硬件上就给操作系统提供了保护。
    • 栈区:又叫堆栈,是用来存放局部变量的,这些变量都是些临时变量,比如非静态局部变量/函数参数/返回值等等,在用到的时候会开辟内存空间,用完以后该空间就会还给操作系统,这些变量的作用域和生命周期也是局部的,并且在开辟空间的时候是向下增长的,也就是先从高地址处开辟空间,再向低地址处开辟空间。
    • 内存映射段:是用来进行文件操作,以及动态库等内容的操作的,这个部分这里暂时先不谈。
    • 堆区:是用来存放动态变量的,这些变量在开辟内存空间的时候,往往是用多少开辟多少,而且空间大小还可以调整,在使用完以后需要手动将这些空间释放掉,否则就会造成内存泄漏,在开辟内存空间的时候,是向上生长的。
    • 数据段:是用来存放全局变量,以及使用static修饰的变量的,这些变量一旦被创建,它们的生命周期就是整个程序的生命周期,只有程序结束以后才会结束,所以在程序它是一个共享变量,因为对它的操作结果是会累加的。
    • 代码段:是用来存放代码以及那些字符常量的,这部分内容是不可以被修改的,只能读取使用,但是存放的并不是我们写好的源文件中的内容,而是经过编译链接以后产生的计算器可以读懂的机器码。

    上面这些仅是本喵的一个感性认识,具体的特性还需要在具体的情况中去体会。

    图
    上图中,将代码中的变量和内存的各个区域一一对应,可以很清楚的看到什么类型的变量放在内存的什么区域。

    下面跟着本喵做一个练习题,代码如下:

    int globalvar = 1;
    static int staticGlobalVar = 1;
    
    void test()
    {
    	static int staticVar = 1;
    	int localvar = 1;
    	int num1[10] = { 1,2,3,4 };
    	char char2[] = "abcd";
    	const char* pChar3 = "abcd";
    	int* ptr1 = (int*)malloc(sizeof(int) * 4);
    	int* ptr2 = (int*)calloc(4, sizeof(int));
    	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 8);
    	free(ptr1);
    	free(ptr3);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    来看一组问题(本喵这里就直接回答了):

    • 变量globalvar是一个全局变量,所以它是存在数据段的(静态区),作用域和生命周期也是全局。
    • 变量staticGlobalVar是用static修饰的全局变量,也是存在数据段的,作用域和生命周期也是全局。
    • 变量staticVal是被static修饰的局部变量,存放在数据段,生命周期是全局的,作用域是test函数内。
    • 变量localvar是一个局部变量,是存放在栈区的,也就是堆栈中,生命周期和作用域都是是test函数内。
    • 变量num1是一个数组,也是临时变量,是存放在栈区的,生命周期和作用域都是是test函数内。
    • 变量char2同样是一个数组,该数组中的字符是从常量区复制到栈区的,所以存放在栈区,生命周期和作用域都是是test函数内。
    • *char2是数明名的解引用,得到的结果就是数组中的第一个字符a,同样是在栈区。
    • pChar3是一个被const修饰的指针变量,仍然是一个临时变量,存放在栈区。
    • Pchar3里面的值是字符a在常量区的地址,所以*Pchar3后得到的值就是在常量区中的字符a,所以是放在常量区的,生命周期和作用域是一直存在的。
    • ptr1是一个指针变量,也是一个临时变量,存放在栈区。
    • *ptr1中的内容是动态开辟空间的地址,所以是放在堆区的,它的生命周期和作用域是视情况而定的。

    通过上面详细分析各个变量的类型以及它们在内存中的位置,相信大家对内存管理的理解更加深刻了。

    🏬C++内存管理方式

    在C语言中,内存的管理是通过malloc,calloc,realloc等函数来实现的,由于C++兼容C语言,所以这些函数在C++中仍然可以使用,但是C++中也提出了新的内存管理方式,就是运算符new和delete。

    int main()
    {
    	//动态申请一个int类型的空间
    	int* p1 = new int;
    	//动态申请一个int类型的空间,并且初始化为10
    	int* p2 = new int(10);
    	//动态申请10个int类型的空间
    	int* p3 = new int[10];
    	//动态申请10个int类型的空间并初始化
    	int* p4 = new int[10]{ 1,2,3,4,5,6,7,8,9,10 };
    
    	//释放p1和p2分别指向空间
    	delete p1;
    	delete p2;
    	//释放p3和p4分别指向的空间
    	delete[] p3;
    	delete[] p4;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    上面是它的用法。

    图
    再结合一张图片来说明。

    多个对象初始化时候,不能使用(),而是要和数组一样,使用{},但是没有引号。

    注意:

    申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]。必须匹配起来使用。

    虽然不匹配的情况下,有时也不会报错,但是原则上我们还是要匹配使用的,有兴趣的小伙伴可以自行研究不配合会怎样。

    在上面的代码中,我们发现,它的作用和malloc等函数是一样的,但实际上它们还是有差别的。

    🏩new/delete和malloc/free的区别

    1. new/delete是关键字,属于运算符,而malloc/free是函数

    图
    上图中,使用malloc和new做同样的事情,malloc需要传参,但是new却不需要,因为malloc是函数,调用函数需要传参,而new是关键字,是运算符,使用的时候不需要传参,free和delete同理。

    1. new会调用自定义类型的构造函数,delete会调用自定义类型的析构函数,而malloc/free不会。

    new和delete之所以在C++中会存在,那就肯定有和malloc和free不同的地方。C++是基于面向对象的语言,所以new和delete也是为了处理自定义类型才有的。在处理内置类型的时候,new和malloc是一样的,没有区别,delete和free也是。

    图
    创建这样一个类,在类中显示定义构造函数和析构函数,并且在函数内打印对应的语句。

    图
    使用new开辟一个A类型的动态空间时,会自动调用A的构造函数,来给动态空间中的对象初始化。这一点和使用calloc开辟动态空间后用0初始化类似,只是这里调用的是构造函数。

    图
    是使用delete释放A类型的动态空间时,会自动调用A的析构函数。

    new/delete 和 malloc/free最大区别是 new/delete对于自定义类型除了开空间还会调用构造函数和析构函数。

    图
    使用new开辟多个动态空间时,就会调用多次构造函数来初始化,当使用delete释放多个动态空间时,同样也会调用多次析构函数。

    1. new开辟空间失败会抛异常,malloc开辟失败返回空指针

    先看malloc开辟空间失败的情况,当开辟的空间很大的时候,系统的内存不够,就会开辟失败。

    图
    每次开辟1G的动态空间,并且不释放,第一次开辟成功,第二次就失败了,因为此时内存不够用了。

    • 打印出开辟失败的原因是,开辟失败后返回的指针是NULL空指针,所以才能符号调节判断,进入开辟失败打印。

    再看使用new开辟失败后的情况。

    图
    同样每次开辟1G的内存空间,第一次开辟成功,第二次就失败了。

    • 打印出的结果不是开辟失败,而是出现异常,说明开辟失败以后并不是返回NULL空指针,所以就没有进if判断语句,而是直接跳到了catch中。
    • try和catch就是专门用来捕获程序中的异常的,如上图中的蓝色圈,以后会相信介绍异常,这里仅需要知道,new开辟失败了以后是抛异常。

    🏬new和delete的实现原理

    🏩operator new和operator delete函数

    是不是感觉很眼熟,这个不是运算符重载吗?不是,这里是俩个函数。

    • operator new和operator delete是系统提供的全局函数。
    • new在底层调用operator new全局函数来申请空间。
    • delete在底层通过operator delete全局函数来释放空间。

    图
    上图中的代码是从C++的库中扒出来的,可以看到,operator new函数的实质就是在使用malloc开辟动态空间,开辟成功则返回地址,开辟失败则抛出异常,如上图中的红色线。

    图
    上图中的代码同样是从C++的库中扒出来的,可以看到,在最下面的红色框中,将free§宏定义为_free_dbg(),在倒数第二个红色框中,又使用了宏定义后的函数来释放空间,也就是使用了free()函数来释放空间。

    以上库封装后的代码可以总结为:

    • operator new函数的本质是在使用malloc开辟动态内存空间。
    • operator delete函数的本质是在使用free释放开辟好的动态内存空间。

    下面我们来看new的底层原理:

    图
    以该段代码为例,我们来看它的汇编代码:
    图

    • 在使用new开辟一个A类型的动态空间的时候,在汇编代码中可以看到,调用了operator new函数和A类型的构造函数。
    • 在使用delete释放刚刚开辟的空间时,在汇编代码中调用了如上图中最后一个绿色框中所示的函数,在该函数内会调用operator delete函数和A类型的析构函数。

    结合operator new函数和operator delete函数的本质,我们就可以得出结论,

    • new的本质就是:使用malloc开辟空间,成功了返回地址,失败了抛异常,并且调用自定义类型的构造函数。
    • delete的本质就是:使用free释放开辟的空间,并且调用自定义函数的析构函数。

    同样的,使用new开辟多个空间,和使用delete释放多个空间,无非就是多调用几次operator new和构造函数,以及operator delete和析构函数。

    图
    来看它的汇编代码:
    图

    • 调用了operator new[]函数,该函数多了一个[],无非就是多调用几次operator new函数,具体次数又[]中的数字决定。
    • 在第二个绿色框内,通过迭代器调用了多次构造函数。
    • delete[]同理,本喵这里就不列出来了。

    所以说,无论是开辟一个自定义类型的动态空间,还是多个,其本质都是在调用malloc和构造函数。在释放的时候,本质也是在调用free函数和析构函数。

    🏬定位new表达式

    定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

    • 用法:new(空间地址)类型(类型初始化列表)
    • 功能:将没有初始化的动态内存空间进行初始化
    class A
    {
    public:
    	A(int a = 10)
    		:_a(a)
    	{
    		cout << "构造函数" << endl;
    	}
    
    private:
    	int _a;
    };
    
    int main()
    {
    	A* pa = (A*)malloc(sizeof(A));
    	if (pa == nullptr)
    	{
    		perror("malloc fail");
    		return -1;
    	}
    
    	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

    上面代码中,使用malloc函数开辟了一个类型A的动态空间。
    图
    通过调试可以看到,此时动态空间中成员变量a的值是随机值,因为malloc开辟的动态空间并不会自动进行初始化。

    图
    此时使用定位new以后,就成功的将原本是随机数的动态空间通过调用类A的构造函数初始化为了20。

    结合前面的知识,可以模拟一下new和delete的实现:
    图

    • new的实现本质就是在调用operator new函数和构造函数,而定位new同样也会调用构造函数。
    • delete的本质就是在调用operator delete函数和析构函数,而operator delete函数的本质也是在调用free函数。

    定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

    🏬总结

    日期类的实现仅是对前面学习内容的一个应用,而在C++的内存管理中,仅需要知道new/delete和malloc/free的区别,以及new和delete的实现原理即可。

  • 相关阅读:
    RK3399平台开发系列讲解(USB篇)如何去学习USB驱动 - 视频课
    rust学习(第一章)
    OSPF 的10种网络类型和5种区域类型
    node.js基于vue框架潮牌官网设计与实现毕业设计源码010955
    java生成excel,uniapp微信小程序接收excel并打开
    MySQL使用SHOW PROCESSLIST 详解
    【无标题】
    JVM原理:JVM运行时内存模型(通俗易懂)
    [MySQL]二、进程的关系、MySQL密码破解、建表和建库相关命令
    Vue练习
  • 原文地址:https://blog.csdn.net/weixin_63726869/article/details/127550932