C++类与对象 (中)

目录

类的6个默认构造函数

构造函数

默认构造函数

析构函数

析构函数特点

默认析构函数

拷贝构造函数

拷贝构造函数特点

 赋值运算符重载

运算符重载

运算符重载的特点 

==运算符重载的实现

 赋值运算符

赋值运算符特点

=赋值运算符重载 ​​

日期类的实现

const 成员

const修饰类的成员函数

mutable关键字

关于const修饰的成员函数的几个常见问题

取地址及const取地址操作符重载


类的6个默认构造函数

class Date{}

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情 况下,都会自动生成下面6个默认成员函数。

 

  • 构造函数,析构函数:用来进行对象创建时初始化工作对象销毁时候的资源清理工作
  • 拷贝构造函数,赋值运算符重载:用来进行对象拷贝
  • 普通对象const对象取地址:这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
  • 默认的成员函数用户自己没有写的时候编译器会自己生成,用户一旦显式提供了编译器就不再生成。

构造函数

构造函数 是一个 特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用 保证每个数据成员 都有 一个合适的初始值,并且 在对象的生命周期内只调用一次 无返回值,可以重载。

本文测试环境均为VS2019 

默认构造函数

对于目前写的这个Date类,由于没有显式定义构造函数,所以系统调用了默认的构造函数,为类中的变量分配空间。

默认的构造函数作用不大,因为对象创建好之后,对象中成员变量的值仍然为随机值。但在下面情况下构造函数就有实际意义。

 

  • 对于内置类型的成员变量仍然是随机值
  • 对自定义类型的的对象,编译器就必须在默认的构造函数中调用内部自定义成员变量对应的无参构造函数

  • 可以看到_t已被赋值

  •  Attention:创造哪个类的对象,编译器就会调用哪个类的构造函数,此处一定不会调用Time类的构造函数,而是在Date类中的默认构造函数中调用了Time类的构造函数,因为此处创建的是Date类的对象,不会在和Date类同一层级上调用Time类构造函数。

析构函数

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源(堆上申请的内存空间,文件指针,套接字等等)的清理工作。

析构函数特点

  •   析构函数名是在类名前加上字符 ~。
  •  无参数无返回值,不可以重载。
  •  一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  •  对象生命周期结束时,C++编译系统系统自动调用析构函数。 

默认析构函数

编译器生成的默认析构函数,会对自定类型成员调用它的析构函数,与默认构造函数类似。

拷贝构造函数

 

拷贝构造函数只有单个形参 ,该形参是对本 类类型对象的引用(一般常用const修饰) ,在用 已存在的类类型对象创建新对象时由编译器自动调用

拷贝构造函数特点

  • 拷贝构造函数是构造函数的一个重载形式。  
  • 拷贝构造函数 参数只有一个 必须使用引用传参 使用 传值方式会引发无穷递归调用

  • 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。                                             
  • 是否需要自己定义拷贝构造函数。  编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样单纯赋值的类是没必要的,但像需要申请空间的类比如说栈就会出现问题。                       拷贝出来的s2与s1内存的array地址是相同的指向了堆上的同一块空间,在程序结束调用析构函数清理资源时s2会先释放对应的堆空间,但s1并不知道s2已经释放了,s1由于先被创建,所以在s2之后析构,会再次将此块内存释放一遍,但一块内存不能被释放两次,就会报错。解决方法为深拷贝。

 赋值运算符重载

运算符重载

在我们想比较两个对象大小时,可以将代码写成上图所示的样子,非常直观很好理解,但是无法进行比较,那我们要是想比较两个对象时该怎么办呢?这里就引入了运算符重载的概念。 

C++为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数 也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator后面接需要重载的运算符符号
函数原型: 返回值类型 operator操作符(参数列表)

运算符重载的特点 

  •  不能通过连接其他符号来创建新的操作符:比如operator@
  •  重载操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
  • 操作符有一个默认的形参this,限定为第一个形参
  • .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

==运算符重载的实现

// ==运算符重载
	bool operator==(const Date& d) {
		return _year == d._year && _month == d._month && _day == d._day;
	}

 赋值运算符

赋值运算符特点

  • 参数类型
  • 返回值
  • 检测是否自己给自己赋值
  • 返回*this
  • 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。

=赋值运算符重载

// 赋值运算符重载
	Date& operator=(const Date& d) {
		_year = d._year;
		_month =d._month;
		_day = d._day;
		return *this;
	}

 

 可以看到在我们没有显式定义时,d2的值还是被赋给了d1,编译器会自动生成一个默认的赋值重载运算符,采用的也是逐字节拷贝(浅拷贝)的方式。那么问题来了,如果是要操作资源的对象是不是也可以使用默认的赋值重载运算符呢?答案是不行的

以栈为例

调用默认的赋值运算符,调用前_array地址是不一样的

 

 调用后由于是浅拷贝,将_array的地址也拷贝了过来,这就造成了当程序结束析构函数清理资源时会将这个空间释放两次,造成崩溃。而且会使被赋值那一个对象原来的内存空间无人回收,造成内存泄露。所以一般在需要操作资源时尽量自己定义赋值运算符。

日期类的实现

博主实现了一个日期类 ,包含了基本的几类赋值重载运算符,可以直接运行。

#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
	// 获取某年某月的天数
	int GetMonthDay(int year, int month) {
		int days[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
		int Days = days[month - 1];
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) {
			Days += 1;
		}
		return Days;
	}
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1) {
		if (year >10000|| year<0 || month < 1 || month>12 || day>GetMonthDay(year, month) || day < 1) {
			cout << "输入错误" << endl;
		}
		_year = year;
		_month = month;
		_day = day;
	}
	// 拷贝构造函数
  // d2(d1)
	Date(const Date& d) {
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	// 赋值运算符重载
  // d2 = d3 -> d2.operator=(&d2, d3)
	Date& operator=(const Date& d) {
		_year = d._year;
		_month =d._month;
		_day = d._day;
		return *this;
	}
	// 析构函数
	~Date(){}
	// 日期+=天数
	Date& operator+=(int day) {
		if (day < 0) {
			cout << "输入有误" << endl;
			assert(0);
		}
		_day += day;
		while (_day > GetMonthDay(_year, _month)) {
			_day -= GetMonthDay(_year, _month);
			_month++;
			if (_month == 13) {
				_year++;
				_month = 1;
			}
		}
		return *this;
	}
	// 日期+天数
	Date operator+(int day) {
		Date tmp(*this);
		tmp += day;
		return tmp;
	}
	// 日期-天
	Date operator-(int day) {
		Date tmp(*this);
		tmp -= day;
		return tmp;
	}
	// 日期-=天
	Date& operator-=(int day) {
		if (day < 0) {
			cout << "输入有误" << endl;
			assert(0);
		}
		_day -= day;
		while (_day <=0) {
			_month--;
			if (_month < 1) {
				_year--;
			}
			_day += GetMonthDay(_year, _month);
		}
		return *this;
	}
	
	// 前置++
	Date& operator++(){
		*this += 1;
		return *this;
	}
	// 后置++
	Date operator++(int){
		Date tmp(*this);
		*this += 1;
		return tmp;
	}
	// 后置--
	Date operator--(int){
		Date tmp(*this);
		*this -= 1;
		return tmp;
	}
	// 前置--
	Date& operator--(){
		*this -= 1;
		return *this;
	}
	// >运算符重载
	bool operator>(const Date& d) {
		if (_year > d._year) {
			return true;
		}
		else if (_year < d._year) {
			return false;
		}
		else {
			if (_month > d._month) {
				return true;
			}
			else if (_month < d._month) {
				return false;
			}
			else {
				if (_day > d._day) {
					return true;
				}
				else {
					return false;
				}
			}
		}
	}
	// ==运算符重载
	bool operator==(const Date& d) {
		return _year == d._year && _month == d._month && _day == d._day;
	}
	// >=运算符重载
	inline bool operator >= (const Date& d) {
		return *this > d || *this == d;
	}
	// <运算符重载
	bool operator < (const Date& d) {
		if (_year < d._year) {
			return true;
		}
		else if (_year > d._year) {
			return false;
		}
		else {
			if (_month < d._month) {
				return true;
			}
			else if (_month > d._month) {
				return false;
			}
			else {
				if (_day < d._day) {
					return true;
				}
				else {
					return false;
				}
			}
		}
	}
	// <=运算符重载
	bool operator <= (const Date& d) {
		return *this < d || *this == d;
	}
	// !=运算符重载
	bool operator != (const Date& d) {
		return !(*this == d);
	}
	// 日期-日期 返回天数
	int operator-(const Date& d) {
		int flag = 1;
		Date future = *this;
		Date past = d;
		if (*this < d) {
			past = *this;
			future = d;
			flag = -1;
		}
		int day_count = 0;
		while (past < future) {
			--future;
			day_count++;
		}
		return day_count * flag;
	}
	void PrintDate() {
		cout << _year << "年" << _month << "月" <<_day<<"日"<<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void TestDate() {
	Date d1(2022, 3, 18);
	Date d2(d1);
	d2.PrintDate();
	d1 += 100;
	d1.PrintDate();
	d1 -= 100;
	d1.PrintDate();
	int n = 100;
	while (n--) {
		++d1;
	}
	d1.PrintDate();
	n = 100;
	while (n--) {
		--d1;
	}
	d1.PrintDate();
	Date d3(2018, 12, 31);
	Date d4(2025, 3, 27);
	cout << (d3 > d4) << endl;
	cout << (d1 == d2) << endl;
	Date d5(2022, 6, 26);
	cout << (d5 - d1) << endl;
}
int main() {
	TestDate();
	return 0;
}

const 成员

const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数 ,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。  

mutable关键字

mutable关键字作用:当或者结构体中的某个变量被mutable关键字修饰时,即便这个结构体或者类被实例化为const类型,其中被mutable修饰的变量仍可以被修改 

关于const修饰的成员函数的几个常见问题

  • const对象可以调用非const成员函数吗?
  • 非const对象可以调用const成员函数吗?
  • const成员函数内可以调用其它的非const成员函数吗?
  • 非const成员函数内可以调用其它的const成员函数吗? 

取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成 

class Date
{ 
public :
 Date* operator&()
 {
 return this ;
 }
 
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

 这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

end 

  • 14
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值