小话c语言

前言----小话c语言
不知道该怎么开头,不过开头的几个字都写了,就继续写下去吧。 看过很多以大话开头的书籍,觉得也不怎么样,觉得还没达到大话的层次,本人本着谦虚的精神,暂且以小话开头吧;可能读者看完,觉得连小话都谈不上,先不管这些了;如果读者确实都觉得连小话都谈不上,到时候我再改个名字好了,这样至少也对得起文章的标题。 言归正传,回到主题吧。 以前写过关于c语言的学习资料,发现它真不是容易就能写好的,里面涉及到很多很多的东西;如果是以基础为目的的,那需要描述的就更多了;如果是稍微提 高一些的,那么可能可以少写一些字。不过,以c语言的本质出发,实在太复杂了,如果让c语言之父来描述,估计那本书也没能足以表达他所有的精神和思想,毕 竟核心思想还是在Dennis Ritchie心里,不过他已经离我们而去了,先默哀一下。 
语言都是相通的 c语言,也是一种语言,和我们中国人平时说的中文其实是类似的,只不过一个最终是给机器来理解的,一个是让人来理解的。我不知道哪种应该更复杂,但 是,有一点是可以肯定的,是语言它的语法就不会太复杂,否则不要说是笨笨的机器,就是地球上应该是最高级的人类可能都不能很好地理解,如果这样,这种语言 的存在价值就需要思考了。"你好",这句话表示的就是个问候语,如果非要细节地分析内部的语法,可以理解成主谓结构吧,"你"是主语,"好"当成谓语 吧。"int i = 1;" 理解成定义一个整形变量i; int表示变量类型, 后面跟着变量名,再跟着赋值号=,再跟着一个赋值的数据,最后以分号结束即可。这个结构可以 用如下的表达式来表达: 类型 类型名 = 初值; 上面的看起来,不是很复杂,就像理解"你从哪里来?", "我从这里来。"这样的话一样。 
为什么需要编译器 写完了int i = 1;之后,机器如何理解呢?当然,没有哪个机器能一下子理解这个。因为,有公司已经把机器的cpu设计成只能理解机器语言的了,那怎么办呢?只能用一种东 西将上面的语句翻译成机器指令,传给机器的cpu,机器就能理解并执行了,而,这个东西就是编译器。不过,有人可能会说,我使用了bash终端,我输入 ls -l再回车,就可以帮我执行命令?哪里有编译器? 解释器是什么 上面说的那个过程真没用到编译器,而是解释器。其实解释器就做了编译器的事情,首先会解析输入的字符串ls -l, 就像编译器解析int i = 1;这个字符串一样,然后解析其中的语义,最后执行对应的操作。

解释器到底是什么 这个东西真的不用多想,它就是一个经过编译器编译ok的程序而已。 
解释器运行程序会比对应编译器编译后的程序运行的慢 说的基本是对的,一般是这样,因为解释器多了一层解释的过程,然后才执行。

结束语 好了,关于解释器和编译器的比较就先到此为止,前言的内容我不想写一些废话,因为多打一句废话也是消耗能量的;

  • 1楼
  • 2012-07-03 00:26
printf函数,能不能简单点?----小话c语言(2) 
开头语 
上面一篇已经写过,c语言不仅仅只是语法很好玩,很让人捉摸不透,它的思想更是深奥,需要慢慢体会的。看了关于c语言的书籍不少于50本,每看一遍都 感觉认识深了一点,但是终究还是无法完全理解它背后最深层的思想,只是在不断地向它走近。正因为如此,下面的描述不会按照一个固定的说明文格式来编写,而 是采用对话的方式来将这个过程展现出来,这样应该能更多地展示出c语言的思想。 
格式: 问题(Question)将以 Q: 开头, 回答(Answer)将以 A: 作为开头。 

Q: c语言的HelloWorld版本是什么样子的? 
A: 起名为hello.c: [cpp] view plaincopy

#include <stdio.h> 

int main() 

printf("Hello World!"); 
return 0; 


Q: #include这条语句是什么意思? 
A: 它以#开头,它表示预处理过程,一般不把它当做编译过程,而是用一个单独的预处理器来处理。因为,从编译源代码的角度来说,将一个头文件插入到最终被编译的代码和编译源代码真的没什么一致的地方。 


Q:如何才能感受预处理器的存在呢? 
A: 一般的编译命令都提供预处理选项,可以查看预处理后的源代码是什么样子。 
[Windows-VS2010] 可以使用cl.exe命令来查看预处理后的源代码: 

cl /P hello.c 


会在同目录下生成一个.i的预处理后的文件hello.i, 内容如下(因为篇幅问题,无法将所有内容上传,否则保存不了,省略号省略了大部分代码): [cpp] view plaincopy

#line 1 "hello.c" 
#line 1 "d:\\Program Files\\Microsoft Visual Studio 10.0\\VC\\INCLUDE\\stdio.h" 

............................. 
............................. 

#line 739 "d:\\Program Files\\Microsoft Visual Studio 10.0\\VC\\INCLUDE\\stdio.h" 

#line 2 "hello.c" 

int main() 

printf("Hello World!"); 
return 0; 


Q:看了那个文件,文件实在是太长了! 
A: 是的,这其实也是c和c++的一个缺点,#include将源代码插入的方式可能会导致被编译的源代码长度迅速增加,这也是预编译出现的一个原因。也可以 看到其实hello.c代码那几行也就是最后的那几句,前面都是#include头文件插入的代码,确实大的很直接。 
Q:hello.i文件最后几句,有#line 2 "hello.c"这句,它是什么意义? 
A: #符号开头,依然是编译器的指令,它表示当前行被设置为2,文件名是hello.c.因为,之前的#include<stdio.h>**入 了很多其它文件的内容,但是在hello.c中,第二行是接着#include <stdio.h>后面的,所以将行数重新设置,这样才能正确地解析hello.c的代码行数。 

Q:cl命令我怎么知道/P参数是预处理作用的? 
A:当然可以用cl /?命令来得到它的帮助,然后找到需要功能的参数。   

Q: 这么多参数,查找要的有点难啊? A: 可以想到unix下的grep命令来过滤字符串。当然,这需要cygwin的支持。   
上面的grep命令是在装了cygwin后才有的。 

  • 2楼
  • 2012-07-03 00:29


Q: 再回到hello.c里面吧。main函数必须有个返回值吗? 
A: 是的,其实这很能体现分层思想,任何一个应用程序最终都可能有个返回值,供调用它的程序来使用,所以在这里有个int的返回值 
也是可以理解的。 

Q: 怎么查看调用一个程序的返回值呢? 
A: [Windows] 在命令行下,可以使用echo %errorlevel%来获取它的值。 
[Mac]在命令行下,可以使用echo $?来获取。 
[Ubuntu]同[Mac]. 
比如,hello.c的代码编译成hello.exe, cl hello.c:   
生成hello.exe,运行它:   
使用echo %errorlevel%打印返回值:   
可以看到,返回的正是代码中return语句后面的0. 

