本文测试环境 :
X86-64 bit 架构的服务器
CentOS x64 5.x
gcc version 4.1.2 20080704
指针和数组是C的比较难搞懂的知识点, 需要结合内存来学习, 非常感谢各位兄弟为我指点迷津.
下面总结一下 :
首先说明一下C程序在运行时, 不同的内容或变量分别存储在什么地方?
分了几块区域分别是, code, constants, global, heap, stack; (内存地址从低到高)
其中constants存储常量(常量值不允许修改), global存储在所有函数以外定义的全局变量(全局变量允许修改), heap是一块动态内存区域(可存放持久化内容, 不会自动释放内存), stack存放函数内的本地变量(函数执行完后本地变量占用的内存将自动释放);
如图 :
接下来要介绍两个符号 * 和 &.
1. * 用在定义变量类型, 或者 强制类型转换时, 表示要定义一个指针 或者 把这个变量类型转换成指针(并告知这个指针指向的是什么类型的数据) .
2. * 用在一个指针变量的前面, 表示这个指针指向地址存储的内容 (内容是什么类型的就取决于这个指针定义时: 告知这个指针指向的是什么类型的数据,指针的加减运算得到的内存地址也和指针指向的是什么类型的数据相关,int * a,a+1 得到的地址则是在a指向的地址基础上加4字节.).
3. & 用在变量名的前面, 表示这个变量所在的地址 .
例1 :
[root@db-172-16-3-33 zzz]# cat b.c
#include <stdio.h>
int main() {
char * a = "hello";
fprintf(stdout, "&a:%p, a:%p, a:%s\n", &a, a, a);
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b
&a:0x7fff71b22230, a:0x400618, a:hello
&a 表示a的地址 .
a 表示a存储的内容, 因为a是指针, 所以它的内容读取出来就是一个地址 .
*a 表示a指针存储的这个地址 存储的内容 .
在内存中的图示 :
这个图包含了几块内容 :
1. 在64位的系统中 , 指针占用了8个字节. 因为指针中存储的是地址, 而且地址是64位的. 所以需要8个字节.
2. 在x86架构的机器中, 内存填充是从低位到高位的. 所以hello在内存中是这样存储的 :
地址: 内容
0x400618 : 0x68 (ascii : h)
0x400619 : 0x65 (ascii : e)
0x40061a : 0x6c (ascii : l)
0x40061b : 0x6c (ascii : l)
0x40061c : 0x6f (ascii : o)
0x40061d : 0x68 (ascii : NULL)
3. 那怎么来证明以上是正确的呢? 很简单, 按照每个字节来打印就知道了.
这里需要注意的是指针的加减法得到的地址和指针指向的地址存储的内容的类型是有关的, 反过来说, 要让指针加1刚好得到的是下一个字节那就告诉编译器, 这个指针指向的地址的内容是char类型就好了, 因为sizeof(char) = 1字节.
先来打印一下hello是不是按照上面说的这样存储的?
[root@db-172-16-3-33 zzz]# cat b.c
#include <stdio.h>
int main() {
char * a = "hello";
fprintf(stdout, "&a:%p, a:%p, a:%s\n", &a, a, a);
fprintf(stdout, "sizeof(char *):%lu, sizeof(char):%lu\n", sizeof(char *), sizeof(char));
fprintf(stdout, "a+0:%p, *(a+0):%x, *(a+0):%c\n", a+0, *(a+0), *(a+0));
fprintf(stdout, "a+1:%p, *(a+1):%x, *(a+1):%c\n", a+1, *(a+1), *(a+1));
fprintf(stdout, "a+2:%p, *(a+2):%x, *(a+2):%c\n", a+2, *(a+2), *(a+2));
fprintf(stdout, "a+3:%p, *(a+3):%x, *(a+3):%c\n", a+3, *(a+3), *(a+3));
fprintf(stdout, "a+4:%p, *(a+4):%x, *(a+4):%c\n", a+4, *(a+4), *(a+4));
fprintf(stdout, "a+5:%p, *(a+5):%x, *(a+5):%c\n", a+5, *(a+5), *(a+5));
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b
&a:0x7ffffe249680, a:0x4006f8, a:hello
sizeof(char *):8, sizeof(char):1
a+0:0x4006f8, *(a+0):68, *(a+0):h
a+1:0x4006f9, *(a+1):65, *(a+1):e
a+2:0x4006fa, *(a+2):6c, *(a+2):l
a+3:0x4006fb, *(a+3):6c, *(a+3):l
a+4:0x4006fc, *(a+4):6f, *(a+4):o
a+5:0x4006fd, *(a+5):0, *(a+5):
解说 :
a 是一个指针, 这个指针指向的内容是char类型的, 所以这个指针的加减运算a+1 表示地址加1字节.
*a 把你带到它指向的0x4006f8, 而fprintf 以16进制和char输出的正是0x4006f8这个地址的这个字节上的内容(1字节刚好用两个16进制数表示, 刚好可以转成char)
a+0这里只是为了说明指针的加减运算, 可以去掉+0.
sizeof(char *):8 表示指针占用了8字节, sizeof(char):1 表示char占用了1字节.
4. 接下来打印 &a 这个地址是不是也是按照从低到高存储的?
为了一个字节一个字节打出, 我们需要再定义一个指针b , 用指针b来做加减运算, 但是这样能如愿吗?
[root@db-172-16-3-33 zzz]# cat b.c
#include <stdio.h>
int main() {
char * a = "hello";
char ** b = &a;
fprintf(stdout, "sizeof(a):%lu, b:%p, &a:%p, b+1:%p\n", sizeof(a), b, &a, b+1);
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b
sizeof(a):8, b:0x7fff3692b520, &a:0x7fff3692b520, b+1:0x7fff3692b528
解说 :
sizeof(a)=8 结果告诉我们, a这个变量占用了8字节, 因为它存储的是个内存地址, 在64位的操作系统中这个是可以理解的.
b这个指针指向a, 所以b和&a 打印出来的值转成内存地址当然是一样的.
b+1 运算得到的地址当然应该是加8个字节 . 因为地址做加减运算得到的当然还是地址, 至于得到的结果和什么有关, 当然和这个做加减运算的指针定义时所告知的它指向什么类型的数据有关. char ** b, ( *b 表示 b是一个指针, 然后char *则是告诉你b指向的是一个指针, 甭管是什么类型的指针,反正b指向的是一个指针, 一个指针占用8字节, 所以b+1就是加8个字节)
那怎么样能让b+1结果是加1个字节呢?
这里要用到强制类型转换. (char *) b 就把b强制转成char *了. 所以 ((char *) b)+1 就是加1字节. *(((char *) b)+1) 就是取这个地址的内容.
%x表示取一个字节并按照16进制打印出来.
[root@db-172-16-3-33 zzz]# cat b.c
#include <stdio.h>
int main() {
char * a = "hello";
char ** b = &a;
unsigned short i;
unsigned short x = (unsigned short) sizeof(a);
fprintf(stdout, "&a:%p, a:%p\n", &a, a);
for(i=0; i<x; i++) {
fprintf(stdout, "i:%u, ((char *) b)+i:%p, *(((char *) b)+i):%x\n", i, ((char *) b)+i, *(((char *) b)+i));
}
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b
&a:0x7fff1ffff608, a:0x400728
i:0, ((char *) b)+i:0x7fff1ffff608, *(((char *) b)+i):28
i:1, ((char *) b)+i:0x7fff1ffff609, *(((char *) b)+i):7
i:2, ((char *) b)+i:0x7fff1ffff60a, *(((char *) b)+i):40
i:3, ((char *) b)+i:0x7fff1ffff60b, *(((char *) b)+i):0
i:4, ((char *) b)+i:0x7fff1ffff60c, *(((char *) b)+i):0
i:5, ((char *) b)+i:0x7fff1ffff60d, *(((char *) b)+i):0
i:6, ((char *) b)+i:0x7fff1ffff60e, *(((char *) b)+i):0
i:7, ((char *) b)+i:0x7fff1ffff60f, *(((char *) b)+i):0
解说 :
a变量存放在内存地址0x7fff1ffff608这里的连续8个字节中.
a变量的8个字节中存储了什么内容呢? 0x400728
它是从低位到高位存储的, 如上面的结果, 28存在0x7fff1ffff608, 07存在0x7fff1ffff609, 40存在0x7fff1ffff60a, 另外5个字节存的都是0x00.
5. 如果是多字节的数据又是怎么存储的呢? 比如int 占据了4个字节, 它是怎么存储的 ?
答案也是从低位到高位.
[root@db-172-16-3-33 zzz]# cat c.c
#include <stdio.h>
int main() {
int a = -987654;
int * b = &a;
unsigned short i;
unsigned short x = (unsigned short) sizeof(a);
fprintf(stdout, "&a:%p, a:%i, a:%x\n", &a, a, a);
for(i=0; i<x; i++) {
fprintf(stdout, "i:%u, ((char *) b)+i:%p, (unsigned char) (*(((char *) b)+i)):%x\n", i, ((char *) b)+i, (unsigned char) (*(((char *) b)+i)));
}
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./c.c -o c && ./c
&a:0x7fffe57051cc, a:-987654, a:fff0edfa //负数是正数的反码加1, 所以得到的是这个结果.
i:0, ((char *) b)+i:0x7fffe57051cc, (unsigned char) (*(((char *) b)+i)):fa
i:1, ((char *) b)+i:0x7fffe57051cd, (unsigned char) (*(((char *) b)+i)):ed
i:2, ((char *) b)+i:0x7fffe57051ce, (unsigned char) (*(((char *) b)+i)):f0
i:3, ((char *) b)+i:0x7fffe57051cf, (unsigned char) (*(((char *) b)+i)):ff
注意, 这里一定要把内容再强制转换成unsigned char再输出, 就是这个(unsigned char) (*(((char *) b)+i)):%x .
否则编译器会输出4字节的16进制数, 不太好看. 如下 :
#include <stdio.h>
int main() {
int a = -987654;
int * b = &a;
unsigned short i;
unsigned short x = (unsigned short) sizeof(a);
fprintf(stdout, "&a:%p, a:%i, a:%x\n", &a, a, a);
for(i=0; i<x; i++) {
fprintf(stdout, "i:%u, ((char *) b)+i:%p, (*(((char *) b)+i)):%x\n", i, ((char *) b)+i, (*(((char *) b)+i)));
}
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./c.c -o c && ./c
&a:0x7ffff6fada3c, a:-987654, a:fff0edfa
i:0, ((char *) b)+i:0x7ffff6fada3c, (*(((char *) b)+i)):fffffffa
i:1, ((char *) b)+i:0x7ffff6fada3d, (*(((char *) b)+i)):ffffffed
i:2, ((char *) b)+i:0x7ffff6fada3e, (*(((char *) b)+i)):fffffff0
i:3, ((char *) b)+i:0x7ffff6fada3f, (*(((char *) b)+i)):ffffffff
6. 那么bit-field又是怎么存储的呢?
答案当然也是从低位到高位存储的, 这里要用到struct来验证.
[root@db-172-16-3-33 zzz]# cat c.c
#include <stdio.h>
int main() {
typedef struct test{
unsigned char f1:1;
unsigned char f2:2;
unsigned char f3:3;
unsigned char f4:2;
} test;
test t1 = {0,1,4,3};
test * pt1 = &t1;
fprintf(stdout, "(unsigned char) (*((unsigned char *) pt1)):%x\n", (unsigned char) (*((unsigned char *) pt1)));
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./c.c -o c && ./c
(unsigned char) (*((unsigned char *) pt1)):e2
解说 :
test t1 = {0,1,4,3}; 转成二进制分别如下 :
f1(0) : 0
f2(1) : 01
f3(4) : 100
f4(3) : 11
如果是按低位到高位存储的, 那么它存储的应该是 : 11100010 . 这个与0xe2 刚好相符.
这里同样用到了强制类型转换, (unsigned char *) pt1是把指向struct test的指针转成了指向unsigned char的指针.
(unsigned char) (*((unsigned char *) pt1)) 是把 *((unsigned char *) pt1) 转成了unsigned char类型.
如果不使用指针, 直接把struct变量转成unsigned是不能编译通过的. 错误如下 :
[root@db-172-16-3-33 zzz]# cat c.c
#include <stdio.h>
int main() {
typedef struct test{
unsigned char f1:1;
unsigned char f2:2;
unsigned char f3:3;
unsigned char f4:2;
} test;
test t1 = {0,1,4,3};
unsigned char a = (unsigned char) t1;
fprintf(stdout, "a:%x\n", a);
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./c.c -o c && ./c
./c.c: In function ‘main’:
./c.c:11: error: aggregate value used where an integer was expected
所以指针在C里面运用真的太灵活了.
进入正题, 讲讲 char **a , char *a[] , char a[][], char * a[][] , char ** a[][] , char * a [][][]
看起来很复杂, 其实理解了就不复杂了.
1.
char **a :
表示a是一个指针, 这个指针指向的地址存储的是 char * 类型的数据. 指针的加减运算在这里的体现 : a + 1 表示地址加8字节 .
char * 也是一个指针, 用(*a)表示, 指向的地址存储的是 char 类型的数据。指针的加减运算在这里的体现 : (*a) + 1 表示地址加1字节 .
[root@db-172-16-3-33 zzz]# cat b.c
#include <stdio.h>
int main() {
char * a = "hello";
char ** b = &a;
fprintf(stdout, "&b:%p, b:%p, &a:%p, a:%p, *a:%c, a:%s\n", &b, b, &a, a, *a, a);
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b
&b:0x7fff5319c1d8, b:0x7fff5319c1e0, &a:0x7fff5319c1e0, a:0x400628, *a:h, a:hello
图示 :
2. char *a[]
表示 a是数组, 数组中的元素是指针, 指向char类型. (数组里面所有的元素是连续的内存存放的).
需要特别注意 :
数组名在C里面做了特殊处理 , 数组名用数组所占用内存区域的第一个字节的内存地址替代了。并且数组名a也表示指针.
如数组占用的内存区域是0x7fff5da3f550到0x7fff5da3f5a0, 那么a就被替换成0x7fff5da3f550.
所以a 并不表示a地址存储的内容, 而是a地址本身(这个从 a = &a 就能够体现出来). 这个一定要理解, 否则会无法进行下去.
a+1 表示a的第二个元素的内存地址, 所以是加8字节.( 因为a的元素是char 指针, 所需要的空间为8字节(64位内存地址). )
*(a+1) 则表示a这个数组的第二个元素的内容 (是个char 类型的指针. 本例表示为world字符串的地址).
*(*(a+1)) 则表示a这个数组的第二个元素的内容(char指针)所指向的内容(w字符).
char * a[10] 表示限定这个数组最多可存放10个元素(char指针), 也就是说这个数组占用10*8 = 80字节.
如果存储超出数组的限额编译警告如下 :
[root@db-172-16-3-33 zzz]# cat b.c
#include <stdio.h>
int main() {
char *a[1] = {"abc","def"};
fprintf(stdout, "a[1]:%s\n", a[1]);
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b
cc1: warnings being treated as errors
./b.c: In function ‘main’:
./b.c:4: warning: excess elements in array initializer // 超出数组长度. 因为赋值时给了2个元素, 而限定只有1个元素.
./b.c:4: warning: (near initialization for ‘a’)
例子 :
[root@db-172-16-3-33 zzz]# cat b.c
#include <stdio.h>
int main() {
char *a[10] = {"hello", "world"};
fprintf(stdout, "a:%p, a+1:%p, &a:%p, (&a)+1:%p, *a:%p, *(a+1):%p\n", a, a+1, &a, (&a)+1, *a, *(a+1));
fprintf(stdout, "*a:%s, *(a+1):%s, **a:%c, *(*(a+1)):%c\n", *a, *(a+1), **a, *(*(a+1)));
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b
a:0x7fff9d0fb180, a+1:0x7fff9d0fb188, &a:0x7fff9d0fb180, (&a)+1:0x7fff9d0fb1d0, *a:0x4006b8, *(a+1):0x4006be
*a:hello, *(a+1):world, **a:h, *(*(a+1)):w
解说 :
a:%p 打印 0x7fff9d0fb180 表示a的内存地址 , 也就是说数组a的第一个元素存储在这个地址里面, 取出第一个元素的值(char指针)使用*a .
a+1:%p 打印的是a这个数组的第二个元素的内存地址 0x7fff5da3f558 (因为a数组里面存储的是char指针, 指针在64位机器里面占用8字节, 所以第二个元素所在的内存地址相比第一个元素所在的内存地址刚好大8字节), 取出第二个元素的值(char指针)使用*(a+1) .
&a:%p 打印出来的结果和a:%p 0x7fff5da3f550 一致, 因为编译器把数组名替换成了数组所在内存的位置 .
(a:%p 打印的是第一个元素所在的内存地址, &a:%p打印的则是a数组所在的内存地址. 所以结果是一样的)
但是 a+1 不等于 (&a)+1. 原因是 a 和 &a 含义不一样, a指向的是数组里面的第一个元素, 所以a+1 指的是第二个元素. &a指向的是a这个数组, 所以(&a)+1 表示整个a数组要用到的内存空间的下一个地址. ( 如果&a不好理解的话, 想象把&a 赋予给另一个指针变量c, 那么c+1 等于啥? )
3. char [][]
char a[2][10] 表示一个二维数组, 存储在最底层的元素是char. 1维最多允许存储2个元素(char []), 2维最多允许存储10个元素(char).
超出将告警 :
[root@db-172-16-3-33 zzz]# cat b.c
#include <stdio.h>
int main() {
char a[2][10] = {"hello", "world", "linux"};
return 0;
}
结果 :
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b
cc1: warnings being treated as errors
./b.c: In function ‘main’:
./b.c:4: warning: excess elements in array initializer // 1维在赋值时给出了3个元素"hello" , "world" , "linux" 因此告警了, 可能导致内存溢出.
./b.c:4: warning: (near initialization for ‘a’)
./b.c:4: warning: unused variable ‘a’
2维超出长度同样告警 :
[root@db-172-16-3-33 zzz]# cat b.c
#include <stdio.h>
int main() {
char a[2][10] = {"helloxxxxxxxx", "world"};
return 0;
}
[root@db-172-16-3-33 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./b.c -o b && ./b
cc1: warnings being treated as errors
./b.c: In function ‘main’:
./b.c:4: warning: initializer-string for array of chars is too long
./b.c:4: warning: (near initialization for ‘a[0]’)
./b.c:4: warning: unused variable ‘a’
char a[2][10] 中, a+$n 或者 & a[n] 表示 指向1维的第$n个元素的指针, *(a+$n)+$m 或 & a[n][m] 表示指向1维的第n个元素中的第m个元素的指针.
*(*(a+$n)+$m) 或者 a[n][m] 表示 1维的第n个元素中的第m个元素 的内容 .
解说 :
以char a[2][10]为例子 :
&a 是一个指针 , 加1 将加整个二维数组所占空间. 相当于加20字节.
a 是一个指针 , 加1 将加加1个1维元素所占空间. 相当于加10字节.
*a 是一个指针 , 加1 将加加1个2维元素所占空间. 相当于加1字节.
**a 不是指针, 是char.
a+1与*(a+1)相等 是因为a+1 表示1维的第2个元素的首地址, *(a+1)表示1维的第2个元素中的第1个元素的地址. 指向同一个地址. 但是含义不一样. (a+1) + 1 和 (*(a+1)) + 1就不一样了.
(a+1) + 1 表示1维的第3个元素的首地址; (*(a+1)) + 1 表示1维的第2个元素中的第2个元素的地址.
以char a[2][6]为例子 :
[root@db-172-16-3-150 zzz]# cat a.c
#include <stdio.h>
int main() {
char a[2][6] = {"hello", "world"};
fprintf(stdout, "sizeof(a):%lu\n", sizeof(a)); // a数组占用空间
fprintf(stdout, "&a:%p, (&a)+1:%p\n", &a, (&a)+1); // a数组首地址, a数组首地址加1
fprintf(stdout, "a:%p, a+1:%p\n", a, a+1); // a数组1维度第1个元素首地址, 加1
fprintf(stdout, "*a:%p, *(a)+1:%p\n", *a, *(a)+1); // a数组1维度第1个元素中的第1个元素首地址, 加1
return 0;
}
结果 :
[root@db-172-16-3-150 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./a.c -o a && ./a
sizeof(a):12
&a:0x7ffffe5cf8b0, (&a)+1:0x7ffffe5cf8bc // 加12个字节
a:0x7ffffe5cf8b0, a+1:0x7ffffe5cf8b6 // 加6个字节
*a:0x7ffffe5cf8b0, *(a)+1:0x7ffffe5cf8b1 // 加1个字节
验证图示正确性 :
[root@db-172-16-3-150 zzz]# cat a.c
#include <stdio.h>
int main() {
char a[2][6] = {"hello", "world"};
fprintf(stdout, "&a:%p, a:%p, *a:%p\n", &a, a, *a);
fprintf(stdout, "(*(a+0))+1:%p, &a[0][1]:%p\n", (*(a+0))+1, &a[0][1]);
fprintf(stdout, "a+1:%p, &a[1]:%p, *(a+1):%p, a[1]:%p\n", a+1, &a[1], *(a+1), a[1]);
fprintf(stdout, "(*(a+1))+1:%p, &a[1][1]:%p\n", (*(a+1))+1, &a[1][1]);
fprintf(stdout, "(&a)+1:%p\n", (&a)+1);
return 0;
}
结果 :
[root@db-172-16-3-150 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./a.c -o a && ./a
&a:0x7fff5c94e670, a:0x7fff5c94e670, *a:0x7fff5c94e670
(*(a+0))+1:0x7fff5c94e671, &a[0][1]:0x7fff5c94e671
a+1:0x7fff5c94e676, &a[1]:0x7fff5c94e676, *(a+1):0x7fff5c94e676, a[1]:0x7fff5c94e676
(*(a+1))+1:0x7fff5c94e677, &a[1][1]:0x7fff5c94e677
(&a)+1:0x7fff5c94e67c
指针运用 :
fprintf 打印 %s 需要传入char *指针 . %c 需要传入的是值.
下面试试使用 a+1 和 *(a+1) 来打印%s, a+1 会报错, 因为它是一个指向指针的指针. 不是char *.
[root@db-172-16-3-150 zzz]# cat a.c
#include <stdio.h>
int main() {
char a[2][6] = {"hello", "world"};
fprintf(stdout, "a+1:%s\n", a+1);
return 0;
}
[root@db-172-16-3-150 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./a.c -o a && ./a
cc1: warnings being treated as errors
./a.c: In function ‘main’:
./a.c:5: warning: format ‘%s’ expects type ‘char *’, but argument 3 has type ‘char (*)[6]’
将 a+1 指针强制转换成char * 就可以了 (char *) (a+1) . 如下 :
[root@db-172-16-3-150 zzz]# cat a.c
#include <stdio.h>
int main() {
char a[2][6] = {"hello", "world"};
fprintf(stdout, "a+1:%s\n", (char *) (a+1));
return 0;
}
[root@db-172-16-3-150 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./a.c -o a && ./a
a+1:world
你可能觉得不对劲, (char *) (a+1) 到底是强制转换还是去a+1这个地址的内容? 那我们就用一个三维数组来验证一下, 如果是取这个地址的值, 那么得到的应该是一个指向指针的指针. 也是不能打印的. 如下 :
[root@db-172-16-3-150 zzz]# cat a.c
#include <stdio.h>
int main() {
char a[2][6][6] = {{"hello", "world"},{"linux", "digoal"}};
fprintf(stdout, "a+1:%s\n", (a+1));
return 0;
}
结果
[root@db-172-16-3-150 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./a.c -o a && ./a
cc1: warnings being treated as errors
./a.c: In function ‘main’:
./a.c:5: warning: format ‘%s’ expects type ‘char *’, but argument 3 has type ‘char (*)[6][6]’
[root@db-172-16-3-150 zzz]# cat a.c
#include <stdio.h>
int main() {
char a[2][6][6] = {{"hello", "world"},{"linux", "digoal"}};
fprintf(stdout, "a+1:%s\n", *(a+1));
return 0;
}
结果
[root@db-172-16-3-150 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./a.c -o a && ./a
cc1: warnings being treated as errors
./a.c: In function ‘main’:
./a.c:5: warning: format ‘%s’ expects type ‘char *’, but argument 3 has type ‘char (*)[6]’
[root@db-172-16-3-150 zzz]# cat a.c
#include <stdio.h>
int main() {
char a[2][6][6] = {{"hello", "world"},{"linux", "digoal"}};
fprintf(stdout, "a+1:%s\n", (char *) (a+1));
return 0;
}
结果
[root@db-172-16-3-150 zzz]# gcc -O3 -Wall -Wextra -Werror -g ./a.c -o a && ./a
a+1:linux
从上面得到一个结论, 只要是数组名, 就会被替换成地址, 不管是1维还是2维. 想象一下a[2][6], a是1维的数组名, *a是2维的数组名. 都是替换成地址了. 多维也是如此.
【其他】
1. 当char []作为函数的参数时, 表示 char *. 当作为函数的参数传入时, 实际上是拷贝了数组的第一个元素的地址 .
所以 void test (char a[]) 等同于 void test ( char * a )
char x[10] ; 然后调用 test(x) 则等同于把 x 的第一个元素的地址赋予给参数 a .
2. char * a 和 char a[]
相同点 : a都是指针, 指向char类型.
不同点 : char a[] 把内容存在stack .
char *a 则把指针存在stack,把内容存在constants.
3. char * a[10] 和 char a[10][20]
相同点 : a 都是2级指针, *a 表示一级指针, **a 表示内存中存储的内容.
不同点 : char * a[10], 数组由char * 类型的指针组成;
char a [10][20] 表示一位放10个元素, 二维放20个元素, 值存放地是一块连续的内存区域, 没有指针.
4. 小窍门 : []和*的数量对应, 如 char a[][]的指针层数是2, 相当于char **a; char *a[]也是如此, 两层指针. 迷糊的时候数数到底有几个*几个[], 就知道什么情况下存储的是内容还是地址了? 如char a[][] 的情况里面: &a, a, *a 都是地址, **a 是内容.
【参考】
SYNOPSIS
#include <stdio.h>
int fprintf(FILE *stream, const char *format, ...);