c++类和对象(2):默认成员函数(上)

1.类的默认成员函数

默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值。
我们要从两个方面去学习:
(1)我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
(2) 编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?

1.1六个默认成员函数

在 C++中,类有六个默认成员函数,分别是:
 
一、构造函数
 
1. 作用:用于创建对象时进行初始化操作。
2. 特点:与类同名,可以有多个重载形式。如果程序员没有显式定义构造函数,编译器会自动生成一个默认构造函数。
 
二、析构函数
 
1. 作用:在对象销毁时被自动调用,用于释放对象占用的资源。
2. 特点:与类名相同,前面加上波浪线(~)。同样,如果程序员没有定义析构函数,编译器会自动生成一个默认析构函数。
 
三、拷贝构造函数
 
1. 作用:用一个已有的对象来初始化另一个同类对象。
2. 特点:参数是本类对象的常量引用,形如  类名(const 类名&) 。如果没有自定义,编译器会生成默认拷贝构造函数,进行浅拷贝。
 
四、赋值运算符重载函数
 
1. 作用:将一个对象赋值给另一个同类对象。
2. 特点:形如  类名& operator=(const 类名&) 。如果没有自定义,编译器会生成默认的赋值运算符重载函数,同样可能进行浅拷贝。
 
五、取地址运算符重载函数
 
1. 作用:返回对象的地址。
2. 特点:有两个版本, 类名* operator&() 用于普通取地址, const 类名* operator&() const 用于常量对象取地址。编译器默认生成的版本简单地返回对象的地址。
 
六、移动构造函数和移动赋值运算符(C++11 引入)
 
1. 作用:用于高效地转移资源,避免不必要的拷贝。
2. 特点:移动构造函数的参数是一个右值引用,形如  类名(类名&&) 。移动赋值运算符的参数也是右值引用,形如  类名& operator=(类名&&) 。如果没有显式定义,编译器可能不会生成高效的移动版本。

1.2对象被销毁的几种情况

在 C++中,对象被销毁通常发生在以下几种情况:
 
一、局部对象
 
1. 当定义局部对象的作用域结束时,该对象被销毁。
- 例如在函数内部定义的对象,当函数执行完毕返回时,函数内的局部对象会被销毁。
以下是一个示例:
void someFunction() {
	MyClass obj;
	// 其他代码
}

// 当 someFunction 执行完毕返回后,obj 对象被销毁。
 
 
二、动态分配的对象
 
1. 使用  delete  操作符释放动态分配的对象时,该对象被销毁。
例如:
 
MyClass* ptr = new MyClass();
// 其他代码
delete ptr; // 此时动态分配的对象被销毁。

 
三、全局对象和静态对象
 
1. 在程序结束时,全局对象和静态对象被销毁。
- 全局对象在整个程序的运行期间都存在,只有在程序结束时才会被销毁。
- 静态对象在其声明的作用域内保持存在,直到程序结束。
 
四、异常导致程序提前退出
 
1. 如果在程序执行过程中发生异常导致程序提前退出,那么在程序退出之前,已经创建的对象会按照创建的逆序被销毁。
 
总的来说,对象被销毁的时机取决于对象的生命周期和程序的执行流程。析构函数的自动调用确保了在对象被销毁时能够进行必要的资源清理和收尾工作。

2.构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。

2.1构造函数的特点

1. 函数名与类名相同。
2. 无返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
3. 对象实例化时系统会自动调用对应的构造函数。
4. 构造函数可以重载。
#include <iostream>
using namespace std;

class Date
{
public:
	//1.无参构造函数
	//构造函数名与类名相同,无返回值
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
    //函数可以重载
	//2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//3.全缺省构造函数
	// 全缺省和无参一般不会同时写,同时写会导致重载调用不明确
	Date(int year = 1, 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; //调⽤默认构造函数
	d1.Print();//1/1/1

	Date d2(2024, 6, 6); // 调⽤带参的构造函数
	d2.Print();  //2024/6/6

	//Date d3(2022);
	//d3.Print();
	return 0;
}
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数⼀旦用户显式定义编译器将不再生成。
6. 无参构造函数全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多人会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调用的构造就叫默认构造。
#include <iostream>
using namespace std;

class Date
{
public:
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1; //不传实参能调用的构造,就叫做默认构造
	d1.Print();

	return 0;
}

#include <iostream>
using namespace std;

class Date
{
public:
		//带参构造函数
	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; //不传实参能调用的构造,就叫做默认构造
	d1.Print();

