C++类与对象(中)【详细】

类与对象(中)

类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

特殊成员函数,我们不写,编译器会自己生成一个;我们自己写了,编译器就不会生成

class Date();

image-20230228190024031


Type One:初始化和清理

1.构造函数:初始化

C语言使用时,经常忘记Init(初始化)就使用,忘记Destroy

//原来我们需要使用下面方式来初始化
Class Date
{
    
    
    public:
    void Init(int year,int month,int day)
    {
    
    
        _year=year;
        _month=month;
        _day=day;
    }
}

能不能在对象定义的时候就将其初始化呢?—— 构造函数

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

其特征如下:

1. 函数名与类名相同。
2. 无返回值。(void都不用写)
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载:提供多种构造函数,多种初始化方式

构造函数的作用是初始化,具体表现如下:

Class Date
{
    
    
    private:
    int _year;
    int _month;
    int _day;
    
    public:
    //初始化
    Date(int year,int month,int day)
    {
    
    
        _year=year;
        _month=month;
        _day=day;
    }  
}
int main()
{
    
    
    Date d1(2023,1,28);
    Date d2(2023,1,29);
    Date d3;
}

image-20230128144047990

当然,由于初始化代表的是日期,所以在初始化时我们检验输入的日期的合法性:

Date(int year = 1, int month = 1, int day = 1)
{
    
    
	_year = year;
	_month = month;
	_day = day;
	//检查日期是否合法
	if (!(year >= 1
		&& (month >= 1 && month <= 12)
		&& (day >= 1 && day <= GetMonthDay(year, month))))
	{
    
    
		cout << "非法日期" << endl;
	}
}

因此当我们输入两个非法日期后,会出现:

image-20230228141947268


再例如我们使用构造函数来实现栈时:

class Stack
{
    
    
    public:
    //栈的初始化(构造函数)
    Stack(int capacity=4)
    {
    
    
        _a=malloc(sizeof(int)*capacity);
        if(a==nullptr)
        {
    
    
            perror("malloc fail");
            exit(-1);
        }
        _top=0;
        _capacity=capacity;
    }
    //...
}

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

假如我们未定义构造函数,那么编译器自动生成结果如下:

image-20230130150959347

关于编译器生成的默认成员函数,对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用?

解答: C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如: int/char… ,自定义类型就是我们使用class/struct/union等自己定义的类型,对于默认生成的构造函数,不会对内置类型处理,对于自定义类型会调用它的默认构造。

因此总结一下:上述的Stack和Date的构造函数需要自己写,因为要按照自己的意愿进行初始化

2.析构函数:销毁

构造函数让我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

即与我们常用的Destory函数功能相似

其特征如下:

1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时(出作用域),C++编译系统系统自动调用析构函数。
//析构函数:类名前加上字符 ~
~Stack()
{
    
    
    free(_a);
    _capacity=0;
    _size=0;
}

因此总结一下:这里的Stack就需要自己写析构函数,释放对应空间

面向需求:编译器默认生成就可以满足,就不用自己写,不满足就需要自己写

注意1:

C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值

例如:

class MyQueue {
    
    
public:
	void push(int x)
	{
    
    
		_pushST.Push(x);
	}
public:
	Stack _pushST;   //自定义类型
	Stack _popST;    //自定义类型
	size_t _size;    //内置类型;unsigned int
};

初始化为0:

image-20230226194154483

注意2:

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

例如:

//无参的构造函数
Date()
{
    
    
	_year = 1;
	_month = 2;
	_day = 3;
}
//全缺省的构造函数
Date(int year = 1, int month = 1, int day = 1)
{
    
    
	_year = year;
	_month = month;
	_day = day;
}

指定了多个默认构造:

image-20230226195616043

不传参数就可以调用的构造函数就叫默认构造

我们将无参构造函数去掉:

image-20230226200257625

这里的Date(int year , int month , int day)构造函数需要传递参数,由上述定义可知,我们自定义了构造函数------>系统不会生成构造函数------>需要传递参数------>不是默认构造函数,因此会出现这类错误

Type Two:拷贝复制

3.拷贝构造函数

引入:

int main()
{
    
    
    Date d1(2023226);   //构造
    //拷贝一份d1
    Date d2(d1);           //拷贝构造 -- 拷贝初始化
}

