【C++】类和对象——中

目录

前言

一、构造函数

二、析构函数

三、构造函数和析构函数的调用顺序

​编辑

五、运算符重载函数

1、=运算符重载

2、==运算符重载

​编辑

​编辑

六、七、取地址重载函数

总结


前言

成员函数是C++ 的类和结构体的一个重要特性。这些数据类型可以包含作为其成员的函数。成员函数分为静态成员函数与非静态成员函数。静态成员函数只能访问该数据类型的对象的静态成员。而非静态成员函数能够访问对象的所有成员。在非静态成员函数的函数体内,关键词this指向了调用该函数的对象。这通常是通过thiscall调用协议,将对象的地址作为隐含的第一个参数传递给成员函数。

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

一、构造函数

构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。

构造函数是一种特殊的成员函数,构造函数的主要任务是初始化对象,而不是开辟空间来创建对象

创建对象主要是由操作系统来完成,以普通的局部变量为例。

创建一个对象要在栈上建立栈帧,用ebp和esp两个与栈帧的创建和维护的寄存器来维护栈帧。

编译器预先计算栈空间大小,提前预留足够的空间,ebp和esp声明这块空间已经被使用。当对象销毁时,esp和ebp向上移动。所谓的空间销毁并不是真的销毁这块空间,而是声明这块空间不能再被使用。

而初始化对象是对象已经创建好了,我们给它赋值。就好比int a = 10;给a初始化赋值为10。

1.构造函数的函数名与类名相同。

2.无返回值。

3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。
5.如果类中没有显式定义构造函数,C++编译器会自动生成一个无参的默认构造函数,一旦
用户显示定义,编译器将不再生成
在日常实例化对象时,我们要将对象初始化,可能会忘记!导致崩溃出现随机值
为了保证对象一定初始化,我们可以使用构造函数。
一般的类都不会让编译器默认生成构造函数,都会自己写,显示写一个全缺省的构造函数就可以了
特殊情况下才会默认生成。
例如。我们用两个栈实现一个队列,队列这个类中的成员变量是两个栈类型的变量,
队列就可以直接用编译器默认提供的构造函数就可以了。前提是栈的构造函数已经写好了。

C++中,数据类型的分类:

内置类型/基本类型:int / double / char /long / 各种指针(包括自定义类型的指针)

自定义类型:struct / class / union / enum ……

C++的默认构造函数对内置类型是不会进行处理的,自定义类型回去调用它的默认构造函数

#include<iostream>
using namespace std;

