谈谈C/C++的指针,数组和可变长数组

下面这些是个人的总结。注意这些内容对GNU C 使用,对标准C不一定适用。如果有不对的地方,欢迎大家指正。

  1. 可以单独定义一个空的指针,但不能单独定义一个空的数组。
    int *a; //合法,&a是a的地址,有意义,a是这个指针指向的地址,未定。如果打印出来可能是0,也可能是其他地址。
    int a[]; //非法。对C和C++都一样。
    但是定义一个维度为0的数组是可以的。
    int a[0]; //合法,而且a 和 &a都表示数组a的地址。

  2. 在struct或class里面可以光定义一个空指针,不可以光定义空数组。

typedef struct 
{
    
    
    int *a;
} A;
//sizeof(A)=8,也就是一个指针的长度。
typedef struct
{
    
    
    int b[]; //error: flexible array member in a struct with no named members
} B;

B是非法的。
但是如果指定b的维度为0,则是可以的。

typedef struct
{
    
    
    int b[0]; 
} B; 

sizeof(B)=0。

  1. 如果在上面含有b[]的B里面增加一个其他字段(不能是空数组),那么B就是合法的。
typedef struct
{
    
    
    int len;
    int b[];
} B;

sizeof(B)=4,也就是int len的长度。这里b[]不占空间,相当于留下一个痕迹。
但要注意,上面这种情况下int b[]只能是结构体中最后一个字段,后面不能再加别的字段。

  1. 参考下面的例子
    https://zhuanlan.zhihu.com/p/378032352
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    
    
    char a[0];
    printf("%p\n", a);

    char b[0];
    printf("%p\n", b);

    const char *c= "Hello";
    printf("%p\n", c);

    const char *d = "Hello";
    printf("%p\n", d);

    return EXIT_SUCCESS;
}

注意编译器有时候会把a和b的地址分配成同一个,因为这两个都是空数组。
但不管怎么样,c和d的地址肯定是同一个,因为"Hello"是字符串常量,放在内存中的字符串常量区,不在栈中。
输出结果为:

0x7fff17fd0216
0x7fff17fd0217
0x55d23d0fb008
0x55d23d0fb008

  1. 通常编译器会把int a[x]当成*(a + x)。这样a[3]和3[a]其实就是一回事。但是因为上面的空数组非法,我们不能认为当x为空时,int a[]等价于*(a)=*a。
    但其他情况大部分应该都是等价的。
    比如下面三个函数,其实是一回事。
#include <stdio.h>

int func1(int a[], int len) {
    
    
    printf("\n%d\n", a[len]);    
    return 0;
}

int func2(int a[0], int len) {
    
     //我发现这里写a[1],a[2],a[3]也是一样的,但写成a[4]以后会有警告“warning: ‘func2’ accessing 16 bytes in a region of size 12 [-Wstringop-overflow=]”,但结果还是对的。
//注意:在算表达式的时候a[2]和2[a]是一回事,但我们不能把int 2[a]或int 0[a]当函数入口参数传进去,编译会出错。
    printf("\n%d\n", a[len]);    
    return 0;
}

int func3(int *a, int len) {
    
    
    printf("\n%d\n", a[len]);    
    return 0;
}

int main(int argc, char* argv[]){
    
    
    int c[3] = {
    
    1,2,8};
    func1(c, 2);
    func2(c, 2);
    func3(c, 2);
    return 0;
}
  1. 当我们定义int a[3]时,a和&a都表示a的地址,是等价的。但是,当我们定义int *a时,a表示指针a的值,也就是指针a指向什么地址,而&a表示指针a本身的地址。

那么有意思的事情来了,一个指针p能不能指向它自己的地址呢?如果可以,那么&p, p, *p是什么关系呢?

#include <stdio.h>

int main(int argc, char* argv[]){
    
    
    int *p = &p;
    printf("\n%X, %X, %X\n", p, &p, *p);
    return 0;
}

