最近重新看了C語言聖經,查漏補缺,記了簡單的筆記,全部來自原書,共9866字,記錄一下。
騰訊文檔地址:https://docs.qq.com/doc/DUmt5VU5Tem1LQUxx
第一章 導言
c語言中一個通用的實例:在允許使用某種類型變量值的任何場合,都可以使用該類型的更復雜的表達式。
標准庫提供的輸入輸出模型非常簡單。無論文本從何處輸入、輸出到何處,其輸入輸出都是按照字符流的方式處理。文本流是由多行字符構成的字符序列,而每行字符則由0個或者多個字符組成,行末是一個換行符。標准庫負責使每個輸入輸出流都能遵守這一模型。
字符在鍵盤、屏幕或者其他任何地方無論以什么形式表現,它在機器內部都是以為位模式存儲的。
EOF定義在頭文件<stdio.h>中,是一個整形數。它與任何char類型的值都不相等。
賦值操作是一個表達式,並且具有一個值,就是賦值后左邊變量保存的值。
出現在main函數之前的函數聲明稱為函數原型。函數原型中的參數名是可選的。
printf函數中的格式規范%s規定,對應的參數必須是以'\0'結尾形式的字符串。
定義表示創建變量會分配存儲單元,而聲明指的是說明變量的性質,但並不分配存儲單元。
第二章 類型、運算符與表達式
數據類型及長度
C語言只有char,int,float,double四種基本數據類型。
Int通常代表特定機器中整數的自然長度。Short類型通常為16位,long類型通常為32位,int類型可以為16位或者32位。各編譯器可以根據硬件特性自主選擇合適的類型長度,但要遵循限制:short和int類型至少為16位,long類型至少為32位,short類型不得長於int類型,int類型不得長於long類型。
常量
字符常量’\0’表示值為0的字符,也就是空字符(null)。
常量表達式是僅僅只包含常量的表達式。這種表達式在編譯時求值,而不在運行時求值。
編譯時可以將多個字符串常量直接連接起來,如”hello” “ world”等價於“hello world”。
C語言對字符串的長度沒有限制,但是必須掃描完整個字符串后才能確定字符串的長度。
枚舉是一個常量整形值的列表。
在沒有顯示說明的情況下,enum類型的第一個枚舉名的值為0,第二個為1,以此類推。如果只指定了部分枚舉名的值,那么未指定值的枚舉名將依着最后一個指定值向后遞增。
聲明
const限定符指定變量的值不能被修改。
算數運算符
在有負操作數的情況下,整數除法截取的方向以及取模運算結果的符號取決於具體機器的實現,這和處理上溢或下溢的情況是一樣的。
類型轉換
C語言的定義保證了機器的標准打印字符集中的字符不會是負值,因此,在表達式中這些字符總是正值。但是,存儲在字符變量中的位模式在某些機器中可能是負的。為了保證程序的可移植性,如果要在char類型的變量中存儲非字符數據,最好指定signed或unsigned限定符。
使用float類型主要是為了在使用較大的數組時節省存儲空間,有時也為了節省機器執行時間(雙精度運算很耗時)。
當表達式中包含unsigned類型的操作數時,轉換規則要復雜一些。主要原因在於,帶符號值與無符號值之間的比較運算是與機器相關的,因為它們取決於機器中不同整數類型的大小。比如-1L>1UL,因為-1L將被提升為unsigned long類型(符號位的1),成為更大的正數。
無論是否進行符號擴展,字符型變量都將被轉換為整形變量。
由於函數調用的參數是表達式,所以在把參數傳遞給函數時也可能進行類型轉換。
強制類型轉換只是生成一個指定類型的值,沒有修改原來的變量值。
賦值運算符與表達式
賦值表達式的類型是左操作數的類型,值是賦值操作完成之后的值。
運算符優先級與求值次序
同大多數語言一樣,C語言沒有指定同一運算符中多個操作數的計算順序(&&,||,?:和,運算符除外)。
同樣,C語言也沒有指定函數各參數的求值順序。
第三章 控制流
Goto語句與標號
C語言提供了可隨意濫用的goto語句以及標記跳轉位置的標號。
Goto語句最常見的用法是終止程序在某些深度嵌套的結構中的處理過程,例如一次跳出兩層或者多層循環。
標號的命名同變量命名的形式相同,標號的后面要緊跟着一個冒號。標號可以位於對應的goto語句所在函數的任意語句的前面。標號的作用域是整個函數。
建議盡量少的使用goto語句。
第四章 函數與數據結構
函數的基本知識
如果函數定義中省略了返回值類型,則默認為int類型。
返回非整形值的函數
如果先前沒有聲明過的一個名字出現在某個表達式中,並且其后緊跟一個左圓括號,那么上下文就會認為該名字是一個函數名字,該函數的返回值將被假定為int類型,但是上下文並不會對其參數作任何假設。
外部變量
外部變量和函數具有下列性質:通過同一個名字對外部變量的所有引用(即使這種引用來自於單獨編譯的不同函數)實際上都是引用同一個對象(標准中把這一性質稱為外部鏈接)。
程序中經常會出現這樣的情況:程序不能確定它已經讀入的輸入是否足夠,除非超前多讀取一些輸入。但是每當程序多讀入一個字符時,就把它壓回到輸入中,對代碼其余部分而言就好像沒有讀入該字符一樣。
標准庫中提供了函數ungetc,它將一個字符壓回到棧中。
作用域規則
如果要在外部變量的定義之前使用該變量,或者外部變量的定義與變量的使用不在同一個源文件中,必須在相應的變量聲明中強制性的使用關鍵字extern。
將外部變量的聲明與定義嚴格區分開很重要。變量聲明用於說明變量的屬性(主要是變量的類型),而變量定義除此之外還將引起存儲器的分配。
外部變量的定義中必須指定數組的長度,但extern聲明則不一定要指定數組的長度。
外部變量的初始化只能出現在其定義中。
靜態變量
用static聲明限定外部變量和函數,可以將其后聲明的對象的作用域限定為被編譯源文件的剩余部分。通過static限定外部對象,可以達到隱藏外部對象的目的。
Static類型的函數除了對該函數聲明所在的文件可見外,其他文件都無法訪問。
Static類型的內部變量是一種只能在某個特定類型函數中使用但一直占據內存空間的變量。
寄存器變量
Register聲明告訴編譯器,它所聲明的變量在程序中使用頻率較高。其思想是將register變量放在機器的寄存器中。但編譯器可以忽略此選項。
無論寄存器變量實際上是不是存放在寄存器中,它的地址都是不能訪問的。
初始化
在不進行顯式初始化的情況下,外部變量和靜態變量都將被初始化為0,而自動變量和寄存器變量的初值則沒有定義(即初值為無用的信息)。
對於外部變量和靜態變量來說,初始化表達式必須是常量表達式,且只初始化一次(概念上將是在程序開始執行前初始化)。
如果初始化表達式的個數比數組元素少,則對外部變量、靜態變量和自動變量來說,沒有被初始化的元素將被初始化為0,如果初始化表達式的個數比數組元素多,則是錯誤的。
遞歸
遞歸並不節省存儲器的開銷,因為遞歸調用過程中必須在某個地方維護一個存儲處理值的棧。遞歸的執行速度並不快,但遞歸代碼比較緊湊,並且比相應的非遞歸代碼更易於編寫與理解。
C預處理器
從概念上講,預處理器是編譯過程中單獨執行的第一個步驟。
文件包含指令(即#include指令)的行將被替換為由文件名指定的文件的內容。如果文件名用引號引起來,則在源文件所在的位置查找該文件;如果在該位置沒有找到文件,或者如果文件名是用尖括號<>括起來的,則將根據相應的規則查找該文件,這個規則同具體的實現有關。
宏定義(即#define指令)是一種最簡單的宏替換:后續所有出現名字記號的地方都將被替換為替換文本。
宏替換只對記號進行,對括在引號中的字符串不起作用。
宏定義也可以帶參數,這樣可以對不同的宏調用使用不同的替換文本。
<stdio.h>頭文件中有一個很實用的例子:getchar與putchar函數在實際中常常被定義為宏,這樣可以避免處理字符時調用函數所需的運行時開銷。
形式參數不能用帶引號的字符串替換。但是如果在替換文本匯總,參數名以#作為前綴則結果將被擴展為由實際參數替換該參數的帶引號的字符串。
還可以使用條件語句對預處理本身進行控制,這種條件語句的值是在預處理執行的過程中進行計算。
第五章 指針與數組
指針與地址
通常情況下,機器的一個字節可以存放一個char類型的數據,兩個相鄰的字節存儲單元可存儲一個short類型的數據,而4個相鄰的字節存儲單元可存儲一個long類型的數據。指針是能夠存放一個地址的一組存儲單元(通常是2個或4個字節)。
地址運算符&只能應用於內存中的對象,即變量和數組元素。
指針只能指向某種特定類型的對象,也就是說,每個指針都必須指向某種特定的數據類型。(一個例外情況是指向void類型的指針可以存放指向任何類型的指針,但它不能間接引用其自身)
指針也是變量,在程序中可以直接使用。
指針與數組
一般來說,用指針編寫的程序比用數組下標編寫的程序執行速度快,但理解起來稍微困難一些。
如果指針pa指向a[0],那么*(pa+1)引用的是數組元素a[1]的內容,pa+i是數組元素a[i]的地址,*(pa+i)引用的是數組元素a[i]的內容。無論數組中元素的類型或數組長度是什么,結論都成立。
根據定義,數組類型的變量或表達式的值是該數組第0個元素的地址。對數組元素a[i]的引用也可以寫成*(a+i)。
如果pa是一個指針,那么在表達式中也可以在它的后面加下標。pa[i]與*(pa+i)是等價的。簡而言之,一個通過數組和下標實現的表達式可等價的通過指針和偏移量實現。
數組名和指針之間有一個不同之處。指針是一個變量,但數組名不是變量。
在函數定義中,形式參數char s[] 和char *s是等價的。
地址算數運算
C語言中的地址算數運算方法是一致且有規律的,將指針、數組和地址的算數運算集成在一起的該語言的一大優點。
和其他類型變量一樣,指針也可以初始化。對指針有意義的初始化值只能是0或者是表示地址的表達式,對后者來說,表達式所代表的地址必須是在此前已定義的具有適當類型的數據的地址。
C語言保證,0永遠不是有效的數據地址。
指針與整數之間不能相互轉換,但0是唯一的例外:常量0可以賦值給指針,指針也可以和常量0進行比較。程序中常用符號常量NULL來代替常量0,這樣便於更清晰的說明常量0是指針的一個特殊值。符號常量NULL定義在標准頭文件<stddef.h>中。
在某些情況下對指針可已經進行比較運算。比如指針p,q指向同一個數組的成員,那么它們之間就可以進行關系比較運算,如果p指向的數組元素的位置在q指向的數組元素位置之前,那么p<q為真。任何指針與0進行相等或不相等的比較運算都有意義。但是,指向不同數組的元素的指針之間的算數或比較運算沒有意義。(有一個特例:指針的算數運算中可以使用數組的最后一個元素的下一個元素的地址)
指針可以和整數進行相加或者相減運算。p+n 表示指針p當前指向的對象之后第n個對象的地址。無論指針p指向的對象是何種類型,該結論都成立。計算p+n時,n將根據p指向的對象的長度按比例縮放,而p指向的對象的長度取決於p的聲明。
指針的減法運算也是有意義的。如果p和q指向相同數組中的元素,且p<q,那么q-p+1就是位於p和q指向的元素之間的元素的數目。
有效的指針運算包括相同類型指針之間的賦值運算;指針同整數之間的加法或減法運算;指向相同數組中元素的兩個指針間的減法或比較運算;將指針賦值為0或指針與0之間的比較運算。其他所有形式的指針運算都是非法的。
字符指針與函數
例如printf(“hello world”); 這樣的語句中,實際上是通過字符指針訪問該字符串的。Printf語句接收的是一個指向字符數組第一個字符的指針。也就是說,字符串常量可以通過一個指向其第一個元素的指針訪問。
C語言中沒有提供將整個字符串作為一個整體進行處理的運算符。
字符數組中的單個字符可以修改,字符數組始終指向同一個存儲位置。如果一個字符指針初值指向一個字符串常量,這個指針可以被修改指向其他地址,但如果嘗試修改字符串的內容,是沒有意義的。
多維數組
數組元素按行存儲。
如果將二維數組作為參數傳遞給函數,那么在函數的參數聲明中必須指明數組的列數。
二維數組作為函數參數可以寫作:f(int a[2][2]),f(int a[][2]), f(int (*a)[2])。第三種聲明形式表示參數是一個指針,它指向具有2個整形元素的一維數組。
指針與多維度數組
指針數組的一個重要優點在於,數組的每一行長度可以不同。
命令行參數
在支持C語言的環境中,可以在程序開始執行時將命令行參數傳遞給程序。調用主函數main時,它帶有兩個參數。第一個參數(習慣上成為argc,用於參數計數)的值表示運行程序時命令行中參數的個數;第二個參數(成為argv,用於參數向量)是一個指向字符串數組的指針,其中每個字符串對應一個參數。
按照C語言的約定,argv[0]的值是啟動該程序的程序名,因此argc的值至少為1。
ANSI標准要求argv[argc]的值必須為一個空指針。
UNIX系統中的C語言程序有一個公共的約定:以負號開頭的參數表示是一個可選標志或參數。
指向函數的指針
在C語言中,函數本身不是變量,但是可以定義指向函數的指針。
任何類型的指針都可以轉換為void * 類型,並且在將它轉換回原來的類型時不會丟失信息。
Int (*comp)(void *, void *);這樣的聲明表明comp是一個指向函數的指針,*comp代表一個函數。
第六章 結構
結構的基本知識
結構聲明時,struct后面的名字是可選的,稱為結構標記。結構標記用於為結構命名。
Struct {...} x,y,z;這種方式的聲明與一般聲明 int x,y,z; 從語法角度上具有相似的意義。
結構與函數
結構的合法操作只有幾種:作為一個整體復制和賦值,通過&運算符取地址,訪問其成員。
結構之間不可以進行比較。
結構類型的參數和其他類型的參數一樣,都是通過值傳遞的。
結構指針的使用頻率非常高,為了使用方便,C語言提供了另一種簡寫方式(相比於(*p).x的方式)。假定p是一個指向結構的指針,可以用 p->結構成員 這種形式引用相應的結構成員。
結構數組
C語言提供了一個編譯時一元運算符sizeof,可用來計算任一對象的長度。它的值等於指定對象或類型占用的存儲空間字節數。(嚴格的說,sizeof的返回值是無符號整形值,類型為size_t,該類型在頭文件<stddef.h>中定義)
條件編譯語句#if中不能使用sizeof,因為預處理器不對類型名進行分析。但預處理器並不計算#define語句中的表達式,因此,在#define中使用sizeof是合法的。
指向結構的指針
千萬不要以為結構的長度等於各成員長度的和。因為不同的對象類型有不同的對齊要求,所以,結構中可能會出現未命名的空穴(hole)。
類型定義(typedef)
C語言提供了一個稱為typedef的功能,它用來建立新的數據類型名,例如聲明
typedef int Length;
將Length定義為與int具有同等意義的名字。類型Length可用於類型聲明、類型轉換等,和類型int完全相同。
從任何意義上講,typedef聲明並沒有創建一個新類型,它只是為某個已存在的類型增加了一個新的名稱而已。
實際上,typedef類似於#define語句,但由於typedef是由編譯器解釋的,因此它的文本替換功能要超過預處理器的能力。
除了表達方式更簡潔之外,使用typedef還有另外兩個重要原因。首先,它可以使程序參數化,以提高程序的可移植性。第二個作用是為程序提供更好的說明性。
聯合
聯合是可以(在不同時刻)保存不同類型和長度的對象的變量,編譯器負責跟蹤對象的長度和對齊要求。聯合提供了一種方式,以在單塊存儲區中管理不同類型的數據,而不需要在程序中嵌入任何同及其有關的信息。
聯合變量讀取的類型必須是最近一次存入的類型。
訪問聯合中成員的方式和訪問結構的方式相同。
實際上,聯合就是一個結構,它的所有成員相對於基地址的偏移量都為0,此結構空間要大到能夠容納最“寬”的成員,並且,其對齊方式要適合於聯合中所有類型的成員。
聯合只能用其第一個成員類型的值進行初始化。
位字段
C語言提供了一種直接定義和訪問一個字中位字段的能力,而不需要通過按位邏輯運算符。位字段(bit-field),或簡稱字段,是“字”中相鄰位的集合。“字”(word)是單個的存儲單元,它同具體的實現有關。
字段的所有屬性幾乎都同具體的實現有關。
第七章 輸入與輸出
標准輸入輸出
命令行 prog <infile 將使得程序prog從輸入文件infile(而不是從鍵盤)中讀取字符。實際上,程序prog本身並不在意輸入方式的改變,並且,字符串“<infile”也並不包含在argv 的命令行參數中。
當文件名用一對尖括號<>括起來時,預處理器將在由具體實現定義的有關位置中查找指定的文件。
格式化輸出--printf函數
格式字符串包含兩個類型的對象,普通字符和轉換說明。
每個轉化說明都有一個百分號字符(即%)開始,並以一個轉換字符結束。在字符%和轉換字符之間可能包含以下部分:
負號:轉換參數以左對齊方式輸出。
數:指定最小字段寬度。
小數點:用於將字段寬度和精度分開。
數:用於指定精度,即指定字符串中要打印的最大字符數、浮點數小數點后的位數、整型最少輸出的數字數目。
字母h或l:h表示整數作為short類型打印,l表示整數作為long類型打印。
轉換字符有d,i,o,x,X,u,c,s,f,e,E,g,G,p,%。
在轉換說明中,寬度或精度可以用星號*表示,這時,寬度或精度的值通過轉換下一參數(必須為int)來計算。
Sprintf函數和printf函數一樣,但它將輸出結果存放到string中。
變長參數表
函數printf的正確聲明形式為:
int printf(char *fmt, ...)
其中,省略號表示參數表中參數的數量和類型是可變的。省略號只能出現在參數表的尾部。
標准頭文件<stdarg.h>中包含一組宏定義,它們對如何遍歷參數表進行了定義,該頭文件的實現因不同的機器而不同,但提供的接口是一致的。
va_list類型用於聲明一個變量,該變量將依次引用各參數。
宏va_start將va_list類型的變量初始化為指向第一個無名參數的指針。在使用va_list類型變量之前,該宏必須被調用一次。參數表必須至少包含一個有名參數,va_start將最后一個有名參數作為起點。
每次調用va_arg,該函數都將返回一個參數,並將va_list類型的變量指向下一個參數。va_arg使用一個類型名來決定返回的對象類型、指針移動的步長。最后,必須在函數返回之前調用va_end,以完成一些必要的清理工作。
格式化輸入--scanf函數
scanf函數掃描完其格式串,或者碰到某些輸入無法與格式控制說明匹配的情況時,該函數將終止,同時,成功匹配並賦值的輸入項的個數將作為函數值返回。如果到達文件結尾,該函數將返回EOF。返回EOF和返回0是不同的,0表示下一個輸入字符與格式串中的第一個格式說明不匹配。
還有一個輸入函數sscanf,用於從一個字符串(而不是標准輸入)中讀取字符序列。
Scanf函數忽略格式串中的空格和制表符。此外,在讀取輸入值時,它將跳過空白符。如果要讀取格式不固定的輸入,最好每次讀入一行,然后再用sscanf將合適的格式分離出來讀入。
Scanf函數可以和其他輸入函數混合使用。無論調用哪個輸入函數,下一個輸入函數的調用將從scanf沒有讀取的第一個字符處開始讀取數據。
文件訪問
在讀一個文件之前,必須通過庫函數fopen打開該文件。Fopen用文件名與操作系統進行某些必要的連接和通信,並返回一個可以用於文件讀寫操作的指針。
該指針稱為文件指針,指向一個包含文件信息的結構,這些信息包括:緩沖區的位置、緩沖區中當前字符的位置、文件的讀或寫狀態、是否出錯或是否已經到達文件結尾等等。<stdio.h>中定義了包含這些信息的結構FILE。
FILE像int一樣是一個類型名,而不是結構標記。是通過typedef定義的。
如果發生錯誤,fopen將返回NULL。
文件打開后,有多種方法可以對文件進行讀寫,其中,getc和putc最簡單。Getc從文件中返回下一個字符,參數是文件指針。
getc函數函數返回文件指針指向的輸入流中的下一個字符。如果到達文件尾或出現錯誤,返回EOF。
putc輸入函數將第一個參數(字符)寫入到第二個參數(文件指針)指向的文件中,並返回寫入的字符。如果發生錯誤則返回EOF。getc和putc是宏而不是函數。
啟動一個C語言程序時,操作系統環境負責打開3個文件,並將這3個文件的指針提供給該程序。這3個文件分別是標准輸入、標准輸出和標准錯誤,相應的文件指針分別為stdin,stdout和stderr,他們在<stdio.h>中聲明。
對於文件的格式化輸入或輸出,可以使用函數fscanf和fprintf。與scanf和printf的唯一區別是它們的第一個參數是文件指針。
文件指針stdin與stdout都是FILE*類型的對象,但它們是常量。
錯誤處理--stderr和exit
Fprintf函數產生的診斷信息將會輸出到stderr上。
標准庫函數exit將終止調用程序的執行。按照慣例,返回值0表示一切正常,非0返回值通常表示出現了異常情況。exit為每個已經打開的輸出文件調用fclose函數,以將緩沖區中的所有輸出寫到相應的文件中。
使用exit有一個優點,它可以從其他函數中調用。
如果流fp(函數ferror的FILE類型的指針參數)中出現錯誤,則函數ferror返回一個非0值。
函數feof(FILE*)與ferror類似。如果指定的文件到達文件結尾,它將返回一個非0值。
行輸入和行輸出
標准庫函數fgets:
char *fgets(char *line, int maxline, FILE *fp)
從fp指向的文件中讀取下一個輸入行(包括換行符),並存放在line中,最多可讀取maxline-1個字符。讀取的行將以’\n’結尾保存到數組中。通過返回line,如果遇到文件結果或發生錯誤,返回NULL。
輸入函數fputs將一個字符串(不需要包含換行符)寫入到一個文件中,如果發生錯誤,返回EOF,否則返回一個非負值。
庫函數gets和puts的功能和fgets和fputs類似,但是它們是對stdin和stdout進行操作。另外,gets函數在讀取字符串時將刪除結尾的換行符(‘\n’),而puts函數在寫入字符串時將在結尾添加一個換行符。
ANSI標准規定,ferror在發生錯誤時返回非0值,而puts在發生錯誤時返回EOF,其他情況返回一個非負值。
其他函數
字符串操作函數
strcat(s, t) 將t指向的字符串連接到s指向的字符串的末尾
strncat(s, t, n) t指向的字符串的n個字符連接到s的末尾
strcmp(s, t) 根據字符串值比較大小
strnncmp(s, t, n) 只在前n個字符中比較
strcpy(s, t) t復制到s
strncpy(s, t, n) t中前n個字符復制到s
strlen(s, t)
strchr(s, c) s指向的字符串中查找c,返回第一此出現的位置的指針或者NULL
strrchr(s, c) 返回最后一次出現的位置的指針或NULL
字符類別測試和轉換函數
isalpha(c) 是字母返回非0值,否則返回0
Isupper(c)
islower(c)
isdigit(c) 是數字返回非0值,否則返回0
isalnum(c) 是字母或數字返回非0值,否則返回0
isspace(c)
toupper(c)
tolower(c)
Ungetc函數
int ungetc(int c, FILE *fp)
將字符c寫回到文件中,成功返回c,否則返回EOF。每個文件只能接收一個回寫字符。
命令執行函數
system(char *s)函數執行包含在字符串s中的命令,然后繼續執行當前程序。S的內容在很大程度上和所用的操作系統有關。
存儲管理函數
函數malloc和calloc用於動態的分配存儲塊。
void *malloc(size_t n)
分配成功時,malloc函數返回一個指針,指向的空閑空間足以容納n個指定長度對象的數組,否則返回NULL。存儲空間被初始化為0。
free(p)函數釋放p指向的存儲空間,p是通過調用malloc或calloc函數得到的指針。存儲空間的釋放順序沒有限制。如果釋放的不是通過malloc和calloc得到的指針所指向的存儲空間,會是一個嚴重錯誤。
使用已經釋放的空間也是錯誤的。
隨機數發生器函數
函數rand()生成介於0和RAND_MAX之間的為隨機整數序列。RAND_MAX是在頭文件<stdlib.h>中定義的符號常量。
函數srand()設置rand函數的種子數。
UNIX系統接口
文件描述符
UNIX操作系統中,所有的外圍設備都被看做是文件系統中的文件,因此,所有的輸入輸出都要通過讀文件或寫文件完成。
通常情況下,在讀寫文件之前,必須先通知系統,這個過程為打開文件。如果一切正常,操作系統將向程序返回一個小的非負整數,稱為文件描述符。任何時候對文件的輸入輸出都是通過文件描述符標識文件。系統負責維護已打開文件的所有信息,用戶程序只能通過文件描述符引用文件。
因為大多數輸入輸出都是通過鍵盤和顯示器實現的,所以UNIX做了特別的安排。當shell運行一個程序時,將打開3個文件,文件描述符分別是0,1,2,依次表示標准輸入,標准輸出和標准錯誤。
任何情況下,文件賦值的改變都不是由程序完成的,而是由shell完成的。
低級IO--read和write
輸入與輸出是通過read和write系統調用實現的。C語言程序中,可以通過函數read和write訪問這兩個系統調用。兩個函數都有三個參數,第一個是文件描述符,第二個是程序中存放讀寫的數據的字符數組,第三個是要傳輸的字節數。
兩個調用返回實際傳輸的字節數。讀文件時,函數的返回值可能會小於請求的字節數。返回值0表示到達文件結尾;-1表示發生錯誤。寫文件時返回的是實際寫入的字節數,返回值與請求數不等,說明發生錯誤。
一次調用中,讀出或寫入的數據的字節數可以為任意大小。最常用的值為1,或是類似於1024或4096這樣的與外圍設備的物理塊大小相對應的值。值大的話效率更高,因為系統調用次數少。
如果要在包含頭文件<stdio.h>的情況下編譯自定義的getchar函數,就需要用#udef預處理指令取消名字getchar的宏定義,因為在頭文件中,getchar是以宏方式實現的。
open、creat、close和unlink
處理默認的標准輸入、標准輸出和標准錯誤文件外,其他文件都必須在讀或寫之前顯式地打開。系統調用open和create用於實現該功能。
open與fopen類似,但是open返回一個文件描述符,僅僅是一個int類型的數值,而fopen返回一個文件指針。發生錯誤時,open將返回-1。
使用open打開一個不存在的文件將導致錯誤。可以用create系統調用創建新文件或覆蓋已有的舊文件。
如果create成功創建了文件,它將返回一個文件描述符,否則返回-1.如果此文件已存在,creat將把該文件的長度截斷為0,從而丟棄原來已有的內容。使用create創建一個已存在的文件不會導致錯誤。
一個程序同時打開的文件數是有限的(通常為20)。相應的,如果一個程序需要同時處理許多文件,那么它必須重用文件描述符。函數close(int fd)用來斷開文件描述符和已打開文件之間的連接,並釋放此文件描述符,以供其他文件使用。Close函數與標准庫中的fclose函數相對應,但它不需要清洗(flush)緩沖區。如果程序通過exit函數退出或從主程序中返回,所有打開的文件將被關閉。
函數unlick(char *name)將文件name從文件系統中刪除,對應於標准庫函數remove。
隨機訪問--lseek
系統調用lseek可以在文件中任意移動位置而不實際讀寫任何數據:
long lseek(int fd, long offset, int origin)
將文件描述符為fd的文件的當前位置設置為offset,offset是相對於origin指定的位置而言的。隨后進行的讀寫操作將從此位置開始。Origin的值可以是0,1,2,分別指定offset從文件開始、從當前位置和從文件結束出開始算起。
使用lseek系統調用時,可以將文件視為一個大數組,其代價是訪問速度會慢一點。
lseek系統調用返回一個long類型的值,此值表示文件的新位置,若發生錯誤,則返回-1。標准庫函數fseek與系統調用lseek類似,但是fseek第一個參數是FILE *類型,且在錯誤時返回一個非0值。
2018-11-11 01:21:37