- C語言編程魔法書:基于C11標準
- 陳軼
- 8字
- 2019-01-05 01:58:58
第二篇 基礎語法篇

第4章
C語言中的基本元素
本章將正式進入C語言編程話題。我們在第1章已經大致介紹了C語言的編譯、連接和加載運行流程,參見圖1-2。我們首先介紹C語言單個源文件的基本構成以及基本元素。
我們在圖4-1中能看到,一個可用來編譯執行的基本C源文件主要包含4個部分。

圖4-1 C源文件的基本構成
第1部分是注釋。注釋主要用于給源代碼做批注,方便閱讀和維護。編譯器會忽略所有注釋部分,而且注釋部分在預編譯處理結束后就不存在了。我們將在10.9節討論程序注釋。
第2部分是預處理器(Preprocessor)。圖4-1中的第9行代碼就是一條#include預處理器,它將標準庫頭文件“stdio.h”中的所有內容都直接放到當前源文件中,這樣我們就可以將它看作在第9行這個位置插入此頭文件的所有內容(這里我們可以先無視上面的注釋部分的處理)。“stdio.h”文件包含了第16行所用到的puts標準庫函數的原型。我們將在第10章詳細討論預處理器。
第3部分是主函數入口main。它是C程序的入口函數。也就是說,當操作系統加載完我們構建生成的C程序后,率先執行的就是main函數。關于main函數,我們將在9.9節中介紹。
第4部分是用{ }包圍著的函數具體實現代碼(第13~17行)。這里的實現就是第16行打印輸出兩行文字。
C語言的頭文件一般用.h后綴表示,源文件一般用.c后綴表示。C源文件是一個文本文件,所以它是由一系列字符構成的。下一節將介紹C源文件中可用的字符集以及執行C程序時可用的字符集。
4.1 C語言中的字符集
一般來說,編程語言的字符集都可分為兩組:一組叫源字符集,另一組叫執行字符集。所謂“源字符集”是指在寫C源代碼時用的字符集,也就是呈現在C源文件中的字符集。而“執行字符集”是指編譯構建完源文件后的目標二進制文件中所表示的字符集,它將用于運行在當前的執行環境中。比如,我們在控制臺或者GUI窗口視圖上所看到的文字信息就屬于執行字符集。
C語言標準允許C語言實現采用多字符擴展字符集,但是必須要滿足一組基本字符集。基本字符集都包含在ASCII碼可顯字符集中,包括半角的大寫字母A~Z、小寫字母a~z、半角的阿拉伯數字0到9以及下列符號:
! " # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^ _ { | } ~
為了敘述方便,上述這排符號后續將統稱為“標點符號”;而大小寫半角英文字母統稱為“字母”;半角阿拉伯數字0到9統稱為“數字”。
由于在C語言的上述基本字符集中有9個字符超出了ISO 646不變字符集的范圍,分別是:# \ ^ [ ] | { } ~。所以,在C90標準中就引入了三字符連拼(Trigraph)的方式來表達這9個字符:
??= 對應于 #
??) 對應于 ]
??! 對應于 |
??( 對應于 [
??' 對應于 ^
??> 對應于 }
??/ 對應于 \
??< 對應于 {
??- 對應于 ~
例如:
?? =define arraycheck(a, b) a? ? (b? ? ) ? ? ! ? ? ! b? ? (a? ? )
printf(“Eh? ? ? /n”);
// 上述代碼等價于:
#define arraycheck(a, b) a[b] || b[a]
printf(“Eh? \n”);
這里我們還能再呈現一下源字符集與執行字符集的差異。上述代碼中,“? ? ? /n”表示源字符集,它在C源文件中就是如此寫的;而最后翻譯成的“\n”就相當于執行字符集,顯示在命令行程序中就是一個換行。
由于C++17標準打算廢棄三字符連拼,筆者估計下一個C語言標準也將廢棄三字符連拼機制,因此不建議各位使用,大家只要了解一下這個歷史即可。
C99標準中引入了對其中5個字符的雙字符連拼(Digraph)表示。
<: 對應于 [
:> 對應于 ]
<% 對應于 {
%> 對應于 }
%: 對應于 #
雙字符連拼在下一個標準中還能正常使用。盡管Trigraph與Digraph基本用不上,不過在看一些較早之前歐洲一些國家的人所寫的代碼時能知道那是什么。由于筆者在日本做過一些項目,所以知道在Windows系統下的日語環境中,“\”這個符號會被顯示成“¥”。因此當我們看到“¥”符號時能反應出是“\”就行。
4.2 C語言中的token
在編程語言中經常會涉及“token”這個詞,token這里不是指網絡通信中所謂的“令牌”,而是用于詞法解析的,通過指定一個詞位(詞的單位)的類別來結構化表示該詞位。如以下代碼:
int a = 3 << 2;
這里就有7個token,分別是:int、a、=、3、<<、2以及最后的分號;。這一行代碼中就已經列出了C語言中的常用幾種token,分別是關鍵字(int)、標識符(a)、字面量(3和2)、操作符(=和<<)、其他標點符號(;)。每個token之間用空白符或標點符號進行分隔。空白符主要包括空格(white space)、制表符(tab)以及換行回車。像上述代碼也能寫成以下形式,兩者是等價的。
int a=3<<2;
但是,這里int與a之間必須用空白符分割。
C語言標準中定義了token和預處理token,分別用于在編譯時和預編譯時的符號解析。token包括關鍵字、標識符、常量、字符串字面量以及標點符號。預處理token主要包括頭文件名、標識符、預處理數、字符常量、字符串字面量、標點符號以及不屬于上述符號的每個非空白字符。
下面我們將分別描述標識符、關鍵字、常量與字符串字面量、標點符號這幾種token。預處理token將放在第10章做詳細描述。
4.2.1 C語言中的標識符
在C11標準中提到,C語言中的標識符可以表示一個對象(object),一個函數(function),一個結構體(structure)、聯合體(union)或枚舉(enumeration)的一個名字(C11標準中將結構體、聯合體以及枚舉類型的名字稱為tag)或其中一個成員、一個typedef名、一個跳轉標簽(label)名、一個宏(macro)名或一個宏的形參(parameter)。當我們提到“標識符”時,要意識到標識符不僅僅是上述所描述實體的名稱,而且也是對它們的引用(reference)。
一般C語言的實現約定,一個標識符由基本字符集中的所有大小寫英文字母、阿拉伯數字0到9以及下劃線_構成,并且標識符不能以數字開頭。比如:aBc、_ab、C11、_3_都是有效的標識符;5ab、a(2、886都是無效的標識符。有些C語言實現允許將$作為構成標識符的有效字符,但有些是將含有$的標識符作為一種內部使用的特殊符號來用,所以我們在命名標識符的時候應該避免使用$符號。此外,C11標準允許使用多字節擴展字符集(通用字符名)來命名標識符,但不能違背上述基本約定。比如,在Apple LLVM編譯器中,允許使用中文、拉丁字母、希臘字母等作為標識符:αντιo、bonné、小鳥遊·六花、ラーメン等都是有效標識符,但是像3百九、十*二,這些就是無效的標識符。此外,C語言標準中還規定,如果一個標識符含有通用字符名,那么每一個通用字符名必須落在ISO/IEC 10646編碼方式的以下范圍內(用十六進制表示):
1)00A8,00AA,00AD,00AF,00B2~00B5,00B7~00BA,00BC ~00BE, 00C0~00D6,00D8~00F6,00F8~00FF;
2)0100~167F,1681~180D,180F~1FFF;
3)200B~200D,202A~202E,203F~2040,2054,2060~206F;
4)2070~218F,2460~24FF,2776~2793,2C00~2DFF,2E80~2FFF;
5)3004~3007,3021~302F,3031~303F;
6)3040~D7FF;
7)F900~FD3D, FD40~FDCF, FDF0~FE44, FE47~FFFD;
8)10000~1FFFD,20000~2FFFD,30000~3FFFD,40000~4FFFD,50000~5FFFD,60000~6FFFD,70000~7FFFD,80000~8FFFD,90000~9FFFD, A0000~AFFFD, B0000~BFFFD, C0000~CFFFD, D0000~DFFFD, E0000~EFFFD。
此外,標識符的第一個通用字符名不能落在以下范圍內:0300~036F,1DC0~1DFF,20D0~20FF, FE20~FE2F。
在C語言標準中沒有特別設定一個標識符的最大長度。不過具體的C語言實現可以根據自己的情況設定標識符最大長度。
在同一作用域(scope)內,一個標識符應該指定一個確切的實體。如果編譯器在當前上下文中無法判定某個標識符用于引用哪個實體,那么就會發生編譯錯誤。關于作用域的詳細介紹請參見11.1節。
4.2.2 C語言中的關鍵字
在編程語言中所謂的“關鍵字”(keyword)是指被編程語言編譯器保留用作特定語義的token,它們不能被程序員當作其他標識符來使用。C11標準中的關鍵字見表4-1。
表4-1 C11標準中的關鍵字

