C++类和对象(中)【万字详解,2024年最新2024金九银十

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注大数据)
img

正文

编译器默认生成的构造函数1.对内置类型不做处理,2.自定义类型自动调用它的默认构造函数.

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

// 默认构造函数
class Date
{
public:
	Date()
	{
		_year = 1900;
		_month = 1;
		_day = 1;
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{
	Date d1;
}

我们看到Date类的构造函数只两个:一个无参的构造函数,一个是全缺省的构造函数.都是默认构造函数,存在了两个,那么存在了什么问题呢?

首先用Date创建一个d1对象,那么d1应该调用哪个构造函数呢?

两个构造函数都可以调用,第一个是无参的没有问题,第二个是没有给参数,也会把缺省值赋给d1,也可进行调用

所以这里就产生了歧义,即二义性。这段代码所以并编译不过去,因为编译器不知道调用哪个构造函数.

不调用可以,但是调用会存在歧义.

7. 关于编译器生成的默认成员函数,很多同学会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数

所以不对内置类型做处理也算是C++的一种小缺陷吧.

看下面一段代码:

#include<iostream>

using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

我们看到用Date类创建了一个d对象,创建之后,得去调用Date的构造函数.

Date没有显式写构造函数,则编译器默认生成一个构造函数

默认生成的构造函数:内置类型不做处理,自定义类型调用它的默认构造函数.

_year,_minth,_day不做处理,调用Time的默认构造函数

调用完成之后,Date默认生成的构造函数也就结束了.

我们运行来运行一下.

可以看到确实调用了.

这里再强调以下,编译器生成的默认构造函数只能调用自定义类型的默认构造函数!

我们在Time类里写一个构造函数,但是不是默认构造的.

这样既然写了构造函数,编译器便不能生成Time的默认构造函数了.所以在Date默认生成的构造函数里调用Time的默认构造函数时便会出错,因为Time此时并没有默认构造函数.

这里可以稍微提一下:

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

这个并不是说默认构造函数可以将内置类型的数据初始化了,而是说可以给内置类型变量的默认值,写默认值的话,默认构造函数则会根据默认值的值进行初始化的.

例如:

class Date
{
public:
	// 基本类型(内置类型)
	int _year = 1;
	int _month = 2;
	int _day = 3;
};
int main()
{
	Date d;
	cout << d._year << endl << d._month << endl << d._day << endl;
	return 0;
}

可以看到没有默认生成的构造函数也将内置类型初始化了.

注意:指针也是内置类型.

析构函数

概念

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?

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

特性

**其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值。
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。//默认生成构造函数特点:跟构造函数类似:a. 内置类型不做处理。b.自定义类型成员回去调用它的析构.
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。**

由于析构函数无参数,无返回值,所以构不成重载,即只能有一个析构函数.

看下面代码:

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

typedef int DataType;
class SeqList
{
public:
	SeqList(int capacity = 10)//构造函数
	{
		_pData = (DataType*)malloc(capacity * sizeof(DataType));
		assert(_pData);
		_size = 0;
		_capacity = capacity;
	}
	~SeqList()//析构函数,注意格式:~类名
	{
		if (_pData)
		{
			free(_pData); // 释放堆上的空间
			_pData = NULL; // 将指针置为空
			_capacity = 0;
			_size = 0;
		}
	}
private:
	int* _pData;
	size_t _size;
	size_t _capacity;
};

5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。

看下面一段代码:

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

class String
{
public:
	String(const char* str = "jack")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
	}
private:
	char* _str;
};
class Person
{
private:
	String _name;
	int _age;
};
int main()
{
	Person p;
	return 0;
}

看运行结果图:

可以看到用Person类生成了一个p对象,person对象里没有析构函数,编译器会默认生成一个析构函数Person();然后这个**默认析构函数去调用自定义类型的析构函数.**所以输出string().

拷贝构造函数

概念

我们创建对象的时候,如果想创建一个和某个对象的完全一样的对象,这个时候就需要拷贝构造函数.

拷贝构造函数实质上是一个构造函数的一个特殊形式,一个重载.但是它必须且只能有一个参数,该形参是本类类对象的引用.

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

特性

拷贝构造函数也是特殊的成员函数,其特征如下:
1.
拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

那么传值为什么会引起无限递归呢?

这里可能有些复杂,难以理解,慢慢来讲解它.

先看来代码:

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

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)//构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)//拷贝构造函数,注意这里必须加引用才可以.
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

