欢迎来到CILMY23的博客
本篇主题为: 类和对象初探:类的实例化与对象生命周期管理,解析this指针的奥秘
个人主页:CILMY23-CSDN博客
系列专栏:Python | C++ | C语言 | 数据结构与算法
感谢观看,支持的可以给个一键三连,点赞关注+收藏。
写在前头:
本文是继上篇的续文,可以从类和对象初探开始看。
链接:
C语言转型之路:从C到C++的类与对象初探-CSDN博客https://blog.csdn.net/sobercq/article/details/137756562?spm=1001.2014.3001.5501
目录
一、类和对象
1.6 类的实例化
1.7 类对象模型
1.7.1 类对象的大小
1.7.2 类对象的存储方式
1.7.3 特殊类的大小
1.8 this指针
1.8.1 this 指针的概念
1.8.2 this 指针的特性
1.8.3 this指针存在位置
1.8.4 this指针可以为空吗
一、类和对象
1.6 类的实例化
当谈到类的实例化时,通常指的是创建类的对象,即在内存中为该类分配存储空间,并创建对象实例的过程。
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没 有分配实际的内存空间来存储它,比如:入学时填写的学生信息表,表格就可以看成是一个 类,来描述具体学生信息。
- 类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。谜语:"年纪不大,胡子一把,主人来了,就喊妈妈" 谜底:山羊
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
- 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设 计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象 才能实际存储数据,占用物理空间
- 类用private限制的成员变量是声明,不是定义,定义是需要开空间,而声明不占用实际的物理空间
对上述第五点我们来延申讲解一下:
假设我们有个日期类举例,我们在main函数中是无法访问year的,这说明它可能是个声明,也可能是个定义,从语法上,private就限制了访问,那我们把权限打开。
权限放开后,我们仍然看到以下情况:
通过上述情况我们知道,无论是公有还是私有,类中的成员变量仍然是无法访问的,而我们可以访问d1.year++,这是因为类的实例化,创建了d1对象,才开辟了空间,从而能将year++。
1.7 类对象模型
类对象模型(Class Object Model,COM)是描述C++编译器如何在内存中组织和管理类和对象的底层机制的概念。COM 描述了类成员变量和成员函数的布局方式、方法的调用方式以及派生和多重继承等问题。
它是 C++ 语言规范和实现之间的桥梁,对于理解面向对象编程的中间表示形式。
1.7.1 类对象的大小
类对象的大小遵循C语言中结构体内存对齐规则(内存对齐可以看链接),所以我们看以下的日期类
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};int main()
{Date d1;d1.Init(2024, 04, 20);cout << sizeof(d1) << endl;return 0;
}
我们知道成员变量遵循内存对齐规则,那这个类要开辟空间就需要十二字节,那函数到底算不算在内? 对象的大小要不要加两个指针?这就涉及下一个问题,类对象是如何存储的?
1.7.2 类对象的存储方式
我们通过汇编来看,相同的函数,他们call的地址是一样的,这说明类的对象函数并没有单独开辟空间,而是共用一个空间
所以我们有两种方案设计,一种是类中有成员变量和成员函数地址,一种是类中有成员变量,在一个公共区域去存储成员函数地址
方案一: 类中有成员变量和成员函数地址
方案二: 一个公共区域去存储成员函数地址
那C++选择了第二种方式,为什么?
对于方案一,每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,就会浪费空间。而代码只保存一份,在对象中保存存放代码的地址,也是同理。因此选择了方案二。
因此来回答第一小节中的问题
那这个类要开辟空间就需要十二字节,那函数到底算不算在内? 对象的大小要不要加两个指针?
答案显而易见,这个类是十二字节大小,函数不算在内,对象的大小不需要加两个指针。
1.7.3 特殊类的大小
接下来还有两个特殊类的大小:
class A1
{
public:void f1(){}
};class A2
{};cout << sizeof(A1) << endl;cout << sizeof(A2) << endl;
结果:
这两个类都是没有成员变量的类,它们的大小理论来说都是0,而这里给了1字节,如果没有空间的话,就无法实例化,所以可以理解为是一个规定,是一个占位,标识对象实例化时,定义出来存在过。
1.8 this指针
1.8.1 this 指针的概念
首先我们先通过一个日期类来看一个例子
有个问题,为什么d1对象打印的是0417,d2对象打印的是0418,明明二者用的都是相同的函数?
这里面其实就涉及了一个C++中隐含的 this 指针。
C++编译器给每个“非静态的成员函数“增加了一个隐藏的 this 指针参数,这个 this 指针是一个指向当前对象(函数运行时调用该函数的对象)的指针,它是类的成员函数中的一个隐式参数。当类的成员函数被调用时,this 指针被自动传递给成员函数,指向调用该函数的对象实例。通过 this 指针,成员函数可以访问调用它的对象的所有成员变量和成员函数。
注意:this 指针是成员函数的第一个参数
例如:成员函数被编译器自动处理成如下形式
void Print(Date* const this)
{cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
而调用的地方会被编译器处理成如下形式:
d1.Print(&d1);
d2.Print(&d2);
1.8.2 this 指针的特性
- this 指针的类型:类类型 * const,即成员函数中,不能给 this 指针赋值。
- 只能在“成员函数”的内部使用
- this 指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this 形参。所以对象中不存储 this 指针。
- this 指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递
1.8.3 this指针存在位置
根据上面的基本四点,我们可以了解this指针了,但有个问题,this指针不存在对象中,那存在哪里?
a.堆 b.栈 c.静态区 d.常量区 e.对象中
答案是b,因为this指针是函数的形参。
这里就必须掏出我们熟悉的内存区域划分图了, 过去我们在讲C语言动态内存分布的时候说过,栈区是局部变量和形式参数的区域,堆区是动态内存区域,静态区是数据段,这里是存放全局变量和静态数据的地方,常量区也就是代码段。
现在我们就将其整理详细点:
所以对this指针存的位置我们马上就可以分辨出来了,因为this指针是函数的形式参数,所以它存在栈上。
1.8.4 this指针可以为空吗
首先我们来看一段代码
下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
答案是:正常运行
我们接着再看下一段代码:
下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:void PrintA(){cout << _a << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->PrintA();return 0;
}
答案是: 运行崩溃
这就涉及到 -> 会不会解引用的问题了
第一种情况,-> 不会解引用,因为 Print 不在 p 的空间内,此时它的意义就是给 this 指针传了 p 的地址,此时 this 指针是空指针,假设 _a 存在 p 的空间,是公有的,那么 -> 就会选择对 this 指针解引用,因为它要到 p 的空间里去找 _a ,所以此时就会报错,那么同理第二个 PrintA 就会运行崩溃。
总结:空指针的传递并不会发生问题,而空指针的访问会出现问题。
总结:
- 类的成员变量是声明,不是定义,定义是需要开空间,而声明不占用实际的物理空间
- 类的实例化是开辟空间,创建对象
- 类的成员变量大小遵循结构体内存对齐原则
- 一个类的大小,实际就是该类”成员变量”之和,遵循内存对齐。
- 空类的大小,空类比较特殊,编译器给了空类1个字节来唯一标识这个类的对象。没有成员变量的类也是如此。
- 内存对齐可以指定对齐数,用 #pragma pack()就可以指定对齐数
- 编译器搜索从局部到全局
- 关于this指针有以下四点
- 形参和实参的位置,我们不能显示写
- 成员函数内部可以使用
- this指针存在栈上,因为它是形式参数
- this指针在某些编译器如vs上会用寄存器传递,这是为了提高效率
- -> 会不会解引用,这还要取决于它要得到什么。
结构体内存对齐规则:
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
感谢各位同伴的支持,本期C++篇就讲解到这啦,如果你觉得写的不错的话,可以给个一键三连,点赞关注+收藏,若有不足,欢迎各位在评论区讨论。