我是靠谱客的博主 激昂网络,最近开发中收集的这篇文章主要介绍4.5、数组&字符串&结构体&共用体&枚举4.5.1程序中的内存从哪里来1:4.5.5、C语言的字符串和字符数组类型4.5.7、结构体的本质4.5.8结构体的对齐访问:4.5.9_offsetof宏与container_of宏:4.5.10、共用体union。4.5.15.枚举,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

4.5.1程序中的内存从哪里来1:

(1)在C中获取内存的三种情况:栈(stack)、堆(heap)、数据区(data)。

(2)栈的详解:运行时自动分配&自动回收:栈是自动管理的,程序员不需要手动干预,方便简单。 反复使用 :栈内存在程序中其实就是那一块空间,程序反复使用这一块空间。 脏内存 ,栈内存由于反复使用,每次使用后程序不会去清理,因此分配到时保留原来的值。 临时性 (函数不能返回栈变量的指针,因为这个空间是临时的), 栈会溢出 (因为操作系统事先给定了栈的大小,如果在函数中无穷尽的分配栈内存总能用完)。

#include <stdio.h>
//函数不能返回函数内部局部变量的地址,因为这个函数执行完返回后这个局部变量已经不在了。
//释放了,但是栈内存还在还可以访问,但是访问时实际上这个
//内存地址已经和当时那个变量无关了。
int *func(void)
{
    int a = 4;//a是局部变量,分配在栈上又叫栈变量,又叫临时变量。
    printf("&a = %p.n",&a);
    return &a;
    //这个函数返回的是一个指针。
}
void func2(void)
{
    int a =33;
    int b = 33;
    int c = 33;
    printf("in func2,&a = %p.n",&a);
}
void stack_overflow2(void)
{
    int a = 2;
    stack_overflow2();//没有递归尽头,程序崩掉了。
}
int main(void)
{
    
    int *p = NULL;
    p = func();
    func2();
    func2();           //重改了地址。
    printf("p = %p.n",p);
    
    printf("*p= %d.n",*p);  //栈的值是脏的。这个变量的地址都被上一次访问的改变了。*p本来是4,现在输出的值是随机的。因为一个函数的局部变量的地址是不能在函数外被访问的
    
    stack_overflow2();
    
    return 0;
}

(3) 堆的详解:堆管理器是操作系统的一个模块,堆管理内存分配灵活,按需分配。堆内存也是反复使用的,而且使用者用完释放前不会清除,因此也是脏的。临时性:堆内存只在malloc和free之间属于我这个进程,而可以访问,在malloc之前和free之后都不能再访问,否则会有不可预料的后果。

(4)堆内存使用的范例:void * 是个指针类型,malloc返回的是一个void *类型的指针,实质上malloc返回的是堆管理器分配给我本次申请的那段内存空间的首地址(malloc返回的值其实是一个数字,这个数字表示一个内存地址),为什么要使用void *作为类型?主要原因是malloc帮我们分配内存时只是分配了内存空间,至于这段空间将来用来存储什么类型的元素malloc是不关心的,具体的指向类型是我们程序员自己来决定的。

(5)什么是void类型:不是表示没有类型,而是表示万能类型,意思是说这个数据类型当前是不确定的,在需要的时候可以再去指定它的具体类型。Void *类型是一个指针类型,指针本身占4字节,但是指针指向的类型是不确定的,换句话说这个指针在需要的时候可以被强制转化成其他任何一种确定类型的指针,也就是说这个指针可以指向任何类型的元素。

(6)malloc的返回值:成功申请空间后返回这个内存空间的指针,申请失败时返回NULL。所以malloc获取的内存指针使用前一定要检查是否为NULL。.。Malloc申请的内存使用完后要free释放,,不要在free()之前对这个内存空间重新赋值,否则这片内存就丢失了,但是你联系不到这段内存,堆管理器也无法通过地址找到,会造成内存泄漏(通俗点就是吃内存)。,Free(p);会告诉堆管理器这段内存我用完了你可以回收了。堆管理器回收了这段内存后当前进程就不应该再使用这段内存了,因为释放后堆管理器就可能把这段内存再次分配给别的进程,所以你就不能再使用了。

(7)再调用free归还这段内存之前,指向这段内存的指针p一定不能丢(也就是不能给p另外赋值)。因为p一旦丢失这段malloc来的内存就永远的丢失了,内存泄漏,直到当前 程序结束 时操作系统才会回收这段内存。 十六进制———》二进制(四位一体)——》十进制。

(8)gcc中的malloc默认最小是以16byte为分配单位的。Malloc(20)去访问25,200,2500.。会怎么样,继续往后访问,总有一个数字处于段错误。

