【精华文】C语言结构体特殊情况分析:结构体指针 / 基本数据类型指针,指向其他结构体

参考链接:Structure pointer pointing to different structure instance
注:可以查看此篇的问题和唯一的回复,那是相对正确的,不要看comment,有很多错误。

我是拒绝分析这种问题的,因为似乎没有人会这么乱用,但是……在华保健老师的编译原理示例代码和Linux0.11内核中,就遇到了这么神奇的代码,那就不得不研究一下了!毕竟是大神写的代码,我不知道应该是我渣。

1 测试代码

#include <stdio.h>
#include <stdlib.h>struct A {char a;int b;
};struct B {int c;int d;
};struct C {int e;char f;
};int main() {struct A a = { 'a', 100 };struct B b = { 101, 300 };struct C c = { 200,'c' };// 根据字节对齐,都占据8字节printf("A: size %d  %c  %d\n", sizeof(a), a.a, a.b);printf("B: size %d  %d  %d\n", sizeof(b), b.c, b.d);printf("C: size %d  %d  %c\n", sizeof(c), c.e, c.f);struct A *ap = &b; // A结构体指针,指向结构体Bprintf("%d %d\n",ap->a, ap->b);printf("%c %d\n", ap->a, ap->b);char *chp = &b;chp[1] = 'b';  // 这块区域其实是字节对齐导致的空闲空间printf("%d %d\n", ap->a, ap->b);printf("%c %d\n", ap->a, ap->b);/* 如何访问这块内存,取决于ap指针,能访问多大地方,取决于内存区域本身 */ap->a = 'c';  // ap->a = 'c'就是相当于 char a = 'c';ap->a = 1000; // ap->a = 1000 就是相当于 char a = 1000; 1000过大会被截断高位ap->b = 3000; // ap->b <=> int b ...struct C *cp = &b; // C结构体指针,指向结构体Bprintf("%d %d\n", cp->e, cp->f);printf("%d %c\n", cp->e, cp->f);cp->e = 3000;cp->f = 'e';cp->f = 1000;// 整形指针指向结构体Aint *bp = &a;bp[0] = 1000;bp[1] = 2000;printf("A: %c  %d\n", a.a, a.b);printf("A: %d  %d\n", a.a, a.b);bp[2] = 2000;	// 可以修改内存,但是堆栈溢出,// 因为该空间没有被分配(局部变量是保存在堆栈中的)return 0;
}

2 结构体占据空间问题 & 字节对齐

struct A {char a;int b;
};struct B {int c;int d;
};struct C {int e;char f;
};...
struct A a = { 'a', 100 };
struct B b = { 101, 300 };
struct C c = { 200,'c' };// 根据字节对齐,都占据8字节
printf("A: size %d  %c  %d\n", sizeof(a), a.a, a.b);
printf("B: size %d  %d  %d\n", sizeof(b), b.c, b.d);
printf("C: size %d  %d  %c\n", sizeof(c), c.e, c.f);
...

运行以上程序,我们可以直到,三个结构体分别创建了一个变量,并且每个结构体占据的空间大小都是8字节

在这里插入图片描述
至于为什么都是8字节,这是内存对齐问题,不展开说明了,我们看看这几个结构体被分配的空间情况吧

在这里插入图片描述

  • 每个结构体都占8字节的内存空间
  • 红色部分表示实际占用的空间
  • 蓝色部分表示空闲空间

注意:这就意味着,凡是被分配的8字节空间,是可以任意访问的,而空间外面是不允许访问的。

让结构体A的指针ap,指向结构体B的变量b

现在我们建立一个结构体A的指针,让其指向b。

struct A *ap = &b; // A结构体指针,指向结构体B
printf("%d %d\n",ap->a, ap->b);
printf("%c %d\n", ap->a, ap->b);

在这里插入图片描述
我们看看内存的情况,再分析一下打印的结果。

在这里插入图片描述

上面是内存的分布情况,现在

  • 访问ap->a打印出来的是:101e
  • 访问ap->b打印出来的是300

所以ap指针实际访问的应该是下面重点标出的部分:
在这里插入图片描述

而这部分,是不是很熟悉?
在这里插入图片描述

所以,ap指针尽管指向了结构体B,但是实际还是按照结构体A的结构访问内存

2.1 使用char指针指向结构体B

刚才我们发现,使用结构体A的指针,可以直接访问结构体B,那么,如果是基本数据类型呢?我们试一下。

