【C++】【类与对象超强知识汇总】(三)


前情提要,本章涉及的知识点较多,建议收藏慢慢消化

在这里插入图片描述在这里插入图片描述


类的6个默认成员函数

但一个函数中什么成员都没有,那么我们就叫它空类

在这里插入图片描述

  • 但是空类里面真的什么都没有嘛?
  • 其实并不是,任何类在什么都不写的时候,编译器会自动生成一下六个默认成员函数

以下为六个默认成员函数👇

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/e7407550a9524bf1abe5417fcef3bea1.png

在这里插入图片描述
在这里插入图片描述


一、 构造函数

  • 概念

我们在用C写栈的时候,是否经常会遇到初始化的问题而导致程序崩溃。忘记调用 Init 就使用,那么下面的构造函数就能很好的解决我们的问题。

int main()
{
    //忘调用Init 就使用
    Date d1;
    Date d2;
    d1.Print();
    d2.Print();
}

在这里插入图片描述

  • 这个时候就很有构造函数的必要了!
    在这里插入图片描述

  • 构造函数可以重载也就代表有多种初始化方式
    比如一下两种:
    在这里插入图片描述

  • 让我们来运行一下看看结果:
    在这里插入图片描述在这里插入图片描述

  • 虽然可以构造函数,但是一般情况下,一般建议每个类都可以写一个全缺省函数。

请添加图片描述

在这里插入图片描述

  • 特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任
务并不是开空间创建对象,而是初始化对象。

其特征如下:

1. 函数名与类名相同
2. 无返回值
3. 对象实例化时编译器自动调用对应的构造函数
4. 构造函数可以重载

在这里插入图片描述

在这里我们使用日期函数来举例子:
在这里插入图片描述

  • 此时我们在用main函数实行的时候发现,欸嘿,完成初始化了!
    在这里插入图片描述
    在这里插入图片描述
  • 在这里,没有调用Init函数,却没有崩!
    在这里插入图片描述
  • 欸?那有一个问题,如果我们不写构造函数,结果又会是什么呢?
    在这里插入图片描述
    糟了…怎么会是随机值呢!
    在这里插入图片描述
    不要慌哈!C++ 在这里给我们的数据类型分为两个部分。

在这里插入图片描述
既然这样,那我们加上一个自定义类型尝试一下。

请添加图片描述
在这里插入图片描述

int main()
{
	Date d1;
	d1.Print();

	return 0;
}

请添加图片描述

  • 然后我们又扯出一个非常重要的知识点:默认构造函数

不实现构造函数的情况下,编译器会生成默认的构造函数。在这里插入图片描述

  • 问题一 ,当我们这样改动的话,他还是构造函数吗?
    在这里插入图片描述
    这个时候,没有构造函数,编译器就报错啦!

  • 总结:不传参数就可以调用默认构造函数
    在这里插入图片描述
    所以,在上面传参后,Date _t 不具备默认构造函数

  • 无参构造函数, 全缺省构造函数, 我们没写编译器默认生成的构造函数,都可以认为是默认构造函数->基本上只能存在一种

对此我们对应的方法就是:给缺省值:就是这样!👇
这个也是C++后期打补丁添加的

在这里插入图片描述

  • 在这里就是声明, 并不是初始化

在这里插入图片描述
总结:

  1. 一般情况下,构造函数都需要我们自己显示的去实现
  2. 只有少数的情况下, 可以让编译器自动生成构造函数

二、析构函数

  • 概念

对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

  • 我们在开辟空间后,常常又会忘记调用 Destory 函数

  • 这样的结果往往会导致内存泄漏,析构函数由而诞生

  • 像上面的Date函数是不需要析构函数的,因为没有开辟新的空间,故而没有资源需要清理

在这里插入图片描述

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

在这里插入图片描述

  • 代码如下👇:
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)  //这个是栈的构造函数
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}


	// 其他方法...
	~Stack()  //析构函数
	{
		cout << "~Stack()" << endl; // 这里打印出是为了更好的看出是否调用
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

欸!我们运行一下,发现析构函数和构造函数都打印出来了,说明该运行过程中调用了该成员函数

在这里插入图片描述

  • 这里说明了析构函数是可以自动调用的!

在这里插入图片描述

在实践中总结:
1. 资源中需要显示清理,就需要写析构,如:Stack List
2. 有两种场景不许呀显示写析构函数,默认生成就可以了
a 没有资源需要清理,如: Date
b 内置类型成员没有资源需要清理,剩下都是自定义类型成员。如: MyQueue
在这里插入图片描述


在这里小小的总结一下哈:

  1. 构造函数就是初始化函数
  2. 析构函数就是销毁函数
    在这里插入图片描述

三、拷贝构造函数

  • 概念

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

在这里插入图片描述
拷贝构造也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
  4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了
    在代码上是这样实现的👇
    在这里插入图片描述
  • 这样是可以进行成功的赋值,但是需要注意的一点是
//当初始化为一下这种形式时,会发生无穷递归
Date(Date d)
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}
//为什么呢?
  • 知识点:传自定义类型的传值传参需要调用拷贝构造完成

