【C++】类和对象【中上】
目录
- 一、类与对象
- 1、构造函数
- 2、析构函数
- 3、拷贝构造函数
个人主页<—请点击
C++专栏<—请点击
一、类与对象
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载、普通对象取地址、const对象取地址
,其中前四个很重要。
1、构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是对象实例化时初始化对象。构造函数的本质是为了替代初始化函数的功能。
构造函数的特点:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时系统会自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则
C++
编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。 - 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。也就是说,不传参即可调用的都是默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。
- 我们不写,编译器默认生成的构造函数,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。
在C++
中,内置类型就是语言提供的原生数据类型,内置类型可分为基本数据类型(如整数、浮点数、字符、布尔值等)和枚举类型。自定义类型就是我们使用class/struct
等自己定义的类型。
我们上期博客已经写了日期类,我们这里直接拷贝过来:
#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;return 0;
}
我们通过刚刚拷贝函数的概念知道编译器会默认生成构造函数,只是对内置类型不知道是否初始化,那我们不妨调用一下d1.print();
看看到底初始化没有。
可以看出编译器默认生成的构造函数,没有对内置类型进行初始化,此时就需要我们自己写了。
默认构造函数一:
Date()
{_year = 1;_month = 1;_day = 1;
}
此时我们再次执行程序,查看:
由于我们显示写了构造函数,所以编译器没有再自己生成默认构造函数,而是用了我们写的默认构造函数。
默认构造函数二:
Date(int year, int month, int day)
{_year = year;_month = month;_day = day;
}
// 对象实例化一定会调用对应的构造,保证了对象实例化出来一定被初始化了
Date d1;
d1.print();Date d2(2025, 1, 1);
//构造函数调用的特殊
d2.print();
我们执行上面的代码:
上下对比就可以看出这次程序没有调用一函数,因为只有二函数符合代码逻辑。
另外还有就是全缺省的构造函数,和一函数是一样的。
Date(int year = 2025, int month = 1, int day = 2)
{_year = year;_month = month;_day = day;
}
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则编译器无法区分这里是函数声明还是实例化对象。
2、析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,C++
规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。有动态开辟内存,其中存在资源的才需要释放,而日期类没有资源需要释放,严格来说也就不需要析构函数。
析构函数的特点:
- 析构函数名是在类名前加上字符
~
。 - 无参数无返回值。
- ⼀个类只能有⼀个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,系统会自动调用析构函数。
- 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数。
- 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用它的析构,也就是说自定义类型成员无论什么情况都会自动调用它的析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如
Date
;如果默认生成的析构就可以用,也就不需要显示写析构,但是有资源申请时,⼀定要自己写析构,否则会造成内存泄漏。 - ⼀个局部域的多个对象,
C++
规定后定义的先析构。
我们写一个栈类来看看,当然其中只包括构造函数和析构函数:
#include <iostream>
using namespace std;class Stack
{
public:Stack(int n = 4){cout << "Stack(int n = 4)" << endl;_a = (int*)malloc(sizeof(int) * n);_capacity = n;_top = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_capacity = 0;_top = 0;}
private:int* _a;int _capacity;int _top;
};int main()
{Stack d1;return 0;
}
我在构造和析构函数上分别做了记号我们看看调用情况:
可以看到程序分别调用了构造函数初始化栈,并且在生命周期结束时还用自动调用了析构函数防止内存泄漏。我们目前写的类中都是内置类型的成员变量,那我们不妨用两个栈做成员变量,实现一个队列类。
class Queue
{
private:Stack _a;Stack _b;
};
我们执行下列程序:
int main()
{Queue d1;return 0;
}
我们没有在队列类中实现构造函数和析构函数,但是Stack
是自定义成员变量,程序会自动调用这个成员变量的构造函数,同样的程序会在生命周期结束时自动调用Stack
成员变量的析构函数,所以Queue
不用自己实现构造函数和析构函数,编译器自动生成的就可以使用。
3、拷贝构造函数
如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。
拷贝构造的特点:
- 拷贝构造函数是构造函数的⼀个重载。
- 拷贝构造函数的第⼀个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值。
C++
规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。- 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用它的拷贝构造。
我们依旧使用日期类来看一看:
#include <iostream>
using namespace std;class Date
{
public:void print(){cout << _year << "/" << _month << "/" << _day << endl;;}Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2030, 1, 1);d1.print();Date d2(d1);d2.print();return 0;
}
这里我定义了一个日期类,然后调用构造函数初始化了d1
,这里我并没有写拷贝构造函数,但是我却使用了Date d2(d1);
,这里程序就会自动生成拷贝构造函数进行浅拷贝,我们运行看一下结果:
我们看到拷贝的效果很好,对于这种只有内置类型,没有动态开辟资源的类,编译器自动生成的拷贝构造函数就可以满足要求了,那我们还是实现一下日期类的拷贝构造函数。
拷贝构造函数:
Date(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;
}
正常运行依旧是没有问题的,这里假如我们的第一个参数不写引用,这里就会出问题:
因为它在逻辑上会引发无穷递归调用:
了解了日期类的拷贝构造函数,由于日期类中没有指向动态申请的资源,而栈中我们动态申请了一个数组,我们再来看一下栈的:
#include <iostream>
using namespace std;class Stack
{
public:Stack(int n = 4){//cout << "Stack(int n = 4)" << endl;_a = (int*)malloc(sizeof(int) * n);_capacity = n;_top = 0;}void Push(const int& x){//扩容..._a[_top++] = x;}void Print(){for (int i = 0;i < _top;i++){cout << _a[i] << " ";}cout << endl;}~Stack(){cout << "~Stack()" << endl;free(_a);_capacity = 0;_top = 0;}
private:int* _a;int _capacity;int _top;
};int main()
{Stack d1;d1.Push(1);d1.Push(2);d1.Print();Stack d2(d1);d2.Print();return 0;
}
由于我们的主要的重心放在了拷贝构造函数,所以我们就没有完善Push
函数,只是让数组中有了两个元素,从上面的代码中可以看出,我们依旧没写拷贝构造函数,那这时执行Stack d2(d1);
语句时编译器会自动生成拷贝构造函数进行浅拷贝,让我们一起调试代码,看看代码的执行情况吧:
走到return 0;
时:
这时就有人发现了,编译器可以呀,走到这里还没逝,不要着急嘛,再调试一步,编译器将万劫不复!
好了,程序被我们搞崩了,这是什么原因呢?我们知道编译器自己生成的拷贝构造函数会进行浅拷贝,也就是一个字节一个字节的拷贝,那么编译器会将d1
中动态开辟内存数组_a
的地址传给d2
中动态开辟内存数组_a
,也就是说,两个类对象中的动态数组指向的是同一块内存空间,然后我们看上图就可以发现,编译器调用了两次析构函数,调用第一次的时候没事,调用第二次的时候就有逝了,此时数组已经被free
了,指向空,再free
一次编译器不崩溃才怪。
这里不只是运行崩溃的问题,当我们修改其中一个类对象时,另一个类对象也会被修改,可谓是充满了危机。
所以面对有动态内存储存资源的不能靠编译器自动生成的拷贝构造函数,因为编译器也不知道你的类中到底封装了什么东西,它猜不透你的心思,这时候就需要我们自己实现了。
拷贝构造函数:
Stack(const Stack& d)
{cout << "Stack(const Stack& d)" << endl;_a = (int*)malloc(sizeof(int) * d._capacity);if (_a == nullptr){perror("malloc fail!");return;}memcpy(_a, d._a, sizeof(int) * d._top);_capacity = d._capacity;_top = d._top;
}
再次运行代码:
这下就没有问题了。
- 这里还有⼀个小技巧,如果⼀个类显示实现了析构并释放资源,那么它就需要显示写拷贝构造,否则就不需要。
- 传值返回会产生⼀个临时对象调用拷贝构造函数,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。传引用返回可以减少拷贝,但是⼀定要确保返回对象在当前函数结束后还在,才能用引用返回。
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~