	return 0;
}

由于类中有一个带参构造函数,所以系统不会自动生成默认构造函数,并且带参构造函数不是默认构造函数,所以编译运行会报错:没有合适的默认构造参数可以用。

7. 我们如果不写构造函数,编译器默认生成的构造对内置类型成员变量的初始化没有要求,也就是说是否初始化是不确定的,看编译器。对于自定义类型成员变量,会调用这个成员变量的默认构造函数进行初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决

说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型, 如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。
#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:
	//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	MyQueue mq;
	return 0;
}

MyQueue中没有写构造函数, 成员变量popst和pushst是自定义类型,所以编译器会生成默认构造函数对其进行初始化。

我们如果将构造函数Stack中的缺省值去掉,使得类中没有默认构造函数,这样就会报错

#include <iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	Stack(int n)
	{
		_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:
	//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	MyQueue mq;
	return 0;
}

3.析构函数

在 C++等语言中,析构函数通常在对象生命周期结束时自动被调用,无论你是否显式地编写了析构函数。
如果没有显式地编写析构函数,编译器可能会自动生成一个默认的析构函数。这个默认析构函数通常会在对象超出作用域、被 delete 操作符删除或者在其他适当的时候被调用,以执行一些基本的清理操作,比如释放对象内部不涉及动态分配资源的成员变量所占的内存空间等。
 
但是,如果对象中包含需要特殊清理操作的资源,比如动态分配的内存、打开的文件、网络连接等,就需要我们显式地编写析构函数来确保这些资源被正确释放。

3.1析构函数的作用
 

1. 资源清理
- 在对象被销毁时,析构函数会自动被调用,用于释放对象在生存期间所占用的资源,比如释放动态分配的内存、关闭文件、释放网络连接等。
- 例如在 C++中,如果一个类的对象在运行过程中动态分配了内存,在析构函数中可以使用 delete 操作符释放这些内存,以防止内存泄漏。


2. 执行收尾工作
- 可以在析构函数中执行一些在对象生命周期结束时需要进行的收尾操作,如保存数据、输出日志等。
 

3.2析构函数的特点

1.析构函数名是在类名前加上字符 ~。

2.无参数无返回值。 (这⾥跟构造类似,也不需要加void)

3.⼀个类只能有⼀个析构函数。若未显式定义,系统会自动生成默认的析构函数。

4.当对象的生命周期结束时,无论是正常结束(如函数执行完毕、对象超出作用域等)还是异常结束(如抛出异常导致程序提前退出),系统会自动调用析构函数。

#include<iostream>
using namespace std;

class SimpleClass 
{
public:
    SimpleClass() 
    {
        cout << "Constructor called." << endl;
    }
    ~SimpleClass() 
    {
        cout << "Destructor called." << endl;
    }

};

int main() 
{
    SimpleClass obj;
 
    return 0;
}

对象实例化是自动调用构造函数

生命周期结束时自动调用析造函数

5.跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。

6.还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。

7.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack。也就是说一般情况下显示申请了资源,才需要自己实现析构,其他情况都不需要显示写析构。
8.⼀个局部域的多个对象,C++规定后定义的先析构。
#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:
	//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
	// 显⽰写析构,也会⾃动调⽤Stack的析构
	//~MyQueue()
	//{
    //
	//}
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	Stack st;
	MyQueue mq;
	return 0;
}

当程序结束也就是生命周期结束时,系统自动调用析构函数


3.3“析构”的含义

在“析构函数”中,“析构”的意思是分解、解构或销毁对象的结构并释放相关资源。
 
具体来说:
 
一、分解对象结构
 
当一个对象的生命周期结束时,析构函数会被自动调用以执行一些特定的操作。这就好像对一个已经完成使命的复杂结构体进行拆解。例如在 C++中,如果一个类包含多个成员变量,其中一些可能是指针指向动态分配的内存,析构函数可以负责释放这些内存资源,确保不会出现内存泄漏。这一过程类似于将一个精心构建起来的结构逐步拆解,把各个组成部分妥善处理。
 
二、释放相关资源
 
析构函数不仅可以释放内存资源,还可以处理其他类型的资源,如关闭文件、释放网络连接、释放数据库连接等。它的作用是在对象不再需要时,将对象在其生命周期内占用的各种资源归还给系统,使得这些资源可以被其他程序或对象再次使用。这就如同在一个工程结束后,对工程中使用的各种设备和材料进行回收和清理,以便为下一个项目做好准备。
 
总之,“析构”意味着在适当的时候对对象进行清理和资源回收,确保程序的资源使用更加高效和安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值