#include <stdio.h>
#include <stdlib.h>//使用malloc的头文件。
int main(void)
{
    //第一步:申请和绑定                    //申请的内存就像一个数组,以数组形式来访问
    int *p = (int *)malloc(20 * sizeof(int));  //malloc默认申请最少内存为16字节。
    
    //int *p = (int *)malloc(100*sizeof(int));
    
    //第二步使用返回值校验分配是否成功
    if(NULL == p)
    {
        printf("malloc error.n");
        return -1;
    }
    //第三步使用申请到的内存
    *(p+3) = 12;
    *(p+300) = 1234;
    printf("*p3 = %d.n",*(p+3));
    printf("*p300 = %d.n",*(p+300));   //但是加到一定程度后,会出现段错误。
    
    free(p);
    /*
    int *p1 = (int *)malloc(4);  //p2 - p1 =     //malloc默认申请最少内存为16字节。
    int *p2 = (int *)malloc(4);  //申请一块4字节的大小,返回这块内存的首地址。
    
    printf("p1 = %p.n",p1);
    printf("p2 = %p.n",p2);
    /*
    //需要一个1000个int类型的元素的数组
    
    //第一步:申请和绑定
    int *p = (int *)malloc(1000 * sizeof(int));
    
    //第二步使用返回值校验分配是否成功
    if(NULL == p)
    {
        printf("malloc error.n");
        return -1;
    }
    //第三步使用申请到的内存
    *(p+0) = 1;
    *(p+1) = 2;
    printf("*(p+0) = %d.n",*(p+0));
    printf("*(p+1) = %d.n",*(p+1));
    //p=NULL;
    //p = &a;  //如果在free之前给p另外赋值,那么malloc申请的那段内存就丢失掉了
            //malloc后p和返回的内存相绑定,p是那段内存在当前进程的唯一联系人,
            //如果p在free之前就丢了,那么这段内存就永远丢失了,丢了的概念就是在
            //操作系统的堆管理器中这段内存是当前进程拿着的,但是你也用不了,所以你想
            //所以你想申请新的内存来替换使用,这就叫程序的  吃内存。即内存泄漏。
             //相当于你挖了一块地,在下面埋了黄金,然后忘记这块地的位置了。
    
    //第四步释放内存
    free(p);
    
    *(p+0) = 345;
    *(p+1) = 567;
    printf("*(p+0) = %d.n",*(p+0));
    printf("*(p+1) = %d.n",*(p+1));
    */
    return 0;
}

4.5.4、数据段:(程序在加载的时候,内容已经确定了。)
(1)编译器在编译程序时,将程序中的所有元素分成了一些组成部分,各部分构成一个段。所以说段是可执行程序的组成部分。

(2)代码段:代码段就是程序中的可执行部分,直观理解代码段就是函数堆叠组成的。

代码段: 代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些 架构 也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

(3)数据段(也被称为数据区,静态数据区,静态区):数据段就是程序中的数据,直观理解就是C语言中全局变量(注意局部变量不算是程序的数据,只能算是函数的数据)。

(4)bss段(又叫ZI(zero  initial)段),bss段的特点就是被初始化为0,bss段本质上也是属于数据段,bss段就是被初始化为0的数据段。
注意区分:数据段(data)和bss段的区别和联系:二者本来没有本质区别,都是用来存放C程序中的全局变量的。区别在于把显示初始化为非0的全局变量存在data段中,而把显式初始化为0或者并未显式初始化(C语言规定未显式初始化的全局变量默认为0)的全局变量存在bss段。

(5)特殊的数据放在代码段:C语言中使用char  *p =“Linux”;定义字符串时,字符串“linux”实际被分配在代码段。这个“Linux”字符串实际上是一个 常量字符串 而不是变量字符串。

(6)const的实现方法有两种:1:、const修饰的放在代码段(常见的单片机的编译器)2:const修饰的放在数据段(gcc就是这样实现的。)

(7)显式初始化为非0的全局变量和静态局部变量放在数据段。。。未初始化或显式初始化为0的全局变量放在bss段。
总结:C语言中所有变量和常量所使用的内存无非以上三种情况。

(1)不同点:栈内存对应C中的普通局部变量(栈是自动的,程序员无法手工控制),堆内存完全是独立于我们的程序存在和管理的,程序需要内存时,手工申请malloc和释放free,数据段在C程序中对应着全局变量和static静态局部变量。

(2)不同的存储方式有不同的特点:
    堆内存和数据段几乎拥有完全相同的属性,但是二者的生命周期不同。如果你这个变量只在程序的某一阶段有用,用完就不用了,适合堆内存,如果这个变量是要和程序共生共灭的则应该用全局变量,实际中,堆内存的使用比全局变量广泛。

#include <stdio.h>
#include <stdlib.h>//使用malloc的头文件。
#include <string.h>
char str[] = "linux";    //第二种方法:定义成全局变量,放在数据段中
int main(void)
{
    /*
    char a[] = "linux";   //第一种方法:定义成局部变量,放在栈上。
    
    char *p = (char *)malloc(10);  //申请10个char类型大小的内存。
    //char *p = (char *)malloc(10);
    
    if(NULL == p)      //申请完一定要判断一下。
    {
        printf("malloc error.n");
        return -1;
    }
    memset(p,0,10);     //memset内存空间初始化函数,第三种方法:放在malloc申请的堆内存中。
    strcpy(p,"linux");
    
    
    printf("%sn",a);
    printf("%sn",str);
    printf("%sn",p);
    printf("%pn",a);
    printf("%pn",str);
    printf("%pn",p);
    */
    
    char *p = "linux";      //分配在代码段的。特殊数据。就相当于const char *p = "linux";
    //*(p+0) = 'f';  //flinux  (这句执行后会有段错误)
    
    printf("p = %s.n",p);
    
    return 0;
}


