上一篇文章开始学习类和对象了,结尾还留了一个疑问,
类的成员函数究竟存放在哪里?
如果有兴趣可以去看看:http://t.csdn.cn/JilEt
这篇文章先解答这个问题然后继续学习类和对象的内容。
目录
写在前面:
1. 类的成员函数存放在哪里?
2. this指针
3. 构造函数
4. 析构函数
5. 探索构造和析构函数的更多细节
写在最后:
实际上,类的成员函数是存放在公共代码区。
你可能会问,那这个公共代码区是在哪里呢?
这个其实我们不用关心,因为我们在调用的时候编译器会帮我们找到,
最重要其实是得理解为什么成员函数要单独放在一个区域,
而类的成员变量就有多份,每个实例化出来的类对象都有一份。
再来看一个例子:(如果这个类没有成员变量呢?)
#include
using namespace std;
class A {
void f() {}
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
输出:
1
为什么是1,没有成员变量,类的大小难道不是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 << _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,就会导致访问空指针,然后崩溃。
我们在使用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就可以直接使用这个对象而不会报错了,
因为在实例化类对象的时候就自动调用了构造函数。
析构函数与构造函数的功能正好相反,
其实就是我们前面讲的,完成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了,解放双手。
来看这段代码:
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;
}
我们重载了构造函数,一个无参,一个全部参数带着缺省值,
这样的情况是报错的,为什么呢?
因为无参调用会出现歧义,两个构造函数都能进行无参调用,导致错误。
这里补充一个知识点,什么是默认构造函数?
不传参就调用的就是默认构造函数,无论是我们自己写的还是编译器自己生成的。
而默认构造函数只能有一个,如果我们写了,编译器就不会生成,
而上面那种情况就是右两个默认构造函数,这个规则的原理刚刚我们也分析了,
其实就是会出现歧义这个问题。
这里就再说一句:
析构函数跟构造函数差不多,如果没写析构函数,编译器会自动生成默认的析构函数,
(当然它也啥都不干)而在类内的定义的自定义类型会调用他们自己的析构函数。
以上就是本篇文章的内容了,感谢你的阅读。
如果感到有所收获的话可以给博主点一个赞哦。
如果文章内容有遗漏或者错误的地方欢迎私信博主或者在评论区指出~