C++ 3.六个默认构造函数

目录

一、类的默认成员函数

二、构造函数

概念

构造函数

默认构造函数

特点

实例

1、对于内置类型

2、对于自定义类型

三、析构函数

概念

特点

实例

1、对于内置类型

2、对于自定义类型

四、拷贝构造

概念

特点

实例

1、对于内置类型

2、对于内置类型

五、赋值重载

运算符重载

概念

特点

实例

赋值运算符重载

概念

特点

关于是否需要显示实现

六、取地址运算符重载和const修饰的取地址运算符重载

七、笔记


一、类的默认成员函数

默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。

分别为构造函数、析构函数、拷贝构造、赋值重载和取地址重载

也就是说,当我们创建了一个类

class Date
{

};

实际上编译器已经生成了六个默认成员函数

class Date
{
public:
	Date();//构造函数

	~Date();//析构函数

	Date(const Date& d);//拷贝构造函数

	Date& operator=(const Date& d);//赋值运算符重载

	Date* operator&();//取地址运算符重载(&)

	const Date* operator&() const;//const修饰的取地址运算符重载(const &)
};

二、构造函数

概念

构造函数

对象实例化时初始化对象(不是开空间创建对象,我们常使⽤的局部对象是栈帧创建时,空间就开好了)

默认构造函数

无参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在

总结⼀下就是不传实参就可以调用的构造就叫默认构造

注意:无参构造函数和全缺省构造函数同时存在会造成歧义

特点

1、函数名和类名相同

2、无返回值

3、对象实例化时系统自动调用

4、构造函数可重载

5、如果没有显式定义,系统会生成一个无参的构造函数(显示定义了就不会生成)

实例

1、对于内置类型

我们不写编译器自动生成

这里发现会给随机值,因为C++编译器在系统默认构造函数中初始化类型的时候,对于内置类型不做处理,对于自定义类型会调用它的默认构造

需要注意的是,当我们写两个构造函数的时候会报错

2、对于自定义类型

自定义类型需要调用这个成员变量的默认构造函数初始化

假设我们要用两个栈实现队列

#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:


private:
	Stack pushst;
	Stack popst;
};
int main()
{
	MyQueue mq;
	return 0;
}

对于Stack,如果没有显式写构造函数,编译器就会报错

对于MyQueue,因为编译器默认⽣成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化(如果这个时候注释Stack的构造,MyQueue也初始化不了)

三、析构函数

概念

与构造函数相反,析构函数的功能是完成对对象中的资源的清理工作(不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了)

特点

1、析构函数函数名是类名前加~

2、无参无返回值⼀个类只能有⼀个析构函数

3、对象⽣命周期结束时,系统会⾃动调⽤析构函数

4、对于内置类型和自定义类型,与构造函数类似

5、我们显示写析构函数,对于自定义类型成员也会调⽤他的析构(自定义类型成员无论什么情况都会自动调用析构函数)

6、有资源申请时,⼀定要 ⾃⼰写析构,否则会造成资源泄漏

7、C++规定后定义的先析构

实例

1、对于内置类型

如果没有资源的开辟,使用编译器生成的析构即可

2、对于自定义类型

还是以两个栈实现队列为例

#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	// ...
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:


private:
	Stack pushst;
	Stack popst;
};
int main()
{
	Stack st;
	MyQueue mq;
	return 0;
}

这里程序结束时,Stack会调用它的析构,编译器默认生成 MyQueue 的析构函数调用了 Stack 的析构,释放的 Stack 内部的资源

四、拷贝构造

概念

如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数 也叫做拷贝构造函数(拷贝构造是一个特殊的构造函数)

特点

1、第⼀个参数必须是类类型对象的引用

2、C++规定自定义类型对象进行拷贝行为必须调⽤拷贝构造(传值传参和传值返回都会调⽤拷贝构造)

3、若未显式定义,编译器会自动生成,自动生成的是浅拷贝/值拷贝(一个字节一个字节拷贝),对于自定义类型会调用它的拷贝构造

4、传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但如果返回一个当前函数局部域的局部对象,函数结束后销毁,使用引用返回就有问题(类似野指针)

实例

1、对于内置类型