答案当然是可以的。&p, p, *p的值就一样了。不过代表的意思还是不一样的。&p是指针p的地址。p是指针p所指向的地址,*p是指针p所指向的地址所储存的值。
main.c:12:20: warning: format ‘%X’ expects argument of type ‘unsigned int’, but argument 3 has type ‘int **’ [-Wformat=]
12 | printf(“\n%X, %X, %X\n”, p, &p, *p);
| ~^ ~~
| | |
| unsigned int int **

输出结果:
63B1BE80, 63B1BE80, 63B1BE80

  1. C的编译器不会管数组是否越界,可能严格的编译等级会给警告。它只会根据数组的起始地址往后算出数组元素的地址。
#include <stdio.h>

int main(int argc, char* argv[]){
    
    
    int e = 6, g = 4, h = 2;
    int a[0];
    int b = 3, c = 7, d = 9;
    printf("%p %p %p %p\n", a, &a[0], &a[1], &a[2]);
    printf("%X %X %X\n", a[0], a[1], a[2]);
    return 0;
}

运行结果为:
0x7ffdb9a67514 0x7ffdb9a67514 0x7ffdb9a67518 0x7ffdb9a6751c
0 57D07B00 23611D58
我们可以看出a数组并没有分配空间,但是a[0], a[1]和a[2]仍然可以访问。它们的地址就是a的地址往后排。a在栈上它们的地址就在栈上。a在堆上它们的地址就在堆上。
当然这是很危险的,因为很容易就访问到非法地址或其他data的地址了。
不过这个方法为后面的可变长数组埋下了伏笔。

  1. 指针变量其实也是变量,它也有地址,地址上存的就是它的值。不同于其他变量是因为我们定义它的时候加了一个*号,告诉编译器它存的值表示一个地址而已。
    我们知道C函数传参的时候,传值和传指针的区别。传值的时候,函数内部会生成一个值的拷贝。
    那传指针的时候,函数内部会不会生成一个指针的拷贝呢?答案是的。不过这个传进来的指针和指针的拷贝的值一样,都表示的同样的地址,这样我们在该地址上修改,结果就会保存下来。而值传递的时候,传进来的参数和它的拷贝的值一样,但是地址不一样。这样我们修改了拷贝,函数返回值之后拷贝就作废了。
#include <stdio.h>

int func(int *a)
{
    
    
    printf("\nin func(), a=%p, &a=%p, *a=%X", a, &a, *a);
    *a = 6;
    return 0;
}

int main(int argc, char* argv[])
{
    
    
    int b = 3;
    int *a = &b;
    printf("\nbefore func(), a=%p, &a=%p, *a=%X", a, &a, *a);
    func(a);
    printf("\nafter func(), a=%p, &a=%p, *a=%X", a, &a, *a);
    return 0;
}

运行结果:
before func(), a=0x7ffff2496bec, &a=0x7ffff2496bf0, *a=3
in func(), a=0x7ffff2496bec, &a=0x7ffff2496bb8, *a=3
after func(), a=0x7ffff2496bec, &a=0x7ffff2496bf0, *a=6
可见在func()中,&a的值变了,说明func()里面的a也是一个传进来的a的一个拷贝,虽然两个都是指针。
总结一个:其实传值和传指针也没什么本质的区别,函数里面都会给传进来的东西生成一个拷贝,函数返回这个拷贝也就作废了。不同的是:
传指针的话,拷贝也是指针,它们的值都是地址,这个地址在函数调用前就存在,函数调用后也还是存在,这样通过指针的拷贝在地址上修改,函数返回后还是有效的。
传值的话,修改是在拷贝上进行的,函数返回后就没用了。

  1. 那么我们能不能只用传值来达到传指针的作用呢?当然可以,我们把传的值设为一个地址就够了。下面就是把b的值变成了6,函数返回后还是有效。之所以a用long long 类型是因为64位机器的地址是8个字节,用int的话4个字节没法保存。
#include <stdio.h>

int func(long long a)
{
    
    
    *(int *)a = 6;
    return 0;
}

