目录
前言
本篇文章全程高能,不仅讲解了this指针的隐藏面目还为大家详细讲解了构造函数与析构函数真正的实现内核,相信大家一定会有所收获的~
一.this指针
#include <iostream>
using namespace std;
class Date
{
public:
void Init(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;
Date d2;
d1.Init(2023, 1, 1);
d2.Init(2022, 1, 1);
d1.Print();//2023 1 1
d2.Print();//2022 1 1
return 0;
}
我们来剖析一下上述代码,明明都是调用同一个Printf函数,为何它们在处理的时候能够精准识别不同的对象并给予相对应的结果呢?
其实这些都是因为编译器对代码进行了隐式处理,在编译器眼中调用Print函数时会传递相应对象的地址,而this就会作为接收对象地址的指针,它是Date* 类型的。在输出的时候相关成员变量的时候也是通过this->来识别各自成功初始化的成员变量。(di传给this,通过this去访问di的成员函数...)
所以我们能够只通过调用函数而不传参达到各自成员变量的数据变化都是由于编译器下的隐式转换得出的this指针帮助实现的。
注意:this不能显示写形参与实参,但可以在类里显示出现
小拓展
其实严格来说this指针应该是Date * const this,const在*后所以修饰的是指针本身(在*之前修饰的是指向的对象),这也意味着this指针是无法被修改的。但其指向的内容还是可以改的(如d1,d2)。
this = nullptr;//错误
面试题
- this指针存在哪里?
首先不可能是在对象里面的,在我们之前计算类与对象大小的时候已经知道成员函数不会包含在对象内,那么函数里的this指针更不可能在对象内了。
this指针是个形参,那就跟局部变量一样,存在栈帧里面~
这里答案选C,也许会有疑问为什么p->_a会出现运行崩溃而p->Print()就不会呢?明明都是空指针~
_a存在于空指针p指向的对象里面,而Print函数不存在。
所以此处的p有点作用:是在编译链接的时候使编译器知道Print是属于A这个类的。
因此我们紧接着看这个问题,当成员函数里面需要访问到成员变量_a时就会报错,因为this是空指针呀,无法通过它去访问_a。前面的只是打印不涉及访问成员变量,this反而没什么用。
二.构造函数
2.1概念
class Date { public: void Init(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.Init(2023, 1, 1); d1.Print(); Date d2; d2.Init(2022, 2, 2); d2.Print(); return 0; }
有时候我们会忘记去调用初始化函数,而这样就会导致出现随机值或运行崩溃。为了避免这种情况发生,我们在C++中引入了一种更高级的初始化函数——构造函数(主要目的也是初始化)。
它最大的特点就是能在我们创建类类型对象的时候自动去初始化,而不是特意去调用初始化函数。
2.2特性
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器会自动调用对应的构造函数
- 构造函数可以重载
class Date { public: //无参数 Date() { _year = 1; _month = 1; _dya = 1; } //有参数 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(); Date d2(2022,2,2);//调用有参数 d2.Print(); Date d3();//不能这样写,会变成函数声明——比如int fun()。。。 return 0; }
不过有一点需要注意,当无参数与全缺省参数构成重载时就需要删掉其中一个重载,不然传无参的时候会造成歧义(不知道用哪个)。不过我们一般都是直接用全缺省参数比较方便点~
//不需要其他重载,只留全缺省参数 Date(int year = 2022, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
构造函数还有一个很特殊的特性,当我们没有自己去定义构造函数(去初始化)时,编译器会自己去调用默认的构造函数
不过当我们注释掉构造函数后发现编译器并没有找到默认的构造函数,所以还得我们自己来写~这里是因为就算是皇子那也分高低贵贱呐~Date类显然是一个不太受编译器待见的皇子,连个服侍他的佣人(默认构造)都没有,只能让他自力更生咯(自行构造).
重头戏来了~
class Date { public: /*Date(int year = 2022, 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; }; class Stack { public: Stack(size_t capacity = 3) { cout << "Stack(size_t capacity = 3)" << endl; _a = (int*)malloc(sizeof(int) * capacity); if (nullptr == _a) { perror("malloc申请空间失败!!!"); } _capacity = capacity; _top = 0; } private: int* _a; int _capacity; int _top; }; // 两个栈实现一个队列 class MyQueue { private: Stack _pushst; Stack _popst; int _size; }; int main() { Date d1; d1.Print(); Stack st1; MyQueue mq; return 0; }
我们再举例MyQueue类并且不去定义构造函数,可以发现它明显就是皇太子的级别哇,根本不用自己动手(自己定义构造函数),别人抢着帮它做(编译器找到并自动调用默认构造函数)。
你说说怎么就这么奇怪呢~明明说好没有写构造函数的时候编译器会调用默认构造函数,那为什么Date类就没有呢?而MyQueue为什么就有呢?两位皇子背后一定有更深的关系~
(o゜▽゜)o☆[BINGO!]通常嘛我们对int,char,指针这种语言提供的数据类型称为内置类型,而对于class,struct等我们自己定义的类型称为自定义类型,而默认构造函数只会对自定义类型生效,对内置类型不做处理~
在Date类中只有内置类型(_year,_month...)当然不会有对应的默认构造啦~因此是随机值。而在MyQueue类中既包含了自定义类型的Stack类又有内置类型int size;那么默认构造函数就会帮助Stack类中的成员变量进行初始化,因为它是自定义的嘛~
注意:C++中针对内置类型成员不初始化的缺陷特意做了改进:内置类型成员变量在类中声明可以给默认值。(这也是一种默认构造下的初始化,不过这一般只针对没写构造只能用默认构造的时候)
class Date { public: /*void Init(int year, int month, int day) { _year = year; _month = month; _day = day; }*/ void Print() { cout << _year << _month << _day << endl; } private: //对声明给缺省值,还是属于声明 int _year = 2023; int _month = 1; int _day = 1; }; int main() { Date d1; d1.Print();//2023 1 1 return 0; }
关于特性的最后一点:无参构造函数,全缺省构造函数,没写构造编译器默认生成的构造函数这三者都可以认为是默认构造函数,默认构造函数有一个限制——这三类型中只能存在一个。
class Date { public: Date() { _year = 2023; _month = 1; _day = 1; } Date(int year = 2023,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(); return 0;
三.析构函数
3.1概念
其实可以理解为销毁函数的升级版,只不过析构函数是在对象销毁时会去自动调用并且完成对象中资源的清理工作。
3.2特性
- 析构函数名是在类名前加上字符~
- 无参数返回类型
- 一个类一析构,无法重载
- 对象在生命周期结束时,会自动调用析构函数
class Stack
{
public:
Stack(size_t capacity = 3)
{
cout << "Stack(size_t capacity = 3)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败!!!");
}
_capacity = capacity;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_capacity = _top = 0;
_a = nullptr;
}
private:
int* _a;
int _capacity;
int _top;
};
int main()
{
Stack st1;
return 0;
}
在没有自定义写析构函数,而是由编译器默认生成析构时需要注意一点,如果只是遇到像Date类这种没有申请资源的类,默认生成的析构会对其进行销毁(内存回收),所以一般不用自己去写。
而如果是Stack这种申请了动态空间资源的类那么默认生成的析构就不会去清理了(不太智能),所以这种只能靠我们自己写,否则会造成资源泄漏。
其实本质上又回归到默认析构的规则上了——对内置类型不做处理(所以在Stack类中对指针类型的int* a申请的动态空间是不做处理的),对自定义类型调用默认析构函数(例如MyQueue)。