【C++】类和对象(中篇)

❤️欢迎来到我的博客❤️

类的6个默认成员函数

空类并不是什么都没有,任何一个类都会自动生成以下6个默认成员函数
在这里插入图片描述


构造函数

平时写代码容易忘记初始化和销毁,并且有些地方写起来很繁琐,有了构造函数析构函数之后就不用担心这些问题了
构造函数是一个特殊的成员函数,构造函数的主要任务并不是开空间创建对象,而是初始化对象
构造函数的特征:

函数名与类名相同
没有返回值(也不需要写void)
对象实例化时编译器自动调用对应的构造函数
构造函数可以重载(应对多种初始化的方式)

class A
{
public:
	//构造函数
	A(int y = 2023, int m = 11, int d = 1)
	{
		_y = y;
		_m = m;
		_d = d;
	}
private:
	int _y;
	int _m;
	int _d;
};


int main()
{
	A day1;//对象实例化自动调用构造函数
	return 0;
}

在这里插入图片描述

如果构造函数设为私有则无法调用
在这里插入图片描述

如果我们不写构造函数会怎么样?

class A
{
public:
	void Print()
	{
		cout << _y << " " << _m << " " << _d << endl;
	}
private:
	int _y;
	int _m;
	int _d;
};


int main()
{
	A day1;
	A day2;
	day1.Print();

	return 0;
}

运行效果:

在这里插入图片描述
代码运行出来是一堆随机值,那么编译器到底有没有调用构造函数呢?
其实是调用了的,C++把类型分成了两类,一类是内置类型/基本类型 - 语言本身定义的基础类型,比如 int char double 任何类型的指针等
还有一类是自定义类型,用struct/class等等定义的类型
如果我们不写,编译器默认生成的构造函数对于内置类型不做初始化处理(有些编译器也会处理,但是我们要默认当成编译器不会处理),对于自定义类型会去调用他的默认构造
编译器默认生成的构造函数无参的构造函数全缺省的构造函数都称为默认构造函数,这三种只能存在一个
注:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(不传参就可以调用的)


C++11标准发布的时候,在成员声明的时候可以给缺省值

class A
{
public:	
	void Print()
	{
		cout << _y << " " << _m << " " << _d << endl;
	}
private:
	//内置类型
	//C++11支持的
	//注:这里并不是初始化,因为没有开空间
	//这里给的是默认的缺省值,给编译器生成的默认构造函数用的
	int _y = 1;
	int _m = 1;
	int _d;	
};

int main()
{
	A day1;
	//注:不能写成这样 A day1() 这样编译器无法区分是day1对象还是函数名
	day1.Print();
	return 0;
}

运行效果:
在这里插入图片描述

结论:

  1. 一般情况下,有内置类型成员,就需要自己写构造函数,不能用编译器自己生成的
  2. 如果内置类型的成员都有缺省值,且初始化符合我们的要求
    或者全是自定义类型的构造,且这些类型都定义了默认构造,这两种情况可以考虑不写构造函数

析构函数

析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作

析构函数的特征:

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

class A
{
public:
	//构造函数
	A(int y = 2023, int m = 11, int d = 1)
	{
		cout << "已调用构造函数" << endl;
		_y = y;
		_m = m;
		_d = d;
	}

	~A()
	{
		cout << "已调用析构函数" << endl;
		_y = 0;
		_m = 0;
		_d = 0;
	}
private:
	int _y;
	int _m;
	int _d;
};


int main()
{
	A day1;
	
	return 0;
}

运行效果:
在这里插入图片描述

在这里插入图片描述
系统自动生成的析构函数和构造函数类似:

  1. 内置类型成员不做处理
  2. 自定义类型会去调用他的析构函数
  3. 一般情况下,如果有动态申请的资源那就要显式写析构函数释放资源
  4. 如果没有动态申请的资源或者需要释放的成员都是自定义类型,就不用写析构

拷贝

浅拷贝

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

