【入土级】详解C++类&对象(中篇)

目录

前言:类的6个默认成员函数

一, 构造函数

1. 概念

2. 特性

二, 析构函数

2.1 概念

2.2 特性

2.3 牛刀小试 

三, 拷贝构造函数

3.1概念

3. 2 特点

四, 赋值运算符重载

4. 1 运算符重载 

五, const成员函数

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

七,练习——实现日期计算器

结语


前言:类的6个默认成员函数

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

 

一, 构造函数

 1. 概念

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

2. 特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务 并不是开空间创建对象,而是初始化对象
其特征如下:
  • 1. 函数名与类名相同
  • 2. 无返回值
  • 3. 对象实例化时编译器自动调用对应的构造函数。
  • 4. 构造函数可以重载。(后面拷贝构造函数会体现)
  • 5. 如果存在未自定义默认构造函数,编译器不再生成默认构造函数。 
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写,编译器默认生成的构造函数,都可以认为是默认构造函数。

下面是一段自定义构造函数的:

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 2023, int month = 5, int day = 9) // 自定义默认构造函数,设置全缺省参数,对数据进行初始化。
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

int main()
{
	Date z;
	Date z1(2012, 12, 12); // 由于我们自定义构造函数支持带参数设置数据初始化。
	// 接下来,我们注释掉自定义默认构造函数,来测试一下编译器默认自动生成的构造函数。
}

 当我们注释掉我们自定义的构造函数时,我们会发现z对象 的类成员变量,依旧是随机值。这里我们不禁会想,编译器自动生成的默认构造函数似乎什么也没做??

解答:C++把类型分成内置类型(基本类型)和自定义类型。
  • 内置类型就是语言提供的数据类型,如:int/char...;
  • 自定义类型就是我们使用class/struct/union等自己定义的类型。 
看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员 _t调用的它的默认成员函数。

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 2023, int month = 5, int day = 9) // 自定义默认构造函数,设置全缺省参数,对数据进行初始化。
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

class Time
{
public:
	Date _t;      // 自定义类型

private:          // 内置类型
	int _hour;
	int _minute;
	int _second;
};
int main()
{
	Time k;
}

调试结果如下: 

  特点:

1. 内置函数不做处理。

2. 自定义类型会调用(自定义类型的)默认构造函数。

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

这里我们就有了两种内置成员初始化的方法: 

  • 1. 通过C++补丁初始化;()
  • 2. 自定义默认构造函数,同时给缺省参数。

二, 析构函数

2.1 概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而 对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

2.2 特性

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

 首先我们来看下面代码:

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 2023, int month = 5, int day = 9)  // 默认构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}

	~Date()   // 默认析构函数
	{
		_year = _month = _day = 0;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date z;
	return 0;
}

我们将显式析构函数注释掉,让我们测试一下编译器自动生成的默认析构函数的结果,下面的程序我们会看到:编译器生成的默认析构函数,对自定类型成员调用它的析构函数。

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 2023, int month = 5, int day = 9)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	~Date()
	{ 
		cout << "~Date()" << endl;   // 对调用Date的析构函数进行标记
		_year = _month = _day = 0;
	}
private:
	int _year;
	int _month;
	int _day;
};

class Time
{
public:
	Date _t;      // 自定义类型
	~Time()
	{
		cout << "~Time()" << endl; // 对调用Time的析构函数进行标记
	}
private:          // 内置类型
	int _hour = 10;
	int _minute = 10;
	int _second = 10;
};

void func()
{
	Time z;
}
int main()
{
	func();
	return 0;
}

 结果可以看出:

2.3 牛刀小试 

看下面代码,输出顺序是什么? 

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

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

A aa3(3);

void f()
{
	static int i = 0;
	static A aa0(0);
	A aa1(1);
	A aa2(2);
	static A aa4(4);
}

// 构造顺序:3 0 1 2 4 1 2
// 析构顺序:~2 ~1 ~2 ~1 ~4 ~0 ~3
int main()
{
	f();
	f();
	return 0;
}

