C++学习记录——오 类和对象(2)


1、构造函数

写栈的时候,我们需要写入各种函数,最前面要有初始化,退出时要有销毁,但这两个结果经常被忘记,总需要操作者记着销毁和初始化,而C++对此有不同的做法,但也并不是很简便。

C++想让类自动初始化对象。因此创造了构造函数这个概念。

创造函数是一个成员函数,用来初始化对象
特征:
1、函数名与类名相同
2、无返回值
3、对象实例化时编译器自动调用对应的构造函数
4、构造函数可以重载,一个类可以有多个构造函数

复习一下,重载需要两个函数的参数类型、个数、类型顺序至少有一个不同

有了构造函数,我们可以这样写代码。

class Stack
{
public:
	Stack()
	{
		_a = nullptr;
		_size = _capacity = 0;
	}

	Stack(int n)
	{
		_a = (int*)malloc(sizeof(int) * n);
		if (nullptr == _a)
		{
			perror("malloc fail");
			return;
		}
		_capacity = n;
		_size = 0;
	}

	void Push(int x)
	{
		_a[_size++] = x;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};
int main()
{
	Stack st;
	return 0;
}

main函数里写上Stack st,那么程序执行后st就被初始化了,会运行第一个Stack ()这个函数。如果在括号里写上数字,带上参数,那么就会调用Stack(int n) 这个函数。

Stack st(4)不可以写作st.Stack(4)。st还没有实例化,所以不能调用函数;加之不能用对象来调用构造函数,构造函数是比较特殊的。

如果写Stack st()会报错,是因为这个代码不确定意图,编译器不知道这是在单纯地声明函数,还是定义对象,所以不能使用。

2、析构函数

既然有初始化,那也要有销毁。但析构函数又不是为了销毁,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。创建的对象在main函数结束后就会跟随着栈帧销毁而销毁,而析构只是清理资源。

析构函数的特点:
1、析构函数名是在类名前加上字符~
2、无参无返回值类型
3、一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数
4、对象生命周期结束时,C++编译系统自动调用析构函数

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

C++中就可以利用 出了作用域会自动调用析构函数的特点,在写数据结构时,代码就会方便很多,并且调用某个成员函数也无需传参,因为参数都在类的私有里面,类自己调用即可。当然成员函数的定义也得写对。

3、构造和析构函数的细究

构造函数

1.基础特点

部分特点

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

这两个函数叫做默认成员函数,默认成员函数还有其他几个,会在之后几篇出现。下面详细介绍构造/析构函数。

我们用之前日期的类来写。

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;
};

int main()
{
	Date d1;
	Date d2(2023, 2, 4);
	d1.Print();
	d2.Print();
	return 0;
}

在这里插入图片描述
这里也可以用到缺省参数,把两个Dtae合并

	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date d1;
	Date d2(2023, 2, 4);
	Date d3(2023, 2);
	d1.Print();
	d2.Print();
	d3.Print();

在这里插入图片描述
有了这个缺省参数,原先的构造函数Date()就不能存在了,因为这样已经存在歧义了,编译器不知道该用哪个。

2、默认构造函数

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

当使用自动生成的构造/析构函数时,可能会有这样的结果

在这里插入图片描述

初始化呢?怎么出来这些个随机值?这也就是C++构造函数的缺陷

C++把类型分为内置(基本)类型和自定义类型。内置类型如int/char…,自定义类型则是class/struct等自己定义的类型。

自动生成的构造/函数,内置类型成员如果给了缺省值,也就是在声明时给值,那就用这个值,否则不做处理,自定义类型的成员,会调用默认构造函数,且不需要传参。

所以这里生成了随机数。写栈,写队列等数据结构时,都是自定义类型,这时候默认函数很简便。

为了解决这个缺陷,C++做了一个补丁,内置类型成员变量在类中声明时可以使用默认值,也就是缺省值。

private:
	//声明位置给缺省值
	int _year = 1;
	int _month = 1;
	int _day = 1;

