类和对象(中)

文章目录

  • 前言
  • 构造函数
    • 构造函数概念
    • 特性
    • 析构函数
    • 概念
    • 特性
  • 构造和析构的进一步讲解
    • 自动生成构造函数和析构函数
      • 补丁
    • 总结构造函数和析构函数
  • 拷贝构造
    • 拷贝构造是什么?
    • 加上const
    • 不写拷贝构造,编译器自动生成的拷贝构造。
    • 默认生成的拷贝构造函数行不行?
    • 什么时候需要自己写一个拷贝构造
    • 拷贝构造的使用场景
  • 赋值重载
    • 运算符重载
      • 增强可读性
      • 特性
      • 怎样写
    • 赋值运算符重载
      • 什么时候用赋值运算符重载
      • 赋值重载举例
        • 连续赋值必须要设定返回值
      • 编译器自动生成的赋值重载

前言

类和对象这块还是很重要的,不然后面会很麻烦。

以前写栈的时候,我们是这样写的,经常忘记初始化,或者Destroy;而且Destroy可能在很多地方都要调用,写起来还很烦。init完成初始化,destory完成资源清理,如果不清理会造成内存泄漏。

c++对此做了一些优化,下面带大家看一下具体是怎样优化的。

int main()
{
	Stack st;
	StackInit(&st);
	StackPush(1);
	StackPush(2);
	StackPush(3);
	st.Destroy();

	return 0;
}

构造函数

怎样解决上面容易忘记初始化的问题?
把这些东西交给编译器来实现,让它自动来做。那具体怎么做呢?用一个函数来代替,并且这个函数编译器自己调用,这个函数称之为构造函数。这个函数是特殊的函数。

构造函数概念

构造函数虽然 叫做构造函数,但它的功能是初始化。

特性

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
  6. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会
    生成默认的构造函数。但是看起来默认构造函数又没什么用?
  7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

下面自己会带大家一 一讲解
先举个简单的例子

class Stack
{
public:
//函数名与类名相同
//没有返回值不是写个void而是什么都不用写。
	Stack()
	{
		_a = nullptr;
		_size = _capacity = 0;
	}
//构造函数可以重载(说明一个类可以有多个构造函数,多种初始化的方式)。
	Stack(int n)
	{
		_a = (int*)malloc(sizeof(int)* n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		_capacity = n;
		_size = 0;
	}
private:
    private:
	// 成员变量
	int* _a;
	int _size;
	int _capacity;
}

既然一个类可以有多个构造函数,那对象实例化自动调用的时候,调用的构造函数具体是哪个?
看你有没有给参数。

int main()
{
	Stack st1;//调用上面的第一个构造函数
	Stack st2(4);//调用上面的第二个构造函数
	return 0;
}

有了构造函数,你再也不用忘记初始化,它会自动调用。
对象在实例化的时候具体有没有调用对应的构造函数,可以自己通过调试看一下。

看了上面的写的对象实例化,肯定有个疑问?
Stack st1;可以写成Stack st1();吗?
答案是不行,因为写成Stack st1();编译器会分不清是函数声明还是实例化。但是如果有参数就不一样,它会加上类型。

那可不可以把stack st2(4);理解成st2.stack(4);?
当然不可以,因为st2还没有实例化。

上面还有一些点没有讲完,我们先讲完析构函数再接着讲解。

析构函数

除了构造函数还有destroy,思路还是设计一个函数让他自动调用,这个函数叫做析构函数。

概念

析构函数,析构函数跟构造函数功能相反,但要注意析构函数不是完成对象本身的销毁.

比如Stack st1, 出了作用域就自动销毁了。析构函数完成的是资源的清理,如果没有资源清理就不需要析构函数。在对象出了作用域,销毁的时候自动调用析构函数。

特性

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

最重要的特性就是自动调用,其他的都只是形式。析构函数用~表示,它有功能跟构造函数相反的意思。

class Stack()
{
pubic:
   Stack()
   {
		_a = nullptr;
		_size = _capacity = 0;
   }
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

private:
	// 成员变量
	int* _a;
	int _size;
	int _capacity;
}
int main()
{
	Stack st1();
	return 0;
}

构造和析构的进一步讲解

先写个日期类

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

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

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

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

Date()和Date(int year, int month, int day)构不构成重载?
构成,但是能不能合并一下,用全缺省。

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

好,我现在有一个问题?
Date()和Date(int year=1, int month=1, int day=1),在语法上可不可以同时存在,构不构成重载?可不可以但同时存在,答案是构成重载并且可以同时存在,但是不调用还好,调用的时候会造成歧义,编译器不知道调用哪一个构造函数。

一个类调用构造函数的时候尽可能用全缺省或半缺省,因为缺省真的很好用。

自动生成构造函数和析构函数

不写构造函数和析构函数,编译器会自动生成一个默认的,所以叫做默认成员函数。没写就用编译器的生成的。

好,那问题也随之而来了?既然编译器会默认生成构造函数和析构函数,那自己还要手动写吗?
这里涉及到祖师爷设计c++最开始没有想明白的东西。
构造函数要进行一些说明,默认生成的构造函数只针对自定义类型而不针对内置类型,什么是内置类型和自定义类型我这里就不赘述了。

什么时候就默认生成很有用呢?,我们再学数据结构的时候肯定都接触过一道题,用两个栈实现对列,就很有用,不需要写构造函数和析构函数。
对于日期类不需要写析构,因为资源不需要清理,栈则需要写析构。

class MyQueue {
public:
	// 默认生成构造函数,对自定义类型成员,会调用他的默认构造函数
	// 默认生成析构函数,对自定义类型成员,会调用他的析构函数
	
