Last Updated: 2023-12-22 08:56:34 Friday
-- TOC --
数组和指针,在C语言里面,一定要放在一起来学习和理解!
编写C语言代码,要记住数组和指针本质上相同,都是一块内存,分割成相同大小,位移访问,但用法和语法细节上也有一些不同。
char *k = NULL;
定义一个指向char类型的指针,初始化为NULL。
还可以在定义的同时做初始化:
char *k = malloc(n); // implicitly convert void* to char*, OK in C!
定义一个指向char类型的指针,这个指针指向的内存空间大小文n个bytes,这块空间在heap中,使用完后必须用free来释放,否则就是内存泄漏。
访问指针指向的内容,既可以用指针风格,也可以用数组风格:
char a = *(k+i);
char b = k[i];
这两种写法含义相同,语法都OK。
k+i,就是C语言中非常重要的 pointer arithmatic operation,它表示移动一个sizeof(type of pointer)的距离
用数组indexing的风格,自带了一次Dereferencing!
#define N 256
char k[N];
定义一个字符数组k,其大小为N个bytes,这块空间在函数调用栈中,函数返回空间自动释放,因为在栈中,因此N不宜太大,防止栈溢出!
数组在定义时,必须在初始化是指定大小,下面这种定义数组的方法是错的:
char k[]; // 错误的定义,编译器无法确定大小,但函数入参却写成这样可以!
还可以这样来定义并初始化数组:
char k[] = {1,2,3,4,5};
编译器能够处理这种定义风格,自动计算出这个数组的大小。
还有一种比较容易迷惑的数组定义方式:
char k[N] = {}; // all zero
char k[N] = {0};
这种定义方式,指定数组空间大小为N个bytes,同时初始化这个空间为0。其实,代码只是将k[0]=0
,其它的从k[1]到k[N-1],是有编译器自动初始化为0的,要特别注意这个细节!编译器会将有初始化的数组申明中的没有用户指定初始化的部分,全部设置为0(我理解为编译器首先把整个空间设置为0,然后再将用户申明的值定点赋值)。再看个示例:
char k[10] = {1,2,3}; // k[0]==1,k[1]==2,k[2]==3,其它位置是0
访问数组的内容,既可以用数组风格,也可以用指针风格:
char a = k[i];
char b = *(k+i);
这两种写法是一样的,k就是指向数组开始地址的指针。
数组申明时的初始化,看下面的测试代码:
$ cat test_array_assign.c
#include <stdio.h>
int main(void) {
char a[2] = {1}; // only set a[0]=1, the rest are all zero!!
printf("a[0] = %d\n", a[0]);
printf("a[1] = %d\n", a[1]);
return 0;
}
$ ./test_array_assign
a[0] = 1
a[1] = 0
char a[10] = {0}; // only set a[0]=0
这个初始化,让a指向的整个空间全部初始化为0,只是将a[0]
显式得设置为0,其它没有显式初始化的元素,是编译器默认设置为0的。
C99给数组申明时的初始化,增加了更多的灵活性,可以用index下标指定位置进行初始化,请看下面的测试代码:
$ cat test2.c
#include <stdio.h>
int main(void) {
int k[8] = {1,2,[5]=3,[7]=9};
for (int i=0; i<8; ++i)
printf("%d ", k[i]);
printf("\n");
return 0;
}
$ gcc test2.c
$ ./a.out
1 2 0 0 0 3 0 9
以上代码,指定了0,1,5,7这4个位置的初始化值,其它位置,编译器代劳,初始化为0。
上文那种错误的定义数组的方式(不指定大小),在作为函数入参的时候,就成了正确的了:
void function(char *k);
void function(char k[]);
专业术语:Array decay to pointer!
常识:函数入参不可能是数据!以上申明,都是指针。C++可以有数组的引用。
以上两种写法,都正确,语义也是一样的,表示将一个char类型的指针传入函数。此时没有k指向空间大小的信息,一不小心就会越界,因此,在某些场景下,有了下面的申明:
void function(char *k, size_t size);
void function(char k[], size_t size);
下面是多此一举的申明方式:
void function(char k[123], size_t size);
123
这个信息,是代码自身无法使用的信息,这行代码虽能够编译通过,但123是无意义的,也许仅仅是在某些时候提供多一点点的可读性。
定义一个字符数组:
char k[] = "abcdefg"; // just big enough to hold abcdefg and '\0'
虽没有直接指明k指向空间的大小,但编译器可以用过字符串计算出需要的空间大小,这段空间也在栈中,特别注意最后的那个\0
也被包含在这个空间的最后一个字符中,此时,k指向一个字符串。
如果用下面这样的定义:
char k[] = {'a','b','c'}; // no `\0` at the end
k指向不再是一个完整的字符串,只是普通的字符数组,没有最后的\0
!
下面是定义一个指针,但指向一个字符串常量,它的空间不在栈中,而是在readonly区域:
char *k = "abcdefg"; // k points to a readonly string constant, only C
const char *k = "abcdefg"; // both C and C++
以上定义方式的区别在于,字符数组的定义方式可以修改每个字符的内容,而指针定义的方式,本质上是定义了一个指针,指向一个字符串常量,如果修改k的地址,会导致这个字符串常量永远无法再被访问,如果修改这个字符串常量,C89的资料说,这是undefined behavior,gcc测试结果,segmentation fault。(这个常量在ELF文件的readonly section中,运行时被加载到readonly区域)
C99开始,C语言开始支持不太建议使用的VLA,Variable Lenght Array,即可以用一个变量来申明数组,也叫作变长数组。
int a = 1024;
int b[a]; // VLA, cannot be initialized
VLA的实现原理,即编译器会动态的在stack上开辟一块空间(或在heap上动态申请,C语言标准没有规定这块内存一定要在哪个区域)。这个特性,gcc支持,msvc不支持!
程序员完全可以用fixed length,malloc或memory pool的方式,代替VLA。如网友所说,似乎VLA的唯一好处,就是这块内存会被自动回收,降低了memory leak的风险,但同时它也带来了其它问题:
#include <stdio.h>
int main(){
goto end;
{ // must be scoped
int a = 128;
int b[a]; // can not be initialized
}
end:
printf("end...\n");
return 0;
}
gcc可以使用-Wvla
来增加出现VLA时的warning...
C语言擅长表达底层概念...它也没有提供高层抽象,这事儿C++干了...
二维数组
#include <stdio.h>
int main(){
char a[2][3] = {{},{1,2}};
char (*b)[3] = a;
printf("%p %p\n", a[0], b[0]); // same addr
printf("%p %p\n", a[1], b[1]); // same addr
for(int i=0; i<2; ++i)
for(int j=0; j<3; ++j)
printf("%d ", b[i][j]);
printf("\n");
return 0;
}
a
是二维数组,b
也是,只是b的定义有点烧脑,这样定义是为了让双下标可以在b上可以正常工作。a[1][0]
和a[1][1]
提供了非零初始值,其它位置都是0。char (*)[3]
,因此b+1移动3个char的长度。char[3]
。三维数组
#include <stdio.h>
int main(){
int n = 4;
char a[2][3][n];
a[0][1][2] = 9;
char (*b)[3][n] = a;
for(int i=0; i<2; ++i){
for(int j=0; j<3; ++j){
for(int k=0; k<n; ++k)
printf("%d ", b[i][j][k]);
}
printf("\n");
}
return 0;
}
b
的方式有个好处,可以接malloc或calloc。LeetCode第6题,字符串Z型转换,有个C语言版本,使用了VLA和多维数组技巧。
最经典的,main接口的第2个入参,以下两种定义方式等价:
int main(int argc, char **argv);
int main(int argc, char *argv[]);
但是在函数体中,定义指针的指针,如果使用[]
,如前所述,需要有更详细的定义,让编译器可以计算长度。示例代码,注释为打印输出的内容:
#include <stdio.h>
int main(int argc, char **argv){
printf("sizeof(argv) = %zu\n", sizeof(argv)); // 8
printf("sizeof(*argv) = %zu\n", sizeof(*argv)); // 8
printf("sizeof(**argv) = %zu\n", sizeof(**argv)); // 1
int a = 1;
int b = 2;
int c = 3;
int *p[] = {&a,&b,&c};
printf("%d %d %d\n", *p[0], *p[1], *p[2]); // 1 2 3
return 0;
}
坑
指针的指针,看起来也像是个二维数组,但下面两种申明是不一样的:
int (*a)[3];
int **b;
测试代码:
#include <stdio.h>
int main(int argc, char **argv){
int (*a)[3];
int **b;
printf("sizeof(a) = %zu\n", sizeof(a)); // 8
printf("sizeof(b) = %zu\n", sizeof(b)); // 8
printf("sizeof(*a) = %zu\n", sizeof(*a)); // 12
printf("sizeof(*b) = %zu\n", sizeof(*b)); // 8
return 0;
}
*a
的类型是int[3]
,只能取a[*][0-2]
,所以sizeof为12*b
的类型是int*
,就是个单纯的指针本文链接:https://cs.pynote.net/sf/c/202112213/
-- EOF --
-- MORE --