需要注意拷贝构造和普通构造的区别,也要注意是否返回局部对象

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 编译报错:error C2652 : "Date":⾮法的复制构造函:第⼀个参数不应是"Date"
	//Date(Date d)
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	Date(Date* d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

void Func1(Date d)
{
	cout << &d << endl;
	d.Print();
}

Date& Func2()
{
	Date tmp(2024, 7, 5);
	tmp.Print();
	return tmp;
}

int main()
{
	Date d1(2024, 7, 5);

	// 传值传参调用拷贝构造
	Func1(d1);
	cout << &d1 << endl;

	// 这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
	Date d2(&d1);
	d1.Print();
	d2.Print();

	//这样写才是拷⻉构造,通过同类型的对象初始化构造,⽽不是指针
	Date d3(d1);
	d2.Print();

	// 也可以这样写,这⾥也是拷⻉构造
	Date d4 = d1;
	d2.Print();

	// Func2返回了⼀个局部对象tmp的引⽤作为返回值

	// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤
	Date ret = Func2();
	ret.Print();

	return 0;
}

2、对于内置类型

#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	Stack(const Stack& st)
	{
		// 需要对_a指向资源创建同样⼤的资源再拷⻉值
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	// ...
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
	//编译器默认⽣成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
	// 显⽰写析构,也会⾃动调⽤Stack的析构
	/*~MyQueue()
	{}*/
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	//Stack st;
	//MyQueue mq;

	Stack st1;
	st1.Push(1);
	st1.Push(2);
	Stack st2 = st1;

	MyQueue mq1;
	MyQueue mq2 = mq1;

	return 0;
}

Stack不显示实现拷贝构造,会用自动生成的拷贝构造完成浅拷贝

导致st1和st2中的_a指向同一块资源,析构时西沟两次,程序崩溃

而MyQueue自动生成的拷贝构造,会调用Stack的拷贝构造

只要Stack拷贝构造自己实现了深拷贝就没有问题

五、赋值重载

运算符重载

概念

当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规 定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编 译报错

形式:operator符号名

特点

1、重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多,⼀元运算符有⼀个参数,⼆元 运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数(成员函数默认第一个为this,这点需要注意一下)

2、运算符重载后,优先级和结合性与之前一致

3、.* :: sizeof ?: . 这五个运算符不能重载

4、前置++和后置++区分(--也是),C++规定后置++重载时增加一个int形参

5、重载<<和>>,由于第一个是this指针,调用时就变成了对象<<cout,这时候重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象

//ostream& out = cout,out作为返回值,成为下一次左侧参数
ostream& operator<<(ostream& out, const Date& d);

//流提取
//提取后的值放d里面,所以不加const
istream& operator>>(istream& in, Date& d);

实例

以日期类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);

赋值运算符重载

概念

用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象

Date d1;
//拷贝构造
Date d2 = d1;

Date d3;
//赋值重载
d3 = d1;

特点

1、没有显示实现时,编译器默认生成的默认赋值运算符重载完成浅拷贝,对于自定义类型成员会调用它的赋值重载函数

2、有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景

关于是否需要显示实现

总的来说,当我们有资源开辟/释放,我们就需要显示实现构造函数、析构函数和拷贝构造函数。如果⼀个类显⽰实现 了析构并释放资源,那么他就需要显示写赋值运算符重载

六、取地址运算符重载和const修饰的取地址运算符重载

这两个一般不需要重载,特殊情况,不想让别人获取到地址,将返回值改成别的

Date* operator&()
{
	return this;
}
const Date* operator&()const
{
	return this;
}

七、笔记

相关推荐
云空11 分钟前
《探秘 QT 5.14.1 类库的奇妙世界》
开发语言·qt
kjl53656614 分钟前
Antd react上传图片格式限制
开发语言·前端·javascript
o不ok!20 分钟前
java中带缓存的输入/输出流
java·开发语言
旺小仔.25 分钟前
数据结构之二叉搜索树(Binary Search Tree)
开发语言·数据结构·c++·b树·算法·链表
夕泠爱吃糖27 分钟前
如何处理对象的创建和销毁?
开发语言·c++
析木不会编程30 分钟前
【C语言】特殊指针汇总
c语言·开发语言
Komorebi_999931 分钟前
javaScript中slice()和splice()的用法与区别
开发语言·前端·javascript
橘颂TA36 分钟前
【C++】大厂算法题(详解)
开发语言·c++·算法
严就方法1 小时前
Qt creator ,语言家功能缺失解决方法
开发语言·qt
云空1 小时前
《 QT 5.14.1 类库模块列表详述》
开发语言·qt·系统架构