一、工作中遇到的怪事
在編寫代碼是,發現了一個崩潰:
// XXXX.m
#if MOU_GE_HONG_DING_YI == 1
_library = // 涉密,刪除代碼;從路徑A獲取文件
#else
// 崩潰發生在這裏
_library = // 涉密,刪除代碼;從路徑B獲取文件
#endif
但是沒有改動到這裏,卻發生了崩潰!
- 經分析,初步判斷可能是需要走if分支,但是卻走到了else分支
- 發現在路徑A確實存在文件、而路徑B卻不存在這個文件
- 那麼問題就發生在編譯XXXX.m文件時,宏MOU_GE_HONG_DING_YI尚未被定義
二、問題詳解
2.1 代碼展示
// 宏在此文件中定義
// Header.h
#define MOU_GE_HONG_DING_YI 1
// 宏在此文件中使用
// XXXX.m
#if MOU_GE_HONG_DING_YI == 1
_library = // 涉密,刪除代碼;從路徑A獲取文件
#else
// 崩潰發生在這裏
_library = // 涉密,刪除代碼;從路徑B獲取文件
#endif
2.2 大膽猜測
根據問題表現,猜測是編譯器在預處理XXXX.m文件時,其尚未了解到有MOU_GE_HONG_DING_YI的宏定義,所以就把if分支的代碼去掉了,只保留了else分支的代碼。
2.3 小心求證
2.3.1 復現問題
爲了驗證自己的猜測,編寫如下測試代碼;
// Person.h
@interface Person : NSObject
+ (void)testMacro;
@end
// Person.m
@implementation Person
// DEBUG 是Xcode預置的
// THIS_IS_A_TEST_MACRO 在Person的子類Student類中聲明
+ (void)testMacro {
#ifdef DEBUG
NSLog(@"Person Define DEBUG");
#else
NSLog(@"Person NO Define DEBUG");
#endif
#if THIS_IS_A_TEST_MACRO == 1
NSLog(@"Person Define THIS_IS_A_TEST_MACRO");
#else
NSLog(@"Person NO THIS_IS_A_TEST_MACRO");
#endif
}
@end
// Student.h
#define THIS_IS_A_TEST_MACRO 1
@interface Student : Person
+ (void)StudentTestMacro;
@end
// Student.m
@implementation Student
+ (void)StudentTestMacro {
#ifdef DEBUG
NSLog(@"Student Define DEBUG");
#else
NSLog(@"Student NO Define DEBUG");
#endif
#if THIS_IS_A_TEST_MACRO == 1
NSLog(@"Student Define THIS_IS_A_TEST_MACRO");
#else
NSLog(@"Student NO THIS_IS_A_TEST_MACRO");
#endif
[super testMacro];
}
@end
// main.m
int main(int argc, const char * argv[]) {
[Student StudentTestMacro];
return 0;
}
/*
2022-05-19 14:51:49.164576+0800 cmdProject[75822:301165] Student Define DEBUG
2022-05-19 14:51:49.165422+0800 cmdProject[75822:301165] Student Define THIS_IS_A_TEST_MACRO
2022-05-19 14:51:49.165453+0800 cmdProject[75822:301165] Person Define DEBUG
2022-05-19 14:51:49.165474+0800 cmdProject[75822:301165] Person NO THIS_IS_A_TEST_MACRO
*/
- 由於Student繼承自Person,所以編譯器優先處理Person的相關代碼
- 在對Person進行預處理的時候,尚未了解到THIS_IS_A_TEST_MACRO宏已被定義,所以至保留了else分支
通過代碼驗證了之前的猜測——某些預處理與文件的依賴有關係,如果宏定義在子孫文件裏,則父文件的預處理會出乎預料
2.2.2 代碼實證
2.2.2.1 XXXX.m原代碼預處理
// XXXX.i
// 可見在原始代碼中,由於引入了Header.h,所有走了if分支
# 1 "...../common/Header.h" 1
_library = // 涉密,刪除代碼;從路徑A獲取文件
2.2.2.2 修改XXXX.m後代碼預處理
// XXXX.i
// 可見在原始代碼中,由於引入了Header.h,所有走了else分支
_library = // 涉密,刪除代碼;從路徑B獲取文件
三、最佳實現
3.1 不推薦使用宏
絕大部分的程序員都聽說過,不要使用宏。它有許多問題:
- 丟失類型
- 可能包含隱蔽錯誤(如加的小括號過少)
- 不支持調試
- 在預處理階段被處理掉,重要信息丟失(語法、語義等後續階段的編譯處理無法獲取宏信息)
- .......
宏的不好,不一而足,以上僅列舉部分
3.2 不得不使用宏
宏就像“渣男、渣女”一樣,除了渣,別的地方都很好!
扯遠啦!
宏雖然有萬般不好,但是許多地方仍然被使用!除了其他的一些宏編寫的最佳時間外,推薦大家在XCode的工程配置裏定義宏(除非你非常確定宏的應用範圍在且只在當前文件範圍內),這樣就可以規避此問題。