【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针(二)

简介: 本章将正式开始学习C++中的面向对象,本篇博客涵盖讲解 访问限定符、封装的基础知识、类的作用域和实例化、探究类对象的存储和对于this指针由浅入深地讲解。

Ⅳ.  类对象模型


0x00 计算类的存储大小

类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?我们该如何计算一个类的大小呢?


❓ 比如这个栈和它定义出来的对象是多大呢?


💬 Stack.h


#include <iostream>
class Stack {
  public:
  void Init();
  void Push(int x);
  private:
  int* _array;
  int  _top;
  int  _capacity;
};

💬 Stack.cpp


#include "Stack.h"
using namespace std;
void Stack::Init() {
  _array = nullptr;
  _top = _capacity = 0;
}
int main(void)
{
  Stack s;
  s.Init();
  cout << sizeof(Stack) << endl;
  cout << sizeof(s) << endl;
  return 0;
}


🚩 运行结果如下:(64位环境)

7917573836b7378be6a467f06cbfd617_95e51a9ff5a544c3b367984b7ab3f5a9.png

对象中存了成员变量,是否存了成员函数呢?没存成员函数!


计算类或类对象的大小只看成员变量!


并且要考虑内存对齐,C++内存对齐规则和C结构体一致。


0x01 类对象的存储方式猜测

① 对象中包含类的各个成员:

04c3ab4ed24762a92891e5941f6d76df_424ee43f6db74c118d0cdd4b1a290f83.png


缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次会浪费空间。


② 只保存成员变量,成员函数存放在公共的代码段:

9ab3896d3d102232864ff26d8f59a2d6_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5p-g5qqs5Y-25a2QQw==,size_20,color_FFFFFF,t_70,g_se,x_16.png


❓ 对于上述两种存储方式,那计算机到底是按照哪种方式来存储的呢?


// 类中既有成员变量,又有成员函数
class A1 {
public:
  void f1() {}
private:
  int _a;
};
// 类中仅有成员函数
class A2 {
public:
  void f2() {}
};
// 类中什么都没有 - 空类
class A3
{};
sizeof(A1): 4
sizeof(A2): 1
sizeof(A3): 1


❓ A2 没有成员变量,A3 更是什么都没有,为什么大小是 1 呢?为什么不是 0 呢?


int main(void)
{
    A2 aa;
    A2 bb;
    cout << &aa << endl;
    cout << &bb << endl;
}


💡 我们尝试给 aa 对象 和 bb 对象取地址,它们是有地址的,


取地址就是要拿出它存储空间的那块,所以这里总不能给一个空指针吧?




如果大小给 0 的话就没办法区分空间了。


所以,空类会给 1 字节,这 1 字节不存储有效数据,只是为了占个坑,表示对象存在。


🔺 结论:一个类的大小,实际就是该类中成员变量之和。当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。


0x02 内存对齐规则

C++ 内存对齐规则和 C语言 中的结构体内存对齐规则是一样的。


(以下内容是C语言教学部分的内容,如果没印象了可以复习一下)


💬 我们先来观察下面的代码:


#include <stdio.h>
struct S
{
    char c1; // 1
    int i; // 4
    char c2; // 1
};
int main()
{
    struct S s = { 0 };
    printf("%d\n", sizeof(s));
    return 0;
}

🚩  12


❓ 为什么是12呢?这就涉及到结构体内存对齐的问题了。

4e5b278780fcdfcca62f7b421269348e_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MDUwMjg2Mg==,size_16,color_FFFFFF,t_70.png



📚 结构体的对齐规则:


① 结构体的第一个成员放在结构体变量在内存中存储位置的0偏移处开始。


② 从第2个成员往后的所有成员,都要放在一个对齐数(成员的大小和默认对齐数的较小值)的整数的整数倍的地址处。VS 中默认对齐数为8!


③ 结构体的总大小是结构体的所有成员的对齐数中最大的那个对齐数的整数倍。


④ 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。


📌 注意事项:VS 中默认对其数为8,Linux中没有默认对齐数概念!

19890adbc9b2a1e3225d28b0b3ff7c9f_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MDUwMjg2Mg==,size_16,color_FFFFFF,t_70.png



Ⅴ.  this指针


0x00 this指针的引出



💬 为了能够更好地讲解,我们首先来定义一个日期类 Date:


#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(void)
{
  Date d1;
  d1.Init(2022, 3, 7);
  d1.Print();
  Date d2;
  d2.Init(2022, 5, 20);
  d2.Print();
  return 0;
}

🚩 运行结果:

1d19bae99488a0aeeeba6f02f1224b48_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5p-g5qqs5Y-25a2QQw==,size_20,color_FFFFFF,t_70,g_se,x_16.png


❓ 这里我们思考一个问题:


Date 类中有 Init 和 Print 两个成员函数,函数体中没有关于不同对象的区分,那当 d1 调用 Print 函数时,这个 Print 函数是如何知道要打印 d1 对象的?而不是去打印 d2 对象呢?

fd7224dcaba1debd1aed3b17e35c56c0_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5p-g5qqs5Y-25a2QQw==,size_20,color_FFFFFF,t_70,g_se,x_16.png


因为 C++ 在这有一个隐藏的东西 —— this 指针!


💡 C++ 通过引入 this 指针解决该问题。


📚 C++ 编译器给每个 "非静态的成员函数" 增加了一个隐藏的指针参数,


