八、this 指针
1、this 指针的引出
为了引出 this 指针,我们先来简略定义一个日期类 Date:
class Date { public: void Init(int year = 1970, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Init(2022, 10, 3); d1.Print(); Date d2; d2.Init(2022, 10, 4); d2.Print(); return 0; }
对于上述类,有这样的一个问题:Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函
数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
实际上,C++中通过引入this指针解决该问题,即:C++编译器给每个 “非静态的成员函数“ 增加了一个隐藏的指针参数,让该指针指向当前对象 (函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问;只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
即上面的代码经过编译器处理后会变成下面这样:
class Date { public: void Init(Date* const this, int year = 1970, int month = 1, int day = 1) { this->_year = year; this->_month = month; this->_day = day; } void Print(Date* const this) { cout << this->_year << "-" << this->_month << "-" << this->_day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Init(&d1, 2022, 10, 3); d1.Print(&d1); Date d2; d2.Init(&d2, 2022, 10, 4); d2.Print(&d2); return 0; }
但是 this 指针参数以及对象的地址都是由编译器自动传递的,当用户主动传递时编译器会报错;不过在成员函数内部我们是可以显示的去使用 this 指针的。
2、this 指针的特性
this 指针有如下特性:
this 指针只能在 “成员函数” 的内部使用;
this 指针使用 const 修饰,且 const 位于指针*的后面;即 this 本身不能被修改,但可以修改其指向的对象 (我们可以通过 this 指针修改成员变量的值,但不能让 this 指向其他对象)
this 指针本质上是“成员函数”的一个形参,当对象调用成员函数时,将对象地址作为实参传递给 this 形参,所以对象中不存储this 指针;
this 指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过建立“成员函数”的函数栈帧时压栈传递,不需要用户主动传递。(注:由于this指针在成员函数中需要被频繁调用,所以VS对其进行了优化,由编译器通过ecx寄存器传递)
3、相关面试题
1、this指针存在哪里?
答:this 指针作为函数形参,存在于函数的栈帧中,而函数栈帧在栈区上开辟空间,所以 this 指针存在于栈区上;不过VS这个编译器对 this 指针进行了优化,使用 ecx 寄存器保存 this 指针;
2、this 指针可以为空吗?
答:this 指针作为参数传递时是可以为空的,但是如果成员函数中使用到了 this 指针,那么就会造成对空指针的解引用;
3、下面这两个程序编译运行的结果分别是什么?
//下面两段程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A //程序1 { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); return 0; } //***********************************// class A //程序2 { public: void PrintA() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
答:程序1正常运行。原因如下:
第一,虽然我们用空指针A访问了成员函数Print,但是由于成员函数并不存在于对象中,而是存在于代码段中,所以编译器并不会通过类对象p去访问成员函数,即并不会对p进行解引用;
第二,当对象是指针类型时,编译器会直接把这个指针作为形参传递给Print函数的 this 指针,而 this 作为参数传递是时可以为空的,在Print函数内部我们也并没有对 this 指针进行解引用。
程序2运行崩溃。原因如下:
程序2在 p->Print 处虽然可以正常运行,但是在Print函数内部,_a 会被转化为 this->_a,发生了空指针的解引用。
九、C语言和C++实现 Stack 的对比
1、C语言实现
typedef int DataType; typedef struct Stack { DataType* array; int capacity; int top; }Stack; void StackInit(Stack* ps) { assert(ps); ps->array = (DataType*)malloc(sizeof(DataType) * 4); if (NULL == ps->array) { perror("malloc fail\n"); exit(-1); } ps->capacity = 4; ps->top = 0; } void StackDestroy(Stack* ps) { assert(ps); if (ps->array) { free(ps->array); ps->array = NULL; ps->capacity = 0; ps->top = 0; } } void CheckCapacity(Stack* ps) { if (ps->top == ps->capacity) { int newcapacity = ps->capacity * 2; DataType* temp = (DataType*)realloc(ps->array,newcapacity * sizeof(DataType)); if (temp == NULL) { perror("realloc fail\n"); exit(-1); } ps->array = temp; ps->capacity = newcapacity; } } void StackPush(Stack* ps, DataType data) { assert(ps); CheckCapacity(ps); ps->array[ps->top] = data; ps->top++; } int StackEmpty(Stack* ps) { assert(ps); return 0 == ps->top; } void StackPop(Stack* ps) { if (StackEmpty(ps)) return; ps->top--; } DataType StackTop(Stack* ps) { assert(!StackEmpty(ps)); return ps->array[ps->top - 1]; } int StackSize(Stack* ps) { assert(ps); return ps->top; } 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结构体变量的地址;
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。
2、C++实现
typedef int DataType; class Stack { public: void Init(int N = 4) { _array = (DataType*)malloc(sizeof(DataType) * N); if (NULL == _array) { perror("malloc fail\n"); exit(-1); } _capacity = N; _top = 0; } void Push(DataType data) { CheckCapacity(); _array[_top] = data; _top++; } void Pop() { if (Empty()) return; _top--; } DataType Top() { return _array[_top - 1]; } int Empty() { return 0 == _top; } int Size() { return _top; } void Destroy() { if (_array) { free(_array); _array = NULL; _capacity = 0; _top = 0; } } void CheckCapacity() { if (_top == _capacity) { int newcapacity = _capacity * 2; DataType* temp = (DataType*)realloc(_array, newcapacity *sizeof(DataType)); if (temp == NULL) { perror("realloc fail\n"); exit(-1); } _array = temp; _capacity = newcapacity; } } private: DataType* _array; int _capacity; int _top; }; 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语言而言,C++中通过类可以将数据以及操作数据的方法进行完美结合,通过访问权限可以控制那些方法在类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。
而且每个方法不需要传递 Stack* 的参数了,编译器编译之后该参数会自动还原,即C++中 Stack* 参数是编译器维护的,而C语言中则需要用户自己维护。