【C++】5.类和对象(3)

文章目录


3.析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

析构函数的特点:

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值。 (这里跟构造类似,也不需要加void)
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,系统会自动调用析构函数。
  5. 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
  6. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
  7. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄漏,如Stack
  8. 一个局部域的多个对象,C++规定后定义的先析构。
cpp 复制代码
#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;
}

对比一下用C++C实现的Stack解决之前括号匹配问题isValid,我们发现有了构造函数和析构函数确实方便了很多,不会再忘记调用InitDestory函数了,也方便了不少。

cpp 复制代码
#include<iostream>
using namespace std;
// 用最新加了构造和析构的C++版本Stack实现
bool isValid(const char* s) {
    Stack st;
    while (*s)
    {
        if (*s == '[' || *s == '(' || *s == '{')
        {
            st.Push(*s);
        }
        else
        {
            // 右括号比左括号多,数量匹配问题
            if (st.Empty())
            {
                return false;
            }
            // 栈里面取左括号
            char top = st.Top();
            st.Pop();
            // 顺序不匹配
            if ((*s == ']' && top != '[')
                || (*s == '}' && top != '{')
                || (*s == ')' && top != '('))
            {
                return false;
            }
        }
        ++s;
    }
    // 栈为空,返回真,说明数量都匹配 左括号多,右括号少匹配问题
    return st.Empty();
}
// 用之前C版本Stack实现
bool isValid(const char* s) {
    ST st;
    STInit(&st);
    while (*s)
    {
        // 左括号入栈
        if (*s == '(' || *s == '[' || *s == '{')
        {
            STPush(&st, *s);
        }
        else // 右括号取栈顶左括号尝试匹配
        {
            if (STEmpty(&st))
            {
                STDestroy(&st);
                return false;
            }
            char top = STTop(&st);
            STPop(&st);
            // 不匹配
            if ((top == '(' && *s != ')')
                || (top == '{' && *s != '}')
                || (top == '[' && *s != ']'))
            {
                STDestroy(&st);
                return false;
            }
        }
        ++s;
    }
    // 栈不为空,说明左括号比右括号多,数量不匹配
    bool ret = STEmpty(&st);
    STDestroy(&st);
    return ret;
}
int main()
{
    cout << isValid("[()][]") << endl;
    cout << isValid("[(])[]") << endl;
    return 0;
}
cpp 复制代码
/*
关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器
生成的默认析构函数,对自定类型成员调用它的析构函数。
*/
class Time
{
public:
    ~Time()
    {
        cout << "~Time()" << endl;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // 自定义类型
    Time _t;
};
int main()
{
    Date d;
    return 0;
}

/*
 程序运行结束后输出:~Time()
 在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是
 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;
 而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。
 但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函
 数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time
 类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
 main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
*/

/*
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;
有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
*/

注意:

  1. 一般情况下,有动态申请的资源,就需要显示所写的析构函数,来释放资源。

    例如:栈需要写析构

  2. 没有动态申请的资源,不需要写析构函数。因为没有资源需要释放。

    例如:

    cpp 复制代码
    class Data{
    private:
    	int _year;
    	int _month;
    	int _day;
    	int _arr[100];
    };
  3. 需要释放资源的成员都是自定义类型,不需要写析构函数。

    例如:

    cpp 复制代码
    class MyQue{
    private:
    	Stack _pushst;
    	Stack _popst;
    };

因为默认生成的构造会自动调用默认构造函数

默认生成的析构会自动调用默认析构函数


4.拷贝构造函数

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

拷贝构造的特点:

  1. 拷贝构造函数是构造函数的一个重载。
  2. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
  3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
  4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
  5. Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。
  6. 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
cpp 复制代码
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// d2(d1)
	//Date(Date& d);// 正确写法
	
    //拷贝构造函数
	Date(const Date& d)
	{
		cout << "Date(Date& d)" << endl;
		//注意:_year = d._year;这个里面的_year不是private:里面的int _year;
		//_year = d._year;这个左边的_year是d2的_year,也就是this->_year,因为this指针是d2,也就是d2传给了this
		//右边的d._year是d,也就是d1的_year
		_year = d._year;
		_month = d._month;
		_day = d._day;
		/*d._year = _year;
		d._month = _month;
		d._day = _day;*/
	}
private:
	int _year;
	int _month;
	int _day;
};

class MyQueue
{
private:
	/*Stack _pushst;
	Stack _popst;*/
};

void func(int i){}

void func(Date d) {}

int main()
{
	// 可以不写,默认生成的拷贝构造就可以用
	Date d1(2023, 4, 25);

	Date d2(d1);//Data(Data& d)里面的d是d1的别名
	//this指针是d2,也就是d2传给了this

	//内置类型直接拷贝,void func(int i);
	//直接把4个字节的10拷贝给i
	func(10);

	//自定义类型的拷贝,规定了要定义拷贝构造去拷贝
	//void func(Date d){}会先调用Date(const Date& d);然后进入void func(Date d) 
	//如果Date(const Date& d);改成了Date(Date d);那么就会出现无限递归,编译器会报错
	func(d1);

	

	// 必须自己实现,实现深拷贝
	/*Stack st1;
	Stack st2(st1);*/


	return 0;
}