拷贝构造函数也是特殊的成员函数
拷贝构造函数是构造函数的一个重载形式

class Date
{
public:
	Date(int year = 2023, int month = 11, int day = 4)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//错误写法Date(Date d)
	Date(Date& d)//正确写法
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date day1;
	Date day2(day1);
	
	return 0;
}

C++规定了,内置类型直接拷贝,自定义类型必须调用拷贝构造完成拷贝
拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
在这里插入图片描述
所以拷贝构造必须传引用或指针,否则就会无限递归
运行效果:
在这里插入图片描述


若未显式定义编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

  1. 内置类型成员完成值拷贝/浅拷贝
  2. 自定义类型成员会调用他的拷贝构造

没有写拷贝构造函数

class Date
{
public:
	Date(int year = 2023, int month = 11, int day = 4)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date day1(2024,1,1);
	Date day2(day1);
	
	return 0;
}

运行效果:
在这里插入图片描述
那我们能不能直接用编译器默认生成的拷贝构造函数呢?
并不行,因为默认生成的只是完成了浅拷贝
比如以下这段代码,无法正常运行

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc失败");
			return;
		}

		_capacity = capacity;
		_top = 0;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = 0;
		_top = 0;
	}

private:
	int* _a = nullptr;
	int _top = 0;
	int _capacity;
};

int main()
{
	Stack st1;
	Stack st2(st1);
	
	return 0;
}

在这里插入图片描述

在这里插入图片描述
可以看到两个对象的_a都是指向的同一块空间,这样会发生什么问题呢?
st2出了作用域会调用一次析构函数 st1出了作用域也会调用一次析构函数,同一块空间就被析构了两次,这就是导致程序崩溃的原因
另一个问题是其中一个对象的修改会影响另一个对象
这时候就必须自己实现深拷贝

深拷贝

出现崩溃的原因是因为都指向了同一块空间,那么我们就必须让两个对象指向不同的空间

//自己实现
Stack(const Stack& st)
{
	_a = (int*)malloc(sizeof(int) * st._capacity);
	if (_a == nullptr)
	{
		perror("malloc失败");
		return;
	}

	memcpy(_a, st._a, sizeof(int) * st._top);
	_top = st._top;
	_capacity = st._capacity;
}

运行效果:
在这里插入图片描述


运算符重载

如果我们有2个日期,我们怎么比较他们两个的大小?

class Date
{
public:
	Date(int year = 2023, int month = 11, int day = 4)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	int _year;
	int _month;
	int _day;
};

正常情况我们会写一个函数来比较他们的大小

//函数里不修改参数最好加上const
bool A(const Date& d1, const Date& d2)
{
	if (d1._year < d2._year)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month < d2._month)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month == d2._month && d1._day < d2._day)
	{
		return true;
	}

	return false;
}

bool B(const Date& d1, const Date& d2)
{
	if (d1._year > d2._year)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month > d2._month)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day)
	{
		return true;
	}

	return false;
}

int main()
{
	Date day1(2023, 11, 4);
	Date day2(2024, 1, 1);
	
	cout << A(day1, day2) << endl;
	cout << B(day1, day2) << endl;
	
	//无法直接比较
	cout << day1 < day2 << endl;
	return 0;
}

那么上面的函数有没有什么问题呢?
有,我们无法通过函数名来判断该函数是进行日期比较的,只能观察函数内部来判断

为什么内置类型可以直接比较,自定义类型不可以
因为编译器知道内置类型该如何比较,但是自定义类型是我们自己定义的,所以编译器不知道该如何比较,只有我们自己知道

因此C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
关键字:operator

bool operator<(const Date& d1, const Date& d2)
{
	if (d1._year < d2._year)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month < d2._month)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month == d2._month && d1._day < d2._day)
	{
		return true;
	}

	return false;
}

bool operator>(const Date& d1, const Date& d2)
{
	if (d1._year > d2._year)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month > d2._month)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day)
	{
		return true;
	}

	return false;
}