class Date
{
public:
	void Show()
	{
		cout << _year << "/ " << _month << "/ " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void TestDate()
{
	Date d1; 
	d1.Show();
}

int main()
{
	TestDate();
	return 0;
}

将日期类的数据打印出来是随机值。

既然说到了默认构造函数,那么什么是默认构造函数?

 我们自己不写,编译器帮我们自动生成的。还有无参的构造函数和全缺省的构造函数都被称为默认构造函数,并且默认构造函数只能有一个。

因为编译器自动生成的和我们写好的必然只能有一个,无参的和全缺省的构造函数虽然构成函数重载,并且语法上是支持这种写法的。但是当我们实例化对象时,不传参数时,编译器无法确定到底要调用哪个函数会出现

代码如下:

#include<iostream>
using namespace std;

class Date
{
public:
	Date()
	{
		_year = 1970;
		_month = 1;
		_day = 1;
	}

	Date(int year = 1970, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Show()
	{
		cout << _year << "/ " << _month << "/ " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void TestDate()
{
	Date d1; 
	d1.Show();
}

int main()
{
	TestDate();
	return 0;
}

一般情况下,对象初始化惯例分为两种,默认值初始化,给定值初始化

我们合二为一,给一个全缺省的构造函数。

我们会发现C++中的构造函数及其诡异,在C++11中,打了一个补丁,使我们能够方便一点来定义构造函数

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

	void Show()
	{
		cout << _year << "/ " << _month << "/ " << _day << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

注意这里面不是初始化,这里是我们给它缺省值,因为这是类的声明部分,声明没有开辟空间,自然不可能是定义。

总结:默认构造函数,不传参就可以调用

我们自己不写 :编译器自动生成

我们自己写的 :无参构造函数

我们自己写的 :全缺省构造函数

二、析构函数

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

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

析构函数与构造函数类似,对于内置类型并不处理,对于自定义类型去调用自定义类型的析构函数。

一些类需要显示的写析构函数。比如Stack Queue……

一些类并不需要显示的写出析构函数。比如:没有动态开辟的类。或者是类中的成员变量是自定义类型。

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

	void Show()
	{
		cout << _year << "/ " << _month << "/ " << _day << endl;
	}

    ~Date()
    {
        cout << "this is ~Date" << endl;
    }

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

因为Date并不需要我们显示的写出析构函数,我们只是打印出一句话来证明已经调用了析构函数。

三、构造函数和析构函数的调用顺序

我们知道一个对象的生成要调用构造函数,销毁调用析构函数。但是多个对象创建时的构造函数和析构函数的顺序呢?

#include<iostream>
using namespace std;

class A
{
public:
	A(int a)
	{
		_a = a;
		cout << "A(int a)->" << _a << endl;
	}
	~A()
	{
		cout << "~A()->" << _a << endl;
	}
private:
	int _a;
};


A aa3(3);

void test()
{
	static A aa0(0);
	A aa1(1);
	A aa2(2);
	static A aa4(4);
}

int main()
{
	test();
	return 0;
}

 

我们以这个例子为例:

先来说明构造函数的顺序。第一个创建的是aa3,因为aa3是全局对象,在编译阶段,地址就以确定,就已经成功创建了。而其它几个对象需要test函数建立栈帧,然后创建。

所以接下来是aa0,aa1,aa2,aa3。因为我们只是创建了对象,并没有使用它,所以很快对象就销毁了,我们还是要明确一点,具有相同生命周期的对象,构造函数和析构函数调用的顺序相反。

又因为aa4定义为static,它没有存储在test函数的栈帧,而是储存在静态区,所以它不是第一个析构。aa2和aa1析构之后,test函数栈帧销毁,就来到了main函数,因为static修饰的变量生命周期变长,所以和aa3这个全局对象的声明周期一样长aa4是后构造的所以先析构。

四、拷贝构造函数

复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用。

复制构造函数的参数可以是 const 引用,也可以是非 const 引用。 一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的。

如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等。编译器自动生成的复制构造函数称为“默认复制构造函数”。

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

A(A aa)
	{
		_a = A._a;
	}

至于为什么会出现无穷递归也是比较好理解的;

我们要想先调用拷贝构造函数,首先要传参,而传参又要调用拷贝构造函数。它们两者相互调用构成递归。当我们传引用时,传递的是对象的别名,对象已经调用过构造函数了,所以不会出现,无穷递归。

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

所谓的浅拷贝是指如果类中有指针类型,编译器会自动生成一个与要拷贝的类的指针指向同一块空间的指针。如果前一个对象修改内容,拷贝之后的对象也会跟着修改,这样就不叫做拷贝了。并且如果前一个对象销毁了,拷贝对象接着使用已经销毁的空间,出现野指针问题。

浅拷贝也就是一个对象修改会影响另一个对象,前一个对象销毁,会造成同一块空间析构两次,程序崩溃。解决办法是自己实现深拷贝。

拷贝构造函数典型调用场景:

使用已存在对象创建新对象

函数参数类型为类类型对象

函数返回值类型为类类型对象

五、运算符重载函数

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
第一个运算符不是 . 而是.*  用来矩阵运算的
我们知道一件事:内置类型可以直接使用运算符运算,因为编译器知道要如何计算
自定义类型无法直接使用运算符运算。编译器不知道要如何运算,为了能够支持运算,需要我们自己实现运算符重载

1、=运算符重载

我们以时间类为例,我们要重载赋值操作符=,使两个Date类类型能够直接赋值
同时要避免,自己给自己赋值
我们在类内实现,操作符重载,传入的第一个参数是左操作数,传入的第二个操作数是右操作数
在类中实现,我们知道会隐式的传入一个this指针,我们只要在传入一个类类型的对象就可以了,又因为类类型对象一般比较大,所以传引用,并且为了防止修改对象,加上const来修饰。
同时要返回一个类类型的引用,为了能够实现,链式访问,连续赋值。
Date& Date::operator=(const Date& d)
{
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;
}

总结:

参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义

同时赋值操作符重载要在类内部,不能重载成全局函数

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的
赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的
成员函数

2、==运算符重载

我们已经重载过一个运算符,对于==来说就得心应手了
两个日期相等的条件是 年和年相等,月和月相等,日和日相等。
只要有一个不相等就是不相等
bool Date::operator==(const Date& d)
{
	return _year == d._year && _month == d._month && _day == d._day;
}

3、>运算符重载

一个日期大于另一个日期的条件是 年大的就大,年相等的,月大的就大,月相等的看天数

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

4、<运算符重载

为了增强代码复用性,小于实际上就是,不大于和等于。而对于日期类的大于和等于我们已经实现好了,直接使用即可。

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

5、>=运算符重载

大于等于就是小于的对立面

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

6、!=运算符重载

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

7、+=运算符重载

对于日期类来说,日期加日期是没有意义的,只有日期加天数有意义,而日期加天数并不是直接相加的,还要考虑每个月的天数,是否是平年闰年。我们将获取每个月的天数封装成一个函数

// 获取某年某月的天数
	int GetMonthDay(int year, int month)
	{
		static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		int day = days[month];
		if (month == 2
			&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
		{
			day += 1;
		}
		return day;
	}

同时,日期加上一个负数就相当与减去一个日期

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 == 13)
		{
			_month = 1;
			_year++;
		}
	}
	return *this;
}

8、+操作符重载

+操作符就是返回相加之后的对象,而原对象不发生改变

这时我们需要利用拷贝构造函数来创建一个临时变量,将临时变量利用+=操作符改变之后,将其返回。

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

9、-=操作符重载

-=的重载思路与+=类似,都是先让day减去天数,然后改变月份和年份

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += (-day);
	}
	_day -= day;
	while (_day <= 0)
	{
		_month--;
		if (_month == 0)
		{
			_month = 12;
			_year--;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

这里进入循环就进行--month是因为我们要向上一个月进行借位。循环条件是_day <= 0是因为,一个月不可能有0天。

10、++操作符重载

++分为前置++和后置++,前置++比较容易实现,直接天数相加然后返回就可以,而后置++返回的是++之前的值,然后它在加一,后置++需要进行拷贝,返回拷贝即可

又因为前置++后置++的操作符都是++,编译器无法区分,因此规定后置++的形参列表有一个int标记。

前置++与后置++构成了函数重载

//前置++
Date& Date::operator++()
{
	return *this += 1;
}
//后置++
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}

11、--操作符重载

--与++类似

Date Date::operator--(int)
{
	Date tmp = *this;
	*this -= 1;
	return tmp;
}

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

12、日期相减返回天数

我们可以铆钉一个基准值例如1月1日,然后分别从1月1日到两个日期所间隔的天数。

但是在我们已经实现了多个运算符重载的条件下,我们只要让小的日期加到大的日期,计算加的次数就可以了。

//日期相减返回天数
int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (max < min)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int day = 0;
	while (min < max)
	{
		++min;
		++day;
	}
	return flag * day;
}

13、友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多
用。
友元分为:友元函数和友元类
友元函数
问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对
象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用
中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办
法访问成员,此时就需要友元来解决。operator>>同理。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声
明,声明时需要加friend关键字
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同

14、>> <<操作符重载

我们想让Date类像内置类型一样可以使用cout直接打印出,我们想到了将<<操作符重载

要想重载<<我们要了解一下cout和cin是什么

cin使istream的全局对象,cout是ostream的全局对象

cin和cout能够对内置类型直接使用是因为:库里面写好了运算符重载,自动识别类型是因为它们构成了函数重载。

运算符重载:让自定义类型对象可以用运算符,转换成调用这个重载函数

函数重载:支持函数名相同的函数同时存在

运算符重载和函数重载虽然都用了重载这个词,但是它们之间并没有什么必然联系。

 

我们在类内实现

ostream& operator<<(ostream& out)
	{
		out << _year << "/ " << _month << "/ " << _day;
		return out;
	}

 这是我们在类中实现的<<重载,乍一看是没有什么问题,当我们调用时就会出现这种情况


	Date d(2022, 7, 25);
	cout << d << endl;

 这是什么情况?

我们以它的原生的方式调用

Date d(2022, 7, 25);
d.operator<<(cout);

发现什么错误也没有。

我们反向调用试一下

Date d(2022, 7, 25);
d << cout;

 以这种诡异的方式<<重载竟然调用成功了

 

流提取运算符和流插入运算符都要在全局实现

因为操作符重载,第一个参数是左操作数,第二个参数是右操作数。而<< 和>>操作符都是作为左操作数使用的,在类中它们被当作右操作数使用所以出现了这种情况。


inline ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "/ " << d._month << "/ " << d._day;
	return out;
}

inline istream& operator>>(istream& in, Date& d)
{
	cin >> d._year >> d._month >> d._day;
	return in;
}

使用inline来修饰<<和>>重载,因为这两个重载函数调用次数比较频繁

六、七、取地址重载函数

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this
指针,表明在该成员函数中不能对类的任何成员进行修改。
Date* operator&()
	{
		return this;
	}

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

它们是默认成员函数,我们不写编译器会自动生成,自动生成的就够用了,所以一般不需要我们自己写。

我们知道this指针是 Date* const this 它是为了防止将this的指向改变,而我们在函数后面加入的const是为了防止this指针解引用之后的值修改,也就是为了防止成员变量的值被修改而设立的。


总结

以上就是类和对象中篇的内容。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值