Q: 可以返回的不是0吗? 
A: 当然可以。修改hello.c源代码为: [cpp] view plaincopy

#include <stdio.h> 

int main() 

printf("Hello World!"); 
return 1; 

再次编译运行后,使用echo%errorlevel%得到:   

Q: printf是一个函数,它的原型是什么? 
A: 在stdio.h中可以看到它的原型是: 
 


Q: 怎么这么复杂?紫色的部分是什么? 
A: 其实,很多原型中有一些附加的文本,它们在编译过程中并没有充当很有意义的东西,只是一种注释或者说明信息,在一些特定情况下起了作用,可以暂且不用特别关注它们。 
比如,_Check_return_opt_的宏定义是: 
 
再接着进入_Check_return_也可以看到类似的宏定义,依次分析。 




  • 3楼
  • 2012-07-03 00:29

Q: 那么printf函数的原型简化版就是 [cpp] view plaincopy

int __cdecl printf(const char * _Format, ...); 
? 
A:是的,也可以再简化成 [cpp] view plaincopy

int printf( const char * _Format, ...); 

Q: __cdecl是什么? 
A: 这是一种函数调用方式;对应的还有__stdcall,__ fastcall等等,它们有一定的区别,比如函数压栈顺序,参数优先保存在寄存器中等,c语言默认是__cdecl方式。 


Q: const char * _Format中的const可以去掉吗? 
A: 可以的,但是用上const表示不可更改的变量,printf函数内部不可以改变_Format指向的字符串,这是一种防御性编程的思想。 


Q: c++中的const不是表示常量吗? 
A: 是的,但是c语言中的const表示的是一个变量,但是不可更改。这个关键字在c语言和c++中是有区别的。 


Q: 举个例子。 
A: 比如在c语言中用const修饰的一个变量,不可以作为静态数组的参数个数,因为它是一个变量;在c++中,这是可以的。 
如hello.c代码修改为如下: [cpp] view plaincopy

#include <stdio.h> 

int main() 

const int size = 10; 
int arr[size]; 
return 0; 

出现编译错误: 
 
错误原因就是需要一个常量的大小表示数组的大小。

  • 4楼
  • 2012-07-03 00:32

Q:如果是c++,如何呢? 
A: 使用cl /TP hello.c将hello.c代码当成c++代码进行编译: 
 
没有出现错误。 


Q: 源代码不是hello.c,后缀名是.c, 怎么是当做c++代码编译了呢? 
A: 这主要是在于/TP参数的作用了: 
 
可以看到,/TP命令行将所有文件当成.cpp来编译,即也会把hello.c代码当成.cpp来编译。 


Q: 这么说来,编译器编译源代码不一定看后缀名的了? 
A: 当然是的,对于编译器来说,只要给文本即可,对于后缀只不过在通常情况下按照指定类型代码编译而已,可以指定将 
某种扩展名的代码当成特定类型代码编译的。 


Q: printf函数原型参数中的三个点表示什么? 
A: 它表示可变参数,即是参数个数不能确定,也许是1个,也许2个或者更多。



Q: 为什么要这么设计? 
A: 因为对于输出功能来说,该输出多少东西,设计者开始是不知道的,所以交给了程序员来是实现,编译器只需要根据获取 
到的参数最后正确转换给内部处理函数即可。 
比如: [cpp] view plaincopy

printf("Hello World!"); 
printf("My name is %s", "xichen"); 
printf("My name is %s, age is %d", "xichen", 25); 
Q: 那么printf函数返回什么呢?

A: 它返回成功输出的字节数。 
源代码: [cpp] view plaincopy

#include <stdio.h> 

int main() 

int ret; 
ret = printf("abc"); 
printf(" ret is %d\n", ret); 
ret = printf("Hello World!"); 
printf(" ret is %d\n", ret); 
ret = printf("My name is %s", "xichen"); 
printf(" ret is %d\n", ret); 

return 0; 

输出: 
 


  • 5楼
  • 2012-07-03 00:32

Q:如果有中文在字符串中,字节数怎么算呢? 
A: 这需要根据终端最终输出的字节数来得到了。 
在Windows下的记事本打开编写如下代码: [cpp] view plaincopy

#include <stdio.h> 

int main() 

int ret; 
ret = printf("abc中国"); 
printf(" ret is %d\n", ret); 

return 0; 

以ANSI格式保存,ANSI即以本地化编码格式保存; 
[Windows7]本地化语言默认为ANSI格式,在注册表中查看得到: 
 
是0804,0804代表简体中文,对应编码为GBK. 
再次查看cmd输出字符使用的编码: 
 
可以看到,确定编码格式是GBK. 
所以,上面的字符串中的"中国"是以GBK格式输出,即一个中文字符对应2个字节,即"abc中国"这个字符串总长度为7. 
所以,输出: 
 


Q: 如果将此源代码另存为UTF8格式保存,进行编译,最终的结果会不会变呢? 
A: 来看看。 
将源代码以UTF8格式保存: 

再次编译hello.c,并运行: 
 
可以看到,它的结果依然是7.

  • 6楼
  • 2012-07-03 00:32

Q:不是编码格式变成UTF8, 这两个中文每个字符占有3个字节吗?输出的字节数怎么还是7,它怎么认为每个中文占用2个字节呢? 
把UTF8格式的hello.c源代码用十六进制打开: 
 
可以看到,"中国"两个中文确实总共占用6个字节,可是为什么printf输出后计算的总字节数不是9而是7呢? 
A: 当然,源代码的编码格式以及源代码中字符串的编码格式和最终输出的编码格式不一定是一样的。正如前面所说,printf 
函数的返回值以终端真实输出的字节数为准,终端的字符编码是GBK,不管原来编写代码的对应编码是什么,和最终输出并没 
有必然关系,所以结果依然是7. 


Q:可执行文件hello.exe中的"abc中国"字符串对应的编码是什么呢? 
A: 将hello.exe用十六进制打开, 
 
可以发现,"中国"字符串用的是用GBK编码格式保存的;这里,应该可以理解为什么一直输出7了吧。 


Q: 编码这个东西还真有意思,如何改变cmd终端的编码格式? 
A: 这个编码格式也被称为代码页,可以使用chcp命令: 
直接输入chcp可以得到当前的代码页: 
 
更改为utf-8格式,使用chcp 65001 : 
 
此时再运行hello.exe,结果依然是7,但是"中国"字符变成了乱码。 
 
我想原因你应该知道了。 


Q:输出字符串,不也是可以用unicode宽字符的吗? 
A: 是的。 
如下代码:
[cpp] view plaincopy

#include <stdio.h> 

int main() 

int ret; 
ret = wprintf(L"abc%S", L"中国"); 
printf(" ret is %d\n", ret); 

return 0; 

