C++类和对象——第二关

目录

类的默认成员函数:

(一)构造函数

(二)析构函数

(三)拷贝构造函数


类的默认成员函数:

类里面有6个特殊的成员函数分别包揽不同的功能;

(一)构造函数

说明:C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型,如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。

构造函数的特点:

(1)自动调用,在类实例化对象的时候会自动的调用构造函数。

(2)构造函数没有返回值(啥都不写,而不是说写void),名字和当前类的名字相同。

(3)构造函数可以分为三种,1.没有形参的构造,2.全缺省参数的构造,3.自动生成的构造,编译器默认生成的是无参构造。他们三个只能存在一个。如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显式定义编译器将不再⽣成。

因为在调用的时候不需要传递任何的参数,所以构造函数总结下来可以称之为0实参构造函数。

自己写了构造函数,(编译器不再生成)。

class Date {
private:
	int _year;
	int _month;
	int _day;

public:
	Date(int year = 1999, int month = 01, int day = 01) // 自己定义的全缺省构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

};

int main()
{
	Date d1(2024, 9, 23);
	Date d2;

	cout << "传参了:" << endl;
	d1.Print();

	cout << "没有传参:" << endl;
	d2.Print();

	return 0;
}

去掉自己写的构造函数的时候(使用的是编译器的默认构造):

class Date {
private:
	int _year;
	int _month;
	int _day;

public:
	//Date(int year = 1999, int month = 01, int day = 01) // 自己定义的全缺省构造函数
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}

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

};

int main()
{
	//Date d1(2024, 9, 23);
	Date d1; 
	Date d2;

	cout << "传参了:" << endl;
	d1.Print();

	cout << "没有传参:" << endl;
	d2.Print();

	return 0;
}

这里可以看到,编译器初始化成员变量是随机值。

因为c++没有明确规定构造函数的初始化规则,所以不同的编译器会有不同的初始化规则,在vs上可能会初始化为随机值,但是在其他编译器上可能就不一样,构造函数中编译器对内置类型是没有确定处理的。

同样一段代码,我们放到devc++中执行的结果:编译器的初始化结果和vs是不一样的。

tips:正因为默认的构造函数初始化打击不确定的,所以大多数构造函数都是需要自己写的。

ps:想要redpanda dev编译器的可以私我给发。

(4)构造函数支持函数重载

我们可以写着两个构造函数,无参构造和全缺省构造函数,但是会产生调用歧义。

class Date {
private:
	int _year;
	int _month;
	int _day;

public:
	Date(int year = 1999, int month = 01, int day = 01) // 自己定义的全缺省构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	 
	
	// 自己定义的无参数的默认构造函数
	//Date() 
	//{
	//	_year = 1999;
	//	_month = 07;
	//	_day = 12;
	//}

	// 自己定义的带参默认构造
	//Date(int year, int month, int day)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}

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

};

int main()
{
	Date d1; 
	Date d2(2024,2,3);

	cout << "传参了:" << endl;

	cout << "没有传参:" << endl;
	d2.Print();

	return 0;
}

还有就是:在创建对象调用的时候,是无参构造就不需要给对象传参了,是有参数构造的必须要传参,不然编译器会报错的。

(5)为了更深层次的理解构造函数,我们使用C++来实现一个栈的初始化和压栈操作:

#include <stdlib.h>
#include <iostream>
using namespace std;
class stack {
private:
	int _top;// 栈顶
	int _capacity; // 数组空间的大小
	int* _arr; // 使用动态数组来实现栈

public:
	stack(int n = 4) // 实现构造函数,和init函数差不多
	{
		_top = 0;
		_capacity = 4;
		_arr = (int*)malloc(sizeof(int) * _capacity);//先预开辟4*4 = 16个字节的空间
	}

	// 压栈入数据
	void push(int x)
	{
		// 判断栈是否满了
		if (_top == _capacity) // 如果栈满了,就重新开空间
		{
			int newcapacity = _capacity * 2;
			int* tmp = (int*)realloc(_arr, sizeof(int) * newcapacity);
			if (tmp == nullptr)
			{
				cout << "realloc fail!" << endl;
				return;
			}
			_capacity = newcapacity;
			_arr = tmp;
		}
		_arr[_top] = x;
		_top++;
	}
};

int main()
{
	stack st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	st.push(5);

	return 0;
}

可以看到我们写的栈的初始化函数就是构造函数我们没有手动调用,它在程序执行过程中自动调用了。

(二)析构函数

和构造函数相反对应的是析构函数,它们可以说是相辅相成。