4.5.5、C语言的字符串和字符数组类型

附加:
 #include <stdio.h>
int size(char b[100])    //数组做函数参数时退化成指针
{
    return sizeof(b);    
}
int main(void)
{
    char a[] = {'C','h','i','n','a',''};
    printf("size(a)=%dn", size(a));        //打印的是4
    return 0;
}

数组退化成指针为“常量指针”,不可修改,而直接指向字符串的指针可修改如:
        
        char a[10] = "China" ;
        char *p = "China";
        a++;        //错误,(GCC下,a+1可以,但不能a = a+1,可能就是因为常量的原因。)
        p++;        //正确


(1)char  *p = "linux";p是一个字符指针(本质上就是一个指针变量,p指向了一个字符串的起始地址。

(2)C语言的字符串有3个核心要点:第一是用一个指针指向字符串的头,第二是固定尾部(字符串总是以''来结尾的);第三是组成字符串的各字符彼此地址相连。

(3)'n'是一个ASCII字符,其实就是编码为0的那个字符(这是 真正的0 ,和数字0是不同的,数字0有它自己的ASCII编码),注意区分''和'0'和0,0等于'','0'等于48,。

注意:指向字符串的指针和字符串本身是分开的两个东西。Char *p = "linux";在这段代码中,p本质上是一个字符指针,占4字节:“linux”分配在代码段,占6个字节,实际上共耗费了10个字节,这10个字节中:4字节的指针p叫做字符串指针(用来指向字符串的,理解为字符串的引子,但是它本身不是字符串),5字节用来存linux这5个字符的内存才是真正的字符串,最后一个用来存''的内存是字符串结尾标志(本质上不属于字符串)

Sizeof和strlen,,sizeof是测量内存字节的大小,strlen是测量元素的个数。

(4)存储多个字符的2种方式:字符串和字符数组的本质差异(内存分配角度)、

(一)字符数组char a [ ] = "linux";来说,定义了一个数组a,数组a占6字节,右值“linux”本身只存在于编译器中,编译器将它用来初始化字符数组a后丢弃掉(也就是说内存中是没有“linux”这个字符串的);相当于:char a [ ] = {'l', 'i', 'n', 'u', 'x','/0'};

(二)字符串char  *p = "linux";定义了一个字符指针p,p占4字节,分配在栈上;同时还定义了一个字符串“linux”,分配在代码段;然后把代码段中的字符串(一共占6字节)的首地址(也就是'l' 的地址)赋给p。

#include<stdio.h>
int main(void)
{
    int i = 0;
    char *p = "linux";   //字符串(1)  ,原生类型。!
    
    char a[] = "linux";   //字符数组(2)
    
    printf("p = %s.n",p);  //为什么用p而不是*p,这个p指向这个字符串的
    printf("a = %s.n",a);  //首地址,用%s 加首地址指针将其打印出来。
    printf("*p = %d.n",*p);
    printf("sizeof(a) = %d.n",sizeof(a));
    for(i=0;i<(sizeof(a)-1);i++)
    printf("a[i] = %p.n",&a[i]);
    return 0;

}

(3)sizeof(数组名)得到的永远是数组的字节数(也就是数组的大小),和数组中有无初始化,初始化多少等是没有关系的;strlen是用来计算字符串的长度的,只能传递合法的字符串进去才有意义,如果随便传递一个字符指针,但是这个字符指针并不是字符串是没有意义的。

总结对比:字符数组和字符串有本质的差别:字符数组本身是数组,数组自身自带内存空间;而字符串本身是指针,本身永远只占4字节,这4字节不能用来存有效数据,只能把有效数据存到别的地方,然后把地址存在p中。也就是说字符数组自己存那些字符;字符串一定需要额外的内存来存那些字符,字符串本身只存那些字符串所在内存空间的首地址。  (自己想着思考,用类似的代码来实现老师的功能,别那么被动的学习)

#include<stdio.h>
#include<string.h>
#include <stdlib.h>
/*
int mystrlen(const char *p)       //自己实现的strlen函数。
{
    int cnt =0;
    while(*p != '')
    {
        cnt++;
        p++;
    }
    return cnt;
}
*/
int mystrlen(const char* p)
{
    int cnt = 0;
    
    while(*p != '')
    {
        ++p;
        ++cnt;
    }
    
    return cnt;
}
int mystrlen2(const char* p)
{
    int count = 0;
    while(*p != '')
    {
        count++;
        p++;
    }
    return count;
}
char b[5] = "qwer";
int main(void)
{
    
    //字符串存在栈上
    char a3[8] = "linux21";//结论:数组空间可以大一些,绝对不能小于要存储的元素大小,否则出错
    char *p1 = a3;   //这里赋给这串字符串的首元素的首地址。本身是不存储字符的,需要额外的内存,只需要给出首地址就可以了
    printf("p1 = %sn",p1);
    
    /*
    //字符串存在数据段
    char *p2 = b;  
    printf("p2 = %sn",p2);
    
    
    //字符串存在堆空间
    char *p3 = (char *)malloc(16);
    if(NULL == p3)
    {
        printf("malloc is errorn");
        return -1;
    }
    p3 = "qwqerqqer";
    printf("p3 = %sn",p3);
    */
    //要弄清楚字符数组和字符串,strlen和sizeof
    char *p4 = "linuxddd";
    printf("sizeof(p4) = %d.n",sizeof(p4));  // 4,指向字符数组的的指针的长度。4字节
    printf("strlen(4p) = %d.n",strlen(p4)); //8,字符数组中元素的个数。
    
    /*
    char a[4]="windows";
    printf("sizeof(a) = %d.n",sizeof(a));  //8  ''。
    printf("strlen(a) = %d.n",strlen(a));  //7
    
    
    /*
    char a[]="windows";
    printf("sizeof(a) = %d.n",sizeof(a));  //8  ''。
    printf("strlen(a) = %d.n",strlen(a));  //7
    */
    
    char a1[10] = "Windows";
    printf("sizeof(a1) = %d.n",sizeof(a1));  
    printf("strlen(a1) = %d.n",strlen(a1));
    
    
    char a[5] = {2,3};
    printf("sizeof(a) = %d.n",sizeof(a));  //5,内存地址的大小,
    printf("strlen(a) = %d.n",strlen(a));  //2,里面元素的个数
    
    
    char *p = "linux";
    int len = mystrlen(p);  //测的是这个指针所指向的代码段的字符串的个数。
    printf("len = %d.n",len);  //5
    
    
    
    
    
    return 0;
}

4.5.7、结构体的本质

数组的缺陷:1、大小固定,2、元素类型必须相同。
(结构体本质访问方法,————》 指针访问

#include<stdio.h>
/*
struct pepole
{
    char name[20];
    int age;
    
};
struct student
{
    char name[20];
    int age;
}s1;             //一个全局变量//这里是定义类型的同时,定义变量
*/
//将类型struct student重命名s1,s1是一个类型名,不是一个变量,可以用这个类型定义变量。
/*
typedef struct student
{
    char name[20];
    int age;
}s1;   //这是一个类型名
*/
struct myStudent
{
    int a;
    double b;
    char c;
    
}s2;
struct s
{
    char c;
    int b;
};
int main(void)
{
    /*
    //printf("sizeof(struct s) = %dn",sizeof(struct s));    //  8
    
    struct s s10;
    s10.c = 't';
    s10.b = 12;
    
    char *p = (char *)((int)&s10);
    printf("*p = %cn",*p);           //t ,原来是什么类型的就用什么类型的去解析,注意结构体对齐访问你的规则
    
    int *p1 = (int *)((int)&s10+1);
    printf("*p1 = %dn",*p1);     //201852036
    
    int *p2 = (int *)((int)&s10+4);  //结构体对齐  //int方式存储,char方式取,出错
    printf("*p1 = %dn",*p2);           //12
*/
    /*
    s2.a = 12;   //本质,通过指针访问:int *p = (int *)&s2;  *p = 12;
    s2.b = 4.4;     // double *p = (double *)((int)&s1 + 4); *p = 4.4
    s2.c = 'a';     //char是一个字节,char *p = (char *)((int)&s2+12); *p = 'a';
    
    printf("s2.a = %dn",s2.a);
    int *p = (int *)&s2;       //int *的作用只是为了强制类型转换成int *类型的地址,将这个地址赋给左边的。
    printf("*p = %dn",*p);
    
    printf("s2.b = %lfn",s2.b);
    double *p2 = (double *)((int)&s2+4);//将地址转换成int 类型的,原来的&s2表示结构体整个的地址,这样转换后,就变成了结构体首元素的首地址了
    printf("*p2 = %lfn",*p2);
    
    printf("s2.c = %cn",s2.c);
    char *p3 = (char *)((int)&s2+12);
    printf("*p3 = %cn",*p3);
    */
    
    
    
    printf("s2.a = %d.n",s2.a);
    int *p = (int *)&s2;   //将这个地址强制类型转换成int *类型的,对结构体变量取地址
    printf("*p = %d.n",*p);   //结构体首元素的值。
    
    printf("s2.b = %lf.n",s2.b);
    double *p2 = (double *)((int)&s2+4);  //地址加四指向下一个值,这里将地址强制类型转换为int就可以的原因:
    //  &s2本身是带类型的,如果不强制类型转换,直接加4,则加了4个结构体大小的地址。所以要转成int类型。
    printf("*p2 = %lf.n",*p2);
    
    printf("s2.c = %c.n",s2.c);
    char *p3 = (char *)((int)&s2+12);    //地址的运算。
    printf("*p3 = %c.n",*p3);
    
/*    
    struct pepole p1;  //使用结构体类型,定义变量
    s1 s4;
    s4.age = 3;
    //printf("s1.age = %dn",s2.age);
    
    s1.age = 3;
    printf("s1.age = %dn",s1.age);
*/    
    
    
    return 0;
}

4.5.8结构体的对齐访问

(1)结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率(主要是为了用户体验问题),否则会大大降低效率。(譬如char c实际占字节数可能是1,也可以是2,也可能是3,也可以能4····)

(2)内存本身是一个物理器件(DDR内存芯片,Soc上的DDR控制器),还有别的原因导致我们需要对齐访问,譬如Cache的一些缓存特点,还有其他硬件(譬如MMU、LCD显示器)的一些内存依赖特性,所以会要求内存对齐访问。内存对齐与不对齐对比:对齐提升了效率, 不对齐节省了内存空间,降低了效率。(以空间来换时间)      博客

http://www.cnblogs.com/dolphin0520/archive/2011/09/17/2179466.html

    http://blog.csdn.net/sno_guo/article/details/8042332

(3)编译器本身可以设置内存对齐的规则,有以下的规则需要记住:第一个是,32位编译器,一般编译器默认对齐方式是4字节对齐。结构体变量对齐原则:结构体变量的结束地址由下一个元素说了算,要凑齐4字节对齐,当整个结构体的所有元素都对齐存放后,还没结束,因为整个结构体大小还要是4的整数倍。当你遇到问题时可以反复测试,看其中的规律是怎么样的。

(4)1:#pragma pack(),一字节对齐,取消字节对齐。第二种:#pragma pack(n) (n=1/2/4/8)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐参数就是n。 gcc推荐 的对齐指令1:__attribute__((packed))使用时直接放在要进行内存对齐的类型定义后面,packed的作用是取消对齐访问。

2: __attribute__((aligned(n))) 使用时 直接放在要进行内存对齐的类型定义后面,它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量 整体 n字节对齐,而不是结构体内各元素也要n字节对齐)

#include<stdio.h>
//#pragma pack(128)            //对内存对齐的设置。
/*
首先是整个结构体,整个结构体变量四字节对齐是由编译器保证的,我们不用操心。
然后是第一个元素a,a的开始地址就是整个结构体的开始地址,是四字节对齐的,但是
a的结束地址要由下一个元素说了算。
然后是第二个元素b,因为上一个元素a本身占4字节,本身就是对齐的,所以留给b的开始
地址也是4字节对齐地址,所以b可以直接放(b放的位置就决定了a一共占四字节,因为不需要填充)
b的起始地址定了后,结束地址不能定(因为可能需要填充),结束地址要看下一个元素来定。
然后是第三个元素c,short类型需要2字节对齐(short类型元素必须放在类似0,2,4这样的地址
处,不能放在1,3这样的奇数地址处),因此c不能紧挨着b来存放,解决方案是在b之后添加1字节的填充
(padding),然后再开始存放c,c放完之后还没结束
当整个结构体的所有元素都对齐存放后,还没结束,因为整个结构体大小还要是4的整数倍
*/
//#pragma pack(8)
struct student
{
    char a;        //2
    short b;    //2
    int c;        //4
    float d;    //4
    double e;    //8
}__attribute__((aligned(8))) s1;    //GCC推荐的指令
//#pragma pack()
typedef struct mystru
{
    int a;
    double b;
    char c;
}__attribute__((aligned(1024))) MyStru;        //使用的时候每一个括号都应该规范,否则容易出错。
struct mystruct1
{                    //四字节对齐
    int a;         //4
    char b;         //2  因为需要内存对齐,所以分配给char2字节
    short c;        //2
};
struct mystruct11
{                    //四字节对齐
    int a;         //4
    char b;         //2  因为需要内存对齐,所以分配给char2字节
    short c;        //2
}__attribute__((packed));      //内存对齐,packed取消对齐访问;1字节对齐
typedef struct mystruct111
{                    //四字节对齐
    int a;         //4
    char b;         //2  因为需要内存对齐,所以分配给char2字节
    short c;        //2
}__attribute__((aligned(4))) myStruct111;   //设置这个结构是4字节对齐。
typedef struct mystruct2
{                //1字节对齐    4字节对齐
    char a;        // 1            4
    int b;      //  4            4          //有两个类型,struct mystruct2和MyS2来定义变量。
    short c;    //  2              4
}MyS2;
//#pragma pack(1)
typedef struct mystruct21
{                //1字节对齐    4字节对齐
    char a;        // 1            4
    int b;      //  4            4          //有两个类型,struct mystruct2和MyS2来定义变量。
    short c;    //  2              4
}__attribute__((packed));          //定义变量时加这个东西是没用的。
//#pragma pack()
typedef struct
{
    int a;
    short b;
//    static int c;
}MyS3;
typedef struct myStruct5
{
    int a;             //4
    struct mystruct1 s1; // 8
    double b;            //8
    int c;                //4
}MyS5;
struct stu
{
    char sex;        //4            1
    int length;      //4            4
    char name[10];    //12          10
};
//#pragma pack()
int main(void)
{
    printf("sizeof(struct mystruct1) = %dn",sizeof(struct mystruct1));
    printf("sizeof(struct mystruct2) = %dn",sizeof(struct mystruct2));
    printf("sizeof(struct myStruct5) = %dn",sizeof(struct myStruct5));
    printf("sizeof(struct stu) = %dn",sizeof(struct stu));
    
    printf("sizeof(struct mystruct11) = %dn",sizeof(struct mystruct11));
    printf("sizeof(struct mystruct21) = %dn",sizeof(struct mystruct21));
    printf("sizeof(struct mystruct111) = %dn",sizeof(struct mystruct111));
    printf("sizeof(struct mystru) = %dn",sizeof(struct mystru));
    printf("sizeof(ni shi shab i) = %cn");
    
    return 0;
}


4.5.9_offsetof宏与container_of宏

(1)offsetof宏的 作用 是:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实质是通过编译器来帮我们计算)。 原理 :我们虚拟一个type类型结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量。