让该指针指向当前对象(函数运行时调用该函数的对象),它是系统自动生成的,


在函数体中所有成员变量的操作,都是通过该指针去访问。


只不过所有的操作对程序员来说是透明的,


就是不需要程序员自己来传递,编译器自动帮你去完成。

7922dc95ca525bca0d18d522f17a06b9_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5p-g5qqs5Y-25a2QQw==,size_20,color_FFFFFF,t_70,g_se,x_16.png


💬 当然,Init 也会被处理(注释的是隐藏前的内容):


#include <iostream>
using namespace std;
class Date {
public:
  // void Init(Date* this, int year, int month, int day)
  void Init(int year, int month, int day) {
  _year = year;
  _month = month;
  _day = day;
  //this->_year = year;
  //this->_month = month;
  //this->_day = day;
  }
  // void Print(Date* this)
  void Print() {
  cout << _year << "-" << _month << "-" << _day << endl;
  // cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Date d1;
  d1.Init(2022, 3, 7);    // d1.Init(&d1, 2022, 3, 7);
  d1.Print();             // d1.Print(&d1);
  Date d2;
  d2.Init(2022, 5, 20);   // d2.Init(&d2, 2022, 5, 20);
  d2.Print();             // d2.Print(&d2);
  return 0;
}

0x01 this 使用细则

📌 注意事项:this 是作为一个关键字存在的


① 调用成员函数时,不能 "显示地" 传实参给 this :

ec0bb0ce1208368cf7c27e0a734c48e9_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5p-g5qqs5Y-25a2QQw==,size_20,color_FFFFFF,t_70,g_se,x_16.png

② 定义成员函数时,也不能 "显示地" 声明形参 this :

760039261231ba1c4c267dd51fda2deb_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5p-g5qqs5Y-25a2QQw==,size_20,color_FFFFFF,t_70,g_se,x_16.png


③ 但是,在成员函数内部,我们可以 "显示地" 使用 this :


4a7b006e60d67bd8e47bac556ed26df8_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5p-g5qqs5Y-25a2QQw==,size_20,color_FFFFFF,t_70,g_se,x_16.png


也就是说,你不写 this 他会自动加,你写了他也是允许你写的。


❓ 这是为什么呢? 因为有些地方我们用得到这个 this,这个我们下一章会讲。(返回本体)


虽然可以 "显示地" 用,但是一般情况下我们都不会自己 "显示地写" 。


因为没有必要,你不写他也会自动加上去的:


void Print() {
    cout << _year << "-" << _month << "-" << _day << endl;
  // cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}

但是你想写也没人会拦你,简单说就是 ——  



0x03 this指针中的const

📌 注意事项: this 指针是不能被改变的。


因为考虑到这是第一次讲解 this 指针,为了方便由浅入深地讲解,


我们选择了 —— 战术装瞎 ,忽略了 this 指针中的 const 来讲解的。


其实 this 指针还被 const 修饰。


this 指针的本质是一个常量指针,是通过 const 修饰 this 指针指向的内存空间。


💬 不信?你改改看:

493c82853d8ac84958e97f8a1b6eb79e_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5p-g5qqs5Y-25a2QQw==,size_20,color_FFFFFF,t_70,g_se,x_16.png

💬 写得更准确些(注释的是隐藏前的内容):

4c376a5dcb90a714a3058da56eafc8ad_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5p-g5qqs5Y-25a2QQw==,size_20,color_FFFFFF,t_70,g_se,x_16.png

class Date {
public:
  // void Init(Date* const this, int year, int month, int day)
  void Init(int year, int month, int day) {
  _year = year;
  _month = month;
  _day = day;
  }
  // void Print(Date* const this)
  void Print() {
  cout << _year << "-" << _month << "-" << _day << endl;
  // cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

0x04 this指针的特性

① this 指针的类型:  类类型* const


② this 指针只能在 "成员函数" 的内部使用。


③ this 指针本质上是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给 this 形参。所以对象中不存储 this 指针。


④ this 指针是成员函数第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。

2728d5118828c7ee53852cb0086c1b2c_watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5p-g5qqs5Y-25a2QQw==,size_20,color_FFFFFF,t_70,g_se,x_16.png



相关文章
|
2月前
|
程序员 编译器 C++
【C++核心】C++内存分区模型分析
这篇文章详细解释了C++程序执行时内存的四个区域:代码区、全局区、栈区和堆区,以及如何在这些区域中分配和释放内存。
51 2
|
7天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
25 4
|
1月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
29 2
|
3月前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
|
4月前
|
存储 数据格式 运维
开发与运维C++问题之更改数据模型为通用数据结构如何解决
开发与运维C++问题之更改数据模型为通用数据结构如何解决
28 1
|
4月前
|
编译器 程序员 调度
协程问题之C++20 的协程实现是基于哪种协程模型的
协程问题之C++20 的协程实现是基于哪种协程模型的
|
4月前
|
机器学习/深度学习 PyTorch 算法框架/工具
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
|
5月前
|
Java
2022蓝桥杯大赛软件类国赛Java大学B组 左移右移 空间换时间+双指针
2022蓝桥杯大赛软件类国赛Java大学B组 左移右移 空间换时间+双指针
43 3
|
5月前
|
存储 Java C#
C++语言模板类对原生指针的封装与模拟
C++|智能指针的智能性和指针性:模板类对原生指针的封装与模拟
|
5月前
|
C++
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)