C语言的本质(15)——C语言的函数接口入门
C语言的本质(15)——C语言的函数接口
函数的调用者和其实现者之间存在一个协议,在调用函数之前,调用者要为实现者提供某些条件,在函数返回时,实现者完成调用者需要的功能。
函数接口通过函数名,参数和返回值来描述这个协议,只要函数名和参数名命名合理,参数和返回值的类型定义的准确,调用者仅仅通过函数接口就能知道函数的用法。当函数接口不能表达函数的全部语义时,文档就起了重要的补充作用,函数文档的写法我们可以参照Linux下的Man Page或MSDN。
下面通过分析C标准库函数来说明函数接口:
在Linux终端下敲命令manstrcpy就可以看到下面的Man Page。
这个Man Page描述了两个函数,strcpy和strncpy,这两个函数的作用是把一个字符串拷贝给另一个字符串。SYNOPSIS部分给出了这两个函数的原型,以及要使用这些函数需要包含哪些头文件。参数dest、src和n都加了下划线,有时候并不想从头到尾阅读整个Man Page,而是想查一下某个参数的含义,通过下划线和参数名就能很快找到我们关心的部分。
dest表示Destination,src表示Source,看名字就能猜到是把src所指向的字符串拷贝到dest所指向的内存空间。这一点从两个参数的类型也能看出来,dest是char *型的,而src是const char *型的,说明src所指向的内存空间在函数中只能读不能改写,而dest所指向的内存空间在函数中是要改写的,显然改写的目的是当函数返回后调用者可以读取改写的结果。通过Man Page我们可以推测到strcpy函数是这样用的:
char buf[10]; strcpy(buf,"hello"); printf(buf);
那么strncpy的参数n是干什么用的呢?单从函数接口无法推测,我们继续看下面的文档。
在文档中强调了strcpy在拷贝字符串时会把结尾的'\0'也拷到dest中,因此保证了dest中是以'\0'结尾的字符串。但另外一个要注意的问题是,strcpy只知道src字符串的首地址,不知道长度,它会一直拷贝到'\0'为止,所以dest所指向的内存空间要足够大,否则有可能写越界,例如:
char buf[10]; strcpy(buf,"hello world");
如果没有保证src所指向的内存空间以'\0'结尾,也有可能读越界,例如:
char buf[10] ="abcdefghij", str[4] = "hell"; strcpy(buf,str);
因为strcpy函数的实现者通过函数接口无法得知src字符串的长度和dest内存空间的大小,所以“确保不会写越界”应该是调用者的责任,调用者提供的dest参数应该指向足够大的内存空间,“确保不会读越界”也是调用者的责任,调用者提供的src参数指向的内存应该确保以'\0'结尾。
此外,文档中还强调了src和dest所指向的内存空间不能有重叠。凡是有指针参数的C标准库函数基本上都有这条要求,每个指针参数所指向的内存空间互不重叠,例如这样调用是不允许的:
char buf[10] ="hello"; strcpy(buf,buf+1);
strncpy的参数n指定最多从src中拷贝n个字节到dest中,如果拷贝到'\0'就结束,如果拷贝到n个字节还没有碰到'\0',那么也结束,调用者负责提供适当的n值,以确保读写不会越界,比如让n的值等于dest所指向的内存空间的大小:
char buf[10]; strncpy(buf,"hello world", sizeof(buf));
那么这意味着什么呢?文档中特别用了Warning指出,这意味着dest有可能不是以'\0'结尾的。例如上面的调用,虽然把"hello world"截断到10个字符拷贝至buf中,但buf不是以'\0'结尾的,如果再printf(buf)就会读越界。如果你需要确保dest以'\0'结束,可以这么调用:
char buf[10]; strncpy(buf,"hello world", sizeof(buf)); buf[sizeof(buf)-1]= '\0';
strncpy还有一个特性,如果src字符串全部拷完了不足n个字节,那么还差多少个字节就补多少个'\0',但是正如上面所述,这并不保证dest一定以'\0'结束,当src字符串的长度大于n时,不但不补多余的'\0',连字符串的结尾'\0'也不拷贝。下面文档非常友好,为了帮助理解,还给出一个strncpy的简单实现。
函数的Man Page都有一部分专门讲返回值的。这两个函数的返回值都是dest指针。可是为什么要返回dest指针呢?dest指针本来就是调用者传过去的,再返回一遍dest指针并没有提供任何有用的信息。之所以这么规定是为了把函数调用当作一个指针类型的表达式使用,比如printf(strcpy(buf, "hello")),一举两得,如果strcpy的返回值是void就没有这么方便了。
CONFORMING TO部分描述了这个函数是遵照哪些标准实现的。strcpy和strncpy是C标准库函数,当然遵照C99标准。以后我们还会看到libc中有些函数属于POSIX标准但并不属于C标准,例如write(2)。
NOTES部分给出一些提示信息。这里指出如何确保strncpy的dest以'\0'结尾,和我们上面给出的代码类似,但由于n是个变量,在执行buf[n - 1]= '\0';之前先检查一下n是否大于0,如果n不大于0,buf[n - 1]就访问越界了,所以要避免。
BUGS部分说明了使用这些函数可能引起的Bug,这部分一定要仔细看。用strcpy比用strncpy更加不安全,如果在调用strcpy之前不仔细检查src字符串的长度就有可能写越界,这是一个很常见的错误,例如:
void foo(char*str) { char buf[10]; strcpy(buf, str); ...... }
str所指向的字符串有可能超过10个字符而导致写越界,在第4 节 “段错误”我们看到过,这种写越界可能当时不出错,而在函数返回时出现段错误,原因是写越界覆盖了保存在栈帧上的返回地址,函数返回时跳转到非法地址,因而出错。像buf这种由调用者分配并传给函数读或写的一段内存通常称为缓冲区(Buffer),缓冲区写越界的错误称为缓冲区溢出(Buffer Overflow)。如果只是出现段错误那还不算严重,更严重的是缓冲区溢出Bug经常被恶意用户利用,使函数返回时跳转到一个事先设好的地址,执行事先设好的指令,如果设计得巧妙甚至可以启动一个Shell,然后随心所欲执行任何命令,可想而知,如果一个用root权限执行的程序存在这样的Bug,被攻陷了,后果将会非常严重。