一個C++引用庫的頭文件預編譯陷阱

寫在前面

老胡最近在工作中,有個場景需要使用一個第三方庫,引用頭文件,鏈接庫,編譯運行,一切都很正常,但是接下來就遇到了一個很詭異的問題,調用該庫的中的一個對象方法爲對象修改屬性的時候,會影響到對象的另外一個屬性,當時百思不得其解,直呼靈異事件。
但後面靜下心來細細看了一下代碼和各種配置,發現了問題所在,現在把這個問題分享在這裏,希望大家在以後的工作中如果遇到了類似的情況知道應該如何處理。
 

場景還原

當時引用的是一個第三方的靜態鏈接庫,場景非常簡單,在項目中包含頭文件,鏈接器指定路徑和靜態庫名稱,我們這裏新建工程來生成一個非常簡單的庫。

其中,

//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。
 

如何修復

知道了出問題的地方,修復起來就很簡單了,一般來說兩個辦法。

  • 如果第三方庫能找到源代碼,那我們可以重新用我們希望的預編譯設置編譯一次
  • 如果找不到源代碼,那我們只有在客戶端代碼添加相應的預編譯設置,確保和編譯庫時候所使用的一致

這兩個辦法都需要仔細閱讀第三方庫的文檔。
 
希望本文能給遇到了類似問題的小夥伴一點啓示,特別當你遇到了類似的情況的時候,這篇文章能夠給你一些思路,畢竟,編譯器甚至在這種情況下都不會給出任何警告,我們只能靠經驗排查了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章