第十一章 高級指針話題 第十二章 預處理器 第十三章 輸入/輸出函數
int i;
int *pi;
int **ppi;
變量i 是一個整數,pi是一個指向整型指針,ppi是一個指向pi的指針,所以它是一個指向整型的指針的指針。
ppi = π這條語句把ppi初始化為指向變量pi。
*ppi = &i;這條語句把pi(通過ppi間接訪問)初始化為指向變量i。經過上面兩條語句之後:
現在,下面各語句具有相同的效果(都是將變量i的值賦值為10):
i = 10;
*pi = 10;
**ppi = 10;
經過上面賦值語句後,變量的內存關系結構:
為什麼需要間接的訪問呢?這是因為簡單的賦值有進並不可行,比如在一個函數裡想修改調用它的函數裡的變量,則只能使用指針的形式來傳遞,對該指針進行間接訪問操作可以訪問需要修改的變量。上面的ppi可以改變i(通過**ppi修改)與pi(通過*ppi修改)兩個變量的值。
int* f,g; 它並沒有聲明兩個指針,盡管它們之間存在空白,但星號是作用於f的,只有f才是一個指針,g只是一個普通的整型變量。
int (*f)(); f是一個函數指針:
int fun() {
return 1;
}
int main(int argc, char **argv) {
int (*f)();
f=fun;
printf("%d", (*f)());//1
}
如果作為函數的參數類型,則可能省略名稱f,如:
void bubble(int *, const int, int(*)(int, int));
int (*f)(int); 是一種類型,定義了一個函數指針類型,指針變量名為f
int (*f())(int); 是一個函數原型,現在的f是一個函數名,這個函數的返回值是一個函數指針
指向函數的指針包含了該函數在內存中的地址。函數名實際上是完成函數任務的代碼在內存中的起始地址,就像數組名一樣,是第一個元素在內存中的地址。
取數組的地址時,前面不需要加上&運算符,數組名就是地址,函數也一樣,函數名就是函數地址,也不需要在函數名前加上&運算符。
指針的通用類型為void *,任何類型的指針都可以轉換為void *,並且在將它轉換回原來的類型時不會丟失信息。
int (*comp) (void *, void *)參數類型表明comp是一個指向函數的指針,該函數具有兩個void*類型的參數,其返回值類型為int。不能去掉 comp 外層的括號,如果去掉了,則表示comp是一個函數,該函數返回一個指向int類型的指針。
int f(int);
int (*pf)(int) = &f;
初始化表達式中的&操作符是可選的,因為函數名被使用時總是由編譯器把它轉換為函數指針。&操作符只是顯式地說明了編譯器將隱式執行的任務(就像數組名一樣,當數組名與&一起使用時,就不會將數組看作是一個常量指針,而是看作一個普通的變量)。我們可以使用三種方式來調用:
f(25);
(*pf)(25);
pf(25);
函數指針數組:指向函數的指針數組:void (*f[3])(int) = { 函數1, 函數2, 函數3 };,函數的調用方式如下:(*f[choice])(choice)
int *(*f[])(int, float);聲明了一個指針數組,每個指針元素所指向的類型是返回值為整型指針的函數,而且這個函數還帶兩個參數類型。
函數指針最大的用處就是回調。函數指針像Java中的接口一樣,可以動態的改變其運行時性為。下面一個冒泡排序,它會根據用戶的選擇來決定調用升序還是降序函數:
#include <stdio.h>
#define SIZE 10
void bubble(int *, const int, int(*)(int, int));
int ascending(const int, const int);
int descending(const int, const int);
//主程序
int main(int argc, char **argv) {
int a[SIZE] = { 2, 6, 4, 8, 10, 12, 89, 68, 45, 37 };
int counter, order;
printf("輸入1使用升序排序,2使用降序:");
scanf("%d", &order);
for (counter = 0; counter < SIZE - 1; ++counter) {
printf("%4d", a[counter]);
}
if (order == 1) {
bubble(a, SIZE, ascending);
printf("\n使用升序排序結果:");
} else {
bubble(a, SIZE, descending);
printf("\n使用降序排序結果:");
}
for (counter = 0; counter < SIZE - 1; ++counter) {
printf("%4d", a[counter]);
}
return 0;
}
//冒泡排序算法
void bubble(int * work, const int size, int(*compare)(int, int)) {
int pass, count;
void swap(int *, int *);
for (pass = 1; pass <= size - 1; pass++) {
for (count = 0; count < size - 2; ++count) {
//動態的調用函數,也可像平時調用函數一樣直接調用,
//如:compare(work[count], work[count + 1]),
//但最好像下面這樣調用,這樣可以很清楚的知道compare
//是一個函數指針,而不是一個普通的函數
if ((*compare)(work[count], work[count + 1])) {
swap(&work[count], &work[count + 1]);
}
}
};
}
//交換
void swap(int *element1Ptr, int * element2Ptr) {
int temp;
temp = *element1Ptr;
//將element2Ptr指向的變量的值賦給element1Ptr指向的變量
*element1Ptr = *element2Ptr;
*element2Ptr = temp;
}
//升序
int ascending(const int a, const int b) {
return b < a;
}
//降序
int descending(const int a, const int b) {
return b > a;
}
調用main函數時,會有兩個參數,第一個參數(argc)的值表示運行程序時命令行中參數的數目;第二個參數(argv)是一個指向字符串數組的指針,其中每個字符串對應一個參數。按照C語言的約定,argv[0]的值是啟動該程序的程序名,因此argc的值至少為1。如果argc的值為1,則說明程序名後面沒有命令行參數。另外,ANSI標准要求argv[argc]的值必須為空指針(即地址值為0)。
int main(int argc, char * argv[]) {...}
或者是:
int main(int argc, char ** argv) {...}
argv是一個指向字符串指針的指針:
/*
* 打印參數,路過程序名
*/
int main(int argc, char **argv) {
while (*++argv != NULL)
printf("%s\n", *argv);
return EXIT_SUCCESS;
}
當一個字符串常量出現於表達式上時,它的值是個指針常量。編譯器把這些指定字符的一份拷貝存儲在內存的某個位置,並存儲一個指向第1個字符的指針。另外,當數組名用於表達式中時,它的值也是指針常量。
"xyz" + 1,這個表達式的結果是一個指針,指向字符串中的第2個字符y
*"xyz",這個表達式的結果是一個字符:x
"xyz"[2],結果為一個字符:z
將某個數以16進制輸出:
void to_hex(unsigned int value) {
unsigned int quotient = value / 16;
if (quotient != 0) {
to_hex(quotient);
}
unsigned int remainder = value % 16;
if (remainder < 10) {
putchar(remainder + '0');
} else {
putchar(remainder - 10 + 'A');
}
}
妙用:現在使用最簡單的方法:
void to_hex(unsigned int value) {
unsigned int quotient = value / 16;
if (quotient != 0) {
to_hex(quotient);
}
putchar("0123456789ABCDEF"[value % 16]);
}
int main(int argc, char **argv) {
char * a[] = { "ab", "cd" };
char **p = a;
printf("%s\n", *a);//ab
printf("%s\n", *(p+1));//cd
printf("%s\n", *(a+1));//cd
}
有一些是標准庫已經定義好的符號,如:
__FILE__:進行編譯的源文件名;
__LINE__:文件當前行的行號;
__FUNCTION__:函數名;
__DATE__:文件被編譯的日期;
__TIME__:文件被編譯的時間;
__STDC__:如果編譯器遵循ANSI C,其值就是1,否則未定義;
宏的命名約定:宏的定義使全使用大寫
#define name stuff
#define指令把一個符號名與一個任意的字符序列聯系在一起,這些字符可能是一個字面值常量、表達式或者是程序語句。
替換文本並不僅限於數值字面常量,可以把任何文本替換到程序中,如:
#define reg register
#define do_forever for(;;);
#define define CASE break;case
如果替換文本很長,可以分成幾行來寫,除了最後一行外,每行的末尾都要加一個反斜槓,如:
#define DEBUG_PRINT printf("File %s line %d:"\
" x=%d, y=%d, z=%d",\
__FILE__,__LINE__,\
x, y, z)
int x = 1, y = x + 1, z = y + 1;
DEBUG_PRINT;//File ..\src\insert3.c line 84: x=1, y=2, z=3
#define甚至還可以定義一段代碼
#define定時最後面最好不要加上分號,語法規定是沒有分號的,如果加在了最後,則會將分號作為文本一起插入到替換位置,這樣有時會出現問題,如替換在未使用{}的if語句中時就可能會出問題。
被替換的文本中可以含有參數
#define name(parameter-list) stuff
參數列表的左括號必須與name緊鄰。如果兩者之間有任何空白存在,參數列表就會被解釋為stuff的一部分。
#define SQUARE(x) ((x)*(x))
在定義時要注意,文本中的每個參數都要使用括號括起來,最後整體也需要使用括號括起來,否則如果參數傳遞的不是單個常量,還是一個表達式時就會出問題。
宏參數和替換文本stuff可以包含其他#define定義的符號,但要注意,宏是不可以遞歸的。#define與宏的處理過程如下:
1、 在調用宏時,首先對參數進行檢查,看看是否包含了任何由#define定義的符號,如果是,它們首先被替換。
2、 替換文本隨後被插入到程序中原來文本的位置。如果文本中含有參數,則它們將被傳進的參數值所替換。
3、 最後,再次對結果文本進行掃描,看它是否包含了任何由#define定義的符號,如果是,就重復上面的過程。
C語言中相鄰字符串會自動連接起來:printf("%s","ab" "cd");//abcd
當預處理器在#define的替換文本中搜索#define定義的符號時,替換文本中的字符串常量的內容並不進行檢查(當然程序中的字符串常量更不會搜索了),如果想把宏參數插入到字符串常量中,可以使用以下兩種技巧:
第一種:根據相鄰字符串自動連接的特性,我們把一個字符串分成幾段,如
#define PRINT(format,value) \
printf("The value is " format "\n",value)
int x = 1;
PRINT("%d",x+3);//The value is 4
上面的 PRINT("%d",x+3) 會解釋成如下:printf("The value is " "%d" "\n",x+3)
所以此種情況下如果你傳遞的不是一個字符串常量時。
注:這種技巧只有當format宏參數傳入的確實是一個字符串時才能使用,因為此種情況下會嚴格將傳遞進來的參數原樣替換,如果原來是數字型而非常量字符串時,拼接就會有問題,因為數字未使用引號引起來,所以不能進行字符串拼接。
第二種:使用預處理器把一個宏參數轉換為一個字符串,可以使用#argument這種寫法就會將宏參數argument轉換為字符串,如
#define PRINT(format,value) \
printf("The value of " #value \
" is " format "\n",value)
int x = 1;
PRINT("%d",x+3);//The value of x+3 is 4
上面的會解釋成:
printf("The value of " "x+3" \
" is " "%d" "\n",x+3)
注:#value的作用是將參數表達式使用雙引號引起來,強制轉換成字符串。
如果此時實參中含有雙引號和 \ 時,都會被自動轉義,如:
#define dprint(expr) printf(#expr " = %s\n",expr)
dprint("str\"\\");//"str\"\\" = str"\
上面會解釋為:
printf("\"str\\\"\\\\\"" " = %s\n","str\"\\")
這可進一步看出 #argument 會將傳遞進來的內容使用引號引起來,即不管傳遞進來的內容是字符串常量、算術表達式、還是變量,都會轉換成字符串,而且如果傳遞進來的是字符串常量,則原字符串常量兩端的引號也會原樣顯示出來。
另外,使用 ## 可以把位於它兩邊的符號(非字符串常量,所以傳遞進來的只能是數字型的)連接成一個符號(前後的空白符將被刪除),它的用途是允許宏定義從分離的文本片段創建標識符:
#define ADD_TO_SUM(sum_number,value) \
_ ## sum_number ## sum += value
int _5sum = 1;
printf("%d", ADD_TO_SUM(5,25));//26
printf("%d", _5sum);//26
其作用是把值25加到變量 sum5 上,注意,這種連接必須要產生一個合法的標識符。
#與##區別
#會將傳遞進來的參數使用雙引號引起來,而##則不會,它會原樣將傳遞進來的內容(數字常量、字符串常量、符號、以及表達式)進行替換:
#define paster(n) printf("token" #n " = %d",token##n)
int token9 = 9;
paster(9);//token9 = 9
上面的 paster(9) 會解釋成:
printf("token" "9" " = %d",token9)
#define paster(n) printf("toke" #n " = %d",toke##n)
int token = 9;
paster(n);//token = 9
上面的 paster(n) 會解釋成:
printf("token" "n" "%d",token)
上面傳遞進去的 n 會在#前綴下會加上雙引號,而在##前綴下都不會加上雙引號
宏非常頻繁地用來執行簡單的計算,如:
#define MAX(a,b) ((a)>(b)?(a):(b))
這裡不使用函數而使用宏的好處:
1、 函數調用需要花時間(函數的調用與返回都需要時間),而宏則是在編譯時就已替換,運行速度會提高
2、 更重要的是,函數的參數必須聲明為一種特定的類型,所以在調用時只適合特定的類型。但是宏的參數是沒有類型的,它可以用於任何一種類型,比如這裡可以對整型、浮點型都可以進行比較。
3、 宏的不好之處是,每次在使用宏時,宏的定義代碼都會拷貝插入到程序中,除非宏非常短,否則使用宏可能會大幅度增加程序的長度。
還有一些根本不能使用函數來實現,比如你要將一個類型標識作為參數進行傳遞時,只能使用宏參數來實現,如:
#define MALLOC(n, type) ((type *)malloc((n) * sizeof(type)))
int *p = MALLOC(2,int);
上面的MALLOC會解釋為:((int *)malloc((2) * sizeof(int)))
#define MAX(a,b) ((a)>(b)?(a):(b))
int main(int argc, char **argv) {
int x = 5, y = 8, z = MAX(x++,y++);
printf("x=%d y=%d z=%d", x, y, z);//x=6 y=10 z=9
}
上面的MAX會解釋為:((x++)>(y++)?(x++):(y++))
移除一個宏的定義:
#undef name
如果一個現在的名字需要被重新定義,那麼首先必須用#undef進行移除
#if constant-expression
statements
#elif constant-expression
other statements ...
#else
other statements ...
#endif
constant-expression是一個常量表達式,它由預處理器進行求值。所謂常量表達式,就是說它組成該表達式的是一些字面值常量,或者是由一些#define定義的符號組成,或者兩者都有。
測試一個符號是否已被定義:
#if defined(symbol) 或 #ifdef symbol
#if !defined(symbol) 或 #ifndef symbol
上面每對定義的兩條語句是等價的,但#if 形式功能更強,因為有可能還包含其他條件:
#if x > 0 || defined(ABC) || defined(BCD)
#ifdef 與 #ifndef 也都是由 #endif 匹配來結尾
條件編譯也可以嵌套
#include指令處理方式:預處理器在編譯時會刪除這條指令,並用包含文件的內容取而代之,然後與源文件一起進行編譯,所以頭文件(以“.h”為後綴名的源文件)本身並不會單獨進行編譯,它是被包含到“.c”後綴的源文件中後再與“.c”後綴源文件一起進行編譯,但“.c”文件本身會進行編譯,所以如果將一個“.c”後綴的源文件使用#include(按理來說.c 後綴源文件是不用作頭文件的,頭文件的的作用是定義一些符號常量與一起函數原型的聲明,並不進行全局變量的定義,而全局變量的定義一般都是在“.c”後綴源文件中定義的,在運行時會自動讀取得到)被包含到其他源文件中,如果這個被包含的.c源文件中定義了全局變量,則在編譯時就會出錯,報全局變量重復定義了,原因就是該被包含的.c源文件被編譯了兩次,一次就是被原樣拷貝到其他源文件中後與源文件一起進行編譯,另外由於是.c後綴源文件,所以本身還要進行一次編譯,所以編譯就通不過。
庫函數頭文件使用下面的語法:
#include <filename>
本地庫文件使用下面的語法:
#include "filename"
如果本地查找失敗,編譯器會去搜索庫函數頭文本。處理本地頭文件的常見策略就是在源文件所在的當前目錄進行查找,如果未找到,編譯器就像查找函數庫頭文件一樣在標准位置查找本地頭文件。
你可以在所有的#include語句中使用雙引號而不是尖括號,但是,使用這種方式,有些編譯器會浪費少許時間,而更不好的是,沒有將庫函數文件與本地文件給區別開來。
有些編譯器可以使用絕對文件路徑:
#include <C:\Documents and Settings\jzj374\workspace\Hello\src\insert2.c>
或
#include "C:\Documents and Settings\jzj374\workspace\Hello\src\insert2.c>"
一旦使用了絕對路徑,不管它是使用在哪種形式的#include,它們都不會去在正常目錄(標准庫目錄或本地頭文件目錄)下去查找,因為這個路徑已經指定了查找的位置。
標准約定:頭文件以 .h 後綴命名
標准要求編譯器必須支持至少8層的並沒有文件嵌套,但沒有限制最深的值。
如果一個頭文件 a.h 中 #include 了 b.h,則我們只需要在源文件中 #include 一下 a.h就可以將 b.h 也包含進來了。
有的頭文件是不能重復包含的,如某個頭文件中聲明了一個全局變量並定義時初始化了,則在多次#include時會出現重復定義編譯問題,如果某個符號被#defined多次,雖然編譯時不報錯,但最後的會覆蓋以前的定義。
為了防止重復宏的定義與變量的聲明,我們一般頭文件這樣寫:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1
//這裡寫需要定義或聲明的內容
//...
#endif
HEADERNAME為頭文件的文件名,這種約定可以避免由於其他並沒有文件使用相同的符號而引起的沖突,大部分的標准函數庫的頭文件都是這樣定義的,另外,“#define_HEADERNAME_H 1”可以寫成“#define _HEADERNAME_H”,盡管現在它的值是一個空字符串而不是“1”,但這個符號仍然是被定義過了的。
即使頭文件的所有內容將被忽略,預處理器仍將讀入整個頭文件內容,由於這種處理將拖慢編譯速度,所以如果可能,應避免出現多重包含。
使用標准庫函數有助於程序的可移植性。一種編譯器可以在它的函數庫中提供額外的函數,但不應修改標准要求提供的函數。
ANSI C函數庫的許多函數調用操作系統來完成某些任務,I/O函數尤其如此。當操作系統執行任務時可能會失敗,標准庫函數在一個外部整型變量 errno(在 errno.h 中定義)中保存錯誤代碼之後把這個信息傳遞給用戶程序,提示操作失敗的原因。perror函數簡化向用戶報告這些特定錯誤的過程,它的原型在stdio.h中定義:
void perror (const char* message);
如果message不是NULL並且指向一個非空字符串,perror函數就打印出這個字符串,後面跟一個分號和一個空格,然後打印出一條用於解釋errno當前存儲的錯誤代碼的信息。
注:只有當一個函數失敗時,errno才被設置,成功時不會被修改。所以我們不能根據判斷errno的值來判斷是否有錯誤發生,因此只有調用的函數發生錯誤時檢查errno才有意義。
頭文件stdio.h包含了與ANSI函數庫的I/O部分有關的聲明,盡管不包含這個頭文件也可以使用某些I/O函數,但絕大多數I/O函數在使用前都需要包含這個頭文件。
printf函數會使用到緩沖,所以在調試程序時,在調用printf後立即調用fflush函數。
流分為兩種:文本流與二進制流。二進制流的字節不經修改地從二進制流讀取或向二進制流寫入。文本流能夠允許的最大文本行因編譯器而異,但至少允許254個字符,如果宿主操作系統使用不同的約定結束文本行標示符,I/O函數必須在這種形式和文本行的內部形式之間進行翻譯轉換。
總規律:在Windows環境中,寫入時,是否在 \n 前加上\r 符,要看寫入模式是否附加了二進制模式 b,如果加上了,則在寫入時不會在\n前加上\r符,puts時也只會在行尾只是加上\n,而不是 \r\n;讀取時,是否去掉\n前的\r符,要看讀取模式是否附加了二進制模式 b,如果加上了,則在寫入時不會丟棄\n前加上\r符,那怕使用gets函數來讀取時,\r\n也只會丟棄\n字符,而不會將前面的\r丟棄掉。
不管是在Windows環境中還是Linux環境中,行I/O函數只要讀取到 \n就算一行讀取完。
文本流會因操作系統不同特性有所不同,其中之一就是文本行的最大長度,標准規定至少允許254個字符,另一個可能不同的特性是文本行的結束方式,如在MS-DOS系統中,文本文件約定以一個回車符和一個換行符結尾,但UNIX系統只使用一個換行符。標准把文本行定義為零個或多個字符,後面跟一個表示結束的換行符,而不帶回車符,對於那些文本行的外在表現形式與這個定義不同的系統上,庫函數會負責外部表現形式和內部存儲形式之間的轉換(請看下面的測試,下面是中寫模式為),如在MS-DOS系統中,輸出時,文本中的換行符被替換成一對回車/換行符(注:只在在寫模式為w的情況下才會這樣,wb情況下則不會在前面加上回車符),在輸入時,文本中的連續的回車/換行符中的回車符會被丟棄(注:只在在讀取模式為r的情況下才會這樣,rb的情況則不會丟棄),這種不必考慮文本的外部形式而操作文本的能力簡化了可移植程序的創建。
freopen("d:/test1", "w", stdout);
printf("\r");//只會輸出一個字符\r
printf("\n");//會輸出兩個字符 \r\n
printf("\r\n");//會輸出三個字符 \r\r\n
printf("\n\r");//會輸出三個字符 \r\n\r
freopen("d:/test1", "wb", stdout);
printf("\r");//只會輸出一個字符\r
printf("\n");//只會輸出一個字符\n
printf("\r\n");//只會輸出兩個字符\r\n
printf("\n\r");//只會輸出兩個字符\n\r
freopen("d:/test1", "w", stdout);
putchar('\r');//只輸出一個字符\r
putchar('\n');//會輸出 \r\n
freopen("d:/test1", "wb", stdout);
putchar('\r');//只輸出一個字符\r
putchar('\n');//也只輸出一個字符\n
從上面的測試可以看出,在Windows系統上,如果使用C語言中的I/O字符輸出函數(注,不管是格式化輸出家族函數printf、行字符輸出家族函數puts、還是字符輸出家族函數putchar都滿足這個規律:putchar('\n')也會輸出兩個字符 \r\n,而不是一個字符 \n )將一個換行字符輸出,則會在換行字符前加上一個回車字符,但如果輸出的是回車字符時,則不會在前面加上換行符。
在Windows環境下,錄入的文本時按一個回車鍵時相當於輸入了兩個字符: \r\n ,單個的 \r 字符在Windows環境中的不同文本編輯器中顯示是不一樣的,有時會顯示可以成換行效果(如notepad、eclipse中),有時顯示成?(如在UltraEdit中,不過在打開時它會詢問你是否轉換成DOS格式,如果選擇了是的話,也會顯示成換行效果);但在Unix環境下時,按一個回車鍵後,只會產生一個字符 \n(注,在使用vi編輯一個文件時,當你在某行輸入了一些字符後即使你沒有按回車鍵,vi編輯器在保存內容時也會在行末加上 \n ,這有點奇怪,規則是vi編輯的文本每行後面都會固定有個 \n 字符,即使從Windows上傳一個只有一行的文本文件到且沒有回車換行符時,在服務上那怕沒有按回車,通過vi編輯保存後就會加上 \n 字符,但如果是通過bin模式傳送時不會相互轉換)
通過編輯vi /etc/vsftpd.conf配置文件,如果將設置為:
ascii_upload_enable=YES
ascii_download_enable=YES
後,則windows與linux之間通過ftp的asc模式傳遞時會相互轉換,windows到linux時,會將回車換行轉換為一個換行符,從linux到windows時,會將換行符轉換為回車換行兩個字符。
Windows環境中:
freopen("d:/test1","w",stdout);
printf("%s","abcde\r");//不會在\r後面加上\n
//或者 printf("abcde\r");
printf("%s","fghij\n");//會在\n前面加上\r
//或者 printf("fghij\n");
最後輸出的文件內容為13個字節(從程序實際輸入的字符來看只有12個),Ultra顯示如下:
(注:回車是ASCII碼為13,換行為10)
在Linux環境中上面程序輸出結果如下,且文本所存儲的內容只有12個字節,存儲的內容即程序中顯示錄入的:
在Windows環境中,在輸入時,文本中的連續的回車/換行符中的回車符會被丟棄:
freopen("d:/test1","r",stdin);
int c ;
while((c= getchar() )!= EOF){
/*
* 注,雖然printf見到\n會將它轉換為 \r\n(在windows環境下)
* ,但如果c以整型類型 %d 輸出時,不會\n字符前加上\r,但如
* 果以字符類型 %c 輸出時,則會將 \n 轉換為 \r\n
*/
printf("%d ",c);
}
二進流中的字節將完全根據程序編寫它們的形式寫入到文件或設備中,而且完全根據它們從文件或設備讀取的形式計入到程序中,它們並未作任何改變,這種類型的流適用於非文本數據,當然如果你不希望I/O函數修改文本文件的末字符,也可以把它用於文本文件的讀取。
啟動一個C語言程序時,操作系統環境負責打開3個文件,並將這3個文件的指針提供給該程序, 它們都是一個指向FILE結構的指針,在程序中我們可以直接使用它們(printf("%c",getc(stdin));)。這3個文件分別是:標准輸入、標准輸出、標准錯誤,相應的FILE文件指針分別是stdin、stdout和stderr,它們在<stdio.h>中定義的(#define),一般stdin指向鍵盤、而stdout和stderr指向顯示器。標准錯誤就是錯誤信息寫入的地方,perror函數把它的輸出也寫到這個地方。
標准錯誤流使用一個單獨的流,這樣即使標准輸出的缺省值重定向為其他位置,錯誤信息仍能夠顯示在它的缺省位置(顯示器)。
FOPEN_MAX 是你能夠同時打開的最多文件數,具體數目因編譯器而異,但不能小於8。
FILENAME_MAX 是用於存儲文件名的字符數組的最大限制長度。
在許多系統中,標准輸出和標准錯誤在缺省情況下是相同的,但是,為錯誤信息准備一個不同的流意味著,即使標准輸出重寫向到其他地方,錯誤信息仍將出現在屏幕或其他缺省的輸出設備上。
在許多環境中,可以使用符號“<”來實現輸入重定向,它將把鍵盤輸入替換為文件輸入:如果程序prog中使用了函數 int getchar (void)(默認從標准輸入中一次讀取一個字符,遇到文件尾時,則返回EOF),則命令行: prog <infile,將使得程序prog從輸入文件infile(而不是從鍵盤)中讀取字符。字符串“<infile”並不包含在argv的命令行參數中。也可通過管道提供輸入源:otherprog | prog 將運行兩個程序otherprog和prog,並將程序otherprog標准輸出通過管道重定向到程序prog的標准輸入上。
如果程序prog調用了函數 int putchar (int)(將字符送至標准輸出上,在默認情況下,標准輸出為屏幕顯示,並返回輸出的字符,發生錯誤時返回EOF),那麼命令行 prog>outfile(可以使用符號“>”來實現輸出重定向),將把程序prog的輸出從標准輸出設備重定向到文件。如果系統支持管道,那麼命令行 prog | anotherprog 將把程序prog的輸出從標准輸出通過管道重定向到程序anotherprog的標准輸入中。
I/O函數以三種基本的形式處理數據:單個字符、文本行和二進制數據,對每種形式,都會有特定的函數來進行處理。下表列出了用於每種I/O形式的函數或家族函數,家族函數在表中以斜體表示,它代表著一組功能相當的函數,這些函數只是輸出來源或輸出地方不同:
數據類型
輸入
輸出
描述
字符
getchar
putchar
讀/寫單個字符
文本行
gets
scanf
puts
printf
文本行未格式化的輸入/出
格式化的輸入輸出
二進制數據
fread
fwrite
讀/寫二進制數據
下表是對上表家族函數進一步說明:
家族名
目的
可用於所有的流
固定用於stdin和stdout
用於內存中的字符串
getchar
字符輸入
int fgetc(FILE *stream)
int getc(FILE *stream)
int getchar(void)
不需要特殊IO函數,可使用字符指針操作
putchar
字符輸出
int fputc(int chr, FILE * stream);
int putc(int chr, FILE * stream);
int putchar(int chr);
不需要特殊IO函數,可使用字符指針操作
gets
文本行輸入
char * fgets(char * buffer, int buffer_size, FILE * stream);
注:不會丟棄行結束符
char * gets(char * buffer);
注:會丟棄行結束符(在Windows上連續的 \r\n 或 單個的 \n 都會被丟棄,但單個的 \r 不會被丟棄。另外,如果模式為rb,則連續的\r\n中的\r還是不會被丟棄,只會將後面的\n丟棄掉)
不需要特殊IO函數,可使用strcpy函數操作
puts
文本行輸出
int fputs(char const* buffer, FILE * stream);
注:不會在字符串後加上行結束符
int puts(char const * buffer);
注:會在字符串後加上行結束符,Windows上為 \r\n
不需要特殊IO函數,可使用strcpy函數操作
scanf
格式化輸入
int fscanf(FILE *stream, char const *format, ...);
int scanf(char const*format, ...);
int sscanf(char const *string, char const *format, ...);
printf
格式化輸出
int fprintf(FILE *stream, char const *format, ...);
int printf(char const *format, ...);
int sprintf(char *buffer, char const*format, ...);
fopen把一個流和文件相關聯。
FILE是一個結構體類型,用於管理緩沖區和存儲流的I/O狀態。它被定義於 stdio.h 頭文件中,聲明原型如下:
typedef struct _iobuf{
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
} FILE;
FILE *fp = fopen(char *name, char *mode);
如果打開時發生錯誤,則fopen將返回NULL,errno存儲了問題的原因。
mode(模式):
讀
寫
添加
文本
r
w
a
二進制
rb
wb
ab
mode需以r、w或a開頭,如果是讀取,則必須是已存在的文件。如果是寫入,文本存在,則會刪除原來內容,如果文件原先不存在,則新建文件。如果是追加,原文件不存在時也會先創建,如果存在則在文件末添加內容,並不會刪除原來的內容。
在mode中添加“a+”表示文件打開用於更新,流即可讀可寫。但是,如果你已經從該文件中讀取了一些數據,那麼在你開始向它寫入數據之前,你必須調用其中一個文件定位函數(fseek、fsetpos、rewind),在你向文件寫入一些數據後,想從文件讀取,你先必須調用fflush函數或者文件定位函數。
"r" 打開文本文件用於讀
"w" 創建文本文件用於寫,並刪除已存在的內容(如果有的話)
"a" 添加;打開或創建文本文件用於在文件末尾寫
"rb" 打開二進制文件用於讀
"wb" 創建二進制文件用於寫,並刪除已存在的內容(如果有的話)
"ab" 添加;打開或創建二進制文件用於在文件末尾寫
"r+" 打開文本文件用於更新(即讀和寫)
"w+" 創建文本文件用於更新,並刪除已存在的內容(如果有的話)
"a+" 添加;打開或創建文本文件用於更新和在文件末尾寫
"rb+"或"r+b" 打開二進制文件用於更新(即讀和寫)
"wb+"或"w+b" 創建二進制文件用於更新,並刪除已存在的內容(如果有的話)
"ab+"或"a+b" 添加;打開或創建二進制文件用於更新和在文件末尾寫
後六種方式允許對同一文件進行讀和寫,要注意的是,在寫操作和讀操作的交替過程中,必須調用fflush()或文件定位函數如fseek()、fsetpos()、rewind()等。
FILE * input;
input = fopen("data3", "r");
if (input == NULL) {
perror("data3");//顯示錯誤信息,如果文件不存在,則顯示類似於:data3: No such file or directory
exit(EXIT_FAILURE);
}
exit為每個已打開的輸出文件調用fclose函數,以將緩沖區中的所有輸出寫到相應的文件中。
在主程序main中,語句return expr 等價於 exit(expr) ,其他函數的退出會直接導致整個程序的退出。
void exit(int status)
status參數值會返回給操作系統,任何調用該程序的進程都可以獲得exit參數的值,因此,調用該程序的進程可以通該參數值來測試該程序是否執行成功。
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
FILE * freopen(char * filename, char *mode, FILE *stream);
將stream(通常是stdin、stdout、stderr)流先關閉,然後重新打開這個流,且這個重新打開的流重定向到了指定文件上,返回的是重新打開的流。簡單的說該函數就是實現重定向功能。
//從GetData.txt文件中讀取數據
freopen("GetData.txt","r",stdin);
//把輸出結果重定向到OutData.txt文件中
freopen("OutData.txt","w",stdout);
模擬Unix上的cat回顯命令:
#include <stdio.h>
#include <stdlib.h>
/*模仿cat命令,可以顯示多個文件內容*/
int main(int argc, char *argv[]) {
FILE *fp;
void filecopy(FILE *, FILE *);
char *prog = argv[0];//記下程序名稱,供錯誤處理使用
printf("%d", argc);
if (argc == 1) {//如果不帶參時,直接將鍵盤輸入的顯示在屏幕上
filecopy(stdin, stdout);
} else {
while (--argc > 0) {
if ((fp = fopen(*++argv, "r")) == NULL) {
fprintf(stderr, "%s: can't open %s\n", prog, *argv);
exit(1);
} else {
filecopy(fp, stdout);
if (fclose(fp) != 0) {
perror("fclose");
exit(EXIT_FAILURE);
};
}
}
}
if (ferror(stdout)) {//判斷是否寫成功
fprintf(stderr, "%s: error writing stdout %s\n", prog);
exit(2);
}
return EXIT_SUCCESS;
}
void filecopy(FILE * ifp, FILE *ofp) {
int c;
while ((c = getc(ifp)) != EOF) {
putc(c, ofp);
fflush(ofp);
}
}
如果在讀或寫的過程中出現錯誤,則函數ferror返回一個非0值。
int ferror(FILE *fp)
int feof(FILE *fp):如果指定的文件到達文件結尾,將返回一個非0值。
int fclose(FILE *fp):關閉文件連接,並刷新緩沖區。當程序正常終止時,程序會自動為每個打開的文件調用fclose,成功時返回0,失敗時返回EOF。
int getc(FILE *stream)
int fgetc(FILE *stream)
int getchar(void)
fgetc、getc從指定的stream參數流中讀取,而getchar固定從標准輸入stdin中讀取,達到文件尾或發生錯誤時返回EOF(-1)。注:讀出來的永遠是一個字節的內容,並且將這一個字節的內容永遠看作是正數,然後將一個字節內容轉換成int類型,且在位擴充時是補零,所以返回的永遠是正數 0~255,表示了256個字符,這個與Java中的字節流是一樣的道理。這些函數返回的都是一個int類型值而不是char型值,盡管表示一個字符的內存代碼(二進制碼)以一個小整型就可以存儲了,但返回int型值的真正原因是為了允許函數返回EOF(-1),如果返回的是char那麼256個字符中必須有一個被指定用於表示EOF(這裡即-1會被占用)如果這個字符出現在了流中,那麼這個字符後面的內容將不會被讀取,因為它被解釋為EOF標志了。
讓函數返回一個int型值就能解決這個問題。EOF被定義為一個整型(約定為-1,其實可為任何負數),它的值在任何可能出現的字符范圍(0~255)之外,這種解決方法允許我們使用這些函數來讀取二進制文件也是可以的。
int fputc(int character, FILE * stream);
int putc(int character, FILE * stream);
int putchar(int character);
在輸出之前,函數把character參數裁剪為一個無符號字符型值,失敗時返回EOF,並返回寫入的字符。
fgetc和fputc都是真正的函數,但getc、putc、getchar和putchar都是通過#define指令定義的宏。宏在執行時間上效率稍高,而函數在程序長度方面更勝一籌,之所以提供兩種類型的方法,是為了允許你根據程序的長度和執行速度哪個更重要來選擇正確的方法。
int ungetc(int character, FILE * stream);
把一個先前讀入的字符character返回到流中,這樣它可以在以後被重新讀入,成功時返回“退回”的字符,失敗時返回EOF。下面從標准輸入中讀取整數,直到非數字字符止:
int read_int() {
int value;
int ch;
value = 0;
/*
** Convert digits from the standard input; stop when we get a
** character that is not a digit.
*/
while ((ch = getchar()) != EOF && isdigit(ch)) {
value *= 10;
value += ch - '0';
}
/*
** Push back the nondigit so we don't lose it.
*/
if (ch != EOF) {
ungetc(ch, stdin);
}
printf("ch = %d, next char in buffer = %c\n", ch, getchar());
return value;
}
輸入“123s”:
123s
ch = 115, next char in buffer = s
123
-end-
每個流都允許至少一個字符被退回。如果一個流允許退回多個字符,那麼這些字符再次被讀取的順序就以退回時的反序進行。注,退回並不等同於寫入,原流的存儲區域中的內容並不受ungetc的影響。“退回”字符和流的當前位置有關,所以如果用fseek、fsetpos或rewind函數改變了流的當前位置,所有退回的字符都將被拋棄。
行I/O可以用兩種方式執行——未格式化或格式化的,這兩種都用於操作字符串。
gets和puts函數家族是用於操作字符串而不是單個字符。
char *fgets(char * buffer, int buffer_size, FILE * stream);
char *gets(char * buffer);
int fputs(char const * buffer, FILE * stream);
int puts(char const * buffer);
fgets函數從stream中讀取字符並把它們復制到buffer中,在讀取時可能以一行為單位來讀取,也可能不以一行為單位(少於一行),這要看buffer_size參數的大小,如果buffer_size大於或等於一行字符總數(包括回車換行字符)加1(每次讀取後放在buffer中時都會加上一個NUL字節來構成字符串)時,就會讀取一整行,但要注意的是如果 buffer_size -1 大於一行字符總數,此時也只會讀取一行的字符,而不會因為buffer空間多余而讀取一行多的字符;如果buffer_size -1小於了一行字符總數時,就只讀取 buffer_size -1 個字符,而不是一行;總的來說,讀取的規則是:如果已經讀取完行結束符或者緩沖區已經存入了buffer_size -1個字符,則都會停止讀取,剩余的字符等到下一次進行讀取。在任何一種情況下,都會在讀取的字符串後加上NUL字節,使它成為一個字符串,所以一次最多只能讀取buffer_size -1個字符。如果在任何字符還沒讀取前就到達了文件尾,緩沖區就不會被修改,此時fgets函數返回一個NULL指針,否則返回它的第一個參數,所以可以通過這個返回值來判斷是否到達文件尾部。
注:fgets無法把字符串計入到一個長度為一的緩沖區,因為其中一個字符需要為NUL字節保留。
傳遞給fputs函數的緩沖區中存儲的字符必須以NUL字節結尾,所以這個函數沒有一個緩沖區長度參數,這個字符串是逐字符寫入的:如果它不包含一個換行符,就不會寫入換行符,如果包含了好幾個換行符,所有換行符都會被寫入。fputs與fgets不同,它即可一次寫入一行的一部分, 也可以一次寫入一整行,甚至可以一次寫入好幾行,如果出錯EOF,否則返回一個非負值。
gets和puts函數幾乎和fgets與fputs相同,之所以存在它們是為了允許向後兼容。它們之間的一個主要的功能性區別在於當gets讀取一個輸入時,它會丟棄掉行標示符(與Java中的BufferedReader的readLine方法有點像);當寫入一個字符時串時,它在字符串寫入之後向輸出再添加一個行結束標示符。另一個區別僅限於gets函數,它並沒有緩沖區長度大小參數,所以在讀取時很有可能會溢出,所以我們盡量不用這個函數。
下面是標准庫中fgets和fputs函數的代碼:
//fgets行函數基於getc函數實現:從iop文件中最多讀取n-1個字符,再加上一個NULL
char * fgets(char *s, int n, FILE *iop) {
register int c;
register char *cs;
cs = s;
while (--n > 0 && (c = getc(iop)) != EOF) {
//先將讀取出的字符存儲在cs中後指針再下移,然後再判斷
//一行是否讀取完
if ((*cs++ = c) == '\n') {
break;
}
}
//最後將在讀取出的字符後面加上字符串結束標示符
*cs = '\0';
//如果為空文件或讀取出錯,則返回NULL
return (c == EOF && cs == s) ? NULL : s;
}
//fputs行函數基於putc函數實現
int fputs(char *s ,FILE *iop){
int c;
while (c=*s++){
putc(c,iop);
}
return ferror(iop)?EOF:非負值;
}
示例:測試fgets是以\n為行結束標示,且當讀取模式為“rb”時,連續的\r\n中的\r不會被丟棄,但當讀取模式為“r”時,會丟棄\r字符:
先在Liunx環境上使用以下程序創建這樣一個文件:
#include <stdio.h>
main(){
freopen("/root/a/a.txt","w",stdout);
printf("ab\r");
printf("cd\r\n");
printf("ef\n");
}
運行的結果為會產生一個10字節的文本文件:
在Windows環境中全以下程序來讀取上面Linux產生的文件:
FILE * fp = fopen("d:/a.txt", "r");
char *s, buffer[9];
int size = 9;
while ((s = fgets(buffer, size, fp)) != NULL) {
printf("%d-\n-", strlen(buffer));
printf("%s-\n-", buffer);
}
輸出結果:
6-
-ab
cd
-
-3-
-ef
-
-
如果將讀取模式改成“rb”,則輸出結果為:
7-
-ab
cd
-
-3-
-ef
-
-
int fscanf(FILE *stream, char const *format, ...);
int scanf(char const *format, ...);
int sscanf(char const *string, char const *format, ...);
這三個函數的輸入源的來源不同。當到達格式化字符串的末尾或讀取的輸入不再匹配格式字符串所指定的類型時,輸入就停止。它們都返回讀取的字符個數。如果達到文件的尾,則返回EOF。
格式化串的空白字符(空白字符包括空格、橫向與縱向制表符、換行符、回車符、換頁符)一般情況下會被忽略掉,它將不會用來與輸出中的空白字符進行匹配(但%c不會忽略空白字符)。
%[*][寬度][限定符] 格式代碼
格式碼
scanf限定符
h
l
L
d i n
short
long
o u x
unsigned short
unsigned long
e f g
double
long double
數據類型
printf/scanf函數轉換格式
long double
%Lf
double
%lf
float
%f
unsigned long
%lu
long
%ld
unsigned
%u
int
%d
unsigned short
%hu
short
%hd
char
%c
scanf格式代碼
代碼
參數類型
含義
c
char *
讀取和存儲單個字符。前導的空白字符並不跳過。如果給出寬度,就讀取和存儲這個數目的字符。字符後面不會添加一個NUL字節。參數必須是一個指向足夠大的字符數組
i
d
int *
有符號整數被轉換。d把輸入解釋為十進制;i根據它的第一個字符決定值的基數,就像整型字面值常量的表示形式一樣
u
o
x(X)
unsigned *
有符號整數被轉換,但它按照無符號數存儲。如果使用u,值被解釋為十進制數;如果使用o,值被解釋為八進制數;如果使用x,值被解釋為十六進制數
f
e(E)
g(G)
float *
轉換一個浮點值
s
char *
讀取一串非空白字符。參數須指向一個足夠大的字符數組。當發現空白時輸入就停止,字符串後面會自動加上NUL終止符
[xxx]
char *
根據給定組合的字符從輸入中讀取一串字符。參數必須指向一個足夠大的字符數組。當遇到第1個不在給定組合中出現的字符時,就停止輸入。字符串後面會自動加上NUL終止符。代碼%[abc] 表示字符組合包括a、b和c。如果列表中以一個 ^ 字符開頭,表示字符組合是所列出字符的補集。右方括號也可以出現在字符列表中,但它必須是列表的第1個字符。至於橫槓是否用於指定某個范圍的字符(如 %[a-z]),則因編譯器而異
p
void *
輸入預期為一串字符,諸如那些由printf函數的%p格式代碼所產生的輸出。它的轉換方式因編譯器而異,但轉換結果將和按照上面描述的進行打印所產生的字符的值是相同的
n
int *
到目前為止通過這個scanf函數的調用從輸入讀取的字符數被返回。%n轉換的字符並不計算在scanf函數的返回值之內。它本身並不消耗任何輸入
%
與%匹配
數字在格式化時會采用四捨五入的方式來截斷。
nfields = fscanf(input, "%4d %4d %4d", &a, &b, &b);
這個寬度參數把整數值的寬度限制為4個數字或者更少。使用下面的輸入:
1 2
a的值將是1,b的值將是2,c的值將沒有改變,nfields的值將是2,但下面的輸入:
12345 67890
a的值將是1234,b的值為5,c的值是6789,而nfields的值是3,輸入中最後一個0將保持在未輸入狀態。
注:格式化輸入函數會跳過空白字符,包括換行符。
示例:假設我們要讀取包含下列日期格式的輸入行:
25 Dec 1988
相應的scanf語句可以這樣編寫:
int day ,year;
char monthname[20];
scanf(“%d %s %d”,&day,monthname,&year);
scanf不會跳過換行,如果想以行為單位來讀取,則可以采用下面的處理方法:
#include <stdio.h>
#define BUFFER_SIZE 100 /* Longest line we'll handle */
void function(FILE *input) {
int a, b, c, d;
char buffer[BUFFER_SIZE];
while (fgets(buffer, BUFFER_SIZE, input) != NULL) {
if (sscanf(buffer, "%d %d %d %d", &a, &b, &c, &d) != 4) {
fprintf(stderr, "Bad input skipped: %s", buffer);
fflush(stderr);
continue;
}
/*
** Process this set of input.
*/
printf("%d %d %d %d", a, b, c, d);
fflush(stdout);
}
}
int fprintf(FILE *stream, char const *format, ...);
int printf(char const *format, ...);
int sprintf(char *buffer, char const *format, ...);
sprintf會在輸出結果末加上NUL字符。
printf家族函數的格式代碼和scanf函數家族的格式代碼用法是完全相同的。
%[零個或多個標志符][最小字段寬度][精度][修改符] 格式化代碼
printf格式代碼
代碼
參數類型
含義
c
int
參數被截斷為unsigned char 類型並作為字符進行打印
i
d
int
參數作為一個十進制整數打印,如果給出了精度而且值的少於精度,前面就用0填充
u
o
x(X)
unsigned *
參數作為一個無符號值打印,u使用十進制,o使用八進制,x或X使用十六進制,兩者的區別是x使用abcdef,而X使用ABCDEF
e(E)
double
參數根據指數形式打印。例如,6.023000e23是使用代碼e,6.023000E23是使用代碼E,小數點後面的位數由精度字段決定,缺省為6
f
double
參數按照常規的浮點格式打印。精度字段決定小數點後面的位數,缺省為6
g(G)
double
參數以%f或%e(如果為%G則為%E)的格式打印,如果指數大於等於-4但小於精度字段就使用%f格式,否則使用指數格式。
s
char *
順序打印字符串中的字符,直到遇到’\0’或打印由精度指定的字符數為止
p
void *
指針值被轉換為一串因編譯器而民的可打印字符。這個代碼主要是和scanf中的%p代碼組合使用
n
int *
這個代碼是獨特的,因為它並不產生任何輸出。相反,到目前為止函數所產生的輸出字符數目將被保存到對應的參數中
%
與%匹配
printf格式標志
標志
含義
-
輸出時左對齊,缺省情況下右對齊
0
當數組為右對齊時,缺省情況下是使用空格填充值左邊未使用的位置。這個標志表示用0來填充,它可用於d i u o x e f g 代碼,使用d i u o x 代碼時,如果給出精度字段,零標示就被忽略。如果格式代碼中出現了負號標示,零標示也沒有效果
+
當用於一個格式化某個有符號值的代碼時,如果值為非負,輸出值前面就會有個正號;如果為負值,則會在輸出值前加上一個負號。缺省情況下,正號並不會顯示
空格
只用於轉換有符號值代碼。當值為非負時,這個標志把一個空格添加到它的開始位置。注意這個標志和正號標志是互斥的,如果同時出現,空格標志會被忽略
#
選擇某些代碼的另一種轉換形式,請見下表:
用於...
#標志...
o
保證產生的值以一個零開頭
x X
在非零值前面加0x前綴(%X則為0X)
e E f
確保結果始終包含一個小數點,即使它後面沒有數字
g G
和上面的e E和f代碼相同,綴尾的0並不從小數中去除
最小字段寬度:一個十進制數,用於指定出現在結果中的最小字符數,如果值的字符數小於字段寬度,就對它進行填充以增加長度。標志決定填充是用空白還是使用零以及它出現在值的左邊還是右邊。
精度:對於d i u o x 類型的轉換,精度字段指定將出在結果中的最小的數字個數並覆蓋零標志。如果轉換後的值的位數小於寬度,就在它的前面插入零。如果值為零且精度也為零,則轉換結果就不會產生數字。對於e f類型的轉換,精度決定將出現在小數點之後的數字位數。對於g類型的轉換,它指定將出現在結果中的最大有效位數。當使用s類型轉換時,精度指定將被轉換的最多字符數。精度以一個句點開頭,後面跟一個可選的十進制整數。如果未給出整數,精度缺省為零。
如果用於表示字段寬度和/或精度的十進制數由一個星號代替,那麼printf的下一個參數(必須是個整數)就提供寬度和(或)精度。因此,這些值可以通過計算獲得而不必預行指定。
當字符或短整型值作為printf函數參數時,它們在傳遞給函數之前先轉換為整數,有時候轉換可能影響函數的輸出。同樣,在一個長整數的長度大於普通整數的環境裡,當一個長整數作為參數傳遞給函數時,prinft必須知道這個參數是個長整數。下表所示的修改符用於指定整數和浮點數參數的准確長度,從而解決了這個問題:
printf格式代碼修改符
修改符
用於...時
表示參數是...
h
d i u o x
一個(可能是無符號)short型整數
h
n
一個指向short型整數的指針
l
d i u o x
一個(可能是無符號)long型整數
l
n
一個指向long型整數的指針
L
e f g
一個long double型值
%p 是專門打印地址的轉換說明符,它通常以十六進制數的格式輸出地址。
使用二進制流寫入二進制數據(如整數和浮點數)比使用字符I/O效率更高。二進制I/O直接讀寫值的各個位,而不必把值轉換為字符。
fread函數用於讀取二進制數據,fwrite函數用於寫入二進制數據。原型:
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
size_t fwrite(void *buffer, size_t size, size_t count, FILE *stream);
buffer是一個指向用於保存數據的內存位置的指針,會被解釋為一個或多個值的數組。count參數指定數組中有多少個值,所以讀取或寫入一個標量時,count的值為1。
size是緩沖區中每個元素的字節數,count是讀取或寫入的元素個數。函數返回是實際讀取或寫入的元素個數
2286166545(十進制)=10001000 01000100 00100010 00010001(二進制)=88442211(十六進制)
int main(int argc, char **argv) {
FILE *input = fopen("d:/a.txt", "w");
int a[] = { 2286166545 };
fwrite(a, sizeof(int), 1, input);
fclose(input);
}
輸出結果為4個字節,即將內存中的原二進制位輸出到了文件中,以下是Ultr二進制視圖(另外,從二進制顯示可以看出內存中是以低字節序存放的,即低位在前,高位在後):
以下是Ultr文本視圖:
'a'=41(十六進制)
int main(int argc, char **argv) {
FILE *input = fopen("d:/a.txt", "w");
char a[] = { 'a' };
fwrite(a, sizeof(char), 1, input);
fclose(input);
}
輸出結果為1個字節,Ultr二進制視圖:
Ultr文本視圖:
下面程序與上面結果是一樣的:
freopen("d:/a.txt","w",stdout);
putchar('a');
從上面示例可以看出,C中的二進制I/O函數與Java中的字節流I/O概念是一樣的(這些函數有點像Java裡的DataInputStream與DataOutputStream兩個類的相關方法),另外都可以使用二進制來讀寫文本文件。
示例:將一個結構類型的變量寫入到文件,然後再讀取出來:
int main(int argc, char **argv) {
struct VALUE {//會占用 4 * 3 = 12字節的空間
char c;
int i;
char chr[2];
};
struct VALUE values = { 'a', 255, { 'a', 'b' } };
struct VALUE v, *buffer = &v;
freopen("d:/a.txt", "w", stdout);
int writeCounts = fwrite(&values, sizeof(struct VALUE), 1, stdout);
fflush(stdout);//寫完後一定要刷新,否則下面讀取不到
fprintf(stderr, "writeCounts = %d\n", writeCounts);
freopen("d:/a.txt", "r", stdin);
int readCounts = fread(buffer, sizeof(struct VALUE), 1, stdin);
fprintf(stderr, "readCounts = %d\n", readCounts);
fprintf(stderr, "v->c = %c\n", buffer->c);
fprintf(stderr, "v->i = %d\n", v.i);
fprintf(stderr, "(v->chr)[1] = %c\n", (buffer->chr)[1]);
}
刷新與定位函數
int fflush(FILE *stream);
fflush迫使一個輸出流的緩沖區內的數據進行物理寫入,不管它是不是已經寫滿。調用fflush函數保證調試信息立即打印出來,stdout使用到了緩沖,而stderr沒有使用緩沖,所以在進行標准輸出進行打印調試信息時,fflush很有用處。
long ftell(FILE *stream);
Returns the current offset in the file, or -1L on error.並設置errno標志為某個正值,具體值由編譯器決定。
int fseek(FILE *stream, long offset, int from);
fseek函數允許你在一個流中定位,offset argument is the position that you want to seek to, and from is what that offset is relative to。For fseek(), on success zero is returned; 非零is returned on failure(errno的值不會改變)。
long pos;
// 將當前的位置存儲在變量 "pos" 中:
pos = ftell(fp);
// 向前移動 10 bytes:
fseek(fp, 10, SEEK_CUR);
// 移動後向流寫入數據
do_mysterious_writes_to_file(fp);
// 返回到"pos"存儲的開始位置:
fseek(fp, pos, SEEK_SET);
form參數如下:
如果from是 ...
你將定位到...
SEEK_SET
從流的起始位置起offset個字節,offset必須是一個非負值
SEEK_CUR
從流的當前位置起offset個字節,offset可正可負
SEEK_END
從流的尾部位置起offset個字節,offset可正可負。如果它是正值,它將定位到文件尾的後面
由於文本流所執行的行標識轉換,由於這種轉換的存在,文本文件的字節數可能和程序寫入的字節數不同,所以一個可移植的程序不應該根據寫入的字符計算結果到文本流的某個位置。
fseek(fp, 100, SEEK_SET); // seek to the 100th byte of the file
fseek(fp, -30, SEEK_CUR); // seek backward 30 bytes from the current pos
fseek(fp, -10, SEEK_END); // seek to the 10th byte before the end of file
fseek(fp, 0, SEEK_SET); // seek to the beginning of the file
rewind(fp); // seek to the beginning of the file
將讀寫位置移動到文件尾:fseek(FILE *stream,0,SEEK_END)
fseek副作用:
1、 行標示符被清除
2、 fseek之前使用ungetc後,被退回的字符會被丟棄,因為在定位操作以後,它不再是“下一個字符”
3、 定位允許你從寫入模式切換到讀取模式,或者回到打開流以便更新
另外三個函數,用一些限制更嚴的方式執行相同的任務:
void rewind(FILE *stream);
rewind設置回指定流的起始位置,同時清除流的錯誤提示標志errno。
int fgetpos(FILE *stream, fpos_t *position);
int fsetpos(FILE *stream, fpos_t const *position);
fgetpos和fsetpos函數是標准C新增的,增加它們的目的是為了處理那些因為過於龐大而無法由long int類型的整數來定位的文件(ftell和fseek使用long int類型來定位)。成功返回0,錯誤返回一個非零,並在errno中存儲一個因編譯器而異的正值。
fseek(fp, 0, SEEK_SET); // same as rewind()
rewind(fp); // same as fseek(fp, 0, SEEK_SET)
char s[100];
fpos_t pos;
fgets(s, sizeof(s), fp); // read a line from the file
fgetpos(fp, &pos); // save the position
fgets(s, sizeof(s), fp); // read another line from the file
fsetpos(fp, &pos); // now restore the position to where we saved
int main(int argc, char *argv[]) {
FILE * stream;
long offset;
fpos_t pos;
stream = fopen("d:/a.txt", "r");
fseek(stream, 5, SEEK_SET);
printf("offset=%d\n", ftell(stream));//5
rewind(stream);
fgetpos(stream, &pos);
printf("offset=%d\n", pos);//0
pos = 10;
fsetpos(stream, &pos);
printf("offset=%d\n", ftell(stream));//10
fclose(stream);
}
為一個流自行指定緩沖區可以防止I/O函數庫為它動態分配一個緩沖區。
void setbuf(FILE *stream, char *buf);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
這些函數允許程序在默認的緩沖區無法滿足要求的罕見情況下控制流的緩沖區策略。這些函數必須在流被打開之後並且在任何數據被讀取或寫入之前被調用。
setbuf以另一個數組為緩沖區,它的長度必須為BUFSIZ(stdio.h中定義),原因是setbuf是setvbuf的簡化版本(請參看後面)。如果buf為NULL,則setbuf將關閉流的所有緩沖方式。
setvbuf函數更為通用。mode參數用於指定緩沖的類型。_IOFBF指定一個完全緩沖的流,_IONBF指定一個不緩沖的流,_IOLBF指定一個行緩沖流(行緩沖,就是每當一個換行符寫入到緩沖區時,緩沖區便進行刷新)。如果buf為NULL,那麼size值必須為0。最好使用BUFSIZ的作為緩沖數組長度,或者是BUFSIZ的倍數,這樣有助於提示效率。調用成功返回0,否則返回非0。
setbuf為setvbuf的簡化版本,相當於下面這個表達式:
((buf==NULL)?(void)setvbuf(stream,NULL,_IONBF,0):(void)setvbuf(stream,buf,_IOFBF,BUFSIZ))
int feof(FILE *stream);
int ferror(FILE *stream);
void clearerr(FILE *stream);
如果流當前處於文件尾,feof函數返回真。如果對流執行了fseek、rewind或fsetpos函數,則流的是否達到末尾狀態會被清除。
ferror函數報告流的錯誤狀態,如果出現任何讀/寫錯誤就返回真。
clearerr函數對指定流的錯誤標示進行重置。
FILE * tmpfile(void);
這個函數會創建一個文件,當文件被關閉或程序終止時,這個文件會自動刪除。該文件以 wb+ 模式(創建二進制文件用於更新,並刪除已存在的內容)打開,這可以用於二進制和文本數據。調用這個函數的目的是創建一個只在程序執行期間使用的新文件。在數據寫入到這個文件之後,可以使用rewind函數把文件位置定位到文件的起始處,以便進行讀取。
如果臨時文件必須以其他模式打開或由一個程序打開但由另一個程序讀取,就不適合用tmpfile函數,此時需使用fopen函數創建並在不使用後使用remove函數刪除。
char *tmpnam(char *buf);
tmpnam函數在每次被調用時均生成不同的名字,為臨時文件創建一個合適的文件名。在程序的執行過程中,最多只能確保(在多線程)TMP_MAX個不同的名字。注意tmpnam函數只是用於創建一個名字,而不是創建一個文件。
如果buf為NULL,tmpnam返回一個指向新文件名字符串的靜態數組的指針,如果不為NULL,buf必須是一個指向一個不小於L_tmpnam(在stdio.h中定義)個字符的數組,tmpnam將把這個新文件名字符串復制到這個數組,並返回buf,如果失敗返回NULL。
int remove(char const *filename);
int rename(char const *oldname, char const *newname);
成功返回零,否則返回非零。