C语言专题笔记——结构体

程序中内存从哪里来

在一个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.csdn.net/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 

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语言专题)