int main()
{
    
    
    int b = 3;
    long long a = (long long)&b;
    func(a);
    printf("b=%d", b);
    return 0;
}
  1. 可变长数组 Variable-length-array,简称VLA。下面这个链接参考的
    https://blog.csdn.net/zwl1584671413/article/details/122459038

最近在写C代码,经常看到Linux 的头文件中有的结构体后面会定义一个空数组,不知道其为何作用?经过高人指点终于明白其要点!

struct inotify_event {
__s32 wd;
__u32 mask;
__u32 cookie;
__u32 len;
char name[0];
};
如上,结构体最后一个元素name为空数组。

这是个广泛使用的常见技巧,常用来构成缓冲区。如果你是做嵌入式开发,这种技巧应该用得漫天飞了。 比起指针用空数组有这样的优势:

  1. 不需要初始化,数组名直接就是缓冲区数据的起始地址(如果存在数据)

  2. 不占任何空间,指针需要占用4 byte长度空间,空数组不占任何空间,节约了空间

“这个数组不占用任何内存”,意味着在计算结构体的size时,不会计算最后一个元素,例如上面sizeof(struct inotify_event) = 16 bytes (前四个元素的内存长度)

这种空数组定义最适合制作动态buffer,因为可以这样分配空间:

malloc( sizeof(struct XXX)+ buff_len );
这样的好处是:直接就把buffer的结构体和缓冲区一块分配了,空数组其实变成了buff_len长度的数组了,一次分配省了不少麻烦。

  1. 大家知道为了防止内存泄漏,如果是分两次分配(结构体和缓冲区),那么要是第二次malloc失败了,必须回滚释放第一个分配的结构体。这样带来了编码麻烦。

  2. 其次,分配了第二个缓冲区以后,如果结构里面用的是指针,还要为这个指针赋值。同样,在free这个buffer的时候,用指针也要两次free。如果用空数组,所有问题一次解决

如此看来,用空数组既简化编码,又解决了内存管理问题提高了性能,何乐不为?应该广泛采用。

  1. 下面这个可变长数组VLA的例子来自
    https://zhuanlan.zhihu.com/p/265986375
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct student
{
    
    
    int id;
    char sex;
    char a[0];
};

int main(void)
{
    
    
    struct student *s = NULL;
    s = malloc(sizeof(struct student) + 20);
    if (NULL == s)
    {
    
    
      printf("malloc failed..\n");
      return 1;
    }
    memset(s, 0, sizeof(struct student) + 20);
    s->id = 1;
    s->sex = 'M';
    strcpy(s->a, "hello world");
    printf("id: %d sex: %c a: %s\n", s->id, s->sex, s->a);
    free(s);
    return 0;
}

也可以不用malloc,而是直接在栈上分配student s。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct 
{
    
    
    int id;
    //char sex;
    char a[0];
} student;

int main(void)
{
    
    
    char str[] = "hello world";
    student s;
     s.id = 1;
 //   s.sex = 'M';
    //strcpy(s.a, "hello world");

    strcpy((char*)(&s + 1), str);
    printf("id: %d a: %s\n", s.id, s.a);
    return 0;
}

运行结果为:
id: 1 a: hello world
注意,如果把上面的 strcpy((char*)(&s + 1), str); 改成strcpy(s.a, “hello world”); 会有下面的编译警告,因为编译器记得a是没有分配空间的,它会觉得有问题。但结果还是对的。直接用(char *)(&s+1)就没问题了。注意这里&s是student类型的地址,所以它长度为4, +1后就直接指向a数组。

main.c:34:5: warning: ‘__builtin_memcpy’ writing 12 bytes into a region of size 0 overflows the destination [-Wstringop-overflow=]
34 | strcpy(s.a, “hello world”);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
id: 1 a: hello world

注意我把上面的char sex字段给删除了,下面是不删除的代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct 
{
    
    
    int id;
    char sex;
    char a[0];
} student;

