🐱作者:一只大喵咪1201
🐱专栏:《C++学习》
🔥格言:你只管努力,剩下的交给时间!
日期类的实现和内存管理
- 🏬日期类的实现
- 🏬C/C++内存分布
- 🏬C++内存管理方式
- 🏩new/delete和malloc/free的区别
- 🏬new和delete的实现原理
- 🏩operator new和operator delete函数
- 🏬定位new表达式
- 🏬总结
🏬日期类的实现
在前面学习完整个类和对象后,接下来本喵带大家写一个日期类来练练手。
这个日期类的内容包括,四大默认函数,日期+=天数,日期+天数,日期-天数,日期-=天数,前置++,后置++,后置–,前置–,>运算符重载,==运算符重载,>=运算符重载,<运算符重载,<=运算符重载,!=运算符重载,日期-日期 返回天数。
日期类的实现由于比较简单,仅是一些基础的知识的运用,所以本喵这里就不进行详细讲解了,在代码的注释中也有相应的解释。
Date.h中的代码:
#include <iostream>
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);
- 在头文件中,有的函数是有定义的,想构造函数,析构函数,拷贝构造函数,赋值运算符重载函数,以及获取天数的函数。
- 因为这些函数会频繁的调用,而定义在类中的函数,编译器会把它当作内联函数处理,因为这些函数调用比较频繁,所以放在类中以减少系统的开销。
- 而对于那些不常调用的成员函数,在类中只写函数的声明。
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;
}
- 成员函数不在类中定义,而在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;
}
部分测试结果,大家可以自己下去试试。
🏬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);
}
来看一组问题(本喵这里就直接回答了):
- 变量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;
}
上面是它的用法。
再结合一张图片来说明。
多个对象初始化时候,不能使用(),而是要和数组一样,使用{},但是没有引号。
注意:
申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]。必须匹配起来使用。
虽然不匹配的情况下,有时也不会报错,但是原则上我们还是要匹配使用的,有兴趣的小伙伴可以自行研究不配合会怎样。
在上面的代码中,我们发现,它的作用和malloc等函数是一样的,但实际上它们还是有差别的。
🏩new/delete和malloc/free的区别
- new/delete是关键字,属于运算符,而malloc/free是函数
上图中,使用malloc和new做同样的事情,malloc需要传参,但是new却不需要,因为malloc是函数,调用函数需要传参,而new是关键字,是运算符,使用的时候不需要传参,free和delete同理。
- 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释放多个动态空间时,同样也会调用多次析构函数。
- 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;
}
上面代码中,使用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的实现原理即可。