这一篇文章我们讲一下箭头运算符的使用。在之前的一些场景下,我们已经使用到了箭头运算符,这次我们可以更深入的聊一下箭头运算符应该如何使用,以及我们如何实现自己的箭头指针。
我们还是以一个最简单的Entity类举例:
class Entity {
public:void Print() {std::cout << "Hello World!" << std::endl;}
};
然后我们随便new一个指针:
Entity* ptr = new Entity;
但是我们需要注意的是,如果我们想要通过这个指针来调用Entity类当中的Print函数,如果还按照之前我们实例化对象调用函数的方法,其实是行不通的,比如写成这样是会报错的:
ptr.Print();
因为我们现在的ptr并不是一个实例化的对象,它只是一个指针,所以这样使用“.”是会错误的,如果想要用“.”,我们需要先对指针进行dereference然后再调用这个函数:
(*ptr).Print();
但是我们可以看到,这样写其实并不直观,那么有没有一种更加直观的办法来表示?那当然是有的,就是箭头运算符:
ptr->Print();
这样我们就可以直接通过指针来调用这个方法了。这其实是一个快捷符号。
需要注意的是,->其实是一个运算符,而我们可以重载这个运算符的。
为什么我们要重载这个运算符?答案也很简单,因为在我们自己实现作用域指针的时候,如果我们还沿袭原有的对于->的定义,会意识到代码写起来会非常奇怪,比如我们实现这样一个ScopedPtr类:
class ScopedPtr {
private:Entity* ptr;
public:ScopedPtr(Entity* ptr):ptr(ptr){}Entity* GetObject() {return ptr;}~ScopedPtr() {delete ptr;}
};
那么在我们实例化了一个ScopedPtr的对象之后,如果我们想要用这个对象去调用Entity当中的函数,就会出现一个很麻烦的事情:我们需要访问ScopedPtr类的private变量才能够获得Entity类的指针!也就是说,我们要绕很大一圈才能调用Entity的函数,如果我们写出来就是这个样子的:
ptr.GetObject()->Print();
这个明显是非常麻烦的,我们希望我们自己定义的指针类是在能拥有普通new指针的全部好处的前提下,最好再可以自动销毁,而不是给我们带来这么多的困扰。所以我们需要可以重载我们的箭头运算符,能让它像普通指针一样直接访问Entity类当中的方法。
具体的使用方法如下:
Entity* operator->() {return ptr;
}
然后我们就可以发现,我们会像使用普通new指针一样使用ScopedPtr类的指针了!而且还有了可以自动销毁的功能。
ScopedPtr ptr = new Entity;
ptr->Print();
当然如果有需要的话,我们还可以有一个返回const指针的版本。
const Entity* operator->() const {return ptr;
}
但是需要注意的是,如果我们返回的是const指针,那么我们也应该只能调用const方法。通过我们这样设计重载箭头运算符,我们成功地让我们的代码保持整洁,看起来非常的nice。
除了这些之外,箭头运算符还有一个神奇的功能,那就是获取偏移量。比如我们定义这样一个三维向量的结构:
struct Vector3 {float x, y, z;
};
很简单的一个向量类,然后我们想要获取变量x,y,z关于我们对Vector3指针的偏移量,该如何获得?也就是说,如果我们有一个Vector3指针,它会指向某一个位置,但是其中的成员变量x,y,z也有自己在内存中的位置,那么成员变量的位置相对于struct指针指向的位置差了多少?我们可以用下面的语句来获得:
int offset = (int)&((Vector3*)nullptr)->x;
std::cout << offset << std::endl;
这句话具体是什么含义?也不难理解,我们首先将一个空指针强制转换成Vector3指针,也就是说我们让这个Vector3指针指向了0位置,然后我们取x,再对它取值,就是x相关于Vector3指针的位置了,至于转成int那无非就是方便输出给我们看。输出之后,我们可以发现这个值为0。同理我们可以把x换成y和z,然后会发现结果分别是4和8。
好了,以上就是全部有关于箭头运算符的内容了,希望大家能够喜欢!