int main()
{
	Date day1(2023, 11, 4);
	Date day2(2024, 1, 1);

	//流插入/流提取的优先级较高,所以需要加上括号
	cout << (day1 < day2) << endl;
	cout << (day1 > day2) << endl;
	
	//实际上编译器会转换成,我们也可以这样写,但是没有意义,因为这样写没有可读性
	cout << operator<(day1, day2) << endl;
	cout << operator>(day1, day2) << endl;

	return 0;
}

操作符是几个操作数,重载函数就有几个参数,是否要重载运算符,取决于运算符对这个类是否有意义
那如果数据是私有/保护的我们该如何进行比较呢?
最好的方式是把函数写成成员函数:

class Date
{
public:
	Date(int year = 2023, int month = 11, int day = 4)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//需要注意的是,左操作数是this,指向调用函数的对象,
	//因此不能写成bool operator<(const Date& d1, const Date& d2)
	bool operator<(const Date& d2)
	{
		if (_year < d2._year)
		{
			return true;
		}
		else if (_year == d2._year && _month < d2._month)
		{
			return true;
		}
		else if (_year == d2._year && _month == d2._month && _day < d2._day)
		{
			return true;
		}

		return false;
	}

	bool operator>(const Date& d2)
	{
		if (_year > d2._year)
		{
			return true;
		}
		else if (_year == d2._year && _month > d2._month)
		{
			return true;
		}
		else if (_year == d2._year && _month == d2._month && _day > d2._day)
		{
			return true;
		}

		return false;
	}

private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date day1(2023, 11, 4);
	Date day2(2024, 1, 1);

	cout << (day1 < day2) << endl;
	cout << (day1 > day2) << endl;
	
	//实际转换为
	//d1.operator<(d2);
	//d1.operator>(d2);

	return 0;
}

注:

不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
在这里插入图片描述
以上5个运算符不能重载

赋值运算符重载

赋值运算符重载是对已经存在的两个对象之间的复制拷贝
构造函数是用一个已经存在的对象初始化另一个对象,不要把这两个搞混了

class Date
{
public:
	Date(int year = 2023, int month = 11, int day = 4)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}


private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date day1(2023, 11, 4);
	Date day2(2024, 1, 1);

	//已经存在的两个对象之间的复制拷贝 -- 运算符重载函数
	day1 = day2;

	return 0;
}

以上函数并不能处理连续赋值

day3 = day4 = day1;

出错的原因是因为我们的函数返回的是void,而想要解决此类问题我们应该对函数进行改造,让他有一个正确的返回值