巧妙之处在于将地址0强制转换为type类型的指针,从而定位到member在结构体中偏移位置。编译器认为0是一个有效的地址,从而认为0是type指针的起始地址

#define offsetof(TYPE,MEMBER) ((int) &((TYPE *)0)->MEMBER)
学习思路:第一步先学会使用offsetof宏,第二步再去理解这个宏的实现原理。解析一:(TYPE *)0,这是一个强制类型转换,把0地址强制类型转换成一个TYPE*类型的指针,这个指针指向TYPE类型的结构体变量。(实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错)。二:((TYPE *)0)->MEMBER  (TYPE *)0是一个TYPE类型结构体变量的指针,这个0地址指向一个TYPE类型变量,通过指针来访问这个结构体变量的member元素。三: &((TYPE *)0) ->MEMBER等效于 &(((TYPE *)0) ->MEMBER),意义就是得到这个MEMBER元素的地址,但是因为整个结构体变量的首地址是0,所以这里的地址就是偏移量的地址了。这个 宏返回的是member元素相对于整个结构体变量的首地址的偏移量,类型是int。

(2)、container_of宏:
这个宏返回的就是指向整个结构体变量的指针,类型是(type *)。typeof关键字的作用:typeof(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。宏的用法://ptr是指向结构体元素member的指针,type是结构体类型,member是结构体中一个元素的元素名。 #define container_of(ptr,type,member)({
 const typeof(((type *)0)->member) *__mptr = (ptr);
 (type *)((char *)__mptr - offsetof(type,member));})

