【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



相关文章
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
10月前
|
机器学习/深度学习 存储 算法
基于 C++ 布隆过滤器算法的局域网上网行为控制:URL 访问过滤的高效实现研究
本文探讨了一种基于布隆过滤器的局域网上网行为控制方法,旨在解决传统黑白名单机制在处理海量URL数据时存储与查询效率低的问题。通过C++实现URL访问过滤功能,实验表明该方法可将内存占用降至传统方案的八分之一,查询速度提升约40%,假阳性率可控。研究为优化企业网络管理提供了新思路,并提出结合机器学习、改进哈希函数及分布式协同等未来优化方向。
289 0
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
12月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
468 12
|
10月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
253 0
|
10月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
396 0