寫在前面
老胡最近在工作中,有個場景需要使用一個第三方庫,引用頭文件,鏈接庫,編譯運行,一切都很正常,但是接下來就遇到了一個很詭異的問題,調用該庫的中的一個對象方法爲對象修改屬性的時候,會影響到對象的另外一個屬性,當時百思不得其解,直呼靈異事件。
但後面靜下心來細細看了一下代碼和各種配置,發現了問題所在,現在把這個問題分享在這裏,希望大家在以後的工作中如果遇到了類似的情況知道應該如何處理。
場景還原
庫
當時引用的是一個第三方的靜態鏈接庫,場景非常簡單,在項目中包含頭文件,鏈接器指定路徑和靜態庫名稱,我們這裏新建工程來生成一個非常簡單的庫。
其中,
//LibObject.h
#pragma once
struct LibObject
{
int valueA{ 0 };
#ifdef AdditionalValue
int valueB{ 0 };
#endif
int valueC{ 0 };
void DoSomething();
};
//LibObject.cpp
#include "LibObject.h"
void LibObject::DoSomething()
{
valueA = 10;
#ifdef AdditionalValue
valueB = 10;
#endif
}
簡單至極,若預編譯變量定義了AdditionalValue則定義多一個valueB並且在方法中賦值。編譯庫的時候我們指定AdditionalValue。
客戶端代碼
//main.cpp
#include "LibObject.h"
#include <iostream>
using namespace std;
int main()
{
LibObject obj;
cout << obj.valueA << endl;
cout << obj.valueC << endl;
obj.DoSomething();
cout << obj.valueA << endl;
cout << obj.valueC << endl;
return 0;
}
客戶端代碼也很簡單,聲明一個對象,調用它的方法並在調用前後檢查它的值,在編譯客戶端代碼的時候,我們不定義AdditionalValue預編譯變量。
運行試試
現在猜一猜輸出是多少?
解惑
藏在背後的祕密
如果這個結果讓你喫驚,那麼相信我,你不是一個人,當時老胡也驚呆了,不管怎麼看,DoSomething僅僅修改了ValueA,爲什麼會讓ValueC的值變了?
祕密就在於編譯庫的時候和編譯客戶端代碼的時候,我們使用了不同的預編譯變量。
- 在客戶端代碼看來,LibObject是一個僅僅包含2個int類型的結構體,並且DoSomething方法會賦值給一個int,該int相對於this指針偏移是0。
- 另一方面,在庫代碼看來,這個結構體包含了3個int類型變量,DoSomething會賦值給相對於this指針偏移爲0和4的兩個int。
所以答案揭曉了,爲什麼valueC的值會被影響,在於DoSomething執行的時候,相當於this指針偏移爲4的int被賦值了,但是在我們從客戶端代碼構建的結構體中,這個位置存放的是valueC。
從這裏可以看出,在方法執行的過程中,所謂的valueB其實內存地址和valueC是一樣的。所以其實是那句給valueB賦值的語句把值給了valueC。
如何修復
知道了出問題的地方,修復起來就很簡單了,一般來說兩個辦法。
- 如果第三方庫能找到源代碼,那我們可以重新用我們希望的預編譯設置編譯一次
- 如果找不到源代碼,那我們只有在客戶端代碼添加相應的預編譯設置,確保和編譯庫時候所使用的一致
這兩個辦法都需要仔細閱讀第三方庫的文檔。
希望本文能給遇到了類似問題的小夥伴一點啓示,特別當你遇到了類似的情況的時候,這篇文章能夠給你一些思路,畢竟,編譯器甚至在這種情況下都不會給出任何警告,我們只能靠經驗排查了。