char *chp = &b;
chp[1] = 'b';  // 这块区域其实是字节对齐导致的空闲空间
printf("%d %d\n", ap->a, ap->b);
printf("%c %d\n", ap->a, ap->b);

在这里插入图片描述
我们看到内存分布如上图,现在执行chp[1] = 'b'(b的ASCII码是62)

之后就变成了:

在这里插入图片描述

哦!这是令人惊讶的,char类型的指针指向了一块内存区域,然后使用下标修改了内存的值!

还记得动态数组申请吗?和内个是一样的原理!

int *a = (int *)malloc(sizeof(int) * 10);
a[0] = 1; // 使用下标访问
a[1] = 2;
...
free(a);

告诉我们两件事

  1. 指针默认指向最开始的元素,索引是0
  2. 使用下标索引可以依次访问后面的元素,每次向后移动的内存数,取决于指针的数据类型

所以上面的事情不难理解。

然后我们继续执行程序

printf("%d %d\n", ap->a, ap->b);
printf("%c %d\n", ap->a, ap->b);

在这里插入图片描述

尽管之前的空闲空间改变了,但是结果依然不变,也就是说我们之前的说法是正确的。

在这里插入图片描述

再进一步验证

/* 如何访问这块内存,取决于ap指针,能访问多大地方,取决于内存区域本身 */
ap->a = 'c';  // ap->a = 'c'就是相当于 char a = 'c';
ap->a = 1000; // ap->a = 1000 就是相当于 char a = 1000; 1000过大会被截断高位
ap->b = 3000; // ap->b <=> int b ...

结果显而易见,对于ap->a = 1000,尽管1000已经超过了1字节大小,但是最终只修改了第一个字节,这就好比char a = 1000一样,a = 0xe8

在这里插入图片描述

是的,1000 = 0x3e8,但是只有一个字节,所以最高位的3被舍弃了。

2.2 用结构体C指针cp指向结构体B

struct C *cp = &b; // C结构体指针,指向结构体B
printf("%d %d\n", cp->e, cp->f);
printf("%d %c\n", cp->e, cp->f);cp->e = 3000;
cp->f = 'e';
cp->f = 1000;

我们再试一试!

最终结果显而易见。

在这里插入图片描述
在这里插入图片描述

2.3 用int指针指向结构体A

// 整形指针指向结构体A
int *bp = &a;
bp[0] = 1000;
bp[1] = 2000;
printf("A: %c  %d\n", a.a, a.b);
printf("A: %d  %d\n", a.a, a.b);
bp[2] = 2000;	// 可以修改内存,但是堆栈溢出,// 因为该空间没有被分配(局部变量是保存在堆栈中的)

其实这个事情我们之前干过了,之前用char,现在用int再干一下。

在这里插入图片描述

这个事情进一步说明了什么呢?

  1. a提供了有限的8字节内存空间
  2. bp指针能够修改哪里,取决于它指向的地址;一次修改多大空间,取决于它数据类型的大小
  3. 指针不能修改未被分配的空间,最后bp[2]访问了外界空间,因此产生了
    在这里插入图片描述

因为局部变量都是被分配在栈中的,现在这个局部变量访问越界了,产生了错误,栈被破坏

栈破坏这里情况非常复杂,先粗浅理解为,使用了未分配的空间导致了错误吧。

Linux0.11 内核中,使用上述方法,实现了GDT和IDT。

3 小结:精华在这里

分析了这么多,最终小结一下吧。

我们的眼中只有两件事

  • 已分配的内存空间
  • 某数据类型的指针

现在,我们就让指针指向内存空间的起始地址,然后就可以操作这个内存空间了。

再增加一些限制

  • 内存空间就这么大,不能访问外面
  • 指针每次访问的地址,是通过下标访问的,一次只能移动数据类型大小的整数倍

在这里插入图片描述

这个时候你眼中的C语言,分配一块内存,再创建一个指针,打遍天下无敌手!

当然了,除了特殊情况一般没人这么干,你会疯掉,看你代码的人也会疯掉!

4 补充:直接深入底层,看汇编代码

之前我们的分析是基于C语言层级的,比较抽象,实际上,编译完成之后的汇编语言,一看就明白了。
在这里插入图片描述

你可以看到ap->a直接访问的是byte,而ap->b访问的是dword,一个是字节,一个是双字,大小自然清晰。

