C++ | 指向类成员变量的指针 ( .* 运算符 与 ->* 运算符)

本文探讨了C++中如何使用指针访问类成员变量及方法。包括普通成员与静态成员的不同访问方式,并介绍了函数指针的应用。


我们都知道在 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;	// 静态变量类外初始化

指针访问类成员变量

1. 指针访问类中普通变量

在类中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 ,因此我们可以通过 .*->* 这种方式访问绑定的类变量。

2. 指针访问类中静态变量

类中的静态成员是所有对象所共有的,并且类中静态的成员可以通过类名调用。如 Test::mb = 10 。(注:静态成员不占用 类/对象 空间,编译器将其放在全局变量区)

因此,静态成员变量不依赖对象调用,我们可以直接进行使用如 Test::mb = 30 ,当然前提是我们需要先在全局作用域内进行类外初始化。有关资料请参考:C++静态成员变量的初始化

既然mb可以不依赖对象调用,那么我们就可以使用普通指针指向mb 。

int* sp = &Test::mb;
*sp = 30;
cout << *sp << endl;	// 输出 30

指针访问类成员方法

既然可以通过指针访问类中的变量,那么同样的我们可以通过指针访问类中的方法。

1. 指针访问类中普通方法

我们通过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)();	// 通过函数指针调用类成员方法
2. 指针访问类中静态方法

同指针访问类中静态变量一样,我们按照普通的函数指针方式即可定义指向类静态成员的指针。

// 函数名就是函数入口地址
void(*spfunc1)() = Test::static_func;
(*spfunc1)();

// 函数名取地址 ==》该函数取地址 ==》  函数入口地址
void(*spfunc2)() = &Test::static_func;
(*spfunc2)();

需要注意的是,函数名即为函数的首地址,而对函数名取地址也表示函数的首地址(可能是由于历史遗留、编程习惯写法,兼容性等问题造成的) ,因此上述函数指针的定义方法有两种,它们是等价的。

<think>我们正在讨论C#中的`.`和`->`运算符。根据之前的对话和引用,我将总结它们的区别。 引用[3]和[4]特别相关,因为它们直接提到了C#(或C++,但概念相似)。 在C#中: - `.`运算符:用于直接访问对象实例的成员(字段、属性、方法等)。当你有对象实例(而不是指针)时,使用点运算符- `->`运算符:用于通过指针访问结构体(struct)的成员。它只在 unsafe 代码上下文中使用,因为涉及到指针。 注意:在C#中,通常我们使用`.`运算符,因为C#是托管语言,不鼓励使用指针(除非在标记为unsafe的代码块中)。所以`->`运算符的使用场景非常有限,仅用于不安全代码中的指针操作。 引用[4]中的C#示例展示了`->`的用法: ```csharp // compile with: /unsafe struct Point { public int x, y; } class MainClass12 { unsafe static void Main() { Point pt = new Point(); Point* pp = &pt; pp->x = 123; // 使用->通过指针访问结构体成员 pp->y = 456; Console.WriteLine("{0} {1}", pt.x, pt.y); } } ``` 在这个例子中,`pp`是一个指向结构体`Point`的指针,我们使用`->`来访问它的字段`x`和`y`。 而在安全代码中,我们直接使用点运算符: ```csharp Point pt = new Point(); pt.x = 123; // 使用点运算符访问结构体成员 pt.y = 456; ``` 总结: | 运算符 | 使用场景 | 示例 | 是否安全 | |--------|----------|------|----------| | `.` | 访问任何对象(实例或结构体)的成员 | `obj.Member` | 安全 | | `->` | 在unsafe代码中,通过指针访问结构体的成员 | `ptr->Member` | 不安全(需要unsafe上下文) | 另外,在C#中,引用类型(类)的变量实际上是引用(类似于指针,但由CLR管理),我们依然使用点运算符来访问成员,因为不需要显式指针操作。 所以,通常我们使用点运算符.)来访问对象的成员,而只有在处理显式指针时才使用箭头运算符->),且仅限于值类型(主要是结构体)。 现在,根据要求,生成相关问题。 </think>在C#中,`.`(点运算符)和`->`(箭头运算符)是两种不同的成员访问运算符,核心区别在于**操作对象类型****使用场景**: ### 核心区别总结 | 特性 | `.` 运算符 (运算符) | `->` 运算符 (箭头运算符) | |--------------------|------------------------------------------|--------------------------------------| | **操作对象** | 对象实例或静态类 | **指针** | | **主要场景** | 安全代码(常规托管代码) | **不安全代码**(需`unsafe`上下文) | | **访问目标** | 字段、属性、方法、事件等成员 | 结构体/类的成员(通过指针间接访问) | | **是否需要指针** | ❌ 否 | ✅ 必须操作指针变量 | | **示例** | `obj.Method()` | `ptr->Field` | --- ### 详细解析 #### 1. `.` 运算符(点运算符- **本质**:直接访问对象实例或静态类的成员 - **使用条件**: ```csharp MyClass obj = new MyClass(); obj.Method(); // ✅ 访问实例方法 obj.Property; // ✅ 访问实例属性 MyClass.StaticMethod(); // ✅ 访问静态方法 ``` - **特点**- 安全代码的默认成员访问方式 - 左侧必须是实例对象或静态类 - 占代码场景的99%以上[^3] #### 2. `->` 运算符(箭头运算符- **本质**:通过**指针**间接访问结构体/类成员 - **使用条件**: ```csharp unsafe // 必须声明不安全上下文 { Point pt = new Point(); Point* ptr = &pt; // 获取指针 ptr->x = 10; // ✅ 通过指针访问成员 ptr->y = 20; // ✅ } ``` - **特点**- 仅用于不安全代码(需编译选项`/unsafe`) - 左侧必须是**指针类型**(如`MyStruct*`) - 主要用于: - 底层系统编程 - 高性能场景(如图形处理) - 非托管代码交互[^4] --- ### 关键区别图示 ```csharp // 安全代码 → 使用 . Vector3 position = transform.position; // 不安全代码 → 使用 -> unsafe { Vector3* ptr = &position; float x = ptr->x; // 等效于 (*ptr).x } ``` - `ptr->x` 本质是语法糖,等价于 `(*ptr).x`(先解引用再访问)[^2] --- ### 使用建议 1. **优先使用`.`运算符** 除非有明确需求,否则始终选择`.`(安全且高效) 2. **限制`->`的使用场景** 仅当需要直接操作内存指针时使用(如非托管库交互) 3. **编译器要求** 使用`->`需: - 添加`unsafe`代码块 - 项目启用`Allow Unsafe Code` - 避免在常规业务逻辑中使用 --- ### 相关问题 1. C#中哪些场景必须使用`unsafe`代码? 2. 如何理解C#中指针引用的本质区别? 3. 为什么常规开发不建议使用`->`运算符? 4. 除了`->`,C#中有哪些语法只能在`unsafe`上下文使用? [^3]: 点运算符用于直接访问对象实例成员 [^4]: 箭头运算符在C#中仅用于不安全代码的指针操作
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫RT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值