数据类型在运行后的存储方式

数据类型在运行后的存储方式
今天因为遇到了二级指针数组的问题,想了好久发现不能理解,从新复习了c与指针的内容,根据自己的理解和参考一些大神的文章,总结了这篇;如果有理解错误的地方还请可以留言告知;下面将从程序的编译过程到存储方式一步步的梳理。

1.首先先了解一下从程序的编译流程:
在linux系统中编译一个C程序可以分为四阶段,预处理阶段->生成汇编代码阶段->汇编阶段->链接阶段,这里以linux环境下gcc编译器为例。使用gcc时默认会直接完成这四个步骤生成可以执行的程序,但通过编译选项可以控制值进入某些阶段,查看下面编译过程:
在这里插入图片描述
gcc [选项] 要编译的文件 [选项] [目标文件]
其中,目标文件可缺省,gcc默认生成可执行的文件名为:a.out
gcc main.c 直接生成可执行文件a.out
gcc -E main.c -o hello.i 生成预处理后的代码(还是文本文件)
gcc –S main.c -o hello.s 生成汇编代码
gcc –c main.c -o hello.o 生成目标代码

2.C程序目标文件和可执行文件结构
目标文件是源代码经过编译但未进行链接的那些中间文件,在linux中的.o文件,它跟可执行文件的内容与结构很相似,所以一般与可执行格式采用一种方式存储,在linux下,我们可以将他们统称ELF文件。ELF文件标准里面把系统中采用ELF格式的文件归为四类:

①可重定位文件.o(relocatable file)文件中包含代码和数据,可以被用来链接成可执行文件或是共享目标文件,静态链接库归为这一类。比如我们编译未连接的目标文件也是这一类 可
②执行文件(executable file)包含了可执行程序,一般没有扩展名。都是链接以后的文件了。
③共享目标文件.so(shared object)包含代码和数据,可以和可重定向文件一起链接,产生新的目标文件(不一定是可执行文件)。或者是被动态链接器与可执行文件结合作为进程映像的一部分运行(在堆和栈之间,靠近栈)
④核心转存文件core(core dump file)当进程以外终止时,系统可以将进程地址空间的内容以及终止时的一些其他信息转存到核心转存文件。

linux下的可执行文件有三个段文本段(text)、数据段(data)、bss段,可用nm命令查看目标文件的符号清单。
在这里插入图片描述
其中注意的BSS段,并没有保存未初始化段的映像,只是记录了该段的大小(应为该段没有初值,不管具体值),到了运行时再到内存为未初始化变量分配空间,这样可以节省目标文件空间。对于data段,只是保存在目标文件中,运行时直接载入。

3.程序和目标的对应关系
在这里插入图片描述

4.运行过程: 可执行文件->内存空间
不管是在Linux下C程序还是Windows下C程序,他们都是由正文段、数据段、BSS段、堆、栈等段构成的,只不过可能他们的各段分配地址不一样。Linux下的C程序正文段在低地址,而Windows下的C程序的正文段(代码段)在高地址。所有不用担心我用Linux环境和Windows环境共同测试带来不正确的数据.
在这里插入图片描述
首先是堆栈区(stack),堆栈是由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。栈的申请是由系统自动分配,如在函数内部申请一个局部变量 int h,同时判别所申请空间是否小于栈的剩余空间,如若小于的话,在堆栈中为其开辟空间,为程序提供内存,否则将报异常提示栈溢出。
其次是堆(heap),堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。堆的申请是由程序员自己来操作的,在C中使用malloc函数,而C++中使用new运算符,但是堆的申请过程比较复杂:当系统收到程序的申请时,会遍历记录空闲内存地址的链表,以求寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,此处应该注意的是有些情况下,新申请的内存块的首地址记录本次分配的内存块大小,这样在delete尤其是delete[]时就能正确的释放内存空间。

5.C语言存储空间布局,C语言一直由下面部分组成:

①正文段(code segment/text segment,.text段):或称 代码段,通常是用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。CPU执行的机器指令部分。( 存放函数体的二进制代码 。)
②只读数据段(RO data,.rodata):只读数据段是程序使用的一些不会被改变的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要修改,因此只需放在只读存储器中。
③已初始化读写数据段(data segment,.data段):通常是用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。常量字符串就是放在这里的,程序结束后由系统释放(rodata—read only data)。已初始化读写数据段(RW data,.data):已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器空间,在程序执行时它们需要位于可读写的内存区域,并具有初值,以供程序读写。 *只读数据段 和数据段统称为 数据段
④BSS段(bss segment,.bss段):未初始化数据段(BSS,.bss)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。全局变量 和 静态变量 的存储是放在一块的。初始化的全局变量和静态变量在一块区域(.rwdata or .data),未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(.bss), 程序结束后由系统释放。未初始化数据是在程序中声明,但是不具有初值的变量,这些变量在程序运行之前不需要占用存储空间。 * 在 C++中,已经不再严格区分bss和 data了,它们共享一块内存区域 * 静态存储区包括bbs段和data段
⑤堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆上被剔除(堆被缩减)。一般由程序员分配释放(new/malloc/calloc delete/free),若程序员不释放,程序结束时可能由 OS 回收。注意:它与数据结构中的堆是两回事,但分配方式倒类似于链表
⑥栈(stack):栈又称堆栈,是用户存放程序临时创建的局部变量,也就是我们函数大括号"{}"中定义的变量(不包括static声明的变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且等调用结束后,函数的返回值也会被存放在回栈中。由于栈的先进先出特性,所有栈特别方便用来保存/恢复调用现场。从这个意义上讲,把堆栈看成一个寄存、交换临时数据的内存区。由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈

6.堆和栈的区别

管理方式:对于栈来讲,是由编译器自动管理;对于堆来说,释放工作由程序员控制,容易产生 memory leak。

空间大小:一般来讲在 32 位系统下,堆内存可以达到接近 4G 的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在 VC6 下面,默认的栈空间大小大约是 1M。

碎片问题:对于堆来讲,频繁的new/delete 势必会造成内存空间的不连续,从而造成大量碎片,使程序效率降低;对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,永远都不可能有一个内存块从栈中间弹出。

生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

分配方式:堆都是动态分配的,没有静态分配的堆;栈有 2 种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配,动态分配由 alloca 函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,不需要我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高; 堆则是 C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,然后进行返回。显然,堆的效率比栈要低得多。

无论是堆还是栈,都要防止越界现象的发生.

7.C语言中变量的定义
存储类型 类型修饰符 数据类型 变量名
如:static volatile int value;
存储类型:决定变量的存储位置(static\auot\extern\regirster);
类型修饰符:决定变量的存储格式及表现方式(long\unsigned\signed\const\volatile);
数据类型:决定变量的存储空间,数据空间的大小将影响数据存储的大小;
变量名:决定引用变量时使用的标识符;

8.Volatile的含义:
volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)
在这里插入图片描述

如果将int a中的赋给b和c,正常情况下cpu需要执行下面步骤:
1.读取a中的值100到寄存器中;
2.将寄存器读取到的100写到b的内存中;
3.再从a中读取100到寄存器中;
4.将寄存器读取到的100写到c的内存中;
在这个过程中编译器为了使代码的效率更高,会对代码进行优化,所以第三步做的“再读取100到寄存器”的这个过程可能会被优化掉;这样将提高代码的效率;
但是并不是所有的情况都适合这样优化的;
例如:
系统中的秒,如果变量a读取了系统时间的秒是20,并将值赋给了b,过了5秒之后需要再次的读取系统时间的秒,并将结果赋给c;如果编译器优化代码后省去了第二次读取的过程,c的值将会有问题,volatile的作用就是告诉编译器,不能优化读写的过程;或者电压的采集过程中也需要注意这种情况的发生;