//this是day4的地址
Date& operator=(const Date& d)
{
	//如果不想出现day1 = day1(自己给自己赋值)就加上判断
	if(this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	return *this;
}

参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
赋值运算符只能重载成类的成员函数不能重载成全局函数
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
默认生成的赋值重载跟拷贝构造行为一样:

  1. 内置类型成员 – 值拷贝/浅拷贝
  2. 自定义类型成员会去调用他的赋值重载

所以和拷贝构造一样,有些情况需要自己写,有些情况则不需要

知识总结实现一个日期类

Date.h

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

class Date
{
public:

	Date(int year = 1, int month = 1, int day = 1);

	//成员函数加上const之后,普通对象和const对象都可以调用
	//只要成员函数内部不修改成员变量,都应该加上const
	//这样const对象和普通对象都可以调用
	void Print() const
	{
		cout << _year << "." << _month << "." << _day << endl;
	}

	bool operator<(const Date& d2) const;
	bool operator==(const Date& d2) const;
	bool operator<=(const Date& d2) const;
	bool operator>(const Date& d2) const;
	bool operator>=(const Date& d2) const;
	bool operator!=(const Date& d2) const;
	
	int GetMonthDay(int year, int month);
	//日期+=天数/日期+天数
	Date& operator+=(int day);
	Date operator+(int day) const;

	//日期-=天数/日期-天数
	Date& operator-=(int day);
	Date operator-(int day) const;

	//注意前置++和后置++
	Date& operator++();
	Date operator++(int);

	//注意前置--和后置--
	Date& operator--();
	Date operator--(int);
	
	//日期-日期
	int operator-(const Date& d) const;
private:
	int _year;
	int _month;
	int _day;
};

Date.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"

Date::Date(int year, int month, int day)
{
	if(month > 0 && month < 13
		&&day > 0 && day <= GetMonthDay(year,month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法日期" << endl;
		assert(false);
	}
}

bool Date::operator<(const Date& d2) const
{
	if (_year < d2._year)
	{
		return true;
	}
	else if (_year == d2._year && _month < d2._month) 
	{
		return true;
	}
	else if (_year == d2._year && _month == d2._month && _day < d2._day)
	{
		return true;
	}

	return false;
}

bool Date::operator==(const Date& d2) const
{
	return _year == d2._year
		&& _month == d2._month
		&& _day == d2._day;
}

bool Date::operator<=(const Date& d2) const
{
	//复用小于或等于
	return *this < d2 || *this == d2;
}

bool Date::operator>(const Date& d2) const 
{
	//<=(结果)取反
	return !(*this <= d2);
}

bool Date::operator>=(const Date& d2) const
{
	//<(结果)取反
	return !(*this < d2);
}

bool Date::operator!=(const Date& d2) const
{
	//==(结果)取反
	return !(*this == d2);
}

int Date::GetMonthDay(int year, int month)
{
	//函数需要频繁调用,直接创建静态数组就不需要每次都创建数组了
	static int daysArr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
	{
		return 29;
	}
	else
	{
		return daysArr[month];
	}
}

Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= abs(day);
	}

	_day += day;
	//判断是否大于当前月的天数
	while (_day > GetMonthDay(_year, _month))
	{
		//如果大于,减去当前月的天数
		_day -= GetMonthDay(_year, _month);
		++_month;
		
		//判断月是否大于12
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}
	
	return *this;
}

Date Date::operator+(int day) const
{
	Date tmp(*this);
	tmp += day;

	return tmp;
}

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += abs(day);
	}

	_day -= day;
	//判断天数是否小于0
	while (_day <= 0)
	{
		//注意借位借的是上一个月的
		_month--;
		//判断月是否为0月
		if (_month == 0)
		{
			_month = 12;
			_year--;
		}

		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

Date Date::operator-(int day) const
{
	Date tmp = *this;
	tmp -= day;

	return tmp;
}

//前置++
Date& Date::operator++()
{
	//前置++返回++以后的对象
	//后置++返回++之前的对象
	*this += 1;

	return *this;
}

//后置++
//增加int参数是为了占位,跟前置++构成重载
//编译器遇到++会转换:
//++day1 -> d1.operator++()
//day1++ -> d1.operator++(0)
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;

	return tmp;
}

Date& Date::operator--()
{
	*this -= 1;

	return *this;
}

//这里的int和++同理
Date Date::operator--(int)
{
	Date tmp = *this;
	*this -= 1;

	return tmp;
}

int Date::operator-(const Date& d) const
{
	//默认左边大
	Date max = *this;
	Date min = d;
	int flag = 1;

	//如果是右边大,那么减出来的数为负数
	//需要乘-1
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}

	return n * flag;
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"

int main()
{
	Date day1(2023, 11, 7);
	Date ret1 = day1++;
	ret1.Print();
	day1.Print();
	
	Date day2(2023, 11, 7);
	day2 += 100;
	day2.Print();
	day2 -= 100;
	day2.Print();
	
	return 0;
}

运行效果:
在这里插入图片描述
剩下的功能由大家自己去测试了


番外(流插入和流提取)

为什么流插入/流提取可以自动识别类型呢?
其实是库里面实现了,可以自动识别类型是因为函数重载
来源cpluscplus