核心思路:遵循栈的思路,先进后出

三, 拷贝构造函数

 3.1概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。 

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

3. 2 特点

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

下面是会发生无穷递归代码:

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

	Date(Date b1)  // 正确代码:Date(Date& b1)
	{
		_year = b1._year;
		_month = b1._month;
		_day = b1._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

无穷递归如图:

 换成类类型引用即可解决无穷递归的问题。 

class Time
{
public:
	Time(int hour = 1 , int minute = 2, int second = 3)
	{
		_hour = hour;
		_minute = minute;
		_second = second;
	}

	Time(const Time& a)
	{
		_hour = a._hour;
		_minute = a._minute;
		_second = a._second;
		cout << "Time(Time& a)" << endl;
	}

	~Time()
	{
		cout << "~Time()" << endl;
	}
private:        
	int _hour;
	int _minute;
	int _second;
};

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

	Date(const Date& b1)  // 正确代码:Date(Date& b1)
	{
		_year = b1._year;
		_month = b1._month;
		_day = b1._day;
	}

	Time _t;
private:
	int _year;
	int _month;
	int _day;
};
void func()
{
	Date z;
	Date k(z);
}

int main()
{
	func();
	return 0;
}
在编译器生成的默认拷贝构造函数中,内置类型是按照 字节方式直接拷贝的,而自定义类型是调用其 拷贝构造函数完成拷贝的。

我们发现,编译器自动生成的拷贝构造函数,足够我们进行值拷贝,那么我们还需要自定义拷贝构造函数吗?

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
 Stack(size_t capacity = 10)
 {
 _array = (DataType*)malloc(capacity * sizeof(DataType));
 if (nullptr == _array)
 {
 perror("malloc申请空间失败");
 return;
 }
_size = 0;
 _capacity = capacity;
 }
 void Push(const DataType& data)
 {
 // CheckCapacity();
 _array[_size] = data;
 _size++;
 }
 ~Stack()
 {
 if (_array)
 {
 free(_array);
 _array = nullptr;
 _capacity = 0;
 _size = 0;
 }
 }
private:
 DataType *_array;
 size_t _size;
 size_t _capacity;
};
int main()
{
 Stack s1;
 s1.Push(1);
 s1.Push(2);
 s1.Push(3);
 s1.Push(4);
 Stack s2(s1);
 return 0;
}

所以类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,需要深度拷贝,则拷贝构造函数是一定要写的,否则就是浅拷贝

四, 赋值运算符重载

4. 1 运算符重载 

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

下面有有几个应用例子:

class Time
{
public:
	Time(int hour = 1 , int minute = 2, int second = 3)
	{
		_hour = hour;
		_minute = minute;
		_second = second;
	}

	Time(const Time& a)
	{
		_hour = a._hour;
		_minute = a._minute;
		_second = a._second;
		cout << "Time(Time& a)" << endl;
	}

	bool operator==(const Time& a)   
	{
		return _hour == a._hour &&
			_minute == a._minute &&
			_second == a._second;
	}

	bool operator>(const Time& a)
	{
		return _hour > a._hour &&
			_minute > a._minute &&
			_second > a._second;
	}

   Time& operator=(const Time& a)   // 内成员赋值运算符重载
	{
		_hour = a._hour;
		_minute = a._minute;
		_second = a._second;
		return *this;
	}
	
    ~Time()
	{
		cout << "~Time()" << endl;
	}
private:        // 内置类型
	int _hour;
	int _minute;
	int _second;
};

这里对赋值运算符进行补充;

1.赋值运算符只能重载成类的成员函数不能重载成全局函数  

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故 赋值运算符重载只能是类的成员函数
2.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
既然 编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2;
	s2 = s1;
	return 0;
}

 结果分析:

所以,如果类中未涉及到资源管理,赋值运算符是否实现(浅拷贝)都可以;一旦涉及到资源管理则必须要实现(深拷贝)。

