C++奇迹之旅:隐含的this指针

2024-04-16 06:52
文章标签 c++ 指针 之旅 奇迹 隐含

本文主要是介绍C++奇迹之旅:隐含的this指针,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

请添加图片描述

文章目录

  • 📝this指针
  • 🌠 this指针的引出
    • 🌉 this指针的特性
    • 🌉this指针存在哪里
  • 🌠思考
  • 🌠C语言和C++实现Stack的对比
    • 🌉 C语言实现
    • 🌉 C++实现
  • 🚩总结


📝this指针

C++编程中,有一个特殊的指针叫做this指针,它在类的成员函数中扮演着重要的角色。本文将从一个简单的例子开始,逐步探讨this指针的概念、作用和用法。

🌠 this指针的引出

我们先来定义一个日期类 Date

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;date d2;d1.init(2006, 6, 27);d2.init(2024, 4, 15);d1.print();d2.print();return 0;
}

对于上述类,有这样的一个问题:
Date类中有 InitPrint 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init
数时,两个Dated1d2调用的是同一个函数,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
在这里插入图片描述
我们先看一个打印函数print(),里面访问的成员变量是哪里的,是private声明下的吗?

void Print(){cout <<_year<< "-" <<_month << "-"<< _day <<endl;}private:int _year;     // 年int _month;    // 月int _day;      // 日
};

其实不是,这里只是声明,没有分配空间,访问的是类对象d1,d2各自的成员变量

这里的 _year_month_day 并不是 Print() 函数中访问的成员变量。这些成员变量是在 private 访问说明符下声明的,但是并没有分配空间。实际上,在 Print() 函数中访问的是类对象 d1d2 各自的成员变量。当我们调用 d1.Print()d2.Print() 时,Print() 函数会分别访问 d1d2 对象的 _year_month_day 成员变量。

这里我们是怎么知道d1和d2对象怎么特定访问到各自的成员变量呢?我们在上节第三种猜想可以知道对象的存储方式:只保存成员变量,成员函数存放在公共的代码段
在这里插入图片描述
类的成员函数代码只保存一份,存放在程序的公共代码段中。每个类对象中只保存成员变量的实际数据。对象中不保存任何指向成员函数的指针。
当通过对象调用成员函数时,编译器会根据成员函数的名称和类型,找到对应的函数代码地址,并传入对象自身的this指针,来完成函数的调用
总结来说:当成员函数print()通过this指针,访问这些成员变量时,本质上来说,是访问调用该对象(d1)下的成员函数(print()),里面的函数的访问的成员变量对应是对象(d1)的成员变量。

那类对象d1,d2是怎么找到两个函数的,this 指针是一个指向当前对象的指针。当我们调用一个类的成员函数时,编译器会自动将当前对象的地址传递给 this 指针。通过 this 指针,我们可以访问当前对象的成员变量和成员函数。这在需要区分同名的成员变量和形参时很有用。
在这里插入图片描述
经过编译器处理,他会自动加上this指针,这就是说为什么叫隐含式this,而在调用的时候编译器是这这样的:

d1.print(&d1);void print(date* this){cout << this->_year << "-" << this->_month << "-" << this->_day << endl;//cout << d1->_year << "-" << d1->_month << "-" << d1->_day << endl;}d2.print(&d2);void print(date* this){cout << this->_year << "-" << this->_month << "-" << this->_day << endl;//cout << d2->_year << "-" << d2->_month << "-" << d2->_day << endl;}

首先会把调用的对象当做形参进行传递,这就是为什么每个类对象能够稳定的访问自己的类成员变量,这里不就同我们C语言数据传递地址使用指针访问变量嘛
在这里插入图片描述
在这里插入图片描述
总结来说:C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

🌉 this指针的特性

  1. this指针的类型:类型 const,即成员函数中,不能给this指针赋值*。函数的本质原型为void print(date* const this),我们在学指针时【C语言】深入解开指针(二)知道const 后的内容能不能改变,取决于const的位置,因此这里的const限制的this指针,但this指针的内容可以修改。
    在这里插入图片描述
  2. this指针是成员函数第一个隐含的指针,一般情况是由编译器ecx寄存器自动传递,不需要用户传递
    在这里插入图片描述
    注意点:
  3. 实参和形参的位置不能显示写,编译器自己加,但是在类里面可以用
    无论是哪一形参或者实参的哪一个位置显示写了,编译器都会发生报错。
    在这里插入图片描述
    在这里插入图片描述
  4. 只能在“成员函数”的内部使用
    在这里插入图片描述

🌉this指针存在哪里

假设我们有一个简单的 C++ 类 MyClass,它包含一个成员变量 x 和一个成员函数 printX()

class MyClass 
{
public:void setX(int value) {x = value;}void printX() {std::cout << "x = " << x << std::endl;}private:int x;
};

当我们创建一个 MyClass 对象并调用 printX() 函数时,内存中的情况如下:

  1. 在堆内存区域,我们创建了一个 MyClass 对象,其中包含成员变量 x

  2. 当我们调用 printX() 函数时,函数会在栈内存区域分配一块空间,用于存储函数的局部变量和参数。

  3. 在这个函数栈空间中,编译器会自动添加一个隐式的 this 指针参数。这个 this 指针指向当前正在执行 printX() 函数的 MyClass 对象在堆内存中的地址。

示意图如下:

在这里插入图片描述

printX() 函数内部,我们可以使用 this 指针来访问和操作当前对象的成员变量 x。例如:

void printX() 
{std::cout << "x = " << this->x << std::endl;
}

这里的 this->x 就是通过 this 指针访问当前对象在堆内存中的成员变量 x

printX() 函数执行完毕并返回时,函数栈空间会被释放,包括 this 指针在内的所有局部变量。但是,对象在堆内存中的成员变量 x 仍然存在,直到对象被销毁。

总结: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;
}

答案是:正常运行
首选我们知道指针p为nullptr,意思是指向A类型的对象的指针p为空,print()只做了一件事,那就是打印,没有访问对象里的成员变量,Print() 函数只是简单地打印一个字符串,Print() 函数调用并不会尝试访问任何无效的内存地址。
在这里插入图片描述
小结:成员函数的调用如果没有依赖于this指针指向对象实例化的某个成员变量的有效内存地址时,也就是不访问该对象的成员变量的的成员函数,通过nullptr调用不会发生运行时的错误,Print() 函数只是简单地打印一个字符串。那如果我们看看底层汇编:
在这里插入图片描述
好的,让我来解释一下这段汇编代码:

 1. `00B925E5 mov dword ptr [p], 0`2. 将指针 `p` 初始化为 `nullptr`(0)。它将 `p` 所指向的内存地址设置为 03. `00B925EC mov ecx, dword ptr [p]`4. 将 `p` 的值(也就是 0)加载到 `ecx` 寄存器中。在 x86 架构上,`ecx` 寄存器通常用作函数调用的第一个参数。5. `00B925EF call A::Print (0B91442h)`6. 调用了 `A::Print()` 函数。由于 `p` 的值为 0(即 `nullptr`),所以这实际上是通过一个空指针来调用成员函数。