//该函数在成员函数里面
void Date::operator<<(ostream& out)
{
	out << _year << "年" << _month << "月" << _day << "日" << endl;
}

int main()
{
	Date day1(2023, 11, 7);
	day1 << cout; // day1.operator<<(cout);
	return 0;
}

这样就可以对自定义对象进行打印了,但是打印的时候会不会有些别扭?怎么是反过来的,原因是流插入不能写成成员函数(写成成员函数也能正常运行,但是用起来会特别别扭),只能写全局

流插入是双操作数的
流插入不能写成成员函数
因为Date对象默认占用了第一个参数做了左操作数
如果在成员函数,那么写出来就是下面这样,能正常运行,但是不符合使用习惯
day1 << cout; // 转换day1.operator<<(cout);

但是写在全局我们如何访问私有的内容呢?
有2种方法,一种是写几个共有的成员函数来获取私有的数据如下,另一种方法是使用友元函数(下篇会讲解,这里先做演示)
Date.cpp

//注:ostream不能加const,因为流插入是在往out里插入东西,const就不能插入了
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;

	return out;
}

//流提取两边都不能加const
istream& operator>>(istream& in, Date& d)
{
	int year, month, day;
	in >> year >> month >> day;

	if (month > 0 && month < 13
		&& day > 0 && day <= d.GetMonthDay(year, month))
	{
		d._year = year;
		d._month = month;
		d._day = day;
	}
	else
	{
		cout << "非法日期" << endl;
		assert(false);
	}

	return in;
}

Date.h

//Date.h
class Date
{
	//友元函数声明
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:

	Date(int year = 1, int month = 1, int day = 1);

	void Print() const
	{
		cout << _year << "." << _month << "." << _day << endl;
	}

	bool operator<(const Date& d2) const;
	bool operator==(const Date& d2) const;
	bool operator<=(const Date& d2) const;
	bool operator>(const Date& d2) const;
	bool operator>=(const Date& d2) const;
	bool operator!=(const Date& d2) const;
	
	int GetMonthDay(int year, int month);
	//日期+=天数/日期+天数
	Date& operator+=(int day);
	Date operator+(int day) const;

	//日期-=天数/日期-天数
	Date& operator-=(int day);
	Date operator-(int day) const;

	//注意前置++和后置++
	Date& operator++();
	Date operator++(int);

	//注意前置--和后置--
	Date& operator--();
	Date operator--(int);
	
	//日期-日期
	int operator-(const Date& d) const;

private:
	int _year;
	int _month;
	int _day;
};
//需要返回cout才能连续流插入
ostream& operator<<(ostream& out,const Date& d);
//流提取两边都不能加const
istream& operator>>(istream& in, Date& d)

test.cpp

int main()
{
	Date day1(2023, 11, 7);
	cout << day1;//operator<<(cout,day1);

	Date day2(2023, 11, 8);
	Date day3(2023, 11, 9);
	Date day4(2023, 11, 10);
	cout << day2 << day3 << day4;

	cin >> day1 >> day2;
	cout << day2 << day1;

	return 0;
}

运行效果:
在这里插入图片描述


以上就是本篇文章的全部内容了,希望大家看完能有所收获

❤️创作不易,点个赞吧❤️

相关推荐

  1. C++对象()

    2024-06-13 06:18:06       42 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-13 06:18:06       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-13 06:18:06       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-13 06:18:06       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-13 06:18:06       20 阅读

热门阅读

  1. PHP框架详解 - symfony框架

    2024-06-13 06:18:06       8 阅读
  2. Mysql union语句

    2024-06-13 06:18:06       6 阅读
  3. 苹果宣布iOS18开始深度集成AI

    2024-06-13 06:18:06       9 阅读
  4. Axios 二次封装详解

    2024-06-13 06:18:06       7 阅读
  5. vscode不能进行go跳转

    2024-06-13 06:18:06       9 阅读