[C程序設計語言]第五部分,c程序設計第五部分
聲明:原創作品,轉載時請注明文章來自
SAP師太博客,並以超鏈接形式標明文章原始出處,否則將追究法律責任!
UNIX系統接口... 1
性能忠告... 7
可移植性忠告... 8
函數原型... 8
UNIX系統接口
文件描述符
在UNIX操作系統中,所有的外圍設備(包括鍵盤和顯示器)都被看作是文件系統中的文件,因此,所有的輸入/輸出都要通過讀文件或寫文件完成。
因為大多數的輸入/輸出是通過鍵盤和顯示器來實現的,為了方便起見,UNIX對此做了特別的安排。當命令解釋程序(即“shell”)運行一個程序的時候,它將打開3個文件,對應的文件描述符分別為0, 1, 2,依次表示標准輸入,標准輸出和標准錯誤。如果程序從文件0中讀,對1和2進行寫,就可以進行輸/輸出而不必關心打開文件的問題。
程序的使用者可通過<和>重定向程序的I/O:
prog <輸入文件名 >輸出文件名
這種情況下,shell把文件描述符0和l的默認賦值改變為指定的文件。通常,文件描述符2仍與顯示器相關聯,這樣,出錯信息會輸出到顯示器上。在任何悄況下,文件賦值的改變都不是程序完成的,而是由shell完成的。只要程序使用文件0作為輸入,文件l和2作為輸出,它就不會知程序的輸入從哪裡來,並輸出到哪裡去。
底層I/O — read和write
輸入與輸出是通過現read和write系統調用實現的。在C語言程序中,可以通過函數read和write訪問這兩個系統調用。這兩個函數中,第一個參數是文件描述符,第二個參數是程序中存放讀和寫的數據的字符數組,第三個參數是要傳輸的字節數:
int n_read = read(int fd, char *buf, int n); int n_written = write(int fd, char *buf, int n); 每個調用返回實際傳輸(讀或寫)的字節數。在讀文件時,函數的返回值可能會小於請求的字節數,如果返回值為0,則表示已到達文件的結尾;如果返回值為-1,則表示發生了某種錯誤。在寫文件時,返回值是實際寫入的字節數,如果返回值與請求寫入的字節數不相等,則說明發生了錯誤。
在一次調用中,讀出或寫入的數據的字節數可以為任意大小。最常用的值為1,即每次讀出或寫入1個字符(無緩沖),或是類似於1024~4096這樣的與外圍設備的物理塊大小相應的值。用更大的值調用該函數可以獲得更高的效率,因為系統調用的次數減少了。
#include"syscalls.h"
main() /*將輸入復制到輸出*/
{
char buf[BUFSIZ];
int n;
while ((n =
read(0, buf, BUFSIZ)) > 0)
write(1, buf, n);
return 0;
}
本章中的程序都將包含該頭文件syscalls.h,不過,該文件的名字不是標准的。參數BUFSIZ已經在syscalls.h頭文件中定義了。對於所使用的操作系統來說,該值是一個較合適的數值。如果文件大小不是BUFSIZ的倍數,則對read的某次調用會返回一個較小的字節數,write再按這個字節數寫。
構造類似於getchar高級函數: 從標准輸入讀入一個字符來實現無緩沖輸入(第一個版本)
#include"syscalls.h"
int getchar(
void) {
char c;
return (
read(0, &c, 1) == 1) ? (
unsignedchar) c : EOF;
}
第二個版本:一次讀入一組字符,但每次只輸出一個字符
#include"syscalls.h"
int getchar(
void) {
staticchar buf[BUFSIZ];
staticchar *bufp = buf;
staticint n = 0;
if (n == 0) { /* buffer is empty */
n =
read(0, buf,
sizeof buf);
bufp = buf;
}
return (n >= 0) ? (
unsignedchar) *bufp++ : EOF;
}
如果要在包含頭文件始<stdio.h>的情況下編譯這些版本的getchar函數,就有必要用#-undef預處理指令取消名字getchar的宏定義,因為在頭文件中,getchar是以宏方式實現的。
open, creat, close和unlink
除了默認的標准輸入、標准輸出和標准錯誤文件外,其它文件都必須在讀或寫之前顯式地打開。系統調用open與creat用於實現該功能。
open與前面討論的fopen相似,不同的是,前者返回一個文件描述符,它僅僅只是一個int類型的數值。而後者返回一個文件指針。如果發生錯誤,open將返回-l。
#include <fcntl.h>
int fd;
int open(char *name, int flags, int perms);
fd = open(name, flags, perms);
與fopen一樣,參數name是一個包含文件名的字符串,第二個參數flags是一個int類型的值,它說明以何種方式打開文件,主要的幾個值如下所示:
O_RDONLY 以只讀方式打開文件
O_WRONLY 以只寫方式打開文件
O_RDWR 以讀寫方式打開文件
在這裡討論的open第三個參數始終為0。
使用create創建或覆蓋已存在的舊文件:
int creat(char *name, int perms);
fd = creat(name, perms);
如果creat成功地創建了文件,它將返回一個文件描述符,否則返回-1。如果此文件已存在,creat將把該文件的長度截斷為0,從而丟棄原先己有的內容使用creat創建一個已存在的文件不會導致錯誤
如果要創建的文件不存在,則creat用參數pems指定的權限創建文件。在UNIX文件系統中,每個文件對應一個9比特的權限信息,它們分別控制文件的所有者、所有者組和其他成員對文件的讀、寫和執行訪問。因此,通過一個3位的八進制數就可方便地說明不同的權限,例如,0755(八進制)說明文件的所有者可以對它進行讀、寫和執行操作,而所有者組和其他成員只能進行讀和執行操作。
下面通過一個簡化UNIX程序cp說明creat的用法,該程序將一個文件復制到另一個文件。目標文件的權限不是通過復制獲得的,而是重新定義的:
#include<stdio.h>
#include<fcntl.h>
#include<stdarg.h>
#include"syscalls.h"
#define PERMS 0666 /* RW for owner, group, others */
void error(
char *, ...);
/* cp: copy f1 to f2 */
main(
int argc,
char *argv[]) {
int f1, f2, n;
char buf[BUFSIZ];
if (argc != 3)
error("Usage: cp from to");
if ((f1 =
open(argv[1], O_RDONLY, 0)) == 1)
error("cp: can't open %s", argv[1]);
if ((f2 =
creat(argv[2], PERMS)) == 1)
error("cp: can't create %s, mode %03o", argv[2], PERMS);
while ((n =
read(f1, buf, BUFSIZ)) > 0)
if (
write(f2, buf, n) != n)
error("cp: write error on file %s", argv[2]);
return 0;
}
void error(
char *fmt, ...) {
va_list args;
va_start(args, fmt);
fprintf(stderr, "error: ");
vprintf(stderr, fmt, args);
fprintf(stderr, "\n");
va_end(args);
exit(1);
}
該程序創建的輸出文件具有固定的權限0666,也可使用stat系統調用,可以獲得一個已存在文件的模式,並將此模式賦值給它的副本。標准庫函數
vprintf數與printf函數類似,所不同的是,它用一個參數取代了變長參數表,因此參數通過調用va_start宏進行初始化。同樣,
vfprintf和
vsprintf函數分別與fprintf和sprintf函數類似。
函數
close (int fd)用來斷開文件描述符和已打開文件之間的連接,並釋放此文件描述符,以供其它文件使用。close函數與標准庫中的fclose函數相對應,但它不需要清除(flush)緩沖區。
函數
unlink(char *name)將文件name從文件系統中刪除,它對應於標准庫函數remove
隨機訪問-lseek
系統調用lseek可以在文件中任意移動位置而不實際讀寫任何數據:
long lseek(int fd, long offset, int origin); 將文件描述符為fd的文件的當前位置設置為offset,其中,offset是相對於orgin指定的位置而言的。隨後進行的讀寫操作將從此位置開始,origin的值可以為0、1或2,分別用於指定offset從文件開始、從當前位置或從文件結束處開始算起。例如,為了向一個文件的尾部添加內容(在UNIX shell程序中使用重定向符>>或在系統調用fopen中使用參數“a”):
lseek(fd, 0L, 2); #include"syscalls.h"
/*get: read n bytes from position pos 從文件的任意位置讀入任意數目的字節,它返回讀入的字節數,若發生錯誤返回-1*/
int get(
int fd,
long pos,
char *buf,
int n) {
if (
lseek(fd, pos, 0) >= 0) /* get to pos 先移動到指定的位置*/
returnread(fd, buf, n); /* 再讀取*/
else return 1;
}
lseek系統調用返回long類型的值,此值表示文件的新位置,若發生錯誤,則返回-1。標准庫函數fseek與系統調用lseek類似,不同的是,前者第一個參數為FILE *類型,並且發生錯誤時返回一個非0值。
實例-fopen和getc函數的實現
標准庫中的文件不是通過文件描述符描述的,而是使用文件指針描述的文件指針是一個指向包含文件各種信息的結構的指針,該結構包含下列內容:一個指向緩沖區的指針,通過它可以一次讀入文件的一大塊內容;一個記錄緩沖區中剩余的字符數的計數器;一個指向緩沖區中下一個字符的指針;文件描述符;描述讀/寫模式的標志;描述錯誤狀態的標志等。
——相關宏定義—— #define NULL 0
#define EOF (1)
#define BUFSIZ 1024
#define OPEN_MAX 20 /* 一次允許打開的最大文件數 */
typedefstruct _iobuf {
intcnt; /* 剩余字符數 */
char *ptr; /* 下一個字符的位置 */
char *base; /* 緩沖區的位置 */
intflag; /* 文件訪問模式與出錯標示 */
intfd; /* 文件描述符 */
} FILE;
externFILE _iob[OPEN_MAX]; //用來存儲打開的文件結構數據
#define stdin (&_iob[0]) //第一個用作標准輸入
#define stdout (&_iob[1]) //第二個用作標准輸出
#define stderr (&_iob[2]) //第三個用作錯誤輸出
enum _flags {
/* 使用了5位來標示文件的訪問方式與文件訪問時出錯標示: 00 011 111,
以位來標示便於使用位運算符&來判斷文件的打開後所處的狀態,如判斷打開
文件是否出錯:(fp -> flag & (_EOF | _ERR)) != 0 */
_READ = 01, /* 以讀方式打開文件 */
_WRITE = 02, /* 以寫方式打開文件 */
_UNBUF = 04, /* 不對文件進行緩沖 */
_EOF = 010, /* 已到文件尾 */
_ERR = 020 /* 該文件發生錯誤 */
};
int _fillbuf(FILE *);
int _flushbuf(
int, FILE *);
#define feof(p) ((p)->flag & _EOF) != 0)
#define ferror(p) ((p)->flag & _ERR) != 0)
#define fileno(p) ((p)->fd)
/* 先將計數器cnt減一,再將指針移到下一個位置,然後返回字符,
* 如果小於0,則就填允緩沖區,重新初始化結構的內容,並返回
* 一個字符*/
#define getc(p) (--(p)->cnt >= 0 \
? (
unsignedchar) *(p)->ptr++ : _fillbuf(p))
/* 緩沖區滿後刷新緩沖到文件中*/
#define putc(x,p) (--(p)->cnt >= 0 \
? *(p)->ptr++ = (x) : _flushbuf((x),p))
#define getchar() getc(stdin)
#define putcher(x) putc((x), stdout)
——fopen實現—— #include<fcntl.h>
#include"syscalls.h"
#define PERMS 0666 /* RW for owner, group, others */
FILE *
fopen(
char *name,
char *mode) {
int fd;
FILE *fp;
if (*mode != 'r' && *mode != 'w' && *mode != 'a')
return NULL;
for (fp = _iob; fp < _iob + OPEN_MAX; fp++)
if ((fp -> flag & (_READ | _WRITE)) == 0)
break; /* 尋找一個空位置,即讓fp指向_iob數據還未使用的元素
,如果結構數據未賦值,則_READ | _WRITE位所在的
位為0,則(_READ | _WRITE)就為0 */
if (fp >= _iob + OPEN_MAX) /* 最大文件個數超限 */
return NULL;
if (*mode == 'w')
fd =
creat(name, PERMS);
elseif (*mode == 'a') {
//如果以寫的方式打開文件後返回的文件描述符為1(標准輸出),
//則重新創建一個文件
if ((fd =
open(name, O_WRONLY, 0)) == 1)
fd =
creat(name, PERMS);
lseek(fd, 0L, 2);//定位到文件結尾
}
else fd =
open(name, O_RDONLY, 0);
if (fd == -1)
/* 不能訪問文件 */
return NULL;
/* 正常打開文件後,將對應的文件結構數據存儲到_iob結構數組中相應空閒元素的位置中,
_iob結構數組主要的是一個全局的,用來記錄當前程序已打開文件,這樣就可以限制打開的
最多文件數*/
fp -> fd = fd;
fp -> cnt = 0;
fp -> base = NULL;
fp -> flag = (*mode == 'r') ? _READ : _WRITE;
return fp;
}
——緩沖函數的實現—— #include"syscalls.h"
/* _fillbuf: 分配並填允緩沖區 */
int _fillbuf(FILE *fp) {
int bufsize;
if ((fp -> flag & (_READ | _EOF | _ERR)) != _READ)
return EOF;
bufsize = (fp -> flag & _UNBUF) ? 1 : BUFSIZ;
if (fp -> base == NULL) /* 還未分配緩沖區時 */
if ((fp -> base = (
char *)
malloc(bufsize)) == NULL)
return EOF; /* 不能分配緩沖區 */
fp -> ptr = fp -> base;
fp -> cnt =
read(fp -> fd, fp -> ptr, bufsize);
if (fp -> cnt < 0) {
if (fp -> cnt == 1)
fp -> flag |= _EOF;
else fp -> flag |= _ERR;
fp -> cnt = 0;
return EOF;
}
return (
unsignedchar) *fp -> ptr++;
}
/* 最後我們還必須定義和初始化數組_iob中的stdin, stdout, stderr*/
FILE _iob[OPEN_MAX] = {
{ 0, (
char *) 0, (
char *) 0, _READ, 0},
{ 0, (
char *) 0, (
char *) 0, _WRITE, 1},
{ 0, (
char *) 0, (
char *) 0, _WRITE | _UNBUF, 2}/* 標准錯誤輸出不用緩沖的 */
};
性能忠告
在內存緊張和要求運行速度的情況下,最好使用數值范圍比較小的整數數據類型。
在聲明時就賦值可能提高程序運行時間。
c +=5 比 c = c + 5要快,原因是前者中c只分析一次,而後者中c要分析兩次。
在使用運算符&&的表達式中,把最可能為假的條件放在最左邊。在使用運算符||的表達式中,把最可能為真的條件放在最左邊,這樣能減少程序的執行時間。
可移植性忠告
使用符號常量EOF而不是使用-1可使用程序具有更好的可移植性。ANSI標准規定EOF是一個負整數,但不是必須為-1。因此,在不同的系統中,EOF可能有不同的值。
因為int類型的整數值在不同的系統中具有不同的范圍,所以如果要處理 -32768 ~ +32767范圍之外的整數值並且要能夠使用程序在不同的計算機系統中運行,使用long數據類型。
函數原型
函數原型告訴編譯器函數返回的數據類型,函數所要接收的參數個數,參數類型和參數順序,編譯器用函數原型校驗函數調用。
int maxinum(int, int, int); 這裡省略了參數名,也可以帶上參數名
函數原型另一個重要特點是強制轉換參數類型,如標准庫函數sqrt函數原型指定了參數為double類型,但在調用時可以傳遞整型,這是因為編譯器根據函數原型,調用前將整型轉換為double型了。如果不遵循C語言的類型“提升規則”,參數類型轉換可能會導致不正確的結果。