[Windows-VS2010]输出: 
 
在Mac或者Ubuntu下,此代码暂未测试;因为关于wprintf以及%ls和%S,不同系统下表现不一致,可能需要修改代码。 


Q: printf函数里面的输出格式有好多种,但是和对应输出格式的数据如果不一致,会导致什么问题呢? 
A: 当然是可能会发生一些问题了,因为printf函数是动态解析格式字符串,然后将对应的数据来填充,可能出现本来是 
%d格式,但是却用了一个double的数据来填充,这样就可能导致错误。 


Q: 比如: 
[cpp] view plaincopy

#include <stdio.h> 

int main() 

printf("%d", 1.5); 

return 0; 
} 为什么编译阶段没有出现编译的错误呢? 
A: 因为编译器将"%d"当成了一个字符串,且并不去分析其中的格式,这个交给了printf函数的实现内部。其实,也就是把判断是否正确的责任交给了程序员,

如果程序员弄错了,那么结果就可能会跟着错。 


Q: printf函数解析格式输出的代码该怎么写? 
A: 这个需要根据字符串中的%为标志,出现这个,说明后面可能跟着一个对应格式的数据,比如d,那么说明是个整数,将栈中对应的数据来填充;如果是s,

  • 7楼
  • 2012-07-03 00:33

那就是取字符串来填充,依次类推;如果没有遇到%符号,那么按原样输出。 
一个简单且调用了printf的类printf函数: [cpp] view plaincopy

int __cdecl cc_printf( 
const char *format, 
... 


va_list argulist; 
int ret = 0; 

va_start(argulist, format); 

while (*format) 

if(*format != '%') 

putchar(*format); 
++ret; 
goto loop; 

else 

++format; 
switch (*format) 

case 'c': 

int value = va_arg(argulist, int); 
ret += printf("%c", (char)value); 
goto loop; 


case 's': 

char *value = va_arg(argulist, char *); 
ret += printf("%s", value); 
goto loop; 


case 'd': 

int value = va_arg(argulist, int); 
ret += printf("%d", value); 
goto loop; 


case 'o': 

int value = va_arg(argulist, int); 
ret += printf("%x", value); 
goto loop; 


case 'x': 

int value = va_arg(argulist, int); 
ret += printf("%x", value); 
goto loop; 


case 'X': 

int value = va_arg(argulist, int); 
ret += printf("%X", value); 
goto loop; 


case 'u': 

unsigned value = va_arg(argulist, unsigned); 
ret += printf("%u", value); 
goto loop; 


case 'f': 

double value = va_arg(argulist, double); 
ret += printf("%f", value); 
goto loop; 


default: 

goto loop; 




loop: 
++format; 


va_end(argulist); 

return ret; 
}

  • 8楼
  • 2012-07-03 00:33

Q: 还回到刚刚那个问题吧。 [cpp] view plaincopy

printf("%d", 1.5); 这个会输出什么呢?

A: 编译一下,输出: 
 


Q: 为什么会输出这个神奇的数字呢? 
A: 根据IEEE754的标准,双精度浮点数1.5的二进制表示形式是: 
00 00 00 00 00 00 F8 3F 
可以看到,低4个字节都是0,而%d正好只取了低4个字节,所以结果是0. 


为了方便地打印出double数据中各个字节的值,可以使用如下的union结构: [cpp] view plaincopy

union double_data 

double f; 
unsigned char data[sizeof(double) / sizeof(char)]; 
}; 
Q: 有的时候,printf格式串后的参数个数超过了格式串中的对应格式,会是什么结果?
A: 先写个代码: [cpp] view plaincopy

#include <stdio.h> 


int main() 

printf("%d %d", 1, 2, 3); 

return 0; 
} 输出的结果是什么呢?
 


Q: 为什么不是输出2和3呢,或者输出3和2呢? 
A: 看汇编: 
使用cl /Fa hello.c命令编译,得到hello.asm文件: [cpp] view plaincopy

; Listing generated by Microsoft (R) Optimizing Compiler Version 16.00.30319.01 

TITLE F:\c_codes\hello.c 
.686P 
.XMM 
include listing.inc 
.model flat 

INCLUDELIB LIBCMT 
INCLUDELIB OLDNAMES 

_DATA SEGMENT 
$SG2637 DB '%d %d', 00H 
_DATA ENDS 
PUBLIC _main 
EXTRN _printf:PROC 
; Function compile flags: /Odtp 
_TEXT SEGMENT 
_main PROC 
; File f:\c_codes\hello.c 
; Line 5 
push ebp 
mov ebp, esp 
; Line 6 
push 3 
push 2 
push 1 
push OFFSET $SG2637 
call _printf 
add esp, 16 ; 00000010H 
; Line 8 
xor eax, eax 
; Line 9 
pop ebp 
ret 0 
_main ENDP 
_TEXT ENDS 
END 
可以看到; Line 6标识的地方,有4个参数压栈的操作,依次是3, 2, 1和$SG2637, $SG2637也就是对应"%d %d"这个字符串。 
这个时候,你应该看明白了吧,调用printf("%d %d", 1, 2, 3);函数的时候,此函数有4个参数,但是从右向左依次压栈,最后解析 
"%d %d"字符串的时候,第一个格式就对应栈中下一个元素,也就是1, 依次找到第二个对应格式的数据,也就是2.所以最后输出 
了1和2.

  • 9楼
  • 2012-07-03 00:34

Q: 如果printf参数中第一个参数中的数据格式数多于后面的参数,那又会发生什么呢? 
A: 当然按照之前的原理,解析数据格式的时候会从栈里多解析一个数据,这很可能导致之后的运行错误,因为它很可能并不是 
程序员的意图。 


Q: 看到一些地方,printf函数后面的参数有好几个带有自增或者自减的操作,最后的结果真的很神奇,这有什么规律吗? 
A: 你是说,比如 [cpp] view plaincopy

#include <stdio.h> 


int main() 

int i = 1; 
printf("%d %d", ++i, ++i); 

return 0; 

最后的结果很可能不是你想要的,不过这个代码也可能让别人有好几种推测。所以,这种代码最好是不要写,要么写就写清楚的,大家 
都能明白的代码;同时,这种代码也不是能够很方便移植的,所以还是尽量少写这样的代码。


Q: 听说,printf函数是带缓冲的输出?这个和不带缓冲的输出有什么区别? 
A: 正如一个道理,cpu速度很快,外设运行速度很慢,为了避免这种速度差距导致cpu的效率被严重降低,所以,对于外设的操作,很多都有缓冲,包括显示器、键盘、硬盘等等。不带缓冲的也就是如果需要输出,那么立即输出,不会被缓冲系统来处理。 


Q: 我怎么感觉不到是不是缓冲输出的? 
A: 一个很容易感受到缓冲的是在cmd里面提示输入的地方。你可以输入一段数据,然后按回车,系统会进行对应处理。这个地方表示的是缓冲输入。 
对于缓冲输出,举个例子: [cpp] view plaincopy