调用func得先传参,自定义类型对象传参要调用拷贝构造函数完成

传自定义类型的传值传参需要调用拷贝构造完成

在这里插入图片描述

Date(const Date& d)  //拷贝构造
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
  • 大概了解拷贝构造函数之后,我们用栈Stack来试一下
int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(1);
	st1.Push(1);

	Stack st2 = st2;

	return 0;
}

在旧版本的vs里面会直接崩溃,这里涉及到一个深浅拷贝的问题
所以在这里的拷贝构造,st1和st2是公用一个空间的,也是浅拷贝
在这里插入图片描述

  • 如何解决?使用深拷贝!

深拷贝也不复杂,开辟另一个空间就好啦!
在这里插入图片描述

  • 深度拷贝代码实现👇
	Stack(const Stack& st)
	{
		_array = (DataType*)malloc(sizeof(DataType) * st._capacity);//开一样的空间
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_array, st._array, sizeof(DataType) * st._size);//开始拷贝赋值
		_size = st._size;
		_capacity = st._capacity;
	}

结果:都拷贝完成
在这里插入图片描述
当我们pop或者push st1的时候,st2并不会收到影响,深拷贝实现成功

👇完整栈函数C++代码实现

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)  //这个是栈的构造函数
	{
		cout << "Stack(size_t capacity = 3)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	DataType Top()
	{
		return _array[_size - 1];
	}

	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	bool Empty()
	{
		return _size == 0;
	}

	void pop()
	{
		--_size;
	}

	Stack(const Stack& st)
	{
		_array = (DataType*)malloc(sizeof(DataType) * st._capacity);//开一样的空间
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_array, st._array, sizeof(DataType) * st._size);//开始拷贝赋值
		_size = st._size;
		_capacity = st._capacity;
	}
	// 其他方法...
	~Stack()  //析构函数
	{
		cout << "~Stack()" << endl; // 这里打印出是为了更好的看出是否调用
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

  • 思考一下,MyQueue需不需要写拷贝构造
    回答:不需要
    因为他会调用栈的拷贝构造

在这里插入图片描述
总结:

  1. 如果没有管理资源,一般情况下不需要写拷贝构造,默认生成的拷贝构造就可以。如:Date
  2. 如果都是自定义类型成员,内置类型成员没有指向资源,也类似默认生成的拷贝构造就可以。如:MyQueue
  3. 一般情况下,不需要显示写析构函数,就不需要写拷贝构造
  4. 如果内部有指针或者和一些指向资源,需要显示写析构释放,通常需要显示写构造完成深拷贝。如:Stack Queue List等

中场休息一下
我敲,好累,终于讲完这一部分了。


四、 赋值运算符重载

咱们先不着急开始讲,来说说其他的,还是用Date函数
⭐我们定义两个date,如何判断他们的大小呢?
在这里插入图片描述

创建一个比较函数,首先来看一下下面有什么不对的地方

bool Compare(Date dt1, Date dt2)
{}

在这里插入图片描述

这里的传参方式是不对的
我们之前讲过,调用函数需要传参,传参要调用拷贝构造,在这里拷贝构造白白的浪费了,调用拷贝构造自定义类型是有代价的,所以需要加上引用

bool Compare(Date& dt1, Date& dt2)
{}
//其实这样也不好,最好的写法是最后一个
bool Compare(const Date& dt1, const Date& dt2)
{}

上面用&作形参是为了减少拷贝,加 const 是为了防止改变

  • 理论成功,实践开始,在这里我们实现大于
bool Compare(const Date& dt1, const Date& dt2)
{
	//实现大于
	if (dt1._year > dt2._year)
	{
		return true;
	}
	else if (dt1._year == dt2._year) 
	{
		if (dt1._mouth > dt2._mouth) {
			return true;
		}
		else if (dt1._mouth == dt2._mouth) 
		{
			return dt1._day > dt2._day;
		}
	}
	return false;
}

int main()
{
	Date d1(2024, 4, 9);
	Date d2(2024, 4, 10);
	
	return 0;
}
  • 但是这里有一个问题:如果我们要实现小于的话,是不是又要在写一个函数?
  • 这样重复的运算就会显得特别的麻烦,并且重复非常多

那么能不能跟简洁直观的比较大小呢?

   cout << (d1 > d2) << endl;
   cout << (d2 > d2) << endl;
//内置类型是可以直接支持的

但是自定义类型是不知道怎么比较的,所以这样写还是有一点缺陷

在这里插入图片描述

函数重载:可以让函数名相同,参数不同的函数存在
运算符重载:让自定义累心更可以用运算符,并且控制运算符的行为,增加可读性

那么现在,我们就正式开始学习运算符重载

概念:

在这里插入图片描述

补充:
可能有人会这样问?我敲这个是什么东西?

在这里插入图片描述
其实一般来说我们确实很少遇到过这种,但是也总得知道他是什么东西吧。没关系,今天我一口气给你搞定!
在这里插入图片描述

#include<iostream>
using namespace std;

class OB
{
public:
	void func() {}  //成员函数指针类型
};

typedef void(OB::*PtrFunc)();

int main()
{

	//普通函数,函数名就是地址
	//但是规定成员函数前面要加上 & 才能取到函数执政
	PtrFunc fp = OB::func;  //定义成员函数指针p指向函数func

	OB temp;  // 定义OB类对象temp

	(temp.*p)();  //但后我们想要用对象去调用成员函数的就需要 .* 

	return 0;
}

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

简单来说就是 operator+运算符 就是运算符重载

  • 具体是这样实施的👇
bool operator>(const Date& dt1, const Date& dt2)
{
	//实现大于
	if (dt1._year > dt2._year)
	{
		return true;
	}
	else if (dt1._year == dt2._year) 
	{
		if (dt1._month > dt2._month) {
			return true;
		}
		else if (dt1._month == dt2._month) 
		{
			return dt1._day > dt2._day;
		}
	}
	return false;
}

在这里插入图片描述

  • 相同的原理,我们写一个判断日期相同
bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year
   && d1._month == d2._month
        && d1._day == d2._day;
}

