前言:在上一篇类和对象(上)的文章中我们已经带领大家认识了类的概念,定义以及对类和对象的一些基本操作,接下来我们要逐步进入到类和对象(中)的学习。我们将逐步的介绍类和对象的核心——类和对象的六个默认成员函数。(注意:这六个默认成员函数是类和对象的核心,学好了它我们才能更好的去理解类和对象!)
文章目录
- 一,什么是成员函数?
- 二,默认成员函数的种类
- 三,六个成员函数
- 3.1构造函数
- 3.2构造函数的种类和使用
- 四,析构函数
- 4.1析构函数的作用
- 4.2析构函数的用法
- 五,拷贝构造
- 5.1拷贝构造的作用
- 5.2拷贝构造的使用
- 六,总结
一,什么是成员函数?
要学习类和对象中的六个成员函数,那我们就要先了解什么是成员函数?
- 成员函数就是在类里面定义的函数,一般定义在类里面的都称为成员如果是变量就称为成员变量,如果是函数就称为成员函数。
#include<iostream>
using namespace std;
class A
{
public://成员函数void func(){cout<<"void func()"<<endl;}
private://成员变量int _a;
}
二,默认成员函数的种类
C++的默认成员函数就是说我们没有显式的写该函数编译器会自动生成该函数就称为默认成员函数。C++有六个默认的成员函数也就是说这六个成员函数如果我们自己不写编译器就会自动生成。至于为什么要搞这些默认成员函数待学完这些默认成员函数你自然就会明白!
六个默认成员函数如下:
六个默认成员函数有三种,分别是执行初始化,拷贝,以及重载功能的函数。
- 执行初始化:构造函数,析构函数
- 执行拷贝:拷贝构造,赋值重载
- 取地址重载:两个重载函数
注意:这六个成员函数中比较重要的是前4个,后两个可以作为了解!
下面我们依次介绍这几个函数。
三,六个成员函数
3.1构造函数
构造函数的概念:构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的状态。通常与类名相同,无返回类型,支持重载。
3.2构造函数的种类和使用
在上面的构造函数中我们看到了几种构造函数的类型分别是:
- 默认构造函数:无参数,如果我们没有显式的写出来,编译器就会自动生成并进行默认初始化。
- 带参的构造函数:带参的构造函数又分为有缺省值和没有缺省值。注意全缺省的构造函数不能与无参的构造函数同时存在!因为这两个函数在调用时会引发冲突!
- 拷贝构造函数,参数是类名这个我们后面介绍。
class Date
{
public:
//默认构造函数与类名相同,无返回值,支持重载//不带参数Date(){_year = 1;_month = 1;_day = 1;}//Date(int year=1, int month=1, int day=1) 全缺省的构造函数//一般的构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << this->_year << "/" << this->_month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};
什么都不写自动调用编译生成的默认构造,对成员变量进行默认初始化(值是随机值).
通过上面的代码我们能了解到构造函数的主要特点:
自动调用:不显式写构造函数的情况下,对象创建时由编译器隐式调用,无需手动触发。
无返回值:即使语法上不写 void,也不实际返回任何值。
支持重载:一个类可以定义多个参数列表不同的构造函数。
注意:对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错!这就要使用初始化列表了,初始化列表后面介绍。
class A
{
};
class Date
{
private:int _year;int _month;int _day;A a; //自定义类a 对于a的初始化就要去调用A这个类的默认构造!
}
四,析构函数
4.1析构函数的作用
析构函数与构造函数相反,构造函数是执行初始化功能的那么析构函数就是释放资源的。析构函数发挥作用的场景是在有资源需要释放的时候使用,如果像日期类没有资源就没有必要使用析构。当然编译器也会自动调用但不影响使用。
4.2析构函数的用法
析构函数相较于构造函数不同的是构造函数名与类名相同,而析构函数是
~类名()
。
注意:若未显式定义析构函数,编译器会生成默认析构函数。
默认析构函数:
1. 对基本类型(如 int、float)无操作。
2. 对类成员调用其析构函数(按成员声明顺序逆序调用)。
class Date
{
public:
//构造函数Date(int year = 1, int month = 1, int day = 1){//在初始化之前先打印一下这个函数 这样便于我们直观的看到它在被调用cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;_year = year;_month = month;_day = day;}
//析构函数~Date(){//在释放资源之前打印一下 方便直观看到被调用cout << "~Date()" << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;return 0;
}
通过上面的代码我们可以认识到析构函数:
- 析构函数跟构造函数类似,无参数,无返回值。
- 自动调用析构函数,一个类只有一个析构若为显式定义则调用编译器生成的析构。
- 在同一个局部域中含有多个对象那么后定义的对象会先析构!
在上面的代码中我们看到析构函数并没有做什么事只是单纯的在函数内部打印方便我们直观看到调用,那这不就跟默认生成的析构一样吗?
像日期类这样没有资源的类编译器默认生成的就够用它不hi对内置类型如(int,float)做处理,但是如果是像栈这样的类那么编译器生成的析构就不能满足我们的需求了。下面我们来看栈这个类:
class Stack
{
public:Stack(int n = 4){cout << "Stack(int n = 4)" << endl;_a = (int*)malloc(sizeof(int) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}//编译器默认生成的析构函数不能满足我们的要求它不会去自动的释放资源 所以需要我们显式写析构函数~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_capacity = _top = 0;}
private:int* _a;size_t _capacity;size_t _top;
};
总结一下就是:在对象生命周期结束时系统就会自动调用析构。析构函数与构造函数类似对内置类型不做处理,对于自定义类型的成员就调用它的析构,且无论我们是否显式写析构只要类里面包含自定义类型的成员那么就会去调用它自己那个类的析构!
五,拷贝构造
5.1拷贝构造的作用
拷贝构造的作用就是使用同类型的对象去初始化创建另外一个对象,其中这两个对象都具有相同的成员变量值。可以将拷贝构造理解为给对象创建副本。
5.2拷贝构造的使用
- 拷贝构造与构造函数类似,就是参数上略有不同,所以拷贝构造是构造函数的一个重载。
- 如果没有显示定义拷贝构造,编译器就会自动生成一个拷贝构造。编译器生成的拷贝构造对内置类型会进行浅拷贝,对于自定义类型就去调用它的拷贝构造。
- C++规定了只要是自定义类型的对象进行拷贝行为就要去调用拷贝构造,无论是自定义类型的传值传参还是传值返回都要去调用拷贝构造。对于引用返回可能引发的野引用问题可以去看我之前的文章:传送门:引用返回的坑
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;_year = year;_month = month;_day = day;}//d2(d1) 拷贝构造函数生成对象的拷贝 参数类型与被拷贝对象的类型相同为DateDate(const Date& d){_year = d._year;_month = d._month;_day = d._day;}//这里拷贝构造其实也可以传指针 引用的本质其实就是指针 但是指针还需要开空间 而引用不需要开空间//所以还是优先使用引用做为函数参数/*Date(Date* d){_year = d->_year;_month = d->_month;_day = d->_day;}*/~Date(){cout << "~Date()" << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2025, 4, 24); // 构造Date d2(d1); // 拷贝构造 构成 副本(原对象)//直接赋值也是拷贝的意思Date d4 = d1; // 拷贝构造//使用指针 不推荐//Date d3(&d1); // 构造//Date d5 = &d1; // 构造return 0;
}
在上面的拷贝构造中参数一定要是传引用传参而不是传值传参,传值传参就会引发无穷递归!下面我们画图来解释:
这里要注意:像日期类内部没有资源我们可以使用编译器默认生成的拷贝构造就可以满足我们的要求;但是像栈这样的类就不一样了,栈这样的类内部有资源如果直接浅拷贝就会有问题,所以要自己写拷贝构造下面我们就来了解一下深浅拷贝问题:
class Stack
{
public://构造函数Stack(int n = 4){_a = (int*)malloc(sizeof(int) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}// st2(st1) 拷贝构造函数生成对象的拷贝 参数类型与被拷贝对象的类型相同为stckStack(const Stack& s){cout << "Stack(Stack& s)" << endl;_a = (int*)malloc(sizeof(int) * s._capacity);if (nullptr == _a){perror("malloc申请空间失败");return;}memcpy(_a, s._a, sizeof(int) * s._top);_top = s._top;_capacity = s._capacity;}
private:int* _a;size_t _capacity;size_t _top;
};
这时可能有人问那以后是不是看到成员变量带有指针的就要去写拷贝构造呢?其实不是写不写拷贝构造其实跟写析构函数一样看类里面有没有资源,一般来说有资源就要写析构和拷贝构造,所以我们也可以看一个类如果显式写了析构函数那么就要写拷贝构造。
六,总结
学完了上面的内容回答下面两个问题:
第⼀:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求?
- 当我们不写时编译器会自动生成调用对应的构造,析构,拷贝构造等函数。如果像日期类这样没有资源的类可以使用编译器自己生成的构造,析构,拷贝构造函数;相反如果有资源则需要我们自己显式写对应的函数!
第⼆:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
- 像有资源的类编译器生成的函数不能满足时比如像构造函数就要自己去创造空间,析构函数就要手动去释放空间,拷贝构造就要完成深拷贝等。
以上就是本篇文章的所有内容了,感谢各位大佬观看,制作不易还望各位大佬点赞支持一下! |