拷贝构造函数也是特殊的成员函数,其特征如下:

1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。

例如,我们定义Date类,通过拷贝构造函数实现如下:

class Date
{
    
    
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}

	// Date(Date d)   // 错误写法
	Date(Date& d)   // 正确写法:编译报错,会引发无穷递归
	{
    
    
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
public:
	int _year;
	int _month;
	int _day;
};
int main() {
    
    
	Date d1(2023, 2, 27);
	Date d2(d1);                  //拷贝构造 -- 拷贝初始化
	cout << d1._year << "," << d1._month << "," << d1._day << endl;
	cout << d2._year << "," << d2._month << "," << d2._day << endl;
	return 0;
}

image-20230227093629731

理解:拷贝构造函数的参数只有一个且必须是类类型对象的引用

image-20230227094533687

如上述程序,我们定义func1和func2两个函数,分别是传值传参(形参是实参的拷贝)和引用传参(形参是实参的别名),因此在每次传值传参时会调用拷贝构造,会引起无穷递归

image-20230227131141535

拷贝构造------>调用该函数------>传参------->传参又是一个拷贝构造------>…(无穷递归)

注意1:

形参加const,防止写反,即:

Date(const Date& d)   // 正确写法:编译报错,会引发无穷递归
	{
    
    
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

注意2:

在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

①对于自定义类型Date,默认拷贝构造如下:

image-20230227131841216

该默认拷贝构造函数实现了我们的目标

②对于自定义类型Stack(栈),默认拷贝构造如下:

image-20230227132209302

这里为什么报错呢,是因为在st1结束后会自动调用析构函数,而在调用完析构函数后,由于st2是st1的拷贝它们指向同一块空间,因此此时st2所指向的区域已被销毁形成野指针,此时默认生成的拷贝构造函数不能实现了我们的目标

编译器默认生成的拷贝构造函数为:浅拷贝

深拷贝

为了解决浅拷贝在某类场景下不适用的情况,我们引用的深拷贝,以Stack为例,如下:

//st2(st1)
Stack(const Stack& st)
{
    
    
	//开一个与st1相同大小的空间
	_a = (int*)malloc(sizeof(int) * st._capacity);
	if (_a == nullptr)
	{
    
    
		perror("malloc fail");
		exit(-1);
	}
	//拷贝该空间上的值
	memcpy(_a, st._a, sizeof(int) * st._top);
	_top = st._top;
	_capacity = st._capacity;
}

可表示为下图:

image-20230227133220948

总结:

需要些析构函数的类,都需要写深拷贝的拷贝构造(Stack)

不需要写析构函数的类,默认生成的浅拷贝的拷贝构造就可以用(date)


4.赋值运算符重载

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

函数名字为:关键字operator后面接需要重载的运算符符号

函数原型: 返回值类型 operator操作符(参数列表)

示例1:

例如我们在Date类中比较两个日期是否相等:

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

即:

image-20230227135245332

这里的:

d1==d2;    //编译器会转换成operator==(d1,d2);

上述对于比较的结果,可更改为:(注意运算符的优先级)

image-20230227135706111


问题:若属性是私有的,怎么访问呢?

image-20230227135933445

方式一:设置一个共有的get方法

方式二:放到类里面

//更改如下:
bool operator==(const Date& d)
{
    
    
    return _year==d._year
        &&_month==d._month
        &&_day==d._day;
}

示例2:

我们再来尝试Date类日期间大于小于的比较:

//放在类里面的
bool operator>(const Date& d)
{
    
    
	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;
}

示例3:

Date类日期间的大于等于:

bool operator>=(const Date& d)
{
    
    
    //this代表d1的地址,因此对this解引用即指向d1
    //复用上述已经书写的大于和等于的判定
    return *this>d||*this==d;
}

示例4:

日期加天数怎么办呢?(例如算某天过100天之后的日期)

函数名:operator+=
参数:(int day)
返回类型:Date

得到每个月的天数;天满了进月,月满了进年

Step1:得到每个月的天数(判定是平年还是闰年)

int GetMonthDay(int year,int month)
{
    
    
    static int monthDayArray[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 monthDayArray[month];
    }
}

Step2:加上天数

Date operator+=(int day)
{
    
    
 	_day+=day;
    //日进
    while(_day>GetMonthDay(year,month))
    {
    
    
        _day-=GetMonthDay(year,month);
        _month++;
        //月进
        if(_month==13)
        {
    
    
            _year++;
            _month=1;
        }
    }
    return *this;
}

注意这里*this的作用

因此结果如下:

image-20230227170645274

很巧妙的是,在编写这篇文章时(2023年2月27日),再加100天就是6月7日,恰好是今年高考的百日誓师!

我们上述所书写的operator+、operator>等函数功能,我们依然可以通过定义普通的函数名实现,那么我们定义运算符重载的意思是什么呢?

运算符重载的真正意义是增加可读性!

注意:

1.不能通过连接其他符号来创建新的操作符:比如operator@

2.重载操作符必须有一个类类型参数(针对自定义类型的)

3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义

4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

5.image-20230228181157954注意以上5个运算符不能重载

5.赋值重载

区分:

Date d2(d1);   //拷贝构造(本质是初始化)   一个初始化另一个马上要创建的对象
d3=d1;         //赋值重载(复制拷贝)      已经存在两个对象直接的拷贝

6.日期类的实现

在上述的例子中,日期类出现的频率极高,可见它是一个非常经典的例子,因此我们在此将日期类的全部用法实现,具体如下:

✍首先,由于日期类所涉及到的函数接口较多,因此我们定义Date.h头文件包含日期类的接口,而Date.cpp文件为我们具体实现日期类的方式

为什么这里我们只在Date.h中提供接口呢(声明)?

答:全局函数在.h文件中会导致重定义,是由于Date.cpp和Test.cpp文件包含了Date.h,因此定义在.h中会在符号表拷贝多次,自己与自己发生重定义,因此才将声明与实现分离;或者置为静态的(static)

.h文件中尽量不含全局变量和函数,否则会出现链接问题

声明:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class Date
{
    
    
	//友元声明(类中的任意位置)
	friend inline ostream& operator<<(ostream& out, const Date& d);
	friend inline istream& operator>>(istream& in, Date& d);
private:
	int _year;
	int _month;
	int _day;
public:
	// 获取某年某月的天数
	int GetMonthDay(int year, int month);

	// 全缺省的构造函数
	Date(int year = 2000, int month = 1, int day = 1);
	// 拷贝构造函数
	// d2(d1)
	Date(const Date& d);
	//日期类打印
	void Print() const;
	// 赋值运算符重载
	// d2 = d3 -> d2.operator=(&d2, d3)
	Date& operator=(const Date& d);
	// 析构函数
	~Date();
	// 日期+=天数
	Date& operator+=(int day);
	// 日期+天数
	Date operator+(int day) const;
	// 日期-天数
	Date operator-(int day) const;
	// 日期-=天数
	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) const;
};