这也是编译器的功能,把C语言提供的,方便人类使用的大量抽象,给翻译成方便机器使用的少量指令的复杂排列组合。

5 什么叫打遍天下无敌手呢?

其实就是瞎玩儿吧……但是的确可以这么干的!我们试一试。

int main() {char aaa[4] = { 1,2,3,4 };char aaa2[4] = { 1,2,3,4 };int *bbb = &aaa;printf("\n\n%x\n\n\n", bbb[0]);return 0;
}

会打印什么呢?显而易见的!内存是01 02 03 04,然后一个int *指针访问了它,打印04030201

在这里插入图片描述

我们可以使用bbb[0]或者*b都行,因为b指向起始地址。

那,能不能通过bbb[1]访问aaa2的内存呢?

不行! 因为aaa1aaa2是两个数组变量,他们在内存中的位置不是连续的,是随机的,如果你想达到内种效果,那就是前面提到的结构体了,把这两个放进一个结构体里面,就是连续分配内存了,就能使用bbb[1]了。


最后,记住只有两件事

  • 一块已分配的内存
  • 一个指针

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384608.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

enum in c language

今天说说C语言中的枚举。 参考&#xff1a;Enumeration (or enum) in C 1 定义 定义一个枚举类型很容易&#xff1a; enum aa { a1, a2, a3 };这里 enum是关键字aa是枚举变量&#xff0c;也就是我们自定义类型a1,a2,a3是枚举成员 然后怎么使用呢&#xff1f; 首先&#…

信号量SIGCHLD的使用,如何让父进程得知子进程执行结束,如何让父进程区分多个子进程的结束

本教程基于 Ubuntu 20.10 gcc 10.2.0. 示例程序如果不能正常编译和执行&#xff0c;说明您系统和工具版本与我的不匹配&#xff0c;请自行查阅资料。 0 概述 先给出该信号的描述&#xff1a; SignalValueDescriptionSIGCHLD17Child status has changed (POSIX). Signal sent …

UNIX哲学

参考&#xff1a; 对比Linux与Windows 使用Linux想要做某些事情的时候&#xff0c;就拆开想&#xff0c;想想我需要哪些功能&#xff0c;需要哪些工具&#xff0c;依次怎么执行&#xff0c;然后用管道建立连接&#xff0c;让数据依次流过不同的工具&#xff0c;从而得到最终结果…

fork创建多个子进程

references: [1] how to create two processes from a single Parent [2] fork() in C [3] linux中fork同时创建多个子进程的方法 fork的本质&#xff0c;就是复制&#xff0c;把当前进程复制一份&#xff0c;然后两个进程并发地执行fork后面的语句&#xff0c;区别就是&#x…

wait系统调用

reference:Wait System Call in C 只强调几点&#xff0c;剩下的直接看参考链接内容就好了&#xff0c;不是偷懒&#xff0c;而是里面内容写的很好了&#xff0c;没必要再写一遍了&#xff0c;这种东西就是单纯的系统调用而已&#xff0c;理解了功能&#xff0c;就完事了&#…

正则表达式特别需要注意的点:“空“字符的匹配

在正则表达式中&#xff0c;[...]代表1个字符&#xff0c;不管里面有多少字符&#xff0c;最终这个东西的结果都是1个字符。 对于表达式[^a]表达的匹配除了a之外的字符&#xff0c;并且是1个字符。 需要注意的是&#xff0c;有些特殊字符是不会被匹配的。 我们看一个示例&am…

vim多列操作--插入/删除

插入 How to insert text at beginning of a multi-line selection in vi/VimVim Commands 删除 ctrl v使用上下左右键选中一片区域按d删除

vim进行行内某部分的复制剪切粘贴

ctrl v使用方向键选中你要复制的部分 按d&#xff08;剪切&#xff09;或者按y&#xff08;复制&#xff09;再移动到你的目标位置&#xff0c;按p粘贴&#xff08;在正常模式下才行&#xff0c;如果不是&#xff0c;先按esc&#xff09; 这个过程与你操作word文档的复制粘贴…

函数调用堆栈

