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

作者:一只大喵咪1201
专栏:《C++学习》
格言:你只管努力,剩下的交给时间!
【C++学习】日期类和内存管理_第1张图片

日期类的实现和内存管理

  • 日期类的实现
  • 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++学习】日期类和内存管理_第2张图片
部分测试结果,大家可以自己下去试试。

C/C++内存分布

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

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

【C++学习】日期类和内存管理_第3张图片
上图就是将内存划分的几个区,其中数据段就是C语言中的静态区,代码段就是C语言中的常量区。不同区中的数据有不同的性质,比如生命周期,作用域等等性质。

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

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

【C++学习】日期类和内存管理_第4张图片
上图中,将代码中的变量和内存的各个区域一一对应,可以很清楚的看到什么类型的变量放在内存的什么区域。

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

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

上面是它的用法。

【C++学习】日期类和内存管理_第5张图片
再结合一张图片来说明。

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

注意:

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

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

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

new/delete和malloc/free的区别

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

【C++学习】日期类和内存管理_第6张图片
上图中,使用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也是。

【C++学习】日期类和内存管理_第7张图片
创建这样一个类,在类中显示定义构造函数和析构函数,并且在函数内打印对应的语句。

【C++学习】日期类和内存管理_第8张图片
使用new开辟一个A类型的动态空间时,会自动调用A的构造函数,来给动态空间中的对象初始化。这一点和使用calloc开辟动态空间后用0初始化类似,只是这里调用的是构造函数。

【C++学习】日期类和内存管理_第9张图片
是使用delete释放A类型的动态空间时,会自动调用A的析构函数。

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

【C++学习】日期类和内存管理_第10张图片
使用new开辟多个动态空间时,就会调用多次构造函数来初始化,当使用delete释放多个动态空间时,同样也会调用多次析构函数。

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

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

【C++学习】日期类和内存管理_第11张图片
每次开辟1G的动态空间,并且不释放,第一次开辟成功,第二次就失败了,因为此时内存不够用了。

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

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

【C++学习】日期类和内存管理_第12张图片
同样每次开辟1G的内存空间,第一次开辟成功,第二次就失败了。

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

new和delete的实现原理

operator new和operator delete函数

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

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

【C++学习】日期类和内存管理_第13张图片
上图中的代码是从C++的库中扒出来的,可以看到,operator new函数的实质就是在使用malloc开辟动态空间,开辟成功则返回地址,开辟失败则抛出异常,如上图中的红色线。

【C++学习】日期类和内存管理_第14张图片
上图中的代码同样是从C++的库中扒出来的,可以看到,在最下面的红色框中,将free§宏定义为_free_dbg(),在倒数第二个红色框中,又使用了宏定义后的函数来释放空间,也就是使用了free()函数来释放空间。

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

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

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

【C++学习】日期类和内存管理_第15张图片
以该段代码为例,我们来看它的汇编代码:
【C++学习】日期类和内存管理_第16张图片

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

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

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

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

【C++学习】日期类和内存管理_第17张图片
来看它的汇编代码:
【C++学习】日期类和内存管理_第18张图片

  • 调用了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的动态空间。
【C++学习】日期类和内存管理_第19张图片
通过调试可以看到,此时动态空间中成员变量a的值是随机值,因为malloc开辟的动态空间并不会自动进行初始化。

【C++学习】日期类和内存管理_第20张图片
此时使用定位new以后,就成功的将原本是随机数的动态空间通过调用类A的构造函数初始化为了20。

结合前面的知识,可以模拟一下new和delete的实现:
【C++学习】日期类和内存管理_第21张图片

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

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

总结

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

你可能感兴趣的:(C++学习,c++,学习,开发语言)