C語言的那些小秘密之預處理

2020-12-13 電子產品世界

  預處理C語言的一個重要知識點,它能改善程序設計的環境,有助於編寫易移植、易調試的程序。因此,我們有必要掌握好預處理命令,在自己編程的時候靈活的使用它,使得編寫的程序結構優良,更加易於調試和閱讀。接下來我儘可能的把預處理中重要知識點向讀者講解清楚,使讀者能夠在自己以後編程的過程中熟練的使用預處理命令。

本文引用地址:http://www.eepw.com.cn/article/275697.htm

  C語言的預處理主要有三個方面:

  1、文件的包含

  2、宏定義

  3、條件編譯

  一、文件包含的形式有下面兩種

  1、#include "文件名"

  2、#include <文件名>

  它們之間的區別在於:<文件名>系統到頭文件目錄查找文件, "文件名"則先在當前目錄查找,如果沒有才到頭文件目錄查找;當然我們也可以使用在命令行來指定頭文件路徑方法。還要注意就是如果在源文件包含的頭文件之間出現調用的情況,那麼被調用的頭文件要出現在調用頭文件的前面。

  二、宏定義

  宏定義的使用有兩種形式,一種不帶參數,而另外一種帶參數。

  1、不帶參數

  格式: #define 標識符 字符串

  相信上面這個格式大家並不陌生,下面還是來看看如何使用吧。當然在講解之前我們的看看使用過程中的如下幾個注意要點:

  (1)預處理不做語法檢查,所以我們選用的時候要尤其小心

  (2)宏定義寫在函數的花括號外邊,作用域為其後的程序,通常在文件開頭部分,直到用#undef命令終止宏定義的作用域

  (3)不要在字符串中使用宏,如果宏名出現在字符串中那麼將按照字符串進行處理

  下面來看段代碼的使用。

  [html] view plaincopy#include

  #define N 9

  int main ()

  {

  int i,a[N];

  for(i=0;i

  {

  a[i]=i;

  printf("%d\t",a[i]);

  if((i+1)%3==0)

  printf("\n");

  }

  //#undef N

  printf("%d\n",N);

  }

  運行結果為:

  [html] view plaincopy0 1 2

  3 4 5

  6 7 8

  9

  Press any key to continue

  我們在此主要是介紹下宏的作用域問題,當在以上代碼中注釋掉#undef N時,接下來的列印語句能夠正常的列印出;但是當我們沒有注釋掉#undef N的時候就會出現error C2065: 'N' : undeclared identifier錯誤,提示N沒有定義。接下來看看帶參數的宏的使用。

  2、帶參數

  #define 宏名(參數表) 字符串

  注意要點:

  (1)宏名和參數的括號間不能有空格

  (2)宏替換隻作替換,不做計算,不做表達式求解,這點要尤其注意

  (3)函數調用在編譯後程序運行時進行,並且分配內存。宏替換在編譯前進行,不分配內存

  (4)宏的啞實結合(所謂的啞實結合類似於函數調用過程中實參替代形參的過程)不存在類型,也沒有類型轉換。

  (5)宏展開使源程序變長,函數調用不會

  下面來看看linux下一個典型的應用:

  #define min(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x < _y ? _x : _y; })

  #define max(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x > _y ? _x : _y; })

  在上面的兩個宏中我們發現有這麼一句代碼(void) (&_x == &_y);可能不少讀者有點發懵的感覺,這啥意思呢?!其實我們細細分析就知道,首先看看「==」,這是一個邏輯表達式,它要求兩邊的比較類型必須一致,如果我們的&x和&y類型不一致,如一個為char*,另一個為int*,不是同一個類型,當我們使用gcc編譯的時候就會出現警告信息,vc6則會報錯error C2446: '==' : no conversion from 'char *' to 'int *'。這句代碼(void) (&_x == &_y); 在此的功能就相當於執行一個簡單的判斷操作,我們用來判斷x和y的類型是否一致。別小看了這句代碼,如果學會了使用它會給你的代碼帶來不少的便捷。下面給出一個小小的事例:

  [cpp] view plaincopy#include

  void print()

  {

  printf("hello world!!!\n");

  return ;

  }

  void main(int argc,char*argv)

  {

  print();

  return ;

  }

  運行結果為:

  

 

  [cpp] view plaincopyhello world!!!

  Press any key to continue

  現在我們來修改下代碼後看看運行結果:

  [cpp] view plaincopy#include

  void print()

  {

  printf("hello world!!!\n");

  return ;

  }

  void main(int argc,char*argv)

  {

  #define print() ((void)(3))

  print();

  return ;

  }

  運行結果為:

  [cpp] view plaincopyPress any key to continue

  這兒的結果沒有了我們之前的那句hello world!!!,可以看出這個時候函數並沒有被調用,這是因為我們使用了#define print() ((void)(3)),使得之後調用函數print()轉換為了一個空操作,所以這個函數在接下來的代碼中都不會被調用了,就像被「衝刷掉」了一樣。看到這兒你是不是想起我們之前的那篇《C語言的那些小秘密之斷言》了呢,我們同樣可以使用這種方法來實現斷言的關閉,方法與之類似,在此就不再講解了,有興趣的讀者可以自己試試。講到這兒似乎應該結束了,但是細心的讀者會有另外一個疑惑?在#define min(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x < _y ? _x : _y; })中,我們為什麼要使用像typeof(y) _y = (y)這樣的轉換呢?而不直接使用typeof(x)==typeof(y)或者(void) (&x == &y); x < y ? x : y; 呢?如果我們使用typeof(x)==typeof(y)就好比使用了char==int一樣,這是不允許的。我們使用一個typeof(y) _y = (y)這樣的轉換,這是為了防止x和y為一個表達式的情況,如x=i++之類的,如果不轉換的話i++就多執行了幾次操作,得到的不是我們想要的結果,但是如果我們使用了typeof(y) _y = (y)這樣的轉換,那就不會出現這樣的問題了。下面我們來看看如何使用宏定義實現變參,先看看實現方法。

  #define print(...) printf(__VA_ARGS__)

  看看上面的宏,其中「...」指可變參數。實現的可變參數的實現方式就是使用「...」所代表的內容替代__VA_ARGS__,看看下面的代碼就知道了。

  [cpp] view plaincopy#include

  #define print(...) printf(__VA_ARGS__)

  int main(int argc,char*argv)

  {

  print("hello world----%d\n",1111);

  return 0;

  }

  運行結果為:

  [cpp] view plaincopyroot@ubuntu:/home/shiyan# ./arg

  hello world----1111

  接著往下看。

  #define printf (tem, ...) fprintf (stdout, tem, ## __VA_ARGS__)

  如有對fprintf不熟悉的讀者可以自己查查函數手冊,在此不再講解。

  [cpp] view plaincopy#include

  #define print(temp, ...) fprintf(stdout, temp, ##__VA_ARGS__)

  int main(int argc,char*argv)

  {

  print("hello world----%d\n",1111);

  return 0;

  }

  運行結果為:

  [cpp] view plaincopyroot@ubuntu:/home/shiyan# ./arg

  hello world----1111

  temp在此的作用為設定輸出字符串的格式,後邊「...」為可變參數。現在問題來了,我們在宏定義中為什麼要使用「##」呢?如果我們沒有使用##會怎麼樣呢?看看下面的代碼:

  [cpp] view plaincopy#include

  #define print(temp, ...) fprintf(stdout, temp, __VA_ARGS__)

  int main(int argc,char*argv)

  {

  print("hello world\n");

  return 0;

  }

  編譯時發生了如下錯誤:

  [cpp] view plaincopyroot@ubuntu:/home/shiyan# gcc arg.c -o arg

  arg.c: In function 『main』:

  arg.c:7:2: error: expected expression before 『)』 token

  為什麼會出現上面的錯誤呢,現在我們來分析下,我們進行下宏替換,print("hello world\n")就變為了fprintf(stdout, "hello world\n",)這樣我們就發現了後面出現了一個逗號,所以導致了錯誤,如果有「##」就不會出現這樣的錯誤了,這是因為如果可變參數被忽略或為空的時候,「##」操作將使預處理器去除掉它前面的那個逗號。如果存在可變參數時候,它也能正常工作。講了「##」,我們當然也要講講「#」。先來看看下面一段代碼:

  [cpp] view plaincopy#include

  #define return_exam(p) if(!(p)) \

  {printf("error: "#p" file_name:%s\tfunction_name:%s\tline:%d .\n",\

  __FILE__, __func__, __LINE__); return 0;}

  int print()

  {

  return_exam(0);

  }

  int main(int argc,char*argv)

  {

  print();

  printf("hello world!!!\n");

  return 0;

  }

  運行結果為:

  [cpp] view plaincopyroot@ubuntu:/home/shiyan# ./arg

  error: 0 file_name:arg.c function_name:print line:9 .

  hello world!!!

  我們發現在運行結果中列印出了出錯的文件名、函數名、以及行號。採用宏定義來檢測函數的返回值是否正確,僅僅是為了體現出我們要講解的宏,所以代碼做了最大的簡化工作,讀者在自己編寫代碼時候要學會這樣的檢測方式。「#」的作用就是將其後面的宏參數進行字符串化操作,就是在宏變量進行替換之後在其左右各加上一個上雙引號,這就使得"#p"變味了""p""我們發現這樣的話剛好兩邊的「""」就消失了。下面來看看最後一個知識點條件編譯。

  三、條件編譯

  條件編譯命令#if、#else、#elif、#endif、#ifdef、#ifndef,條件編譯指令的意思很簡單,跟我們學習的if語句類似。

  一般格式

  #if 常量表達式

  程序段1;

  [#else

  程序段2;]

  #endif

  功能:當表達式為非0(「邏輯真」)時,編譯程序段1,否則編譯程序段2。

  一般格式

  #ifdef 標識符

  程序段1;

  [#else

  程序段2;]

  #endif

  功能:當「標識符」已經被#define命令定義過,則編譯程序段1,否則編譯程序段2。

  #ifndef 標識符

  程序段1;

  [#else

  程序段2;]

  #endif

  功能:當「標識符」未被#define命令定義過,則編譯程序段1,否則編譯程序段2。

  學習了條件編譯指令之後,我們在調試代碼的時候,就不要再隨心所欲的刪減代碼了,如果我們不想某段代碼被編譯就可以使用條件編譯指令來將其注釋掉。如:

  #if (0)

  注釋代碼段;

  #endif

  就可以實現代碼的注釋了,需要的時候也可以將其啟用,而不會為需要重新編輯代碼時,發現已被刪除而頭疼了。

  其中值得注意的地方為,常量表達式在編譯時求值,所以表達式只能是常量或者已經定義過的標識符,不能為變量,也不可以為那些在編譯時候求值的操作符,如sizeof。

  下面來看段代碼:

  [cpp] view plaincopy#include

  #define N 1

  int main(int argc,char*argv)

  {

  int a=3;

  #if(a)

  printf("#if後面的表達式為變量\n");

  #endif

  #if(N)

  printf("#if後面的表達式已定義,且不為0---success\n");

  #else

  printf("#if後面的表達式已定義,且不為0---fail\n");

  #endif

  return 0;

  }

  運行結果為:

  

 

  [cpp] view plaincopy#if後面的表達式已定義,且不為0---success

  Press any key to continue

  看看上面的代碼我們的表達式為變量a時並沒有列印出來,所以我們不能在其後的表示中使用變量。如果我們使用sizeof操作符會怎麼樣呢?為了加深印象看看下面的代碼後結果吧。

  [cpp] view plaincopy#include

  int main(int argc,char*argv)

  {

  int a=9;

  #if(sizeof(a))

  printf("#if後面的表達式含有sizeof操作符\n");

  #endif

  return 0;

  }

  編譯出現了如下錯誤:

  [cpp] view plaincopyfatal error C1017: invalid integer constant expression

  所以我們在使用條件編譯的時候要牢記這兩點,常量表達式不能為變量和含有sizeof等在編譯時求值的操作符。

  接下來看看這裡要講的最後一個#pragma指令。

  一般格式為:

  #pragma 參數

  下面給出幾種經常使用的形式

  1、#pragma message("消息")

  看看下面一段代碼。

  [cpp] view plaincopy#include

  #define FDSA

  int main(int argc,char*argv)

  {

  #ifdef FDSA

  #pragma message("FDSA 已經定義過了")

  #endif

  return 0;

  }

  編譯的時候我們可以在編譯輸出窗口中看到了輸出「FDSA 已經定義過了」,通過這種方式我們可以在一些我們想要的地方輸出很多我們需要的信息。

  2、#pragma once

  如果我們在頭文件的開頭部分加入這條指令,那麼就能保證我們的頭文件僅僅被編譯一次。

  3、#pragma hdrstop

  該指令表示編譯頭文件到此為止,後面的無需在進行編譯了。

  4、#pragma pack()

  設定字節的對齊長度,這個指令我們在《C語言的那些小秘密之字節對齊》中已經講解了,在此不再複述。

  5、#pragma warning(disable:M N;once:H;error:K)

  表示不顯示M和N號的警告信息,H號警告信息只報告一次,把K號警告信息作為一個錯誤來處理。

  到此關於預處理的講解就結束了。由於本人水平有限,博客中的不妥或錯誤之處在所難免,殷切希望讀者批評指正。同時也歡迎讀者共同探討相關的內容,如果樂意交流的話請留下你寶貴的意見。

