写在前面:
上一篇文章开始学习类和对象了,结尾还留了一个疑问,
类的成员函数究竟存放在哪里?
如果有兴趣可以去看看:http://t.csdn.cn/JilEt
这篇文章先解答这个问题然后继续学习类和对象的内容。
目录
写在前面:
1. 类的成员函数存放在哪里?
2. this指针
3. 构造函数
4. 析构函数
5. 探索构造和析构函数的更多细节
写在最后:
1. 类的成员函数存放在哪里?
实际上,类的成员函数是存放在公共代码区。
你可能会问,那这个公共代码区是在哪里呢?
这个其实我们不用关心,因为我们在调用的时候编译器会帮我们找到,
最重要其实是得理解为什么成员函数要单独放在一个区域,
而类的成员变量就有多份,每个实例化出来的类对象都有一份。
再来看一个例子:(如果这个类没有成员变量呢?)
#include using namespace std; class A { void f() {} }; int main() { cout << sizeof(A) << endl; return 0; }
输出:
1
为什么是1,没有成员变量,类的大小难道不是0吗?
实际上,没有成员变量的类保留一个字节的大小是为了占位,
表示对象存在,不存储有效数据。
2. this指针
我们来看这样一段代码:
#include 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, d2; d1.Init(2022, 1, 11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0; }
/
我们定义了一个存放日期的类,
实现了一个初识化的函数和一个打印函数,
可是我们在调用打印函数的时候,都是直接调用 Print(),
编译器是怎么区分我们的成员变量究竟是用那一份数据的呢?
实际上,类的成员函数存在着这一个隐藏的参数,
我们通常把它称作隐藏的this指针:
还是这份代码:(以Print函数为例)
#include 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; } //实际上编译器是这样操作的: //void Print(Date* const this) //{ // cout << this->_year << "-" << this->_month << "-" << this->_day << endl; //} private: int _year; int _month; int _day; }; int main() { Date d1, d2; d1.Init(2022, 1, 11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0; }
我们不能在形参或者实参显示传递this指针 ,
但是我们可以在函数里面使用this指针的,
像这样是允许的:
//#include //using namespace std; // //class A { // void f() {} //}; // //int main() //{ // cout << sizeof(A) << endl; // return 0; //} #include using namespace std; class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << this->_year << "-" << this->_month << "-" << this->_day << endl; } //实际上编译器是这样操作的: //void Print(Date* const this) //{ // cout << this->_year << "-" << this->_month << "-" << this->_day << endl; //} private: int _year; int _month; int _day; }; int main() { Date d1, d2; d1.Init(2022, 1, 11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0; }
不过我们是不能修改this指针的,
看到那个const了吗,他修饰this,所以this不能被修改,
但是this指针指向的内容是可以被改变的。
这时候问题来了,this指针是存在哪里的?
看清楚了,this指针是一个形参啊,他就跟普通的参数一样存在函数调用的栈帧里面。
这个时候来一道紧张刺激的题目试试水,看看对this指针的理解如何:
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
答案选C,
先来说A选项,这段代码没有语法错误。
再来看B选项,p调用Print函数的时候不会发生解引用,
因为Print的地址不在对象中,在调用前已经找到该函数了,所以这里没有访问空指针,
其它地方也没有访问空指针,所以也不会运行的时候崩溃,
所以答案选C,代码正常运行。
那我们再来看这一道题:
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A { public: void PrintA() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); return 0; }
答案选B,
做完第一道题目,这道题其实就已经很清楚了,
调用PrintA这个函数的时候,通过this指针调用_a,就会导致访问空指针,然后崩溃。
3. 构造函数
我们在使用C语言实现数据结构或者说使用数据结构的时候,
都必须先Init初始化一下,使用完了之后又得Destory销毁,不然会内存泄漏,
怎么说呢,用个一两次到还好,要是频繁创建销毁实在是太不方便了,
这个时候,祖师爷就想了个办法,要是能自动初始化和销毁就好了,
然后,祖师爷就设计了构造函数,专门用来做初始化工作。
构造函数的特征:
1. 函数名与类名相同
2. 没有返回值(也不需要写void)
3. 对象实例化的时候编译器自动调用对应的构造函数
4. 构造函数可以重载
为什么构造函数这么特殊?别问,问就是他是祖师爷的亲儿子。
比如说我们写一个构造函数:
#include using namespace std; class Stack { public: //构造函数 Stack(int capacity = 4) { _arr = (int*)malloc(sizeof(int) * capacity); if (malloc == nullptr) { perror("Stack::malloc::fail"); return; } _capacity = capacity; _size = 0; } //不需要这个了 //void Init() { // //... //} void Push() { //... } void Destory() { //... } private: int* _arr; int _size; int _capacity; }; int main() { Stack st; st.Push(); return 0; }
这个时候,我们不需要Init就可以直接使用这个对象而不会报错了,
因为在实例化类对象的时候就自动调用了构造函数。
4. 析构函数
析构函数与构造函数的功能正好相反,
其实就是我们前面讲的,完成Destroy的功能,
对象在销毁的时候会自动调用析构函数,完成对象中资源的清理工作。
析构函数的特征:
1. 析构函数名是在类名前加上字符 "~"
2. 无参数和返回值
3. 一个类只有一个析构,如果没有定义,系统会生成一个默认的析构函数(析构函数不能重载)
4. 对象生命周期结束时,编译器会自动调用析构函数
有了析构函数,Destroy自然也不需要了,
还是这段代码:
#include using namespace std; class Stack { public: //构造函数 Stack(int capacity = 4) { _arr = (int*)malloc(sizeof(int) * capacity); if (malloc == nullptr) { perror("Stack::malloc::fail"); return; } _capacity = capacity; _size = 0; } //不需要这个了 //void Init() { // //... //} void Push() { //... } //不需要这个了 //void Destory() { // //... //} //析构函数 ~Stack() { free(_arr); _arr = nullptr; _size = 0; _capacity = 0; } private: int* _arr; int _size; int _capacity; }; int main() { Stack st; st.Push(); return 0; }
有了构造和析构函数,以后就不用再用Init和Destroy了,解放双手。
5. 探索构造和析构函数的更多细节
来看这段代码:
class Date { public: void Print() { cout << this->_year << "-" << this->_month << "-" << this->_day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); return 0; }
我们并没有自己实现构造函数,
那类里面有构造函数吗?
根据我们刚刚学习的构造函数的特性,编译器会给我们自动生成一份默认的构造函数,
那默认的构造函数有做什么事情吗?
来看这段代码的输出:
-858993460--858993460--858993460
是的,一堆随机值,
默认生成的构造函数看起来啥也没干。
实际上,编译器默认生成的构造函数,内置类型不做处理,
而自定义类型会去调用他们自己的默认构造函数。
(自定义类型:使用struct/class定义的类型)
这里补充一下:有些编译器可能会处理内置类型(C++没有规定,所以我们默认没有处理)
默认构造函数啥都不干好像不太好,所以C++就打了个补丁:
来看例子:
#include using namespace std; class Date { public: void Print() { cout << this->_year << "-" << this->_month << "-" << this->_day << endl; } private: int _year = 2023; int _month = 6; int _day = 26; }; int main() { Date d1; d1.Print(); return 0; }
输出:
2023-6-26
可以在成员函数的声明那里给缺省值,
这个是C++11添加的新语法。
这里还有一种情况,
来看代码:
#include using namespace std; 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 << this->_year << "-" << this->_month << "-" << this->_day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); return 0; }
我们重载了构造函数,一个无参,一个全部参数带着缺省值,
这样的情况是报错的,为什么呢?
因为无参调用会出现歧义,两个构造函数都能进行无参调用,导致错误。
这里补充一个知识点,什么是默认构造函数?
不传参就调用的就是默认构造函数,无论是我们自己写的还是编译器自己生成的。
而默认构造函数只能有一个,如果我们写了,编译器就不会生成,
而上面那种情况就是右两个默认构造函数,这个规则的原理刚刚我们也分析了,
其实就是会出现歧义这个问题。
这里就再说一句:
析构函数跟构造函数差不多,如果没写析构函数,编译器会自动生成默认的析构函数,
(当然它也啥都不干)而在类内的定义的自定义类型会调用他们自己的析构函数。
写在最后:
以上就是本篇文章的内容了,感谢你的阅读。
如果感到有所收获的话可以给博主点一个赞哦。