上面拷贝构造加了引用,但是如果不加引用为什么会引发无限递归呢?

首先我们要理解传值调用时发生了什么

先看这段Date d2(d1);用d1对象构造d2,所以要去调用拷贝构造函数.

拷贝构造函数:Date(const Date d);我们想要的是把d1对象赋值给d.

但是赋值的时候,它要怎么赋值呢?是不是又要用d1对象构造d,因为参数会拷贝一份实参.

d1对象构造d,再次调用d的拷贝构造函数.

调用了d的拷贝构造函数,需要把参数传给d,而此时传给d,又需要调用拷贝构造函数…

如此循环,就发生了无限递归.

这是无限递归的原因图.

改正的方法就是直接加上引用.

改成引用之后,传过去的就是d1的别名,相当于d就是d1,此时便不再创建一份拷贝,即也不需要再调用拷贝构造函数.也就不会引发无限递归问题了.

3. 若未显式定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝

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

浅拷贝就是新拷贝的对象和原对象指向同一块空间,新拷贝的对象的值的改变也会引发原对象的值的改变,所以也是不推荐的.

深拷贝实质是在内存里重新为新拷贝的对象开辟一块空间,不指向同一块空间。然后这样修改值也就会不会影响到原对象了.

#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	// 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
	Date d2(d1);
	return 0;
}

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

class String
{
public:
	String(const char* str = "jack")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
	}
private:
	char* _str;
};
int main()
{
	String s1("hello");
	String s2(s1);
}

可以看到s1和s2的地址完全一样.

一个地址还好,我不修改它不就行了吗?但是我们来运行一下这个程序.

可以看到,程序报错了.这是为什么呢?

仔细想一下,s1,s2都是对象,当程序结束的时候,他们都要调用对应的析构函数,但是我们又知道,因为是浅拷贝,s1和s2地址一样.s1被析构以后,相当于这块空间已经被释放掉了,然后呢,s2又释放了一次,这样就造成了错误,因为同一块空间不能被析构两次.

所以,浅拷贝还存在一个问题,析构的时候会存在被释放两次的问题.

拷贝构造函数暂时到这里.

赋值运算符重载

运算符重载

先说我们上面写的Date类,里面记录了年月日.

当我们定义了两个Date对象,比如Date d1(2007,4,6);Date d2(2003,6,9);

我们如果想比较两个对象谁的日期比较大,该怎么比较呢??

先来说一下:

内置类型可以直接使用运算符运算,编译器知道该如何运算.

自定义类型无法直接使用运算符,因为编译器不知道要如何运算,想要支持,自己实现运算符的重载即可.

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型或者枚举类型的操作数

3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员的重载函数时,其形参看起来比操作数数目少1,因为成员函数的操作符有一个默认的形参this,限定为第一个形参.
5.操作符有一个默认的形参this,限定为第一个形参
.*  、 :: 、 sizeof 、?:  、. 注意以上5个运算符不能重载。

根据下面代码看一下用法

#include<iostream>
using namespace std;

#pragma warning(disable:4996)
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//private:
	int _year;
	int _month;
	int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是共有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
	&& d1._month == d2._month
		&& d1._day == d2._day;
}
void Test()
{
	Date d1(2007, 4, 6);
	Date d2(2003, 6, 9);
	cout << (d1 == d2) << endl;
}
int main()
{
	Test();
	
	return 0;
}

我们运行一下:

输出0,此时已经实现了自定义对象之间的比较了.

为了更加严谨,我们将两个日期改成相同的.如果输出1,说明是真,结果正确.

结果符合预期.这样我们就实现了==运算操作符.

这里提一下:写运算符重载时,**最好在参数前面加上const,**因为后面我们不需要修改这两个参数内容,而且在我们不小心失误修改了参数的情况下,能直接报错,会省很多事的.

但是上面那种写法,我们不得不把类的私有成员改成公有的,因为在类外无法调用类对象的私有成员.