作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。
(2)工作原理:先用typeof得到member元素的类型,定义成一个指针,然后用这个指针减去该元素相对于整个结构体的偏移量(offsetof宏),减去之后得到的就是整个 结构体变量的首地址 了,再把这个地址强制类型转换为type *即可。

#include <stdio.h>
struct mystruct
{
    double a;
    int b;
    short c;
};
#define offsetof(TYPE,MEMBER) ((int) &((TYPE *)0)->MEMBER)
//ptr是指向结构体元素member的指针,type是结构体类型,member是结构体中一个元素的元素名
//这个宏返回的就是指向整个结构体变量的指针,类型是(type *)
#define container_of(ptr,type,member)({
const typeof(((type *)0)->member) *__mptr = (ptr);(type *)((char *)__mptr - offsetof(type, member));})
//定义一个typeof((type *)0)类型的指针,这个指针指向该元素的地址,减去offsetof测出来的元素相对于结构体的偏移量,就是这个结构体的首地址了。
int main(void)
{
    struct mystruct s1;
    struct mystruct *pS = NULL;
    
    short *p = &(s1.c);  //p就是指向结构体中某个member的指针,只在定义变量的时候*p表示指针
    
    printf("s1的指针等于:%pn",&s1);
    
    //问题是通过p来计算得到s1的指针
    pS = container_of(p,struct mystruct,c);
    printf("pS = %pn",pS);
    
    
    struct mystruct mystr1;
    mystr1.a = 'a';
    mystr1.b = 4;
    char *b = (char *)((int)&mystr1);   //(char *)这是一个强制类型转换。
    int *c = (int *)((int)&mystr1+4);
    
    printf("*b = %cn",*b);
    printf("*c = %dn",*c);
    
    int offsetofa = offsetof(struct mystruct,a);
    printf("offsetofa = %d.n",offsetofa);
    int offsetofb = offsetof(struct mystruct,b);
    printf("offsetofb = %d.n",offsetofb);
    int offsetofc = offsetof(struct mystruct,c);
    printf("offsetofc = %d.n",offsetofc);
    
    printf("整个结构体变量的首地址:%p.n", &mystr1);        //0xbf986410.
    printf("mystr1.b的首地址:%p.n",&(mystr1.b));
    printf("偏移量为:%d.n",(char *)&(mystr1.b)-(char *)&(mystr1));    
    return 0;
}


