招聘公司在笔试或者面试中,基本上不可能不考字符串。C语言中的字符串是一种相对简单的数据结构,但也确实能够从某种程度上,考察出求职者对C语言一些细节掌握程度,因此面试官常常喜欢反复的问一些字符串相关的问题。事实上,字符串也是一个能够考验程序员编程规范和编程习惯的重要考点,作为求职者,我们不能忽视这些细节。
先来个问题热热身
下面这个问题来自中国大陆某著名汽车制造公司的嵌入软件部的一道笔试题,请看:
不使用 itoa 函数,编写C语言程序将整数转换为字符串数。
首先,字符串数也属于字符串,是由若干个字符组成的。数字字符 '0'~'9' 常常是连续编码的,所以 '1' 等于 1+'0','2' 等于 2+'0',...。这么看来,这道题还是非常基础的,只要能够将整数逐位取出,然后做字符转换,然后组合成在一起,就构成了一个字符串。
将一个整数逐位分解,相信即使是C语言初学者,也能写出相关代码。假设要逐位分解的是 num=12345,则相关C语言代码可以如下写,请看:
int num = 12345;
while(num){
printf("%d\n", num%10);
num /= 10;
}
上面这段C语言程序会将 12345 逐位分解,依次打印出 5,4,3,2,1。那么,只要再将各位数加上字符 '0',就转换成了字符,再反向将各个字符组合,就解决了这道笔试题。这一过程使用C语言实现是简单的,请看:
#include <stdio.h>
int main()
{
int num = 12345, i = 0;
char strnum[64];
while(num){
strnum[i++] = num%10 + '0';
num /= 10;
}
strnum[i] = '\0';
printf("%s\n", strnum);
return 0;
}
上面这段C语言代码,将 num=12345 逐位分解,并且转换为字符存放在 strnum 里。这里有一个细节需要注意, strnum[i] = '\0'; 非常重要,因为C语言中的字符串默认是以字符 '\0' 作为结束符的,一些字符串操作函数都是遵守此约定的。编译上面的C语言代码并执行,得到如下输出:
# gcc t.c
# ./a.out
54321
printf() 函数是以 %s,也即字符串的形式输出 strnum 的,得到了“54321”字符串。显然,现在问题变成了字符串倒序排列问题。使用C语言解决这个问题是简单的,请看:
char res[64];
int j = 0;
while(--i >= 0){
res[j++] = strnum[i];
}
res[j] = '\0';
res 中存放的就是整数 12345 的字符串形式,现在我们将这一过程封装成 myitoa() 函数,方便以后使用,相关C语言代码如下,请看:
char *myitoa(char *strnum, int num)
{
char tmp[64];
int i = 0, j = 0;
while(num){
tmp[i++] = num%10 + '0';
num /= 10;
}
while(--i >= 0){
strnum[j++] = tmp[i];
}
strnum[j] = '\0';
return strnum;
}
编写 main() 函数测试之,相关C语言代码如下:
int main() {
int num = 12345;
char strnum[64];
printf("%s\n", myitoa(strnum, num));
return 0;
}
编译并执行这段C语言代码,发现输出与预期一致:
# gcc t.c
# ./a.out
12345
myitoa() 函数只支持 10 进制的字符串转换,其他进制该如何实现?而且,既然 myitoa() 函数可以把 num 转换成字符,拷贝到 strnum,为何还要 char * 型返回值?
再来看一道面试题
下面这道题目来自美国某著名计算机软件公司的面试题:
#include <stdio.h>
#include <string.h>
int main()
{
char s[] = "abcdefghijklmnopqrstuvwxyz";
char d[] = "123";
strcpy(d, s);
printf("d:%s\ns:%s\n", d, s);
return 0;
}
上面这段C语言程序会输出什么呢?
之前的章节曾经提到,C语言初始化一维数组时允许不指定维数,但是一定要有初始化内容,编译器会根据初始化内容确定数组的维数。那显然,这里的数组 d 是存放不下 s 中的内容的,这时执行 strcpy(d,s) 后,再以字符串的形式打印 d 和 s,会输出什么呢?
得到答案最简单粗暴的方法是编译运行这段C语言代码,请看:
# gcc t.c
# ./a.out
d:abcdefghijklmnopqrstuvwxyz
s:qrstuvwxyz
这个输出的环境是 ubuntu x86_64 平台,gcc v4.8.4 编译器。
没有经验的看到这个输出估计会比较迷惑,源字符串居然被截掉了一部分!不过,原因其实很简单:s 和 d 都是 main() 函数的局部变量,它们都处于 main() 函数的栈空间中,根据 d 的初始化信息,编译器认为只分配给它 3 个字节的内存空间就可以了。
接下来遇到变量 s,编译器没有理由把它放在距离 d 很远处,可能直接就把它放在 d 的后面了。这样一来,在执行 strcpy(d, s); 后,"abcdefghijklmnopqrstuvwxyz"的长度超过了 d 的长度,因此它会占用 d 后面 s 的空间,如下图:
按照前文的分析,C语言中的一些字符串操作函数是以字符 '\0' 作为结束符的,printf() 函数也是如此,在打印 d 的时候它遇到 '\0' 结束,输出 "abcdefghijklmnopqrstuvwxyz"。在打印 s 的时候,printf() 函数也是遇到 '\0' 结束,所以会输出 “qrstuvwxyz”,后面的字符都没有机会输出了。
更详细的分析,其实可以看看我的这篇文章。
小结
从本节的两个例子可以看出,C语言不像其他一些编程语言,并没有多少“保护机制”,比如第二题,C语言并不会因为数组 d 的长度不够而终止执行,相反,它可能认为程序员故意如此,C语言相信每一个C程序员都是高手。所以,为了不辜负C语言的信任,作为C程序员,应该注重每一个细节,确保一切都在掌控中。