程序中内存从哪里来
在一个c语言程序中,能够获取内存的三种情况:栈(stack)、堆(heap)、数据区(.data)。
栈:
运行时自动分配&自动回收
反复使用:栈内存在程序中其实就是一块空间,程序反复使用这块空间。
脏内存
临时性
栈会溢出
堆:
OS堆管理器管理
大块内存
程序手动申请&释放
脏内存
临时性:堆内存只在malloc和free之间属于此进程,而可以访问,在malloc之前和free之后都不能再访问,否则会有不可预料的后果。
malloc(4) gcc中的malloc默认最小是以16B为分配的单位的。如果malloc小于16B的大小都会返回一个16字节大小的内存,malloc实现时没有实现任意自己分配,而是允许一些大小的块内存的分配。
数据区:
编译器在编译程序的时候,将程序中的所有元素分成了一些组成部分,各个部分构成了一个段,所以说段是可执行程序的组成部分。
代码段:代码段就是程序中的可执行部分,直观理解代码段就是函数堆叠组成的。
数据段(也被称为数据区、静态数据区、静态区):数据段就是程序中的数据,直观理解就是c语言程序中的全局变量。(注意:全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函数的数据)
bss段(又叫ZI(zero initial)段):bss段的特点就是被初始化为0,bss段本质上就是属于数据段,bss段就是被初始化位0的数据段。
注意区分:数据段和bss段的区别和联系;二者本来没有本质区别,都是用来存放c程序中的全局变量的。区别在于把显示初始化位非零的全局变量存在.data段中,而把显式初始化位0或者并未显式初始化(C语言规定未显式初始化的全局变量默认为0)的全局变量存在bss段。
有哪些特殊的数据会被放到代码段
char *p = "linux";
// 定义字符串时,字符串"linux"实际被分配在代码段,也就是说这个"linux"字符串实际上是一个常量字符串而不是变量字符串。
const型常量:
c语言中const关键字用来定义常量,常量就是不能被改变的量。
const的实现方法至少有2种:
第一种就是编译将const修饰的变量放在代码段去以实现不能修改(普遍见于各种单片机的编译器);
第二种就是由编译器来检查以确保const型的常量不会被修改,实际上const型的常量还是和普通变量一样放在数据段的(gcc中就是这样实现的)。
结构体概述
数组有2个明显的缺陷:
第一个是定义时必须明确给出大小,且这个大小在以后不能再更改;
第二个是数组要求所有的元素的类型必须一致。
结构体是用来解决数组的第二个缺陷的,可以将结构体理解为一个其中元素类型可以不相同的数组。
结构体完全可以取代数组,只是在数组可用的范围内数组比结构体更简单。
结构体的对齐访问
结构体为何要对齐访问
结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率,否则会大大降低效率。
内存本身是一个物理器件(DDR内存芯片,SoC上的DDR控制器),本身有一定的局限性:如果内存每次访问时按照4字节对齐访问,那么效率是最高的;如果你不对齐访问效率要低很多。
还有很多别的因素和原因,导致我们需要对齐访问。譬如Cache的一些缓存特性,还有其他硬件(譬如MMU、LCD显示器)的一些内存依赖特性,所以会要求内存对齐访问。
对比对齐访问和不对齐访问:对齐访问牺牲了内存空间,换取了速度性能;而非对齐访问牺牲了访问速度性能,换取了内存空间的完全利用。
gcc支持但不推荐的对齐指令:#pragma pack() #pragma pack(n) (n=1/2/4/8)
(1)#pragma是用来指挥编译器,或者说设置编译器的对齐方式的。编译器的默认对齐方式是4,但是有时候我不希望对齐方式是4,而希望是别的(譬如希望1字节对齐,也可能希望是8,甚至可能希望128字节对齐)。
(2)常用的设置编译器编译器对齐命令有2种:
第一种是#pragma pack(),这种就是设置编译器1字节对齐(有些人喜欢讲:设置编译器不对齐访问,还有些讲:取消编译器对齐访问);
第二种是#pragma pack(4),这个括号中的数字就表示我们希望多少字节对齐。
(3)我们需要#prgama pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐参数就是n。
(4)#prgma pack的方式在很多C环境下都是支持的,但是gcc虽然也可以不过不建议使用。
gcc推荐的对齐指令__attribute__((packed))or__attribute__((aligned(n)))
(1)__attribute__((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。
(2)__attribute__((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)
struct stu
{ //1字节对齐 4字节对齐
int a; //4 4
char b; //1 2(1+1)
short c; //2 2
short d; //2 4(2+2)
}__attribute__((packed));
or__attribute__((aligned(n)));
// n为1、2、4时,sizeof(struct stu)都为12
// 8 为 16, 1024 为 1024
参考阅读blog:
http://www.cnblogs.com/dolphin0520/archive/2011/09/17/2179466.html
http://blog.youkuaiyun.com/sno_guo/article/details/8042332
offsetof宏与container_of宏
由结构体指针进而访问各元素的原理
通过结构体整体变量来访问其中各个元素,本质上是通过指针方式来访问的,形式上是通过.的方式来访问的(这时候其实是编译器帮我们自动计算了偏移量)。
offsetof宏
// TYPE是结构体类型,MEMBER是结构体中一个元素的元素名
// 宏返回的是member元素相对于整个结构体变量的首地址的偏移量
#define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实质是通过编译器来帮我们计算)。
offsetof宏的原理:我们虚拟一个type类型结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量。
学习思路:第一步先学会用offsetof宏,第二步再去理解这个宏的实现原理。
(TYPE *)0 这是一个强制类型转换,把0地址强制类型转换成一个指针,这个指针指向一个TYPE类型的结构体变量。 (实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错)。
((TYPE *)0)->MEMBER (TYPE *)0是一个TYPE类型结构体变量的指针,通过指针指针来访问这个结构体变量的member元素
&((TYPE *)0)->MEMBER 等效于&(((TYPE *)0)->MEMBER),意义就是得到member元素的地址。但是因为整个结构体变量的首地址是0
container_of宏
// ptr是指向结构体元素member的指针,type是结构体类型,member是结构体中一个元素的元素名
// 这个宏返回的就是指向整个结构体变量的指针,类型是(type *)
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。
typeof关键字的作用是:typeof(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。
工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了,再把这个地址强制类型转换为type *即可。
大小端问题
include <stdio.h>
union myunion
{
int a;
char b;
};
// 如果是小端模式则返回1,小端模式则返回0
int is_little_endian(void)
{
union myunion u1;
u1.a = 1; // 地址0的那个字节内是1(小端)或者0(大端)
return u1.b;
}
int is_little_endian2(void)
{
int a = 1;
char b = *((char *)(&a));// 指针方式其实就是共用体的本质
return b;
}
int main(void)
{
int i = is_little_endian1();
if (1 == i){
printf("小端模式\n");
}
else{
printf("大端模式\n");
}
return 0;
}
本文介绍了C语言中结构体的相关知识,包括内存的分配(栈、堆、数据区)、结构体的对齐访问原理以及offsetof和container_of宏的使用。同时探讨了大小端问题在结构体中的影响,帮助读者深入理解C语言的内存管理和结构体操作。
692

被折叠的 条评论
为什么被折叠?