相關焦點

  • C語言的那些小秘密之volatile
    -4], 0h}  int b = a;  printf("b的值為:%d\n",b);  }  先分析下上面的代碼,我們使用了一句__asm {mov dword ptr [ebp-4], 0h}來修改變量a在內存中的值,如果有對這句代碼功能不清楚的讀者可以參考我之前的一篇《C語言的那些小秘密之堆棧
  • C語言的那些小秘密之字節對齊
    按照預先的計劃安排,這次應該是寫《C語言的那些小秘密之鍊表(三)》的,但是我發現如果直接開始講解linux內核鍊表的話,可能有些地方如果我們不在此做一個適當的講解的話,有的讀者看起來可能難以理解,所以就把字節對齊挑出來另寫一篇博客,我在此儘可能的講解完關於字節對齊的內容,希望我的講解對你有所幫助。
  • C語言的那些小秘密之動態數組
    不管什麼情況下通通使用靜態數組的方法來解決,在當初學習C語言的時候我就是一個典型的例子,但是現在發現這是一個相當不好的習慣,甚至可能導致編寫的程序出現一些致命的錯誤。那麼我們在自己編寫C語言代碼的時候就應該學會使用動態數組,這也就是我這篇博客要給大家講的,我盡我所能的用一些簡單的代碼來講解動態數組,希望我所講的對你有所幫助。  那麼我們首先來看看什麼是動態數組,動態數組是相對於靜態數組而言,從「動」字我們也可以看出它的靈活性,靜態數組的長度是預先定義好的,在整 個程序中,一旦給定大小後就無法改變。
  • C/C+編程筆記:C語言預處理命令是什麼?不要以為你直接寫#就行!
    C語言源文件要經過編譯、連結才能生成可執行程序: 1) 編譯(Compile)會將源文件(.c文件)轉換為目標文件。 這些在編譯之前對源文件進行簡單加工的過程,就稱為預處理(即預先處理、提前處理)。 預處理主要是處理以#開頭的命令,例如#include 等。預處理命令要放在所有函數之外,而且一般都放在源文件的前面。 預處理是C語言的一個重要功能,由預處理程序完成。
  • C語言的那些小秘密之異常處理
    第一行代碼定義了一個函數指針(註:如果有對函數指針知識點不熟悉的讀者可以去閱讀我之前寫的那篇文章《C語言的那些小秘密之函數指針》),其類型為含有一個int型參數,無返回值;  第二行代碼中,signal函數的返回值是一個函數指針,與第一行我們定義的類型相同,第二個參數也為一個函數指針,其實signal的返回值就是第二個函數指針指向的函數地址。
  • C語言的那些小秘密之函數的調用關係
    root@ubuntu:/home/shiyan# gcc -g -Wall sss.c -o p  root@ubuntu:/home/shiyan# ./p |awk '{print "addr2line "$3" -e p"}'>t.sh;. t.sh;rm -f t.sh  輸出結果為:  /home/shiyan/sss.c:12  /home/shiyan/sss.c:27  /home/shiyan/sss.c:34  /home/shiyan/sss.c:40
  • C語言的那些小秘密之斷言
    可能花了九六二虎之力寫出來的東西,因為摘要的失敗而前功盡棄,因為絕大多數的讀者看文章之前都會瀏覽下摘要,如果他們發現摘要「不對口」,沒有什麼特色和吸引人的地方,那麼輕則採用一目十行的方法看完全文,重則對文章判「死刑」,一篇文章的好壞雖然不能用摘要來衡量,但是它卻常常被讀者用來衡量一篇文章的好壞,從而成為了文章讀者數量多少的一個關鍵因素。
  • C語言的那些小秘密之變參函數的實現
    在學習C語言的過程中我們可能很少會去寫變參函數,印象中大學老師好像也沒有提及過,但我發現變參函數的實現很巧妙,所以還是特地在此分析下變參函數的實現原理
  • C語言預處理命令分類和工作原理
    本文轉載自【微信公眾號:strongerHuang,ID:strongerHuang】經微信公眾號授權轉載,如需轉載與原文作者聯繫C語言編程過程中,經常會用到如 #include、#define 等指令,這些標識開頭的指令被稱為預處理指令,預處理指令由預處理程序(預處理器)操作。
  • C語言的那些小秘密之函數指針
    我們經常會聽到這樣的說法,不懂得函數指針就不是真正的C語言高手。我們不管這句話對與否,但是它都從側面反應出了函數指針的重要性,所以我們還是有必要掌握對函數指針的使用。先來看看函數指針的定義吧。本文引用地址:http://www.eepw.com.cn/article/270442.htm  函數是由執行語句組成的指令序列或者代碼,這些代碼的有序集合根據其大小被分配到一定的內存空間中,這一片內存空間的起始地址就成為函數的地址,不同的函數有不同的函數地址,編譯器通過函數名來索引函數的入口地址,為了方便操作類型屬性相同的函數,c/c++引入了函數指針,函數指針就是指向代碼入口地址的指針
  • C語言中的 define預處理指令老手都是這樣用,你全都掌握了嗎?
    C語言有許多預處理命令,#define是其預處理命令之一。所有預處理命令以「#」號開頭,如包含命令#include,標準錯誤指令#error,#pragma指令等。#define指令用於宏定義,可以提高原始碼的可讀性,為編程提供方便,一般放在源文件的前面部分。本文簡要總結#define指令的多種用法及其注意事項。
  • CSS 預處理語言less快速入門
    Less 是一門 CSS 預處理語言,它擴充了 CSS 語言,增加了諸如變量、混合(mixin)、函數等功能,讓 CSS 更易維護、方便製作主題
  • c語言程序設計自學教程
    如果您不甘落後,那麼請自製自控,自學c語言程序設計也是完全可能的。c語言十分依賴於計算機思維,而思維的培養不是一日之功,而是一個日積月累的過程一:準確把握語法語句概念1、編譯預處理不是C語言的一部分,不佔運行時間,不要加分號。
  • 快速上手系列-C語言之預編譯命令、宏定義及條件編譯
    上一篇寫了C語言中變量的存儲類別,提到普通局部變量、普通全局變量和靜態局部變量及靜態全局變量,這裡簡單了解一下C語言的預編譯命令、宏定義和條件編譯。預編譯命令(預編譯處理--->編譯---->彙編--->連接)1、預處理:預處理是C語言的一個重要功能,如文件包含、常量定義都屬於預處理命令,C語言提供的預處理功能主要有以下三種:1)文件包含 #include2)宏定義 #define3)條件編譯 #if #endif4)防止頭文件重複包含2、文件包含處理
  • 《C語言入門指南》中篇
    此為中篇,涵蓋知識點為:函數、預處理命令、數組、排序和查找,中篇全文共計12000餘字,適用初學者入門C語言,非初學者也可以通過本文複習C語言相關知識點,強化記憶!十三發布這篇筆記也是為了複習C語言!本文已收錄到GitHub[1]開源倉庫【Ye13[2]】,點擊閱讀原文即可跳轉,進行star!
  • C語言簡明教程(一)C語言簡單剖析
    在C語言編譯器中新建一個文檔,命名為 1.1.c,輸入一下代碼:(我用的是VSCode,大家可以用其他的,編程的本質是不變的)#include<stdio.h>創建 C 程序C 語言的簡單結構預處理指令main()函數程序框架printf() 函數創建 C 程序C 程序的創建過程有四個基本步驟:編寫;編譯;連結;執行。
  • 《邊學邊用攻破C語言》第17集 宏定義define的用法
    《邊學邊用攻破C語言》是專門為單片機初學者準備的C語言基礎視頻教學課程,是科技老頑童
  • C語言全局變量那些事兒
    今天我們就來黑一把C語言,好好展示一下這門經典語言令人抓狂的一面。如果我們將main.c中的b初始化賦值,那麼就存在兩個強符號而違反了規則一,編譯器報錯。如果滿足規則二,則僅僅提出警告,實際運行時決議的是foo.c中的強符號。而變量a都是弱符號,所以只選擇一個(按照目標文件連結時的順序)。事實上,這種規則是C語言裡的一個大坑,編譯器對這種全局變量多重定義的「縱容」很可能會無端修改某個變量,導致程序不確定行為。
  • Linux下C編程基礎之:gcc編譯器
    表3.6 gcc所支持後綴名解釋後綴名所對應的語言後綴名所對應的語言.cC原始程序.s/.S彙編語言原始程序.C/.cc/.cxxC++原始程序.h預處理文件(頭文件).mObjective-C原始程序.o
  • C語言編寫程序求水仙花數
    C語言編寫程序求水仙花數水仙花數是一個數學問題,其實質是一個三位數,個位數的立方加十位數的立方加百位數的立方之和等於這個三位數本身。例如153=1*1*1+5*5*5+3*3*3,即153=1+125+27。