iOS學習——iOS 宏(define)與常量(const)的正確使用
- 2019 年 10 月 3 日
- 筆記
概述
在iOS開發中,經常用到宏定義,或用const修飾一些數據類型,經常有開發者不知怎麼正確使用,導致項目中亂用宏與const修飾。你能區分下面的嗎?知道什麼時候用嗎?
#define HSCoder @"漢斯哈哈哈" NSString *HSCoder = @"漢斯哈哈哈"; extern NSString *HSCoder; extern const NSString *HSCoder; static const NSString *HSCoder = @"漢斯哈哈哈"; const NSString *HSCoder = @"漢斯哈哈哈"; NSString const *HSCoder = @"漢斯哈哈哈"; NSString * const HSCoder = @"漢斯哈哈哈";
當我們想全局共用一些數據時,可以用宏、變數、常量
//宏 #define HSCoder @"漢斯哈哈哈" //變數 NSString *HSCoder = @"漢斯哈哈哈"; //常量,四種寫法 static const NSString *HSCoder = @"漢斯哈哈哈"; const NSString *HSCoder = @"漢斯哈哈哈"; NSString const *HSCoder = @"漢斯哈哈哈"; NSString * const HSCoder = @"漢斯哈哈哈";
宏、變數、常量之間的區別
- 宏:只是在預處理器里進行文本替換,沒有類型,不做任何類型檢查,編譯器可以對相同的字元串進行優化。只保存一份到 .rodata 段。甚至有相同後綴的字元串也可以優化,你可以用GCC 編譯測試,”Hello world” 與 “world” 兩個字元串,只存儲前面一個。取的時候只需要給前面和中間的地址,如果是整形、浮點型會有多份拷貝,但這些數寫在指令中。占的只是程式碼段而已,大量用宏會導致二進位文件變大
- 變數:共享一塊記憶體空間,就算項目中N處用到,也不會分配N塊記憶體空間,可以被修改,在編譯階段會執行類型檢查
- 常量:共享一塊記憶體空間,就算項目中N處用到,也不會分配N塊記憶體空間,可以根據const修飾的位置設定能否修改,在編譯階段會執行類型檢查
我們來看一段程式碼
#define avatar @"60" if (false) { #define avatar @"80" } NSLog(avatar);
這段程式碼會輸出多少,我們將“avatar”定義為了60,然後在一個永遠不會執行的程式碼裡面重新定義了“avatar”為80,if語句中的程式碼永遠不會執行,但是在編譯時期,編譯器會編譯這段程式碼,而這個時候編譯器就會將avatar這個名字替換為@“80”,所以這段程式碼最後的輸出結果就是80。
當然這個時候編譯器是會有一個警告的,但是不知道有多少同學會忽略這個警告。或者你會告訴我你對警告十分敏感,不會放過他的,但是記住你不是一個人在寫程式碼,可能在別人的頁面他給你重新定義了你的define,給你挖了一個大坑,還找不著………
所以還是盡量使用const,看蘋果api也是使用常量多點,如下圖:
const的用法
const修飾符定義的變數是不可變的,比如說你需要定義一個動畫時間的常量,你可以這麼做:
static const NSTimeInterval kAnimateDuration = 0.3;
當你試圖去修改“ kAnimateDuration”的值的時候,編譯器會報錯。更加重要的是用這種方法定義的常量是帶有類型資訊的,而這點則是define不具備的。也許你已經發現了,如果你像如下這樣定義,你是可以修改userName的值的,(說好的常量呢~~~)
static const NSString * kUserName = @"StrongX";
首先我們需要確定的是以下的三種寫法中前兩種是一樣的(可以修改kUserName的內容,也就是說const放在類型前還是類型後是一樣的效果),第三種的效果不一樣(無法修改kUserName的內容)。
static NSString const * kUserName = @"StrongX"; static const NSString * kUserName = @"StrongX"; static NSString * const kUserName = @"StrongX";
需要注意的是const 修飾的是他右邊的部分,也就是說:
static NSString const * kUserName = static NSString const (* kUserName ) static NSString * const kUserName = static NSString * const (kUserName)
當const修飾的是(userName)的時候,不可變的是userName。當const修飾的是( * )的時候,“*”在C語言中表示指針指向符,也就是說這個時候userName指向的記憶體塊地址不可變,而記憶體保存的內容是可變的,我們來做個嘗試:
NSLog(@"記憶體地址: %x",& kUserName); kUserName = @"superXLX"; NSLog(@"記憶體地址: %x",& kUserName);
以上NSLog會列印*userName指向的記憶體塊地址,而他的輸出如下圖,我們已經發現當我們改變記憶體的內容的時候他的地址並沒有發生改變,也就是說這是符合“const”修飾符的規定的。
所以當我們需要定義一個不可變的常量的時候 ,我們還是需要將“const”修飾符放到“*”指針指向符後邊才對。
static NSString * const kUserName = @"StrongX";
extern和static的用法
在常量定義時我們經常會用到兩個關鍵字,extern和static。那麼這兩個關鍵字的具體用法和作用是什麼呢?下面我們就一起探究一下。
關鍵字extern
關鍵字extern主要是用來引用全局變數
,它的原理是先在本文件中查找,查找不到再到其他文件中查找。用“extern”定義的常量必須也只能初始化一次,不滿足必須以及只能一次的條件那麼編譯器就會提醒你。在定義全局變數的時候需要要注意你的命名,你可以使用規定好的前綴來命名。我們一般的用法是在.h文件中用extern申明一個常量名稱,表示該常量可以讓外部引用,然後在.m文件中對該常量進行初始化。
//在"constants.h"文件中,聲明常量: extern NSString *const XUserName;
//然後在“constants.m”中定義他: NSString *const XUserName = @"StrongX";
關鍵字static
在探討static的用法之前,我們首先需要了解兩個概念:生命周期、作用域。
生命周期
:這個變數能存活多久,它所佔用的記憶體什麼時候分配,什麼時候收回。作用域
:這個變數在什麼區域是可見的,可以拿來用的。
static
分兩種情況:修飾局部變數、修飾全局變數。
1、static
修飾局部變數
- 局部變數:在函數/方法/程式碼塊內聲明的變數。它的生命周期、作用域都是在這個程式碼塊內。局部變數 存儲在棧區(stack)一旦出了這個程式碼塊,存儲局部變數的這個棧記憶體就會被回收,局部變數也就被銷毀。
靜態局部變數:
當用static
修飾局部變數時,變數被稱為靜態局部變數
,和全局變數,靜態全局變數一樣,是存儲在‘靜態存儲區’。存儲在 靜態存儲區 的變數,其記憶體直到 程式結束 才會被銷毀。即,生命周期是整個源程式。
所以,靜態局部變數
的生命周期是整個源程式,但,作用域是聲明它的程式碼塊內。
2、static
修飾全局變數
- 當全局變數沒有使用
static
修飾時其存儲在靜態存儲區,直到程式結束才銷毀。也就是其作用域是整個源程式。我們可以使用extern
關鍵字來引用這個全局變數。 - 當全局變數使用
static
修飾時,其生命周期沒有變,依舊是在程式結束時才銷毀。但是其作用域變了。現在只限於申明它的這個文件才可見。使用extern
關鍵字無法引用這個全局變數。 - 全局變數本來是在整個源程式的所有文件都可見,
static
修飾後,改為只在申明自己的文件可見,即修改了作用域。即如果在.m文件中用static定義了常量,那麼就不能在.h文件中使用extern進行外部申明。//在.m文件中這樣定義,則該常量只能在當前.m文件中使用,並且不能再.h文件中使用extern進行外部申明使用 static NSString * const kUserName = @"userName";
他會告訴你在兩個目標文件(.0文件是.m文件編譯後的輸出文件)有一個重複的符號。(OC中沒有類似C++中的名字空間的概念)
所以當你在你自己的.m文件中需要聲明一個只有你自己可見的局部變數(k開頭)的變數的時候一定要同時使用“static”和“const”兩個符號。