🔥个人主页:Quitecoder
🔥专栏:c++笔记仓
朋友们大家好,本节内容来到类和对象第二篇,本篇文章会带领大家了解this指针
目录
- 1.this指针
- 1.1this指针的引出
- 1.2this指针的特性
- 1.3思考题
- 1.4C语言和C++实现Stack的对比
1.this指针
1.1this指针的引出
首先我们定义一个日期类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, d2;
d1.Init(2005, 6, 23);
d2.Init(2024, 3, 25);
d1.Print();
d2.Print();
return 0;
}
我们来思考这么一个问题:
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,也就是说,d1和d2调用的是同一个函数,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢
首先思考,这里打印函数,访问的变量是哪里的?
void Print()
{
cout << _year << "-" << _month << "-" << _day <<endl;
}
这里访问的是private声明下的吗?
private:
int _year;
int _month;
int _day;
并不是,因为这里只是声明,并没有开辟空间,真正访问的是实例化的d1,d2
在
private
部分声明的变量_year
、_month
、_day
等,在类中只是进行了声明,实际上并没有为它们分配内存空间。**内存空间是在创建类的实例(也就是对象)**时为这些成员变量分配的。每个对象都有自己独立的一套成员变量,占用各自的内存空间
因此,当成员函数
Print()
通过this
指针(隐式指向当前对象)访问这些成员变量时,它实际上访问的是调用这个成员函数的那个==特定对象(实例)==的成员变量。每个对象的_year
、_month
和_day
都存储在各自独立的内存区域中,这些内存区域是在对象被创建时随对象一起分配的
那么我d1,d2如何找到这两个函数呢?
这里就与隐含的this
指针有关了
this指针是面向对象编程语言中的一个特殊指针,它指向调用成员函数的那个对象。通过this指针,成员函数可以访问调用它的那个对象的成员变量和成员函数。this指针是隐式传递给成员函数的,是成员函数的一个隐含参数
可以理解为,编译器处理后处理为上述的样子,调用的地方,编译器也会处理:
它会把调用对象当做形参进行传递
这里我们也能知道,为什么d1访问能打印d1,d2访问能打印d2
这个东西我们并不陌生,在前面数据结构中我们也有学过:
1.2this指针的特性
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
- this指针的类型:类类型* const,(
Date* const this
)即成员函数中,不能给this指针赋值,但是this指向的内容可以被改变
特点:
- 在形参和实参的位置,我们不能显示写出来
- 在函数内部可以使用
1.3思考题
一,this指针是存在哪里的?
不同的数据是存储在不同的区域的,思考一下this指针是存在哪个区域的呢?
const int i = 0;
int j = 1;
cout << &i << endl;
cout << &j << endl;
c++中,const定义的变量是存储在栈中的,我们可以打印它们的地址:
发现是相邻的
const int i = 0;
int j = 1;
const char* p = "abcdefg";
cout << &i << endl;
cout << &j << endl;
cout << &p << endl;
cout << (void*)p << endl;
在C++中,变量和数据的存储位置分为几个区域,主要包括栈(Stack)、堆(Heap)、全局/静态存储区(Global/Static Area)和常量区(Constant Pool)。具体到您提供的代码示例中的变量,它们的存储位置如下:
-
const int i = 0;
i
是一个常量整型变量。在C++中,const
修饰的局部变量默认存储在栈上,但是编译器优化可能会将其存储在程序的只读数据段中(常量区),尤其是当它被视为编译时常量时。然而,取地址操作&i
表明i
必须在内存中有实际的存储位置,所以它很可能位于栈上,除非进行了特殊的优化
-
int j = 1;
j
是一个非const
局部变量,存储在栈上。栈用于存储局部变量和函数调用的上下文
-
const char* p = "abcdefg";
- 这里
p
是一个指针,指向一个字符串常量。字符串常量"abcdefg"
存储在常量区(也称为字符串字面量区或只读数据段),这是因为字符串字面量在程序的整个生命周期内都不应被修改。而指针p
本身(即存储字符串地址的变量)作为局部变量,存储在栈上
- 这里
i
(取决于编译器优化)和j
存储在栈上。- 字符串常量
"abcdefg"
存储在常量区。 - 指针
p
(存储字符串常量的地址)存储在栈上。
在上述的讲解后,我们能够推出this指针的存储位置:this是一个形参,它指向调用该成员函数的对象,this指针在成员函数调用时需要被快速访问并用于访问对象的成员,所以我们推测它存储在栈上
为了提高访问速度,某些编译器可能选择将this指针存储在某个寄存器中,尤其是在成员函数调用时。这实际上可以减少内存访问次数,从而提高程序的执行效率,寄存器是CPU内部的极小量存储器,具有非常高的数据访问速度
二,判断下面程序的运行结果(this能否是空指针?)
class A
{
public:
void PrintA()
{
cout << "PrintA()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
我们发现它是可以正常运行的,我们接下来简单分析一下
尽管p被初始化为nullptr,指向A类型对象的指针p是空的,但PrintA()函数只是打印一条消息,没有访问任何对象的成员变量。这种特殊情况下,代码可运行,主要是因为成员函数的调用并没有实际依赖于this指针指向的对象实例的状态
因为PrintA()不访问对象的任何成员变量,所以这个调用在技术上不需要访问通过this指针指示的内存地址。因此,对于这种不访问任何成员变量的成员函数,通过nullptr调用可能不会导致运行时错误
简单来说,
void PrintA()
{
cout << "PrintA()" << endl;
}
这串代码传递空指针并没有任何影响
接下来看下面的代码:
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
这串代码运行异常,因为这里访问a是通过this->_a
来实现的
1.4C语言和C++实现Stack的对比
c语言实现
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++实现:
typedef struct Stack
{
DataType* array;
int capacity;
int size;
}Stack;
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语言中需用用户自己维护
感谢大家阅读!!!后续给大家带来析构函数和构造函数有关内容!