如果用默认构造,那么就会使用这个缺省值,如果给了构造函数,那么就会使用构造函数里的值,不过如果构造里没有给全,剩下的就用缺省值。

	Date()
	{
		_year = 10;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	//声明位置给缺省值
	int _year = 1;
	int _month = 1;
	int _day = 1;

在这里插入图片描述

但是默认构造会产生随机值,会影响程序,所以一般需要自己写构造函数,并且对于自定义类型,使用默认构造会更不可控。

3、构造函数的唯一性

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

简单来说,不传参就可以调用构造函数。且要有一个观点的认知,默认构造函数不只是编译器自动生成的构造函数,只要无参,全缺省也是无参。

析构函数

当我们知道了构造函数的特点后,析构函数也是一样,内置不处理,自定义即调用。创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。不过对于析构要做的事,也有所不同。如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

4、拷贝构造函数

1、拷贝构造的传参

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

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 2, 6);
	Date d2;
	return 0;
}

如果想把d2变成d1的拷贝,相当于d2被初始化成d1。这样作用的构造函数就是拷贝构造函数,不过拷贝构造是构造函数的一种重载形式。那么我们在d1里写一个构造函数。

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

但是呢,这时候Date下面会有红线,也就是这样是错误写法。编译器会告诉你第一个参数不应该是Date,并且实际运行起来这将会是一个无穷递归的函数。

我们一步步理解这个问题。

void Func1(Date d)
{

}

void Func2(Date& d)
{

}

int main()
{
	Date d1(2023, 2, 6);
	//Date d2;

	Func1(d1);
	Func2(d1);
	return 0;
}

这里是一个很正常的函数,参数类型是Date,传引用和传值。但与常见的有不同的是,这是一个自定义类型的传参,如果是内置类型,那么编译器自有规则地去拷贝,但自定义类型编译器无法正常拷贝。

编译器在拷贝自定义类型时,会调用拷贝构造。如果是内置类型,比如int,那么编译器就会做浅拷贝,也就是按字节拷贝过去,实际上C/C++大多是浅拷贝,编译器也是浅拷贝。而自定义类型,比如栈,队列等等,如果是浅拷贝,那么按照字节拷贝,两个自定义类型的变量指向的会是同一块空间,在程序结束时,就会对同一块空间调用两次析构函数,这就出错了。

C++在这方面会用传引用来拷贝,这样就相当于深拷贝了。不过引用就需要考虑一个问题,d2的数据变动会影响d1。

不过还有一个问题没有解决,为什么会无穷递归?

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

上面的func函数里,有传值和传引用,调试起来看看内部如何变化:传值时,程序先走一遍Date类,会调用拷贝构造和构造函数等,再走func函数,这里就是一个纯粹的传参,Func函数将d1传给Date d;而像上面这样写拷贝构造,那么也是一样,把d1传过去,d1传过去就要调用拷贝构造,因为d1是自定义类型,所以要调用拷贝构造函数,拷贝构造函数括号里的Date d又会再次调用d1的拷贝构造,所以就是无穷递归。

用引用的拷贝构造就是这样

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

做拷贝时可以这样写。

	Date d1(2023, 2, 6);
	Date d2(d1);
	Date d3 = d1;

2、默认拷贝构造函数

拷贝构造函数和构造/析构函数一样,也是默认成员函数,所以系统会自动生成,对于日期类这样不改变资源的类,默认拷贝构造没问题;但是像栈这样的类,就会崩溃。与正常的数据不同,自定义类型除了有数据,还指向堆上的一块空间,那么这样只按值拷贝,两个栈指向同一块空间,数据一旦更改,两者都改,并且有析构两次的风险。 两个指针指向同一块空间,有一个置空后,指向NULL,那么另一个指针也会变成野指针。

拷贝构造函数的默认情况也有缺陷,那么什么时候需要自己写拷贝构造函数呢?

有析构函数存在时,需要写拷贝构造函数

这里作为一个参考,因为析构函数意味着这里有资源管理。

所以默认拷贝构造函数对于自定义类型,会调用这个成员的拷贝构造。

结束。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值