9.存储类型的定义
Auto:
auto类型即自动类型,只能用于修饰局部变量,定义局部变量时,如果省略存储类型,编译器会将变量默认为auto存储类型,使用auto修饰的局部变量存储再栈中,其作用域在其所定义的一堆{}内,不初始化其值为随机值,变量的生命周期会随着所在函数的解释而结束,所有每次调用函数的时候变量的值都不会保持上次调用的值。
Static:
Static类型即静态类型,既能修饰局部变量也能修饰全局变量,使用static修饰局部变量时,变量存储在静态区中(如果初始化就在数据段,未初始化在bss),其作用域在其所定义的一堆{}内,未初始化时其值为“0”变量的声明周期也全局变量一样,会一直到这个进程结束,所以每次调用其所在函数时,变量的值会保持上一次调用结束时的值,使用static修饰全局变量时,该全局变量不能在其他.c文件中被引用。
Extern:
当一个.c文件中引用了其他.c中定义的全局变量时,需要在当前的.c中将变量用extern声明,因为该全局变量已经在其他文件中分配了存储空间,只是在当前的文件引用,所以当前文件就不能再为该变量分配存储空间了,所以用extern修饰一个全局变量,就是告诉编译器要使用该变量,但该变量已经再其他文件分配了空间。
Regirster:
Regirster类型即寄存器类型,只能用于修饰局部变量,且只能修饰整形和字符型,程序中使用一个变量时需要将其从内存读取到CPU中的寄存器进行运算,运算结束后还要将其写回到内存中,如果一个变量经常被使用,我们可以使用regirster修饰该变量,这样该变量就直接的存储在寄存器中了,使用时就不需要从内存中读取,运算结束后也不需要写回到内存,提升了代码的效率,但CPU中寄存器的数量有限,所以程序中不允许有太多的变量被regirster修饰,寄存器不同意内存,没有地址,所以使用regirster修饰的变量不能进行取地址操作。

10.地址与内容
在这里插入图片描述

11.数组和结构体的构造和存储方式

//************************************************
	int b[5] = {1,2,3,4,5};
	int *p;
	p = b;
	printf("%d %d %d %d\n",b[1],*(b+1),p[1],*(p+1));
//************************************************

在这里插入图片描述

//************************************************
	int i,*p,*q;
	p = (int *)malloc(sizeof(int)*5);
	for(i=1;i<=5;i++)
	{
		p[i-1] = i;
		*(p+i-1) = i;
	} 
	q = p;
	printf("%d %d\n",p[1],*(p+1));
//**************************************************

在这里插入图片描述

//****************************************
	typedef struct node
	{
		int a;
		char ch1;
		char ch2;
		struct node *next;
	}lnode;
//****************************************
	lnode t,*p;
	p = &t;
	t.a = 5;
	printf("%d\n",t.a);
	p->a = 6;
	printf("%d\n",p->a);
	(*p).a = 7;
	printf("%d\n",(*p).a);
//****************************************

在这里插入图片描述

//****************************************
	typedef struct node
	{
		int a;
		char ch1;
		char ch2;
		struct node *next;
	}lnode;
//****************************************
	lnode *t;
	
	t = (lnode*)malloc(sizeof(lnode));
	(*t).a = 5;
	printf("%d\n",(*t).a);
	t->a = 6;
	printf("%d\n",t->a);
//****************************************

在这里插入图片描述

//********************************
typedef struct node
    {
	int a;
	struct node *next;
    }lnode;
//********************************
    lnode *q;

    q = (lnode*)malloc(sizeof(lnode)*5);
    for(i=1;i<=5;i++)
    {
	(*(q+i-1)).a = i;
	q[i-1].a = i;
	(q+i-1)->a = i;
    }	
    printf("%d %d %d %d\n",q[1].a,(*(q+1)).a,q[i-1].a,(q+1)->a); //********************************

在这里插入图片描述

//*********************************
    typedef struct node
    {
	int a;
	struct node *next;
    }lnode;
//*********************************
    lnode **r,*p;
    int i;
    r = (lnode **)malloc(sizeof(lnode *)*5);
    for(i=0;i<5;i++)
    {
	r[i] = NULL;
	*(r+i) = NULL;
    }

    for(i=0;i<5;i++)
    {
	p = (lnode *)malloc(sizeof(lnode));
	p->a = i;
	p->next = r[i];
	p->next = *(r+i);
	r[i] = p;
	*(r+i) = p;
    }
//*********************************

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值