char *
和char []
总有说不尽的故事,有很多时候,我们都不需要去仔细区分它俩,但是它俩又确实有很大的不同。最近看到一些大企业的面试题里又喜欢折腾这方面的内容,我便顺势来作个对比。
主要还是通过示例代码来说明它俩的不同。
示例一:
/* test1.c */
#include <stdio.h>
int main()
{
char a[] = "abcdef";
char *b = "abcdef";
printf("%d",sizeof(a));
printf("%d",sizeof(b));
return 0;
}
这个区别比较明显,char a[]
表示数组,所以sizeof(a)会得到数组占用空间的大小,这里为7,别忘记了最后那个'\0'哟。而char *b
是一个字符指针,32系统里指针大小为4字节,所以sizeof(b)的果果是4。
示例二:
/* test2.c */
#include <stdio.h>
void print_size(char a[])
{
printf("%d",sizeof(a));
}
int main()
{
char a[] = "abcdef";
print_size(a);
printf("%d",sizeof(a));
return 0;
}
main()函数里的sizeof(a)为7,在示例一中已经讲过了。而print_size(a)函数中的sizeof(a)会是多少呢,答案是4,你会觉得奇怪吗?其实print_size(char a[])
与print_size(char *a)
是一样的,它并不会为你记录数组的大小,所以常常,我需要传入一个size_t类型的参数来告诉函数这个串有多长。
示例三:
/* test3.c */
#include <stdio.h>
void ps(char a[11])
{
int i;
for (i = 0; i < 10; ++i)
a[i] = '0' + i;
a[10] = '\0';
printf("%d\n", sizeof(a));
printf("%s\n", a);
}
int main(void)
{
char a[] = "1234";
ps(a); printf("%d\n", sizeof(a));
printf("%s\n", a);
return 0;
}
这是一个很有趣的例子,输出果果为:
4
0123456789
5
0123456789
第一个4告诉你,char a[]
变成char a[11]
并不能阻止a成为一个char *
型指针的命运。而printf()函数是一个相当愚蠢的函数,它负责不停地打印出字符只到遇到'\0'为止,你可以试试将一个字符串的最后那个'\0'改成任意一个可打印字符,然后使用printf()函数输出,看看效果如何。最后的那句printf("%s\n",a);
再次表现了printf()函数的愚蠢性。你奇怪为什么printf("%d\n",sizeof(a));
还能正确输出果果5呢,这至少可见它并不是通过查看'\0'来做简单判断的。
示例四:
/* test4.c */
#include <stdio.h>
char* f1()
{
char a[]="Hello, world![]";
return a;
}
char* f2()
{
char *a="Hello, world!*";
return a;
}
int main()
{
char *s1;
char *s2;
s1=f1();
s2=f2();
puts(s1);
puts(s2);
return 0;
}
使用命令gcc -S test4.c
可以得到编译器转换成的汇编代码test4.s,如下:
/* test4.s */
.file "question.c"
.text
.globl f1
.type f1, @function
f1:
pushl %ebp
movl %esp, %ebp
subl $40, %esp
movl %gs:20, %eax
movl %eax, -12(%ebp)
xorl %eax, %eax
movl $1819043144, -28(%ebp)
movl $1998597231, -24(%ebp)
movl $1684828783, -20(%ebp)
movl $6118177, -16(%ebp)
leal -28(%ebp), %eax
movl -12(%ebp), %edx
xorl %gs:20, %edx
je .L3
call __stack_chk_fail
.L3:
leave
ret
.size f1, .-f1
.section .rodata
.LC0:
.string "Hello, world!*"
.text
.globl f2
.type f2, @function
f2:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl $.LC0, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
.size f2, .-f2
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
call f1
movl %eax, 28(%esp)
call f2
movl %eax, 24(%esp)
movl 28(%esp), %eax
movl %eax, (%esp)
call puts
movl 24(%esp), %eax
movl %eax, (%esp)
call puts
movl $0, %eax
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.1-4ubuntu9) 4.4.1"
.section .note.GNU-stack,"",@progbits
要真正区分char []
和char *
,查看汇编代码才是王道,以上是通过gcc -S
得到的AT&T汇编代码,以.globl
开头表示全局,f1,f2,main前面都有.globl表示它们三个是全局函数。
你会发现f1()函数的"Hello, world![]"
是通过movl指令,将一个个ascii码顺序排放而得到的,而f2()函数是将"Hello, world!*"
放到了常量数据区,然后通过地址值进行索引。一个在指令段里,一个在数据段里,两者当然是截然的不同了,这是它们的根本区别。常量数据段表明了它有不可改变的属性,若加入s2[0]='A';
会产生段错误(Segmentation fault),这便验证了此种说法。而在指令区的char []
则要自由得多。