其实我们可以知道一个知识点:那就是运算符重载是可以显示调用的
在这里插入图片描述

  • 但是这个并不是优势,因为我们可以直接这样写

在这里插入图片描述
想必看到现在,我们对于运算符重载有了大概的了解,但是这里有一个潜在的问题

在这里插入图片描述
🔺问题:重载成全局,无法访问私有成员
有三个方法可以解决:

  1. 提供这些成员的get和set(这里熟悉java的同学估计非常熟悉)
  2. 友元 后面很快就会提到
  3. 重载为成员函数->这是我们今天重点要讲的

⭐如何?直接就把operator扔到类里面!

在这里插入图片描述

  • 直接扔进去也会报错。
    在这里插入图片描述

在这里插入图片描述
因为所有的成员函数都有一个隐含 this
🔺参数个数应该跟操作数个数一致,所以我们应该这样子修正

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

不是很明白?main函数调用一下就明白了!

int main()
{
    //显式调用
    d3.operator==(d4);
    //这里就是默认this就是d3
    //等同于为下方

    //转换调用
    d3 == d4;
    return 0;
}

讲完了运算符重载,接着我们要开始赋值运算符重载

在这里插入图片描述
很简单, 拷贝构造

	Date d1(2024, 4, 14);
	Date d2(d1);
	Date d3 = d1;

但是如果这样写,它还是拷贝构造吗?

	Date d4(2024, 5, 1);

	d1 = d4;

不是:拷贝构造本质是初始化,而这里的d1和d2已经初始化了
在这里插入图片描述
具体复制拷贝实现代码:👇

void operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	 _day = d._day;
}
  • 但有一些人会用 Date 作为返回值而不是 void,其实道理很简单,是为了能够可以连续赋值。
d1 = d2 = d3;

在这里插入图片描述

  • 这里衍生出一个小小的知识点:
  • 传值返回和引用返回

在这里插入图片描述

传值拷贝会生成一个临时对象,有一个方法可以不生成拷贝,那就是引用拷贝

  • 传值返回和引用返回有什么区别呢?
  • 我们在拷贝构造这里打印一句话,这样就可以判断该过程是否使用

在这里插入图片描述