	//具体实现过程就不写了
	//...................

	Stack _pushST;
	Stack _popST;
	int _size = 0;
};

但是最终都还是要处理内置类型的,如果内置类型和自定义类型混在一起的话,还是只处理自定义类型。

补丁

内置类型不处理,其实很不合理。祖师爷后来为此打了一个补丁。在声明位置给缺省值,意思是默认构造的函数用缺省值去初始化。

如果混在一次呢?
大部分的场景还是要自己去写构造函数的,只有少部分才不用。无参的构造函数和全缺省的构造函数都成为默认构造函数。

class Date
{
public:
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

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

但要注意这只是给了缺省值,并没有实例化开空间。

看下面一段代码,它会报错,这里又涉及一个东西。

class Date
{
	public:
		Date(int year)
		{
		}
		private:
		{
			int_year;
			int_moth;
			int_day;
		}
}

注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。

一般建议,一个类里面有一个默认构造函数,只要是无参的都可以,不然会报错。所以上面的代码自己手写了一个构造函数,所以编译器没有自动生成默认构造函数。并且自己手写的构造函数不是默认构造函数,所以它会报错。

构造函数就7点,照着看就可以了,这七点搞懂了就没什么问题了。

总结构造函数和析构函数

1、我们不写,编译器会自动生成,我们写了,编译器就不会自动生成。

默认生成构造和析构:
a、内置类型不做处理
b、自定义类型会调用对应构造/析构

2、自动调用

拷贝构造

拷贝构造是什么?

比如

int main()
{
	Date d1(2023, 11,25);
	Date d2(d2);
	return 0;
}

我想把d2变成d1的拷贝,怎样变成d1的拷贝呢?就相当于我想用d1去初始化d2,这其实相当于j=i,就这么简单。

构造函数可以重载,前面初始化时用年月日,现在我要用一个同类型的日期类对象来初始化我。那这个构造函数怎么写

Date( Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

这就叫做拷贝构造。所以也是构造函数,它是构造函数的一个重载。

上面我用的是传引用传参,那我能不能用传值传参呢?

Date( Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

这是拷贝构造一个非常难以理解的地方,这里我先说结论。
**编译器不允许这样写。传值传参会被认为无穷递归。规定必须用引用,为什么? **
以前传的都是内置类型,内置类型编译器自己会拷贝,但是自定义类型不能随便拷贝,编译器无法承担拷贝的行为。为什么?因为有些时候没有问题,有些时候就会出问题,下面我们来看一个例子。

如果是Date类,只有年月日编译器随便拷贝,这都是浅拷贝,直接按字节拷贝,编译器干的事情。如果是Stack,直接拷贝就出大事了。_a出问题,两个栈指向同一块空间,会出现什么问题。后面两个栈都要调用析构函数。同一块空间析构两次。

类和对象(中)_第1张图片
基于这样的原因,c++规定,自定义类型需要调用拷贝构造,(深拷贝的拷贝构造,以后会讲)。至于到底是深拷贝还是浅拷贝程序员判断。

所以自定义类型的传值传参就要拷贝构造,如果引用传参,就没有拷贝构造。
所以上面说的无穷递归也就很容易理解了。

int main()
{
	Date d1(2023, 10, 10);
	Date d2(d1);
	//这样也可以
	Date d2=d1;
	return 0;
}

加上const

Date (&Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

尽量把const加上。

好处 :
1,防止d被改
2.用const做 形参也是很棒的,const的对象也可以传给我,普通对象也可以传给我,因为权限可以缩小。

不写拷贝构造,编译器自动生成的拷贝构造。

默认生成的拷贝构造会完成哪些行为呢?
与构造函数和析构函数不一样,默认生成的拷贝构造对内置类型也是会处理的看,所以日期类不需要写拷贝构造。

但是自定义类型是怎样处理的呢?
看一个例子。

int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

这个程序运行时直接报错了,为什么日期类不写拷贝构造没事,栈不写拷贝构造就出事了?
类和对象(中)_第2张图片
关键在于栈对象里有个指针指向堆上的一块空间。
如果把值拷贝过去,两个栈对象指向同一块空间,会导致两大问题。

指向同一块空间的问题:

1、插入删除数据会互相影响
2、析构两次,程序崩溃

默认生成的拷贝构造函数行不行?

结合上面的自定义类型,默认拷贝构造行不行?
默认的拷贝构造都是值或浅拷贝.我们需要自己写一个深拷贝。

Stack(const Stack& st)
	{
		cout << "Stack(const Stack& st)" << endl;
		_array = (DataType*)malloc(sizeof(DataType)*st._capacity);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			exit(-1);
		}

		memcpy(_array, st._array, sizeof(DataType)*st._size);
		_size = st._size;
		_capacity = st._capacity;
	}

类和对象(中)_第3张图片

什么时候需要自己写一个拷贝构造

结合我们上面看到的栈的例子,我们可能会觉得有个指针就要 写一个拷贝构造,但这时不严谨的,得看你这个指针指向得具体空间。

不能看有没有指针来衡量。
**自己实现了析构函数释放空间 ,就需要实现拷贝构造。**说明涉及资源管理。

拷贝构造的使用场景

这里直接总结一下吧!!!后面还会涉及。

1.传值传参
2.传引用返回

问个小问题

int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

st1和st2谁先调用析构函数呢?
这跟函数栈帧有关系,后定义先出,st1先调用析构函数。

赋值重载

在理解赋值重载的时候先理解一下运算符重载。

运算符重载

这也是非常重要的东西!!!

首先运算符重载跟函数重载没有任何关系。

运算符重载:自定义类型对象可以使用运算符
函数重载:支持函数名相同,参数不同的函数,同时可以用

增强可读性

举例,我们可能想要比较两个日期相等

a.首先我们可能直接写一个函数。
但是这样的话,可能要写很多函数,而且写的很不规范。

b.那我们可不可以用直接用d1==d2表示呢?

我们知道内置类型是可以直接这样写的,那自定义类型可不可以直接这样写呢?
1.编译器只认识内置类型知道内置类型怎么比较。自定义类型是你自己写的,编译器不知道它的存在,怎么比较是你自己的事情。
2.自定义类型加这个运算符这些没有意义所以这些使用由程序员来确定的。
比如两个日期相加毫无意义,但是两个日期相减可以表示这中间的天数。

我们这就引用了运算符重载

特性

函数名字为:关键字operator后面接运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个自定义类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this

这些总结结合后面的内容再看才不费劲。

怎样写

首先我们还是不要再类外面定义,因为这涉及到权限。
类里面是可以随意访问成员的,在类外面,公有的可以,私有的不可以。

class Date 
{
public:
	bool operator<(Date& d)
	{
		return _year < d._year || (_year == d._year && _month < d._month) ||
			( _year == d._year && _month == d._month && _date < d._date);
	}
private:
	int _year;
	int _month;
	int _date;
};
int main()
{
	Date d1;
	Date d2;
	//1.可以这样写,但是我们不会这样写
	d1.operator(d2);
	//2.我们都这样写,可读性更高
	//首先编译器看到两个日期类对象会看有没有重载,然后再转换成调用d1.operator(d2);
	d1<d2;
	return 0;
}
cout << (d1<d2) << endl;
count << d1.operator<(d2) << endl;

赋值运算符重载

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

这几点直接看还是很不容易理解,这是总结,先看后面的内容吧。

什么时候用赋值运算符重载

int main()
{
	Date d1(2024, 1,1);
	Date d2;
	d2=d1;//
	return 0;
}

注意,这是拷贝构造不是赋值运算符重载

Date d2=d1;

赋值运算的对象是两个实例化好的对象。

赋值重载举例

class Date 
{
public:
	void operator=(const Date& d)//传引用返回,不用拷贝构造,带来额外的消耗。
{
	//首先判断一下是否是自己,自己给自己赋值没必要做多余的消耗。
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_date = d._date;
	}
	return *this;
}
private:
	int _year;
	int _month;
	int _date;
};
连续赋值必须要设定返回值
class Date 
{
public:
	Date& operator=(const Date& d)//传引用返回,不用拷贝构造,带来额外的消耗。
{
	//首先判断一下是否是自己,自己给自己赋值没必要做多余的消耗。
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_date = d._date;
	}
	return *this;
}
private:
	int _year;
	int _month;
	int _date;
};
int main()
{
	Date d1(2024, 1, 1);
	Date d2;
	Date d3;
	d2=d3=d1;
	return 0;
}

这里要提醒大家一点的就是,不要为了用传引用返回,而再加一个static.
记住static只会用一次,后面就直接略过了,这是不利于我们写代码的,关于静态变量,一定要慎用。

编译器自动生成的赋值重载

不写赋值赋值重载,编译器也会帮助我们生成一个赋值重载。

那什么时候要写赋值重载
内置类型不要,自定义类型需要。
毫无疑问,这其中的道理是和前面讲的拷贝构造是一样的。
所以日期类对象不用写赋值重载,栈需要。

你可能感兴趣的:(java,jvm,开发语言)