五, const成员函数

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

让我们看看下面的实例代码吧:

class moss
{
public:
	moss(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << "Print()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
	void Print() const  // 这就是const成员函数,同上面的Print函数是重载函数。
		// 试试将尾巴上的const 删去,看看会有什么事情发生。
	{
		cout << "Print()const" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
void Test3()
{
	moss d1(2022, 1, 13);
	d1.Print();
	const moss d2(2022, 1, 13);
	d2.Print();
}

 首先我们已经知道const具有限制权限的功能,比如 int this,如图:

 对本次事例解析:

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

这个比较容易理解,这两个默认成员函数一般不用重新定义 ,编译器默认会生成。 看下面代码:

class Date
{ 
public :
 Date* operator&()
 {
 return this ;
 }
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如 想让别人获取到指定的内容!

七,练习——实现日期计算器

用C++类编写一个日期计算器,利用今天的运算符重载知识,实现日期+天数,日期-天数,日期-日期的功能。

下面是代码分享:

头文件:

#pragma once

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

class Date
{
public:
	// 获取某年某月的天数
	int GetMonthDay(int year, int month)
	{
		// 因为需要频繁调用,所以写成内联函数。
		static int M[13] = { 0,31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		int day = M[month];
		if (month == 2 &&
			((year % 4 == 0 && year % 100 != 0) ||
				(year % 400 == 0)))
		{
			day++;
		}
		return day;
	}
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;

		if (!CheckDate())
		{
			cout << "非法日期" << endl;
			exit(-1);
		}
	}

	bool CheckDate()
	{
		if (_month > 12 || _day > GetMonthDay(_year, _month))
		{
			return false;
		}
		else
		{
			return true;
		}
	}
	// 拷贝构造函数
    // 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);
	// 日期+天数
	Date operator+(int day);
	// 日期-天数
	Date operator-(int day);
	// 日期-=天数
	Date& operator-=(int day);
	// 前置++
	Date& operator++();
	// 后置++
	Date operator++(int);
	// 后置--
	Date operator--(int);
	// 前置--
	Date& operator--();
	// >运算符重载
	bool operator>(const Date& d);
	// ==运算符重载
	bool operator==(const Date& d);
	// >=运算符重载
	bool operator >= (const Date& d);
	// <运算符重载
	bool operator < (const Date& d);
	// <=运算符重载
	bool operator <= (const Date& d);
	// !=运算符重载
	bool operator != (const Date& d);
	// 日期-日期 返回天数
	int operator-(const Date& d);

	int WeekDay()
	{
		Date start(1, 1, 1);
		int LeveDay = 1;
		int day = (*this - start);
		LeveDay += day;
		int WeekDay = LeveDay % 7;
		return WeekDay;
	}
	// 友元函数为外边全局函数拥有调用类成员的权限。
	friend ostream& operator<<(ostream& cou, const Date& a);
	friend istream& operator>>(istream& cin, Date& a);

	void Print()const  // 本身是 Date* const this--是不能修改其指向的this,
		//但可以修改其内容,为了能接受被const Date* const this的对象所以需要缩小权限(方法就是函数名后加const)
		//反正print函数没有修改功能。
	{
		cout << _year << endl;
		cout << _month << endl;
		cout << _day << endl;
	}

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

// 放在全局中可以外部调用重载后的运算符,但放在外面又不能访问类成员,这里会用到后面的友元函数。
// 重载cout输出流
inline ostream& operator<<(ostream& cou,  const Date& a)
{
	cou << a._year << "年" << a._month << "月" << a._day << "日";
	return cou;
}

// 重载cint提取流
inline istream& operator>>(istream& cin, Date& a)
{
	cin >> a._year >> a._month >> a._day;
	assert(a.CheckDate());
	return cin;
}

函数实现源文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"

// 日期+=天数
Date& Date::operator+=(int day)
{
	_day += day;
		while ( Date::GetMonthDay(_year, _month) < _day)
		{
			_day -= Date::GetMonthDay(_year, _month);
			_month++;

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

// 日期+天数     让我们得出结果的临时拷贝,所以可以复用+= ,
// 但有一个缺点是,日期其实已经被污染,不能在原日期下重新调用。
Date Date::operator+(int day)
{
	*this += day;
	Date tmp = *this;
	return tmp;
}


// 日期-天数  //可以复用 -= ,但原日期被修改。
Date Date::operator-(int day)
{
	*this -= day;
	Date tmp = *this;
	return tmp;
}
// 日期-=天数
Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day <= 0)
	{
		_month--;   // 如果_month == 0了,就提取不了对应月份的天数
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		_day += Date::GetMonthDay(_year, _month);
	}
	return *this;
}

// 前置++  加完后返回
// 为了区分重载函数前置与后置++,C++做了重载区分,就是后置++增加一个int参数。
Date& Date::operator++()
{
    // 可以进行复用+=
	* this += 1;
	return *this;
}

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

// 后置--
Date Date::operator--(int)
{
	Date tmp(*this);
	*this -= 1;
	return tmp;
}
// 前置--
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}
// // >运算符重载
bool Date::operator>(const Date& d) 
{
	if ((_year > d._year) ||
		(_month > d._month) ||
		(_day > d._day))
	{
		return true;
	}
	else
	{
		return false;
	}
}
// ==运算符重载
bool Date::operator==(const Date& d)
{
	return _year == d._year &&
		_month == d._month &&
		_day == d._day;
}

// >=运算符重载
bool Date::operator >= (const Date& d)
{
	return (*this > d) || (*this == d);
}

// <运算符重载
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;
	}
}
// <=运算符重载
bool Date::operator <= (const Date& d)
{
	return (*this < d) || (*this == d);
}
// !=运算符重载
bool Date::operator != (const Date& d)
{
	return _year != d._year &&
		_month != d._month &&
		_day != d._day;
}

// 日期-日期 返回天数  思路:用小的加到大的,计算中间的次数。
int Date::operator-(const Date& d)
{
	int flog = 1;
	Date a1(*this); // 假设a1大
	Date a2(d);
	if (a1 < a2)    // a1小替换为a2
	{
		a1 = d;
		a2 = *this;
		flog = -1;
	}
	int n = 0;
	while (a2 < a1)
	{
		/*if (a2._day == 9 && a2._month == 5)
		{
			int x = 0;
		}*/
		++a2;
		n++;
	}
	return n * flog;
}

测试源文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include"Date.h"

void Test()
{
	const char* WeekRoom[7] = { "周天", "周一", "周二", "周三", "周四", "周五", "周六" };
   
	do
	{
	cout << "------------------------------------" << endl;
	cout << "--------请输入要操作的选项----------" << endl;
	cout << "----1. 日期+天数---2.日期-天数----" << endl;
	cout << "----3. 日期-日期---0.结束程序 --------" << endl;
	cout << "----4.  --------" << endl;
	int opertaton = 0;
	cin >> opertaton;
	Date a1, a2;
	int day;
	switch (opertaton)
	{
	case 1: 
		cout << "-----------"<< endl;
		cin >> a1 >> day;
		cout << (a1 += day) << endl;
		cout << WeekRoom[a1.WeekDay()] << endl;
		break;
	case 2:
		cout << "-----------" << endl;
		cin >> a1 >> day;
		cout << (a1 -= day) << endl;
		cout << WeekRoom[a1.WeekDay()] << endl;
		break;
	case 3:
		cout << "-----------" << endl;
		cin >> a1 >> a2;
		cout << (a1 - a2);
		break;
	case 0:
		cout << "结束程序";
		exit(-1);
	default:
		break;
	}
	} while (true);
}

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

结语

本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论;如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力。

猜你喜欢

转载自blog.csdn.net/qq_72112924/article/details/130545520