4.5.11.4、学习指南和要求:
(1)最基本要求是:必须要会这两个宏的使用。就是说能知道这两个宏接收什么参数,返回什么值,会用这两个宏来写代码。看见代码中别人用这两个宏能理解什么意思。

(2)升级要求:能理解这两个宏的工作原理,能表述出来。(有些面试笔试题会这么要求)

(3)更高级要求:能自己写出这两个宏(不要着急,慢慢来)

4.5.10、共用体union。

(1)共用体和结构体的不同:结构体类似一个包裹,结构体中的成员彼此是独立存在的,分布在内存的不同单元中,它们只是被打包成一个整体叫做结构体而已。
共用体中各个成员其实是一体的,彼此不独立,它们使用同一个内存单元。可以理解为:有时候是这个元素,有时候是那个元素,更准确的说法是同一个内存空间有多种解释方式。(1:节省内存,2:用来查看大小端模式)

(2)union的sizeof测到的大小实际就是union中各个元素里面占用内存最大的那个元素的大小。因为union实际只有一个内存空间,所以不涉及内存对齐

(3)不同点是本质上的不同。struct是多个独立元素(内存空间)打包在一起;union是一个元素(内存空间)的多种不同解析方式。

(4)共用体用在那种对同一个内存单元进行多种不同规则解析的这种情况下。
用指针和强制类型转换可以替代结构体完成同样的功能,但是共用体的方式更简单、更便捷、更好理解。

