c primer plue 第 16 章 C前處理器和C庫 筆記
《預處理指標》
——————————————————————————————
前處理器不能理解c,它一般是接受一些文字將其轉換成其他文字。
——————————————————————————————
程式作預處理前,編譯器會對它進行幾次翻譯處理。
16.1 翻譯程式的第一步
第一步:編譯器先將原始碼中出現的字元對映到源字符集,這個過程處理多位元組字元和使c外觀更加國際化的三元字元擴充套件
========================================================================================================= (意思就是轉換成3原字元)
第二步:編譯器查詢反斜槓(\)後緊跟的換行符的例項並刪除,轉換成一個邏輯行。
因為預處理表示式的長度為一邏輯行,所以這個功能為預處理做了準備工作。而一個邏輯行可能會比物理行多。預處理指
令從#開始,直到其後第一個換行符為止(即指令的長度限於一行程式碼,即前處理器表示式的長度為一邏輯行)。在預處理
開始之前,系統會刪除反斜線和換行符之間的組合。因此可以把指令擴充套件到幾個物理行,由這些物理行組成單個邏輯行
例如:
printf(“That's wond\
erful!\n”);
轉換成邏輯行
printf("That's wonderful!\n");
注意:這個換行符是指按下鍵盤迴車的換行符,而不是‘\n’這個換行符。
這個功能是為預處理作準備,因為預處理表示式的長度為邏輯行。
========================================================================================================== (意思就是將物理行轉變為邏輯行)
第三步:編譯器將文字劃分成預處理的 1語言符號序列 2空白字元 3註釋序列
注意:用一個空格字元來代替每一個註釋 例:
int/*這不是個空格*/ fox;
變成
int fox;
c實現可能還會選用單個空格代替每一個空白字元序列(不包括換行符)
========================================================================================================== (將文字劃分成語言序列,空白字元,註釋序列)
第四部:
程式進入預處理階
段。前處理器尋找可能存在的預處理指令。這些指令由一行開始處的#符號標識。
==========================================================================================================
16.2 明顯常量:#define
預處理指令用#號作為開頭。
ANSI標準允許#符號前有空格或者製表符,而且該標準還允許在#和指令的其餘部分之間有空格(意思就是#define的其他部分有空格)。
指令可以在原始檔的任何地方,指令定義的作用域從定義出現的位置開始知道檔案的結尾。~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~(作用域從定義開始到檔案結尾)
預處理指令從#開始,到其後第一個換行符為止。也就是說指令的長度限於一行程式碼。也就是前面所說的刪除反斜槓(\)和換行符的~~~~(從開始到其後的第一個換行符為止)
組合。因為可以把指令擴充套件到幾個物理行,由這些物理行組成單個邏輯行
每個#define行(邏輯行)由3部分組成:
1.指令#define自身
2.縮略語句(自己定義的名字),也稱為“巨集”這些巨集稱為“類物件巨集”,巨集名字中不允許空格遵循變數名規則。
3.(#define行的其餘部分)稱為“替換列表”或“主體”。
#define px printf(“x is %d\n”,x)
| 指令 ||巨集| | 替換列表或主體 |
在程式中發現的巨集都是用替換列表替代這些巨集。
巨集變成替換列表的過程稱為“巨集展開”。
預處理指令不進行計算的,只按照指令進行文字替換操作,一系列運算過程發生在編譯階段。
巨集定義可以包含其他巨集
const char * fmt = "X is %d.\n"; fmt可用作printf控制字串。
#define HAL‘Z’字元常量 #define HAL "Z"字串因為“Z\0”
一般而言,預處理髮現程式中的巨集後,會用等價替換列表進行代替巨集。如果該字串中還包含巨集則繼續替換
例外情況雙引號內的巨集。雙引號內的巨集其實不是巨集是字串的一部分。
什麼時候使用符號常量?(就是一般用#define這些都是"符號常量"\"明顯常量")
記憶值的能力、易更改性、可移植性,這些功能使得符號常量很有使用價值
=========================================================================================================
16.2.1 語言符號
系統把巨集的主體當作語言符號型別字串,而不是字元型字串。
c前處理器中的語言符號是巨集定義主體中的一個單獨的詞,用空白字元把這些詞分開。例如:
#defien FOUR 2*2 //有一個語言符號
#define SIX 2 * 3 // 這裡有3個語言符號2、*和3。因為2*3用空白字元分開 ~~~~~~~~~~~~~~~~~~~~~~~~~~(有幾個空格就有幾個語言符號)
處理主體中的多個空格時,字元型字串和語言符號型別字串處理方法不同:
#define EIGHT 4 * 8
主體解析為“字元型字串”時,預處理用4 * 8 替換EIGHT,額外的空格也當成替換的一部分~~~~~~~~( 用字元型字串空格也是主體一部分)
主體解析位“語言符號型別字串”時,預處理用空格分隔3個語言符號即4 * 8來替換EIGHT。~~~~~~~~~~~~~(用語言符號型別字串空格只是分隔主體語言符號的符號)
c編譯器處理語言符號要比前處理器的處理方式更加複雜,編譯器能理解c的規則不需要用空格分隔語言符號。
例如: 2*2 //3個語言符號。原因c編譯器認為每個2都是一個常量,而*是一個運算子。
=========================================================================================================
16.2.2 重定義常量
例如:把LIMIT 定義為20,後來在該檔案中又把LIMIT定義為25.這個過程稱為重定義常量
ANSI標準採用的第一種方法:只允許新定義與舊定義完全相同。
意味著相同定義主體具有相同順序的語言符號。
定義相同:
#define SIX 2 * 3
#define SIX 2 * 3
這兩者相同都是有三個語言符號,額外的空格不是主體的一部分。
#define SIX 2*3 則只有一個語言符號與前面的兩個定義不同
=========================================================================================================
16.3 在#define中使用引數
通過使用引數,可以建立外形和作用都與函式相似的“類函式巨集”。
引數也是用括號括起來。引數會出現在替換主體部分。
#define MEAN(x,y) ((x)+(y)/2)
| 巨集 | | 替換主體 |
類函式巨集定義的組成
#define SQUARE(X) X*X
// SQUARE為巨集識別符號, SQUARE(X)中的X為巨集的引數,X*X為替換列表。所以程式中出現SQUARE(X)的地方都用X*X代替。
使用這個巨集時,即可以使用x,也可以自由地使用其他符號。巨集定義中的x由程式呼叫的巨集中的符號代替。
例子:
SQUARE(X)X*X
z=SQUARE(2) z=4
SQUARE(X)X*X
z=SQUARE(z+2)X*X 是4+2*4+2=14而不是6*6=36因為預處理不進行計算,只進行文字替換。函式呼叫和巨集呼叫之間的重要差異。
函式呼叫是把引數的值“傳遞“給函式,而編譯前,巨集呼叫把引數語言符號傳遞給程式。
那如何把z+2輸出36?只須在替換主體加括號就行:
SQUARE(z+2) (X)*(X)=(4+2)*(4+2)=36
但是還未解決所有問題如下:
100/SQUARE(2)
=100/2*2
=(100/2)*2
=100 //理想值是25
解決這個問題就要
#define SQUARE(X) (X*X)
100/(2*2)=25
所以要解決以上問題就要如下才行:
#define SQUARE(x)((x)*(x))
注意還有一種特殊情況:
SQUARE(++X)
就變成++x*++x進行兩次增量運算5*6=30。
避免在巨集的引數中使用增量或減量
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結就是不要在巨集中使用增量或者減量還有要使用括號)
=========================================================================================================
16.3.1 利用巨集引數建立字串:#運算子
#define PSQR(x)printf("The square of x is %d.\n",((x)*(x)))
PSQR(8)輸出 The square of x is 64.
注意:引號內的X被看作普通文字,而不是可被替換的語言符號。
如果希望字串包含巨集引數。在類函式巨集的替換部分中,#符號用作一個預處理運算子,它可以把語言符號轉化為字串。
例如:如果x是一個巨集參量,那麼#x可以把引數名轉化為相應的字串。該過程稱為字串化
#define PSQR(X) printf(“The square of”#x“is %d。\n”,((x)*(x)))
PSQR(y): The square of y is 25
PSQR(2+4):The square of 2+4 is 36
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結在替換列表(主體)的字串中使用“#x”,會用巨集引數代替字串中的#x)
=========================================================================================================
16.3.2前處理器的粘合劑:##運算子
和#運算子一樣,
##運算子可以用於"類函式巨集"的替換部分,
##還可以用於“類物件巨集”的替換部分。
#define XNAME(n) x ## n
XNAME(4)//x4
#define PRINT_XN(n) printf(“x”#n“=%d\n”,x ## n);
int XNAME(1)=14//變為 int x1=14;
int XNAME(2)=20//變為int x2=20;
PRINT_XN(1);//變為printf(“X1=%d \n ”,x1);
PRINT_XN(2);//變為printf(“X2=%d \n”,x2);
注意:如何使用#組合字串,如何使用##把兩個語言符號組合為一個新的“識別符號”
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結使用##可以把兩個語言符號組合為一個新的識別符號)
=========================================================================================================
16.3.3 可變巨集:...和_ _VA_ARGS_ _
在巨集定義中引數列表的最後一個引數為省略號(...),就可以做到可變數量的引數。
這樣預定義巨集 _ _VA_ARGS_ _用在替換(替換列表(主體))部分中,以表明省略號代表什麼。
例:
#define PR(...) printf(_ _VA_ARGS_ _)
PR(“Howdy”);
PR(“weight=%d,shipping=$%.2f\n”,wt,sp);
第一次呼叫,展開引數為1個引數:
“Howdy”
第二次呼叫,展開引數為3個引數:
“weight=%d,shopping=$%.2f\n”,wt,sp
因此,展開後的程式碼為:
printf(“Howdy”);
printf(“weight=%d,shopping=$%.2f\n”,wt,sp);
#define PR(x,...) printf("Message " #x" :"_ _VA_ARGS_ _)
PR(1,"X=%g\n",x);//展開printf("MESSAGE" "1" ":" "x=%g\n",x);
PR(2,"X=%.2f,y=%.4f\n",x,y);//展開printf("MESSAGE" "2" ":" "X=%.2f,y=%.4f\n",x,y);
注意:省略號只能代表最後一個巨集引數
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結用...和_ _VA_ARGS_ _可變引數)
=========================================================================================================
16.4 巨集,還是函式
許多工既可以使用帶引數的巨集,也可以使用函式完成。那應該使用巨集還是函式?
1.有些巨集不注意就會產生一些奇怪的現象,某種情況下要比函式複雜。有些編譯器限制巨集只能定義成一行。
2.巨集與函式的選擇就是"時間和空間"的權衡。巨集產生內”聯程式碼”;也就是說,在程式中產生語句。如果:
用巨集20次,則會把20行程式碼插入程式中。如果使用函式20次,那麼程式中只有一份函式語句拷貝,因此節省了空間。
另外一方面,程式的控制必須轉移到函式中並隨後反悔呼叫程式,因此這比內“聯程式碼”花費的時間多。
總結:函式佔用空間少,時間長,巨集反之。
3.巨集的一個優點它不檢查其中的變數型別(因為巨集處理字元型字串,而不是實際值)。因此,對於int或folat
都可以使用巨集SQUARE(x)。
注意幾點:
1.巨集名字中間不能有空格,但是在替換字串中可以使用空格(引數列表允許使用空格)。
2.用圓括號括住“每”一個引數,並括住巨集的整體定義。
3.用大寫字母表示巨集函式名。大寫字母可以提醒程式設計師注意巨集可能產生副作用。
4.如果打算用巨集代替函式提高速度,那首先確定巨集是否引起重大差異。在程式中使用一次的巨集對程式執行時間不會產生明顯的改善。
巢狀裡使用巨集有助於程式執行速度。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結用巨集速度快但是佔用空間多,用函式反之)
=========================================================================================================
16.5 檔案包含:#include
前處理器發現#include後,就會尋找後跟的檔名並把檔案的內容包含到當前檔案中。
被包含檔案中的文字將替換原始碼檔案中的#include指令。就像你把被包含檔案中的全部內容鍵入到原始檔中
的這個特定位置一樣。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結使用標頭檔案等於將檔案全部內容鍵入原始檔中)
#include指令的兩種使用形式
1.#include<stdio.h> 檔名放在尖括號中 :尖括號告訴前處理器在一個或多個標準系統目錄中尋找檔案。
2.#include“mystuff.h” 檔名放在雙引號中 :雙引號告訴前處理器先在當前目錄(或檔名中指定的其它目錄)中尋找檔案,
然後在標準位置尋找檔案。
#include指令的一些例子
#include<stdio.h> 搜尋系統目錄
#include"hot.H" 搜尋當前工作目錄
#inlcude"/usr/biff/p.h" 搜尋/usr/biff目錄
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結指令的使用兩種方法尖括號和雙引號中)
對於系統標頭檔案,整合開發環境(IDE)具有標準搜尋路徑。許多整合開發環境提供選單選項用於指定使用尖括號時搜尋的其他路徑
對於UNIX,使用雙引號意味著“先”搜尋本地目錄,但具體搜尋那一個目錄依賴於編譯器。
為什麼要使用標頭檔案?
因為這些檔案包含編譯器所需的資訊。很多情況下,標頭檔案中的內容是編譯器產生最終程式碼所需的資訊,而不是加到最終程式碼裡的具體語句。
標頭檔案內容的最常見的形式包括:
1.明顯常量 ---例如,典型的stdio.h檔案定義EOF、NULL、BUFSIZE(標準I\O運衝區的大小)。
2.巨集函式 ---例如,getchar()通常被定義為getc(stdin),getc()通常被定義為較複雜的巨集,而
標頭檔案ctype.h 通常包含ctype函式的巨集定義。
3.函式宣告 ---例如,標頭檔案 string.h(在某些舊的系統中為 strings.h)包含字串函式系列的函式
宣告。在ANSI C中,宣告採用函式原型形式。
4.結構模版定義---標準I/O函式使用FILE結構,該結構包含檔案及檔案相關緩衝區的資訊。頭文
件 stdio.h 中存放FILE結構的宣告。
5.型別定義---可以使用指向 FILE 的指標作為引數呼叫標準 I/O 函式。通常,stdio.h 用#define 或
typedef 使得FILE代表指向FILE結構的指標。與之類似,size_t 和 time_t 型別也在標頭檔案中定義。
開發自己的標準標頭檔案以便在程式中使用。特別有價值
用標頭檔案宣告多個檔案共享的外部變數。例如開發共享某個變數的一系列函式,報告某種型別的狀態,這方法很有用處
使用具有檔案作用域、內部連線、const限定詞的變數或陣列。使用const可以避免值被意外改變。使用static後,每個額包含該標頭檔案
的檔案都獲得一份該常量的副本。因此,不存在這樣的問題,即需要在一個檔案中進行定義宣告,並且需要在其他檔案中進行引用
宣告。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結開發自己的標準標頭檔案有很高的價值)
=========================================================================================================
16.6其他指令
不同的工作環境準備不同的c程式或c庫包。
=========================================================================================================
16.6.1 #undef 指令
#undef指令取消前面#define的定義。
#define LIMIT 400
則指令:
#undef LIMIT
會取消該定義。現在可以重新定義LIMIT,以使它有一個新的值。即使開始沒有定義 LIMIT,取消LIMIT的定義也是合法的。
如果想使用一個特定的名字,但又不能確定前面是否已經使用該名字,為安全起見就可以取消該名字定義。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(#undef取值前面#define定義的巨集)
=========================================================================================================
16.6.2 已定義:c前處理器的觀點
與處理器在預處理指令中遇到'識別符號"時,要麼把識別符號當作已定義,要麼當作未定義的。
已定義表示又前處理器定義。如果識別符號是該檔案前面的#define指令建立的巨集名,並且沒有用#undef指令關閉該識別符號,
則識別符號是已定義的。
如果識別符號不是巨集,而是(例如)一個具有檔案作用域的c變數,那麼前處理器把識別符號當作未定義。
已定義巨集可以為類物件巨集(包括空巨集)或類函式巨集:
#define LIMIT 1000 // LIMIT 是已定義的
#define GOOD // GOOD 是已定義的
#define A(X)((-(X))*(X)) // A 是已定義的
int q; // q 不是一個巨集,因此是未定義的
#undef GOOD // GOOD 是未定義的
注意,#defien巨集的作用域從檔案中的定義點開始,直到用#undef指令取消巨集為止,或值到檔案尾為止。
注意:如果用標頭檔案引入巨集,那麼#define檔案中的位置就依賴於#include指令的位置。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結巨集是否定義)
=========================================================================================================
16.6.3 條件編譯
設定條件編譯,可以使用這些指令告訴編譯器:根據編譯時的條件接受或者忽略資訊(或程式碼)塊。
//////////////////////////////////////////////////////////////////////
一、#ifdef、#else、#endif指令
比較新的實現和ANSI標準支援的縮排格式。
#ifdef MAVIS
#include "horse.h" //如果已經用#define定義了MAVIS,則執行這裡的指令
#define STABLES 5
#else //如果沒有用#define定義MAVIS,則執行這裡的指令
#include "cow.h"
#define STABLES 15
#endif
舊的編譯器,必須要使用所有指令,或者至少使#指令符號左對齊。
#ifdef MAVIS
# include "horse.h" //如果已經用#define定義了MAVIS,則執行這裡的指令
# define STABLES 5
#else //如果沒有用#define定義MAVIS,則執行這裡的指令
# include "cow.h"
# define STABLES 15
#endif
#ifdef指令說明:前處理器已經定義了後面的識別符號,那麼執行所有指令並編譯c程式碼,直到下一個#else或#endif出現為止。
如果有#else指令,那麼,在未定義識別符號時會執行#else和#endif之間的所有程式碼。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結#ifdef條件編譯,前處理器是否已經定義後面的識別符號是就執行程式碼到#else或#endif為止)
#ifdef #else格式非常類似於c的 if else。主要差異就是前處理器不能識別標記程式碼塊的花括號{},
因此使用#else(如果需要)和#endif(必須存在)來標記指令塊,這些條件結構可以巢狀。
#include <stdio.h>
#define JUST_CHECKING
#define LIMIT 4
int main(void)
{
int i;
int total = 0;
for(i = 1;i <= LIMIT;i++)
{
total += 2*i*i + 1;
#ifdef JUST_CHECKING
printf("i=%d,running total = %d\n",i,total);
#endif
}
printf("Grand total = %d\n",total);
return 0;
}
編譯並執行該程式,產生下列輸出:
i=1,running total = 3
i=2,running total = 12
i=3,running total = 31
i=4,running total = 64
Grand total = 64
使用這種方法輔佐除錯程式。使編譯器包含那些用於列印輔佐除錯的中間值的程式程式碼。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結#ifdef、#else和if、else的分別在於預處理不能識別程式碼塊的花括號。#endif必須要有)
//////////////////////////////////////////////////////////////////////
二、#ifndef指令
類似#ifndef指令,#ifndef指令可以與#else、#endif指令一起使用。#ifndef判斷後面的識別符號是否為未定義的,#ifndef的
反義詞為#ifdef。#ifndef通常用來定義此前未定義的常量。
/*arrays.h*/
#ifndef SIZE (檢測SIZE是否為未定義)
#define SIZE 100
#endif
#ifndef防止對巨集重複定義。當某檔案包含幾個標頭檔案,而且每個標頭檔案都可能定義相同的巨集時就使用#ifndef防止重複定義。
第一個標頭檔案的定義變成有效定義,其他標頭檔案中的定義則忽略。
例如:
#define SIZE 10
#include"arrarys.h"
因為SIZE已經定義所以arrarys.h的#ifndef不執行SIZE 100。
#ifndef指令通常用於防止多次包含同一檔案。也就是說,標頭檔案可採用類似下面幾行的設定:
/*things.h*/
#ifndef THINGS_H_
#define THINGS_H_
//標頭檔案的其餘部分//
#endif
假設多次包含了thigs.h這個檔案,第一次遇到該包含檔案時,THINGS_H_是未定義的,因此程式接著定義THINGS_H_,並處理檔案的其餘部分
前處理器下一次遇到該檔案時,THINGS_H_已經定義,因此,前處理器會跳過該檔案的其餘部分。
為什麼會多次包含同一檔案?
常見原因是許多包含檔案自身包含其他檔案,因此可能顯式地包含其他檔案已經包含的檔案。
//////////////////////////////////////////////////////////////////////
三、#if和#elif指令
#if指令更像常規的c中的if;#if後跟常量整數表示式。如果表示式為非零值,則表示式為真。在該表示式中可以使用c的關係運算子和
邏輯運算子。
#if SYS==1
#include “ibm.h”
#endif
可以使用#elif(有些早期的實現不支援#elif)指令擴充套件if-else序列。例如,可以這樣使用:
#if SYS==1
#include “ibmpc.h”
#elif SYS==2
#include "VAX.h"
#elif SYS==3
#include "mac.h"
#else
#include "general.h"
#endif
一種新的方法來判斷一個名字是否已經定義。
不需要用舊的:#ifdef VAX
新的方法:#if defined(VAX)
defined是一個前處理器運算子。如果引數已經用#define定義過,那麼defined反悔1;否則返回0。這種方法的優點是
在於它可以和#elif一起使用。
#if defined(IBMPC)
#include "ibmpc.h"
#elif defined(VAX)
#include "VAX.H"
#elif defined(MAC)
#include "mac.h"
#else
#include "general.h"
#endif
使用條件編譯可以使程式更易於移植。不同系統設定不同值幷包含不同檔案。
=========================================================================================================
16.6.4 預定義巨集
表16.3 預定義巨集
巨集 意 義
-DATE-- 進行預處理的日期("Mmm dd yyyy"形式的字串文字)
-FILE-- 代表當前原始碼檔名的字串文字
-LINE-- 代表當前原始碼檔案中的行號的整數常量
-STDC-- 設定為1 時,表示該實現遵循C標準
-STDC_HOSTED-- 為本機環境設定為1,否則設為 0
-STDC_VERSION-- 為C99 時設定為 199901L
-TIME-- 原始檔編譯時間,格式為"hh:mm:ss"
C99標準提供了一個名為__func__的預定義識別符號。__func__展開為一個代表函式名(該函式包含該識別符號)的字串。
該識別符號具有函式作用域,而巨集本質上具有檔案作用域。因而__func__是C語言的預定義識別符號,而非預定義巨集。
=========================================================================================================
16.6.5 #line 和#error
#line 指令用於重置 由_ _LINE_ _和 _ _FILE_ _巨集報告的行號和檔名。
#line 1000 //把當前行好重置為1000
#line 10 “cool.c” //把行號重置為10,檔名重置為cool.c
#error指令使前處理器發出一條錯誤訊息,訊息包含指令中的文字。在“編譯過程”中斷。
#if _ _STDC_VERSION_ _!=199901L
#error NOT c99
#endif
=========================================================================================================
16.6.6#pragma
在現代的編譯器中,可用命令列引數或IDE選單修改編譯器的某些設定。也可用#pragma將編譯器指令置於程式碼中。
例如:開發c99時,用c9x代表c99.使用"編譯指示"來啟用對c9x的支援:
#pragma c9x on
一般來說,每臺編譯器都有自己的編譯指示集
1.控制分配給自動變數的記憶體大小。
2.設定錯誤檢查的嚴格程度。
3.啟動非標準語言特徵。
C99 還提供了_Pragma前處理器運算子。_Pragma可將字串轉換成常規的"編譯指示"。例如:
_Pragma("nonstandardtreatmenttypeB on")
等價於下面的指令:
#pragma nonstandardtreatmenttypeB on
因為該運算子沒有使用#符號,所以可將它作為巨集展開的一部分:
#define PRAGMA(X)_Pragma(#X)
#define LIMRG(X)PRAGMA(STDC CX_LIMITED_RANGE X)
接著可以使用類似下面的程式碼:
LIMRG(ON)
{
LIMRG(NO)展開 PRAGMA(STDC CX_LIMITED_RANGE NO)//因為LIMARG的引數是X所以替換主體的語言符號X被替換成NO
PRAGMAX(x)展開 _PRAGMA("STDC CX_LIMITED_RANGE NO")
}
順便提一下,雖然下面的定義看上去可以正常執行,但實際並非如此:
#define LIMRG(X)_Pragma(STDC CX_LIMITED_RANGE #X)
問題在於上面的程式碼依賴於字串連線功能,但是,直到預處理過程完成後編譯器才連線字串。
_Pragma運算子完成字串析構(destringizing)工作;也就是說,將字串中的轉義序列轉換成它所
代表的字元。因而:
_Pragma("use_bool \"true \"false")
變成:
#pragma use_bool "true "false
=========================================================================================================
16.7行內函數
通用函式呼叫需要一定的時間開銷。執行呼叫的時間花費用於建立呼叫、傳遞引數、跳轉到函式程式碼段並返回。
使用類函式巨集可以減少執行時間。
C99提供另外一種方法:行內函數,把函式變為行內函數將建議編譯器儘可能快速地呼叫該函式。
使用行內函數可能簡化函式的呼叫機制,但也“可能不起作用”。
建立行內函數的方法的在函式宣告中使用函式說明符inline,首次使用行內函數前在檔案中對該函式進行定義:
#include<stdio.h>
inline void eatline()//行內函數的定義/原型
{
while(getchar()!='\n')
continue;
}
int main()
{
....
eatline();
....
}
編譯器看到內聯宣告後會用eatline()函式代替函式呼叫,等於你在此處鍵入了函式體的程式碼:
int main()
{
...
while(getchar()!='\n')
continue;
...
}
因為行內函數沒有預留給它的單獨程式碼塊,所以無法獲得行內函數地址(實際上,可以獲得地址,但是這樣會使編譯器產生非行內函數)。
行內函數不會在偵錯程式中顯示。
行內函數要比較短小,對於很長的函式,呼叫函式的時間少於執行函式主體的時間;此時使用行內函數不會節省多少時間。
行內函數的定義和對該函式的呼叫必須在同一檔案中。因為這樣,行內函數通常具有內部連結。因此,每個呼叫行內函數的檔案都要對
該函式進行定義。
達到這個目的的最簡單方法:在標頭檔案中定義行內函數,並在使用該函式的檔案中包含該標頭檔案。
與c++不同,c允許混合使用行內函數定義和外部函式定義。
c甚至允許在包含行內函數定義的檔案中放置外部函式宣告。
以下情況不宜使用內聯:
(1)如果函式體內的程式碼比較長,使用內聯將導致記憶體消耗代價較高。
(2)如果函式體內出現迴圈,那麼執行函式體內程式碼的時間要比函式呼叫的開銷大。
一個好的編譯器將會根據函式的定義體,自動地取消不值得的內聯(這進一步說明了inline 不應該出現在函式的宣告中)。
=========================================================================================================
16.8 c庫
如何使用庫:
16.8.1 訪問c庫
首先在多個不同的位置找到庫函式,第二不同系統使用不同的方法搜尋這些函式
一、自動訪問
許多系統上,你只需要編譯程式,一些常見的庫函式自動可用。
記住,應該宣告所使用的函式的型別,通常包含適當的標頭檔案即可做到這一點。描述庫函式,函式型別到使用者手冊中查詢。
二、檔案包含
如果函式定義為巨集,可以使用#include指令來包含擁有該定義的檔案。
三、庫包含
程式編譯或連線的某些階段,你可能需要指定庫選項。即使在自動檢查標準庫的系統上,也有可能有不常使用的函式庫。
必須使用編譯時選項來顯示地指示這些庫。注意要把這個過程與包含標頭檔案區分開來。
標頭檔案提供函式宣告或原型,而庫選項告訴系統到那尋找函式程式碼。
16.8.2 參考庫描述
庫文件可以在一下地方查詢:系統線上手冊、整合開發環境線上幫助、c提供商描述庫函式的使用者指南書籍、出版社發行c庫函式參考手冊
附錄B中提供了一個小結。
void型別的指標作為通用指標,指向不同型別的指標時可採用void指標
=========================================================================================================
16.9 數學庫
數學庫包含許多有用的數學函式。標頭檔案math.h
要回頭看
=========================================================================================================
16.10 通用工具庫
通用工具庫包含各種函式,有隨機數產生函式、搜尋和排序函式、轉換函式、和記憶體管理函式。
16.10.1 exit()和atexit()函式
main()返回時自動呼叫exit()函式。
指定執行exit()是呼叫特定函式,通過對退出時呼叫的函式提前進行註冊,atexit()函式提供這項註冊功能。
用函式指標作為引數。
一、使用atexit()
這個函式的引數是函式指標。只要把退出時要呼叫的函式的地址傳遞給atexit()進行“列表註冊”。這個列表中至少可放“32個”函式
呼叫exit()函式,按先進後出(先執行最後新增的函式)的順序執行這些函式。
atexit()註冊的函式不接受任何“引數”的VOID函式。
main()終止時會隱似地呼叫exit(),因此,即使未顯示地呼叫exit(),也會執行。
二、使用exit()
exit()執行了atexit()指定的函式後:
自身清理工作
重新整理輸出流
關閉所有開啟的流
並關閉通過呼叫標準i/o函式tmpfile()建立的臨時檔案。
然後exit()把控制返回給主機環境。
向主機環境報告終止狀態。
unix程式用0表示成功終止,用非零值表示失敗。
unix返回的程式碼並不適用於所有系統,因此ANSI C 定義了可移植的表示失敗的巨集EXIT_FAILURE,成功EXIT_SUCCESS。
exit()也接受用0代表成功。
在非遞迴的main()函式中使用exit()函式等價於使用關鍵字return。但是,在main()以為的函式中使用exit()也會終止程式。
=========================================================================================================
16.10.2 qsort()函式
快速排序(quick sort)法是排序演算法之一。它把陣列不斷分成更小的陣列,直到變成單個元素陣列。
qsort()函式對資料物件“陣列”進行排序。
函式原型 void qsort (void*base,size_t nmemb.size_t size,int(*compar)(const void *,const void *));
第一個引數為指向要排序的陣列頭部的指標。“ANSI C允許將任何資料型別的指標轉換為VOID型別指標”
因此第一個實際引數可以指向任何型別的陣列。
第二個引數為需要排序的專案數量。函式原型將該值轉換為size_t型別。size_t是由運算子sizeof返回。
第三個引數因為qsort()將第一個引數轉換為void指標,所以會“失去每一個陣列元素的大小資訊”。為補充該資訊
必須把資料物件的大小明確地告訴qsort()。例如:對double陣列排序,可使用sizeof(double)作為qsort()的第三個引數
第四個引數,qsort()還需要一個指向函式的指標,被指向的函式用於確定排序順序。
比較函式接受兩個引數,分別指向進行比較的兩個專案的指標。如果第一個專案的值大於第二個的值,比較函式返回正數;
如果兩個專案值相等,返回0;如果第一個專案值小於第二個專案值返回負數。qsort()根據給定的其他資訊計算出兩個指標的值,
然後把他們傳遞給該比較函式。
int(*compar)(const void *,const void *)指向函式的指標,返回int,並接受兩個引數,每一個引數指向const void型別指標。
這兩個指標指向需要比較的專案。
=========================================================================================================
一、使用qsort()
void qsort (void*base,size_t nmemb.size_t size,int(*compar)(const void *,const void *));
第一個引數為指向排序的陣列頭部指標,第一個實際引數指向任何型別的陣列。
第二個引數為排序的專案數量,即陣列元素的個數。
第三個引數為每個元素的大小。sizeof(double)。
第四個引數為對元素進行比較的函式的地址。
二、定義mycomp()
qsort()原型規定了比較函式的形式:
int(*compar)(const void *,const void *) 指向函式的指標。
函式名是該函式的指標
注意:qsort()的第一個引數指整個陣列,比較函式的兩個引數是指陣列中的兩個元素。
問題:要比較指標型值,需要對指標進行取值運算。因為要比較的值為double型別,所以應當對double型別的指標進行取值運算。
但是qsort要求void型指標。
解決這個問題的方法是:在函式內部宣告兩個正確型別的指標,並把他們初始化為傳遞進來的引數值。
為通用性,qsort()和比較函式使用void指標。因此,必須把陣列中的每個元素的大小明確告訴qsort;並在比較函式定義中,把指標
引數轉換為對具體應用型別正確的指標。
=========================================================================================================
16.11 診斷庫
由標頭檔案assert.h支援的診斷庫是設計用於輔佐除錯程式的小型庫。
由巨集assert()構成,引數接受整數表示式。如果表示式值為假(非零),assert()向標準錯誤流(stderr)寫一條錯誤訊息
並呼叫“abort()函式以終止程式”(stdlib.h中定義了abort()函式的原型)。
assert()巨集的作用為:標識出程式中某個條件應為“真”的關鍵位置,並在條件為假的時候用assert()語句終止該程式。
通常assert()引數為關係或邏輯表示式。
如果assert()終止程式,那麼它首先會顯式失敗的判斷、包含該判斷的檔名和行號。
assert(z>=0)並不是聲稱z>=0,而是聲稱z>=0這個條件沒有得到滿足。
用if語句也可以完成類似的工作:
if(z<0)
{
puts("z less than 0");
abort();
}
但是assert()方式有幾個好處。
1、自動識別檔案
2、自動識別發生問題的行號。
3、無需修改程式碼就能開啟或禁用assert()巨集的機制。
如果您認為已經排除了程式的漏洞,那麼可把巨集定義 “#define NDEBUG”放在assert.h包含語句所在位置前。編譯器將禁用檔案中所有
assert()語句。如果程式又出現問題,可以去除這個#define指令,就會重新啟用assert()
=========================================================================================================
16.12 string.h庫中的memcpy()和memmove()
不能把一個陣列的值賦予給另一陣列,因此,利用迴圈把陣列中的元素逐個複製到另一陣列。
例外情況:strcpy和strncpy函式複製字元陣列。
memcpy和memove函式為複製其他型別的陣列提供了類似的便利工具
void *memcpy(void *restrict s1,const void *restrict s2,size_t n);
void *menmove(void *s1,const void *s2,size_t n);
功能:兩個函式都是從s2指向的位置複製n位元組資料到s1指向的位置,且均返回s1的值。
差別:兩者的差別由關鍵詞 restrict造成,即memcpy()可以假定兩個“記憶體區域”之間沒有重疊。memmove()函式則不做這個假定,
因此,複製過程類似於首先將所有位元組複製到一個臨時緩衝區,然後再複製到最終目的地。
如果兩個區域存在重疊時使用memcpy()會怎樣呢?其行為是不可預知的,可能正常,可能失敗。
在不應該使用memcpy()時,編譯器不會禁止使用memcpy()。因此,使用memcpy()時,你必須確保沒有重疊區域。這是衝需要的任務的一部分。
c允許任何型別的指標賦值給void*型別指標。
接受各種型別指標導致函式無法知道要複製的資料型別。因此,這兩個函式使用第三個引數來指定要複製的位元組數。注意,對“陣列而已,位元組數一般不等於
元素的個數。”因此複製10個double值組成的陣列,那麼應使用10*sizeof(double)作為第三個引數,不應該使用10
=========================================================================================================
16.13 可變引數:stdarg.h
標頭檔案stdarg.h 為函式提供了可變巨集,可變個數的引數。不過使用方法稍微複雜一些。必須按如下步驟進行:
1.在函式原型中使用省略號。void f1(int n,...)
2.在函式定義中建立一個va_list型別的變數。 va_list
3.用巨集將該變數初始化為一個引數列表。va_start()
4.用巨集訪問這個引數列表。va_arg()
5.用巨集完成清理工作。va_end()
1、這類函式的原型應具有一個“參量列表”,參量列表中至少有一個“後跟省略號”的參量:
void f1(int n,...) 合法
int f2(const char *s,int k,...) 合法
char f3(char c1,...,char c2) //無效,省略號不是最後一個參量
double f3() 無效,沒有任何參量
最右邊的參量(省略號前)起著特殊作用;ANSI標準使用parmn表示該參量。f1 n為parmn,f2 k為parmn。傳遞給該參量的實際引數值將是“省略號部分代表的
引數個數”
f1(2,200,400)//額外的2個引數
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
2、在標頭檔案stdargs.h中宣告的va_list型別代表一種資料物件。在函式定義中宣告va_list,該資料物件用於存放參量列表中省略號部分代表的參量。
可變函式定義的起始部分應像下面這樣:
double sum(int lim,...)
{
va_list ap;
...
}
本例中,lim為參量parmN,由它來“指定可變引數列表中的引數個數”。
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
3、函式將使用stdargs.h中定義的巨集va_start()把引數列表複製到va_list 變數中。
巨集va_start()有兩個引數:va_list型別的變數和參量parmN。
va_start(ap,lim); 把ap初始化為引數列表
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
4、訪問訪問引數列表中的內容。
第一步 涉及va_arg()的使用。該巨集接受兩個引數:va_list型別變數、一個型別名。
第一次呼叫va_arg()時,返回列表的第一項,下一次呼叫返回第二項,以此類推。
型別引數指定返回值的型別。例如,如果引數列表中第一個引數為double型別,第二個為int型別,那麼可以使用下列語句:
double tic;
int toc;
。。。
tic=va_arg(ap,double);取得第一個引數
toc=va_arg(ap,int); 取得第二個引數
注意,實際引數的型別必須和說明的型別相匹配。如果第一個引數為10.0,那麼前面的tic部分的程式碼正常工作;
但是如果引數為10,程式碼就可能無法工作。不像賦值那樣進行型別自動轉換的。
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
5、應用巨集va_end()完成清理工作。例如,釋放動態分配的用於存放引數的記憶體。巨集接受一個va_list變數作為引數:
va_end(ap); //清理工作
此後只能用va_start()重新對ap初始化後,才能使用變數ap。
因為va_arg()不提供後退回先前引數的方法,所以儲存va_list變數的副本會是有用的。
c99為此專門新增了巨集va_copy().該巨集的兩個引數均為va_list型別變數,它將第二個引數複製到第一個引數中:
va_list ap;
va_list apcopy;
double
double tic;
int toc;
...
va_start(ap,lim); // 把 ap 初始化為引數列表
va_copy(apcopy,ap); // apcopy 是 ap的一個副本
tic = va_arg(ap,double); // 取得第一個引數
toc = va_arg(ap,int); // 取得第二個引數
可變函式比可變巨集更復雜,但是函式使用範圍更廣
——————————————————————————————
前處理器不能理解c,它一般是接受一些文字將其轉換成其他文字。
——————————————————————————————
程式作預處理前,編譯器會對它進行幾次翻譯處理。
16.1 翻譯程式的第一步
第一步:編譯器先將原始碼中出現的字元對映到源字符集,這個過程處理多位元組字元和使c外觀更加國際化的三元字元擴充套件
========================================================================================================= (意思就是轉換成3原字元)
第二步:編譯器查詢反斜槓(\)後緊跟的換行符的例項並刪除,轉換成一個邏輯行。
因為預處理表示式的長度為一邏輯行,所以這個功能為預處理做了準備工作。而一個邏輯行可能會比物理行多。預處理指
令從#開始,直到其後第一個換行符為止(即指令的長度限於一行程式碼,即前處理器表示式的長度為一邏輯行)。在預處理
開始之前,系統會刪除反斜線和換行符之間的組合。因此可以把指令擴充套件到幾個物理行,由這些物理行組成單個邏輯行
例如:
printf(“That's wond\
erful!\n”);
轉換成邏輯行
printf("That's wonderful!\n");
注意:這個換行符是指按下鍵盤迴車的換行符,而不是‘\n’這個換行符。
這個功能是為預處理作準備,因為預處理表示式的長度為邏輯行。
========================================================================================================== (意思就是將物理行轉變為邏輯行)
第三步:編譯器將文字劃分成預處理的 1語言符號序列 2空白字元 3註釋序列
注意:用一個空格字元來代替每一個註釋 例:
int/*這不是個空格*/ fox;
變成
int fox;
c實現可能還會選用單個空格代替每一個空白字元序列(不包括換行符)
========================================================================================================== (將文字劃分成語言序列,空白字元,註釋序列)
第四部:
程式進入預處理階
段。前處理器尋找可能存在的預處理指令。這些指令由一行開始處的#符號標識。
==========================================================================================================
16.2 明顯常量:#define
預處理指令用#號作為開頭。
ANSI標準允許#符號前有空格或者製表符,而且該標準還允許在#和指令的其餘部分之間有空格(意思就是#define的其他部分有空格)。
指令可以在原始檔的任何地方,指令定義的作用域從定義出現的位置開始知道檔案的結尾。~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~(作用域從定義開始到檔案結尾)
預處理指令從#開始,到其後第一個換行符為止。也就是說指令的長度限於一行程式碼。也就是前面所說的刪除反斜槓(\)和換行符的~~~~(從開始到其後的第一個換行符為止)
組合。因為可以把指令擴充套件到幾個物理行,由這些物理行組成單個邏輯行
每個#define行(邏輯行)由3部分組成:
1.指令#define自身
2.縮略語句(自己定義的名字),也稱為“巨集”這些巨集稱為“類物件巨集”,巨集名字中不允許空格遵循變數名規則。
3.(#define行的其餘部分)稱為“替換列表”或“主體”。
#define px printf(“x is %d\n”,x)
| 指令 ||巨集| | 替換列表或主體 |
在程式中發現的巨集都是用替換列表替代這些巨集。
巨集變成替換列表的過程稱為“巨集展開”。
預處理指令不進行計算的,只按照指令進行文字替換操作,一系列運算過程發生在編譯階段。
巨集定義可以包含其他巨集
const char * fmt = "X is %d.\n"; fmt可用作printf控制字串。
#define HAL‘Z’字元常量 #define HAL "Z"字串因為“Z\0”
一般而言,預處理髮現程式中的巨集後,會用等價替換列表進行代替巨集。如果該字串中還包含巨集則繼續替換
例外情況雙引號內的巨集。雙引號內的巨集其實不是巨集是字串的一部分。
什麼時候使用符號常量?(就是一般用#define這些都是"符號常量"\"明顯常量")
記憶值的能力、易更改性、可移植性,這些功能使得符號常量很有使用價值
=========================================================================================================
16.2.1 語言符號
系統把巨集的主體當作語言符號型別字串,而不是字元型字串。
c前處理器中的語言符號是巨集定義主體中的一個單獨的詞,用空白字元把這些詞分開。例如:
#defien FOUR 2*2 //有一個語言符號
#define SIX 2 * 3 // 這裡有3個語言符號2、*和3。因為2*3用空白字元分開 ~~~~~~~~~~~~~~~~~~~~~~~~~~(有幾個空格就有幾個語言符號)
處理主體中的多個空格時,字元型字串和語言符號型別字串處理方法不同:
#define EIGHT 4 * 8
主體解析為“字元型字串”時,預處理用4 * 8 替換EIGHT,額外的空格也當成替換的一部分~~~~~~~~( 用字元型字串空格也是主體一部分)
主體解析位“語言符號型別字串”時,預處理用空格分隔3個語言符號即4 * 8來替換EIGHT。~~~~~~~~~~~~~(用語言符號型別字串空格只是分隔主體語言符號的符號)
c編譯器處理語言符號要比前處理器的處理方式更加複雜,編譯器能理解c的規則不需要用空格分隔語言符號。
例如: 2*2 //3個語言符號。原因c編譯器認為每個2都是一個常量,而*是一個運算子。
=========================================================================================================
16.2.2 重定義常量
例如:把LIMIT 定義為20,後來在該檔案中又把LIMIT定義為25.這個過程稱為重定義常量
ANSI標準採用的第一種方法:只允許新定義與舊定義完全相同。
意味著相同定義主體具有相同順序的語言符號。
定義相同:
#define SIX 2 * 3
#define SIX 2 * 3
這兩者相同都是有三個語言符號,額外的空格不是主體的一部分。
#define SIX 2*3 則只有一個語言符號與前面的兩個定義不同
=========================================================================================================
16.3 在#define中使用引數
通過使用引數,可以建立外形和作用都與函式相似的“類函式巨集”。
引數也是用括號括起來。引數會出現在替換主體部分。
#define MEAN(x,y) ((x)+(y)/2)
| 巨集 | | 替換主體 |
類函式巨集定義的組成
#define SQUARE(X) X*X
// SQUARE為巨集識別符號, SQUARE(X)中的X為巨集的引數,X*X為替換列表。所以程式中出現SQUARE(X)的地方都用X*X代替。
使用這個巨集時,即可以使用x,也可以自由地使用其他符號。巨集定義中的x由程式呼叫的巨集中的符號代替。
例子:
SQUARE(X)X*X
z=SQUARE(2) z=4
SQUARE(X)X*X
z=SQUARE(z+2)X*X 是4+2*4+2=14而不是6*6=36因為預處理不進行計算,只進行文字替換。函式呼叫和巨集呼叫之間的重要差異。
函式呼叫是把引數的值“傳遞“給函式,而編譯前,巨集呼叫把引數語言符號傳遞給程式。
那如何把z+2輸出36?只須在替換主體加括號就行:
SQUARE(z+2) (X)*(X)=(4+2)*(4+2)=36
但是還未解決所有問題如下:
100/SQUARE(2)
=100/2*2
=(100/2)*2
=100 //理想值是25
解決這個問題就要
#define SQUARE(X) (X*X)
100/(2*2)=25
所以要解決以上問題就要如下才行:
#define SQUARE(x)((x)*(x))
注意還有一種特殊情況:
SQUARE(++X)
就變成++x*++x進行兩次增量運算5*6=30。
避免在巨集的引數中使用增量或減量
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結就是不要在巨集中使用增量或者減量還有要使用括號)
=========================================================================================================
16.3.1 利用巨集引數建立字串:#運算子
#define PSQR(x)printf("The square of x is %d.\n",((x)*(x)))
PSQR(8)輸出 The square of x is 64.
注意:引號內的X被看作普通文字,而不是可被替換的語言符號。
如果希望字串包含巨集引數。在類函式巨集的替換部分中,#符號用作一個預處理運算子,它可以把語言符號轉化為字串。
例如:如果x是一個巨集參量,那麼#x可以把引數名轉化為相應的字串。該過程稱為字串化
#define PSQR(X) printf(“The square of”#x“is %d。\n”,((x)*(x)))
PSQR(y): The square of y is 25
PSQR(2+4):The square of 2+4 is 36
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結在替換列表(主體)的字串中使用“#x”,會用巨集引數代替字串中的#x)
=========================================================================================================
16.3.2前處理器的粘合劑:##運算子
和#運算子一樣,
##運算子可以用於"類函式巨集"的替換部分,
##還可以用於“類物件巨集”的替換部分。
#define XNAME(n) x ## n
XNAME(4)//x4
#define PRINT_XN(n) printf(“x”#n“=%d\n”,x ## n);
int XNAME(1)=14//變為 int x1=14;
int XNAME(2)=20//變為int x2=20;
PRINT_XN(1);//變為printf(“X1=%d \n ”,x1);
PRINT_XN(2);//變為printf(“X2=%d \n”,x2);
注意:如何使用#組合字串,如何使用##把兩個語言符號組合為一個新的“識別符號”
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結使用##可以把兩個語言符號組合為一個新的識別符號)
=========================================================================================================
16.3.3 可變巨集:...和_ _VA_ARGS_ _
在巨集定義中引數列表的最後一個引數為省略號(...),就可以做到可變數量的引數。
這樣預定義巨集 _ _VA_ARGS_ _用在替換(替換列表(主體))部分中,以表明省略號代表什麼。
例:
#define PR(...) printf(_ _VA_ARGS_ _)
PR(“Howdy”);
PR(“weight=%d,shipping=$%.2f\n”,wt,sp);
第一次呼叫,展開引數為1個引數:
“Howdy”
第二次呼叫,展開引數為3個引數:
“weight=%d,shopping=$%.2f\n”,wt,sp
因此,展開後的程式碼為:
printf(“Howdy”);
printf(“weight=%d,shopping=$%.2f\n”,wt,sp);
#define PR(x,...) printf("Message " #x" :"_ _VA_ARGS_ _)
PR(1,"X=%g\n",x);//展開printf("MESSAGE" "1" ":" "x=%g\n",x);
PR(2,"X=%.2f,y=%.4f\n",x,y);//展開printf("MESSAGE" "2" ":" "X=%.2f,y=%.4f\n",x,y);
注意:省略號只能代表最後一個巨集引數
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結用...和_ _VA_ARGS_ _可變引數)
=========================================================================================================
16.4 巨集,還是函式
許多工既可以使用帶引數的巨集,也可以使用函式完成。那應該使用巨集還是函式?
1.有些巨集不注意就會產生一些奇怪的現象,某種情況下要比函式複雜。有些編譯器限制巨集只能定義成一行。
2.巨集與函式的選擇就是"時間和空間"的權衡。巨集產生內”聯程式碼”;也就是說,在程式中產生語句。如果:
用巨集20次,則會把20行程式碼插入程式中。如果使用函式20次,那麼程式中只有一份函式語句拷貝,因此節省了空間。
另外一方面,程式的控制必須轉移到函式中並隨後反悔呼叫程式,因此這比內“聯程式碼”花費的時間多。
總結:函式佔用空間少,時間長,巨集反之。
3.巨集的一個優點它不檢查其中的變數型別(因為巨集處理字元型字串,而不是實際值)。因此,對於int或folat
都可以使用巨集SQUARE(x)。
注意幾點:
1.巨集名字中間不能有空格,但是在替換字串中可以使用空格(引數列表允許使用空格)。
2.用圓括號括住“每”一個引數,並括住巨集的整體定義。
3.用大寫字母表示巨集函式名。大寫字母可以提醒程式設計師注意巨集可能產生副作用。
4.如果打算用巨集代替函式提高速度,那首先確定巨集是否引起重大差異。在程式中使用一次的巨集對程式執行時間不會產生明顯的改善。
巢狀裡使用巨集有助於程式執行速度。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結用巨集速度快但是佔用空間多,用函式反之)
=========================================================================================================
16.5 檔案包含:#include
前處理器發現#include後,就會尋找後跟的檔名並把檔案的內容包含到當前檔案中。
被包含檔案中的文字將替換原始碼檔案中的#include指令。就像你把被包含檔案中的全部內容鍵入到原始檔中
的這個特定位置一樣。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結使用標頭檔案等於將檔案全部內容鍵入原始檔中)
#include指令的兩種使用形式
1.#include<stdio.h> 檔名放在尖括號中 :尖括號告訴前處理器在一個或多個標準系統目錄中尋找檔案。
2.#include“mystuff.h” 檔名放在雙引號中 :雙引號告訴前處理器先在當前目錄(或檔名中指定的其它目錄)中尋找檔案,
然後在標準位置尋找檔案。
#include指令的一些例子
#include<stdio.h> 搜尋系統目錄
#include"hot.H" 搜尋當前工作目錄
#inlcude"/usr/biff/p.h" 搜尋/usr/biff目錄
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結指令的使用兩種方法尖括號和雙引號中)
對於系統標頭檔案,整合開發環境(IDE)具有標準搜尋路徑。許多整合開發環境提供選單選項用於指定使用尖括號時搜尋的其他路徑
對於UNIX,使用雙引號意味著“先”搜尋本地目錄,但具體搜尋那一個目錄依賴於編譯器。
為什麼要使用標頭檔案?
因為這些檔案包含編譯器所需的資訊。很多情況下,標頭檔案中的內容是編譯器產生最終程式碼所需的資訊,而不是加到最終程式碼裡的具體語句。
標頭檔案內容的最常見的形式包括:
1.明顯常量 ---例如,典型的stdio.h檔案定義EOF、NULL、BUFSIZE(標準I\O運衝區的大小)。
2.巨集函式 ---例如,getchar()通常被定義為getc(stdin),getc()通常被定義為較複雜的巨集,而
標頭檔案ctype.h 通常包含ctype函式的巨集定義。
3.函式宣告 ---例如,標頭檔案 string.h(在某些舊的系統中為 strings.h)包含字串函式系列的函式
宣告。在ANSI C中,宣告採用函式原型形式。
4.結構模版定義---標準I/O函式使用FILE結構,該結構包含檔案及檔案相關緩衝區的資訊。頭文
件 stdio.h 中存放FILE結構的宣告。
5.型別定義---可以使用指向 FILE 的指標作為引數呼叫標準 I/O 函式。通常,stdio.h 用#define 或
typedef 使得FILE代表指向FILE結構的指標。與之類似,size_t 和 time_t 型別也在標頭檔案中定義。
開發自己的標準標頭檔案以便在程式中使用。特別有價值
用標頭檔案宣告多個檔案共享的外部變數。例如開發共享某個變數的一系列函式,報告某種型別的狀態,這方法很有用處
使用具有檔案作用域、內部連線、const限定詞的變數或陣列。使用const可以避免值被意外改變。使用static後,每個額包含該標頭檔案
的檔案都獲得一份該常量的副本。因此,不存在這樣的問題,即需要在一個檔案中進行定義宣告,並且需要在其他檔案中進行引用
宣告。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結開發自己的標準標頭檔案有很高的價值)
=========================================================================================================
16.6其他指令
不同的工作環境準備不同的c程式或c庫包。
=========================================================================================================
16.6.1 #undef 指令
#undef指令取消前面#define的定義。
#define LIMIT 400
則指令:
#undef LIMIT
會取消該定義。現在可以重新定義LIMIT,以使它有一個新的值。即使開始沒有定義 LIMIT,取消LIMIT的定義也是合法的。
如果想使用一個特定的名字,但又不能確定前面是否已經使用該名字,為安全起見就可以取消該名字定義。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(#undef取值前面#define定義的巨集)
=========================================================================================================
16.6.2 已定義:c前處理器的觀點
與處理器在預處理指令中遇到'識別符號"時,要麼把識別符號當作已定義,要麼當作未定義的。
已定義表示又前處理器定義。如果識別符號是該檔案前面的#define指令建立的巨集名,並且沒有用#undef指令關閉該識別符號,
則識別符號是已定義的。
如果識別符號不是巨集,而是(例如)一個具有檔案作用域的c變數,那麼前處理器把識別符號當作未定義。
已定義巨集可以為類物件巨集(包括空巨集)或類函式巨集:
#define LIMIT 1000 // LIMIT 是已定義的
#define GOOD // GOOD 是已定義的
#define A(X)((-(X))*(X)) // A 是已定義的
int q; // q 不是一個巨集,因此是未定義的
#undef GOOD // GOOD 是未定義的
注意,#defien巨集的作用域從檔案中的定義點開始,直到用#undef指令取消巨集為止,或值到檔案尾為止。
注意:如果用標頭檔案引入巨集,那麼#define檔案中的位置就依賴於#include指令的位置。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結巨集是否定義)
=========================================================================================================
16.6.3 條件編譯
設定條件編譯,可以使用這些指令告訴編譯器:根據編譯時的條件接受或者忽略資訊(或程式碼)塊。
//////////////////////////////////////////////////////////////////////
一、#ifdef、#else、#endif指令
比較新的實現和ANSI標準支援的縮排格式。
#ifdef MAVIS
#include "horse.h" //如果已經用#define定義了MAVIS,則執行這裡的指令
#define STABLES 5
#else //如果沒有用#define定義MAVIS,則執行這裡的指令
#include "cow.h"
#define STABLES 15
#endif
舊的編譯器,必須要使用所有指令,或者至少使#指令符號左對齊。
#ifdef MAVIS
# include "horse.h" //如果已經用#define定義了MAVIS,則執行這裡的指令
# define STABLES 5
#else //如果沒有用#define定義MAVIS,則執行這裡的指令
# include "cow.h"
# define STABLES 15
#endif
#ifdef指令說明:前處理器已經定義了後面的識別符號,那麼執行所有指令並編譯c程式碼,直到下一個#else或#endif出現為止。
如果有#else指令,那麼,在未定義識別符號時會執行#else和#endif之間的所有程式碼。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結#ifdef條件編譯,前處理器是否已經定義後面的識別符號是就執行程式碼到#else或#endif為止)
#ifdef #else格式非常類似於c的 if else。主要差異就是前處理器不能識別標記程式碼塊的花括號{},
因此使用#else(如果需要)和#endif(必須存在)來標記指令塊,這些條件結構可以巢狀。
#include <stdio.h>
#define JUST_CHECKING
#define LIMIT 4
int main(void)
{
int i;
int total = 0;
for(i = 1;i <= LIMIT;i++)
{
total += 2*i*i + 1;
#ifdef JUST_CHECKING
printf("i=%d,running total = %d\n",i,total);
#endif
}
printf("Grand total = %d\n",total);
return 0;
}
編譯並執行該程式,產生下列輸出:
i=1,running total = 3
i=2,running total = 12
i=3,running total = 31
i=4,running total = 64
Grand total = 64
使用這種方法輔佐除錯程式。使編譯器包含那些用於列印輔佐除錯的中間值的程式程式碼。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>(總結#ifdef、#else和if、else的分別在於預處理不能識別程式碼塊的花括號。#endif必須要有)
//////////////////////////////////////////////////////////////////////
二、#ifndef指令
類似#ifndef指令,#ifndef指令可以與#else、#endif指令一起使用。#ifndef判斷後面的識別符號是否為未定義的,#ifndef的
反義詞為#ifdef。#ifndef通常用來定義此前未定義的常量。
/*arrays.h*/
#ifndef SIZE (檢測SIZE是否為未定義)
#define SIZE 100
#endif
#ifndef防止對巨集重複定義。當某檔案包含幾個標頭檔案,而且每個標頭檔案都可能定義相同的巨集時就使用#ifndef防止重複定義。
第一個標頭檔案的定義變成有效定義,其他標頭檔案中的定義則忽略。
例如:
#define SIZE 10
#include"arrarys.h"
因為SIZE已經定義所以arrarys.h的#ifndef不執行SIZE 100。
#ifndef指令通常用於防止多次包含同一檔案。也就是說,標頭檔案可採用類似下面幾行的設定:
/*things.h*/
#ifndef THINGS_H_
#define THINGS_H_
//標頭檔案的其餘部分//
#endif
假設多次包含了thigs.h這個檔案,第一次遇到該包含檔案時,THINGS_H_是未定義的,因此程式接著定義THINGS_H_,並處理檔案的其餘部分
前處理器下一次遇到該檔案時,THINGS_H_已經定義,因此,前處理器會跳過該檔案的其餘部分。
為什麼會多次包含同一檔案?
常見原因是許多包含檔案自身包含其他檔案,因此可能顯式地包含其他檔案已經包含的檔案。
//////////////////////////////////////////////////////////////////////
三、#if和#elif指令
#if指令更像常規的c中的if;#if後跟常量整數表示式。如果表示式為非零值,則表示式為真。在該表示式中可以使用c的關係運算子和
邏輯運算子。
#if SYS==1
#include “ibm.h”
#endif
可以使用#elif(有些早期的實現不支援#elif)指令擴充套件if-else序列。例如,可以這樣使用:
#if SYS==1
#include “ibmpc.h”
#elif SYS==2
#include "VAX.h"
#elif SYS==3
#include "mac.h"
#else
#include "general.h"
#endif
一種新的方法來判斷一個名字是否已經定義。
不需要用舊的:#ifdef VAX
新的方法:#if defined(VAX)
defined是一個前處理器運算子。如果引數已經用#define定義過,那麼defined反悔1;否則返回0。這種方法的優點是
在於它可以和#elif一起使用。
#if defined(IBMPC)
#include "ibmpc.h"
#elif defined(VAX)
#include "VAX.H"
#elif defined(MAC)
#include "mac.h"
#else
#include "general.h"
#endif
使用條件編譯可以使程式更易於移植。不同系統設定不同值幷包含不同檔案。
=========================================================================================================
16.6.4 預定義巨集
表16.3 預定義巨集
巨集 意 義
-DATE-- 進行預處理的日期("Mmm dd yyyy"形式的字串文字)
-FILE-- 代表當前原始碼檔名的字串文字
-LINE-- 代表當前原始碼檔案中的行號的整數常量
-STDC-- 設定為1 時,表示該實現遵循C標準
-STDC_HOSTED-- 為本機環境設定為1,否則設為 0
-STDC_VERSION-- 為C99 時設定為 199901L
-TIME-- 原始檔編譯時間,格式為"hh:mm:ss"
C99標準提供了一個名為__func__的預定義識別符號。__func__展開為一個代表函式名(該函式包含該識別符號)的字串。
該識別符號具有函式作用域,而巨集本質上具有檔案作用域。因而__func__是C語言的預定義識別符號,而非預定義巨集。
=========================================================================================================
16.6.5 #line 和#error
#line 指令用於重置 由_ _LINE_ _和 _ _FILE_ _巨集報告的行號和檔名。
#line 1000 //把當前行好重置為1000
#line 10 “cool.c” //把行號重置為10,檔名重置為cool.c
#error指令使前處理器發出一條錯誤訊息,訊息包含指令中的文字。在“編譯過程”中斷。
#if _ _STDC_VERSION_ _!=199901L
#error NOT c99
#endif
=========================================================================================================
16.6.6#pragma
在現代的編譯器中,可用命令列引數或IDE選單修改編譯器的某些設定。也可用#pragma將編譯器指令置於程式碼中。
例如:開發c99時,用c9x代表c99.使用"編譯指示"來啟用對c9x的支援:
#pragma c9x on
一般來說,每臺編譯器都有自己的編譯指示集
1.控制分配給自動變數的記憶體大小。
2.設定錯誤檢查的嚴格程度。
3.啟動非標準語言特徵。
C99 還提供了_Pragma前處理器運算子。_Pragma可將字串轉換成常規的"編譯指示"。例如:
_Pragma("nonstandardtreatmenttypeB on")
等價於下面的指令:
#pragma nonstandardtreatmenttypeB on
因為該運算子沒有使用#符號,所以可將它作為巨集展開的一部分:
#define PRAGMA(X)_Pragma(#X)
#define LIMRG(X)PRAGMA(STDC CX_LIMITED_RANGE X)
接著可以使用類似下面的程式碼:
LIMRG(ON)
{
LIMRG(NO)展開 PRAGMA(STDC CX_LIMITED_RANGE NO)//因為LIMARG的引數是X所以替換主體的語言符號X被替換成NO
PRAGMAX(x)展開 _PRAGMA("STDC CX_LIMITED_RANGE NO")
}
順便提一下,雖然下面的定義看上去可以正常執行,但實際並非如此:
#define LIMRG(X)_Pragma(STDC CX_LIMITED_RANGE #X)
問題在於上面的程式碼依賴於字串連線功能,但是,直到預處理過程完成後編譯器才連線字串。
_Pragma運算子完成字串析構(destringizing)工作;也就是說,將字串中的轉義序列轉換成它所
代表的字元。因而:
_Pragma("use_bool \"true \"false")
變成:
#pragma use_bool "true "false
=========================================================================================================
16.7行內函數
通用函式呼叫需要一定的時間開銷。執行呼叫的時間花費用於建立呼叫、傳遞引數、跳轉到函式程式碼段並返回。
使用類函式巨集可以減少執行時間。
C99提供另外一種方法:行內函數,把函式變為行內函數將建議編譯器儘可能快速地呼叫該函式。
使用行內函數可能簡化函式的呼叫機制,但也“可能不起作用”。
建立行內函數的方法的在函式宣告中使用函式說明符inline,首次使用行內函數前在檔案中對該函式進行定義:
#include<stdio.h>
inline void eatline()//行內函數的定義/原型
{
while(getchar()!='\n')
continue;
}
int main()
{
....
eatline();
....
}
編譯器看到內聯宣告後會用eatline()函式代替函式呼叫,等於你在此處鍵入了函式體的程式碼:
int main()
{
...
while(getchar()!='\n')
continue;
...
}
因為行內函數沒有預留給它的單獨程式碼塊,所以無法獲得行內函數地址(實際上,可以獲得地址,但是這樣會使編譯器產生非行內函數)。
行內函數不會在偵錯程式中顯示。
行內函數要比較短小,對於很長的函式,呼叫函式的時間少於執行函式主體的時間;此時使用行內函數不會節省多少時間。
行內函數的定義和對該函式的呼叫必須在同一檔案中。因為這樣,行內函數通常具有內部連結。因此,每個呼叫行內函數的檔案都要對
該函式進行定義。
達到這個目的的最簡單方法:在標頭檔案中定義行內函數,並在使用該函式的檔案中包含該標頭檔案。
與c++不同,c允許混合使用行內函數定義和外部函式定義。
c甚至允許在包含行內函數定義的檔案中放置外部函式宣告。
以下情況不宜使用內聯:
(1)如果函式體內的程式碼比較長,使用內聯將導致記憶體消耗代價較高。
(2)如果函式體內出現迴圈,那麼執行函式體內程式碼的時間要比函式呼叫的開銷大。
一個好的編譯器將會根據函式的定義體,自動地取消不值得的內聯(這進一步說明了inline 不應該出現在函式的宣告中)。
=========================================================================================================
16.8 c庫
如何使用庫:
16.8.1 訪問c庫
首先在多個不同的位置找到庫函式,第二不同系統使用不同的方法搜尋這些函式
一、自動訪問
許多系統上,你只需要編譯程式,一些常見的庫函式自動可用。
記住,應該宣告所使用的函式的型別,通常包含適當的標頭檔案即可做到這一點。描述庫函式,函式型別到使用者手冊中查詢。
二、檔案包含
如果函式定義為巨集,可以使用#include指令來包含擁有該定義的檔案。
三、庫包含
程式編譯或連線的某些階段,你可能需要指定庫選項。即使在自動檢查標準庫的系統上,也有可能有不常使用的函式庫。
必須使用編譯時選項來顯示地指示這些庫。注意要把這個過程與包含標頭檔案區分開來。
標頭檔案提供函式宣告或原型,而庫選項告訴系統到那尋找函式程式碼。
16.8.2 參考庫描述
庫文件可以在一下地方查詢:系統線上手冊、整合開發環境線上幫助、c提供商描述庫函式的使用者指南書籍、出版社發行c庫函式參考手冊
附錄B中提供了一個小結。
void型別的指標作為通用指標,指向不同型別的指標時可採用void指標
=========================================================================================================
16.9 數學庫
數學庫包含許多有用的數學函式。標頭檔案math.h
要回頭看
=========================================================================================================
16.10 通用工具庫
通用工具庫包含各種函式,有隨機數產生函式、搜尋和排序函式、轉換函式、和記憶體管理函式。
16.10.1 exit()和atexit()函式
main()返回時自動呼叫exit()函式。
指定執行exit()是呼叫特定函式,通過對退出時呼叫的函式提前進行註冊,atexit()函式提供這項註冊功能。
用函式指標作為引數。
一、使用atexit()
這個函式的引數是函式指標。只要把退出時要呼叫的函式的地址傳遞給atexit()進行“列表註冊”。這個列表中至少可放“32個”函式
呼叫exit()函式,按先進後出(先執行最後新增的函式)的順序執行這些函式。
atexit()註冊的函式不接受任何“引數”的VOID函式。
main()終止時會隱似地呼叫exit(),因此,即使未顯示地呼叫exit(),也會執行。
二、使用exit()
exit()執行了atexit()指定的函式後:
自身清理工作
重新整理輸出流
關閉所有開啟的流
並關閉通過呼叫標準i/o函式tmpfile()建立的臨時檔案。
然後exit()把控制返回給主機環境。
向主機環境報告終止狀態。
unix程式用0表示成功終止,用非零值表示失敗。
unix返回的程式碼並不適用於所有系統,因此ANSI C 定義了可移植的表示失敗的巨集EXIT_FAILURE,成功EXIT_SUCCESS。
exit()也接受用0代表成功。
在非遞迴的main()函式中使用exit()函式等價於使用關鍵字return。但是,在main()以為的函式中使用exit()也會終止程式。
=========================================================================================================
16.10.2 qsort()函式
快速排序(quick sort)法是排序演算法之一。它把陣列不斷分成更小的陣列,直到變成單個元素陣列。
qsort()函式對資料物件“陣列”進行排序。
函式原型 void qsort (void*base,size_t nmemb.size_t size,int(*compar)(const void *,const void *));
第一個引數為指向要排序的陣列頭部的指標。“ANSI C允許將任何資料型別的指標轉換為VOID型別指標”
因此第一個實際引數可以指向任何型別的陣列。
第二個引數為需要排序的專案數量。函式原型將該值轉換為size_t型別。size_t是由運算子sizeof返回。
第三個引數因為qsort()將第一個引數轉換為void指標,所以會“失去每一個陣列元素的大小資訊”。為補充該資訊
必須把資料物件的大小明確地告訴qsort()。例如:對double陣列排序,可使用sizeof(double)作為qsort()的第三個引數
第四個引數,qsort()還需要一個指向函式的指標,被指向的函式用於確定排序順序。
比較函式接受兩個引數,分別指向進行比較的兩個專案的指標。如果第一個專案的值大於第二個的值,比較函式返回正數;
如果兩個專案值相等,返回0;如果第一個專案值小於第二個專案值返回負數。qsort()根據給定的其他資訊計算出兩個指標的值,
然後把他們傳遞給該比較函式。
int(*compar)(const void *,const void *)指向函式的指標,返回int,並接受兩個引數,每一個引數指向const void型別指標。
這兩個指標指向需要比較的專案。
=========================================================================================================
一、使用qsort()
void qsort (void*base,size_t nmemb.size_t size,int(*compar)(const void *,const void *));
第一個引數為指向排序的陣列頭部指標,第一個實際引數指向任何型別的陣列。
第二個引數為排序的專案數量,即陣列元素的個數。
第三個引數為每個元素的大小。sizeof(double)。
第四個引數為對元素進行比較的函式的地址。
二、定義mycomp()
qsort()原型規定了比較函式的形式:
int(*compar)(const void *,const void *) 指向函式的指標。
函式名是該函式的指標
注意:qsort()的第一個引數指整個陣列,比較函式的兩個引數是指陣列中的兩個元素。
問題:要比較指標型值,需要對指標進行取值運算。因為要比較的值為double型別,所以應當對double型別的指標進行取值運算。
但是qsort要求void型指標。
解決這個問題的方法是:在函式內部宣告兩個正確型別的指標,並把他們初始化為傳遞進來的引數值。
為通用性,qsort()和比較函式使用void指標。因此,必須把陣列中的每個元素的大小明確告訴qsort;並在比較函式定義中,把指標
引數轉換為對具體應用型別正確的指標。
=========================================================================================================
16.11 診斷庫
由標頭檔案assert.h支援的診斷庫是設計用於輔佐除錯程式的小型庫。
由巨集assert()構成,引數接受整數表示式。如果表示式值為假(非零),assert()向標準錯誤流(stderr)寫一條錯誤訊息
並呼叫“abort()函式以終止程式”(stdlib.h中定義了abort()函式的原型)。
assert()巨集的作用為:標識出程式中某個條件應為“真”的關鍵位置,並在條件為假的時候用assert()語句終止該程式。
通常assert()引數為關係或邏輯表示式。
如果assert()終止程式,那麼它首先會顯式失敗的判斷、包含該判斷的檔名和行號。
assert(z>=0)並不是聲稱z>=0,而是聲稱z>=0這個條件沒有得到滿足。
用if語句也可以完成類似的工作:
if(z<0)
{
puts("z less than 0");
abort();
}
但是assert()方式有幾個好處。
1、自動識別檔案
2、自動識別發生問題的行號。
3、無需修改程式碼就能開啟或禁用assert()巨集的機制。
如果您認為已經排除了程式的漏洞,那麼可把巨集定義 “#define NDEBUG”放在assert.h包含語句所在位置前。編譯器將禁用檔案中所有
assert()語句。如果程式又出現問題,可以去除這個#define指令,就會重新啟用assert()
=========================================================================================================
16.12 string.h庫中的memcpy()和memmove()
不能把一個陣列的值賦予給另一陣列,因此,利用迴圈把陣列中的元素逐個複製到另一陣列。
例外情況:strcpy和strncpy函式複製字元陣列。
memcpy和memove函式為複製其他型別的陣列提供了類似的便利工具
void *memcpy(void *restrict s1,const void *restrict s2,size_t n);
void *menmove(void *s1,const void *s2,size_t n);
功能:兩個函式都是從s2指向的位置複製n位元組資料到s1指向的位置,且均返回s1的值。
差別:兩者的差別由關鍵詞 restrict造成,即memcpy()可以假定兩個“記憶體區域”之間沒有重疊。memmove()函式則不做這個假定,
因此,複製過程類似於首先將所有位元組複製到一個臨時緩衝區,然後再複製到最終目的地。
如果兩個區域存在重疊時使用memcpy()會怎樣呢?其行為是不可預知的,可能正常,可能失敗。
在不應該使用memcpy()時,編譯器不會禁止使用memcpy()。因此,使用memcpy()時,你必須確保沒有重疊區域。這是衝需要的任務的一部分。
c允許任何型別的指標賦值給void*型別指標。
接受各種型別指標導致函式無法知道要複製的資料型別。因此,這兩個函式使用第三個引數來指定要複製的位元組數。注意,對“陣列而已,位元組數一般不等於
元素的個數。”因此複製10個double值組成的陣列,那麼應使用10*sizeof(double)作為第三個引數,不應該使用10
=========================================================================================================
16.13 可變引數:stdarg.h
標頭檔案stdarg.h 為函式提供了可變巨集,可變個數的引數。不過使用方法稍微複雜一些。必須按如下步驟進行:
1.在函式原型中使用省略號。void f1(int n,...)
2.在函式定義中建立一個va_list型別的變數。 va_list
3.用巨集將該變數初始化為一個引數列表。va_start()
4.用巨集訪問這個引數列表。va_arg()
5.用巨集完成清理工作。va_end()
1、這類函式的原型應具有一個“參量列表”,參量列表中至少有一個“後跟省略號”的參量:
void f1(int n,...) 合法
int f2(const char *s,int k,...) 合法
char f3(char c1,...,char c2) //無效,省略號不是最後一個參量
double f3() 無效,沒有任何參量
最右邊的參量(省略號前)起著特殊作用;ANSI標準使用parmn表示該參量。f1 n為parmn,f2 k為parmn。傳遞給該參量的實際引數值將是“省略號部分代表的
引數個數”
f1(2,200,400)//額外的2個引數
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
2、在標頭檔案stdargs.h中宣告的va_list型別代表一種資料物件。在函式定義中宣告va_list,該資料物件用於存放參量列表中省略號部分代表的參量。
可變函式定義的起始部分應像下面這樣:
double sum(int lim,...)
{
va_list ap;
...
}
本例中,lim為參量parmN,由它來“指定可變引數列表中的引數個數”。
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
3、函式將使用stdargs.h中定義的巨集va_start()把引數列表複製到va_list 變數中。
巨集va_start()有兩個引數:va_list型別的變數和參量parmN。
va_start(ap,lim); 把ap初始化為引數列表
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
4、訪問訪問引數列表中的內容。
第一步 涉及va_arg()的使用。該巨集接受兩個引數:va_list型別變數、一個型別名。
第一次呼叫va_arg()時,返回列表的第一項,下一次呼叫返回第二項,以此類推。
型別引數指定返回值的型別。例如,如果引數列表中第一個引數為double型別,第二個為int型別,那麼可以使用下列語句:
double tic;
int toc;
。。。
tic=va_arg(ap,double);取得第一個引數
toc=va_arg(ap,int); 取得第二個引數
注意,實際引數的型別必須和說明的型別相匹配。如果第一個引數為10.0,那麼前面的tic部分的程式碼正常工作;
但是如果引數為10,程式碼就可能無法工作。不像賦值那樣進行型別自動轉換的。
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
5、應用巨集va_end()完成清理工作。例如,釋放動態分配的用於存放引數的記憶體。巨集接受一個va_list變數作為引數:
va_end(ap); //清理工作
此後只能用va_start()重新對ap初始化後,才能使用變數ap。
因為va_arg()不提供後退回先前引數的方法,所以儲存va_list變數的副本會是有用的。
c99為此專門新增了巨集va_copy().該巨集的兩個引數均為va_list型別變數,它將第二個引數複製到第一個引數中:
va_list ap;
va_list apcopy;
double
double tic;
int toc;
...
va_start(ap,lim); // 把 ap 初始化為引數列表
va_copy(apcopy,ap); // apcopy 是 ap的一個副本
tic = va_arg(ap,double); // 取得第一個引數
toc = va_arg(ap,int); // 取得第二個引數
可變函式比可變巨集更復雜,但是函式使用範圍更廣
相關文章
- C前處理器和C庫
- 【C++ Primer Plus】學習筆記--第10章 物件和類C++筆記物件
- C_Primer第2章 C語言概述C語言
- 筆記:《C++ Primer 中文版(第5版)》 第1章 開始筆記C++
- C++ Primer筆記C++筆記
- C++ primer 筆記C++筆記
- C++語言程式設計筆記 - 第12章 - 異常處理C++程式設計筆記
- c++ primer 第二章閱讀筆記C++筆記
- C++ primer Plus學習筆記(第二章)C++筆記
- C++ Primer 讀書筆記 - 第一章C++筆記
- c#常用的前處理器指令C#
- C語言細節 前處理器C語言
- 瞭解下C# 前處理器指令C#
- 《C++ Primer》筆記-#include,#ifndefC++筆記
- 開心檔之C++ 前處理器C++
- C++ Primer Plus第6版18個重點筆記C++筆記
- 《Effective C++》第1章 讓自己習慣C++-讀書筆記C++筆記
- 《C++ Primer Plus》16.1 string類 學習筆記C++筆記
- 《C++ Primer》讀書筆記(第一章 開始)C++筆記
- 《C++ Primer》學習筆記(五):迴圈、分支、跳轉和異常處理語句C++筆記
- 《C++ Primer》學習筆記(八):標準 IO 庫C++筆記
- 《Effective C++》第5章 實現-讀書筆記C++筆記
- 編譯warp,d語言寫的c/c++前處理器.編譯C++
- C++ Primer 第二章 學習筆記及習題答案C++筆記
- C/C++學習筆記八(斷言與異常處理)C++筆記
- C Primer Plus 第9章 函式 程式設計練習函式程式設計
- C與C++中的異常處理16 (轉)C++
- C++ Primer Plus 隨記(第八章)C++
- 《Effective C++》第8章 定製new和delete-讀書筆記C++delete筆記
- 【C++】 C++異常捕捉和處理C++
- 《C++ Primer》學習筆記(六):C++模組設計——函式C++筆記函式
- 拷貝控制c++primer13章C++
- c++ primer 第一章C++
- C++primer第一章C++
- c++primer 第一章C++
- C++ Primer plus 第12章類和動態記憶體分配複習題參考答案C++記憶體
- 《Effective C++》第3章 資源管理(2)-讀書筆記C++筆記
- 《Effective C++》第3章 資源管理(1)-讀書筆記C++筆記