在上述關鍵字中有些是由大寫、小寫以及下劃線混合組成的,各位在編寫代碼的時候需要注意大小寫。這些關鍵字會從第5章開始分別進行介紹。
看到以上這些關鍵字讀者可能會感到奇怪,為何有些關鍵字是以下劃線打頭的呢?以下劃線打頭的關鍵字均是從C99標準開始引入的。由于在C99之前,有不少C語言編譯器已經對C99標準新引入的特性給予支持,為了防止C99標準的關鍵字與一些編譯器已有的擴展關鍵字沖突,從而通過以下劃線作為前綴,然后首字母大寫來定義這些關鍵字。而通過C語言新標準引入新的標準庫可使得這些關鍵字能被統一。
所以,大家在使用以下劃線打頭的關鍵字時,請盡量先引入相應的標準庫頭文件,然后使用非下劃線形式的相應關鍵字。比如,<stdbool.h>頭文件中將_Bool類型定義為了bool類型;<complex.h>頭文件中將_Complex定義為了complex,等等。我們最好使用bool、complex來代替_Bool和_Complex,這樣一來書寫更為簡潔,二來又有更好的向前兼容以及跨平臺等特性。當然,還有一些關鍵字是沒有相應標準庫定義形式的,比如_Generic,我們在使用的時候直接用_Generic即可。
4.2.3 C語言中的常量與字符串字面量
C語言中,常量(contant)有4種,分別是整數常量、浮點數常量、枚舉常量以及字符常量。每個常量都具有一個特定的類型以及該常量所指定的值,常量值必須在其類型所能表示的范圍內。整數常量和字符常量將在5.1節中描述;浮點數常量將在5.2節中描述;枚舉常量將在6.1節描述。
字符串字面量我們之前已經見過了,圖4-1中的u8“Hello, world\n你好,世界!”就是一個字符串字面量。在C11中,一個字符串字面量由一對雙引號包裹的一系列的字符構成。如果字符串中含有諸如回車、雙引號等字符的話,需要對它們進行轉義,轉義字符將在5.1.6節中描述。此外,字符串的第一個雙引號前可以加u8、u和U這三種前綴。u8指定了該字符串字面量是一個UTF-8字符串;u表示該字符串字面量是一個UTF-16字符串;U表示該字符串字面量是一個UTF-32字符串。如果不加前綴,則默認為當前系統實現的字符編碼格式。字符串字面量將在7.10節做進一步描述。
4.2.4 C語言中的標點符號
C語言的標點符號如下:
[ ] ( ) { } . ->
++ -- & * + - ~ !
/ % << >> < > <= >=
== ! = ^ | && || ? :
; ... = *= /= %= += -=
<<= >>= &= ^= |= , # ##
<: :> <% %> %: %:%:
標點符號是具有獨立語法和語義意義的符號。它作為一個要執行的操作時,又稱為操作符(operator)。操作符所作用的實體稱為操作數(operand)。比如,3+2這個表達式中,+是一個操作符,表示整數加法操作。而3和2則是+的操作數,3作為+的左操作數;2作為+的右操作數。
上述列出的標點符號中,有些無法單獨成為一個操作符,比如[、], (、)等,而是需要將它們組合起來[ ]、( )才行。而在( )操作符里邊的表達式則作為該操作符的操作數。比如:(3+2)的操作數是表達式3+2。此外,有些標點符號可進行組合形成一個操作符,比如<<、+=、>>=等。這些組合標點符號之間不允許帶有空白符,比如<<表示左移操作,而< <僅僅表示兩個小于號。
C語言中,操作符按照可作用的操作數個數來分可分為單目操作符(unary operator)、雙目操作符(binary operator)和三目操作符(ternary operator)。
1)單目操作符有!(表示邏輯非)、&(用作地址操作符時)、*(作為間接操作符時)、+(表示正數符號時)、-(表示負數符號時)、~(表示按位取反)。
2)雙目操作符有++(表示自增操作)、--(表示自減操作)。
3)三目操作符只有一組,即?與:的組合,作為條件表達式的操作符,這將在8.2節中詳細描述。
其余的,除了#和##作為預處理操作符之外,上述列出的操作數中都是雙目操作符。
不同的操作符可能會有不同的計算優先次序。在計算一個表達式時,如果該表達式含有多個操作符,那么這些操作符按照優先級高的先開始計算,然后再計算低優先級的操作。如果幾個操作符具有相同優先級,那么按照從左到右的順序依次計算。在C11標準中定義了如下表達式的計算優先次序,排列從高到低。
1)基本表達式:標識符、常量、字符串字面量、圓括號表達式(比如(3+2))、泛型表達式。
2)后綴操作符:數組下標(比如a[0])、函數調用、結構體與聯合體成員訪問操作符(.和->)、后綴自增及自減操作符(比如a++; a--)、復合字面量(比如(int[]){1, 2, 3})。
3)單目操作符:前綴自增與自減操作符、地址操作符與間接操作符(比如++a;--a)、單目算術操作符(+、-、!、~,其中這里的+和-表示正負號)、sizeof操作符與_Alignof操作符。
4)類型投射操作符(詳見5.6節)。
5)乘法操作符(包括乘、除、求余數*、/、%)。
6)加法操作符(+、-)。
7)移位操作符(左移、右移)。
8)關系操作符(大于、小于、大于等于、小于等于)。
9)相等性操作符(等于和不等于,==、! =)。
10)按位與操作符。
11)按位異或操作符。
12)按位或操作符。
13)邏輯與操作符。
14)邏輯或操作符。
15)條件操作符(即三目表達式)。
16)賦值操作符。
17)逗號操作符。
下面舉一個簡單的例子:
int a = 3 + 2 * 10 / 4- -(3-2);
上述代碼中,(3-2)最先被計算得到結果1,然后再計算-(1)的結果是-1,然后計算2*10的結果等于20,再計算20/4的結果等5,再是3+5的結果等于8,然后是8-(-1)的結果是9,最后是將結果9賦值給變量a。
4.3 關于C語言中的“對象”
C11標準將“對象”定義為執行環境中的數據存儲區域,對象中的內容用于表達它的值。當引用了某一對象時,該對象就可稱為具有一個特定類型。言下之意,C語言標準中的“對象”是指數據實體,而不是一個函數。此外,它具有一個特定的存儲區域,無論是在寄存器中還是在存儲器中。另外,它具有一個特定的類型。
C語言不是一門面向對象的編程語言,所以這里的“對象”與面向對象編程語言所涉及的對象概念有些差別,不過從范圍上來講,這里的“對象”比面向對象中的對象范圍更廣。從總體上將對象進行劃分可分為兩大類——變量和常量。
□變量是指在程序運行時,允許該對象所存放的值被修改。
□常量是指在程序運行時,該對象所存放的值不允許被修改。
在C語言實現中,常量可以被寫入ROM,尤其對于嵌入式設備而言,更有可能如此。這樣,一旦對某個常量對象進行修改,那么系統會直接發出異常。而在通用桌面操作系統中,常量也被分配在RAM中,所以我們仍然可以通過類型轉換或是其他奇技淫巧對常量對象進行修改,不過后果是無法預估的。
在計算機編程語言中還有一個比較常見的概念就是字面量。在傳統編程語言中,字面量就是指在源代碼中用于表示一個固定值的文字記號。
比如,像3、-10、3.14、"hello"等都屬于字面量。
其中:
□3、-10表示整數字面量。
□3.14表示浮點數字面量。
□"hello"表示一個字符串字面量。
這些字面量往往都是常量,而像一般的整數字面量在概念上我們也無需關心它到底是不是一個對象,即不需要關心它有沒有自己的存儲空間。由于字面量以及像(3+2)等常量表達式是在編譯時就能計算出結果的,所以對于這些字面量的算術邏輯計算也無需在程序運行時體現出來。
另外,C11還包括了結構體、聯合體以及數組的復合字面量。這些復合字面量無需是常量,而且它們自己所包含的元素也完全可以是變量,并且在運行時也完全可被修改。
4.4 C語言中的“副作用”
在很多編程語言中都會提到“副作用”(side effects)這個概念。在C11標準中對副作用是這么描述的:對一個易變對象的訪問、對一個對象的修改、對一個文件的修改,或調用一個函數,所有這些操作都具有副作用。副作用對執行環境中的狀態做了改變。對一個表達式的計算通常包含了對值的計算以及對副作用的初始化。對一個左值表達式的值計算包含了判定該表達式所表示對象的標識。
通常來講,所謂副作用就是在C源代碼中的某一條表達式在目標程序中執行時,對當前程序的執行狀態產生了或潛在產生改變,那么我們稱該表達式產生了副作用。所謂程序執行狀態包含了許多元素,比如對目標程序指令、寄存器的值、存儲器中的數據等。
4.5 C語言標準庫中的printf函數
我們這里先簡單介紹一下本書后續會大量使用的控制臺字符串輸出函數printf。這是一個C語言標準庫函數。printf函數的原型為:
int printf(const char * restrict format, ...);
此函數第一個參數format是一個字符串格式符,后面的省略號表示不定個數的參數,這些參數的數據類型需要分別與format所指向的字符串中的格式匹配。函數最后返回的是一個int類型整數,表示被傳遞到控制臺的字符的個數。如果輸出或者字符串編碼發生錯誤,那么該函數將返回一個負值。但當前大部分編譯器的實現并非返回傳遞到控制臺的字符個數,而是字節個數,這對輸出UTF-8編碼的字符串時尤為如此。
下面簡單介紹一下本書中常用的format字符串中的格式符。
1)%c:對應參數是一個int類型,但實際運行時會將該int類型對象轉換為unsigned char類型。
2)%d:對應參數是一個int類型。
3)%f:對應參數是一個double類型。
4)%ld:對應參數是一個long int類型。
5)%s:對應參數是一個const char*類型,表示輸出一個字符串。
6)%u:對應參數是一個unsigned int類型。
7)%zu:對應參數是一個size_t類型。
8)%td:對應參數是一個ptrdiff_t類型。
9)%x(或%X):對應參數是一個int類型,不過會以十六進制形式輸出,其中大于9的數字根據字母x大小寫進行轉換,如果是%x,則大于9的數用a~f表示;如果是%X,則用A~F表示。
10)%%:輸出一個%符號。
各位可以在自己的計算機上嘗試編寫下列代碼,熟悉一下pritnf函數的使用方式:
#include <stdio.h> #include <math.h> int main(int argc, const char * argv[]) { int len = printf("你好\n"); printf("長度為:%d\n", len); printf("輸出字符是:%c,輸出浮點數是:%f\n", 'A', M_PI); printf("100的十六進制數為:0x%X\n", 100); const char *s = "hello, world! "; printf("幾乎100%會出現在編程語言教科書上的字符串是:%s\n", s); }
各位可以編譯運行上述代碼。如果各位在某些Unix/Linux上實踐,沒有中文輸入法也沒有關系,可以用相應的英文來代替上述中文。另外,上述字符串中所出現的\n是一個轉義字符,關于轉義字符,我們將在5.1.6節中加以描述。
4.6 本章小結
本章我們大概描述了C語言構成的基本元素。一開始,我們列出了一個完整的C語言源文件應該包含的幾個部分。然后我們提到了C語言中的可用字符集以及各類符號與它們的定義。關于C語言執行環境限制的更多詳細信息可參考此博文:http://www.cnblogs.com/zenny-chen/p/4251813.html。
通過本章學習,各位應該已經能體會到C語言書寫的大致格式,并且通過本章列出的一些代碼片段,自己能試試身手寫一些簡單短小的代碼出來,然后利用printf函數可以打印出一些計算結果。
- PostgreSQL for Data Architects
- WSO2 Developer’s Guide
- Learning Apache Kafka(Second Edition)
- Mastering AndEngine Game Development
- Securing WebLogic Server 12c
- Python機器學習編程與實戰
- RabbitMQ Cookbook
- Visual Basic程序設計
- 新一代SDN:VMware NSX 網絡原理與實踐
- Building Microservices with .NET Core
- C#程序設計(項目教學版)
- React.js實戰
- C# 7 and .NET Core 2.0 Blueprints
- Python程序設計教程
- 軟件工程實用教程 (第3版)