C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。

析构函数的特性:

(1)函数名和对象名一样,没有返回值,没有参数,定义时在函数名前面加~:

(2)同构造函数一样,一个类只能存在一个析构函数,构造函数在创建对象时调用,析构函数在对象生命周期结束的时候调用。

class Date {
private:
	int _year;
	int _month;
	int _day;

public:
	Date(int year = 1999, int month = 01, int day = 01) // 自己定义的全缺省构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	// 自己写的析构函数
	~Date()
	{
		cout << "这里调用了析构函数:~Date()" << endl;
	}
	void Print()
	{
		cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
	}

};

int main()
{
	Date d1;
	// 这里定义的两个对象没有显示的调用析构函数如:d1.Date().
	Date d2;

	return 0;
}

step1:d1和d2两个对象当代码执行到return 0;这句代码的时候生命周期结束,我们在这句代码处打一个断点进行调试。

step2:我们接下来对代码进行逐步调试,发现代码直接跳到了我们刚自己实现的析构函数的位置

step3:程序继续执行,执行完析构函数内的程序代码之后打印结束,程序结束。

在调用析构函数的时候,有几个对象就析构几次,可以看到我们创建了两个析构函数,打印了两次。因为在main的函数栈帧中,执行规则和数据结构的栈的执行规则差不多,都是后进先出,所以在析构多个对象的时候,遵循先定义的后析构,也可以理解为生命周期先结束的最后析构。

(3)跟构造函数类似,编译器默认⽣成的析构函数对内置类型成员不做处理,自定义类型成员会调⽤他的析构函数。

(4)还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数

比如说:我们定义了一个类,里面存在另外一个类实例化的对象,我们在另一个类中实现了析构函数,在定义这个类实例化的对象的时候,这个实例化的对象生命周期结束的时候会去调用另一个类的析构函数。

(5)没有申请资源时,析构函数可以不写,有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏。

        在上面我面我们定义了一个栈的类做构造函数的例子,我们在写的时候申请了空间,但是我们没有将空间释放掉,也没有写析构函数,而编译器自己默认生成的析构函数只会清理内置类型的空间,所以其实已经造成了内存泄漏了,最终泄露的这块内存由操作系统回收。

        我们给它加上析构函数将开辟在堆上的空间释放掉。

#include <cstdlib>
#include <stdlib.h>
#include <iostream>

using namespace std;

class stack {
private:
	int _top;// 栈顶
	int _capacity; // 数组空间的大小
	int* _arr; // 使用动态数组来实现栈

public:
	stack(int n = 4) // 实现构造函数,和init函数差不多
	{
		_top = 0;
		_capacity = 4;
		_arr = (int*)malloc(sizeof(int) * _capacity);//先预开辟4*4 = 16个字节的空间
	}

	// 压栈入数据
	void push(int x)
	{
		// 判断栈是否满了
		if (_top == _capacity) // 如果栈满了,就重新开空间
		{
			int newcapacity = _capacity * 2;
			int* tmp = (int*)realloc(_arr, sizeof(int) * newcapacity);
			if (tmp == nullptr)
			{
				cout << "realloc fail!" << endl;
				return;
			}
			_capacity = newcapacity;
			_arr = tmp;
		}
		_arr[_top] = x;
		_top++;
	}

	// 自己写的析构函数
	~stack()
	{
		free(_arr);
		_arr = nullptr;
		_capacity = _top = 0;
	}
};

int main()
{
	stack st;

	return 0;

}

当然,如果可以也可以在析构函数里面申请空间,除非你有病。

(三)拷贝构造函数

需要理解的几个点:

(1)拷贝构造是构造函数的重载,他和构造函数一样,都是为了初始化一个对象。构造函数是初始化当前对象,拷贝构造是使用另一个函数来初始化当前对象。

(2)拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。

假设不是类类型的对象引用——>引发无穷递归:

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

int main()
{
	Date d1;
	Date d2(d1);

	return 0;
}

tips:为了避免发生被拷贝对象成员被修改的情况,在拷贝构造函数的参数前面加一个const限定。如在上面的日期类的拷贝构造函数中:

Date(const Date&  d) //这里加一个const修饰能够避免源对象(d1)被修改
{
    _year = d._year;
    _month = d._month;
	_day = d._day;
}
 
int main()
{
	Date d1;
	Date d2(d1);

	return 0;
}

(3)C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成

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

		Date(const Date&  d) //这里加一个const修饰能够避免源对象
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		} 

		void Print()
		{
			cout << _year << "-" << _month << "-" << _day << endl;
		}