int main()
{
	func();

	return 0;
}
Date func()  // 这里的func是传值返回
{
	Date d;
	return d;
}

运行结果来看,引用拷贝构造,所以传值返回会创拷贝一个临时对象作为函数的返回值

int main()
{
	func();

	return 0;
}
Date& func()  //引用返回
{
	Date d;
	return d;
}

运行结果来看,没有引用拷贝构造,所以引用返回不会创拷贝一个临时对象作为函数的返回值
在这里插入图片描述

//在Date里面,我们创建了一个析构函数~date,来看一下整个进程
int main()
{
	Date& ref = func(); //这里再套一层,看一下结果是如何
	ref.Print();

	return 0;
}
Date& func()
{
	Date d;
	return d;
}

糟糕,就算成功调用析构函数,怎么还会是随机值
在这里插入图片描述

在这里插入图片描述

  • 这里比较神奇的是,如果用的是传值返回的话,结果才会使正确的
    在这里插入图片描述
    在这里插入图片描述
  • 此时ref指向的空间已经被释放,它现在就是一个野指针,没有保障

总结一下:

  1. 返回对象是一个局部对象或者临时对象,出了当前函数作用域,就析构销毁了,呢吗不能用引用返回,因为是存在风险的,因为引用对象在出函数已经销毁了
  2. 虽然引用返回可以减少一次拷贝,但是出了函数,返回对象还在,才能用引用返回

简洁的来说:
在这里插入图片描述
🔺赋值运算符只能重载成类的成员函数不能重载成全局函数

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

🔺用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。


我们通过完善日期函数来巩固一下我们的知识点吧

  • 首先我们来看一下最基本的加减乘除
	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);

通过下面我们可以看到,当我们只写了一个小于的函数,后面的函数可以通过其他方式来避免重复的代码

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

//d1 <= d2
bool Date::operator<=(const Date& d)
{
	return *this < d || *this == d;
}
bool Date::operator>(const Date& d)
{
	return !(*this <= d);
}
bool Date::operator>=(const Date& d)
{
	return !(*this < d);
}
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);
}


  • 欸? ++d1 和 d1++ 都是 operator++ ,我们该怎么区分

在这里插入图片描述

  • ++ – 代码👇:
//++d1
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

//d2++
//为了区分,构成重载,给后置++,强行增加了一个形参
//不需要写形参名,因为接受值是什么不重要
//仅仅是形式上为了构成重载
Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}



Date& Date::operator--()
{
	*this -= 1;
	return *this;
}
Date Date::operator--(int)
{
	Date tmp = *this;
	*this -= 1;
	return tmp;
}
  • 好的,那么我们继续看下一个问题,我们怎么计算两个日期相差多少天呢?

在这里插入图片描述
但是编译器又怎么会知道该怎么计算呢?
这个时候我们又要重载一个运算符叫做operate-
那么上面的公式就会自动转换为operate-
我们定义之后自定义成员就可以使用了

在这里插入图片描述

//日期相减
int Date::operator-(const Date& d)
{


	Date max = *this;
	Date min = d;

	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}
	return n;

}

敲敲敲,还有两部分大家加油
休息一会,消化完了吗?


五、const成员函数

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

  • 我们不想改变初始化的值,所以用const修饰,但是当我们用const修饰的d1时候,会出现一个编译报错的问题
    在这里插入图片描述
    在这里插入图片描述
    很简单,在函数后面加上 const
//Date.h
void Print() const;

//Date.cpp
void Date::Print() const
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

在这里插入图片描述

  • const 修饰 this ,本质改变this的类型
    在这里插入图片描述
    在这里插入图片描述

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

  • 先看代码👇:
class A
{
public:
	A* operator&()
	{
		cout << "A* operator&()" << endl;
		return this;
		
	}
	const A* operator&()const
	{
		cout << "const A* operator&()const" << endl;

		return this;
	}
	

private:
	int _a1 = 1;
	int _a2 = 2;
	int _a3 = 3;
};

int main()
{
	A aa1;
	const A aa2;

	cout << &aa1 << endl;
	cout << &aa2 << endl;
}

可以看到,他们分别都调用了取地址重载在这里插入图片描述

在这里插入图片描述

我们不实现,编译器会自己实现,我们实现,编译器就不会自己实现了

  • 一般不需要我们自己实现,除非不让让别人取到这个类型对象的地址

在这里插入图片描述

妈妈QAQ我终于写完了,天哪我写了好久了
创作不易,三连支持一下吧~
点一个小小的赞都是对我莫大的鼓励!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值