中文字幕理论片,69视频免费在线观看,亚洲成人app,国产1级毛片,刘涛最大尺度戏视频,欧美亚洲美女视频,2021韩国美女仙女屋vip视频

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
C語言預處理命令詳解
一  前言
預處理(或稱預編譯)是指在進行編譯的第一遍掃描(詞法掃描和語法分析)之前所作的工作。預處理指令指示在程序正式編譯前就由編譯器進行的操作,可放在程序中任何位置。
預處理是C語言的一個重要功能,它由預處理程序負責完成。當對一個源文件進行編譯時,系統(tǒng)將自動引用預處理程序?qū)υ闯绦蛑械念A處理部分作處理,處理完畢自動進入對源程序的編譯。
C語言提供多種預處理功能,主要處理#開始的預編譯指令,如宏定義(#define)、文件包含(#include)、條件編譯(#ifdef)等。合理使用預處理功能編寫的程序便于閱讀、修改、移植和調(diào)試,也有利于模塊化程序設計。
本文參考諸多資料,詳細介紹常用的幾種預處理功能。因成文較早,資料來源大多已不可考,敬請諒解。
二  宏定義
C語言源程序中允許用一個標識符來表示一個字符串,稱為“宏”。被定義為宏的標識符稱為“宏名”。在編譯預處理時,對程序中所有出現(xiàn)的宏名,都用宏定義中的字符串去代換,這稱為宏替換或宏展開。
宏定義是由源程序中的宏定義命令完成的。宏替換是由預處理程序自動完成的。
在C語言中,宏定義分為有參數(shù)和無參數(shù)兩種。下面分別討論這兩種宏的定義和調(diào)用。
2.1 無參宏定義
無參宏的宏名后不帶參數(shù)。其定義的一般形式為:
#define  標識符  字符串
其中,“#”表示這是一條預處理命令(以#開頭的均為預處理命令)。“define”為宏定義命令。“標識符”為符號常量,即宏名。“字符串”可以是常數(shù)、表達式、格式串等。
宏定義用宏名來表示一個字符串,在宏展開時又以該字符串取代宏名。這只是一種簡單的文本替換,預處理程序?qū)λ蛔魅魏螜z查。如有錯誤,只能在編譯已被宏展開后的源程序時發(fā)現(xiàn)。
注意理解宏替換中“換”的概念,即在對相關命令或語句的含義和功能作具體分析之前就要進行文本替換。
【例1】定義常量:
1 #define MAX_TIME 1000
若在程序里面寫if(time < MAX_TIME){.........},則編譯器在處理該代碼前會將MAX_TIME替換為1000。
注意,這種情況下使用const定義常量可能更好,如const int MAX_TIME = 1000;。因為const常量有數(shù)據(jù)類型,而宏常量沒有數(shù)據(jù)類型。編譯器可以對前者進行類型安全檢查,而對后者只進行簡單的字符文本替換,沒有類型安全檢查,并且在字符替換時可能會產(chǎn)生意料不到的錯誤。
【例2】反例:
1 #define pint (int*)2 pint pa, pb;
本意是定義pa和pb均為int型指針,但實際上變成int* pa,pb;。pa是int型指針,而pb是int型變量。本例中可用typedef來代替define,這樣pa和pb就都是int型指針了。因為宏定義只是簡單的字符串代換,在預處理階段完成,而typedef是在編譯時處理的,它不是作簡單的代換,而是對類型說明符重新命名,被命名的標識符具有類型定義說明的功能。typedef的具體說明見附錄6.4。
無參宏注意事項:
宏名一般用大寫字母表示,以便于與變量區(qū)別。
宏定義末尾不必加分號,否則連分號一并替換。
宏定義可以嵌套。
可用#undef命令終止宏定義的作用域。
使用宏可提高程序通用性和易讀性,減少不一致性,減少輸入錯誤和便于修改。如數(shù)組大小常用宏定義。
預處理是在編譯之前的處理,而編譯工作的任務之一就是語法檢查,預處理不做語法檢查。
宏定義寫在函數(shù)的花括號外邊,作用域為其后的程序,通常在文件的最開頭。
字符串" "中永遠不包含宏,否則該宏名當字符串處理。
宏定義不分配內(nèi)存,變量定義分配內(nèi)存。
2.2 帶參宏定義
C語言允許宏帶有參數(shù)。在宏定義中的參數(shù)稱為形式參數(shù),在宏調(diào)用中的參數(shù)稱為實際參數(shù)。
對帶參數(shù)的宏,在調(diào)用中,不僅要宏展開,而且要用實參去代換形參。
帶參宏定義的一般形式為:
#define  宏名(形參表)  字符串
在字符串中含有各個形參。
帶參宏調(diào)用的一般形式為:
宏名(實參表);
在宏定義中的形參是標識符,而宏調(diào)用中的實參可以是表達式。
在帶參宏定義中,形參不分配內(nèi)存單元,因此不必作類型定義。而宏調(diào)用中的實參有具體的值,要用它們?nèi)ゴ鷵Q形參,因此必須作類型說明,這點與函數(shù)不同。函數(shù)中形參和實參是兩個不同的量,各有自己的作用域,調(diào)用時要把實參值賦予形參,進行“值傳遞”。而在帶參宏中只是符號代換,不存在值傳遞問題。
【例3】
1 #define INC(x) x+1 //宏定義2 y = INC(5); //宏調(diào)用
在宏調(diào)用時,用實參5去代替形參x,經(jīng)預處理宏展開后的語句為y=5+1。
【例4】反例:
1 #define SQ(r) r*r
上述這種實參為表達式的宏定義,在一般使用時沒有問題;但遇到如area=SQ(a+b);時就會出現(xiàn)問題,宏展開后變?yōu)閍rea=a+b*a+b;,顯然違背本意。
相比之下,函數(shù)調(diào)用時會先把實參表達式的值(a+b)求出來再賦予形參r;而宏替換對實參表達式不作計算直接地照原樣代換。因此在宏定義中,字符串內(nèi)的形參通常要用括號括起來以避免出錯。
進一步地,考慮到運算符優(yōu)先級和結合性,遇到area=10/SQ(a+b);時即使形參加括號仍會出錯。因此,還應在宏定義中的整個字符串外加括號,
綜上,正確的宏定義是#define SQ(r) ((r)*(r)),即宏定義時建議所有的層次都要加括號。
【例5】帶參函數(shù)和帶參宏的區(qū)別:
1 #define SQUARE(x) ((x)*(x)) 2 int Square(int x){ 3 return (x * x); //未考慮溢出保護 4 } 5 6 int main(void){ 7 int i = 1; 8 while(i <= 5) 9 printf("i = %d, Square = %d\n", i, Square(i++));10 11 int j = 1;12 while(j <= 5)13 printf("j = %d, SQUARE = %d\n", j, SQUARE(j++));14 15 return 0;16 }
執(zhí)行后輸出如下:
1 i = 2, Square = 12 i = 3, Square = 43 i = 4, Square = 94 i = 5, Square = 165 i = 6, Square = 256 j = 3, SQUARE = 17 j = 5, SQUARE = 98 j = 7, SQUARE = 25
本例意在說明,把同一表達式用函數(shù)處理與用宏處理兩者的結果有可能是不同的。
調(diào)用Square函數(shù)時,把實參i值傳給形參x后自增1,再輸出函數(shù)值。因此循環(huán)5次,輸出1~5的平方值。
調(diào)用SQUARE宏時,SQUARE(j++)被代換為((j++)*(j++))。在第一次循環(huán)時,表達式中j初值為1,兩者相乘的結果為1。相乘后j自增兩次變?yōu)?,因此表達式中第二次相乘時結果為3*3=9。同理,第三次相乘時結果為5*5=25,并在此次循環(huán)后j值變?yōu)?,不再滿足循環(huán)條件,停止循環(huán)。
從以上分析可以看出函數(shù)調(diào)用和宏調(diào)用二者在形式上相似,在本質(zhì)上是完全不同的。
帶參宏注意事項:
宏名和形參表的括號間不能有空格。
宏替換只作替換,不做計算,不做表達式求解。
函數(shù)調(diào)用在編譯后程序運行時進行,并且分配內(nèi)存。宏替換在編譯前進行,不分配內(nèi)存。
宏的啞實結合不存在類型,也沒有類型轉換。
函數(shù)只有一個返回值,利用宏則可以設法得到多個值。
宏展開使源程序變長,函數(shù)調(diào)用不會。
宏展開不占用運行時間,只占編譯時間,函數(shù)調(diào)用占運行時間(分配內(nèi)存、保留現(xiàn)場、值傳遞、返回值)。
為防止無限制遞歸展開,當宏調(diào)用自身時,不再繼續(xù)展開。如:#define TEST(x)  (x + TEST(x))被展開為1 + TEST(1)。
2.3 實踐用例
包括基本用法(及技巧)和特殊用法(#和##等)。
#define可以定義多條語句,以替代多行的代碼,但應注意替換后的形式,避免出錯。宏定義在換行時要加上一個反斜杠”\”,而且反斜杠后面直接回車,不能有空格。
2.3.1 基本用法
1. 定義常量:
1 #define PI 3.1415926
將程序中出現(xiàn)的PI全部換成3.1415926。
2. 定義表達式:
1 #define M (y*y+3*y)
編碼時所有的表達式(y*y+3*y)都可由M代替,而編譯時先由預處理程序進行宏替換,即用(y*y+3*y)表達式去置換所有的宏名M,然后再進行編譯。
注意,在宏定義中表達式(y*y+3*y)兩邊的括號不能少,否則可能會發(fā)生錯誤。如s=3*M+4*M在預處理時經(jīng)宏展開變?yōu)閟=3*(y*y+3*y)+4*(y*y+3*y),如果宏定義時不加括號就展開為s=3*y*y+3*y+4*y*y+3*y,顯然不符合原意。因此在作宏定義時必須十分注意。應保證在宏替換之后不發(fā)生錯誤。
3. 得到指定地址上的一個字節(jié)或字:
1 #define MEM_B(x) (*((char *)(x)))2 #define MEM_W(x) (*((short *)(x)))
4. 求最大值和最小值:
1 #define MAX(x, y) (((x) > (y)) ? (x) : (y))2 #define MIN(x, y) (((x) < (y)) ? (x) : (y))
以后使用MAX (x,y)或MIN (x,y),就可分別得到x和y中較大或較小的數(shù)。
但這種方法存在弊病,例如執(zhí)行MAX(x++, y)時,x++被執(zhí)行多少次取決于x和y的大??;當宏參數(shù)為函數(shù)也會存在類似的風險。所以建議用內(nèi)聯(lián)函數(shù)而不是這種方法提高速度。不過,雖然存在這樣的弊病,但宏定義非常靈活,因為x和y可以是各種數(shù)據(jù)類型。
以下給出MAX宏的兩個安全版本(源自linux/kernel.h):
1 #define MAX_S(x, y) ({ 2 const typeof(x) _x = (x); 3 const typeof(y) _y = (y); 4 (void)(&_x == &_y); 5 _x > _y ? _x : _y; }) 6 7 #define TMAX_S(type, x, y) ({ 8 type _x = (x); 9 type _y = (y); 10 _x > _y ? _x: _y; })
Gcc編譯器將包含在圓括號和大括號雙層括號內(nèi)的復合語句看作是一個表達式,它可出現(xiàn)在任何允許表達式的地方;復合語句中可聲明局部變量,判斷循環(huán)條件等復雜處理。而表達式的最后一條語句必須是一個表達式,它的計算結果作為返回值。MAX_S和TMAX_S宏內(nèi)就定義局部變量以消除參數(shù)副作用。
MAX_S宏內(nèi)(void)(&_x == &_y)語句用于檢查參數(shù)類型一致性。當參數(shù)x和y類型不同時,會產(chǎn)生” comparison of distinct pointer types lacks a cast”的編譯警告。
注意,MAX_S和TMAX_S宏雖可避免參數(shù)副作用,但會增加內(nèi)存開銷并降低執(zhí)行效率。若使用者能保證宏參數(shù)不存在副作用,則可選用普通定義(即MAX宏)。
5. 得到一個成員在結構體中的偏移量(lint 545告警表示"&用法值得懷疑",此處抑制該警告):
1 #define FPOS(type, field) 2 /*lint -e545 */ ((int)&((type *)0)-> field) /*lint +e545 */
6. 得到一個結構體中某成員所占用的字節(jié)數(shù):
1 #define FSIZ(type, field) sizeof(((type *)0)->field)
7. 按照LSB格式把兩個字節(jié)轉化為一個字(word):
1 #define FLIPW(arr) ((((short)(arr)[0]) * 256) + (arr)[1])
8. 按照LSB格式把一個字(word)轉化為兩個字節(jié):
1 #define FLOPW(arr, val) 2 (arr)[0] = ((val) / 256); 3 (arr)[1] = ((val) & 0xFF)
9. 得到一個變量的地址:
1 #define B_PTR(var) ((char *)(void *)&(var))2 #define W_PTR(var) ((short *)(void *)&(var))
10. 得到一個字(word)的高位和低位字節(jié):
1 #define WORD_LO(x) ((char)((short)(x)&0xFF))2 #define WORD_HI(x) ((char)((short)(x)>>0x8))
11. 返回一個比X大的最接近的8的倍數(shù):
1 #define RND8(x) ((((x) + 7) / 8) * 8)
12. 將一個字母轉換為大寫或小寫:
1 #define UPCASE(c) (((c) >= 'a' && (c) <= 'z') ? ((c) + 'A' - 'a') : (c))2 #define LOCASE(c) (((c) >= 'A' && (c) <= 'Z') ? ((c) + 'a' - 'A') : (c))
注意,UPCASE和LOCASE宏僅適用于ASCII編碼(依賴于碼字順序和連續(xù)性),而不適用于EBCDIC編碼。
13. 判斷字符是不是10進值的數(shù)字:
1 #define ISDEC(c) ((c) >= '0' && (c) <= '9')
14. 判斷字符是不是16進值的數(shù)字:
1 #define ISHEX(c) (((c) >= '0' && (c) <= '9') ||2 ((c) >= 'A' && (c) <= 'F') ||3 ((c) >= 'a' && (c) <= 'f'))
15. 防止溢出的一個方法:
1 #define INC_SAT(val) (val = ((val)+1 > (val)) ? (val)+1 : (val))
16. 返回數(shù)組元素的個數(shù):
1 #define ARR_SIZE(arr) (sizeof((arr)) / sizeof((arr[0])))
17. 對于IO空間映射在存儲空間的結構,輸入輸出處理:
1 #define INP(port) (*((volatile char *)(port)))2 #define INPW(port) (*((volatile short *)(port)))3 #define INPDW(port) (*((volatile int *)(port)))4 #define OUTP(port, val) (*((volatile char *)(port)) = ((char)(val)))5 #define OUTPW(port, val) (*((volatile short *)(port)) = ((short)(val)))6 #define OUTPDW(port, val) (*((volatile int *)(port)) = ((int)(val)))
18. 使用一些宏跟蹤調(diào)試:
ANSI標準說明了五個預定義的宏名(注意雙下劃線),即:__LINE__、__FILE __、__DATE__、__TIME__、__STDC __。
若編譯器未遵循ANSI標準,則可能僅支持以上宏名中的幾個,或根本不支持。此外,編譯程序可能還提供其它預定義的宏名(如__FUCTION__)。
__DATE__宏指令含有形式為月/日/年的串,表示源文件被翻譯到代碼時的日期;源代碼翻譯到目標代碼的時間作為串包含在__TIME__中。串形式為時:分:秒。
如果實現(xiàn)是標準的,則宏__STDC__含有十進制常量1。如果它含有任何其它數(shù),則實現(xiàn)是非標準的。
可以借助上面的宏來定義調(diào)試宏,輸出數(shù)據(jù)信息和所在文件所在行。如下所示:
1 #define MSG(msg, date) printf(msg);printf(“[%d][%d][%s]”,date,__LINE__,__FILE__)
19. 用do{…}while(0)語句包含多語句防止錯誤:
1 #define DO(a, b) do{2 a+b;3 a++;4 }while(0)
20. 實現(xiàn)類似“重載”功能
C語言中沒有swap函數(shù),而且不支持重載,也沒有模板概念,所以對于每種數(shù)據(jù)類型都要寫出相應的swap函數(shù),如:
1 IntSwap(int *, int *); 2 LongSwap(long *, long *); 3 StringSwap(char *, char *);
可采用宏定義TSWAP (t,x,y)或SWAP(x, y)交換兩個整型或浮點參數(shù):
1 #define TSWAP(type, x, y) do{ 2 type _y = y; 3 y = x; 4 x = _y; 5 }while(0) 6 #define SWAP(x, y) do{ 7 x = x + y; 8 y = x - y; 9 x = x - y; 10 }while(0)11 12 int main(void){13 int a = 10, b = 5;14 TSWAP(int, a, b);15 printf(“a=%d, b=%d\n”, a, b);16 return 0;17 }
21. 1年中有多少秒(忽略閏年問題) :
1 #define SECONDS_PER_YEAR (60UL * 60 * 24 * 365)
該表達式將使一個16位機的整型數(shù)溢出,因此用長整型符號L告訴編譯器該常數(shù)為長整型數(shù)。
注意,不可定義為#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL,否則將產(chǎn)生(31536000)UL而非31536000UL,這會導致編譯報錯。
以下幾種寫法也正確:
1 #define SECONDS_PER_YEAR 60 * 60 * 24 * 365UL2 #define SECONDS_PER_YEAR (60UL * 60UL * 24UL * 365UL)3 #define SECONDS_PER_YEAR ((unsigned long)(60 * 60 * 24 * 365))
22. 取消宏定義:
#define [MacroName] [MacroValue]       //定義宏
#undef [MacroName]                                 //取消宏
宏定義必須寫在函數(shù)外,其作用域為宏定義起到源程序結束。如要終止其作用域可使用#undef命令:
1 #define PI 3.141592 int main(void){3 //……4 }5 #undef PI6 int func(void){7 //……8 }
表示PI只在main函數(shù)中有效,在func1中無效。
2.3.2 特殊用法
主要涉及C語言宏里#和##的用法,以及可變參數(shù)宏。
2.3.2.1 字符串化操作符#
在C語言的宏中,#的功能是將其后面的宏參數(shù)進行字符串化操作(Stringfication),簡單說就是將宏定義中的傳入?yún)?shù)名轉換成用一對雙引號括起來參數(shù)名字符串。#只能用于有傳入?yún)?shù)的宏定義中,且必須置于宏定義體中的參數(shù)名前。例如:
1 #define EXAMPLE(instr) printf("The input string is:\t%s\n", #instr)2 #define EXAMPLE1(instr) #instr
當使用該宏定義時,example(abc)在編譯時將會展開成printf("the input string is:\t%s\n","abc");string str=example1(abc)將會展成string str="abc"。
又如下面代碼中的宏:
1 define WARN_IF(exp) do{ 2 if(exp) 3 fprintf(stderr, "Warning: " #exp"\n"); 4 }while(0)
則代碼WARN_IF (divider == 0)會被替換為:
1 do{2 if(divider == 0)3 fprintf(stderr, "Warning" "divider == 0" "\n");4 }while(0)
這樣,每次divider(除數(shù))為0時便會在標準錯誤流上輸出一個提示信息。
注意#宏對空格的處理:
忽略傳入?yún)?shù)名前面和后面的空格。如str= example1(   abc )會被擴展成 str="abc"。
當傳入?yún)?shù)名間存在空格時,編譯器會自動連接各個子字符串,每個子字符串間只以一個空格連接。如str= example1( abc    def)會被擴展成 str="abc def"。
2.3.2.2 符號連接操作符##
##稱為連接符(concatenator或token-pasting),用來將兩個Token連接為一個Token。注意這里連接的對象是Token就行,而不一定是宏的變量。例如:
1 #define PASTER(n) printf( "token" #n " = %d", token##n)2 int token9 = 9;
則運行PASTER(9)后輸出結果為token9 = 9。
又如要做一個菜單項命令名和函數(shù)指針組成的結構體數(shù)組,并希望在函數(shù)名和菜單項命令名之間有直觀的、名字上的關系。那么下面的代碼就非常實用:
1 struct command{2 char * name;3 void (*function)(void);4 };5 #define COMMAND(NAME) {NAME, NAME##_command}
然后,就可用一些預先定義好的命令來方便地初始化一個command結構的數(shù)組:
1 struct command commands[] = {2 COMMAND(quit),3 COMMAND(help),4 //...5 }
COMMAND宏在此充當一個代碼生成器的作用,這樣可在一定程度上減少代碼密度,間接地也可減少不留心所造成的錯誤。
還可以用n個##符號連接n+1個Token,這個特性是#符號所不具備的。如:
1 #define LINK_MULTIPLE(a, b, c, d) a##_##b##_##c##_##d2 typedef struct record_type LINK_MULTIPLE(name, company, position, salary);
這里這個語句將展開為typedef struct record_type name_company_position_salary。
注意:
當用##連接形參時,##前后的空格可有可無。
連接后的實際參數(shù)名,必須為實際存在的參數(shù)名或是編譯器已知的宏定義。
凡是宏定義里有用'#'或'##'的地方,宏參數(shù)是不會再展開。如:
1 #define STR(s) #s2 #define CONS(a,b) int(a##e##b)
則printf("int max: %s\n", STR(INT_MAX))會被展開為printf("int max: %s\n", "INT_MAX")。其中,變量INT_MAX為int型的最大值,其值定義在<climits.h>中。printf("%s\n", CONS(A, A))會被展開為printf("%s\n", int(AeA)),從而編譯報錯。
INT_MAX和A都不會再被展開,多加一層中間轉換宏即可解決這個問題。加這層宏是為了把所有宏的參數(shù)在這層里全部展開,那么在轉換宏里的那一個宏(如_STR)就能得到正確的宏參數(shù)。
1 #define _STR(s) #s 2 #define STR(s) _STR(s) // 轉換宏3 #define _CONS(a,b) int(a##e##b)4 #define CONS(a,b) _CONS(a,b) // 轉換宏
則printf("int max: %s\n", STR(INT_MAX))輸出為int max: 0x7fffffff;而printf("%d\n", CONS(A, A))輸出為200。
這種分層展開的技術稱為宏的Argument Prescan,參見附錄6.1。
【'#'和'##'的一些應用特例】
1. 合并匿名變量名
1 #define ___ANONYMOUS1(type, var, line) type var##line2 #define __ANONYMOUS0(type, line) ___ANONYMOUS1(type, _anonymous, line)3 #define ANONYMOUS(type) __ANONYMOUS0(type, __LINE__)
例:ANONYMOUS(static int)即static int _anonymous70,70表示該行行號。
第一層:ANONYMOUS(static int)  →  __ANONYMOUS0(static int, __LINE__)
第二層:                                  →  ___ANONYMOUS1(static int, _anonymous, 70)
第三層:                                  →  static int _anonymous70
即每次只能解開當前層的宏,所以__LINE__在第二層才能被解開。
2. 填充結構
1 #define FILL(a) {a, #a} 2 3 enum IDD{OPEN, CLOSE}; 4 typedef struct{ 5 IDD id; 6 const char * msg; 7 }T_MSG;
則T_MSG tMsg[ ] = {FILL(OPEN), FILL(CLOSE)}相當于:
1 T_MSG tMsg[] = {{OPEN, "OPEN"},2 {CLOSE, "CLOSE"}};
3. 記錄文件名
1 #define _GET_FILE_NAME(f) #f2 #define GET_FILE_NAME(f) _GET_FILE_NAME(f)3 static char FILE_NAME[] = GET_FILE_NAME(__FILE__);
4. 得到一個數(shù)值類型所對應的字符串緩沖大小
1 #define _TYPE_BUF_SIZE(type) sizeof #type2 #define TYPE_BUF_SIZE(type) _TYPE_BUF_SIZE(type)3 char buf[TYPE_BUF_SIZE(INT_MAX)];4 //--> char buf[_TYPE_BUF_SIZE(0x7fffffff)];5 //--> char buf[sizeof "0x7fffffff"];
這里相當于:char  buf[11];
2.3.2.3 字符化操作符@#
@#稱為字符化操作符(charizing),只能用于有傳入?yún)?shù)的宏定義中,且必須置于宏定義體的參數(shù)名前。作用是將傳入的單字符參數(shù)名轉換成字符,以一對單引號括起來。
1 #define makechar(x) #@x2 a = makechar(b);
展開后變成a= 'b'。
2.3.2.4 可變參數(shù)宏
...在C語言宏中稱為Variadic Macro,即變參宏。C99編譯器標準允許定義可變參數(shù)宏(Macros with a Variable Number of Arguments),這樣就可以使用擁有可變參數(shù)表的宏。
可變參數(shù)宏的一般形式為:
#define  DBGMSG(format, ...)  fprintf (stderr, format, __VA_ARGS__)
省略號代表一個可以變化的參數(shù)表,變參必須作為參數(shù)表的最右一項出現(xiàn)。使用保留名__VA_ARGS__ 把參數(shù)傳遞給宏。在調(diào)用宏時,省略號被表示成零個或多個符號(包括里面的逗號),一直到到右括號結束為止。當被調(diào)用時,在宏體(macro body)中,那些符號序列集合將代替里面的__VA_ARGS__標識符。當宏的調(diào)用展開時,實際的參數(shù)就傳遞給fprintf ()。
注意:可變參數(shù)宏不被ANSI/ISO C++所正式支持。因此,應當檢查編譯器是否支持這項技術。
在標準C里,不能省略可變參數(shù),但卻可以給它傳遞一個空的參數(shù),這會導致編譯出錯。因為宏展開后,里面的字符串后面會有個多余的逗號。為解決這個問題,GNU CPP中做了如下擴展定義:
#define  DBGMSG(format, ...)  fprintf (stderr, format, ##__VA_ARGS__)
若可變參數(shù)被忽略或為空,##操作將使編譯器刪除它前面多余的逗號(否則會編譯出錯)。若宏調(diào)用時提供了可變參數(shù),編譯器會把這些可變參數(shù)放到逗號的后面。
同時,GCC還支持顯式地命名變參為args,如同其它參數(shù)一樣。如下格式的宏擴展:
#define  DBGMSG(format, args...)  fprintf (stderr, format, ##args)
這樣寫可讀性更強,并且更容易進行描述。
用GCC和C99的可變參數(shù)宏, 可以更方便地打印調(diào)試信息,如:
1 #ifdef DEBUG2 #define DBGPRINT(format, args...) 3 fprintf(stderr, format, ##args)4 #else5 #define DBGPRINT(format, args...)6 #endif
這樣定義之后,代碼中就可以用dbgprint了,例如dbgprint ("aaa [%s]", __FILE__)。
結合第4節(jié)的“條件編譯”功能,可以構造出如下調(diào)試打印宏:
1 #ifdef LOG_TEST_DEBUG 2 /* OMCI調(diào)試日志宏 */ 3 //以10進制格式日志整型變量 4 #define PRINT_DEC(x) printf(#x" = %d\n", x) 5 #define PRINT_DEC2(x,y) printf(#x" = %d\n", y) 6 //以16進制格式日志整型變量 7 #define PRINT_HEX(x) printf(#x" = 0x%-X\n", x) 8 #define PRINT_HEX2(x,y) printf(#x" = 0x%-X\n", y) 9 //以字符串格式日志字符串變量10 #define PRINT_STR(x) printf(#x" = %s\n", x)11 #define PRINT_STR2(x,y) printf(#x" = %s\n", y)12 13 //日志提示信息14 #define PROMPT(info) printf("%s\n", info)15 16 //調(diào)試定位信息打印宏17 #define TP printf("%-4u - [%s<%s>]\n", __LINE__, __FILE__, __FUNCTION__);18 19 //調(diào)試跟蹤宏,在待日志信息前附加日志文件名、行數(shù)、函數(shù)名等信息20 #define TRACE(fmt, args...)21 do{22 printf("[%s(%d)<%s>]", __FILE__, __LINE__, __FUNCTION__);23 printf((fmt), ##args);24 }while(0)25 #else26 #define PRINT_DEC(x)27 #define PRINT_DEC2(x,y)28 29 #define PRINT_HEX(x)30 #define PRINT_HEX2(x,y)31 32 #define PRINT_STR(x)33 #define PRINT_STR2(x,y)34 35 #define PROMPT(info)36 37 #define TP38 39 #define TRACE(fmt, args...)40 #endif
三  文件包含
文件包含命令行的一般形式為:
#include"文件名"
通常,該文件是后綴名為"h"或"hpp"的頭文件。文件包含命令把指定頭文件插入該命令行位置取代該命令行,從而把指定的文件和當前的源程序文件連成一個源文件。
在程序設計中,文件包含是很有用的。一個大程序可以分為多個模塊,由多個程序員分別編程。有些公用的符號常量或宏定義等可單獨組成一個文件,在其它文件的開頭用包含命令包含該文件即可使用。這樣,可避免在每個文件開頭都去書寫那些公用量,從而節(jié)省時間,并減少出錯。
對文件包含命令要說明以下幾點:
包含命令中的文件名可用雙引號括起來,也可用尖括號括起來,如#include "common.h"和#include<math.h>。但這兩種形式是有區(qū)別的:使用尖括號表示在包含文件目錄中去查找(包含目錄是由用戶在設置環(huán)境時設置的include目錄),而不在當前源文件目錄去查找;使用雙引號則表示首先在當前源文件目錄中查找,若未找到才到包含目錄中去查找。用戶編程時可根據(jù)自己文件所在的目錄來選擇某一種命令形式。
一個include命令只能指定一個被包含文件,若有多個文件要包含,則需用多個include命令。
文件包含允許嵌套,即在一個被包含的文件中又可以包含另一個文件。
四  條件編譯
一般情況下,源程序中所有的行都參加編譯。但有時希望對其中一部分內(nèi)容只在滿足一定條件才進行編譯,也就是對一部分內(nèi)容指定編譯的條件,這就是“條件編譯”。有時,希望當滿足某條件時對一組語句進行編譯,而當條件不滿足時則編譯另一組語句。
條件編譯功能可按不同的條件去編譯不同的程序部分,從而產(chǎn)生不同的目標代碼文件。這對于程序的移植和調(diào)試是很有用的。
條件編譯有三種形式,下面分別介紹。
4.1 #ifdef形式
#ifdef  標識符  (或#if defined標識符)
程序段1
#else
程序段2
#endif
如果標識符已被#define命令定義過,則對程序段1進行編譯;否則對程序段2進行編譯。如果沒有程序段2(它為空),#else可以沒有,即可以寫為:
#ifdef  標識符  (或#if defined標識符)
程序段
#endif
這里的“程序段”可以是語句組,也可以是命令行。這種條件編譯可以提高C源程序的通用性。
【例6】
1 #define NUM OK 2 int main(void){ 3 struct stu{ 4 int num; 5 char *name; 6 char sex; 7 float score; 8 }*ps; 9 ps=(struct stu*)malloc(sizeof(struct stu));10 ps->num = 102;11 ps->name = "Zhang ping";12 ps->sex = 'M';13 ps->score = 62.5;14 #ifdef NUM15 printf("Number=%d\nScore=%f\n", ps->num, ps->score); /*--Execute--*/16 #else17 printf("Name=%s\nSex=%c\n", ps->name, ps->sex);18 #endif19 free(ps);
20 return 0;21 }
由于在程序中插入了條件編譯預處理命令,因此要根據(jù)NUM是否被定義過來決定編譯哪個printf語句。而程序首行已對NUM作過宏定義,因此應對第一個printf語句作編譯,故運行結果是輸出了學號和成績。
程序首行定義NUM為字符串“OK”,其實可為任何字符串,甚至不給出任何字符串,即#define NUM也具有同樣的意義。只有取消程序首行宏定義才會去編譯第二個printf語句。
4.2 #ifndef形式
#ifndef  標識符
程序段1
#else
程序段2
#endif
如果標識符未被#define命令定義過,則對程序段1進行編譯,否則對程序段2進行編譯。這與#ifdef形式的功能正相反。
“#ifndef  標識符”也可寫為“#if  !(defined 標識符)”。
4.3 #if形式
#if 常量表達式
程序段1
#else
程序段2
#endif
如果常量表達式的值為真(非0),則對程序段1 進行編譯,否則對程序段2進行編譯。因此可使程序在不同條件下,完成不同的功能。
【例7】輸入一行字母字符,根據(jù)需要設置條件編譯,使之能將字母全改為大寫或小寫字母輸出。
1 #define CAPITAL_LETTER 1 2 int main(void){ 3 char szOrig[] = "C Language", cChar; 4 int dwIdx = 0; 5 while((cChar = szOrig[dwIdx++]) != '\0') 6 { 7 #if CAPITAL_LETTER 8 if((cChar >= 'a') && (cChar <= 'z')) cChar = cChar - 0x20; 9 #else10 if((cChar >= 'A') && (cChar <= 'Z')) cChar = cChar + 0x20;11 #endif12 printf("%c", cChar);13 }14 return 0;15 }
在程序第一行定義宏CAPITAL_LETTER為1,因此在條件編譯時常量表達式CAPITAL_LETTER的值為真(非零),故運行后使小寫字母變成大寫(C LANGUAGE)。
本例的條件編譯當然也可以用if條件語句來實現(xiàn)。但是用條件語句將會對整個源程序進行編譯,生成的目標代碼程序很長;而采用條件編譯,則根據(jù)條件只編譯其中的程序段1或程序段2,生成的目標程序較短。如果條件編譯的程序段很長,采用條件編譯的方法是十分必要的。
4.4 實踐用例
1. 屏蔽跨平臺差異
在大規(guī)模開發(fā)過程中,特別是跨平臺和系統(tǒng)的軟件里,可以在編譯時通過條件編譯設置編譯環(huán)境。
例如,有一個數(shù)據(jù)類型,在Windows平臺中應使用long類型表示,而在其他平臺應使用float表示。這樣往往需要對源程序作必要的修改,這就降低了程序的通用性。可以用以下的條件編譯:
1 #ifdef WINDOWS2 #define MYTYPE long3 #else4 #define MYTYPE float5 #endif
如果在Windows上編譯程序,則可以在程序的開始加上#define WINDOWS,這樣就編譯命令行    #define MYTYPE long;如果在這組條件編譯命令前曾出現(xiàn)命令行#define WINDOWS 0,則預編譯后程序中的MYTYPE都用float代替。這樣,源程序可以不必作任何修改就可以用于不同類型的計算機系統(tǒng)。
2. 包含程序功能模塊
例如,在程序首部定義#ifdef FLV:
1 #ifdef FLV2 include"fastleave.c"3 #endif
如果不許向別的用戶提供該功能,則在編譯之前將首部的FLV加一下劃線即可。
3. 開關調(diào)試信息
調(diào)試程序時,常常希望輸出一些所需的信息以便追蹤程序的運行。而在調(diào)試完成后不再輸出這些信息??梢栽谠闯绦蛑胁迦胍韵碌臈l件編譯段:
1 #ifdef DEBUG2 printf("device_open(%p)\n", file);3 #endif
如果在它的前面有以下命令行#define DEBUG,則在程序運行時輸出file指針的值,以便調(diào)試分析。調(diào)試完成后只需將這個define命令行刪除即可,這時所有使用DEBUG作標識符的條件編譯段中的printf語句不起作用,即起到“開關”一樣統(tǒng)一控制的作用。
4. 避開硬件的限制。
有時一些具體應用環(huán)境的硬件不同,但限于條件本地缺乏這種設備,可繞過硬件直接寫出預期結果:
1 #ifndef TEST2 i = dial(); //程序調(diào)試運行時繞過此語句3 #else4 i = 0;5 #endif
調(diào)試通過后,再屏蔽TEST的定義并重新編譯即可。
5. 防止頭文件重復包含
頭文件(.h)可以被頭文件或C文件包含。由于頭文件包含可以嵌套,C文件就有可能多次包含同一個頭文件;或者不同的C文件都包含同一個頭文件,編譯時就可能出現(xiàn)重復包含(重復定義)的問題。
在頭文件中為了避免重復調(diào)用(如兩個頭文件互相包含對方),常采用這樣的結構:
1 #ifndef <標識符>2 #define <標識符>3 //真正的內(nèi)容,如函數(shù)聲明之類4 #endif
<標識符>可以自由命名,但一般形如__HEADER_H,且每個頭文件標識都應該是唯一的。
事實上,不管頭文件會不會被多個文件引用,都要加上條件編譯開關來避免重復包含。
6. 在#ifndef中定義變量出現(xiàn)的問題(一般不定義在#ifndef中)。
1 #ifndef PRECMPL2 #define PRECMPL3 int var;4 #endif
其中有個變量定義,在VC中鏈接時會出現(xiàn)變量var重復定義的錯誤,而在C中成功編譯。
(1) 當?shù)谝粋€使用這個頭文件的.cpp文件生成.obj時,var在里面定義;當另一個使用該頭文件的.cpp文件再次(單獨)生成.obj時,var又被定義;然后兩個obj被第三個包含該頭文件.cpp連接在一起,會出現(xiàn)重復定義。
(2) 把源程序文件擴展名改成.c后,VC按照C語言語法對源程序進行編譯。在C語言中,遇到多個int var則自動認為其中一個是定義,其他的是聲明。
(3) C語言和C++語言連接結果不同,可能是在進行編譯時,C++語言將全局變量默認為強符號,所以連接出錯。C語言則依照是否初始化進行強弱的判斷的(僅供參考)。
解決方法:
(1) 把源程序文件擴展名改成.c。
(2) .h中只聲明 extern int var;,在.cpp中定義(推薦)
1 //<x.h>2 #ifndef __X_H3 #define __X_H4 extern int var;5 #endif6 <x.c>7 int var = 0;
綜上,變量一般不要定義在.h文件中。
五  小結
預處理功能是C語言特有的功能,它是在對源程序正式編譯前由預處理程序完成的。程序員在程序中用預處理命令來調(diào)用這些功能。
宏定義是用一個標識符來表示一個字符串,這個字符串可以是常量、變量或表達式。在宏調(diào)用中將用該字符串代換宏名。
宏定義可以帶有參數(shù),宏調(diào)用時是以實參代換形參。而不是“值傳遞”。
為了避免宏替換時發(fā)生錯誤,宏定義中的字符串應加括號,字符串中出現(xiàn)的形式參數(shù)兩邊也應加括號。
文件包含是預處理的一個重要功能,它可用來把多個源文件連接成一個源文件進行編譯,結果將生成一個目標文件。
條件編譯允許只編譯源程序中滿足條件的程序段,使生成的目標程序較短,從而減少了內(nèi)存的開銷并提高了程序的效率。
使用預處理功能便于程序的修改、閱讀、移植和調(diào)試,也便于實現(xiàn)模塊化程序設計。
六 附錄
6.1 Argument Prescan
(摘自http://gcc.gnu.org/onlinedocs/cpp/Argument-Prescan.html)
Macro arguments are completely macro-expanded before they are substituted into a macro body, unless they are stringified or pasted with other tokens. After substitution, the entire macro body, including the substituted arguments, is scanned again for macros to be expanded. The result is that the arguments are scanned twice to expand macro calls in them.
宏參數(shù)被完全展開后再替換入宏體,但當宏參數(shù)被字符串化(#)或與其它子串連接(##)時不予展開。在替換之后,再次掃描整個宏體(包括已替換宏參數(shù))以進一步展開宏。結果是宏參數(shù)被掃描兩次以展開參數(shù)所(嵌套)調(diào)用的宏。
若帶參數(shù)宏定義中的參數(shù)稱為形參,調(diào)用宏時的實際參數(shù)稱為實參,則宏的展開可用以下三步來簡單描述(該步驟與gcc摘錄稍有不同,但更易操作):
1) 用實參替換形參,將實參代入宏文本中;
2) 若實參也是宏,則展開實參;
3) 繼續(xù)處理宏替換后的宏文本,若宏文本也包含宏則繼續(xù)展開,否則完成展開。
其中第一步將實參代入宏文本后,若實參前遇到字符“#”或“##”,即使實參是宏也不再展開實參,而當作文本處理。
上述展開步驟示例如下:
1 #define TO_STRING(x) _TO_STRING(x)2 #define _TO_STRING(x) #x3 #define FOO 4
則_TO_STRING(FOO)展開為”FOO”;TO_STRING(FOO)展開為_TO_STRING(4),進而展開為”4”。相當于借助_TO_STRING這樣的中間宏,先展開宏參數(shù),延遲其字符化。
6.2 宏的其他注意事項
1. 避免在無作用域限定(未用{}括起)的宏內(nèi)定義數(shù)組、結構、字符串等變量,否則函數(shù)中對宏的多次引用會導致實際局部變量空間成倍放大。
2. 按照宏的功能、模塊進行集中定義。即在一處將常量數(shù)值定義為宏,其他地方通過引用該宏,生成自己模塊的宏。嚴禁相同含義的常量數(shù)值,在不同地方定義為不同的宏,即使數(shù)值相同也不允許(維護修改后極易遺漏,造成代碼隱患)。
3. 用只讀變量適當替代(類似功能的)宏,例如將#define PIE 3.14改為const float PIE = 3.14。這樣做的好處如下:
1) 預編譯時用宏定義值替換宏名,編譯時報錯不易理解;
2) 跟蹤調(diào)試時顯示宏值,而不是宏名;
3) 宏沒有類型,不能做類型檢查,不安全;
4) 宏自身沒有作用域;
5) 只讀變量和宏的效率同樣高。
注意,C語言中只讀變量不可用于數(shù)組大小、變量(包括數(shù)組元素)初始化值以及case表達式。
4. 用inline函數(shù)代替(類似功能的)宏函數(shù)。好處如下:
1) 宏函數(shù)在預編譯時處理,編譯出錯信息不易理解;
2) 宏函數(shù)本身無法單步跟蹤調(diào)試,因此也不要在宏內(nèi)調(diào)用函數(shù)。但某些編譯器(為了調(diào)試需要)可將inline函數(shù)轉成普通函數(shù);
3) 宏函數(shù)的入?yún)]有類型,不安全;
5) inline函數(shù)會在目標代碼中展開,和宏的效率一樣高;
注意,某些宏函數(shù)用法獨特,不能用inline函數(shù)取代。當不想或不能指明參數(shù)類型時,宏函數(shù)更合適。
5. 不帶參數(shù)的宏函數(shù)也要定義成函數(shù)形式,如#define HELLO( )  printf(“Hello.”)。
括號會暗示閱讀代碼者該宏是一個函數(shù)。
6. 帶參宏內(nèi)定義變量時,應注意避免內(nèi)外部變量重名的問題:
1 typedef struct{ 2 int d; 3 }T_TEST; 4 T_TEST gtTest = {0}; 5 #define ASSIGN1(_d) do{ 6 T_TEST t = {0}; 7 t.d = _d; 8 gtTest = t; 9 }while(0)10 11 #define ASSIGN2(_p) do{ 12 int _d; 13 _d = 5; 14 (_p) = _d; 15 }while(0)
若宏參數(shù)名或宏內(nèi)變量名不加前綴下劃線,則ASSIGN1(c)將會導致編譯報錯(t.d被替換為t.c),ASSIGN2(d)會因宏內(nèi)作用域而導致外部的變量d值保持不變(而非改為5)。
7. 不要用宏改寫語言。例如:
1 #define FOREVER for ( ; ; )2 #define BEGIN {3 #define END }
C語言有完善且眾所周知的語法。試圖將其改變成類似于其他語言的形式,會使讀者混淆,難于理解。
6.3 do{…}while(0)妙用
1. 函數(shù)中使用do{…}while(0)可替代goto語句。例如:
goto寫法
替代寫法
bOk = func1();
if(!bOk) goto errorhandle;
bOk = func2();
if(!bOk) goto errorhandle;
bOk = func3();
if(!bOk) goto errorhandle;
//… …
//執(zhí)行成功,釋放資源并返回
delete p;
p = NULL;
return true;
errorhandle:
delete p;
p = NULL;
return false;
do{
//執(zhí)行并進行錯誤處理
bOk = func1();
if(!bOk) break;
bOk = func2();
if(!bOk) break;
bOk = func3();
if(!bOk) break;
// ..........
}while(0);
//釋放資源
delete p;
p = NULL;
return bOk;
2. 宏定義中使用do{…}while(0)的原因及好處:
1) 避免空的宏定義產(chǎn)生warning,如#define DUMMY( ) do{}while(0)。
2) 存在一個獨立的代碼塊,可進行變量定義,實現(xiàn)比較復雜的邏輯處理。
注意,該代碼塊內(nèi)(即{…}內(nèi))定義的變量其作用域僅限于該塊。此外,為避免宏的實參與其內(nèi)部定義的變量同名而造成覆蓋,最好在變量名前加上_(基于如下編程慣例:除非是庫,否則不應定義以_開始的變量)。
3) 若宏出現(xiàn)在判斷語句之后,可保證作為一個整體來實現(xiàn)。
如#define SAFE_DELETE(p)  delete p; p = NULL;,則以下代碼
1 if(NULL != p)2 SAFE_DELETE(p)3 else4 DUMMY( );
就有兩個問題:
a) 因為if分支后有兩條語句,else分支沒有對應的if,編譯失敗;
b) 假設沒有else,則SAFE_DELETE中第二條語句無論if判斷是否成立均會執(zhí)行,這顯然違背程序設計的原始目的。
那么,為了避免這兩個問題,將宏直接用{}括起來是否可以?如:
#define SAFE_DELETE(p)  {delete p; p = NULL;}
的確,上述問題不復存在。但C/C++編程中,在每條語句后加分號是約定俗成的習慣,此時以下代碼
1 if(NULL != p)2 SAFE_DELETE(p);3 else4 DUMMY( );
其else分支就無法通過編譯(多出一個分號),而采用do{…}while(0)則毫無問題。
使用do{...} while(0)將宏包裹起來,成為一個獨立的語法單元,從而不會與上下文發(fā)生混淆。同時因為絕大多數(shù)編譯器都能夠識別do{...}while(0)這種無用的循環(huán)并優(yōu)化,所以該法不會導致程序的性能降低。
6.4 類型定義符typedef
C語言不僅提供了豐富的數(shù)據(jù)類型,而且還允許由用戶自己定義類型說明符,也就是說允許由用戶為數(shù)據(jù)類型取“別名”。類型定義符typedef即可用來完成此功能。
typedef定義的一般形式為:
typedef 原類型名  新類型名
其中原類型名中含有定義部分,新類型名一般用大寫表示,以便于區(qū)別。
例如,有整型量int a,b。其中int是整型變量的類型說明符。int的完整寫法為integer,為增加程序的可讀性,可把整型說明符用typedef定義為typedef  int  INTEGER。此后就可用INTEGER來代替int作整型變量的類型說明,如INTEGER a,b等效于int a,b。
用typedef定義數(shù)組、指針、結構等類型將帶來很大的方便,不僅使程序書寫簡單而且意義更為明確,因而增強了可讀性。
例如,typedef char NAME[20]表示NAME是字符數(shù)組類型,數(shù)組長度為20。然后可用NAME 說明變量,如NAME a1,a2,s1,s2完全等效于:char a1[20],a2[20],s1[20],s2[20]。
又如:
1 typedef struct{2 char name[20];3 int age;4 char sex;5 }STU;
然后可用STU來定義結構變量:STU body1,body2;
有時也可用宏定義來代替typedef的功能,但是宏定義是由預處理完成的,而typedef則是在編譯時完成的,后者更為靈活方便。
此外,采用typedef重新定義一些類型,可防止因平臺和編譯器不同而產(chǎn)生的類型字節(jié)數(shù)差異,方便移植。如:
1 typedef unsigned char boolean; /* Boolean value type. */ 2 typedef unsigned long int uint32; /* Unsigned 32 bit value */ 3 typedef unsigned short uint16; /* Unsigned 16 bit value */ 4 typedef unsigned char uint8; /* Unsigned 8 bit value */ 5 typedef signed long int int32; /* Signed 32 bit value */ 6 typedef signed short int16; /* Signed 16 bit value */ 7 typedef signed char int8; /* Signed 8 bit value */ 8 //下面的不建議使用 9 typedef unsigned char byte; /* Unsigned 8 bit value type. */10 typedef unsigned short word; /* Unsinged 16 bit value type. */11 typedef unsigned long dword; /* Unsigned 32 bit value type. */12 typedef unsigned char uint1; /* Unsigned 8 bit value type. */13 typedef unsigned short uint2; /* Unsigned 16 bit value type. */14 typedef unsigned long uint4; /* Unsigned 32 bit value type. */15 typedef signed char int1; /* Signed 8 bit value type. */16 typedef signed short int2; /* Signed 16 bit value type. */17 typedef long int int4; /* Signed 32 bit value type. */18 typedef signed long sint31; /* Signed 32 bit value */19 typedef signed short sint15; /* Signed 16 bit value */20 typedef signed char sint7; /* Signed 8 bit value */
本站僅提供存儲服務,所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊舉報
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
預處理概述
C語言基本概念理解
C語言入門之C 預處理器
編譯C程序有很多步驟,其中第一步為預處理(preprocessing)階段
宏定義
C語言中如何去理解預處理階段
更多類似文章 >>
生活服務
熱點新聞
分享 收藏 導長圖 關注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服