#include <stdio.h> 
#include <stdlib.h> 

int main() 

int i = 1; 
char *buf = (char *)malloc(512); 
if(buf) 

setbuf(stdout, buf); 


printf("hello"); 
printf("xichen"); 

return 0; 

这里,申请了一个512字节的空间,然后使用setbuf函数将标准输出的缓冲区设置为buf. 
然后向标准输出输出2个字符串。 
在printf("xichen");行打断点,调试到此行,可以发现控制台什么也没输出,继续运行依然没有输出,没有输出的原因就在于它们被缓冲了。 
然后,修改代码,设置缓冲区为空: [cpp] view plaincopy

#include <stdio.h> 
#include <stdlib.h> 

int main() 

int i = 1; 

setbuf(stdout, NULL); 

printf("hello"); 
printf("xichen"); 

return 0; 

依然在printf("xichen");行打断点,调试: 
 
运行到断点处,此时查看控制台输出: 
 
可以看出,前面一句代码的字符串已经被输出了,这就是没有缓冲的。 


结束语 
前面写了这么多,不知不觉发现printf这个基本输出的函数确实不简单;但是了解了关于它的代码的底层信息,也会对它的理解更深刻, 
前面写的东西不一定只适用于printf, 很多内部实现的实验都可以从上面的内容找到一些影子,希望这些对大家有帮助。

  • 10楼
  • 2012-07-03 00:34
scanf函数,想说输入不容易!----小话c语言(3) 

Q: 前面是关于输出的,给个关于输入的代码吧。 A: 如下: [cpp] view plaincopy

#include <stdio.h> 

int main() 

int i; 
scanf("%d", &i); 
printf("%d", i); 
return 0; 

Q:%d格式和printf中的都表示整形是吧,为什么后面用了&i,用i的地址呢? 不能直接用下面的代码吗? [cpp] view plaincopy

scanf("%d", i); 
A: 这是因为,scanf是一个函数调用,如果仅仅将i以值的形式传进去,那么scanf函数最终不会也没有能力修改对于它来说是外部的i. 总之,原因在于c语言函数调用参数的值传递原理。 对于char *字符串,也同样是这样的原理。 [cpp] view plaincopy

char buf[32]; 
scanf("%s", buf); 
你会发现这里的buf也是地址。 
Q:说说这个代码的运行效果吧。 A: 暂且把开头那个代码保存为scanf.c.然后简单编译一下: [Mac-10.7.1 Lion Intel-based] gcc scanf.c   (因为笔者习惯在苹果系统下面编程,可以看到上面的提示Mac-10.7.1表示在mac系统10.7.1下面; 当然,因为某些场合可能需要切换到windows或者ubuntu,都会具体显示各个平台,以保证读者测试代码的正确性。) gcc版本是: 

  • 11楼
  • 2012-07-03 00:36
Q: 为什么gcc scanf.c执行后什么都没输出? A: gcc编译源代码,如果什么都没输出,那么说明已经正确编译成功了。 可以看到,使用ls -l | grep a.out 命令可以看到同目录下面已经有了a.out这个文件,这也是用上面的编译命令默认编译得到的可执行文件名。   
Q: 看看运行效果吧。 A: 在命令行下输入./a.out ,结果如下:   可以看到,输入完./a.out后回车,命令行进入了等待状态,输入100后回车,输出了100 Q: 怎么输出的100和它后面的命令行提示符连到一起了? A: 因为这个程序的输出的最后没有换行导致的。 修改代码: [cpp] view plaincopy

#include <stdio.h> 

int main() 

int i; 
scanf("%d", &i); 
printf("%d\n", i); 
return 0; 

重新编译,再运行:   可以看到,这次运行的输出和后面的命令行提示符已经分开了。如果查看此解释器bash的源代码,也可以看到,每次执行完应用程序, 解释器就会接着将自己的提示符再次输出,所以,如果让程序的执行结果不和它混在一起,一般都需要在代码加换行。 如果需要查看bash源代码,下面是苹果的官方开源代码网址:  http://opensource.apple.com/

  • 12楼
  • 2012-07-03 00:36
Q: scanf函数有返回值吗? A: 是的。如下是scanf函数的原型: [Mac-10.7.1 Lion Intel-based] [cpp] view plaincopy

int scanf(const char * __restrict, ...) __DARWIN_LDBL_COMPAT(scanf) __scanflike(1, 2); 
Q: 原型中__restrict是什么意思? A: 它是C99标准引入的,它只可以用于限定指针,并表明该指针是访问一个数据对象的唯一且初始的方式,这样编译器就可以做一些特定的优化。 举个例子吧: [cpp] view plaincopy

int f(int * x, int * y) 

*x = 0; 
*y = 1; 
return *x; 

上面这段代码,看起来似乎只返回了和x习惯的数据,但是不能肯定x和y究竟有没有关系,因为可能y和x是一样的; 所以,编译器不敢直接将代码编译优化成: [cpp] view plaincopy

int f(int * x, int * y) 

*x = 0; 
*y = 1; 
return 0; 

但是,如果加上__restrict关键字:[cpp] view plaincopy

int f_restrict(int *__restrict x, int *__restrict y) 

*x = 0; 
*y = 1; 
return *x; 

上面的代码表示:对x对应内容的修改只可能通过x,绝对不可能和y相关,所以编译器就可以做上面的优化了。 scanf函数原型,还有后面的__DARWIN_LDBL_COMPAT(scanf) __scanflike(1, 2)的意思,可以自己去查看,这里不介绍了。 
Q: scanf函数返回值是什么意思? A: 可以使用man scanf命令得到scanf函数的说明。   
可以看到,它返回的是被成功赋值变量的个数。 也就是说,上面的代码scanf("%d", &i); 如果返回为1,则表示成功输入了变量i. 修改代码来验证: [cpp] view plaincopy

#include <stdio.h> 

int main() 

int i; 
int ret; 
ret = scanf("%d", &i); 
if(ret == 1) 
printf("%d\n", i); 
else 
printf("input i error\n"); 
return 0; 

编译运行: 输入100:   重新运行,输入字符d   可以发现这里就进入了错误显示。 scanf函数的返回值能够很好地帮助我们分析需要输入的变量到底有没有被正确输入。

  • 13楼
  • 2012-07-03 00:36
Q: 如果调用scanf函数进行输入,但是输入了不正确格式的数据,再次循环输入,怎么不能输入了,且一直打印错误输入? 代码是: [cpp] view plaincopy

#include <stdio.h> 

int main() 

int i; 
int ret; 
while(1) 

ret = scanf("%d", &i); 
if(ret == 1) 
printf("%d\n", i); 
else 
printf("input i error\n"); 

return 0; 