可以看出,在VS汇编上,它就把指针存到寄存器中,然后call函数的地址,然后在函数内部,如果用这个指针去访问成员变量,就会发生运行崩溃。

  1. 下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{ 
public:void PrintA() {cout<<_a<<endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->PrintA();return 0;
}

答案是:运行崩溃
分析:如前面所说那样,this->_a,等同于空指针访问成员变量,访问内存,会发生运行时的崩溃。
在这里插入图片描述

🌠C语言和C++实现Stack的对比

🌉 C语言实现

typedef int DataType;
typedef struct Stack
{DataType* array;int capacity;int size;
}Stack;
void StackInit(Stack* ps)
{assert(ps);ps->array = (DataType*)malloc(sizeof(DataType) * 3);if (NULL == ps->array){assert(0);return;}ps->capacity = 3;ps->size = 0;
}
void StackDestroy(Stack* ps)
{assert(ps);if (ps->array){free(ps->array);ps->array = NULL;ps->capacity = 0;ps->size = 0;}
}
void CheckCapacity(Stack* ps)
{if (ps->size == ps->capacity){int newcapacity = ps->capacity * 2;DataType* temp = (DataType*)realloc(ps->array,newcapacity * sizeof(DataType));if (temp == NULL){perror("realloc申请空间失败!!!");return;}ps->array = temp;ps->capacity = newcapacity;}
}
void StackPush(Stack* ps, DataType data)
{assert(ps);CheckCapacity(ps);ps->array[ps->size] = data;ps->size++;
}
int StackEmpty(Stack* ps)
{assert(ps);return 0 == ps->size;
}
void StackPop(Stack* ps)
{if (StackEmpty(ps))return;ps->size--;
}
DataType StackTop(Stack* ps)
{assert(!StackEmpty(ps));return ps->array[ps->size - 1]
}
int StackSize(Stack* ps)
{assert(ps);return ps->size;
}
int main()
{Stack s;StackInit(&s);StackPush(&s, 1);StackPush(&s, 2);StackPush(&s, 3);StackPush(&s, 4);printf("%d\n", StackTop(&s));printf("%d\n", StackSize(&s));StackPop(&s);StackPop(&s);printf("%d\n", StackTop(&s));printf("%d\n", StackSize(&s));StackDestroy(&s);return 0;
}

可以看到,在用C语言实现时,Stack相关操作函数有以下共性:

  • 每个函数的第一个参数都是Stack*
  • 函数中必须要对第一个参数检测,因为该参数可能会为NULL
  • 函数中都是通过Stack*参数操作栈的
  • 调用时必须传递Stack结构体变量的地址
    结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据
    的方式是分离开的
    ,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出
    错。

🌉 C++实现

class A
{
public:void PrintA(){cout << _a << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->PrintA();return 0;
}typedef int DataType;
class Stack
{
public:void Init(){_array = (DataType*)malloc(sizeof(DataType) * 3);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = 3;_size = 0;}void Push(DataType data){CheckCapacity();_array[_size] = data;_size++;}void Pop(){if (Empty())return;_size--;}DataType Top() { return _array[_size - 1]; }int Empty() { return 0 == _size; }int Size() { return _size; }void Destroy(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:void CheckCapacity(){if (_size == _capacity){int newcapacity = _capacity * 2;DataType* temp = (DataType*)realloc(_array, newcapacity *sizeof(DataType));if (temp == NULL){perror("realloc申请空间失败!!!");return;}_array = temp;_capacity = newcapacity;}}
private:DataType* _array;int _capacity;int _size;
};
int main()
{Stack s;s.Init();s.Push(1);s.Push(2);s.Push(3);s.Push(4);printf("%d\n", s.Top());printf("%d\n", s.Size());s.Pop();s.Pop();printf("%d\n", s.Top());printf("%d\n", s.Size());s.Destroy();return 0;
}

C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。而且每个方法不需要传递Stack*的参数了,编译器编译之后该参数会自动还原,即C++Stack * 参数是编译器维护的,C语言中需用用户自己维护。


🚩总结

请添加图片描述

这篇关于C++奇迹之旅:隐含的this指针的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/908075

相关文章

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

go 指针接收者和值接收者的区别小结

《go指针接收者和值接收者的区别小结》在Go语言中,值接收者和指针接收者是方法定义中的两种接收者类型,本文主要介绍了go指针接收者和值接收者的区别小结,文中通过示例代码介绍的非常详细,需要的朋友们下... 目录go 指针接收者和值接收者的区别易错点辨析go 指针接收者和值接收者的区别指针接收者和值接收者的

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

C++ vector的常见用法超详细讲解

《C++vector的常见用法超详细讲解》:本文主要介绍C++vector的常见用法,包括C++中vector容器的定义、初始化方法、访问元素、常用函数及其时间复杂度,通过代码介绍的非常详细,... 目录1、vector的定义2、vector常用初始化方法1、使编程用花括号直接赋值2、使用圆括号赋值3、ve

如何高效移除C++关联容器中的元素

《如何高效移除C++关联容器中的元素》关联容器和顺序容器有着很大不同,关联容器中的元素是按照关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,本文介绍了如何高效移除C+... 目录一、简介二、移除给定位置的元素三、移除与特定键值等价的元素四、移除满足特android定条件的元

Python获取C++中返回的char*字段的两种思路

《Python获取C++中返回的char*字段的两种思路》有时候需要获取C++函数中返回来的不定长的char*字符串,本文小编为大家找到了两种解决问题的思路,感兴趣的小伙伴可以跟随小编一起学习一下... 有时候需要获取C++函数中返回来的不定长的char*字符串,目前我找到两种解决问题的思路,具体实现如下:

Java Optional避免空指针异常的实现

《JavaOptional避免空指针异常的实现》空指针异常一直是困扰开发者的常见问题之一,本文主要介绍了JavaOptional避免空指针异常的实现,帮助开发者编写更健壮、可读性更高的代码,减少因... 目录一、Optional 概述二、Optional 的创建三、Optional 的常用方法四、Optio