在所有關於 C++ 程式設計的經典教材中,都會詳細闡述類別的三大存取指定符:public、private 與 protected。教科書告訴我們,private 成員僅限類別自身的成員函式存取,外部物件實例無法直接引用;protected 則為繼承體系保留介面,同時隔絕外部存取。這構成了 C++ 封裝特性的基石。
然而,現實遠比理論複雜。C++ 允許指標進行任意類型轉換,這項特性為繞過存取控制提供了「後門」。以下範例程式碼生動演示了如何透過指標運算「穿越保護屏障」,直接修改私有甚至 const 資料成員:
#include <iostream>
using namespace std;
class CMyClass {
double d;
int x, y;
const char ch;
public:
int z;
CMyClass(int xx, int yy, char c) : ch(c) { x = xx; y = yy; d = 9.8759; z = 0; }
void Show() {
cout << "d=" << d << " " << "x=" << x << " " << "y=" << y << " " << "ch=" << ch << " ";
cout << "z=" << z << endl;
}
};
int main() {
cout << sizeof(CMyClass) << endl;
CMyClass p(1, 2, 'U');
p.Show();
CMyClass *ptr = &p;
// ptr->x = 9.32145; // 編譯錯誤:x 為私有成員
double *dP = (double*)ptr; // 取得 p.d 位址
*dP = 9.32145; // 修改 p.d
int *iP = (int*)(dP + 1); // 取得 p.x 位址
*iP = 300; // 修改 p.x
*(iP + 1) = 200; // 修改 p.y
*(iP + 2) = 65; // 修改 ch 為 'A' —— 連 const 都不放過!
*(iP + 3) = 100; // 修改 p.z
p.Show();
return 0;
}
執行結果:
24 d=9.8759 x=1 y=2 ch=U z=0 d=9.32145 x=300 y=200 ch=A z=100
程式碼中,CMyClass 擁有四個私有成員(double d、int x, y、const char ch),正常情況下外部物件 p 完全無法存取。但透過將物件指標 ptr 強制轉型為 double*,再利用指標算術逐步偏移,我們成功修改了所有私有成員——包括理應不可變更的 const char ch。輸出結果中 ch 從 'U' 變為 'A'(ASCII 65),鐵證如山。
更值得玩味的是,sizeof(CMyClass) 回傳 24 位元組,而非預期的 17 位元組(8+4+4+1)。這揭示了類別記憶體佈局受 alignment(記憶體對齊) 影響:char 在類別中佔用與 int 相同大小的空間。指標運算正是利用這種確定的記憶體佈局實現「越界存取」。
reinterpret_cast 風格的強制轉型讓編譯器「睜一隻眼閉一隻眼」。const 成員同樣無法倖免,這動搖了「常數正確性」的基礎。雖然語言層面無法徹底杜絕,但可採取以下措施降低風險:
&obj 或 this 指標傳遞給不可信代碼。.cpp 定義,徹底隱藏記憶體佈局。/analyze、Clang Static Analyzer,可偵測可疑指標轉型。reinterpret_cast 用於存取私有成員,改用友元類別或公開介面。這個經典案例深刻揭示:C++ 的存取控制是「紳士協定」而非「鐵律」。真正的資料安全仍須仰賴開發者的自律、架構設計與靜態分析工具的多重防線。