#include<stdio.h>
struct mystruct
{
    int a;
    int b;
    char c;
    double d;
};
//a和b其实是指向同一块内存空间,只是对这块内存的2种不同的解析方式,
//如果我们使用u1.a那么就按照int类型来解析这个内存空间:如果我们用了char,
//就按照char类型来解析这个内存空间。
typedef union xx                  //这是一块内存空间。用最大的int类型存储,
{
    int a;
    int b;
    char c;
    double d;
}xxx;//  定义了一个共用体类型。
union myunion
{
    int a;
    int b;
    char c;
};
union test
{
    int a;
    float b;
};
int main(void)
{
    union test t1;
    t1.a = 1149249405;         //学会反过来想,由结果去找原因
    printf("value = %f.n",t1.b);  //以int型的存,以float取,乱码。
    
    int b = 1149249405;
    printf("指针方式访问b = %fn",*((float *)&b));//int型的数据以指针的方式来访问。
    //int *p  = &a;  //表示这个指针是int *类型的,指针所指向的数是int类型的。
    /*
    struct mystruct s1;
    s1.a = 23;
    printf("s1.b = %d.n",s1.b); //值是随机的。s1.b = 134513787
    printf("&s1.a = %p.n",&s1.a);
    printf("&s1.b = %p.n",&s1.b);
    */
    
    union myunion u1;
    u1.a = 9527;                 //共用体变量的定义
    printf("u1.b = %d.n",u1.b);// 共用体元素的使用,u1.b = 9527,
    printf("&u1.a = %p.n",&u1.a);
    printf("&u1.c = %p.n",&u1.c);  //a和b指向同一块内存,只是对这块内存的不同解析。
    
    printf("sizeof(xxx) = %dn",sizeof(xxx));
    printf("sizeof(union xx) = %dn",sizeof(union xx));
    return 0;
}

4.5.11大小端模式
(1)高字节(数据的高位)对应(内存的)低地址(大端模式)、高字节对应高地址(小端模式);使用共用体测试。bit的排列方式不同。


数字0x12 34 56 78在内存中的表示形式为:
1)大端模式:
低地址 -----------------> 高地址
0x12  |  0x34  |  0x56  |  0x78
2)小端模式:(低地址对应低字节)
低地址 ------------------> 高地址
0x78  |  0x56  |  0x34  |  0x12

可见,大端模式和字符串的存储模式类似。
(2)不能测试大小端的方式:位与,移位,强制类型转换:(是地址层面的运算。)

位与的方式无法测试机器的大小端模式。(表现是大端机器和小端机器的&运算后的值相同)理论分析:位与运算是编译器提供的运算,这个运算是高于内存层次的(或者说&运算在二进制层次具有可移植性,也就是说&的时候一定是高字节与高字节,低字节与低字节,和二进制存储无关)。

移位:不能测试,因为C语言对运算符的级别是高于二进制层次的,右移运算永远是将低字节移除,而和二进制存储时这个低字节在高位还是低位无关的。
大小端模式只是存储二进制的序列不一样,C语言编译器在进行一些符号运算时会替我们优化,不论大端小端存储方式,运算时的数字经过包装,高字节与低字节不变。

/*
    创建一个union,格式类似struct,里面,int,char两个类型,然后给int赋值1,用char来解析,看返回的是0还是1,查看大小端模式。
*/
#include <stdio.h>
union myunion
{
    
    int a;
    char b;
};
//如果是小端模式则返回0,大端模式则返回1(高地址对应低字节)
int is_little_endian(void)
{
    union myunion u1;        //union的存放顺序是所有成员都从低地址开始存放,
    u1.a = 1;             //
                             //高字节->低字节
    return u1.b;            //00 00 00 01
                           //低地址 ->  高地址
}
int is_little_endian2(void)
{
    int a = 1;
    char b = *((char *)&a);//指针方式访问就是共用体的本质。
    
    return b;
}
int main(void)
{
    int i = is_little_endian();
    if(1 == i)
    {
        printf("大端模式n");
    }else
    {
        printf("小端模式n");
    }
    
    
    return 0;
}


(3)通信系统中的大小端(数组的大小端)

(1)譬如要通过串口发送一个0x12345678给接收方,但是因为串口本身限制,只能以字节为单位来发送,所以需要发4次;接收方分4次接收,内容分别是:0x12、0x34、0x56、0x78.接收方接收到这4个字节之后需要去重组得到0x12345678(而不是得到0x78563412)。(1byte = 8 bit)