基于孟宁老师的Linux内核分析 1 int g(int x){ 2 int y x 3;3 return y;4 }5 6 int f(int x){7 int z x 10;8 return g(z);9 }10 11 int main(){12 int a f(8) 1;13 return 0;14…

Vivado提高综合和实现的速度

让计算机的资源尽可能给vivado&#xff0c;综合、实现的时候修改一个参数 jobs改为你的计算机的最大值&#xff0c;我的计算机是12核的。 速度会快很多&#xff01;

安装Ubuntu RISC V toolchain失败(网速、git配置原因)

git获取大容量工程出错&#xff1a;RPC failed&#xff1b; curl GnuTLS recv error : Decryption has failed. error: RPC failed; curl 56 GnuTLS recv error (-54): Error in the pull function.fatal: The remote end 官方GitHub仓库 gitee镜像仓库 如果网速不够&#xff0…

VirtualBox Ubuntu个人配置

注意这里VT-x启用&#xff0c;除了在BIOS启用CPU虚拟化&#xff0c;还得在命令行设置一次才可以勾选。 F:\>cd F:\VirtualBox # 进入VirtualBox安装目录F:\VirtualBox>VBoxManage.exe list vms # 查找所有虚拟机 "rhel64" {240f96d8-6535-431d-892e-b70f3dc4…

Ubuntu停止维护版本的软件源配置和系统升级方法

这里以Ubuntu 20.10版本为例&#xff08;当前是2022.2.14&#xff0c;该版本已经停止维护&#xff09;&#xff0c;我们现在需要正常使用该版本&#xff0c;并且期待升级到21.10版本&#xff0c;我们需要 配置正确是软件源升级该版本 配置正确的软件源 配置国内镜像源 我们…

diff and colordiff on Ubuntu

在Ubuntu中使用diff来对比文件差异&#xff0c;但是不是很好用&#xff0c;尤其是着色方面&#xff0c;用起来很麻烦&#xff0c;因此可以安装colordiff。 我们有两个文件file1和file2&#xff0c;使用命令 colordiff file1 file2 -y -B -W 140就可以对比文件差异&#xff0c…

帮助你成为高手的视频和资料

1. 为什么大多数人不会真正成功 博客链接 视频链接 2. TED演讲&#xff1a;真正拉开你与周围人之家差距的&#xff0c;是自学能力 视频链接 3 埃隆马斯克&#xff1a;第一性原理&#xff0c;少用类比&#xff0c;类比多了就不能抓住本质了 4 如何成为一个顶尖高手 文章链…

【数据结构】快速排序非递归算法及其改进

在学数据结构中排序这一章节的时候&#xff0c;有一道有关快速排序的作业题描述如下&#xff1a; 按下述要求编写快速排序的非递归算法&#xff1a; 定义一个栈&#xff08;或队列&#xff09;&#xff0c;把整个序列的上、下界入栈&#xff08;或队列&#xff09;。当栈&#…

【数据结构】对快速排序原理的理解(图解,通俗易懂)

学习数据结构时&#xff0c;书本上直接给出了快速排序的过程以及代码&#xff0c;对其原理解释的不够详细&#xff0c;琢磨代码后&#xff0c;发现其原理其实十分简单&#xff0c;简述如下&#xff1a; &#xff08;1&#xff09;在待排序列中找一个“中枢元素”&#xff08;书…

【离散数学】图论基础知识

文章目录1 图的基本概念2 图的连通性3 图的矩阵表示4 几种特殊的图4.1 二部图4.2 欧拉图4.3 哈密顿图4.4 平面图5 无向树6 生成树1 图的基本概念 无向图&#xff1a; 简而言之&#xff0c;边不带方向的图就是无向图。 有向图&#xff1a; 简而言之&#xff0c;边带方向的图就…

【运筹与优化】单纯形法解线性规划问题(matlab实现)

文章目录单纯形法步骤&#xff1a;1.将线性规划问题化为标准形式2.列出单纯形表3.进行最优性检验4.从一个基可行解转换到另一个目标值更大的基可行解&#xff0c;列出新的单纯形表5.重复3、4直到计算结束为止举例单纯形法matlab实现单纯形法是一种解线性规划问题的算法&#xf…

【Linux系统编程学习】 GCC编译器

此为牛客网Linux C课程1.2&1.3的课程笔记。 0. 简介 1. gcc和g的安装 sudo apt install gcc g2. gcc常用参数选项 3. gcc工作流程 首先是预处理器对源代码进行预处理&#xff08;后缀名.i&#xff09;&#xff0c;主要做以下事情&#xff1a; 把头文件加入到源代码当中删…