【C++初阶3-类和对象-中】空类不就是空的吗?

前言

空类真的是空的吗,并不是!

本期概览:

类的五个默认成员函数

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 赋值运算符重载
  5. 取地址运算符重载

五个默认成员函数

空类中真的如我们看到的一样,真的是空的吗?

其实不然,里边还有五个默认生成的成员函数(如果自己写了对应功能的函数,编译器就不会自动生成;没写就会自动生成)

【为什么要有默认成员函数?】
  1. 初始化、销毁等等,总是容易忘记,干脆搞个默认的!

  2. 对于编译器来说,内置类型是很熟悉,能轻松初始化、赋值、拷贝,但是复杂的自定义类型它可搞不定,需要一个函数来操作

一、构造函数

1. 构造函数是什么?

是用来初始化对象的成员函数。

2. 特性

  • 函数名是 className(类的名字)
  • 无返回值
  • 可重载

3. 调用

  • 实例化新对象的时候自动调用。
  • 调用顺序符合顺序规则。(谁先实例谁先构造)

4. 编译器自动生成的

  • 对内置类型,不处理
  • 对自定义类型,调用其构造函数

最终也会走到内置类型这一步啊,那么…

有没有办法弄一下内置类型?

C++打了个补丁:内置类型成员在声明的时候可以给缺省值。

注意,这不是初始化,只是赋初值——定义后才给的缺省值,初始化是定义时赋值。

class Date
{
public:
	//...
	
private:
	int _year = 1970;
	int _month = 1;
	int _day = 1;
};
默认构造函数

需要特别强调,默认构造函数并不特指编译器自动生成的构造函数,而指不用传参的,以下几种都叫做默认构造函数:

  1. 编译器自动生成的
  2. 无参数的
  3. 全缺省的

注意:默认构造函数只能有一个(避免二义性)

5. 使用

  1. 编译器自动生成的默认构造函数,无需传参
class Date
{
public:
	void ShowDate()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}

private:
    //在声明的时候给缺省值
	int _year = 1970;
	int _month = 1;
	int _day = 1;
};

int main()
{
	Date d1;
	d1.ShowDate();
	return 0;
}

:1970-1-1
  1. 全缺省的默认构造函数,可传可不传
class Date
{
public:
	Date(int year = 2022, int month = 10, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void ShowDate()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
		if (_month == 10 && _day == 1)
			cout << "祖国万岁!" << endl;
	}

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

int main()
{
    //Date d1(2022, 1, 1);
	Date d1;
	d1.ShowDate();

	return 0;
}

:2022-10-1
祖国万岁!
  1. 需要传参的构造函数
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void ShowDate()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
		if (_month == 10 && _day == 1)
			cout << "祖国万岁!\n" << endl;
	}

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

int main()
{
	Date d1(2022, 10, 1);
	d1.ShowDate();

	return 0;
}

:2022-10-1
祖国万岁!
【什么时候自己写?】
  1. 关乎指针
  2. 关乎数组

直接对指针/数组赋值达不到效果,指向同一块空间不好玩了。


二、析构函数

1. 析构函数是什么?

是用来清理对象资源的成员函数。(如释放空间)

2. 特性

  • 函数名是~className(类名前加个’~')
  • 无参数无返回值
  • 不可重载

3. 调用

  • 出对象的生命周期自动调用。
  • 调用顺序符合栈的规则。(后实例的先析构)

4. 编译器自动生成的

  • 对内置类型,不处理。
  • 对自定义类型,调用其析构函数。

5. 使用

  1. 编译器自动生成的默认析构函数(无法调试看见或打印信息)

  2. 自定义的析构函数

class DynamicArr
{
public:
	DynamicArr(int capacity = 4)
	{
		cout << "DynamicArr(int capacity = 4)" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_capacity = capacity;
	}

	void Push()
	{
		//...
	}

	~DynamicArr()
 	{
		cout << "~DynamicArr()" << endl;
		free(_a);
		_a = NULL;
		_capacity = _size = 0;
	}

private:
	void CheckCapacity()
	{
		//...
	}
	int* _a = NULL;
	int _size = 0;
	int _capacity = 0;
};

int main()
{
	DynamicArr a;

	return 0;
}

:DynamicArr(int capacity = 4)
~DynamicArr()

三、拷贝构造函数

1. 拷贝构造函数是什么?

拷贝别人用于初始化自己的函数——用一个已存在对象给同类型新对象初始化

我们知道,新对象实例化的时候会调用构造,但是在此基础上,我们如果想用已存在对象的成员给新对象初始化,就可以用拷贝构造

2. 特性

  • 是构造函数的一种重载
  • 有且只有一个参数,类型为该类的常引用——const className&

:不需要修改用来拷贝的已存在对象,所以加const;引用减少拷贝,提高效率,且传值传参会无穷调用!

在这里插入图片描述

  1. 用已存在的d1给d2初始化,触发调用拷贝构造
  2. 调用拷贝构造时,d1拷贝给形参,又触发调用拷贝构造
  3. 此时的拷贝构造,又是同类型的实参拷贝给形参,又触发拷贝构造

:调用拷贝构造,传参的时候又触发调用拷贝构造,无穷调用

3. 调用

当用已存在对象,给新对象初始化的时候调用

  1. 已存在类型给新对象初始化
  2. 函数传值传参:形参是实参的临时拷贝,要把已存在实参对象拷贝给新的形参对象
  3. 函数返回对象:返回的时候会产生临时变量,待返回对象拷贝给tmp,tmp再拷贝到返回的地方

4. 编译器自动生成的

  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(s1);
	return 0;
}