为了解决这种问题,我们可以把这个运算符重载写到类里面,这样就可以调用类的私有成员了,但有没有其它的问题?我们来看一下.

此时看到问题:此运算符函数的参数太多.不就是两个参数吗,怎么参数过多?

在类里面的函数,是会有一个隐藏的this指针,代表调用的那个对象。我们又知道不能显式的写this,所以这个运算符函数里面只写一个参数即可.

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date& d2)//只写一个参数,因为前面会有一个隐藏的this指针
	{
		return _year == d2._year
			&& _month == d2._month
			&& _day == d2._day;
	}
	private:
	int _year;
	int _month;
	int _day;
};

再次运行:

void Test()
{
	Date d1(2022, 1, 4);
	Date d2(2022, 1, 4);
	cout << (d1 == d2) << endl;
}

依然成功运行了.

其实d1==d2这个时候相当于是d1.operator(d2);

此时类里的this相当于也是d1.用d1来调用operator运算符重载函数.

这里只举了一些样例,比如>=,<=,>,<这些都还没实现,我最后会把所有日期类所有这些操作符的实现全部写出来,放到本文章最后.

还有操作的类型不一定是同类的,还可以是类型类+整型类等等,都可以.

赋值运算符重载

和运算符重载是一个意思,只不过是赋值类的运算符重载.

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 4, 6);
	Date d2;
	d2 = d1;

	return 0;
}

我看先断点到d2=d1处,但注意,此时d1=d2还没有运行.

接着向下运行:

我们发现已经赋值成功了.

这就是赋值重载运算符的一种应用.

赋值运算符主要注意以下五点:

1. 参数类型
2. 返回值
3. 检测是否自己给自己赋值
4. 返回*this
5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。

我们主要说以下第五条.

先看下面一段程序.

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(2022,6, 9);
	// 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。
	d1 = d2;
	return 0;
}

我们看一看没有写赋值运算符重载的情况下,只依靠编译器默认生成的能不能完成拷贝操作.

看运行结果:

可以看到也完成了拷贝赋值.

那么这里就有一个问题:

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

其实和刚才讲拷贝构造函数的情况一样,浅拷贝造成的同一块地址被析构两次,进而造成系统崩溃.

#include<iostream>
using namespace std;
class String
{
public:
	String(const char* str = "")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
	}
private:
	char* _str;
};
int main()
{
	String s1("hello");
	String s2("world");
	s1 = s2;
}

运行一下:

这就造成了错误.因为释放的地址不能再被释放.

写日期类的时候注意:任何一个类,只需要写一个 > == 或者< == 重载,剩下的比较运算符复用即可.

比如我们要实现>=,假设我们已经实现了 > ,我们此时只需要返回d1>d2 && d1d2即可,这样就实现了>=,所以要学会复用写过的东西.

const成员

const修饰的类成员函数

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

编译器会做以下处理:

这样就无法改变this指针所指向的内容了若改变则会报错

#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1990, int month = 10, int day = 20)
	{
		_year = year;
		_month = month;
		_day = day;


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)**
![img](https://img-blog.csdnimg.cn/img_convert/672e1c4bdf6daf1a23b2618e0a73bb58.png)

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

](https://i-blog.csdnimg.cn/blog_migrate/31ee91cf6f247a49c1c9b3734d741cb4.png)


这就造成了错误.因为释放的地址不能再被释放.


**写日期类的时候注意:任何一个类,只需要写一个 > == 或者< == 重载,剩下的比较运算符复用即可.**


比如我们要实现>=,假设我们已经实现了 > ==,我们此时只需要返回d1>d2 && d1==d2即可,这样就实现了>=,所以要学会复用写过的东西. 


## const成员


### const修饰的类成员函数



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


编译器会做以下处理:


![](https://i-blog.csdnimg.cn/blog_migrate/280053717330d7d92d54fa2226cb4bd3.png)



这样就无法改变this指针所指向的内容了若改变则会报错



#include
using namespace std;

class Date
{
public:
Date(int year = 1990, int month = 10, int day = 20)
{
_year = year;
_month = month;
_day = day;

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)
[外链图片转存中…(img-7AXkSt41-1713275739734)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值