运行时输入一个字符d :  会一直打印input i error不能停止,这是为什么? A: 这个原因就在于输入缓冲区了。还记得前面说过的缓冲区的概念吗?提到了输出缓冲区,scanf同样也有一个输入缓冲区。 调用scanf, scanf就会从它的输入缓冲区中来获取对应的数据,如果获取到就会传入再继续执行,否则就会等待输入。 当输入了字符d并回车后,scanf会将d和整形匹配,结果发现是不匹配的,所以会打印input i error信息,接着再次进入循环, 但是之前的输入缓冲区中依然保留着字符d呢,所以又会继续打印错误信息,依次循环永不停止。 
Q: 那应该怎么办呢? A: 很简单,如果发现输入数据错误,那么清空输入缓冲区即可。使用rewind(stdin);即可, 修改后的代码为: [cpp] view plaincopy

#include <stdio.h> 

int main() 

int i; 
int ret; 
while(1) 

ret = scanf("%d", &i); 
if(ret == 1) 
printf("%d\n", i); 
else 

rewind(stdin); 
printf("input i error\n"); 


return 0; 
} 运行:
 
Q: 可以使用fflush(stdin); 来清空输入缓冲区吗? A: c语言标准并没有对此规定可行,在笔者的mac系统下此函数就无效;所以,最好不要用这个。 不过,因为不同系统都可能存在差异,就算上面用的rewind(stdin); 也许在某些系统上也无效;所以,有个 更好的建议,那就是使用如下代码: [cpp] view plaincopy

int ch; 
while((ch = getchar()) != EOF && ch != '\n') 

Q: 我写了如下代码,scanf用于输入两个整形数,为什么总是有问题? [cpp] view plaincopy

#include <stdio.h> 

int main() 

int i, j; 
int ret; 
ret = scanf("%d,%d", &i, &j); 
if(ret == 2) 
printf("%d %d\n", i, j); 
else 
printf("input i or j error\n"); 
return 0; 

运行:   
A: 你需要注意scanf的输入格式,"%d,%d",两个整形中间是有个逗号的,但是看看你的输入,中间是个空格,并没有 逗号,所以会导致错误。所谓,格式化输入,你得符合它的格式才行。   和这个问题类似的还有: scanf("%d\n", &i); 格式串中有\n,所以需要输入两次回车才能继续; scanf("%2d%2d", &i, &j); %2d指整形占用两位,所以当输入1234时, 12被赋值给i, 34被赋值给j; float f; scanf("%4.1f", f); scanf的格式串中没有类似小数位数的格式,所以这样的输入经常会导致输入错误; 等等。

  • 14楼
  • 2012-07-03 00:37
Q: 我想输入一个double型浮点数,怎么会不管怎么输入都得不到正确的结果? [cpp] view plaincopy

#include <stdio.h> 

int main() 

double d; 
scanf("%f", &d); 
printf("%f\n", d); 
return 0; 

运行:   A: 这个问题在于, scanf格式中的%f表示输入的是一个float类型,在笔者的平台上是4字节的,而实际上却被保存 在double类型变量中,所以它被保存到变量d的低4个字节中(intel小端),但是整个double变量的值却和低4个字节的 值根本不能直接运算,可以参考IEEE 754关于浮点数的标准。 其实,在编译过程中应该也会有警告:   解决这个问题不复杂,只需要将格式%f改为%lf即可。 
Q: 写了一个代码,scanf用于输入字符,但是第二个需要输入的字符却使用不正确,为什么呢? [cpp] view plaincopy

#include <stdio.h> 

int main() 

char ch1, ch2; 
scanf("%c%c", &ch1, &ch2); 
printf("|%c|%c|\n", ch1, ch2); 
return 0; 

运行: 输入a然后回车准备输入第二个字符,但是没有进入等待输入状态,却直接输出了2个字符,且输出的不正确。   A: 其实这里,主要是因为在输入a后的回车也被当做了一个字符用于存储在b当中导致的。记得,scanf在输入整形、浮点型等 数据的时候可以忽略回车、空格、制表符等字符,但是输入格式为%c的时候就不这样了,此时它会很认真。 所以,将输入的代码改为scanf("%c\n%c", &ch1, &ch2); 即可满足你的要求。 

Q: scanf函数和printf函数一样都有输入或输出的变量个数和格式中的变量个数不一致的情况吧,那么运行结果会如何呢? A: 是的,当出现这样的不一致的时候,可能导致运行结果匪夷所思。当然,可以根据分析scanf的源代码得到不同情况下的运行结果。 比如,scanf("%d", &i, &j); 这种情况下, j不会被输入,因为格式中仅有1个,只会向i中输入,&j仅仅被当做一个参数传进来,却没有使用。 再比如, scanf("%d%d", &i); 这种情况下,多余需要被输入的整数可能导致某个内存被意外改写甚至崩溃; 当然,不管怎么样,上面这样的写法终究可能导致一些问题,不过有一点是肯定的,一个优秀的编译器会给出相应的警告信息,这样的代码 不要写在项目中,把它当做研究c语言内部机制的一种方式即可。 

Q: 这个scanf函数还真有意思,到底还有多少有意思的? A: 其实,对于输入概念来说,到底用什么格式来输入是五花八门的,在特定需求下可能产生一种别于寻常的输入方式,可能由此产生了一种 新的输入格式%XXXX. 举些例子: 类似%*d的方式表示此输入的整数被忽略; scanf("%d%*d%d", &i, &j); 输入: 100 2 200 然后回车,100被保存在i中,2被忽略, 200被保存在j中; char s[32]; scanf("%[abcd]", s); %[abcd]表示如果输入的字符是a,b,c,d中的任何一个,那么接收,否则不接收; 如果输入 abr3dc 然后回车,那么字符串ab将被保存在s中。 这里可以看到正则表达式的影子。 查看c标准,可以发现%[abcd]也可以用%[a-d]来表示。 
使用%i可以输入整数,但是可以输入八进制或者十六进制的形式,这是它和%d作为输入整数格式的不同之处: scanf("%i", &i); 输入012然后回车,10会被保存在i中; 如果输入0x12, 那么18会被保存在i中; 

Q: 看来scanf还是很精彩的,不过要是想全面学习它的格式规则,还真是不容易。 A: 是的,c语言很底层,越是底层的语言,越是深入细节就越需要精深的知识去解释,犯错误不是c语言的错, 而是程序员的错。

  • 15楼
  • 2012-07-03 00:37
基本输入输出,基本但不简单----小话c语言(4) 

