【C++学习】日期类和内存管理
创始人
2024-01-18 15:27:41

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

日期类的实现和内存管理

  • 🏬日期类的实现
  • 🏬C/C++内存分布
  • 🏬C++内存管理方式
    • 🏩new/delete和malloc/free的区别
  • 🏬new和delete的实现原理
    • 🏩operator new和operator delete函数
  • 🏬定位new表达式
  • 🏬总结

🏬日期类的实现

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

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

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

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);
  • 在头文件中,有的函数是有定义的,想构造函数,析构函数,拷贝构造函数,赋值运算符重载函数,以及获取天数的函数。
  • 因为这些函数会频繁的调用,而定义在类中的函数,编译器会把它当作内联函数处理,因为这些函数调用比较频繁,所以放在类中以减少系统的开销。
  • 而对于那些不常调用的成员函数,在类中只写函数的声明。

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类型的空间,并且初始化为10int* 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的区别

  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;
}

上面代码中,使用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的实现原理即可。

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
苁蓉的食用方法 鲜苁蓉的吃法有... 肉苁蓉号称“温而不热,补而不峻,暖而不燥,滑而不泄,故有从容之名。”历来也是传统药食同源的滋补佳品。...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
阳澄湖在哪里哪个省的 阳澄湖是... 点击题目下方苏州生活指南有用 有趣 有态度近期的阳澄湖度很高,3月24日,央视二套《第一时间》直播连...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...