之前有個小哥和我吐槽:接手了離職同事的代碼,打開一看全是復(fù)制粘貼,同一個邏輯寫了N遍,改個bug要改N個地方,心態(tài)崩了。
這不奇怪,每個程序員都有糟糕的過去,看看我以前寫的代碼:


這種代碼,只有一個奢望:希望別出BUG。
現(xiàn)在看到這種代碼,完全忍不了,寧愿花了幾天時間用宏定義給重構(gòu)了。代碼量直接砍了一半,改bug再也不用滿篇找了。
說實(shí)話,很多剛?cè)胄械拈_發(fā)者都喜歡復(fù)制粘貼(包括當(dāng)年的我)。
為啥?因?yàn)榭彀。?/span>
寫完一個功能,復(fù)制一下,改幾個變量名,又是一個新功能。不用動腦子,不用想設(shè)計(jì),代碼"刷刷刷"就出來了。
但是,問題來了。
給個例子,假設(shè)你要控制5個LED,代碼是這樣的:
// LED1閃爍void LED1_Blink(void){GPIO_SetBits(GPIOA, GPIO_Pin_0);Delay_ms(500);GPIO_ResetBits(GPIOA, GPIO_Pin_0);Delay_ms(500);}// LED2閃爍void LED2_Blink(void){GPIO_SetBits(GPIOA, GPIO_Pin_1);Delay_ms(500);GPIO_ResetBits(GPIOA, GPIO_Pin_1);Delay_ms(500);}// LED3閃爍void LED3_Blink(void){GPIO_SetBits(GPIOA, GPIO_Pin_2);Delay_ms(500);GPIO_ResetBits(GPIOA, GPIO_Pin_2);Delay_ms(500);}// ... LED4、LED5以此類推
看著沒問題對吧?
但是,如果你想把閃爍間隔從500ms改成300ms,你得改5個函數(shù)!
如果老板說"LED要用GPIOB不是GPIOA",你得改5個函數(shù)!
如果你想加個錯誤處理,你得改5個函數(shù)!
復(fù)制粘貼的代碼,維護(hù)成本是N倍。
而且,復(fù)制粘貼還容易出錯。改的時候漏改一個地方,bug就來了。
有人可能會說:這還不簡單,用函數(shù)封裝??!
// 封裝LED控制函數(shù)void LED_Blink(unsigned char pin){GPIO_SetBits(GPIOA, pin);Delay_ms(500);GPIO_ResetBits(GPIOA, pin);Delay_ms(500);}// 調(diào)用LED_Blink(GPIO_Pin_0); // LED1LED_Blink(GPIO_Pin_1); // LED2LED_Blink(GPIO_Pin_2); // LED3
這樣確實(shí)好多了。但是還不夠徹底!
為什么呢?
因?yàn)镚PIOA、延時時間這些都是"硬編碼"的。
如果老板說"LED1用GPIOA,LED2用GPIOB",你這函數(shù)又得改。
如果老板說"LED1閃爍500ms,LED2閃爍300ms",你這函數(shù)又得改。
函數(shù)封裝只能解決"邏輯重復(fù)"的問題,解決不了"參數(shù)重復(fù)"的問題。
要真正解決復(fù)制粘貼的問題,你得用宏定義。
給個例子,用宏定義重構(gòu)后的LED控制:
// 定義LED控制的宏#define LED_BLINK(port, pin, delay) do { \GPIO_SetBits(port, pin); \Delay_ms(delay); \GPIO_ResetBits(port, pin); \Delay_ms(delay); \} while(0)// 調(diào)用LED_BLINK(GPIOA, GPIO_Pin_0, 500); // LED1LED_BLINK(GPIOA, GPIO_Pin_1, 300); // LED2LED_BLINK(GPIOB, GPIO_Pin_0, 500); // LED3
看懂了嗎?
用宏定義,你可以把"邏輯"和"參數(shù)"完全分離。
這樣,你想改哪個LED的參數(shù),只需要改對應(yīng)的宏調(diào)用就行。
宏定義不只是簡單的文本替換,你還可以寫"帶參數(shù)的宏",甚至"帶代碼塊的宏"。
給個復(fù)雜點(diǎn)的例子,假設(shè)你要處理多個隊(duì)列:
// 原來的代碼(復(fù)制粘貼)typedef struct {unsigned char data1[16];unsigned char idx1;unsigned char len1;} Queue1_t;typedef struct {unsigned char data2[16];unsigned char idx2;unsigned char len2;} Queue2_t;typedef struct {unsigned char data3[16];unsigned char idx3;unsigned char len3;} Queue3_t;void Queue1_Init(Queue1_t *q){q->idx1 = 0;q->len1 = 0;}void Queue2_Init(Queue2_t *q){q->idx2 = 0;q->len2 = 0;}void Queue3_Init(Queue3_t *q){q->idx3 = 0;q->len3 = 0;}
同樣的邏輯寫了3遍!
用宏定義重構(gòu):
// 定義隊(duì)列的宏#define DEFINE_QUEUE(name, size) \typedef struct { \unsigned char data[size]; \unsigned char idx; \unsigned char len; \} Queue_##name##_t; \\void Queue_##name##_Init(Queue_##name##_t *q) \{ \q->idx = 0; \q->len = 0; \}// 生成不同大小的隊(duì)列DEFINE_QUEUE(4, 4) // Queue_4_tDEFINE_QUEUE(8, 8) // Queue_8_tDEFINE_QUEUE(16, 16) // Queue_16_t
看到##沒?這是宏定義的"連接符",可以把兩個符號連接起來。
Queue_##name##_t展開后就是Queue_4_t、Queue_8_t、Queue_16_t。
這樣,你只需要寫一遍邏輯,就能生成N個不同的隊(duì)列結(jié)構(gòu)!
STM32項(xiàng)目里,位操作是最常見的操作。但是原生庫的位操作函數(shù)寫起來很啰嗦:
// 原來的寫法GPIOA->ODR |= GPIO_Pin_0; // 置位GPIOA->ODR &= ~GPIO_Pin_0; // 復(fù)位if(GPIOA->IDR & GPIO_Pin_0) // 讀取
你可以用宏定義封裝一下:
// 來源于STM32標(biāo)準(zhǔn)庫 stm32f10x.h:8288// 功能:位操作宏定義#define SET_BIT(REG, BIT) ((REG) |= (BIT))#define CLEAR_BIT(REG, BIT) ((REG) &= ~(BIT))#define READ_BIT(REG, BIT) ((REG) & (BIT))// 使用SET_BIT(GPIOA->ODR, GPIO_Pin_0); // 置位CLEAR_BIT(GPIOA->ODR, GPIO_Pin_0); // 復(fù)位if(READ_BIT(GPIOA->IDR, GPIO_Pin_0)) // 讀取
這樣寫是不是清晰多了?
而且,宏定義是在預(yù)處理階段展開的,編譯器會把SET_BIT(GPIOA->ODR, GPIO_Pin_0)直接展開成GPIOA->ODR |= GPIO_Pin_0。
沒有函數(shù)調(diào)用的開銷,執(zhí)行效率更高!
雖然宏定義很強(qiáng)大,但是用的時候要注意幾個坑:
// 錯誤寫法#define MUL(a, b) a * bint result = MUL(2 + 3, 4); // 展開后是 2 + 3 * 4 = 14,不是20!// 正確寫法#define MUL(a, b) ((a) * (b))int result = MUL(2 + 3, 4); // 展開后是 (2 + 3) * 4 = 20
// 錯誤寫法#define SWAP(a, b) \int temp = a; \a = b; \b = tempif(condition)SWAP(x, y); // else會匹配錯誤!// 正確寫法#define SWAP(a, b) \do { \int temp = a; \a = b; \b = temp; \} while(0)
不是所有東西都適合用宏定義。給你幾個判斷標(biāo)準(zhǔn):
適合用宏定義的場景:
#define LED_ON 1)SET_BIT、CLEAR_BIT)DEFINE_QUEUE)#define DEBUG_PRINT(x))不適合用宏定義的場景:
很多初學(xué)者搞不清楚什么時候用宏定義,什么時候用函數(shù)。
給你個對比表:
選擇建議:
復(fù)制粘貼的代碼就是技術(shù)債務(wù)。你今天爽了,明天改bug的時候就會哭。
end

下面是更多無際原創(chuàng)的個人成長經(jīng)歷、行業(yè)經(jīng)驗(yàn)、技術(shù)干貨。
1.電子工程師是怎樣的成長之路?10年5000字總結(jié)
2.如何快速看懂別人的代碼和思維
3.單片機(jī)開發(fā)項(xiàng)目全局變量太多怎么管理?
4.C語言開發(fā)單片機(jī)為什么大多數(shù)都采用全局變量的形式?
5.單片機(jī)怎么實(shí)現(xiàn)模塊化編程?實(shí)用程度讓人發(fā)指!
6.c語言回調(diào)函數(shù)的使用及實(shí)際作用詳解
7.手把手教你c語言隊(duì)列實(shí)現(xiàn)代碼,通俗易懂超詳細(xì)!
8.c語言指針用法詳解,通俗易懂超詳細(xì)!