程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> 深入理解C語言

深入理解C語言

編輯:關於C

與文件I/O圍繞文件描述符操作不同,標准I/O的操作是圍繞流進行的。

流:

對於流,《C和指針》裡有一段解釋得很好:

ANSI C進一步對I/O的概念進行了抽象。就C程序而言,所有的I/O操作只是簡單地從程序移進或移出字節的事情。因此,毫不驚奇的是,這種字節流便被稱為流(stream)。程序只需要關心創建正確的輸出字節數據,以及正確地解釋從輸入讀取的字節數據。特定I/O設備的細節對程序員是隱藏的。

TCPL Appendix B.1中這麼解釋:

A stream is a source or destination of data that may be associated with a disk or other peripheral.(流是一個可能與硬盤或者其他設備關聯的數據的源或者目的地)

簡單地說,流是對信息的一種抽象。C系統在處理文件(文本文件和二進制文件)時,並不區分類型,都看成是字符流,按字節進行處理。
輸入輸出字符流的開始和結束只由程序控制而不受物理符號(如回車符)的控制。

流有最小的信息單元就是二進制位,含有最小的信息包就是字節,C標准庫提供兩種類型的流:二進制流(binary stream)和文本流(text stream)。二進制流是有未經處理的字節構成的序列;文本流是由文本行(每行有0個或多個字符,並以'\n'結束)組成的序列。注意在UNIX中,並不區別兩種流。


當一個程序啟動時,,標准輸入、輸出、出錯三個流就已經被自動打開,並對應到默認的物理終端。這三個標准I/O流通過預定義(stdio.h)文件指針stdin,stdout,stderr加以引用當一個進程正常終止時(直接調用exit(),或從main返回)所有打開的標准I/O流都會被關閉,所有帶未寫緩沖數據的I/O流都會被沖洗。

PS:在main()中return(expr)等價於exit(expr),而exit則調用fclose()關閉每個文件描述符並刷洗對應緩存。

在Linux的應用程序中,通常用文件描述符0,1,2與標准輸入,標准輸出,標准出錯輸出相關聯。為符合POSIX規范,在中,0,1,2分別被替換成常量符號STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。


緩沖區:

(詳細內容可以參考APUE3.9和5.4,本段純屬摘抄)

標准I/O提供緩存的目的是盡可能減少使用read和write調用的數量(系統調用比普通函數調用開銷大)。它也對每個I/O流自動地進行緩存管理,避免了應用程序需要考慮這一點所帶來的麻煩。

標准I/O提供了三種類型的緩存:
(1) 全緩存。在這種情況下,當填滿標准I/O緩存後才進行實際I/O操作。對於駐在磁盤上的文件通常是由標准I/O庫實施全緩存的。
(2) 行緩存。在這種情況下,當在輸入和輸出中遇到新行符時,標准I/O庫執行I/O操作。這允許我們一次輸出一個字符(用標准I/O fputc函數),但只有在寫了一行之後才進行實際I/O操作。
(3) 不帶緩存。標准I/O庫不對字符進行緩存。如果用標准I/O函數寫若干字符到不帶緩存的流中,則相當於用write系統調用函數將這些字符寫至相關聯的打開文件上。


標准出錯流stderr通常是不帶緩存的,這就使得出錯信息可以盡快顯示出來,而不管它們是否含有一個新行字符。
ANSI C要求下列緩存特征:
(1) 當且僅當標准輸入和標准輸出並不涉及交互作用設備時,它們才是全緩存的。
(2) 標准出錯決不會是全緩存的

但是,這並沒有告訴我們如果標准輸入和輸出涉及交互作用設備時,它們是不帶緩存的還是行緩存的,以及標准輸出是不帶緩存的,還是行緩存的。
SVR4和4.3 + BSD的系統默認使用下列類型的緩存:
? 標准出錯是不帶緩存的。
? 如若是涉及終端設備的其他流,則它們是行緩存的;否則是全緩存的。


可以通過下面的函數改變緩存類型(APUE5.4):

void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
這些函數必須在流打開之後、但是未對流做任何操作之前被調用

參數buf通常指向一個長度為BUFSIZ的緩沖區,BUFSIZ在stdio.h中定義,可自行輸出查看

stdio.h:	#ifndef BUFSIZ
		# define BUFSIZ _IO_BUFSIZ
libio.h:	#define _IO_BUFSIZ _G_BUFSIZ
_G_config.h:	#define _G_BUFSIZ 8192
如要關閉緩沖,將buf置為NULL

Liunx上的默認情況是,當標准輸入輸出連接終端時是行緩沖的,緩沖區大小1024字節,重定向到普通文件時,他們變為全緩沖(APUE 5.12 程序5.3提供查看I/O相關信息的方法)


強制沖洗一個流

int fflush(FILE *fp)

使該流所有未寫數據傳送至內核。如fp為NULL,將使所有輸出流被清洗。

應當注意的是:fflush(NULL)並不能有效地清空輸入緩存。後面詳細討論


常用I/O函數:

流打開:

FILE * fopen ( const char * filename, const char * mode );
FILE * freopen ( const char * filename, const char * mode, FILE * stream );
常用freopen進行輸入輸出重定向。

單字符讀寫:

int getc(FILE *fp)
int fgetc(FILE *fp)
int getchar(void)
getchar等價於getc(stdin)。前兩個函數區別在於,getc可被實現為宏,意味著fgetc調用時間略長。

不管是出錯還是到達文件尾端,三個函數都返回-1.

在大多數實現中,FILE維護了兩個標志:出錯標志和文件結束標志。可用下面三個函數判斷流是出錯還是結束,最後一個函數是清除兩標志:

int ferror(FILE *fp)
int foef(FILE *fp)
void clearerr(FILE *fp)

