函数参数类型必须在函数参数表中或紧跟其后的部分中说明吗?
函数参数必须在参数表中说明,除非你使用的是一种过时的编译程序,在这种情况下,你应该通过#ifdef指令来同时实现两种可能的说明方式。定义函数有两种方法。例如,以fool()和foo2()这样两个函数为例,它们都以一个字符指针作为参数,并且返回一个整型值。假设它们是按如下形式定义的:
/* old style*/
int
foo1(p)
char *p;
{
/*body ot function goes here*/
}
/*new style*/
int
foo2(char *p)
{
/*body of function goes here*/
}
旧方式的唯一好处在于当参数表很长时它显得更美观。新方式的好处在于它在提供函数定义的同时,还提供了函数原型。
这样,在定义了foo2()以后,如果相同的“.c”文件中有对foo2()的调用,编译程序就会根据定义中的参数检查函数调用中的参数。如果参数不匹配,编译程序就会报告出现严重错误(标准并不要求有这一步,但大多数编译程序中都有)。如果函数调用中的参数可以被转换为定义中的参数,它们就会被转换。只有当函数按新方式定义或使用了函数原型时,才会进行以上处理。如果函数按旧方式定义,或者没有使用函数原型,那么就不会进行参数转换,而且很可能也不会进行参数检查。
新方式的唯一缺陷在于至今仍有不支持它的编译程序(这些大多数是基于UNIX的编译程序,它们随操作系统一起提供给用户,并且不另外收费。另一方面,许多版本的UNIX也提供了遵循ANSI标准的C编译程序)。
如果你可能需要使用ANSI标准以外的C编译程序,你最好使用一个宏,它可以在支持函数原型和新的函数定义方式时被定义。在知道能支持函数原型的情况下,你可以让一个相应的头文件自动定义该宏:
#ifdef __ANSI__
#ifndef USE_PROTOS
#define USE_PROTOS 1
#endif
#endif
函数说明可以是这样的:
#ifdef USE_PROTOS
int fool(char*);
Int foo2(char*);
#else
int foo1();
int foo2():
#endif
函数定义可以是这样的:
int
#ifdef USE_PROTOS
foo1(char *p)
#else
foo1(p)
char *p;
#endif
{
/*body of function goes here*/
}
如果你的软件只运行在MS-DOS,MS-Windows或Macintosh个人计算机上,你就不必考虑旧方式,只管用新方式好了。
C语言main()应该总是返回一个值吗?
main()不必总是带有返回值,因为它的调用者,通常是COMMAND.CoM,并不怎么关心返回值。偶而,你的程序可能会用在一个批处理文件中,而这个文件会到DOS的errorLevel符号中检查一个返回码。因此,main()是否有返回值完全取决于你自己,但是,为了以防万一,给main()的调用者返回一个值总是好的。如 main()返回void类型(或者没有return语句),也不会引起任何问题。
函数参数的类型必须在函数头部或紧跟在其后说明口马?为什么?
ANSI标准要求函数参数的类型要在函数头部说明。在第20章中你将会发现,C语言最初设计于70年代,并且运行于UNX操作系统上,显然当时还没有什么ANSI C标准,因此早期的C编译程序要求在紧接着函数头部的部分说明参数的类型。现在,ANSI标准要求参数的类型应该在函数头部说明。以前的方法中存在的问题是不允许进行参数检查——编译程序只能进行函数返回值检查。如果不检查参数,就无法判断程序员传递给函数的参数类型是否正确。通过要求在函数头部说明参数,以及要求说明函数原型(包括参数类型),编译程序就能检查传递给函数的参数是否正确。
怎样在C语言程序中调用BIOS函数?
与前文中的例子一样,在使用象一setvideomode()这样的函数时,将频繁地调用BIOS函数。此外,就连前文例子中使用过的DOS函数(INT 21H,AH=01H和INT 21H,AH=09H)最终也要通过调用BIOS来完成它们的任务。在这种情况下,DOS只是简单地把你的DOS请求传给相应的低级BIOS函数。下面的例子很好地说明了这一事实,该例与前文中的例子完成相同的任务,只不过它完全跳过了DOS,直接调用了BIOS。# include <stdlib. h>
# include <dos. h>
char GetAKey(void) ;
void OutputString( char * );
main(int argc, char * * argv)
{
char str[128];
union REGS regs;
int ch;
/ * copy argument string; if none, use "Hello World" * /
strcpy(str, (argv[1] == NULL ? "Hello World" : argv[1]));
while ((ch = GetAKeyO) !=27){
OutputString(str);
}
}
char
GetAKey()
{
union REGS regs;
regs. h. ah = 0; /* get character */
int86(0xl6, &xegs, ®s);
return( (char)regs. h. al) ;
}
void
OutputString(char * string)
{
union REGS regs;
regs. h. ah = 0x0E; /* print character * /
regs. h. bh = 0;
/ * loop, printing all characters * /
for(; * string !='\0'; string+ + ){
regs. h. al= * string;
int86(0xl0, ®s, ®s);
}
}
你可以发现,唯一的变化发生在GetAKey()和OutputString()自身之中。函数GetAKey()跳过了DOS,直接调用键盘BIOS来获得字符(注意,在本例这个调用中,键入的字符并不在屏幕上显示,这一点与前文中的例子不同);函数OutputString()跳过了DOS,直接调用了Video BIOS来打印字符串。注意本例效率不高的一面——打印字符串的C代码必须位于一个循环中,每次只能打印一个字符。尽管Vidoeo BIOS支持打印字符串的函数,但C无法存取创建对该函数的调用所需的所有寄存器,因此不得不一次打印一个字符。不管怎样。运行该程序可以得到与前文例子相同的输出结果。
怎样在C语言程序中调用DOS函数?
其实,当调用printf(),fopen(),fclose(),名字以一dos开始的函数以及很多其它函数时,都将调用DOS函数。Microsoft和Borland还提供了一对名为int86()和int86x()的函数,使你不仅可以调用DOS函数,还可以调用其它低级函数。用这些函数可以跳过标准的C函数而直接调用DOS函数,这常常可以节省你的时间。下面的例子说明了如何通过调用DOS函数,而不是getch()和printf()函数,从键盘上得到一个字符并将其打印出来(该程序需要在大存储模式下编译)。# include <stdlib. h>
# include <dos. h>
char GetAKey(void);
void OutputString(char * );
main(int argc, char * * argv)
{
char str[l28];
union REGS regs;
int ch;
/ * copy argument string; if none, use "Hello World" * /
strcpy(str, (argv[1]== NULL ? "Hello World": argv[1])),
while ((ch = GetAKey()) ! =27){
OutputString(str);
}
}
char
GetAKeyO
{
union REGS regs;
regs.h. ah = 1; /* function 1 is "get keyboard character" * /
int86(0x21, ®s, ®s);
return( (char)regs. h. al) ;
}
void
OutputString(char * string)
{
union REGS regs;
struct SREGS segregs;
/ * terminate string for DOS function * /
* (string + strlen(string)) = '$';
regs.h. ah = 9; / * function 9 is "print a string" * /
regs.x. dx = FP_OFF(string) ;
segregs. ds= FP_SEG(string) ;
int86x(0x21, ®s, ®s, &segregs);
}
上例创建了两个函数来代替getch()和printf(),它们是GetAKey()和OutputString()。实际上,函数GetAKey()与标准c函数getche()更为相似,因为它与getche()一样,都把键入的字符打印在屏幕上。这两个函数中分别通过int86()(在GetAKey()中)和int86x()(在OutputString()中)调用DOS函数来完成所要求的任务。
可供函数int86()和int86x()调用的DOS函数实在太多了。尽管你会发现其中许多函数的功能已经被标准的C函数覆盖了,但你也会发现还有许多函数没有被覆盖。DOS也包含一些未公开的函数,它们既有趣又有用。DOS忙标志(DOS Busy Flag)就是一个很好的例子,它也被称作InDos标志。DOS函数34H返回指向一个系统内存位置的指针,该位置包含了DOS忙标志。当DOS正忙于做某些重要的事情并且不希望被调用(甚至不希望被它自己调用)时,该标志就被置为1;当DOS不忙时,该标志将被清除(被置为O)。该标志的作用是当DOS正在执行重要的代码时,把这一情况通知DOS。然而,该标志对程序员也是很有用的,因为他们能由此知道什么时候DOS处于忙状态。尽管从DOS 2.0版开始就有这个函数了,但因为Microsoft最近已经公开了这个函数,所以从技术角度上讲它已不再是一个未公开的函数。有几本很不错的书介绍了已公开和未公开的DOS函数,对这个问题有兴趣的读者可以去阅读这些书。
怎样操作由多字节字符组成的字符串?
假设你的程序既要处理英文文本(很容易纳As位字符,并且还能空出一位),又要处理日文文本(需要16位才能包含所有的可能性)。如果你用相同的代码来处理这两种不同国家的文本,你是否需要给每个字符,甚至英文符都分配16位呢?也许不必这样做,因为有些多字节字符的编码方法会保存关于是否需要多于一个字节的空间的信息。mbstowcs()(“多字节字符串到宽字符串”)和wcstombs()(“宽字符串到多字节字符串”)用于wchar—t类型的数组(其中每个字符占16位或两个字节)和多字节字符串(可能的话,一个字符会被存入一个字节中)。
你无法保证你的编译程序能以紧缩的方式存储多字节字符串(因为没有一种普遍接受的方法)。如果你的编译程序能帮助你处理多字节字符串,mbstowcs()和wcstombs()就是完成这部分工作的函数。
C语言的多字节字符(multibyte characters)是什么
多字节字符是使国际化的程序更容易编写的另一种途径。具体地说,它们有助于支持永远无法纳入8位字符的语言,例如汉语和日语。如果你的程序永远不需要使用除英语之外的其它任何语言,你可以不必了解多字节字符。你不得不承认这样一个事实:可能到处都有人想使用你的软件,但并不是人人都懂英语。幸运的是,已经有了可以把欧洲语言的各种特殊字符纳入8位字符集的标准(不幸的是,这样的标准有好几种,并且它们相互并不一致)。
到了亚洲,这个问题变得更复杂。有些语言的字符超过256个,例如汉语和日语,它们永远无法纳入8位字符集中(一个8位字符能存放O和255之间的一个数字,因此它能只有256种不同的值)。
幸运的是,C标准库已经开始解决这个问题。<stddef.h>定义了wchar_t类型,它的长度足以存放c程序能处理的任何语言中的任何字符。根据到目前为止的所有协议,16位已经足够了。这通常就是short类型,但最好还是相信编译程序开发商所提供的wchar_t的正确性,以免在short类型的长度发生变化时遇到麻烦。
函数mblen(),mbtowc()和wctomb()能将单字节字符串转换为多字节字符。如果你想了解更多的有关这些函数的信息,请查阅你的编译程序手册。
适用于整数和浮点数的数学函数分别有哪些?
运算符+,-,*和/(加、减、乘和除)对整数和浮点数都适用,而运算符%(求余)仅适用于整数。适用于浮点数的大多数函数在头文件math.h中说明。为了提高精确度,这些函数大多以双精度浮点数的精度进行操作。如果传递过来的参数不在其定义域内(函数的定义域是指函数参数有效值的集合),这些函数会返回一些不确定的值,并将变量errno置为EDOM。如果返回值太大或太小,无法用一个double类型表示(造成上溢或下溢),这些函数会返回HUGEVAL(表示上溢)或O(表示下溢),并将errno置为ERANGE,EDOM,ERANGE和HUGEVAL都在math.h中定义。
下面列出了在math.h中说明的函数的描述:
- double COS(double),double sin(double)和double tan(double)的参数都是一个弧度值,其返回值分别为该值的正弦值、余弦值和正切值。
- double acos(double),double asin(double)和double atan(double)的参数都是一个值,其返回值分别为该值的反正弦值、反余弦值和反正切值。传递给acos()和asin()的值必须在-1和1之间。
- double atan2(double x,double y)返回x/y的反正切值,不管x/y是否能表示成double类型(例如y为0时)。
- double cosh(double),double sinh(double)和double tanh(double)的参数都是一个弧度值,其返回值分别为该值的双曲正弦值、双曲余弦值和双曲正切值。
- double exp(double x),double log(double x)和double logl0(double x)的参数都是一个值,其返回值分别为e。,x的自然对数值和x的以10为底的对数值。当x为0或一个负数时,后两个函数都将分别导致一个范围错误(ERANGE)或一个定义域错误(EDOM)。
- double sqrt(double)将返回其参数的平方根值。当该参数为负数时,该函数将导致一个定义域错误(EDOM)。
- double ldexp(double n,double e)返回n*2e。这与整数的“<<”运算符有些相似。
- double pow(double b,double e)返回be。当b为O而e小于等于0时,或者当b小于O而e不是一个整数值时,该函数将导致一个定义域错误(EDOM)。
- double frexp(double n,int*i)返回n的尾数(mantissa),并将n的指数(exponent)存放在i所指向的整型变量中。尾数在o.5和1之间(不包括1本身),而指数是这样一个数,它将使n=mantissa*2exponent。
- double modl(double n,int *i)返回n的小数部分,并将n的整数部分存放在i所指向的整型变量中。
- double celt(double)和double floor(double)分别返回大于其参数的最小整数和小于其参数的最大整数。例如,ceil(-1.1)返回-1.O,而floor(-1.1)返回-2.0。
- double fmod(double x,double y)返回x/y的余数。这与整数的%运算符相似,但该函数的参数和返回值并不局限于整数。当y为O时,该函数将导致一个定义域错误(EDOM)。
- double fabs(double)返回其参数的绝对值(一个数量相同的数字,但永远是正数)。例如,labs(-3.14)返回3.14。
为什么编译程序提供了两个版本的malloc()函数?
包含了头文件stdlib.h后,你就可以在程序中使用malloc()和free()函数了。这些函数是编译程序从C函数库中包含到你的程序中的。有些编译程序还提供了一个独立的库,你可以要求编译程序用其中的版本来代替标准库中的malloc()和free()版本(只需在命令行中加入类似一lmalloc这样的标志)。malloc()和free()的替代版本和标准版本的功能完全一样,只不过前者被认为在对内存分配错误不那么宽容的代价下,能产生更好的执行效果。笔者在15年的C语言编程经历中从未使用过这些替代版本,但为了回答这个问题,笔者编写了一个大量使用malloe()和free()的简单的测试程序,并用一种非常著名的C编译程序,分使用和不使用malloc库两种情况对其进行了编译。结果笔者没有发现明显的差异,并且笔者怀疑该开发商在实现这两种版本时使用了相同的代码,因为两个版本的程序的大小是一样的。正因为如此,笔者也就不便指出该开发商的名字了。
以上的情况说明,也许不必去使用malloc()的其它版本,并且也不要指望它们会提高程序的性能。如果剖视(profiling)表明程序把大量时间花费在malloc()和free()上,并且通过改进算法也无法解决这个问题,那么你可以自己编写一个“缓冲池(pool)”分配函数,也许能提高程序的性能。
大量调用malloc()和free()函数的程序往往是为相同类型的数据分配内存和释放内存,这些数据具有固定的长度。当知道要分配和释放的数据的大小后,自己编写的缓冲池分配函数会比malloc()和free()运行得更快。一个缓冲池分配函数的工作方式是这样的:调用malloc()一次分配许多大小相同的结构,然后每次交付一个供使用。该函数通常从来不调用free(),它所使用的内存将一直保留到程序退出。例12.12给出了一个用于自定义类型struct foo的缓冲池分配函数。
例12.12一个缓冲池分配函数的例子
# include <stdio. h>
/ * declaration of hypothetical structure "foo" * /
struct foo {
int dummy1;
char dummy2;
long dummy3;
};
/ * start of code for foo pool allocator * /
# include <stdlib. h>
/ * number of foos to mallocO at a time * /
# define NFOOS 64
/*
* A union is used to provide a linked list that
* can be overlaid on unused foos.
*/
union foo_u {
union foo_u *next;
struct foo f;
};
static union foo_u * free_list ;
struct foo *
alloc_foo()
{
struct foo * ret = 0;
if (!free_list) {
int i;
free_list = (union foo_u * ) malloc(NFOOS
* sizeof (union foo_u));
if (free_list) {
for (i = 0; i<NFOOS-1; i+ + )
free_list[i]. next =
&iree_list[i + 1];
free_list [NFOOS -1 ]. next = NULL;
if (free_list) {
ret = &free_list ->f;
free_list = free_list ->next;
}
return ret;
}
void
free_foo(struct foo * fp)
{
union foo_u * up= (union foo_u * ) fp;
up ->next = free_list)
free_list = up;
}
int
main(int argc, char * * argv)
{
int i;
int n;
struct foo ** a ;
if (argc <2) {
fprintf(stderr, "usage: %s f\n" , argv[0]);
fprintf(stderr. "where f is the number of");
fprintf(stderr, "'foo's to allocate\n" ) ;
exit(1);
}
i = atoi(argv[l]);
a = (struct foo * * ) malloc(sizeof (struct foo * ) * i);
for (n = 0; n<i; n+ + )
a[n] = alldc-foo() ;
for (n = 0j n<i; n+ + )
free_foo(a[n]);
return 0;
}
笔者用30000这样一个参数编译并运行了上述程序,并将其结果与用malloc()和free()代替alloc_foo()和free_foo()的一个类似的程序进行比较,发现前者使用的CPU时间为O.46秒,而后者为0.92秒。
需要注意的是,使用缓冲池分配函数只能是最后的选择,它也许能提高速度,但它会造成内存的巨大浪费。此外,如果你不调用free(),而又没能小心地把从缓冲池中申请到的内存返回去,就会导致微妙的内存分配错误。
为什么变量名不能以下划线开始?
凡是以两个或一个下划线开始,后面紧跟着一个大写字母的标识符,不管它出现在哪里,都是保留给编译程序或标准库函数使用的。此外,凡是以一个下划线开始,后面不管跟着什么内容的标识符,如果它出现在文件范围内(即它不是出现在一个函数内),那么它也是被保留的。如果你用一个保留的标识符来作一个变量的名称,结果是没有定义的(程序可能无法编译,或者可以编译但会崩溃)。即使你能非常幸运地找到一个目前还没有被你的编译程序或函数库使用的标识符,你也应该记住这样的标识符是保留起来供将来使用的。因此,最好还是避免使用以下划线开始的变量名或函数名。
C语言中的信号(signal)是怎么回事?
信号是程序执行过程中出现的异常情况。它可能是由程序中的错误造成的,例如引用内存中的一个非法地址;或者是由程序数据中的错误造成的,例如浮点数被0除;或者是由外部事件引发的,例如用户按了Ctrl+Break键。你可以利用标准库函数signal()指定要对这些异常情况采取的处理措施(实施处理措施的函数被称为“信号处理函数”)。signal()的原型为:
#include <signal.h>
void(*signal(int hum,void(*func)(int)))(int);
这恐怕是你在C标准函数库中能见到的最复杂的说明了。如果你先定义一个typedef,理解起来就容易一些了。下面给出的sigHandler_t类型是指向一个程序的指针,该函数有一个int类型的参数,并且返回一个void类型:
typedef void(*sigHandler_t)(int);
sigHandler_t signal(int num , sigHandler_t func);
signal()有两个参数,分别为int类型和sigHandler_t类型,其返回值为sigHandler_t类型。以func参数形式传递给signal()的那个函数将成为第num号异常情况的新的信号处理函数。signal()的返回值是信号hum原来的信号处理函数。在设置了一个暂时的信号处理函数之后,你可以利用该值恢复程序先前的行为。num的可能值依赖于系统,并且在signal.h中列出。func的可能值可以是你的程序中的任意函数,或者是SIG_DFL和SLG_IGN这两个特别定义的值之一。SIG_DFL是指系统的缺省处理措施,通常是暂停执行程序;SIG_IGN表示信号将被忽略。
当下面这行代码被执行后,程序将不去响应按Ctrl+Break键这个信号,除非修改signal()函数,使其重新响应该信号。尽管hum的可能值依赖于系统,但SIGINT这个值通常用来表示用户试图中断程序运行的信号(在DOS下,为Ctrl+C或Ctrl+Break)。
signal(SIGINT,SIG_IGN)
有没有办法从一个或多个函数中跳出?
在极少数确实需要这样做的情况下,可以利用标准库函数setjmp()和longjmp()实现一种能从一个或多个函数中跳出的跳转(goto)。要正确地使用setjmp()和longjmp()函数,必须满足几个条件。首先,你必须包含setjmp.h头文件,该文件提供了setjmp()和longimp()函数的原型,并定义了jmp—buf类型。你需要把一个jmp—bur类型的变量作为一个参数传递给setjmp()和longjmp()函数,这个变量将包含使跳转发生所需的信息。
其次,你必须调用setjmp()函数来初始化jmp—bur变量。如果setjmp()函数返回0,则说明jmp_buf变量已被初始化;如果setjmp()函数返回其它值,则说明程序刚才通过调用longjmp()函数跳转到了对应于该值的位置。在后一种情况下,setjmp()函数的返回值就是程序传递给longjmp()函数的第二个参数。
从概念上讲,longjmp()函数的作用就好象是这样:当它被调用时,当前正在执行的函数便会返回;然后,调用这个函数的函数将返回;依此类推,直到调用setjmp()的函数成为正在执行的函数。程序的执行将跳转到调用setjmp()函数的位置,并从setjmp()函数返回那一点继续往下执行,但此时setjmp()函数的返回值已被置为传递给longjmp()函数的第二个参数。
换句话说,如果函数f()调用了setjmp(),然后又调用了函数g(),而函数g()调用了函数h(),函数h()调用了longjmp(),那么程序运行起来就好象h()立即返回了,然后g()立即返回,然后f()执行一次回到调用setjmp()的位置的跳转。
这就是说,为了使对10ngjmp()的调用能正常工作,程序必须已经调用setjmp(),并且还没有从调用setjmp()的函数中返回。如果这些条件得不到满足,那么longjmp()的行为是没有定义的(这意味着你的程序很可能会崩溃)。例12.9中的程序说明了setjmp()和longjmp()的用法。这个程序显然是为此而设计的,因为如果不使用setjmp()和longjmp(),程序就会更简洁些。总的来说,当你想使用setjmp()和longjmp()时,最好先找一种可以不使用它们的编程方法,因为它们容易被误用,并且会使程序难于阅读和维护。
例12.9 一个使用setjmp()和longjmp()的例子
# include <setjmp. h>
# include <stdio. h>
# include <string. h>
# include <stdlib. h>
# define RETRY_PROCESS 1
# define QUIT_PROCESS 2
jmp_buf env;
int nitems;
int
procItem()
{
char buf[256];
if (gets (buf) &&.strcmp(buf, "done")) {
if (strcmp(buf, "quit") ==0)
longjmp (env, QUIT_PROCESS );
if (strcmp(buf, "restart") ==0)
longjmp(env, RETRY_PROCESS);
nitems+ + ;
return 1;
}
return 0;
}
void
process()
{
printf ("Enter items, followed by 'done'. \n") ;
printf("At any time, you can type 'quit' to exit\n");
printf ("or 'restart' to start over again\n");
nitems = 0;
while (procItem())
}
void
main() {
for (; ;) {
switch (setjmp(env)) {
case 0:
case RETRY_PROCESS:
process () ;
printf("You typed in %d items. \n" ,
nitems);
break ;
case QUIT_PROCESS:
default:
exit(O);
}
}
}
C语言“局部环境(locale)”是什么
局部环境是对特定环境下程序要遵循的特定规则的一种描述,它对程序的国际化很有帮助。如果你要打印一笔钱的数目,你总是使用美元符号吗?不,如果你的程序要在英国运行,你就要使用英镑符号。在有些国家,货币符号要写在钱数的前面,而在有些国家,货币符号要写在钱数的后面。一个负数的负号要放在哪里呢?在美国写成1,234.56的一个数字,在另外一些国家中可能要写成1.234,56。同样的值在不同的国家中会有不同的表示规则。时间和日期又是如何表示的呢?简而言之,也是因国而异。如果一个程序员要编写一个必须在全世界运行的程序,那么这些情况就是使他头疼的部分技术原因。
幸运的是:部分差异已经被标准化了。C编译程序支持不同的“局部环境”,即程序在不同地方的不同表示规则。例如,函数strcoll()(string collate,字符串的依序整理)和strcmp()函数相似,但它能反映出不同国家和语言对字符串值进行排序和整理(collate)的方式。函数setlocale()和localeconv()提供了这方面的支持。
不幸的是:并没有一种标准化了的关于这些有趣的局部环境的清单。你的编译程序唯一能保证提供的只有“C”局部环境。这是一种通用的美式英语规则,对于码值在32和127之间的ASCII字符,这种规则工作得最好。尽管如此,如果你想正确地编写一个能在全世界运行的程序,那么从局部规则这个角度来考虑问题就是一个好的开端(接下来,如果你能再找到几种你的编译程序能支持的局部环境,或者让你的编译程序接受你定义的几种局部环境,那就更好了)。
怎样判断一个字符是数字、字母或其它类别的符号?
在头文件ctype.h中定义了一批函数,它们可用来判断一个字符属于哪一类别。下面列出了这些函数:---------------------------------------------------------------------------------------
函数 字符类别 返回非零值的字符
---------------------------------------------------------------------------------------
isdigit() 十进制数 0--9
isxdigit() 十六进制数 0--9,a—f,或A--F
isalnum() 字母数字符号 0--9,a--Z,或A--Z
isalpha() 字母 a--Z或A--Z
islower() 小写字母 a--Z
isupper() 大写字母 A--Z
isspace() 空白符 空格符,水平制表符,垂直制表符,换行符,换页符,或回车符
isgraph() 非空白字符 任何打印出来不是空白的字符(ASCII码从21到7E)
isprint() 可打印字符 所有非空白字符,加上空格符
ispunct() 标点符 除字母数字符号以外的所有非空白字符
iscntrl() 控制字符 除可打印字符外的所有字符(ASCII码从00到1F,加上7F)
----------------------------------------------------------------------------------------
与前文提到过的使用标准库函数的好处相似,调用上述这些宏而不是自己编写测试字符类别的程序也有三点好处。首先,这些宏运算速度快,因为它们的实现方式通常都是利用位屏蔽技术来检查一个表,所以即使是进行一项相当复杂的检查,也比真正去比较字符的值要快得多。
其次,这些宏都是正确的。如果你自己编写一个测试程序,你很容易犯逻辑上或输入上的错误,例如引入了一个错误的字符(或漏掉了一个正确的字符)。
第三,这些宏是可移植的。信不信由你,并非所有的人都使用同样的含PC扩充字符的ASCII字符集。也许今天你还不太在意,但是,当你发现你的下一台计算机使用的是Unicode字符集而不是ASCII字符集,你就会庆幸自己原来没有按照字符集中的字符值来编写程序。
头文件ctype.h中还定义了两个可以对字母进行大小写转换的函数,即函数toupper()和tolower()。如果toupper()函数的参数不是小写字母或tolOWel"()函数的参数不是大写字母,那么这两个函数的行为是没有定义的,因此,在调用这两个函数之前,你应该用函数islower()或isupper()来检查一下。
C语言中对内存进行操作的标准库函数有哪些?
有些函数可用来拷贝、比较和填写任意的内存块,它们都带有void。类型(并不指向任何具体类型的指针)的参数,可以处理指向任何类型的指针。有两个函数(有点象strncpy()函数)可用来拷贝信息。第一个函数是memmove(),它把内存中的内容从一个地方拷贝到另一个地方,不管源区域和目标区域是否有相互覆盖的部分。为什么要提到这两个区域是否相互覆盖呢?假设缓冲区中已有部分数据,而你要把它们移到“后面”,以腾出缓冲区前面的空间。例12.6给出了一个试图进行这项工作的程序,但它做得并不正确:
例12.6一个试图移动数据,结果毁掉数据的程序
static char buf[] =
{'R','I','G','H','T','\0','-','-','-'};
int
main()
{
int i;
for (i = 0; i<6; ++i)
{
buf[i + 3] = buf[i]i
}
}
上述程序的意图是把buf从"RIGHT"改为“RIGRIGHT”,这样就可以在前面三个字节中存入其它数据。不幸的是,程序并没有真正实现这个意图。如果把for循环展开(或者通过调试程序来观察程序正在做什么),你就会发现程序实际上是在这样做:
buf[3] = buf[0];
buf[4] = buf[l];
buf[5] = buf[2];
buf[6] = buf[3];
buf[7] = buf[4];
buf[8] = buf[5];
buf[9] = buf[6];
数据的移动效果如图12.6a所示(新拷贝的数据用粗黑体表示)——该程序毁掉了它原来想移动的某些数据。
R I G H T \0 - - -
R I G R T \0 - - -
R I G R I \0 - - -
R I G R I G - - -
R I G R I G R - -
R I G R I G R I -
R I G R I G R I G
图12·6a“移动”相互覆盖的数据的错误方法
在移动或拷贝相互覆盖的数据时,有这样一个简单的原则:如果源区域和目标区域相互覆盖,并且源区域在目标区域的前面,则应该从源区域的末尾开始按逆向顺序依次移动数据,直到达到源区域的头部;如果源区域在目标区域的后面,则应该从源区域的头部开始移动数据,直到达到源区域的末尾。请看图12.6b。
R I G H T \0 - - -
R I G H T \0 - - \n
R I G H T \0 - T \0
R I G H T \O H T \0
R I G H T G H T \0
R I G H I G H T \O
R I G R I G H T \O
< < < L E F T \O
L < < L E F T \O
L E < L E F T \O
L E F L E F T \0
L E F T E F T \O
L E F T \0 F T \O
图12.6b“移动”相互覆盖的数据的正确方法
解释这些情况的目的是为了指出这样一点:memmove()函数知道上述原则,它能保证用正确的方法拷贝数据,不管数据是否相互覆盖。如果在拷贝或移动数据时你并不知道源区域和目标区域是否相互覆盖,你就应该使用memmove()函数。如果你能确定它们并没有相互覆盖,那么可以使用memcpy()函数,这样能稍快一些。
memcmp()函数与strncmp()函数基本相似,只是它在遇到NUL字符时不会结束。memcmp()函数不能用来比较结构的值。假设你有下面这样一个结构:
struct foo{
short s;
long 1;
}
并且假设你的程序将运行在一个short类型为两个字节(16位),long类型为4个字节(32位)的系统上。在32位的计算机中,许多编译程序会在s和l之间加入两个字节的“无用信息”,以使I从下一个字的边界开始。如果你的程序运行在低位优先(低位字节存放在低位地址中)的计算机上,那么上述结构展开后可能会如下所示:
struct foo byte[O] s的低位字节
struct foo byte[1] s的高位字节
struct foo byte[2] 无用信息(使l从一个long类型边界开始)
struct foo byte[3] 无用信息(使l从一个long类型边界开始)
struct foo byte[4] l的最低位字节
struct foo byte[5] l的次低位字节
struct foo byte[6] l的次高位字节
struct foo byte[7] 1的最高位字节
用memcmp()函数比较具有相同的s和l值的两个foo结构时,其结果并不一定相等,因为所加入的“无用信息”并不一定相同。
memchr()函数与strchr()函数基本相似,只不过它是在指定的一块内存空间中查找一个字符串,并且它在遇到第一个NUL字符时不会结束。
memset()函数对所有的C程序员都是很有用的,它能把某种字节拷贝到指定的内存空间中。memset()函数的一种常见的用法是把某种结构全部初始化为零字节。如果p是指向一个结构的指针,那么语句memset(p,'\0',size01 * p);
将把p所指向的对象全部改写为零(NUL或'\O')字节(那些使结构成员从字边界开始的“无用信息”也会被改写,但这样做没有关系,因为这些信息没有用,所以谁也不会在乎它们被改写成什么样子)。
C语言中对字符串进行操作的标准库函数有哪些?
简单的回答是:(string.h)中的函数。C语言没有固有的字符串类型,但c程序可以用以NUL(’\O’)字符结束的字符数组来代替字符串。C程序(以及c程序员)应该保证数组足够大,以容纳所有将要存入的内容。这一点可以通过以下三种方法来实现:
(1)分配大量的空间,并假定它足够大,不考虑它不够大时将产生的问题(这种方法效率高,但在空间不足时会产生严重的问题);
(2)总是分配并重新分配所需大小的空间(如果使用realloc()函数,这种方法的效率不会太低;这种方法需要使用大量代码,并且会耗费大量运行时间);
(3)分配应该足够的空间,并禁止占用更多的空间(这种方法既安全又高效,但可能会丢失数据)。
注意:C++提供了第4种方法:直接定义一种string类型。由于种种原因,用C++完成这项工作要比用C简单得多。即便如此,用C++还是显得有点麻烦。幸运的是,尽管定义一个标准的C++ string类型并不简单,但这种类型使用起来却非常方便。
有两组函数可用于C语言的字符串处理。第一组函数(strcpy,strcat,等等)按第一种或第二种方法工作。这组函数完全按需要拷贝字符串或使用内存,因此最好留出所需的全部空间,否则程序就可能出错。大多数C程序员使用第一组函数。第二组函数(strncpy,strncat,等等)按第三种方法工作。这组函数需要知道应该使用多大的空间,并且永远不会占用更多的空间,因此它们会忽略所有已无法容纳的数据。
函数strncpy()和strncat()中的参数“n”(第三个)的意义是不同的:
对strncpy()函数来说,它意味着只能使用“n”个字符的空间,包括末尾的NUL字符。
strncpy()函数也恰好只拷贝“n”个字符。如果第二个参数没有这么多字符,strncpy()函数会用NUL字符填充剩余的空间。如果第二个参数有多于“n”个的字符,那么strncpy()函数在还没有拷贝到NUL字符之前就结束工作了。这意味着,在使用strncpy()函数时,你应该总是自己在目标字符串的末尾加上NUL字符,而不要指望strncpy()函数为你做这项工作。对strncat()函数来说,它意味着最多只能拷贝“n”个字符,如果需要还要加上一个NUL字符。因为你真正知道的是目标字符串能存放多少个字符,所以通常你要用strlen()函数来计算可以拷贝的字符数。
函数strncpy()和strncat()之间的区别是“历史性”的(这是一个技术用语,指的是“它对某些人确实起到了一定的作用,并且它可能是处理问题的正确途径,但为什么正确至今仍然说不清楚”)。
例12.5a给出了一个使用strncpy()和strncat()函数的程序。
注意:你应该去了解一下"string-n”函数,虽然它们使用起来有些困难,但用它们编写的程序兼容性更好,错误更少。
如果你愿意的话,可以用函数strcpy()和strcat()重新编写例12.5a中的程序,并用很长的足以溢出缓冲区的参数运行它。会出现什么现象呢?计算机会挂起吗?你会得到"GeneralProtection Exception”或内存信息转储这样的消息吗?
例12.5a使用"string—n”函数的一个例子
# include <stdio. h>
# include <string. h>
/*
Normally, a constant like MAXBUF would be very large, to
help ensure that the buffer doesn't overflow. Here, it's very
small, to show how the "string-n" functions prevent it from
ever overflowing.
*/
# define MAXBUF 16
int
main (int argc, char* * argv)
{
char buf[MAXBUF];
int i;
buf[MAXBUF - 1] = '\0';
strncpy(buf, argv[0], MAXBUF-1);
for (i = 1; i<argc; ++i) {
strncat(buf, " " ,
MAXBUF -1 - strlen (buf) ) ;
strncat(buf, argv[i],
MAXBUF -1 - strlen (buf ) ) ;
}
puts (buf );
return 0;
}
注意:许多字符串函数都至少有两个参数,在描述它们时,与其称之为“第一个参数”和“第二个参数”,还不如称之为“左参数”和“右参数”。
函数strcpy()和strncpy()用来把字符串从一个数组拷贝到另一个数组,即把右参数的值拷贝到左参数中,这与赋值语句的顺序是一样的。
函数strcat()和strncat()用来把一个字符串连接到另一个字符串的末尾。例如,如果数组a1的内容为“dog”,数组a2的内容为“wood”,那么在调用strcat(al,a2)后,a1将变为“dogwood”。
函数strcmp()和strncmp()用来比较两个字符串。当左参数小于、等于或大于右参数时,它们都分别返回一个小于、等于或大于零的值。常见的比较两个字符串是否相等的写法有以下两种:
if (strcmp(sl, s2)) {
/ * si !=s2 * /
}
和
if (! strcmp(s1, s2)) {
/* s1 ==s2 * /
}
上述代码可能并不易读,但它们是完全有效并且相当常见的c代码,你应该记住它们。如果在比较字符串时还需要考虑当前局部环境(locale,见12.8),则要使用strcoll()函数。
有一些函数用来在字符串中进行检索(在任何情况下,都是在左参数或第一个参数中进行检索)。函数strchr()和strrchr()分别用来查找某个字符在一个字符串中第一次和最后一次出现的位置(如果函数strchr()和strrchr()有带“n”字母的版本,那么函数memchr()和memrchr()是最接近这种版本的函数)。函数strspn()、strcspn()(“c”表示"complement")和strpbrk()用来查找包含指定字符或被指定字符隔开的子字符串:
n = strspn("Iowa" , "AEIOUaeiou");
/ * n = 2( "Iowa" starts with 2 vowels * /
n=strcspn("Hello world" ,"\t" ) ;
/ * n = 5; white space after 5 characters * /
p = strbrk("Hellb world" ,"\t" ) ;
/ * p points to blank * /
函数strstr()用来在一个字符串中查找另一个字符串:
p = strstr("Hello world", "or");
/ * p points to the second "or" * /
函数strtok()按照第二个参数中指定的字符把一个字符串分解为若干部分。函数strtok()具有“破坏性”,它会在原字符串中插入NUL字符(如果原字符串还要做其它的改变,应该拷贝原字符串,并将这份拷贝传递给函数strtok())。函数strtok()是不能“重新进入”的,你不能在一个信号处理函数中调用strtok()函数,因为在下一次调用strtok()函数时它总是会“记住”上一次被调用时的某些参数。strtok()函数是一个古怪的函数,但它在分解以逗号或空白符分界的数据时是非常有用的。例12.5b给出了一个程序,该程序用strtok()函数把一个句子中的单词分解出来:
例12.5b一个使用strtok()的例子
# include <stdio. h>
# include <string. h>
static char buf[] = "Now is the time for all good men . . . " ;
int
main()
{
char * p;
p = strtok(buf, " ") ;
while (p ) {
printf("%s\n" ,p);
p = strtok(NULL, " ");
}
return 0;
}
&nbs�(bp�����bsp; stdio. h
TMP_MAX stdio. h
tolower ctype. h
toupper ctype. h
ungetc stdio. h
va_arg stdarg. h
va_end stdarg. h
valist stdarg. h
va_ start stdarg. h
vfprintf stdio. h
vprintf stdio. h
vsprintf stdio. h
wchar_t stddef. h. stdlib. h
wcstombs stdlib. h
wctomb stdlib. h
-------------------------------------------------------------------------
种假设很可能会使你犯一个危险的错误。另一种危险可能来自对a值的使用,因为当循环结束后,a的值并不一定就是100。
c语言能帮助你解决这样的问题,你可以按如下形式编写这个for循环:
for(a=O;a<100&&Func1(a)!=2;++a)
上述循环清楚地告诉程序员:“从0循环到99,但一旦Func1()等于2就停止循环”。因为整个退出条件非常清楚,所以程序员此后就很难犯前面提到的那些错误了。
函数名和变量名应具有描述性
使用具有描述性的函数和变量名能更清楚地表达代码的意思——并且在某种程度上这本身就是一种注释。以下几个例子就是最好的说明:y=p+i-c;
和
YearlySum=Principal+Interest-Charges:
哪一个更清楚呢?
p=*(l+o);
和
page=&List[offset];
哪一个更清楚呢?
独立(free—standing)环境和宿主(hosted)环境之间有什么区别?
并不是所有的C程序员都在编写数据库管理系统和字处理软件,有些C程序员要为嵌入式系统(embedded system)编写代码,例如防抱死刹车系统和智能型的烤面包机。嵌入式系统可以不要任何类型的文件系统,也可以基本上不要操作系统。ANSI/1SO标准称这样的系统为“独立(free—standing)”系统,并且不要求它们提供除语言本身以外的任何东西。与此相反的情况是程序运行在RC机、大型机或者介于两者之间的计算机上,这被称为“宿主(hosted)”环境。即使是开发独立环境的程序员也应该重视标准库:其一,独立环境往往以与标准兼容的方式提供某种功能(例如求平方根函数,重新设计该函数显然很麻烦,因而毫无意义);其二,在将嵌入式程序植入烤面包机这样的环境之前,通常要先在PC机上测试该程序,而使用标准库函数能增加可同时在测试环境和实际环境中使用的代码的总量。
对于C语言,怎样编写参数数目可变的函数?
你可以利用(stdarg.h)头文件,它所定义的一些宏可以让你处理数目可变的参数。注意:这些宏以前包含在名为(varargs.h)或类似的一个头文件中。你的编译程序中可能还有这样一个文件,也可能没有;即使现在有,下一个版本中可能就没有了。因此,还是使用(stadrg.h)为好。
如果对传递给c函数的参数不加约束,就没有一种可移植的方式让c函数知道它的参数的数目和类型。如果一个c函数的参数数目不定(或类型不定),就需要引入某种规则来约束它的参数。例如,printf()函数的第一个参数是一个字符串,它将指示其后都是一些什么样的参数:
printf(" Hello, world! \n" ); /* no more arguments */
printf("%s\n" , "Hello, world!"); /* one more string argument */
printf("%s, %s\n" , "Hello" , "world!"); /* two more string arguments */
printf("%s, %d\n", "Hello", 42); /* one string, one int */
例12.3给出了一个简单的类似printf()的函数,它的第一个参数是格式字符串,根据该字符串可以确定其余参数的数目和类型。与真正的printf()函数一样,如果格式字符串和其余参数不匹配,那么结果是没有定义的,你无法知道程序此后将做些什么(但很可能是一些糟糕的事情)。
例12.3一个简单的类似printf()的函数
# include <stdio. h>
# include <stdlib. h>
# include <string. h>
# include <stdarg. h>
static char *
int2str (int n)
{
int minus = (n < 0) ;
static char buf[32];
char * p = &buf[3l];
if (minus)
n = —n;
*P = '\0',
do {
*---p = '0'+n%10;
n/=10;
} while (n>0);
if (minus)
*- - p = '-';
return p;
}
/*
* This is a simple printf-like function that handles only
* the format specifiers %%, %s, and %d.
*/
void
simplePrintf(const char * format, . . . )
{
va_list ap; / * ap is our argument pointer. * /
int i;
char * s ;
/*
* Initialize ap to start with the argument
* after "format"
*/
va_start(ap, format);
for (; * format; format + + ) {
if (* format !='%'){
putcharC * format);
continue;
}
switch ( * ++format) {
case 's' :
/ * Get next argument (a char * ) * /
s = va_arg(ap, char * );
fputs(s, stdout);
break;
case 'd':/ * Get next argument (an int) * /
i = va_arg(ap, int);
s = int2str(i) ;
fputs(s, stdout) ;
break s
case ' \0' : format---;
breaks
default :putchar ( * format) ;
break;
}
}
/ * Clean up varying arguments before returning * /
va_end(ap);
}
void
main()
{
simplePrintK "The %s tax rate is %d%%. \n" ,
"sales", 6);
}
为了定义我要使用的标准库函数,我需要使用哪些头文件?
你需要使用ANSI/ISO标准规定的你应该使用的那些头文件,见表12.2。有趣的是,这些文件并不一定定义你要使用的函数。例如,如果你要使用宏EDOM,你的编译程序保证你能通过包含(errno.h)得到这个宏,而(errno.h)可能定义了宏EDOM,也可能只包含定义这个宏的头文件。更糟的是,编译程序的下一个版本可能会在另一个地方定义宏EDOM。
因此,你不用去寻找真正定义一个函数的头文件并使用这个文件,而应该使用那个被假定为定义了该函数的头文件,这样做是肯定可行的。
有几个名字在多个头文件中被定义:NULL,size_t和wchar_t。如果你需要其中一个名字的定义,可以使用任意一个定义了该名字的头文件((stddef.h>是一个较好的选择,它不仅小,而且包含了常用的宏定义和类型定义)。
表12.2标准库函数的头文件
----------------------------------------------------------------------
函数 头文件
----------------------------------------------------------------------
abort stdlib. h
abs stdlib. h
acos math. h
asctime time. h
asin math. h
assert assert.h
atan math. h
atan2 math. h
atexit stdlib. h
atof stdlib. h
atoi stdlib. h
atol stdlib. h
bsearch stdlib. h
BUFSIZ stdio. h
calloc stdlib. h
ceil math. h
clearerr stdio. h
clock time. h
CLOCKS-PER-SEC time. h
clock_t time. h
cos math. h
cosh math. h
ctime time. h
difftime time. h
div stdlib. h
div_t stdlib. h
EDOM errno. h
EOF stdio. h
ERANGE errno. h
errno errno. h
exit stdlib. h
EXIT_FAILURE stdlib. h
EXIT_SUCCESS stdlib. h
exp math. h
fabs math. h
fclose stdio. h
feof stdio.h
ferror stdio.h
fflush stdio. h
fgetc stdio.h
fgetpos stdio. h
fgets stdio.h
FILE stdio. h
FILENAME-MAX stdio. h
floor math. h
fmod math. h
fopen stdio. h
FOPEN_MAX stdio. h
fpos_t stdio. h
fpnntf stdio. h
fputc stdio.h
fputs stdio. h
head stdio. h
free stdlib. h
freopen stdio. h
frexp math. h
fscanf stdio. h
fseek stdio. h
fsetpos stdio. h
ftell stdio. h
fwrite stdio. h
getc stdio.h
getchar stdio. h
getenv stdlib. h
gets stdio.h
gmtime time. h
HUGE-VAL math.h
_IOFBF stdio. h
_IOLBF stdio. h
_IONBF stdio. h
isalnum ctype. h
isalpha ctype. h
iscntrl ctype. h
isdigit ctype. h
isgraph ctype. h
islower ctype. h
isprint ctype. h
ispunct ctype. h
isspace ctype. h
isupper ctype. h
isxdigit ctype. h
jmp_buf setjmp. h
labs stdlib. h
LC_ALL locale. h
LC_COLLATE locale. h
LC_CTYPE locale. h
LC_MONETARY locale. h
LC_NUMERIC locale. h
LC_TIME locale. h
struct lconv locale. h
ldexp math. h
ldiv stdlib. h
ldiv_t stdlib. h
localeconv locale. h
localtime time. h
log math. h
log10 math. h
longjmp setjmp. h
L_tmpnam stdio. h
malloc stdlib. h
mblen stdlib. h
mbstowcs stdlib. h
mbtowc stdlib. h
MB_CUR_MAX stdlib. h
memchr string. h
memcmp string. h
memcpy string. h
memmove string. h
memset string. h
mktime time. h
modf math. h
NDEBUG assert. h
NULL locale. h.stddef. h.stdio. h.stdlib. h.string. h.time. h
offsetof stddef. h
perror stdio.h
pow math. h
printf stdio.h
ptrdiff_t stddef. h
putc stdio. h
putchar stdio. h
puts stdio. h
qsort stdlib. h
raise signal. h
rand stdlib. h
RAND_MAX stdlib. h
realloc stdlib. h
remove stdio. h
rename stdio. h
rewind stdio. h
scanf stdio.h
SEEK_CUR stdio. h
SEEK_END stdio. h
SEEK_SET stdio. h
setbuf stdio. h
setjmp setjmp. h
setlocale locale. h
setvbuf stdio. h
SIGABRT signal. h
SIGFPE signal. h
SIGILL signal. h
SIGINT signal. h
signal signal. h
SIGSEGV signal. h
SIGTERM signal. h
sig_atomic_t signal. h
SIG_DFL signal. h
SIG_ERR signal. h
SIG_IGN signal. h
sin math. h
sinh math. h
size_t stddef. h.stdlib. h.string. h
sprintf stdio. h
sqrt math. h
srand stdlib. h
sscanf stdio. h
stderr stdio.h
stdin stdio. h
stdout stdio. h
strcat string. h
strchr string. h
strcmp string. h
strcoll string. h
strcpy string. h
strcspn string. h
strerror string.h
strftime time. h
strlen string. h
strncat string. h
strncmp string. h
strncpy string. h
strpbrk string. h
strrchr string. h
strspn string. h
strstr string. h
strtod stdlib. h
strtok string. h
strtol stdlib. h
strtoul stdlib. h
strxfrm string. h
system stblib. h
tan math. h
tanh math. h
time time. h
time_t time. h
struct tm time. h
tmpfile stdio. h
tmpnam stdio. h
TMP_MAX stdio. h
tolower ctype. h
toupper ctype. h
ungetc stdio. h
va_arg stdarg. h
va_end stdarg. h
valist stdarg. h
va_ start stdarg. h
vfprintf stdio. h
vprintf stdio. h
vsprintf stdio. h
wchar_t stddef. h. stdlib. h
wcstombs stdlib. h
wctomb stdlib. h
-------------------------------------------------------------------------
种假设很可能会使你犯一个危险的错误。另一种危险可能来自对a值的使用,因为当循环结束后,a的值并不一定就是100。
c语言能帮助你解决这样的问题,你可以按如下形式编写这个for循环:
for(a=O;a<100&&Func1(a)!=2;++a)
上述循环清楚地告诉程序员:“从0循环到99,但一旦Func1()等于2就停止循环”。因为整个退出条件非常清楚,所以程序员此后就很难犯前面提到的那些错误了。
函数名和变量名应具有描述性
使用具有描述性的函数和变量名能更清楚地表达代码的意思——并且在某种程度上这本身就是一种注释。以下几个例子就是最好的说明:
y=p+i-c;
和
YearlySum=Principal+Interest-Charges:
哪一个更清楚呢?
p=*(l+o);
和
page=&List[offset];
哪一个更清楚呢?
为什么应该使用标准库函数而不要自己编写函数?
标准库函数有三点好处:准确性、高效性和可移植性。准确性:编译程序的开发商通常会保证标准库函数的准确性。更重要的是。至少开发商做了全面的检测来证实其准确性,这比你所能做到的更加全面(有些昂贵的测试工具能使这项工作更加容易)。
高效性:优秀的C程序员会大量使用标准库函数,而内行的编译程序开发商也知道这一点。如果开发商能提供一套出色的标准库函数,他就会在竞争中占优势。当对相互竞争的编译程序的效率进行比较时,一套出色的标准库函数将起到决定性的作用。因此,开发商比你更有动力,并且有更多的时间,去开发一套高效的标准库函数。
可移植性:在软件要求不断变化的情况下,标准库函数在任何计算机上,对任何编译程序都具有同样的功能,并且表达同样的含义,因此它们是C程序员屈指可数的几种依靠之一。
有趣的是,你很难找到一项关于标准库函数的最标准的信息。对于每一个函数,都需要有一个(在极少数情况下需要两个)保证能将该函数的原型提供给你的头文件(在调用任何一个函数时,都应该包含其原型,见8.2)。有趣的是什么呢?这个头文件可能并不是真正包含该函数原型的文件,在有些(非常糟糕!)情况下,甚至由编译程序手册推荐的头文件都不一定正确。对于宏定义,typedef和全局变量,同样会发生这种情况。
为了找到“正确的”头文件,你可以在一份ANSI/ISO c标准的拷贝中查阅相应的函数。如果你手头没有这样一份拷贝,你可以使用表12.2。
C语言exit()和return有什么不同?
用exit()函数可以退出程序并将控制权返回给操作系统,而用return语句可以从一个函数中返回并将控制权返回给调用该函数的函数。如果在main()函数中加入return语句,那么在执行这条语句后将退出main()函数并将控制权返回给操作系统,这样的一条return语句和exit()函数的作用是相同的。下例是一个使用了exit()函数和return语句的程序:#include <stdio.h>
#include <stdlib.h>
int main (int, char** );
int do_processing (void);
int do_something_daring();
int main (int argc, char** argv)
{
int ret_code;
if (argc <3)
{
printf ("Wrong number of arguments used ! \n");
/* return 1 to the operating system * /
exit(1);
}
ret_code = do_processing ();
......
/* return 0 to the operating system * /
exit(0);
}
int do_processing(void)
{
int rc;
rc = do_aomcthing_daring();
if (rc == ERROR)
{
printf ("Something fiahy ia going on around here... *\n);
/* return rc to the operating syatem * /
exit (re);
}
/* return 0 to the calling function * /
return 0;
}
在上例的main()函数中,如果argc小于3,程序就会退出。语句“exit(1)”指示程序在退出时将数字1返回给操作系统。操作系统有时会根据程序的返回值进行一些相关的操作,例如许多DOS批处理文件会通过一个名为ERRORLEVEL的全局变量来检查可执行程序的返回值。
C语言中,用PASCAL修饰符说明的函数与普通函数有什么不同?
用PASCAL修饰符说明的函数的调用约定与普通函数有所不同。对于普通的C函数,参数是自右至左传递的,而根据PASCAL调用约定,参数是自左至右传递的。下例是一个普通的C函数:int regular_func(int,char*,long);
根据普通C函数的调用约定,函数参数入栈时的顺序为自右至左,因此,在调用regular()函数时,其参数的入栈顺序如下所示:
long
char·
int
当regular_func()函数返回时,调用regular_func()函数的函数负责恢复栈。
下例是一个用PASCAL修饰符说明的函数:
int PASCAL pascal_func(int,char *,long);
根据PASCAL调用约定,函数参数入栈时的顺序为自左至右,因此,在调用‘pascal—func()函数时,其参数的入栈顺序如下所示:
int
char *
long
当pascal_func()函数返回时,调用pascal_func()函数的函数负责恢复栈指针。
采用PASCAL调用约定的函数比普通C函数的效率要高一些——前者的函数调用要稍快一些。MicrosoftWindows就是一个采用PASCAL调用约定的操作环境的例子,WindowsSDK中有数百个用PASCAL修饰符说明的函数。
当Windows的第一个版本于80年代末期编写成功时,使用PASCAL修饰符能明显提高程序的执行速度。现在,计算机的运行速度已经相当快,PASCAL修饰符对程序运行速度的作用已经很小了。事实上,Microsoft在其WindowsNT操作系统中已经放弃了PASCAL调用约定。
在大多数情况下,采用PASCAL调用约定对程序的运行速度几乎没有明显的作用,因此,采用普通C函数的调用约定完全能满足编程要求。但是,当几个毫秒的运行时间对你的程序也很重要时,你就应该用PASCAL修饰符来说明你的函数。
在程序退出main()函数之后,还有可能执行一部分代码吗?
可以,但这要借助C库函数atexit()。利用atexit()函数可以在程序终止前完成一些“清理”工作——如果将指向一组函数的指针传递给atexit()函数,那么在程序退出main()函数后(此时程序还未终止)就能自动调用这组函数。下例的程序中就使用了atexit()函数:# include <stdio.h>
# include <atdlib. h>
void close_files(void);
void print_regiatration_message(void);
int main(int, char ** );
int main (int argc, char** argv)
{
atcxitCprint_regiatration_message);
atexit(cloae_files) ;
while (rec_count <max_recorda)
{
process_one_record ( );
}
exit (0);
}
在上例中,通过atexit()函数指示程序在退出main()函数后自动调用函数close_files()和print_registration_message(),分别完成关闭文件和打印登记消息这两项工作。
在使用atexit()函数时你要注意这样两点:第一,由atexit()函数指定的要在程序终止前执行的函数要用关键字void说明,并且不能带参数;第二,由atexit()函数指定的函数在入栈时的顺序和调用atexit()函数的顺序相同,即它们在执行时遵循后进先出(LIFO)的原则。例如,在上例中,由atexit()函数指定的函数在入栈时的顺序如下所示:
atexit(print_registration_message);
atexit(close_files);
根据LIFO原则,程序在退出main()函数后将先调用close_files()函数,然后调用print_registration_message()函数。
利用atexit()函数,你可以很方便地在退出main()函数后调用一些特定的函数,以完成一些善后工作(例如关闭程序中用到的数据文件)。
C语言技巧:怎样把数组作为参数传递给函数?
在把数组作为参数传递给函数时,有值传递(by value)和地址传递(by reference)两种方式。在值传递方式中,在说明和定义函数时,要在数组参数的尾部加上一对方括号([]),调用函数时只需将数组的地址(即数组名)传递给函数。例如,在下例中数组x[]是通过值传递方式传递给byval_func()函数的:# include <atdio.h>
voidbyval_func(int[]); /*the byval_func() function is passed an
integer array by value * /
void main (void);
void main (void)
{
int x[10];
int y;
/* Set up the integer array. * /
for (y=0; y<10; y++)
x[y] = y;
/* Call byval_func() ,passing the x array by value. * /
byval_func(x);
}
/* The byval_function receives an integer array by value. * /
void byval_func(int i[])
{
int y;
/* print the content: of the integer array. * /
for (y=0; y<10; y++)
printf("%d\n", i[y]);
}
在上例中,定义了一个名为x的数组,并对它的10个元素赋了初值。函数byval_func()的说明如下所示:
intbyval_func(int []);
参数int[]告诉编译程序byval_func()函数只有一个参数,即一个由int类型值组成的数组。在调用byval_func()函数时,只需将数组的地址传递给该函数,即:
byval_func(x);
在值传递方式中,数组x将被复制一份,复制所得的数组将被存放在栈中,然后由byval_func()函数接收并打印出来。由于传递给byal_func()函数的是初始数组的一份拷贝,因此在byval_func()函数内部修改传递过来的数组对初始数组没有任何影响。
值传递方式的开销是非常大的,其原因有这样几点:
第一,需要完整地复制初始数组并将这份拷贝存放到栈中,这将耗费相当可观的运行时间,因而值传递方式的效率比较低;
第二,初始数组的拷贝需要占用额外的内存空间(栈中的内存);
第三,编译程序需要专门产生一部分用来复制初始数组的代码,这将使程序变大。
地址传递方式克服了值传递方式的缺点,是一种更好的方式。在地址传递方式中,传递给函数的是指向初始数组的指针,不用复制初始数组,因此程序变得精练和高效,并且也节省了栈中的内存空间。在地址传递方式中,只需在函数原型中将函数的参数说明为指向数组元素数据类型的一个指针。请看下例:
# include <atdio. h>
void conat_func(const int* );
void main (void);
void main(void)
{
int x[10];
int y;
/* Set up the integer array. * /
for (y=0; y<10; y++)
x[y] = y;
/* Call conat_func(), passing the x array by reference. */
conat_func(x);
}
/*The const_function receives an integer array by reference.
Notice that the pointer i» declared aa const, which renders
it unmodif table by the conat_funcO function. * /
void conat_func(conat int* i)
{
int y;
/ * print the contents of the integer array. * /
for (y=0; y<10; y++)
printf(""%d\n", *(i+y));
}
在上例中,同样定义了一个名为x的数组,并对它的10个元素赋了初始值。函数const_func()的说明如下所示:
int const_func(const int·);
参数constint·告诉编译程序const_func()函数只有一个参数,即指向一个int类型常量的指针。在调用const_func()函数时,同样只需将数组的地址传递给该函数,即:
const_rune(x);
在地址传递方式中,没有复制初始数组并将其拷贝存放在栈中,const_rune()函数只接收到指向一个int类型常量的指针,因此在编写程序时要保证传递给const_func()函数的是指向一个由int类型值组成的数组的指针。const修饰符的作用是防止const_func()函数意外地修改初始数组中的某一个元素。
地址传递方式唯一的不足之处是必须由程序本身来保证将一个数组传递给函数作为参数,例如,在函数const—rune()的原型和定义中,都没有明确指示该函数的参数是指向一个由int类型值组成的数组的指针。然而,地址传递方式速度快,效率高,因此,在对运行速度要求比较高时,应该采用这种方式。
如果一个函数没有返回值,是否需要加入return语句?
在C语言中,用void关键字说明的函数是没有返回值的,并且也没有必要加入return语句。在有些情况下,一个函数可能会引起严重的错误,并且要求立即退出该函数,这时就应该加入一个return语句,以跳过函数体内还未执行的代码。然而,在函数中随意使用return语句是一种不可取的编程习惯,因此,退出函数的操作通常应该尽量集中和简洁。
C语言内部函数到底是什么?
内部函数(用static关键字说明)是作用域只限于说明它的源文件的函数。作用域指的是函数或变量的可见性。如果一个函数或变量在说明它的源文件以外也是可见的,那么就称它具有全局或外部作用域;如果一个函数或变量只在说明它的源文件中是可见的,那么就称它具有局部或内部作用域。内部函数只能在说明它的源文件中使用。如果你知道或希望一个函数不会在说明它的源文件以外被使用,你就应该将它说明为内部函数,这是一种好的编程习惯,因为这样可以避免与其它源文件中可能出现的同名函数发生冲突。请看下例:
#include <stdio.h>
int open_customer_table(void); /*global function, callable from
any module * /
static int open_customer_indexes(void); /*local function, used only in
this module * /
int open_customer_table(void)
{
int ret_code;
/* open the customer table * /
......
if (ret_code == OK)
{
ret_code = opcn_customer_indexes();
}
return ret_code;
}
static int open_customer_indexes(void)
{
int ret_code;
/* open the index files used for this table * /
......
return ret_code;
}
在上例中,函数open_customer_table()是一个外部函数,它可以被任何模块调用,而函数open_customer_indexes()是一个内部函数,它永远不会被其它模块调用。之所以这样说明这两个函数,是因为函数open_customer_indexes()只需被函数open_customer_table()调用,即只需在上例所示的源文件中使用。
对于C语言,一个函数可以有多少个参数?
一个函数的参数的数目没有明确的限制,但是参数过多(例如超过8个)显然是一种不可取的编程风格。参数的数目直接影响调用函数的速度,参数越多,调用函数就越慢。另一方面,参数的数目少,程序就显得精练、简洁,这有助于检查和发现程序中的错误。因此,通常应该尽可能减少参数的数目,如果一个函数的参数超过4个,你就应该考虑一下函数是否编写得当。如果一个函数不得不使用很多参数,你可以定义一个结构来容纳这些参数,这是一种非常好的解决方法。在下例中,函数print_report()需要使用10个参数,然而在它的说明中并没有列出这些参数,而是通过一个RPT_PARMS结构得到这些参数。
# include <atdio. h>
typedef struct
(
int orientation ;
char rpt_name[25];
char rpt_path[40];
int destination;
char output_file[25];
int starting_page;
int ending_page;
char db_name[25];
char db_path[40];
int draft_quality;
)RPT_PARMS;
void main (void);
int print_report (RPT_PARMS* );
void main (void)
{
RPT_PARMS rpt_parm; /*define the report parameter
structure variable * /
/* set up the report parameter structure variable to pass to the
print_report 0 function */
rpt_parm. orientation = ORIENT_LANDSCAPE;
rpt_parm.rpt_name = "QSALES.RPT";
rpt_parm. rpt_path = "Ci\REPORTS"
rpt_parm. destination == DEST_FILE;
rpt_parm. output_file = "QSALES. TXT" ;
rpt_parm. starting_page = 1;
rpt_pann. ending_page = RPT_END;
rpt_pann.db_name = "SALES. DB";
rpt_parm.db_path = "Ci\DATA";
rpt_pann. draft_quality = TRUE;
/*call the print_report 0 function; paaaing it a pointer to the
parameteM inatead of paMing it a long liat of 10 aeparate
parameteM. * /
ret_code = print_report(cu*pt_parm);
}
int print_report(RPT_PARMS*p)
{
int rc;
/*acccM the report parametcra paaaed to the print_report()
function */
oricnt_printcr(p->orientation);
Kt_printer_quality((p->draft_quality == TRUE) ? DRAFT ; NORMAL);
return rc;
}
上例唯一的不足是编译程序无法检查引用print_report()函数时RPT_PARMS结构的10个成员是否符合要求。
C语言中,为什么要说明函数原型?
函数原型能告诉编译程序一个函数将接受什么样的参数,将返回什么样的返回值,这样编译程序就能检查对函数的调用是否正确,是否存在错误的类型转换。例如,现有以下函数原型;int some_func(int,char·,long);
编译程序就会检查所有对该函数的引用(包括该函数的定义)是否使用了三个参数并且返回一个int类型的值。如果编译程序发现函数的调用或定义与函数原型不匹配,编译程序就会报告出错或警告消息。例如,对上述函数原型来说,当编译程序检查以下语句时,就会报告出错或警告消息:
x = some_func(l); /* not enough arguments passed */
x = somc_funcC*HELLOl", 1, "DUDE:"); /* wrong type of arguments used */
x = aome_funcd, sir, 2879, "T"); /* too many arguments passed */
下例中的函数调用同样是不正确的,因为函数some_func()的返回值不是一个long*类型的值。
lValue=some_func(1,str,2879); /*some_rune()returns anint,not a long* */
同样,编译程序还能检查函数的定义(或函数体)是否与函数原型匹配。例如,当编译程序检查以下函数定义时,就会报告出错或警告消息:
int some_func(char *string,longlValue,int iValue) /* wrong order Of
{ parameters */
......
}
总之,在源文件中说明函数原型提供了一种检查函数是否被正确引用的机制。目前许多流行的编译程序都会检查被引用的函数的原型是否已在源文件中说明过,如果没有,就会发出警告消息。
什么时候说明函数?
只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。例如,如果函数stat_func()只在源文件stat.c中使用,应该这样说明:/* stat.c */
# include <atdio.h>
atatic int atat_func(int,int); /* atatic declaration of atat-funcO */
void main (void);
viod main (void)
{
......
rc=stat_func(1,2);
......
}
/* definition (body) of stat-funcO */
static int stat-funcdnt argl,int arg2)
{
return rc;
}
在上例中,函数stat_func()只在源文件stat.c中使用,因此它的原型(或说明)在源文件stat.c以外是不可见的,为了避免与其它源文件中可能出现的同名函数发生冲突,应该将其说明为内部函数。
在下例中,函数glob_func()在源文件global.c中定义和使用,并且还要在源文件extern,c中使用,因此应该在一个头文件(本例中为proto.h)中说明,而源文件global.c和extern.c
中都应包含这个头文件。
File: proto.h
/* proto.h */
int glob_func(int,int); /* declaration of the glob-funcO function * /
File: global. c
/* global. c */
# include <stdio.h>
# include "proto. h" /*include this file for the declaration of
glob_func() */
viod main(void);
viod main (void)
{
rc_glob_func(l,2);
}
/* deHnition (body) of the glob-funcO function */
int glob_func(int argl,int arg2)
{
return rc;
}
File extern. c
/* extin.c */
# include <atdio.h>
# include "proto. h" /*include thia file for the declaration of
glob_func() */
void ext_func(void);
void ext_func(void)
{
/* call glob_func(), which ia deHncd in the global, c source file * /
rc=glob_func(10,20);
}
在上例中,在头文件proto.h中说明了函数glob_func(),因此,只要任意一个源文件包含了该头文件,该源文件就包含了对函数glob_func()的说明,这样编译程序就能检查在该源文件中glob_func()函数的参数和返回值是否符合要求。请注意,包含头文件的语句总是出现在源文件中第一条说明函数的语句之前。