开头将文章中代码所在的环境介绍下: [Mac-10.7.1 Lion Intel-based] 
Q: 看到stdio.h中有这么多关于输入或者输出的函数,怎么会这么多? A: 其实基本的函数不多,不过从易于上层使用的角度,又被封装出很多特定情况的函数接口,所以才显得很多。
Q: 比如关于字符操作的函数有getc, fgetc, getch, getche还有getchar,它们有什么区别吗? A: 一个重要的区别是getc, fgetc, getchar是标准函数,而getch和getche不是标准规定的。不是标准,就意味着它的实现可能在不同平台有不一样的表现。正如getch一 样,windows平台被包含在conio.h头文件中,运行到getch,可能需要等待输入,当有输入时就会运行过去不需要再按回车; getche在windows下和getch类似,不过多了最后的字母e,它表示echo,即在输入后会回显输入的字符。不过它们在mac平台的表现却不 大一样,这里不做具体介绍。 现在介绍下getc, fgetc和getchar.用man getc得到如下信息:   
可以发现,getc基本等同于fgetc,getchar是getc的特殊形式,是从标准输入获取字符。 
Q: getch和getche接收了输入即可继续运行,而getchar函数当缓冲区中没有数据时,需要输入字符最终回车才能继续执行,这是不是意味着getch和getche是不带缓冲的,而getc, fgetc和getchar是带缓冲的? A: 是的。按照标准来说,getc和fgetc在不涉及交互的时候是全缓冲模式(在缓冲区大小满后提交数据),如果是涉及到交互(如标准输入),那么是行缓冲;而getchar就行缓冲模式(回车会提交输入缓冲区数据,不过如果缓冲区已经有数据了就不需要等待回车了). 
Q: 既然fgetc是接收输入的字符,返回值用char或者unsigned char不就行了,为什么用int呢? A: 这个主要是因为文件结束或者读写文件出错的标志被规定成EOF,也就是-1导致的。unsigned char根本取不到-1这个值,而如果用char做返回值的话,它无法分辨0xFF字符和EOF,因为这两个数值都被char认为是-1,所以它也不能作为返回值。 举个例子吧: 使用如下代码向一个文件中只写入0xFF字符,然后保存: [cpp] view plaincopy

#include <stdio.h> 

int main (int argc, const char * argv[]) 

FILE *fp = fopen("test", "w"); 
if(fp) 

fputc(0xFF, fp); 
fclose(fp); 


return 0; 

运行完,用hexdump确认此文件中的数据为0xFF:  
然后使用如下代码读取test文件中的数据,并打印出来: [cpp] view plaincopy

#include <stdio.h> 

int main (int argc, const char * argv[]) 

FILE *fp = fopen("test", "r"); 
if(fp) 

char ch; 

printf("open test ok\n"); 
while((ch = fgetc(fp)) != EOF) 

printf("%d", ch); 


printf("read test end\n"); 
fclose(fp); 


return 0; 

运行后可以发现输出结果如下,这就意味着fgetc执行是遇到0xFF时被当做了EOF而导致结束了。   
Q: 上面提到getchar等同于getc(stdin), stdin到底是什么? A: [Mac-10.7.1 Lion Intel-based] stdio头文件中: [cpp] view plaincopy

#define stdin __stdinp 
#define stdout __stdoutp 
#define stderr __stderrp [cpp] view plaincopy

extern FILE *__stdinp; 
extern FILE *__stdoutp; 
extern FILE *__stderrp; 
可以看到,它们只是FILE *类型的一个变量,对于任何一个应用程序,使用c运行时库都会默认自动为应用程序创建这3个文件句柄。

  • 16楼
  • 2012-07-03 00:38
Q: 还有关于字符串输入的gets函数,听说使用它进行输入有风险的,如何表现的? A: 因为它没有对于输入数据大小进行确定,也就是可能导致输入的数据过多覆盖了不该覆盖的数据导致出问题。 举个例子: [cpp] view plaincopy

#include <stdio.h> 

#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

char ch = 'a'; 
char buf[4]; 
char *ret = gets(buf); 
if(ret != NULL) 

PRINT_STR(ret) 

PRINT_CH(ch) 

return 0; 

运行: 如果输入3个字符hel后回车,得到的结果是正常的:   
如果输入了4个字符hell后回车,结果如下:   可以看到ch字符的数据已经不正常了;
如果输入5个字符hello后回车,结果如下:   可以看到ch字符已经被覆盖为hello中的o了。这和刚刚输入4个字符的方式都是发生了缓冲区溢出,也就是超过了缓冲区buf的大小,覆盖了其它数据。 至于ch为什么是字符o, 这需要明白:ch和buf都被保存在栈中,且ch和buf相邻,如果栈是从高到低,那么ch保存在高地址,数据hello将前4个字节保存在buf中,最后一个o就被保存在了ch所在的地址区域里面。 
Q: 既然这样,那么使用什么可以更好地避免缓冲区溢出呢? A: 可以使用fgets函数,它的一个参数就是缓冲区大小。 [cpp] view plaincopy

char *fgets(char * restrict str, int size, FILE * restrict stream); 
如果读取成功,函数返回读取字符串的指针; 如果开始读取直接遇到文件结尾,返回NULL,缓冲区数据和原来保持一致; 如果读取出错,返回NULL, 缓冲区数据不确定; 
Q: fgets如果返回NULL,可能是直接遇到文件结束或者获取出错,怎么区分呢? A: 这就需要如下两个函数了:feof和ferror. [cpp] view plaincopy

int feof(FILE *stream); [cpp] view plaincopy

int ferror(FILE *stream); 
如果遇到文件尾,那么feof返回非0的数值,否则返回0.
如果遇到文件操作错误,那么ferror返回非0的数值,否则返回0.

  • 17楼
  • 2012-07-03 00:39
Q: 做一个测试吧,从一个文件中不断读取数据,然后打印出来。 A: 首先,先写个脚本,向一个文件中写入100个字符,为依次重复10次0123456789. [python] view plaincopy

#!/bin/bash 

i=0 
j=0 