還有一個神奇的函數,可以把字符壓送回流中:
int ungetc(int c, FILE *fp)
注意不能回送EOF

類似地,輸出函數:

int putc(int c, FILE *fp)
int fputc(int c, FILE *fp)
int putchar(int c)
putchar(c)等價於putc(c,stdout),putc可被實現為宏。

通常為了避免過多的函數調用開銷,putchar和getchar都被實現為宏。


行讀寫函數:

char * fgets ( char * str, int num, FILE * stream );
它會讀取不超過num-1個字符,然後在末尾加上結束符 '\0' ,或者遇到換行符結束輸入,同時換行符也被傳入。

另一個函數:

char * gets(char* buf);

由於存在緩沖區溢出漏洞,不推薦使用。

相應地,輸出

int fputs(const char * str, FILE * fp);
int puts(const char *char);
fputs()將一個以NULL終止的字符串寫到指定的流,終止符NULL不寫出。
puts()雖然安全,但是他的不方便在於,每次將換行符寫到輸出。

因此,我們貫徹這樣的方針,堅持使用fgets和fputs,並自己處理換行符。


各I/O函數的效率對比可參看APUE5.9


格式化I/O:

格式化輸出:

int printf ( const char * format, ... );
int fprintf (FILE *fp, const char * format, ... );
int snprintf (char *buf, size_t n, const char * format, ... );

注意參數轉換說明中 %[flags][fldwidth][lenmodifier]convtype,寬度和精度字段可被置為*,而後用一個整形參數指定其值。

printf的實現:

printf是C中為數不多的變參函數之一,主要通過stdarg.h中的一系列宏來對參數列表進行處理。其源碼實現可以參看:點擊打開鏈接和點擊打開鏈接

使用變參函數機制,簡單模擬printf

#include
#include
#include
#include

void simon_printf(char *fmt, ...)
{
	char buf[10];
	char *p = fmt;
	char c_tmp, *s_tmp;
	int i_tmp;
	double f_tmp;

	va_list  ap;
	va_start(ap, fmt);

	while (*p)
	{
		if (*p != '%')
		{
			putchar(*p++);
			continue;
		}
		else 
		{
			switch (*++p)
			{
				case 'd':
					{
						i_tmp = va_arg(ap, int);
				//		sprintf(buf, "%d", i_tmp);
				//			write(STDOUT_FILENO, buf, strlen(buf));
						printf("%d", i_tmp);
						break;
					}
				case 'f'://float在內部被提升為double
					{
						f_tmp = va_arg(ap, double);
						printf("%f", f_tmp);
						break;
					}
				case 'c'://char在內部被提升為int
					{
						i_tmp = va_arg(ap, int);
						printf("%c", i_tmp);
						break;
					}
				case 's':
					{
						for(s_tmp = va_arg(ap, char*); *s_tmp; s_tmp++)
							printf("%c", *s_tmp);
						break;
					}
			}
			p++;
		}
	}

	va_end(ap);
}

int main()
{
	int a = 1;
	float b = 2.0;
	char c = 'a';
	char *str = {"test"};

	simon_printf("This is a test Message:\n int:%d\n float:%f\n string: %s\n char:%c\n ", a, b, str, c);
	return 0;
}


格式化輸入:

int scanf(const char *format, ...);
int fscanf(FILE *fp, const char *format, ...);
int sscanf(const char *buf, const char *format, ...);

scanf中*表示抑制,不把該輸入賦值給對應變量,即跳過。

scanf()還有一些正則用法:[]表示輸入字符集,可以使用連字符表示范圍,scanf() 連續吃進集合中的字符並放入對應的字符數組,直到發現不在集合中的字符為止。用字符 ^ 可以說明補集。把 ^ 字符放為掃描集的第一字符時,構成其它字符組成的命令的補集合。

通常並不推薦使用scanf()的正則用法,用法復雜, 容易出錯。編譯器作語法分析時會很困難, 從而影響目標代碼的質量和執行效率。

關於輸入緩沖區清空:

1)fflush(NULL)

fflush的定義說得很清楚了,這種用法導致的結果不確定

If the given stream was open for writing (or if it was open for updating and the last i/o operation was an output operation) any unwritten data in its output buffer is written to the file.
If stream is a null pointer, all such streams are flushed.
In all other cases, the behavior depends on the specific library implementation. In some implementations, flushing a stream open for reading causes its input buffer to be cleared (but this is not portable expected behavior).
The stream remains open after this call.
When a file is closed, either because of a call to fclose or because the program terminates, all the buffers associated with it are automatically flushed.

如果stream指向輸出流或者更新流(update stream),並且這個更新流最近執行的操作不是輸入,那麼fflush函數將把任何未被寫入的數據寫入stream指向的文件(如標准輸出文件stdout)。
fflush(NULL)清空所有輸出流和上面提到的更新流。
否則,fflush函數的行為是不確定的。取決於編譯器,某些編譯器(如VC6)支持用 fflush(stdin) 來清空輸入緩沖,而gcc就不支持。

2)setbuf(stdin, NULL);
setbuf(stdin, NULL);是使stdin輸入流由默認緩沖區轉為無緩沖區,在沒有特殊要求的情況下還是適用的


3)int c;
while((c = getchar()) != '\n' && c != EOF);
由代碼知,不停地使用getchar()獲取緩沖區中字符,直到獲取的字符c是換行符’\n’或者是文件結尾符EOF為止。這個方法可以完美清除輸入緩沖區,並且具備可移植性。


4)scanf("%[^\n]%*c");
這裡用到了scanf格式化符中的“*”,即賦值屏蔽;“%[^集合]”,匹配不在集合中的任意字符序列。這也帶來個問題,緩沖區中的換行符’\n’會留下來,需要額外操作來單獨丟棄換行符。


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved