从一个修改私有变量的问题想到的

本文探讨了在不修改类代码且不使用友元的情况下,如何访问C++类的私有成员变量。通过多种方法,包括指针转换、联合体以及模具类等手段,解决了不同复杂度下的问题。

前几天,在论坛里看到一个帖子,内容是:
(原帖见:http://community.youkuaiyun.com/Expert/topic/5014/5014384.xml?temp=.3018152
========================================
class a
{
private:
int k;
};
要求不用友元,不在这个类里添加任何代码,去访问成员变量k。
能做出的高手,请贴出完整源码,以便大家测试。
========================================

这道题目本身所要解决的问题并没有多少现实意义,但如果试着去解决它,以及比它更复杂的情况,我认为至少对理解C++的对象模型是很有帮助的。

开始讨论之前不得不说,这道题做为一个题目存在逻辑上的重大缺陷:“不给类增加一行代码”,我实在想不出如何在验正解题人所提供方案的正确性。只有一个private,难道用解题人所提供的读出方案来验证他自己所提供的写入方案?你用你的方法读出来,然后告诉我那就是你用你的方法写进去的值——那能让人信服吗?所以我决定还是把问题改一改,并稍微具体化如下:
class Test {
public:
int get_value() { return value; }
private:
int value;
};
要求不用友元,不在这个类里添加任何代码,把成员变量k的值改为100,结果自然是通过公共成员函数get_value来验证。

“不在类里添加任何代码”,除了
#define private public
我实在想不出其它的“偏门”方法了。那就想想不偏门的吧,论坛里好几位朋友提供了相当于如下代码的方法(为控制篇幅,本文中所有程序段都假设已包含了<iostream>头文件并引入了std名字空间,必要时还有其它头文件):
Test t;
*(int*)&t = 100;
cout << t.get_value() << endl;
这种方式利用对象内存布局的特点:整个类只有一个整型成员,没有继承或虚拟继承,也没有任何虚函数,那么这个对象的地址也就是它的第一个成员变量的地址,所以只需要把对象地址强转成整型,那么获得的就是那个成员变量的地址,然后对转换后的地址再解引用,修改即可,在VC2003中验证,结果是正确的。
但指针的强制转换总给人带来不爽,不大安全的感觉,上面那条最关键的语句相当于:
*reinterpret_cast<int*>(&t) = 100;
也就是说,它动用了C++语言中最“强”的指针转换方式(说它最强,是因为没有什么指针之间他不能转换的)。其实我们完全可以做得更“文明”一点,方法是再定义一个联合体,比如:
union TestInt {
Testt;
inti;
};
然后再:
TestInt ti;
ti.i = 100;
cout << ti.t.get_value() << endl;
同样达到了目的,但实质上依据的机理跟上面的指针转换是一致的。

这个方法没啥大问题,就是有局限性,只能用于修改类的第一个成员,如果在value之前再加一个成员,比如:
class Test {
public:
int get_value() { return value; }
private:
char ch;
int value;
};
这种方法就不灵了。
当然,你可以手工算,认为char占一个字节,于是会试图取对象地址再加1得到成员value的地址。但第一,这种方法无法不跨平台跨实现,char及int类型在不同的平台和编译器实现中的长度都可能是不一样的;第二,没有考虑字对齐问题,在内存中,value成员一般不会紧接着排布在ch之后,而是中间间开几个字节,最后将int类型对齐到另一个位置,比如4的倍数的地址上;而更糟糕的是,字对齐不仅跟平台相关,还跟预编译指令,甚至编译选项都会有关。所以,这种手工计算的方式还是放弃了吧。
有朋友提到了使用一种宏求出value成员相对于整个对象起始地址的偏移量,即定义一个宏:
#define OFFSET(TYPE,MEM) ((int)(char*)&(((TYPE*)0)->MEM))
这个宏通过把0地址转换为TYPE指针类型,然后从这个指针上“取”MEM成员,而MEM成员的地址转换后结果就是MEM成员相对于整个对象的偏移量(我们既然是从0地址开始算的,就不用再减去起始地址0)。
然后同,我们通过使用这个宏作用于原来的类和目标字段,即:
OFFSET(Test, value)
就可以获得value字段在Test类型对象中的偏移量,用对象的首地址加上这个偏移量,就可以得到value变量的地址,从而可以像上面一样解引用,修改。
这种方法不仅看起来难受,费解。事实上也根本行不通,因为这个宏所用到的技巧是从Test类型的指针上访问value成员——而valuee是private的!所以连编译都通不过。

论坛里有位朋友提出了另外一种方法可以巧妙地对付这个复杂一点的类,先做一个辅助类,它跟Test类很像,唯一的不同是它的成员都是public的:
class TestTwin {
public:
int get_value() { return value; }
public:
char ch;
int value;
};
于是,这个TestTwin类跟原来的Test类在内存布局上不会有什么不同,通过指针转换,很容易借助于它来修改Test类对象的value成员:
Test t;
TestTwin* p = reinterpret_cast<TestTwin*>(&t);
p->value = 100;
cout << t.get_value() << endl;
如果你不熟悉C++的显式指针转换方式:reinterpret_cast,在这里可以认为它相当于C风格的:
TestTwin* p = (TestTwin*)&t;
而前述的两条语句也可以合在一起,直接写成:
reinterpret_cast<TestTwin*>(&t)->value = 100;
还有,厌恶指针操作的朋友仍可采用前面介绍的联合体方法来运用这个模具类,只是这次定义的联合体是这样:
union TestTestTwin {
Testt;
TestTwintw;
};
而程序是这样:
TestTestTwin ttw;
ttw.tw.value = 100;
cout << ttw.t.get_value() << endl;

问题都解决了吗?如果类更复杂一些,会不会还有局限性呢?我们再把类改一改:
class Test {
public:
int get_value() { return value; }
~Test() {}
private:
char ch;
int value;
public:
int a;
double b;
protected:
string e;
private:
short d;
};
这次不仅成员多了许多,有string类型的成员(须include <string>),还弄出个虚析构函数来(我们都知道拥有虚函数的类会导致其实例中多一个虚表指针)。但后面会看到,虚函数对我们讨论的问题影响不大,我们加上它只是想证明:只要方法足够好,不怕对象更复杂。
那上面的模具办法问题出在哪里呢?为什么不能同样再搞一个类,把那个value改为public的,然后用它来“套住”原来对象中value成员呢?
原因是C++语言只保证类中同一个access section(即从一个访问权限修饰符public/private/protected到另一个修饰符之间的部分)中定义的非静态成员变量会按照声明时的顺序分布的内存中,但并不保证跨越了不同access section的所有成员变量都在内存中按声明时的顺序存放,某种编译器完全有可能把所有的private块都合成一块,甚至整个给扔到所有protected成员的后边去(虽然VC并没这么做)。
换句话说:改掉了一个成员的访问权限,就可能改变了对象的内存布局。于是,改变了的模子也就不再能够套住相应位置上的成员。
但办法还是有,只需要将原来的改进一下:
在现有的C++对象模型中,为类增加一个非虚成员函数,不会改变对象的内存布局,我们可以利用这一点来写一个TestTwin:
class TestTwin {
public:
int get_value() { return value; }
void set_value(int v) { value = v; }
~TestTwin() {}
private:
char ch;
int value;
public:
int a;
double b;
protected:
float e;
private:
short d;
};
这个模具跟原来的Test类也是只有一点不同:增加了一个公共的,非虚的set_value方法,用来给私有成员value赋值。于是,程序可以这么写:
Test t;
reinterpret_cast<TestTwin*>(&t)->set_value(100);
cout << t.get_value() << endl;
验证通过。
增加的虚函数纯粹是个障眼物而已,它跟我们采用的方法几乎没有丝毫联系,所以也就丝毫不用担心虚函数对内存分布的影响会影响到这个方法的正确性。但被它一搞,那个使用联合体的方法这一次还真是不管用了,因为有了析构函数的类不能再放进联合体中了——否则当联合体实例的生命周期结束时,析构谁呢?

想了关天,能想到的只有这么多了。
最后,不行不承认,“增加一个非虚成员函数,不会改变对象的内存布局”这句话也无法从C++标准中得到直接支持,只是对于目前大多数编译器来说,这都是没问题的。因为这种“让类的每个实例拥有一份独立的成员变量,而类的所有实例共享一份成员函数”的C++对象模型是C++之父Bjarne Stroustrup先生本人所提出的,其时间、空间效率都很好地符合了C++语言的设计初衷,不仅现代C++编译器没有不这么做的,就连Java/C#编译器也都这么做。所以,也算是个“相对真理”了。
 

<think>好的,我现在要回答用户的问题:如何在C++中从一个函数访问另一个函数的变量。首先,我需要理解用户的需求。用户可能遇到了在两个不同的函数之间共享变量问题,需要知道有哪些方法可以实现这一点。 根据C++的作用域规则,函数的局部变量在其生命周期结束后会被销毁,所以直接访问另一个函数的局部变量是不可能的。因此,我需要考虑其他方法。 首先想到的是通过参数传递。如果一个函数需要访问另一个函数的变量,可以将该变量作为参数传递给目标函数。这种方法简单直接,适用于变量在调用时已经存在的情况。比如,函数A调用函数B时,将变量作为参数传入B中,这样B就可以使用这个变量了。 接下来,如果变量需要在多个函数调用之间保持状态,可以使用静态局部变量。静态局部变量在函数调用结束后不会被销毁,其生命周期贯穿整个程序运行期间。这样,每次函数调用时都可以访问和修改一个变量。例如,在函数中声明static int x;,每次调用该函数时,x的值会保留下来,其他函数也可以通过返回其引用或指针来访问它,不过需要注意线程安全和初始化的问题。 第三种方法是使用类的成员变量。如果这两个函数都是同一个类的成员函数,那么可以将需要共享的变量声明为类的私有或公有成员变量。这样,类的任何成员函数都可以直接访问这些变量。例如,在类中定义int a;,然后在成员函数set和get中操作a,这样不同的成员函数都可以访问和修改a的值。引用中的例子也说明了这一点,成员函数可以访问同一类的私有变量,但不能访问其他类的私有变量,如引用[2]所述。 第四种方法是全局变量。将变量声明在全局作用域中,这样任何函数都可以访问它。不过全局变量容易引起命名冲突和代码维护问题,所以需要谨慎使用。例如,在函数外部声明int globalVar;,然后在需要的函数中直接使用globalVar。 还有一种情况是使用友元函数或类,但这需要修改类的定义,授予其他函数或类访问私有成员的权限。不过这可能破坏封装性,通常不推荐频繁使用。 此外,如果两个函数属于不同的类,可以考虑将其中一个类的对象作为参数传递给另一个类的成员函数,或者通过指针和引用来访问。例如,在引用[3]中,Person类的成员函数print()可以访问自己的私有成员,但如果另一个类需要访问,可能需要通过友元关系或公共接口。 需要注意的是,用户提到的“访问另一个函数中的变量”可能有两种情况:同一作用域下的不同函数,或者不同类中的函数。如果是同一类中的成员函数,成员变量是共享的;如果是不同类的函数,可能需要传递对象实例或使用其他方法。 还需要考虑变量的生命周期。如果函数A的局部变量在函数返回后已经销毁,那么即使通过指针或引用保留下来,访问它也是未定义行为,会导致程序崩溃或错误。 总结起来,可能的解决方案有参数传递、静态变量、成员变量、全局变量、友元等。根据具体情况选择合适的方法,并注意作用域和生命周期的限制。</think>在C++中,函数内部的局部变量具有作用域和生命周期限制,无法直接跨函数访问。以下是实现变量共享的常用方法及示例: --- ### 一、通过参数传递 将变量作为参数传递给目标函数,实现间接访问。 ```cpp void funcA() { int x = 10; funcB(x); // 将x作为参数传递 } void funcB(int value) { cout << "访问funcA的变量: " << value; // 输出10 } ``` --- ### 二、使用静态局部变量 通过`static`关键字延长变量生命周期,但需注意线程安全。 ```cpp int& getStaticVar() { static int staticVar = 0; // 生命周期持续到程序结束 return staticVar; } void funcA() { getStaticVar() = 5; // 修改静态变量 } void funcB() { cout << "静态变量值: " << getStaticVar(); // 输出5 } ``` --- ### 三、类的成员变量 若函数是同一类的成员函数,可通过成员变量共享数据。 ```cpp class MyClass { private: int sharedVar; // 成员变量 public: void setVar(int x) { sharedVar = x; // 成员函数可访问私有变量[^2] } void printVar() { cout << "共享变量: " << sharedVar; } }; // 使用示例 MyClass obj; obj.setVar(20); obj.printVar(); // 输出20 ``` --- ### 四、全局变量变量声明在全局作用域(慎用,易导致耦合)。 ```cpp int globalVar; // 全局变量 void funcA() { globalVar = 30; } void funcB() { cout << "全局变量值: " << globalVar; // 输出30 } ``` --- ### 五、返回指针/引用(需谨慎) 通过指针或引用传递变量,但需确保变量生命周期有效。 ```cpp int* funcA() { int* ptr = new int(40); // 动态内存分配 return ptr; } void funcB() { int* p = funcA(); cout << "动态变量值: " << *p; // 输出40 delete p; // 需手动释放内存 } ``` --- ### 关键注意事项 1. **生命周期管理**:避免访问已销毁的局部变量(如返回局部变量的指针)[^1]。 2. **封装性**:优先使用成员变量和参数传递,减少全局变量使用。 3. **线程安全**:静态变量和全局变量需考虑多线程竞争问题。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值