// 流插入的重载
inline ostream& operator<<(ostream& out, const Date& d)
{
    
    
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

// 流提取的重载
inline istream& operator>>(istream& in, Date& d)
{
    
    
	in >> d._year >> d._month >> d._day;
	return in;
}

实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
using namespace std;

//1.月份内的天数
int Date::GetMonthDay(int year, int month)
{
    
    
	int monthDay[] = {
    
    
		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 monthDay[month];
	}
}

//2.构造函数
//这里最好写一个全缺省的构造函数,这样即使不传参对象也会被初始化
Date::Date(int year, int month, int day)
{
    
    
	//这里的缺省值已经在声明的时候给出了
	_year = year;
	_month = month;
	_day = day;
}

//3.拷贝构造函数
//拷贝构造函数必须是传值引用传参,并且最好加上const防止拷贝的时候实参被改变
//这里的日期类可以不用书写,编译器会自动给出
Date::Date(const Date& d)
{
    
    
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

//4.析构函数
Date::~Date()
{
    
    
	_year = 0;
	_month = 0;
	_day = 0;
}

//5.打印日期
void Date::Print() const
{
    
    
	cout << _year << "/" << _month << "/" << _day << endl;
}

//6.赋值重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& Date::operator=(const Date& d)
{
    
    
	_year = d._year;
	_month = d._month;
	_day = d._day;

	return *this;
}

//7.日期+=天数
//这个属于赋值运算符重载,由内置类型+=符号运算,会改变左操作数的值;
//因此在返回时我们直接返回运算之后的对象即可
Date& Date::operator+=(int day)
{
    
    
	if (day < 0)
	{
    
    
		return *this -= -day;
	}
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
    
    
		_day -= GetMonthDay(_year, _month);
		_month++;

		if (_month > 12)
		{
    
    
			_month = 1;
			_year++;
		}
	}
	return *this;
}

//8.日期+天数
//复用上述日期+=天数即可
//需要注意这里不会改变日期原本的值
//而是返回一个临时产生的对象
Date Date::operator+(int day) const
{
    
    
	Date tmpObject(*this);     //产生临时对象
	return tmpObject += day;   //复用
}

//9.日期-=天数
//同理,会改变日期,直接返回即可
Date& Date::operator-=(int day)
{
    
    
	if (day < 0)
	{
    
    
		return *this += -day;
	}
	_day -= day;
	while (_day <= 0)
	{
    
    
		_month--;
		_day += GetMonthDay(_year, _month);
		//月份减完年减一
		if (_month < 1)
		{
    
    
			_month = 12;
			_year--;
		}
	}
	return *this;
}

//10日期-天数
//同理,复用日期-=天数即可
Date Date::operator-(int day) const
{
    
    
	Date tmpObject(*this);
	return tmpObject -= day;
}

//11.==运算符重载
//意思是判断日期是否相等,返回的是0/1(bool型)
bool Date::operator==(const Date& d) const
{
    
    
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

//12.>运算符重载
//比较日期的大小:先比年-年相等比月-年月相等比日
bool Date::operator>(const Date& d) const
{
    
    
	if (_year > d._year)
	{
    
    
		return true;
	}
	else if (_year == d._year)
	{
    
    
		if (_month > d._month)
		{
    
    
			return true;
		}
		else if (_month == d._month)
		{
    
    
			if (_day > d._day)
			{
    
    
				return true;
			}
		}
	}
	return false;
}

//13.>=、<、<=等运算符重载
//均可以通过复用>、==来实现
bool Date::operator>=(const Date& d) const
{
    
    
	return *this > d || *this == d;
}

bool Date::operator<(const Date& d) const
{
    
    
	return !(*this > d);
}

bool Date::operator<=(const Date& d) const
{
    
    
	return !(*this > d || *this == d);
}

bool Date::operator!=(const Date& d) const
{
    
    
	return !(*this == d);
}

//14.日期-日期
//日期-日期是有意义的
//首先需要区分日期间的大小日期,我们直接小的日期每次循环+1天
//循环次数即为间隔天数
int Date::operator-(const Date& d) const
{
    
    
	//默认是当前对象日期大
	//很经典的找大的方法:先假设
	Date min = d;
	Date max = *this;
	int flag = 1;
	if (*this < d)
	{
    
    
		max = d;
		min = *this;
		flag = -1;
	}

	//计算相差天数
	int count = 0;
	while (min != max)
	{
    
    
		min++;
		count++;
	}

	return count * flag;
}

//15.前置和后置++/--
//前置++,先加再用,因此直接对该对象加减即可
Date& Date::operator++()
{
    
    
	//引用返回
	return *this += 1;
}
//后置++,先用再加,返回加之前的值
//规定:后置++多一个参数,是为了与前置++做出区分,构成重载,无其它作用
Date Date::operator++(int)
{
    
    
	Date tmpObject(*this);
	*this += 1;
	//传值返回
	return tmpObject;
}
// 前置--:返回减后的值
Date& Date::operator--()
{
    
    
	return *this -= 1;
}
//后置--:后置会多两次拷贝
Date Date::operator--(int)
{
    
    
	Date tmpObject(*this);
	*this -= 1;

	return tmpObject;
}

7.const成员

凡是内部不改变成员变量,其实也是*this对象数据的,这些成员函数都应该加const

Type Three:取地址重载

8.取地址重载

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

class Date
{
    
    
public:
	Date* operator&()
	{
    
    
		return this;
	}

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

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

特殊需求:要求这个类的对象不让取地址

Date* operator&()
{
    
    
	return nullptr;
}

const Date* operator&() const
{
    
    
	return nullptr;
}

猜你喜欢

转载自blog.csdn.net/kevvviinn/article/details/129537734
今日推荐