private:
	int _year;
	int _month;
	int _day;
};

void func(Date d)
{
	d.Print();
}

int main()
{
	Date d1;
	Date d2(d1);

	func(d1); // 这里传值传参会调用拷贝构造

	return 0;
}

对代码进行调试运行过程如图:

(4)若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉)类似于memcpy函数,对⾃定义类型成员变量会调⽤他的拷⻉构造

(5)如果在定义对象(类)的时候产生了资源的申请,就需要自己写拷贝构造函数,同析构函数一样,是否有资源的申请决定是否需要自己写拷贝构造函数,一般需要自己写析构函数的话就需要自己写拷贝构造函数。

(4)(5)两点且看下面代码:

class stack {
private:
	int _top;// 栈顶
	int _capacity; // 数组空间的大小
	int* _arr; // 使用动态数组来实现栈

public:
	stack(int n = 4) // 实现构造函数,和init函数差不多
	{
		_top = 0;
		_capacity = 4;
		_arr = (int*)malloc(sizeof(int) * _capacity);//先预开辟4*4 = 16个字节的空间
	}

	// 压栈入数据
	void push(int x)
	{
		// 判断栈是否满了
		if (_top == _capacity) // 如果栈满了,就重新开空间
		{
			int newcapacity = _capacity * 2;
			int* tmp = (int*)realloc(_arr, sizeof(int) * newcapacity);
			if (tmp == nullptr)
			{
				cout << "realloc fail!" << endl;
				return;
			}
			_capacity = newcapacity;
			_arr = tmp;
		}
		_arr[_top] = x;
		_top++;
	}

	// 自己写的析构函数
	~stack()
	{
		free(_arr);
		_arr = nullptr;
		_capacity = _top = 0;
	}
};

int main()
{

	stack st1;

	stack st2(st1);//使用st1来初始化st2

	return 0;

}

当我们执行这段代码的时候会发生下列情况:

程序挂掉了,为什么?

我们来调试看一下:

对于这种情况我们就需要自己写一个拷贝构造函数了。

class stack {
private:
	int _top;// 栈顶
	int _capacity; // 数组空间的大小
	int* _arr; // 使用动态数组来实现栈

public:
	stack(int n = 4) // 实现构造函数,和init函数差不多
	{
		_top = 0;
		_capacity = 4;
		_arr = (int*)malloc(sizeof(int) * _capacity);//先预开辟4*4 = 16个字节的空间
	}

	// 压栈入数据
	void push(int x)
	{
		// 判断栈是否满了
		if (_top == _capacity) // 如果栈满了,就重新开空间
		{
			int newcapacity = _capacity * 2;
			int* tmp = (int*)realloc(_arr, sizeof(int) * newcapacity);
			if (tmp == nullptr)
			{
				cout << "realloc fail!" << endl;
				return;
			}
			_capacity = newcapacity;
			_arr = tmp;
		}
		_arr[_top] = x;
		_top++;
	}

	// 自己写的析构函数
	~stack()
	{
		free(_arr);
		_arr = nullptr;
		_capacity = _top = 0;
	}

	// 自己写的拷贝构造函数
	stack(const stack& st)
	{
		_capacity = st._capacity;
		_top = st._top;
		int new_capacity = st._capacity;
		int* tmp = (int*)malloc(sizeof(int) * new_capacity);//重新申请一片空间
		if (tmp == nullptr)
		{
			perror("malloc fail!");
			return;
		}
		_arr = tmp;
	}

};

执行代码重新调试看一下:

地址不一样了,我们重新申请了空间。

        在上面的代码调试过程中,我们发现使用编译器默认生成的拷贝构造函数不会申请空间,它会一个个字节的拷贝,拷贝动态数组的时候它拷贝的值动态数组指针变量的地址,并没有申请空间这一操作,我们将这种拷贝方式称为浅拷贝。而我们自己实现的拷贝构造函数自己申请了空间,我们将这种拷贝称为深拷贝。

        也可以使用等号的方式调用拷贝构造函数初始化对象,在承接上面的代码,我们定义一个新的对象,如:

int main()
{

	stack st1;

	stack st2(st1);//使用st1来初始化st2

	// 也可以使用等号的方式调用拷贝构造函数初始化对象
	stack st3 = st1; 

	return 0;

}

这样写不会报错。

(6) 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,产生拷贝,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。

不能返回临时对象,返回临时对象是会销毁的,里面的各种指针就成了野指针,返回的是一个被销毁了的空间,所以引用这时候是这块被销毁的空间的别名。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值