我们都知道在 C/C++ 中指针的使用的方式非常灵活,在C语言中指针可以指向任意的变量或是函数,并通过指针解引用的方式对其访问。
而在C++中我们引入了OOP的思想,并且产生了类这种结构,其中封装作为类的一大特性将变量和方法封装在类内部使得我们直接无法使用,而作为类实例化存在的对象成为了间接调用类内部属性和方法的载体(通过对象的this指针间接调用,需要注意的是对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this表示的是对象本身的地址)。
那么我们是否可以在类外部通过指针访问类中的数据呢?
如下,我们设计了一个测试类。
class Test{
public:
void func() { cout << "call Test : : func" << endl; }
static void static_func() { cout << "Test : : static_func" << endl; }
int ma;
static int mb;
};
int Test::mb; // 静态变量类外初始化
在类中Test中存在一个 int 类型的变量 ma 。那么我们在类外是否可以通过定义指针的方式访问 ma 。
int* p = &Test::ma;
很明显这是不可以的。因为类的封装性使得Test内部的ma对外不可见。而编译器给我们报的错是“ 错误 C2440 “初始化”: 无法从“int Test::* ”转换为“int * ”
。
根据提示信息来看是一个类型不匹配问题,那么我们按照错误提示定义一个 int Test::*
类型的指针是否能够成功初始化呢。
int Test::*p = &Test::ma;
这次编译器没有提示错误,编译成功通过。
而我们知道,类与结构体非常类似,我们之前声明的类只表示存在一个这样的结构,如果我们没有实例化产生对象,就不会存在该类的实体,也就是说我们在上一行代码的初始化是无效的,因为此时并不存在对象,我们也无法通过 p 访问一个不存在的 ma 。
同时,我们定义的 int Test::*
类型并不是一个简单的指针类型,而是一个Test类作用域下的指针类型,由于类中的数据只能通过对象访问,所以我们的指针 p 在访问时也需要通过对象才可以调用。
int Test::*p = &Test::ma;
Test t1; // 实例化对象 t1
t1.*p = 10;
cout << t1.ma << endl; // 输出 10
我们可以看到,通过对象 t1 我们成功的使 t1 对象内部的 ma 变量变成了 10 。 而这里我们使用了 .*
运算符。
int Test::*p = &Test::ma;
Test t1; // 实例化对象 t1
t1.*p = 10;
cout << t1.ma << endl; // 输出 10
Test* t2 = new Test(); // 实例化对象 t2 // 注意 delete
t2->*p = 20;
cout << t2->ma << endl; // 输出 20
这里我们又实例化了一个对象t2,并且在 t2 的调用下通过 p 修改了 t2 对象的 ma 。在这里我们使用了 ->*
运算符。
我们可以发现通过 Test::*
这种类型申请的指针 p 并不局限于某个一个对象,我们通过 对象t1 可以修改 t1.ma ,通过 对象指针t2 可以修改 t2->ma 。这就像是我们的函数指针的用法,通过指针绑定某个函数就可以通过该指针进行调用。而在这里我们让指针 p 绑定了 Test类中的变量 ma ,因此我们可以通过 .*
或 ->*
这种方式访问绑定的类变量。
类中的静态成员是所有对象所共有的,并且类中静态的成员可以通过类名调用。如 Test::mb = 10 。(注:静态成员不占用 类/对象 空间,编译器将其放在全局变量区)
因此,静态成员变量不依赖对象调用,我们可以直接进行使用如 Test::mb = 30 ,当然前提是我们需要先在全局作用域内进行类外初始化。有关资料请参考:C++静态成员变量的初始化
既然mb可以不依赖对象调用,那么我们就可以使用普通指针指向mb 。
int* sp = &Test::mb;
*sp = 30;
cout << *sp << endl; // 输出 30
既然可以通过指针访问类中的变量,那么同样的我们可以通过指针访问类中的方法。
我们通过auto 自动获得 Test::fun() 函数的类型,并且通过 typeid() 函数查看该函数类型。
auto p = &Test::func;
cout << typeid(p).name() << endl; // void (__thiscall Test::*)(void)
在输出的结果中我们可以看到函数返回值为 void 类型,函数指针类型为 Test:: *(Test作用域下的指针) ,参数为 void。调用约定为 _thiscall 。
因此我们设计如下的函数指针来指向该类成员函数。
void (Test::*pfunc)() = &Test::func;
pfunc 是一个指针,使用指针的方式自然是解引用了,而pfunc指向的又是一个函数,使用函数的方式就是函数名后加“()”,因此使用函数指针调用类方法时 形如 (T.pfunc)()
。 其中,T表示对象。
void (Test::*pfunc)() = &Test::func;
Test t1;
Test* t2 = new Test();
t1.func(); // 正常调用类成员方法
(t1.*pfunc)(); // 通过函数指针调用类成员方法
t2->func(); // 正常调用类成员方法
(t2->*pfunc)(); // 通过函数指针调用类成员方法
同指针访问类中静态变量一样,我们按照普通的函数指针方式即可定义指向类静态成员的指针。
// 函数名就是函数入口地址
void(*spfunc1)() = Test::static_func;
(*spfunc1)();
// 函数名取地址 ==》该函数取地址 ==》 函数入口地址
void(*spfunc2)() = &Test::static_func;
(*spfunc2)();
需要注意的是,函数名即为函数的首地址,而对函数名取地址也表示函数的首地址(可能是由于历史遗留、编程习惯写法,兼容性等问题造成的) ,因此上述函数指针的定义方法有两种,它们是等价的。