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

这篇具有很好参考价值的文章主要介绍了谈谈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文章来源地址https://www.toymoban.com/news/detail-406478.html

到了这里,关于谈谈C/C++的指针,数组和可变长数组的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Python 编程中,下面这些常见的错误需要特别注意!

    在Python编程中,常见的错误有很多种,它们可以分为语法错误、运行时错误和逻辑错误。 拼写错误: 在变量名、函数名或的拼写上出现错误。 避免方法: 仔细检查代码,使用IDE或文本编辑器的拼写检查功能。 缩进错误: Python使用缩进来表示代码块,如果缩进不正确

    2024年02月14日
    浏览(29)
  • 很好辨别!央视3·15晚会上曝光的“主板机”,有下面这些特征

    2024年央视3·15晚会,曝光了制造水军的“主板机”。报道指出,该“主板机”可集成20块手机主板于一体,使用者只需一台设备即可同时操控20部手机,实现海量信息发布、地域伪装等功能。广泛应用于网络营销、游戏作弊、社交平台刷量等多种灰色地带,甚至不乏用于网络投

    2024年03月18日
    浏览(32)
  • 【C语言数组】一维数组,二维数组详解,数组传参,变长数组,这篇文章让你更全面的认识数组。

    前言: 大家好,我是 良辰丫 💞,今天带大家全面认识一下C语言里面的 数组 ,大家是不是满怀期待呢?嘿嘿嘿,别着急,我们往下看,感受C语言数组的魅力!!!💌💌💌 要么出众,要么出局。💝 乾坤未定,💟你我皆是黑马。 保存一组成绩的数据,数据多的时候难道要

    2024年01月19日
    浏览(42)
  • Scala的集合操作之可变数组和不可变数组,可变List集合与不可变List集合,可变Set与不可变Set操作,可变和不可变Map集合和元组操作

    for推导式的用法 Scala中的for推导式是一种用于对集合进行迭代和转换的强大工具。它提供了一种简洁的语法来处理集合中的元素,并生成新的集合或执行特定的操作。 for推导式的基本语法如下: 其中, pattern 是一个模式,用于解构集合中的元素, collection 是要遍历的集合。

    2024年02月10日
    浏览(42)
  • C语言可变数组 嵌套的可变数组,翻过了山跨过了河 又掉进了坑

    ​ 专栏内容 : postgresql内核源码分析 手写数据库toadb 并发编程 个人主页 :我的主页 座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物. 数组中元素是顺序存放,这一特性让我们存储和访问数据都很简单, 但也因为这一特性,我们在写代码时,往往不能确定数组

    2024年02月12日
    浏览(26)
  • 11-数组-二维区域和检索 - 矩阵不可变

    这是数组的第11篇算法,力扣链接。 给定一个二维矩阵  matrix ,以下类型的多个请求: 计算其子矩形范围内元素的总和,该子矩阵的  左上角  为  (row1, col1)  , 右下角  为  (row2, col2)  。 实现  NumMatrix  类: NumMatrix(int[][] matrix)  给定整数矩阵  matrix  进行初始化 int

    2024年01月24日
    浏览(44)
  • leetcode303. 区域和检索 - 数组不可变(java)

    难度 - 简单 原题链接 - 区域和检索 - 数组不可变 给定一个整数数组 nums,处理以下类型的多个查询: 计算索引 left 和 right (包含 left 和 right)之间的 nums 元素的 和 ,其中 left = right 实现 NumArray 类: NumArray(int[] nums) 使用数组 nums 初始化对象 int sumRange(int i, int j) 返回数组 num

    2024年02月12日
    浏览(32)
  • 【Rust 基础篇】Rust 的 `Rc<RefCell<T>>` - 共享可变性的智能指针

    在 Rust 中, RcRefCellT 是一种组合智能指针,用于实现多所有权共享可变数据。 Rc 允许多个所有者共享相同的数据,而 RefCell 允许在有多个引用的情况下对数据进行可变操作。 本篇博客将详细介绍 Rust 中 RcRefCellT 的使用方法和相关概念,以及它在代码中的应用场景。 RcRefCell

    2024年02月16日
    浏览(30)
  • 掌握了这些,才算真正了解C语言数组

    也许你认为,C语言中的数组非常好理解,就是把一组相同类型的元素存储在同一块空间里。但是你可能并没有真正理解数组的本质,不信的话请回答一下下面的几个小问题,如果你能非常清晰的回答这些问题,那么你对C语言中的数组的理解就入门了。 一维数组和二维数组在

    2024年02月02日
    浏览(29)
  • C语言--指针详解(下)--字符指针、数组指针、指针数组、函数指针、函数指针数组(转移表)

    在C语言中有一种指针类型为字符指针 char*,常用其来表示字符,使用如下: 除了上述用法之外,还可以有以下的用法: 在上面的代码中,字符 \\\" hello word \\\"是常量字符串,将\\\"hello word\\\"放入 pstr 指针的实质是将第一个字符 “ h \\\" 的地址传递给了 pstr ,通过首字符 ” h \\\" 就可以访问

    2024年02月03日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包