while [ $i -lt 100 ]; 
do 
let "j = i % 10" 
echo -n $j >> numbers 
let "i = i + 1" 
done 
执行它,会在同目录下找到一个numbers的文件,使用vi打开确认一下内容:  现在使用fgets函数将此文件的数据读出来: [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 

#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

FILE *fp = fopen("numbers", "r"); 
if(fp) 

char buf[11] = {0}; 
while(fgets(buf, sizeof(buf), fp) != NULL) 

if(!ferror(fp)) 
PRINT_STR(buf) 
else 
PRINT_STR("ferror happens...") 

memset(buf, 0, sizeof(buf)); 

fclose(fp); 


return 0; 

这里,为了分便显示结果,缓冲区被设置大小为11,最后一个字节保存结束符\0, 前10个字符保存0~9. 运行结果:   
Q: fgets会自动在缓冲区结尾加上\0作为结束符是吧? A: 是的。这和strcpy在这个方面是一致的。不过,fgets也有自己的特点,正如gets一样,遇到回车符,它会提前结束;如果遇到回车符,而且缓冲区还没填满,那么回车符会被填充到缓冲区中,最后再加上\0作为结束符。 看个例子: [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

char buf[3]; 
char *ret = fgets(buf, sizeof(buf), stdin); 
if(ret) 

PRINT_D(strlen(buf)) 
PRINT_STR(buf) 


return 0; 

运行: 输入h然后回车, 结果:   可以发现回车字符被保存在了buf中.它和gets在这方面不一致,这是需要注意的。

  • 18楼
  • 2012-07-03 00:40
Q: 如果不希望到换行就结束,也可以指定缓冲区大小,该怎么办? A: 可以使用fread(不过它是适用于二进制读的函数,用于文本方式可能在某些情况下出错,需要小心). [cpp] view plaincopy

size_t fread(void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream); 
ptr: 表示读入数据保存的地址; size: 表示每组数据的大小; nitems: 表示读入数据的组数; stream: 表示文件指针; 返回值: 成功读取数据的组数;如果返回NULL,可能是直接遇到文件尾或者读取出错,需要用feof和ferror来区分;当然,如果读取不成功,返回值会比传入的参数nitems要小。 这个函数使用的时候要注意:正因为它是用于二进制读的,它不会在读取ok后自动为缓冲区最后加上\0作为结束符,这个要小心;如果第二个和第三个参数互换了,那么返回值也会发生相应改变,这也需要小心。 举个例子吧: [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

char buf[3] = {0}; 
size_t ret = fread(buf, sizeof(buf) - 1, 1, stdin); 
if(ret == 1) 

PRINT_STR(buf) 


return 0; 

可以看到,开始就将buf初始化为0了,fread的第二个参数是buf总大小减去1的数值,因为后面把读取的数据当成char *字符串,最后一个字节需要保存为\0. 第三个参数为1表示1组数组,第二个参数表示每组数据大小为sizeof(buf) - 1. 最后一个参数表示从标准输入stdin读取。 需要注意:正如之前说过,文件读写为交互设备时,是属于行缓冲,需要输入回车来提交缓冲区。 运行:   输入he并回车,可以看到打印了he. 如果输入h并回车,那么效果如下:   可以发现回车字符被保存在了buf中。 在这里不用太小心,如果输入多余2个字符,不会发生缓冲区溢出,因为fread有参数已经标志了缓冲区大小。 
Q: 上面是关于文件输入,文件输出的fputc, putc, putchar是不是也和fgetc, getc, getchar的关系类似? A: 是的,使用man putc可以看到它们之间的关系。   
Q: 标准中也没有putch和putche么? A: 是的,实际上在mac系统默认下也没发现它们的非标准实现。 
Q: 字符串输出的fputs和puts有什么区别么? A:  可以看到,fputs将字符串向指定文件输出,但是并不会自动加上换行字符;而puts会自动加上换行字符。 另外,fputs返回EOF表示操作失败,返回非负数表示成功;所以判断fputs是否成功最好使用if(fputs(xxxx) != EOF)或者if(fputs(xxx) == EOF)来表示。 如下代码: [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

int ret = fputs("hello", stdout); 
if(ret != EOF) 

PRINT_D(ret) 


return 0; 

执行:   可以看到,输出的hello后面并没有换行。

  • 19楼
  • 2012-07-03 00:40
puts函数返回值同fputs. puts的使用: [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

int ret = puts("hello"); 
if(ret != EOF) 

PRINT_D(ret) 


return 0; 

运行结果:   可以看到puts输出后自动加了换行。 
Q: 文件输入和输出可以像printf和scanf一样有格式化操作么? A: 当然可以了,printf和scanf只是文件输入输出的一个特例而已。 fprintf函数和fscanf函数可以实现此功能。 [cpp] view plaincopy

int fprintf(FILE * restrict stream, const char * restrict format, ...); [cpp] view plaincopy

int fscanf(FILE *restrict stream, const char *restrict format, ...); 
可以看到,它们和printf和scanf很相似,除了第一个参数表示文件指针,对于printf也就是fprintf(stdout, xxx)的特例;scanf也就是fscanf(stdin, xxx)的特例。
不过,fprintf还可以用文件指针stderr表示错误输出,它是不带缓冲的输出。 fprintf的返回值表示成功输出的字符字节个数,printf的返回值和它一致。 fscanf返回值表示成功输入的变量个数。 这里不做更多说明了。 
Q: 一直提到的文件句柄,它到底是什么? A: [cpp] view plaincopy

typedef struct __sFILE { 
unsigned char *_p; /* current position in (some) buffer */ 
int _r; /* read space left for getc() */ 
int _w; /* write space left for putc() */ 
short _flags; /* flags, below; this FILE is free if 0 */ 
short _file; /* fileno, if Unix descriptor, else -1 */ 
struct __sbuf _bf; /* the buffer (at least 1 byte, if !NULL) */ 
int _lbfsize; /* 0 or -_bf._size, for inline putc */ 

/* operations */ 
void *_cookie; /* cookie passed to io functions */ 
int (*_close)(void *); 
int (*_read) (void *, char *, int); 
fpos_t (*_seek) (void *, fpos_t, int); 
int (*_write)(void *, const char *, int); 

/* separate buffer for long sequences of ungetc() */ 
struct __sbuf _ub; /* ungetc buffer */ 
struct __sFILEX *_extra; /* additions to FILE to not break ABI */ 
int _ur; /* saved _r when _r is counting ungetc data */ 

/* tricks to meet minimum requirements even when malloc() fails */ 
unsigned char _ubuf[3]; /* guarantee an ungetc() buffer */ 
unsigned char _nbuf[1]; /* guarantee a getc() buffer */ 

/* separate buffer for fgetln() when line crosses buffer boundary */ 
struct __sbuf _lb; /* buffer for fgetln() */ 

/* Unix stdio files get aligned to block boundaries on fseek() */ 
int _blksize; /* stat.st_blksize (may be != _bf._size) */ 
fpos_t _offset; /* current lseek offset (see WARNING) */ 
} FILE; 
可以看到它封装了操作文件的缓冲区、当前位置等信息,这也能很好地体现带缓冲的事实。比如,fopen函数,它和open函数的一大区别就是是否使用用户层缓冲。 
Q: fopen和open的关系是什么样子的? A: open是POSIX标准,也是基于Unix系统的系统调用。fopen是C语言标准,它的内部当然必须调用open系统调用才能完成真正功能。 
Q: 给个使用open, read, write, close函数的例子吧。 A: 首先使用echo命令创建一个内容为hello的文件,文件名为test. [python] view plaincopy

echo hello > test 
然后编写如下代码: [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 
#include <fcntl.h> 
#include <unistd.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

char buf[32] = {0}; 
ssize_t ret; 
int file = open("test", O_RDONLY); 

if(file < 0) 

perror("open file error"); 
return -1; 

ret = read(file, buf, sizeof(buf)); 
if(ret > 0) 
PRINT_STR(buf) 

close(file); 

return 0; 

运行,结果为: 

  • 20楼
  • 2012-07-03 00:40
Q: 我想在文件开头插入一个字符,用如下的代码怎么不行? [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 
#include <fcntl.h> 
#include <unistd.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

FILE *fp = fopen("test", "r+"); 
if(!fp) 

perror("fopen error"); 
return -1; 

fseek(fp, 0, SEEK_SET); 
if(fputc('a', fp) == EOF) 
perror("fputc error"); 

fclose(fp); 

return 0; 

本来test文件里面的内容是hello\n,执行上面的程序后,为什么不是ahello\n,而是aello\n ? A: 这个问题的原因在于fputc, fputs, fwrite等写操作的函数均为覆盖写,不是插入写导致的。如果需要将文件读出来,将文件开头插入字符a,然后将读出的数据全部追加到文件后面,这样才行。 
Q: 一直听到二进制文件和文本文件,它们到底有什么区别? A: 计算机底层最终只能处理所谓的二进制文件,文本文件只是人们将文件的内容做了抽象得到的一种特殊的二进制文件。不过,它们是有一定区别的,可 能在某些时候,不同的平台,它们的表现也不同。但是从理论上来说,二进制文件可能更节省空间,这一方面是因为二进制文件是文件最终的形式,另一方面是因为 可以用二进制的1比特表示数值1,甚至整形1,但是用文本方式,却至少用1字节。另外,不同平台对于回车换行,CR与LF, "\r\n"的表示方式不太一致,导致对于它的文本解读和二进制形式有不一致的地方。在mac上,文本形式的换行用'\n'表示,而在windows上, 文本换行是\r\n, 所以有时会发现同一个文件在不同操作系统下的换行显示会有不同。不过,二进制和文本方式读取也不是水火不相容的,正如下面的例子, 显示的结论没什么不同: 假设test文件中保存hello\n, 文本方式读: [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 
#include <fcntl.h> 
#include <unistd.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

int ch; 
FILE *fp = fopen("test", "rt"); 
if(!fp) 

perror("fopen error"); 
return -1; 

while ((ch = fgetc(fp)) != EOF) 

printf("ch:%c %d\n", ch == '\n' ? 'N' : ch, ch); 


fclose(fp); 

return 0; 

二进制读: [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 
#include <fcntl.h> 
#include <unistd.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

int ch; 
FILE *fp = fopen("test", "rb"); 
if(!fp) 

perror("fopen error"); 
return -1; 

while ((ch = fgetc(fp)) != EOF) 

printf("ch:%c %d\n", ch == '\n' ? 'N' : ch, ch); 


fclose(fp); 

return 0; 

二个应用程序的运行结果均为:   显示为N的地方为换行字符。

  • 21楼
  • 2012-07-03 00:41
Q: 有的时候,想要直接将一个整形数据以二进制形式写入文件中,有什么更分便的调用方式? A: putw可以实现你要的功能。 [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 
#include <fcntl.h> 
#include <unistd.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

FILE *fp = fopen("test", "w"); 
if(!fp) 

perror("fopen error"); 
return -1; 

putw(32767, fp); 

fclose(fp); 

return 0; 

运行完后,用hexdump test查看test文件的十六进制形式,可以发现32767对应int类型大小的数据已经保存在test中了。 当然,如果不嫌麻烦,可以使用如下的代码实现上面类似的功能: [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 
#include <fcntl.h> 
#include <unistd.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

int n = 32767; 
char *pn = (char *)&n; 
int i; 
FILE *fp = fopen("test", "w"); 
if(!fp) 

perror("fopen error"); 
return -1; 

for(i = 0; i < sizeof(n); ++i) 

fputc(*(pn + i), fp); 


fclose(fp); 

return 0; 

Q: 之前看过好多关于文件打开方式的字符串,"rb", "w+"等等,到底怎么很好地明白它们的打开方式? A: 其实很简单。以r开头的方式打开,如果文件不存在必然失败;打开方式含有b即表示是二进制方式,如果没有b或者有t,那就说明是文本方式;如果打开方式中有+,那么表示可读可写; 
Q: 很多时候,如果读取输入缓冲区的数据读多了,想放回去怎么办? A: 可以使用ungetc函数。 [cpp] view plaincopy

int ungetc(int, FILE *); 
举个例子吧: 从标准输入读取一个十进制整数和最后一个分隔符(不是十进制整数字符),然后打印这个整数和最后的分隔符。 [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 
#include <fcntl.h> 
#include <unistd.h> 
#include <ctype.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

int n = 0; 
int ch; 
char end_ch; 

while ((ch = getchar()) != EOF && isdigit(ch)) 

n = 10 *n + (ch - '0'); 

if(ch != EOF) 

ungetc(ch, stdin); 

end_ch = getchar(); 

PRINT_D(n) 
PRINT_CH(end_ch) 

  • 22楼
  • 2012-07-03 00:41

return 0; 

运行时,输入1234;然后换行,   可以发现,ungetc会将;字符重新放回输入流中,是的end_ch被赋值为;字符; 如果没有使用ungetc的话:   可以发现,end_ch被赋值为输入1234;后面的换行了。 
Q: 关于输入输出重定向已经听说了很多了,c语言中有函数可以实现重定向吗? A: 是的, freopen函数可以实现这个作用。 [cpp] view plaincopy

FILE *freopen(const char *restrict filename, const char *restrict mode, FILE *restrict stream); 
第一个参数表示需要重定向的文件位置;第二个参数表示重定向方式,如"w"为写方式;第三个参数表示被重定向的文件句柄; 下面有个代码将展示如何将stdout重定向到test文件,然后再恢复stdout的输出; 代码如下: [cpp] view plaincopy

#include <stdio.h> 
#include <string.h> 
#include <fcntl.h> 
#include <unistd.h> 
#include <ctype.h> 

#define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue)); 
#define PRINT_CH(ch) printf(#ch" is %c\n", (ch)); 
#define PRINT_STR(str) printf(#str" is %s\n", (str)); 

int main (int argc, const char * argv[]) 

FILE *file; 
int fd; 
fpos_t pos; 

// output to stdout 
fprintf(stdout, "1111"); 

// backup the stdout info 
fflush(stdout); 
fgetpos(stdout, &pos); 
fd = dup(fileno(stdout)); 

// redirect stdout to "test" file 
file = freopen("test", "w", stdout); 
if(!file) 

perror("freopen error"); 
return -1; 


// now the stdout is redirected to "test" file, file "test" will contain "hello" str 
fprintf(stdout, "hello"); 

// restore the stdout 
fflush(stdout); 
dup2(fd, fileno(stdout)); 
close(fd); 
clearerr(stdout); 
fsetpos(stdout, &pos); 

// now the stdout is as the default state 
fprintf(stdout, "2222"); 

return 0; 

运行结果:   可以看出,最开始的标准输出和最后的标准输出都正常显示在屏幕,第二个标准输出因为被重定向输出到了test文件中;查看test文件的内容:   不过,如果是在windows平台,恢复stdout的方式和上面的代码可能不一致。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值