数组和指针的大小
对于指针,无论它指向何种类型(int、char或是结构体等类型),指针自身总是占用4个字节(32位的long型大小),因为指针和它指向的内容是分离开的,指针本身只保存实际内容的地址,因此其大小就是系统地址范围的大小。
对于数组,其本身就是用在存放实际内容的,因此它的大小就是这个数组实际需要的内存大小,例如数组 short score[6]; 就是定义了一个包含6个short元素的数组,其大小为6*sizeof(short)=12。这个大小通常是静态的,在编译阶段就可以计算出来(除动态数组)。
我们先自己思考下面程序的输出结果,然后看和实际运行结果是否符合:
int main()
{
short * count;
char * name_p;
char name_str[36];
int classes[12];
char * array_of_ptr[8]; //指针数组
char (* ptr_of_array)[36]; //数组指针
char real_str[36];
int i,j;
int array_2d[3][4] = {
{2, 4, 5, 8},
{8, 6, 3, 7},
{7, 1, 9, 2},
};
char * strarr[] = {
"Xi'an",
"Guangzhou",
"Hefei",
};
char * cityname = "Guangzhou";
/* test1: 指针和数组的size测试 */
printf("sizeof(count) = %d, sizeof(name_p) = %d, "
"sizeof(name_str) = %d, sizeof(classes) = %d, "
"sizeof(array_of_ptr) = %d, sizeof(array_of_ptr[0]) = %d, "
"sizeof(array_of_ptr[1]) = %d, sizeof(ptr_of_array) = %d\n",
sizeof(count), sizeof(name_p),
sizeof(name_str), sizeof(classes),
sizeof(array_of_ptr), sizeof(array_of_ptr[0]),
sizeof(array_of_ptr[1]), sizeof(ptr_of_array));
/* test2: 给指针数组赋值 */
name_p = (char *) malloc (36);
if (!name_p)
return 1;
strncpy(name_p, "Li Lei.", 35);
strncpy(name_str, "Han Meimei.", 35);
array_of_ptr[0] = name_p;
array_of_ptr[1] = name_str;
printf("\n%s, %s\n", array_of_ptr[0], array_of_ptr[1]);
/* test3: 指针内容的size测试 */
printf("\nsizeof(*count) = %d, sizeof(*name_p) = %d, sizeof(*array_of_ptr) = %d, sizeof(*array_of_ptr[0]) = %d, "
"sizeof(*array_of_ptr[1]) = %d, sizeof(*ptr_of_array) = %d\n",
sizeof(*count), sizeof(*name_p), sizeof(*array_of_ptr), sizeof(*array_of_ptr[0]), sizeof(*array_of_ptr[1]), sizeof(*ptr_of_array));
/* test4: 给数组指针赋值 */
ptr_of_array = &real_str;
strncpy(*ptr_of_array, "Li Lei.", 35);
printf("\n%s, %d\n\n", *ptr_of_array, sizeof(**ptr_of_array));
/* test5: 二维数组的元素地址 */
for (i = 0; i < 3; i++)
for (j = 0; j < 4; j++)
printf("%x ", &array_2d[i][j]);
printf("\n");
/* test6: 指针数组的元素地址 */
for (i = 0; i < 3; i++)
{
printf("strarr[%d] pointer addr addr %#x, str addr %#x, size = %d\n", i, &strarr[i], strarr[i], sizeof(*strarr[i]));
}
printf("strarr total size = %d\n", sizeof(strarr));
printf("cityname addr = %#x\n", cityname);
return 0;
}
其中,
test1考察了数组和指针大小的最简单情形。
test3则是计算指针指向的内容的大小,这就要看内容实际的数据类型了,因此sizeof(*name_p)就等于1,因为它指向的是char型的数组(的第一个元素)。
我们也知道(name_p + 1)实际上指向的是 (name_p的地址 + 1×sizeof(char)) 的位置,对于指针的++运算要注意这一点:每次加1是加了一个数据类型的大小,因此对于一个int型指针p来说,p+1实际上地址加了4。若不想受此限制我们可以将p强制转换成char型指针再做加法,或者直接转换成整数的加法,即 ((unsigned long)p + 1)。
test2和test4下面会讲到。
test5可以看出,二维数组的每个元素实际上是连续的,也就是说用一维数组 array_1d[12] 来取 array_2d[3][4] 的元素完全没有问题。
我们一开始就说指针实际上分为两部分:指针本身和它指向的内容,因此打印&name_p和name_p的地址是不同的。而对于数组,数组定义只是给这块内存起了个别名,因此打印&name_str和name_str的地址是相同的。对于test6,指针数组的每个元素都是指针,因此可以看到&strarr[i]和strarr[i]的地址是不同的。
上面程序的运行结果:
sizeof(count) = 4, sizeof(name_p) = 4, sizeof(name_str) = 36, sizeof(classes) =
48, sizeof(array_of_ptr) = 32, sizeof(array_of_ptr[0]) = 4, sizeof(array_of_ptr[
1]) = 4, sizeof(ptr_of_array) = 4
Li Lei., Han Meimei.
sizeof(*count) = 2, sizeof(*name_p) = 1, sizeof(*array_of_ptr) = 4, sizeof(*arra
y_of_ptr[0]) = 1, sizeof(*array_of_ptr[1]) = 1, sizeof(*ptr_of_array) = 36
Li Lei., 1
d8fcac d8fcb0 d8fcb4 d8fcb8 d8fcbc d8fcc0 d8fcc4 d8fcc8 d8fccc d8fcd0 d8fcd4 d8f
cd8
strarr[0] pointer addr addr 0xd8fc98, str addr 0xdb6a84, size = 1
strarr[1] pointer addr addr 0xd8fc9c, str addr 0xdb6a78, size = 1
strarr[2] pointer addr addr 0xd8fca0, str addr 0xdb6a70, size = 1
strarr total size = 12
cityname addr = 0xdb6a78
test2和test4涉及到指针数组和数组指针,我们下面单独讲一讲。
指针数组
顾名思义就是“指针的数组”,数组的每个元素都是一个指针,因此上面的 array_of_ptr[8] 就是定义了8个char型指针,它的size当然也就是 4 * 8 = 32。每个指针的使用方法就和普通指针一样,如上面的test2。
数组指针
就是“指向数组的指针”,也就是定义一个指针,这个指针指向一个数组的起始位置。上面的程序中,ptr_of_array就是一个数组指针,那么 *ptr_of_array 就是这个数组。注意,由于ptr_of_array本质上仍是一个指针,因此sizeof(ptr_of_array)=4。
上面最后一次打印中可以看到,sizeof(*ptr_of_array)是36,虽然这时ptr_of_array尚未指向任何实际的空间。实际上这也是数组指针的用途:指明类型和大小,那么这里ptr_of_array就指明了由它指代的数组的元素个数为36,每个元素都是char型。
数组指针用来明确数据类型,指针本身没什么意义,在使用时,要将其指向一个具体的、相同类型的实体,如上面test4。
数组指针还可用在二维数组的传参上,我们来看这个例子:
int manip_array(int (*ap)[4], int row)
{
int i, j;
for (i = 0; i < row; i++)
{
for (j = 0; j < 4; j++)
{
printf("%d ", ap[i][j]);
}
printf("\n");
}
return 0;
}
int main()
{
int array_2d[3][4] = {
{2, 4, 5, 8},
{8, 6, 3, 7},
{7, 1, 9, 2},
};
manip_array(array_2d, 3);
/*
运行结果:
Li Lei.
2 4 5 8
8 6 3 7
7 1 9 2
*/
return 0;
}
可见,参数ap就协助指明了二维数组的列数。(传递二维数组也可以用一个一维数组的指针,但是要重新计算数组下标,例如这里的array_2d[1][0]就相当于array_2d[4]。不过这种方式的可读性不强。)
数组和指针做形参时的区别
在做函数形参时,数组就完全被当做指针来对待了,二者没有区别。例如下面的各种数组的传参,sizeof(arg)都是4:
void handle_str_1(char * arg)
{
printf("sizeof arg = %d\n", sizeof(arg));
}
void handle_str_2(char arg[])
{
printf("sizeof arg = %d\n", sizeof(arg));
}
void handle_str_3(char arg[16])
{
printf("sizeof arg = %d\n", sizeof(arg));
}
void handle_str_4(int arg[3][4]) //注意这里和传入 char (*arg)[4] 完全相同
{
printf("sizeof arg = %d, sizeof arg[0] = %d\n", sizeof(arg), sizeof(arg[0]));
}
void handle_str_5(char * arg[8])
{
printf("sizeof arg = %d\n", sizeof(arg));
}
void handle_str_6(char (* arg)[6])
{
printf("sizeof arg = %d\n", sizeof(arg));
}
int main_array_pointer()
{
char str[16];
char * strp = "hello world!";
char * array_of_ptr[8];
char (* ptr_of_array)[6];
char real_str[6];
int array_2d[3][4] = {
{2, 4, 5, 8},
{8, 6, 3, 7},
{7, 1, 9, 2},
};
int (* ptr_of_nums)[4] = array_2d; //指向二维数组
/* sizeof的题目再复习一下~ */
printf("sizeof(str) = %d, sizeof(strp) = %d, sizeof(array_2d) = %d, "
"sizeof(array_2d[0]) = %d\n\n",
sizeof(str), sizeof(strp), sizeof(array_2d), sizeof(array_2d[0]));
printf("addr of &str = %#x, str = %#x, str + 1 = %#x\n", &str, str, str + 1);
printf("addr of &strp = %#x, strp = %#x, strp + 1 = %#x\n", &strp, strp, strp + 1);
printf("addr of array_2d = %#x, &array_2d = %#x, array_2d[0] = %#x, array_2d + 1 = %#x, *(array_2d + 1) + 1 = %#x\n\n",
array_2d, &array_2d, array_2d[0], array_2d + 1, *(array_2d + 1) + 1);
printf("%d\n", array_2d[1][1]);
printf("%d\n", ptr_of_nums[1][1]);
printf("%d\n", (*(ptr_of_nums + 1))[1]);
printf("%d\n\n", (*(array_2d + 1))[1]);
/* 传参 */
handle_str_1(str);
handle_str_1(strp);
handle_str_2(str);
handle_str_2(strp);
handle_str_3(str);
handle_str_3(strp);
handle_str_4(array_2d);
handle_str_5(array_of_ptr);
ptr_of_array = &real_str;
handle_str_6(ptr_of_array);
return 0;
}
运行结果:
sizeof(str) = 16, sizeof(strp) = 4, sizeof(array_2d) = 48, sizeof(array_2d[0]) =
16
addr of &str = 0x30fac8, str = 0x30fac8, str + 1 = 0x30fac9
addr of &strp = 0x30fabc, strp = 0xc76874, strp + 1 = 0xc76875
addr of array_2d = 0x30fa40, &array_2d = 0x30fa40, array_2d[0] = 0x30fa40, array
_2d + 1 = 0x30fa50, *(array_2d + 1) + 1 = 0x30fa54
6
6
6
6
sizeof arg = 4
sizeof arg = 4
sizeof arg = 4
sizeof arg = 4
sizeof arg = 4
sizeof arg = 4
sizeof arg = 4, sizeof arg[0] = 16
sizeof arg = 4
sizeof arg = 4
你就可以看到数组指针和二维数组的关系,我们也知道了为什么定义二维数组时 a[][4] 这样写是OK的,我们不必知道数组的行数,但必须知道列数,因为它就相当于 (*a)[4] ,而 a+1就得到a[1][0]的地址,a+2就得到a[2][0]的地址
函数指针
延伸开来,我们可联想到函数指针,也具有相同的用途,例如:
int (*pf) (int x); //声明一个函数指针
就定义了一个指针变量pf,它的类型是带一个int型参数、返回值是整型的函数。
同样的,fp要指向一个具体的函数才有意义:
pf = func; //给fp赋值
这样,调用fp()和调用func()的效果一样, y = fp(x) 。
我们还可以借助typedef来定义一个函数指针的类型,我们应该还记得typedef一个结构体类型的做法:
typedef struct _sd_buf {
char * name;
int sd_no;
} sd_buf;
这样的话,在定义结构体实例时,就可以写成 sd_buf sb; 而不必写成 struct _sd_buf sb;。
那么,对于函数指针,我也可以这样做:
typedef int (*my_pf)(int x);
这里 my_pf 就是一个新的类型,可以简化代码,看下面这段代码就知道了:
int chn0_send_frame(int fd, char * frame_data)
{
//send hd frame data to fd.
printf("chn 0 sending frame...\n");
return 0;
}
int chn1_send_frame(int fd, char * frame_data)
{
//send hd frame data to fd.
printf("chn1 sending frame...\n");
return 0;
}
typedef int (*frame_handler)(int fd, char * frame_data); //定义一个函数指针类型frame_handler
int (*vga_send_frame)(int fd, char * frame_data); //直接定义一个函数指针变量
struct frame_policy {
int chn;
int (*send_frame)(int fd, char * frame_data); //不使用typedef定义成员
};
//不使用typedef定义形参
int register_frame_hander(struct frame_policy * fp, int (*send_frame)(int fd, char * frame_data))
{
fp->send_frame = send_frame;
return 0;
}
//使用typedef定义形参
int register_frame_hander_01(struct frame_policy * fp, frame_handler send_frame)
{
fp->send_frame = send_frame;
return 0;
}
int main_func_pointer()
{
frame_handler fhandler;
struct frame_policy chn0_policy;
/* 给函数指针赋值的几种方式 */
fhandler = chn0_send_frame;
vga_send_frame = chn0_send_frame;
chn0_policy.send_frame = chn0_send_frame;
/* 调用函数指针 */
fhandler(6, NULL);
vga_send_frame(6, NULL);
chn0_policy.send_frame(6, NULL);
/* 参数为函数指针时,传参的方式 */
//register_frame_hander(&chn0_policy, chn1_send_frame); //OK
register_frame_hander_01(&chn0_policy, chn1_send_frame); //OK
chn0_policy.send_frame(6, NULL);
return 0;
}
可以看到,定义一个函数指针类型,简化了代码的编写,也增强了可读性。
PS: 对应的,“指针函数”就是一个返回值是指针类型的函数,没什么特别的,例如 int * func(int x);
给数组和指针赋值
在修改数组或指针的内容时,要注意是否允许修改,例如下面这段代码中的几种情况:
int main()
{
char * strp = "abcdef";
char strarr[] = "abcdef";
char strarrb[] = "xyeigh";
char * const strpconst = strarrb;
strarr[2] = 'm'; //OK,数组自身的内存是可读写的
printf("strarr = %s, sizeof = %d\n", strarr, sizeof(strarr));
//strp[3] = 'n'; //错误,指针指向的是一个常量,不能修改该常量的内存。
strp = "hijklmno"; //OK,指针自身是可以修改的,这里指向了另一个地址。
printf("strp = %s\n", strp);
//strarr = strarrb; //错误,数组本身不能作为左值。
strcpy(strarr, strarrb); //可以通过逐个元素赋值或strcpy等字符串操作函数修改字符数组内容。
printf("strarr = %s\n", strarr);
printf("strpconst = %s\n", strpconst);
//strpconst = strarr; //指针常量只能在定义时赋值,后面就不能更改了。
return 0;
}
上面strp指向的是字符串常量(存放在一块只读内存),给strp[3]赋值就是试图修改这块内存,因此是不允许的。当然,如果strp指向的是strarr,修改strp[3]就没问题,因为strarr是可读写的。
对于strp自身,它是一个指针变量,它指向的地址可以改变,即可以指向另外一块内存区域。
而对于指针常量(注意代码中const的位置),和其他类型的const常量一样,在定义之后指针本身也不能修改了,即不能改变它指向的地址。
更多数组和指针的区别,有一篇很好的文章: http://blog.chinaunix.net/uid-26983585-id-3336911.html ,看完这篇文章,上面一些执行结果会理解的更清楚。
0长数组
sizeof的补充:sizeof 是关键字不是函数,sizeof计算大小在编译阶段就可以完成(变长数组除外),也意味着sizeof(a)和sizeof a都是正确的写法。
其次,sizeof的计算结果跟字节对齐有关,例如对于结构体,默认情况下结构体的大小是其中最长的成员的整数倍。可以通过#pragma pack(n)或者__attribute((aligned (n))或者attribute(packed)来改变默认对齐方式。
struct header
{
char a;
long b[0];
};
struct header1
{
char a;
long b[1];
short c;
};
struct header2
{
char a;
short c;
};
int main_size0()
{
struct header aa;
struct header1 bb;
struct header2 cc;
printf("sizeof aa = %d, sizeof bb = %d, sizeof cc = %d\n",
sizeof(aa), sizeof(bb), sizeof(cc));
return 0;
}
运行结果为:
sizeof aa = 4, sizeof bb = 12, sizeof cc = 4
这里我们看到struct header结构体里有一个b[0]成员,这是个0长数组,
struct header
{
char a;
long b[0]; //或写作 long b[];
};
我机器上long长度是4,那么sizeof(header) = 4。可见成员bb[0]虽然不占空间,但在计算整个结构体size的时候也发挥了作用,仍然是按照最长类型来对齐的。
使用0长数组可以方便数据的分配和传输,我们在一个结构体后面紧跟数据的情况下就会用0长数组:没有数据时不占空间,有数据时可以很方便地分配和访问数据。例如下面的例子:
struct conn_ext {
unsigned char offset[CONN_EXT_NUM];
unsigned char len;
char data[0];
};
struct conn_ext * ct_ext =
(struct conn_ext *) malloc (sizeof(struct conn_ext) + data_len);
可见,在分配内存的时候,结构体和数据是一起分配的,且地址连续。这样,不仅通过结构体实例访问数据就很方便,而且拷贝这个实例的过程也很方便,直接从结构体指针开始拷贝sizeof(struct conn_ext) + data_len的长度,拷贝一次即可完成。
但要注意一点,在这个例子中,我们看到data的起始地址可能不是word对齐,那么在不支持非对齐内存访问的系统上,就要小心,在计算size时可以通过诸如 ALIGN(sizeof(struct conn_ext), align) + data_len; 的方式,在访问data时同样要注意。