文章目录
3.析构函数析构函数的特点: 4.拷贝构造函数拷贝构造的特点:
3.析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++
规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack
实现的Destroy
功能,而像Date
没有Destroy
,其实就是没有资源需要释放,所以严格说Date
是不需要析构函数的。
析构函数的特点:
析构函数名是在类名前加上字符~
。无参数无返回值。 (这里跟构造类似,也不需要加void
)一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。对象生命周期结束时,系统会自动调用析构函数。跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date
;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue
;但是有资源申请时,一定要自己写析构,否则会造成资源泄漏,如Stack
。一个局部域的多个对象,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;}
对比一下用C++
和C
实现的Stack
解决之前括号匹配问题isValid
,我们发现有了构造函数和析构函数确实方便了很多,不会再忘记调用Init
和Destory
函数了,也方便了不少。
#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;}
/*关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。*/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类。*/
注意:
一般情况下,有动态申请的资源,就需要显示所写的析构函数,来释放资源。
例如:栈需要写析构
没有动态申请的资源,不需要写析构函数。因为没有资源需要释放。
例如:
class Data{private:int _year;int _month;int _day;int _arr[100];};
需要释放资源的成员都是自定义类型,不需要写析构函数。
例如:
class MyQue{private:Stack _pushst;Stack _popst;};
因为默认生成的构造会自动调用默认构造函数
默认生成的析构会自动调用默认析构函数
4.拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
拷贝构造的特点:
拷贝构造函数是构造函数的一个重载。拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。C++
规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。像Date
这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack
这样的类,虽然也都是内置类型,但是_a
指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue
这样的类型内部主要是自定义类型Stack
成员,编译器自动生成的拷贝构造会调用Stack
的拷贝构造,也不需要我们显示实现MyQueue
的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。 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拷贝给ifunc(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;}
警惕无穷递归!
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):自定义类型成员会调用它的拷贝构造
自定义类型指向浅拷贝会出现两个问题:
析构两次,报错
一个函数修改会影响另一个函数
Data
和MyQueue
都不需要写。因为MyQueue
里面会调用Stack
,而Stack
需要自己实现。Stack
的实现和MyQueue
无关。
Stack
需要自己实现
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;}
注意:
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
//这个采用引用返回是可以的,因为采用引用返回可以减少拷贝,而且函数结束后返回的值是没被销毁的Stack& func1(){ static Stack st; return st;}//这个采用引用返回是不可以的,因为函数结束后返回的值是被销毁了Stack& func2(){ Stack st; return st;}int main(){ func1(); func2(); return 0;}
传引用返回要谨慎,传值引用没事
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;}
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;}
这里就不调用拷贝构造了,因为这里是引用返回,不是传值返回
传值返回,返回的是值的拷贝,所以要调用拷贝构造
引用返回,返回的不是值的拷贝,返回的是它的别名,所以不调用拷贝构造
Date& operator=(const Date& d)//返回的是*this这个对象的别名,*this是d4{ if (this != &d)//以预防d1 = d1;的情况 { _year = d._year; _month = d._month; _day = d._day; } return *this;}