4.5.15.枚举

4.5.15.1、枚举是用来干嘛的?
(1)枚举在C语言中其实是一些符号常量集。直白点说:枚举定义了一些符号,这些符号的本质就是int类型的常量,每个符号和一个常量绑定。这个符号就表示一个自定义的一个识别码,编译器对枚举的认知就是符号常量所绑定的那个int类型的数字。

(2)枚举中的枚举值都是常量,怎么验证?

(3)枚举符号常量和其对应的常量数字相对来说,数字不重要,符号才重要。符号对应的数字只要彼此不相同即可,没有别的要求。所以一般情况下我们都不明确指定这个符号所对应的数字,而让编译器自动分配。(编译器自动分配的原则是:从0开始依次增加。如果用户自己定义了一个值,则从那个值开始往后依次增加)

4.5.15.2、C语言为何需要枚举
(1)C语言没有枚举是可以的。使用枚举其实就是对1、0这些数字进行符号化编码,这样的好处就是编程时可以不用看数字而直接看符号。符号的意义是显然的,一眼可以看出。而数字所代表的含义除非看文档或者注释。

(2)宏定义的目的和意义是:不用数字而用符号。从这里可以看出:宏定义和枚举有内在联系。宏定义和枚举经常用来解决类似的问题,他们俩基本相当可以互换,但是有一些细微差别。

#include<stdio.h>
//宏定义来解决返回值问题
#define FALSE 0
#define TRUE 1
//这个枚举用来表示函数返回值,ERROR表示对错,RIGHT表示对
enum return_value
{
    ERROR,           //值是常量。    如果不给他赋值,则默认的就是ERROR=0,RIGHT等于1;
    RIGHT,       //枚举值是全局的。
};
//定义方法2,定义类型的同时定义变量,变量是int类型的
/*
enum week
{
    SUN,
    MON,
    TUE,
    WEN,
    THU,
    FRI,
    SAT,
}today,yesterday;
*/
//定义方法4,用typedef定义枚举类型别名,并在后面使用别名进行变量定义。
typedef enum week
{
    SUN,          //0
    MON,
    TUE,
    WEN,
    THU,
    FRI,
    SAT,
}week;          //week代表枚举类型。
//定义方法5,用typedef定义枚举类型别名,并在后面使用别名进行变量定义
/*
typedef enum
{
    SUN,          //0     
    MON,
    TUE,
    WEN,
    THU,
    FRI,
    SAT,
}week;
*/
enum return_value func1(void);
int func3(void);
int main(void)
{
    /*
    printf("SUN = %dn",SUN);
    
    today = WEN;
    yesterday = SAT;
    printf("today = %dn",today);
    printf("yesterday = %dn",yesterday);
    */
    
    
    //测试定义方法4,5
    week today;
    today = WEN;
    printf("today = %dn",today);
    /*
    int a = func3();
    if(a == FALSE){
        printf("程序执行错误n");
    }else{
        
    }
    */
    /*
    enum return_value r = func1();
    if(r == ERROR)
    {
        printf("程序执行错误n");
    }else{
        printf("程序执行正确n");
    }
    printf("ERROR = %dn",ERROR);
    printf("RIGHT = %dn",RIGHT);
    */
    return 0;
}
//经过测试,两个struct类型内的成员名称可以重名,而两个enum类型中的成员不可以重名。实际上从两者的成员
//在访问方式上的
enum return_value func1(void)
{
    enum return_value r1;
    r1 = RIGHT;
    return r1;
}
int func3(void)
{
    return FALSE;
}

4.5.15.3、宏定义和枚举的区别

(1)枚举是将多个有关联的符号封装在一个枚举中,而宏定义是完全散的。也就是说枚举其实是多选一。

(2)什么情况下用枚举?当我们要定义的常量是一个有限集合时(譬如一星期有7天,譬如一个月有31天,譬如一年有12个月····),最适合用枚举。(其实宏定义也行,但是枚举更好)

(3)不能用枚举的情况下(定义的常量符号之间无关联,或者无限的)用宏定义。

总结:宏定义先出现,用来解决符号常量的问题;后来人们发现有时候定义的符号常量彼此之间有关联(多选一的关系),用宏定义来做虽然可以但是不贴切,于是乎发明了枚举来解决这种情况。









最后

以上就是激昂网络为你收集整理的4.5、数组&字符串&结构体&共用体&枚举4.5.1程序中的内存从哪里来1:4.5.5、C语言的字符串和字符数组类型4.5.7、结构体的本质4.5.8结构体的对齐访问:4.5.9_offsetof宏与container_of宏:4.5.10、共用体union。4.5.15.枚举的全部内容,希望文章能够帮你解决4.5、数组&字符串&结构体&共用体&枚举4.5.1程序中的内存从哪里来1:4.5.5、C语言的字符串和字符数组类型4.5.7、结构体的本质4.5.8结构体的对齐访问:4.5.9_offsetof宏与container_of宏:4.5.10、共用体union。4.5.15.枚举所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(51)

评论列表共有 0 条评论

立即
投稿
返回
顶部