原因是s2的 _array被按字节拷贝成了s1的_array,两指针指向同一块空间,于是析构s1、s2的时候对同一块空间释放两次,自然崩溃。

5. 使用

对于这样的日期类

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

	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
  1. 已存在类型给新对象初始化
int main()
{
	Date d1;
	Date d2(d1);

	return 0;
}

:Date(const Date& d)
  1. 函数传值传参:形参是实参的临时拷贝,要把已存在实参对象拷贝给新的形参对象
void test(Date d)
{}

int main()
{
	Date d;
	test(d);

	return 0;
}

:Date(const Date& d)
  1. 函数返回对象:返回的时候会产生临时变量,待返回对象拷贝给tmp,tmp再拷贝到返回的地方
Date test()
{
	Date d;
	return d;//返回了局部对象,不合理的代码,此处仅示范
}

int main()
{
	Date d = test();
	return 0;
}

:Date(const Date& d)

诶?奇怪,为什么这里只调用了一次拷贝构造?

6. 编译器对拷贝构造的优化

对于紧贴着连续调用的

  • 拷贝 + 拷贝构造
  • 拷贝构造 + 拷贝构造

部分编译器会直接合并,用内置类型来理解:

int a = 10, b, c;

//不合并
b = a;//a拷贝给b
c = b;//b拷贝给c

//合并
c = a;//a直接拷贝给c

这样的场景有:

  1. 传值传参
  2. 传值返回
  3. 临时变量
    1. 类型转换
    2. 传值返回

一句话总结就是

连续出现的拷贝构造会合并。


*运算符重载

想要学习赋值运算符重载,我们得先了解什么是运算符重载。

1. 运算符重载是什么?

拥有特殊函数名(operator + [运算符])的函数

Date& operator+=(const Date& d2);//Date类的加号运算符重载

*operator是C++中的关键字,用于运算符重载。

2. 特性

  1. 函数名是 operator [运算符],operator后接要重载的运算符
  2. 函数的参数必须和操作符的操作数一致
  3. 隐式传递的this指针也是一个参数
  4. 用于内置类型的运算符不能重载(不然基本运算都乱了)
  5. 不能重载出新的运算符,如 # $
  6. [ .* ] [ : : ] [ sizeof ] [ ? : ] [ . ] 不能重载

3. 调用

直接对对象使用即可。(可读性高)

4. 使用

  1. Date的全局==重载
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;
};

//全局的operator==
bool operator==(const Date& d1, const Date& d2)
{
	//在类外无法访问私有
	//1. 可以直接重载成成员函数(类内可以访问)
	//2. 也可以将此函数声明成友元(后面会讲)
    //此处为了简单演示,直接把私有的权限放开了
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

int main()
{
	Date d1(2022, 1, 1);
	Date d2(2022, 1, 2);

	//这里要注意,==的优先级低于<<
	//cout << d1 == d2 << endl;//wrong
	cout << (d1 == d2) << endl;

	return 0;
}

:0
  1. Date的成员运算符重载
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    
	bool operator==(const Date& d2)
	{
		return _year == d2._year
			&& _month == d2._month
			&& _day == d2._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 1, 1);
	Date d2(2022, 1, 1);

	cout << (d1 == d2) << endl;

	return 0;
}

:1

四、赋值运算符重载

1.运算符重载是什么?

用同类的已存在对象,给另一已存在对象赋值的函数(运算符重载)

2. 特性

  1. 参数类型:const className&
  2. 返回值类型:className&
  3. 要返回*this(这样才符合赋值运算符的用法功能)
int a;
int b = a = 10;//没有返回值则无法正常使用

3. 调用

d1 == d2;

d1.operator==(d2);

二者相等,后者可以更好地理解:运算符重载的本质是函数

4. 编译器自动生成的

按字节拷贝

5. 使用

int main()
{
	Date d1(2022, 1, 1);
	Date d2(2022, 1, 2);
	
	cout << (d1 == d2) << endl;
	cout << (d1.operator==(d2)) << endl;

	return 0;
}

:0
0

6. 注意事项

  1. 要检查是不是给自己赋值(是就返回)
  2. 运算符只能重载成类的成员函数,不能重载成全局函数

在这里插入图片描述

五、取地址运算符重载

取地址运算符重载分为 取地址运算符重载 和 const对象取地址运算符重载

1. 取地址运算符重载

是对对象取地址的时候调用的函数。

基本不用自己写,除非有这样的需求:让类的用户取地址时取到你指定的东西

2. const取地址运算符重载

是对const对象取地址的时候调用的函数

基本不用自己写,除非有这样的需求:让类的用户取地址时取到你指定的东西


越学越感觉有很多nb的玩法等待挖掘,C++…

今天的分享就到这里啦,这里是培根的blog,期待与你共同进步!

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周杰偷奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值