警惕无穷递归!

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

	Date(const Date& d);// 正确写法

	//Date(const Date& d)   // 错误写法:编译报错,会引发无穷递归
	//{
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}

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

int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

1):内置类型成员完成值拷贝/浅拷贝

2):自定义类型成员会调用它的拷贝构造
自定义类型指向浅拷贝会出现两个问题:

  1. 析构两次,报错

  2. 一个函数修改会影响另一个函数


  1. DataMyQueue都不需要写。因为MyQueue里面会调用Stack,而Stack需要自己实现。Stack的实现和MyQueue无关。

  2. Stack需要自己实现

cpp 复制代码
class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack()" << endl;

		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		_capacity = capacity;		
		_top = 0;
	}

	// st2(st1)
	Stack(const Stack& st)
	{
		_a = (int*)malloc(sizeof(int) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		memcpy(_a, st._a, sizeof(int) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

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

private:
	int* _a = nullptr;
	int _top = 0;
	int _capacity;
};

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

class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;
};

int main()
{
	//1):内置类型成员完成值拷贝/浅拷贝
	// 可以不写,默认生成的拷贝构造就可以用
	Date d1(2023, 4, 25);
	Date d2(d1);//Data(Data& d)里面的d是d1的别名
	//this指针是d2,也就是d2传给了this

	//2);自定义类型成员会调用它的拷贝构造
	//如果只传值,那么就会导致两个函数指向了同一个空间,析构函数调用的话就崩了
	// 而且就算不析构函数也会出问题。比如给其中一个函数赋值会影响另一个函数
	
	// 所以必须调用拷贝构造函数
	// 必须自己实现拷贝构造函数,实现深拷贝
	//栈后进先出,后创建的先析构,st2先析构,st1后析构
	//添加Stack(const Stack& st);前,会报错,因为st1和st2的析构函数指向了同一个空间,而一个空间无法释放两次
	//添加Stack(const Stack& st);后,不报错了
	//Stack(const Stack& st);就是我们自己实现的深拷贝
	Stack st1;
	Stack st2(st1);

	return 0;
}

注意:

在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。


为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

cpp 复制代码
//这个采用引用返回是可以的,因为采用引用返回可以减少拷贝,而且函数结束后返回的值是没被销毁的
Stack& func1(){
    static Stack st;
    return st;
}
//这个采用引用返回是不可以的,因为函数结束后返回的值是被销毁了
Stack& func2(){
    Stack st;
    return st;
}

int main(){
    func1();
    func2();
    return 0;
}

传引用返回要谨慎,传值引用没事

cpp 复制代码
Stack& Func()
{
 	static Stack st;//改成Stack st;就不行,因为Stack st;在Func()结束后就销毁了,就会导致拷贝构造传值错误
	st.Push(1);
	st.Push(2);
	st.Push(3);
	//...

	return st;
}

int main()
{
	Stack ret = Func();
	cout << ret.Top() << endl;

	return 0;
}

cpp 复制代码
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// Date d2(d1);
	//是拷贝构造
	/*Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}*/

	// 不是拷贝构造,就是一个普通构造
	//Date(Date* p)
	//{
	//	_year = p->_year;
	//	_month = p->_month;
	//	_day = p->_day;
	//}

	//析构函数
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

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

	STDataType Top()
	{
		assert(_top > 0);

		return _a[_top - 1];
	}

	// st2(st1)
	Stack(const Stack& st)
	{
		_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;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

class MyQueue
{
public:
private:
	Stack pushst;
	Stack popst;
};

void Func(Stack st){}

void Func(int x){}

Date f()
{
	Date ret;
	//...
	return ret;
}

int main()
{
	Date d1(2024, 8, 9);
	//都是拷贝构造
	//自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝(一个字节一个字节的拷贝)
	Date d2(d1);
	Date d4 = d1;

	Date d5(f());
	Date d6 = f();

	//Satck不可以浅拷贝,因为Stack这里_a是一个指针,直接浅拷贝会导致两个指针指向同一块空间,析构就会崩溃
	Stack st1(10);
	Stack st2(st1);

	Func(st1);
	Func(1);

	MyQueue m1;
	MyQueue m2(m1);

	return 0;
}

这里就不调用拷贝构造了,因为这里是引用返回,不是传值返回

传值返回,返回的是值的拷贝,所以要调用拷贝构造

引用返回,返回的不是值的拷贝,返回的是它的别名,所以不调用拷贝构造

cpp 复制代码
Date& operator=(const Date& d)//返回的是*this这个对象的别名,*this是d4
{
    if (this != &d)//以预防d1 = d1;的情况
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    return *this;
}
相关推荐
Theodore_10222 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
‘’林花谢了春红‘’4 小时前
C++ list (链表)容器
c++·链表·list
----云烟----4 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024064 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it5 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康5 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神5 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
机器视觉知识推荐、就业指导6 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
宅小海6 小时前
scala String
大数据·开发语言·scala