int main(void)
{
    
    
    char str[] = "hello world";
    student s;
 
    s.id = 1;
    s.sex = 'M';
    strcpy((char*)(&s + 1), str);
    printf("id: %d sex: %c, a: %s\n", s.id, s.sex, s.a);
    return 0;
}

该代码会输出
id: 1 sex: M, a:
为什么a数组没内容了?我觉得这是由于char sex字段后面有3个字节的padding引起的。在int a[]字段前面应该不要有padding, 否则VLA就不好用了。

  1. 下面这个VLA的例子来自https://www.jianshu.com/p/cdc4c3ffc031,不过我加了很多修改的版本比较。
#include <stdio.h>
#include <malloc.h>
#include <string.h>

typedef struct line
{
    
    
    int len;
    char contents[0];    // 修改的地方。
}line;

int main(int argc, char **argv)
{
    
    
    char str[] = "hello world";

    struct line *ptr = (struct line*)malloc(sizeof(line) + strlen(str) + 1);
//    struct line *ptr = (struct line*)malloc(sizeof(line));
    
    ptr->len = strlen(str);
    strcpy((char*)(ptr + 1), str);
  
    printf("start: %p\n\n", (char*)ptr);

    printf("(char*)(ptr+1)address: %p\n", (char*)(ptr+1));
    printf("(char*)(ptr+1): %s\n\n", (char*)(ptr+1));

    printf("&(ptr->contents): %p\n", &(ptr->contents));
    printf("ptr->contents: %s\n\n", ptr->contents);

    return 0;
}

输出结果为:
start: 0x55f1250482a0

(char*)(ptr+1)address: 0x55f1250482a4
(char*)(ptr+1): hello world

&(ptr->contents): 0x55f1250482a4
ptr->contents: hello world

注意:把上面的
struct line ptr = (struct line)malloc(sizeof(line) + strlen(str) + 1);
换成
struct line ptr = (struct line)malloc(sizeof(line));
程序照样可以work,但是这是有风险的,因为没有分配的strlen(str)+1长度的空间谁也不知道会发生什么。
start: 0x561f821812a0

(char*)(ptr+1)address: 0x561f821812a4
(char*)(ptr+1): hello world

&(ptr->contents): 0x561f821812a4
ptr->contents: hello world

甚至你不用malloc,而是用line l把它定义在栈上,程序照样可以work!。这个风险更大,因为栈空间是有限的,没有分配的空间一是别人可能会写,而是如果这部分空间很大的话可能超出栈的范围。

/******************************************************************************

                            Online C Compiler.
                Code, Compile, Run and Debug C program online.
Write your code in this editor and press "Run" button to compile and execute it.

*******************************************************************************/
#include <stdio.h>
#include <malloc.h>
#include <string.h>

typedef struct line
{
    
    
    int len;
    char contents[0];
}line;

int main(int argc, char **argv)
{
    
    
    char str[] = "hello world";
    line l;
    printf("sizeof(l)=%lu\n", sizeof(l));
    l.len = strlen(str);
    strcpy((char*)(&l + 1), str);
    printf("start: %p\n", (char*)&l);
    printf("(char*)(&l+1) address: %p\n", (char*)(&l+1));
    printf("(char*)(&l+1): %s\n", (char*)(&l+1));
    printf("&(l.contents): %p\n", &(l.contents));
    printf("l.contents: %s\n", l.contents);
    printf("l.contents: %c%c%c%c%c%c%c%c%c%c%c\n", l.contents[0], l.contents[1], l.contents[2], l.contents[3], l.contents[4], l.contents[5], l.contents[6], l.contents[7], l.contents[8], l.contents[9], l.contents[10]);
    return 0;
}

运行结果为:
sizeof(l)=4
start: 0x7ffcfc72cd98
(char*)(&l+1) address: 0x7ffcfc72cd9c
(char*)(&l+1): hello world
&(l.contents): 0x7ffcfc72cd9c
l.contents: hello world
l.contents: hello world

猜你喜欢

转载自blog.csdn.net/roufoo/article/details/129991002