原文
http://pl-learning-blog.logdown.com/posts/1049271-usually-terror-words-o-muhammad-c-ch11-reading-notes-unfinished
11.1 替換file的內容 (include) 第五章,接觸了include這個preprocessor的命令。這個命令是說在#include < file名稱 > 的情況下,從MinGW底下的include或/usr/include資料夾下找到同名檔案並置換掉這一行。比如說,我們寫了
這一行,那麼就會被換成以下東西:
# 28 "/usr/include/stdio.h" 2 3 4
# 1 "/usr/lib/gcc/x86_64-linux-gnu/5/include/stddef.h" 1 3 4
# 216 "/usr/lib/gcc/x86_64-linux-gnu/5/include/stddef.h" 3 4
# 216 "/usr/lib/gcc/x86_64-linux-gnu/5/include/stddef.h" 3 4
typedef long unsigned int size_t ;
# 34 "/usr/include/stdio.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/types.h" 1 3 4
# 27 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 28 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4
typedef unsigned char __u_char ;
typedef unsigned short int __u_short ;
typedef unsigned int __u_int ;
typedef unsigned long int __u_long ;
typedef signed char __int8_t ;
typedef unsigned char __uint8_t ;
typedef signed short int __int16_t ;
typedef unsigned short int __uint16_t ;
typedef signed int __int32_t ;
typedef unsigned int __uint32_t ;
typedef signed long int __int64_t ;
typedef unsigned long int __uint64_t ;
typedef long int __quad_t ;
typedef unsigned long int __u_quad_t ;
# 121 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/typesizes.h" 1 3 4
# 122 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4
typedef unsigned long int __dev_t ;
typedef unsigned int __uid_t ;
typedef unsigned int __gid_t ;
typedef unsigned long int __ino_t ;
typedef unsigned long int __ino64_t ;
typedef unsigned int __mode_t ;
typedef unsigned long int __nlink_t ;
typedef long int __off_t ;
typedef long int __off64_t ;
typedef int __pid_t ;
typedef struct { int __val [ 2 ]; } __fsid_t ;
typedef long int __clock_t ;
typedef unsigned long int __rlim_t ;
typedef unsigned long int __rlim64_t ;
typedef unsigned int __id_t ;
typedef long int __time_t ;
typedef unsigned int __useconds_t ;
typedef long int __suseconds_t ;
typedef int __daddr_t ;
typedef int __key_t ;
/*中間好多行*/
extern void funlockfile ( FILE * __stream ) __attribute__ (( __nothrow__ , __leaf__ ));
# 942 "/usr/include/stdio.h" 3 4
在稍微詳細說明一下include 命令吧。file名稱通常是指定一種叫head file的file。head file一般來說會有data type、常數、還有函數的定義。用<>括起來的東西,會在MinGW底下的/include或/usr/include資料夾這兩個標準C語言library的head file放置處,尋找相同名稱的檔案。
如果想自己寫很多函數的話,那麼就會想要一個專為那群函數所用的head file。在這種時候,用""
把自己寫的head file名稱括起來。這樣就會在跟source code一樣的目錄中尋找。首先來把範例程式build起來吧。
source code
main.c
#include <stdio.h>
#include "functions.h" //this head file the functions made by myself
int main (){
int num_1 ;
int num_2 ;
int answer ;
num_1 = 1 ;
num_2 = 2 ;
//execute function sum() which prototype is defined in functions.h
answer = sum ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
answer = sub ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
answer = mul ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
answer = div ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
return 0 ;
}
source code
function.h
int sum ( int , int );
int sub ( int , int );
int mul ( int , int );
int div ( int , int );
source code
function.c
int sum ( int a , int b ){
int return_value ;
return_value = a + b ;
return ( return_value );
}
int sub ( int a , int b ){
int return_value ;
return_value = a - b ;
return ( return_value );
}
int mul ( int a , int b ){
int return_value ;
return_value = a * b ;
return ( return_value );
}
int div ( int a , int b ){
int return_value ;
return_value = a / b ;
return ( return_value );
}
compile時要下三條指令
gcc -g -Wall -c -o main.o main.c
gcc -g -Wall -c -o functions.o functions.c
gcc -g -Wall -o include main.o functions.o
這樣會生出一個名為include的執行檔。如果是makefile的話,就會寫成這樣:
PROGRAM = include
OBJS = main.o functions.o
SRCS = $ ( OBJS: %.o= %.c)
CC = gcc
CFLAGS = -g -Wall
LDFLAGS =
$(PROGRAM) : $(OBJS)
$(CC) $(CFLAGS) $() -o $(PROGRAM) $(OBJS) $(LDLIBS)
因為這次是自己定義的head file實驗,所以試試了「分割compile」。來解釋一下compile那三行命令的意義吧。第一行是compile main.c,assemble後生成main.o; 第二行是compile functions.c,assemble後生成functions.o; 第三行是把main.o跟functions.o一起跟標準C library link起來,生成include這個執行檔。
雖然有點繞遠路,但讓我們來思考一下為何分割compile還有自己定義的head file會很重要吧。在第十章時自己寫函數的時候,說明了一定要宣告prototype。為了分割compile,我們把程式分成好幾個file去寫,在functions.c裡追加了新的函數; 這樣的話,就不需要在main.c中追加新的prototype宣告。
還有一點,就是main.c跟function.c的開發者是不同人的情況。假如我們一開始是把prototype宣告寫在main.c,而且寫function.c的人把函數的宣告稍微改了一下,但寫main.c的人不知道這件事所以沒改prototype。在這種情況下,就算compile跟link都沒異常的結束了,在執行時也可能會出現錯誤。
要避免這種事,寫function.c的人也要同時負責寫宣告prototype的functions.h,而寫main.c的人只要include functions.h就可以。這樣的話,如果function.c有變更的話compile時就會出現警告error,寫main.c的人注意到就可以修正用到函數的地方。
還有,一人在開發的時候,如果想讓別的程式也使用function.c的話,只要複製那個file(?)跟functions.h,讓別的程式去include,就可以使用function.c的函數。
因為這樣的理由,所以我們在把程式分成好幾個檔案去寫並分割compile時,我們也會分開去定義head file。標準C library的函數的定義都被整理在head file裡面,也是這個理由所造成的。比如說,如果沒有標準C library的話,想要利用到printf() 或fgets() 函數時,每一次都要自己在程式開頭宣告prototype,如下圖:
int printf ( const char * format , ...);
char * fgets ( char * str , int size , FILE * stream );
typedef long time_t ;
time_t time ( time_t * tloc );
char * ctime ( const time_t * clock );
每次都要寫這些,很麻煩吧,但是像下面這樣寫就輕鬆多了:
#include <stdio.h>
#include <time.h>
下面是include這個執行檔的執行畫面:
執行結果:
answer = 3
answer = -1
answer = 2
answer = 0
就和預想的一樣吧。那麼,我們輸入以下指令,來看看main.c中#include "function.h" 是怎麼置換的吧:
gcc -E main.c > main.txt
可以發現輸出的txt檔有以下訊息:
//前面省略
# 2 "main.c" 2
# 1 "functions.h" 1
# 1 "functions.h"
int sum ( int , int );
int sub ( int , int );
int mul ( int , int );
int div ( int , int );
# 3 "main.c" 2
// 以後省略
順代一提,include 這個命令除了head file外,還可以用在讀取的置換上。就當做是玩玩把下面的範例build看看吧。
source code
a.c
#include "a.5"
;
return 0 ;
}
a.1
a.2
a.3
a.4
a.5
#include "a.3"
#include "a.4"
( "hello, world \n " )
只要打
再打
就會執行經典的hello, world程式了
執行結果:
只要是文字文件什麼都可以include,雖然絕對不會像上面這樣用就是了,不過刻意這樣做可以讓大家有更強的「include 就是文字置換」的感覺。
11.2 一對一置換 (define) 還有一個跟include 類似的define 命令。不過跟include 置換檔案不同,define 是可以置換source code中的內容。用法如下:
第九章求出質數的peime_3.c的範例程式,之中的end = 100; 代表我們可以求到100為止的質數。可以用define 來改寫成下面這樣:
#define MAX 100
end = MAX ;
在preprocess的時候,MAX 這字串就會因為define 的命令通通換成100。趕快來實驗看看吧。
source code
define.c
#include <stdio.h>
#define INTEGER_NUM_1 100
#define FLOAT_NUM_1 3.14
#define STRING_1 "%s"
#define STRING_2 "hello, world\n"
int main (){
#define INTEGER_NUM_2 200
#define FLOAT_NUM_2 2.71
int a = INTEGER_NUM_1 ;
printf ( "%d %d \n " , a , INTEGER_NUM_2 );
float b = FLOAT_NUM_1 ;
float c = FLOAT_NUM_2 ;
printf ( "%f %f \n " , b , c );
printf ( STRING_1 , STRING_2 );
return 0 ;
}
執行結果:
100 200
3.140000 2.710000
hello, world
如何呢? 應該能了解就是單純的文字替換吧。
11.3 把define的東西無效化 (undef) 一但寫了define 的命令,就對宣告那一行之後的程式碼就一直都有效。如果因為什麼理由而想把define 無效化的話,可以用undef 。使用方法很簡單如下:
範例:
可以對剛剛的define.c實驗看看,在main() 之前寫上這一行,int a = INTEGER_NUM_1; 這一行就會出現錯誤。
11.4 有條件的compile (if) 這個define ,還有另一個使用方法; 而搭配這個使用方法的是if 。
根據值的內容做有條件的compile (if) 使用if 的話,會根據被define 的值讓source code本身分歧(這時還沒被compile)。非常難懂,所以來實驗一下下面的範例程式吧。
source code
define_2.c
#define TEST 1
#if TEST == 1
#include <stdio.h>
int main (){
printf ( "hello, world \n " );
return 0 ;
}
#else
#endif
我們先打下面這一行:
gcc -E define_2.c > define_2.txt
再來看看define_2.txt的內容,會發現最底下有main() 函數的內容。那麼,我們把#define TEST 1 改成#define TEST 0 實驗看看吧。我們會發現,define_2.txt的內容如下,幾乎是空的:
# 1 "define_2.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "define_2.c"
原因的話,在#define TEST 1 的時候,符合#if TEST == 1 的條件,所以會去用到#if ~#else 的範圍,但是當#define TEST 0 的時候因為和if 條件不符,所以會用到#else ~#endif 的範圍,也就是空的。
這次,我們把#define TEST 1 這一行給刪掉,分別下這兩行命令:
gcc -E -DTEST=1 define_2.c > define_2.txt
gcc -E -DTEST=0 define_2.c > define_2.txt
這兩行命令所產生的define_2.txt內容不同,-DTEST=1是當作有main() 函數,-DTEST=0則是沒有。像這樣,define 的內容也可以透過preprocessor的option指定。這個機能非常特別,C語言以外很少看到。比如說程式中分別寫了Windows用還有Linux用兩種,可以使用Makefile的option來對應兩方的環境。為了這種用途,define 在沒有寫該置換什麼的話,那麼就會自動補1,也就是說,
跟
是相同的。
調查是否已經定義過的條件compile 就算不用一個一個的寫if 後面的條件,用ifdef 這命令的話就跟#if TEST == 1 是同樣的效果,反過來也有跟#if TEST == 0 一樣意思的ifndef 可以用。範例如下:
#ifdef TEST
#include <stdio.h>
int main (){
printf ( "hello, world \n " );
return 0 ;
}
#else
#endif
#ifndef TEST
#else
#include <stdio.h>
int main (){
printf ( "hello, world \n " );
return 0 ;
}
#endif
(筆者注: 筆者不知道這樣寫有什麼意義,因為不管TEST是1是0都是執行一樣的程式,再說,#define跑到哪了呢?)
已經定義的define gcc的命令已經在內部define了許多的字串。比如說linux的gcc,就算gcc什麼option都不加,也在preprocess的時候#define linux 1 。也就是說,如果想寫一個可以跨平台(linux,windows等不同作業系統)的程式,可以像下面這樣寫:
#ifdef linux
//Linux專用處理
#else
//Linux以外的OS的專用處理
#endif
如果打了以下命令
就可以列出linux已經#define好的字串,筆者電腦顯示如下:
#define __SSP_STRONG__ 3
#define __DBL_MIN_EXP__ (-1021)
#define __UINT_LEAST16_MAX__ 0xffff
#define __ATOMIC_ACQUIRE 2
#define __FLT_MIN__ 1.17549435082228750797e-38F
#define __GCC_IEC_559_COMPLEX 2
#define __UINT_LEAST8_TYPE__ unsigned char
#define __SIZEOF_FLOAT80__ 16
#define __INTMAX_C(c) c ## L
#define __CHAR_BIT__ 8
#define __UINT8_MAX__ 0xff
#define __WINT_MAX__ 0xffffffffU
#define __ORDER_LITTLE_ENDIAN__ 1234
#define __SIZE_MAX__ 0xffffffffffffffffUL
#define __WCHAR_MAX__ 0x7fffffff
#define __GCC_HAVE_SYNC_COMPARE_AND_SWAP_1 1
#define __GCC_HAVE_SYNC_COMPARE_AND_SWAP_2 1
#define __GCC_HAVE_SYNC_COMPARE_AND_SWAP_4 1
#define __DBL_DENORM_MIN__ ((double)4.94065645841246544177e-324L)
#define __GCC_HAVE_SYNC_COMPARE_AND_SWAP_8 1
#define __GCC_ATOMIC_CHAR_LOCK_FREE 2
#define __GCC_IEC_559 2
#define __FLT_EVAL_METHOD__ 0
#define __unix__ 1
//中間好多行...
#define linux 1
#define __SSE2__ 1
#define __LDBL_MANT_DIG__ 64
#define __DBL_HAS_QUIET_NAN__ 1
#define __SIG_ATOMIC_MIN__ (-__SIG_ATOMIC_MAX__ - 1)
#define __code_model_small__ 1
#define __k8__ 1
#define __INTPTR_TYPE__ long int
#define __UINT16_TYPE__ short unsigned int
#define __WCHAR_TYPE__ int
#define __SIZEOF_FLOAT__ 4
#define __UINTPTR_MAX__ 0xffffffffffffffffUL
#define __DEC64_MIN_EXP__ (-382)
#define __INT_FAST64_MAX__ 0x7fffffffffffffffL
#define __GCC_ATOMIC_TEST_AND_SET_TRUEVAL 1
#define __FLT_DIG__ 6
#define __UINT_FAST64_TYPE__ long unsigned int
#define __INT_MAX__ 0x7fffffff
#define __amd64__ 1
#define __INT64_TYPE__ long int
#define __FLT_MAX_EXP__ 128
#define __ORDER_BIG_ENDIAN__ 4321
#define __DBL_MANT_DIG__ 53
#define __SIZEOF_FLOAT128__ 16
#define __INT_LEAST64_MAX__ 0x7fffffffffffffffL
#define __GCC_ATOMIC_CHAR16_T_LOCK_FREE 2
#define __DEC64_MIN__ 1E-383DD
//之後省略
如果想寫可以在複數環境下執行的程式,利用這命令,可以利用已經定義好的值來寫程式。
恐怖實驗:如果已經define的字串跟變數名相同
利用像是#define linux 1 等等的話,在寫需要在複數環境下執行的程式時非常便利,但是其中有陷阱。下面的範例程式,會發生什麼事呢?
source code
define_3.c
#include <stdio.h>
int main (){
char linux [ 16 ] = "For Linux!" ;
printf ( "%s \n " , linux );
return 0 ;
}
在compile時會出現以下的警告跟錯誤信息
define_3.c: In function ‘main’:
define_3.c:5:7: error: expected identifier or ‘(’ before numeric constant
char linux[16] = "For Linux!";
^
define_3.c:7:9: warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=]
printf("%s\n", linux);
^
這是因為系統已經#define linux 1 了,所以對我們的程式碼做置換,變成了
char 1 [ 16 ] = "For Linux!" ;
所以就產生奇怪的結果。
在gcc裡定義的大部分#define都有__(兩個underscore),所以不太發生這種事,但還是注意一下。除了linux 可能會不小心跟#define 重複到,其他還有i386 ,unix 也是跟linux 同樣的情形。
11.5 像函數的macro函數 define 是可以把接收的參數置換的命令,也就是說,可以用它做出類似函數的東西,這被稱為「macro函數」。
macro函數的寫法 比如說,想寫出跟function.c的sum() 一樣的macro函數,就要寫成下面這樣:
先來看看範例程式的執行吧。
source code
define_4.c
#include <stdio.h>
#define SUM(a, b) a + b
#define SUB(a, b) a - b
#define MUL(a, b) a * b
#define DIV(a, b) a / b
int main (){
int num_1 ;
int num_2 ;
int answer ;
num_1 = 1 ;
num_2 = 2 ;
answer = SUM ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
answer = SUB ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
answer = MUL ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
answer = DIV ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
return 0 ;
}
執行結果:
answer = 3
answer = -1
answer = 2
answer = 0
跟預想一樣的結果吧。
macro函數不是函數! 這個macro函數,有著「函數名(參數)」這樣的型態,怎麼看都是函數。但是,實際上函數跟macro函數是完全不同的東西。要說的話,因為macro函數跟其它我們看過的define 一樣,不過是單純的文字置換而已。
我們把define_4.c的範例程式編譯時gcc加上一些option觀察一下吧。
gcc -E define_4.c > define_4.txt
可以看到原本程式碼中的
answer = SUM ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
answer = SUB ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
answer = MUL ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
answer = DIV ( num_1 , num_2 );
printf ( "answer = %d \n " , answer );
在define_4.txt中的最後,變成了下面這樣子:
answer = num_1 + num_2 ;
printf ( "answer = %d \n " , answer );
answer = num_1 - num_2 ;
printf ( "answer = %d \n " , answer );
answer = num_1 * num_2 ;
printf ( "answer = %d \n " , answer );
answer = num_1 / num_2 ;
printf ( "answer = %d \n " , answer );
return 0 ;
可以發現SUM(num_1, num_2); 的行蹤完全消失,變成了num_1 + num_2; ,其他式子也是如此。
恐怖實驗: macro函數的陷阱
因為macro函數的便利,在C語言裡常常被利用。雖然之前的SUM() 不怎麼實用所以不太可能會為它去寫macro函數; 不過因為是文字置換,不需要在意data type,所以我們會把兩個變數a, b的交換寫成像下面的macro函數:
#define SWAP(a, b) a^=b; b^=a; a^=b;
但是,macro函數是有陷阱的。首先來執行看看可以正確動作的的範例程式吧。
source code:
define_5.c
#include <stdio.h>
#define SWAP(a, b) a^=b; b^=a; a^=b;
int main (){
int num_1 ;
int num_2 ;
num_1 = 1 ;
num_2 = 2 ;
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
SWAP ( num_1 , num_2 );
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
return 0 ;
}
執行結果
num_1 = 1, num_2 = 2
num_1 = 2, num_2 = 1
變數num_1 跟變數num_2 確實交換了。接下來,我們使用if ,使得只有在num_1 等於0的時候進行交換,先故意不用block吧,就像下面:
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
if ( num_1 == 0 )
SWAP ( num_1 , num_2 );
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
執行結果
num_1 = 1, num_2 = 2
num_1 = 2, num_2 = 3
執行結果很奇怪吧。原本範例程式中的範例程式變數num_1 是1才對,macro函數SWAP() 應該沒有實行,而且還置換了奇怪的數字。我們用gcc -E
來調查原因吧。輸入:
gcc -E define_5.c > define_5.txt
在define_5.txt中,可以發現SWAP() 的函數被置換變成下面這樣子:
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
if ( num_1 == 0 )
num_1 ^= num_2 ; num_2 ^= num_1 ; num_1 ^= num_2 ;;
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
因為沒有{},所以if的分歧處理就只有num_1^=num_2這一句; 要用{}解釋上面的code的話,就是下面這樣:
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
if ( num_1 == 0 ){
num_1 ^= num_2 ;
}
num_2 ^= num_1 ;
num_1 ^= num_2 ;;
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
所以才會出現奇怪的結果。原本在if 的下面block的話就沒問題了,共同作業的人也不一定會在if 加上block,而且就算弄錯也不會出現警告error,我們希望可以讓SWAP() 函數本身可以應對不加block的問題。利用置換文字的特性,寫法如下:
#define SWAP(a, b) {a^=b; b^=a; a^=b;}
這樣寫的話,就可以依照我們的想法執行了。
/*if不加block*/
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
if ( num_1 == 0 )
{ a ^= b ; b ^= a ; a ^= b ;};
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
/*if加block*/
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
if ( num_1 == 0 ){
{ a ^= b ; b ^= a ; a ^= b ;};
}
printf ( "num_1 = %d, num_2 = %d \n " , num_1 , num_2 );
順代一提,macro函數也可以寫出分歧這種複雜的處理。在這情況下,可能會因為使用的制御文而造成compile error,可以使用像下列的不重複的do while 寫法。
#define SWAP(a, b) do{ \
// 這裡可以寫if \
( a ) ^= ( b ); \
( b ) ^= ( a ); \
( a ) ^= ( b ); \
} while ( 0 )
這裡會寫成(a)還有(b)這樣把變數用括號括起來,是為了不要發生優先順位的問題。\是代表換行後define 依然繼續的意思。還有,同常我們呼叫完函數後一定會加個;不過while(0)後面是不需要加的。
這些不是現在立刻要用到的知識,不過記起來也沒什麼損失。
11.6 結語 在這章,我們學習了C語言特有的preprocessor命令。
雖然是其他大部分的語言都沒有的機能所以不好懂,但是在gcc -E的命令下看到是怎樣置換的,應該會明白到這意外的簡單。
stdio.h等等的head file中有許多被define 定義的常數,而為了讓UNIX系的OS可以像collection一樣利用list或quene的head file,其機能也幾乎都是用macro來實現的(為了使用時可以不受dat type影響)。
就算是說學習了preprocessor命令,能做的事也不會戲劇性的增加,不過C語言到處都會用到,所以記起來吧。