ARM架构与C语言(韦东山)学习笔记(7)-联合体、内存对齐问题、位域、头文件


一、结构体、联合体在内存中是如何存储的?

(1)结构体

结构体在内存中的存储方式与数组类似,是一块连续的内存空间,每个成员变量占用一定的字节大小

在结构体定义时,编译器会根据成员变量的类型和顺序来确定结构体的内存布局。通常情况下,成员变量会按照定义的顺序依次存储在内存中,且每个成员变量的起始地址都是该成员变量大小的整数倍。如果结构体中的成员变量包含了不同的数据类型,编译器可能会根据数据类型的对齐要求来进行调整,以保证结构体的访问效率和可靠性。

例如,考虑以下的结构体定义:

struct person {
    
    
    char name[20];
    int age;
    float height;
};

假设该结构体在内存中的起始地址为0x1000,且char类型占用1个字节,int类型占用4个字节,float类型占用4个字节。那么,结构体中的成员变量将按照以下顺序存储在内存中:

0x1000: name[0]
0x1001: name[1]

0x1013: name[19]
0x1014: age (低字节)
0x1015: age
0x1016: age
0x1017: age (高字节)
0x1018: height (低字节)
0x1019: height
0x101A: height
0x101B: height (高字节)
可以看到,结构体中的成员变量按照定义的顺序依次存储在内存中,并且每个成员变量的起始地址都是其大小的整数倍。这种存储方式可以使得结构体的访问效率和可靠性更高。

(2)联合体

联合体中的成员也是按照定义的顺序依次存储在同一块内存空间中。不同的是,联合体的所有成员共用同一块内存空间,因此只有其中一个成员可以被访问,其他成员的值可能会被覆盖。因此,在使用联合体时需要特别小心。

例如,考虑以下的联合体定义:

union data {
    
    
    int i;
    float f;
    char str[20];
};

假设该联合体在内存中的起始地址为0x1000,且int类型占用4个字节,float类型占用4个字节,char类型占用1个字节。那么,该联合体中的成员变量将按照以下顺序存储在内存中:

0x1000: i (低字节)
0x1001: i
0x1002: i
0x1003: i (高字节)

0x1000: f (低字节)
0x1001: f
0x1002: f
0x1003: f (高字节)

0x1000: str[0]
0x1001: str[1]

0x1013: str[19]
可以看到,联合体中的成员变量按照定义的顺序依次存储在同一块内存空间中,并且每个成员变量的起始地址都是该成员变量大小的整数倍。但是,只有其中一个成员可以被访问,其他成员的值可能会被覆盖。例如,如果先访问了联合体中的int类型成员变量,然后又访问了联合体中的float类型成员变量,那么原先存储在int类型成员变量中的值可能会被覆盖。因此,在使用联合体时需要特别小心,避免出现错误。

(3)二者在内存中的占用空间

以这段代码为例:

#include <stdio.h>

struct example_struct {
    
    //结构体
    int a;
    char b;
    double c;
};

union example_union {
    
    //联合体
    int a;
    char b;
    double c;
};

int main() {
    
    
    struct example_struct s;
    printf("Size of struct: %lu bytes\n", sizeof(s));

    union example_union u;
    printf("Size of union: %lu bytes\n", sizeof(u));
    return 0;
}

结果如下:

Size of struct: 16 bytes
Size of union: 8 bytes

可以看出,
(1)对于结构体,结构体example_struct包含一个int整型、一个char字符型和一个double双精度浮点型,它在内存中占用的空间为16个字节(4字节+4字节+8字节)。
(2)联合体example_union中最大的成员是double型,所以它在内存中占用的空间为8个字节。

也就是说,结构体的内存大小就按照内部成员变量的大小,根据内存对齐原则,相加得到
联合体的内存大小,因为所有成员占用同一块内存空间,所以其内存空间为其中最大的一个成员的内存大小

二、内存对齐机制

1.引出问题

struct example_struct {
    
    //结构体
    int a;
    char b;
    double c;
};

 struct example_struct s;
 printf("Size of struct: %lu bytes\n", sizeof(s));

根据一中的结果,结构体s的大小为16bytes,也就是说,包含一个int整型、一个char字符型和一个double双精度浮点型,它在内存中占用的空间为16个字节(4字节+4字节+8字节)。那么为什么char本身是1个字节,这里却又占用了4个字节的空间呢?
在这里插入图片描述
字符b只有第一个字节是占用了,但是后面三个字节空了出来,强制变成4个字节。

2.解释

内存对齐机制是指在分配和使用内存时,系统会按照一定的规则对内存进行分配和对齐,从而提高程序的性能和安全性。

在C语言中,每个变量都需要占用一定的字节大小,例如int类型通常占用4个字节,而double类型通常占用8个字节。为了提高内存的访问效率,系统会将变量存储在按照特定规则对齐的地址上,这样可以减少内存访问次数,提高程序的性能

内存对齐机制的规则通常是由硬件架构和操作系统决定的。在x86架构的计算机中,通常会按照4字节或8字节对齐。例如,一个int类型变量通常会被分配在4字节对齐的地址上,而一个double类型变量通常会被分配在8字节对齐的地址上。

在C语言中,可以使用#pragma pack(n)指令来改变默认的对齐方式,其中n为指定的对齐值。例如,使用#pragma pack(1)指令可以将对齐值设置为1字节,从而取消内存对齐机制。但是,取消内存对齐机制可能会影响程序的性能和可移植性,因此需要谨慎使用。

在这里插入图片描述

如果不采用内存对齐,那么不等长的数据们,可能会有一些数据被CPU读取时要截取两段来读取两次,并且还要缝合数据,极大地影响力CPU运行效率。

三、位域

(1)位域是什么

位域是C语言中的一种数据类型,它可以让程序员定义一个结构体成员或联合体成员占用指定位数的空间,而不是整个字节或字。它通常用于节省内存或者与硬件进行通信。

在C语言中,位域可以使用冒号(:)操作符来声明,语法格式如下:

struct {
    
    
    type [member_name] : width;
};

其中,type表示位域的基本数据类型,member_name表示位域的名称(可选),width表示该位域占用的位数。例如:

struct {
    
    
    unsigned int flag: 1;
    unsigned int value: 15;
};

上述代码定义了一个结构体,其中包含两个位域成员:flag和value。其中,flag占用1位,value占用15位。

在使用位域时需要注意以下几点:
位域的宽度不能超过它的基本数据类型的宽度。
对于结构体中的位域,它们的顺序和大小通常是由编译器来确定的,可以使用#pragma pack指令来控制对齐方式。
位域的行为在不同的编译器中可能会有所不同,因此需要谨慎使用。
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

(2)位域的应用

位域可以用于节省内存空间,特别是在需要存储大量bool类型或枚举类型数据时。以下是一些位域应用的示例:

1.存储布尔类型的数据

布尔类型只有两个值:true和false。使用位域可以将多个布尔类型数据压缩到一个字节中,从而节省内存空间。

struct bool_fields {
    
    
    unsigned int a: 1;
    unsigned int b: 1;
    unsigned int c: 1;
    unsigned int d: 1;
};

在这个示例中,我们定义了一个包含4个bool类型成员的结构体bool_fields。每个成员只占用1位,可以将它们压缩到一个字节中。
这个结构体的大小为4字节,但是变量a/b/c/d只占最低字节的最低4个位。

2.存储枚举类型的数据

枚举类型通常只有几个取值,使用位域可以将多个枚举类型数据压缩到一个字节中,从而节省内存空间。

enum color {
    
    RED, GREEN, BLUE};

struct color_fields {
    
    
    enum color a: 2;
    enum color b: 2;
    enum color c: 2;
    enum color d: 2;
};

在这个示例中,我们定义了一个包含4个枚举类型成员的结构体color_fields。每个成员占用2位,可以将它们压缩到一个字节中。

3.存储位图数据

位图是一种用于存储图像的数据结构,它将图像中的每个像素表示为一个二进制数。使用位域可以将位图数据压缩到较小的存储空间中,从而减少存储和传输的开销。

struct bitmap {
    
    
    unsigned int width: 10;
    unsigned int height: 10;
    unsigned char data[1024];
};

在这个示例中,我们定义了一个包含位图宽度和高度的位域结构体bitmap。宽度和高度的位数分别为10位,可以表示最大值为1023。位图数据存储在一个1024字节的字符数组中。

四、头文件

1.头文件的概念

C语言中的头文件是指包含预定义函数、变量、宏定义和类型声明等信息的文件,可以被其他C文件引用和调用。头文件通常包含在源文件中,可以使用#include指令将其包含到当前文件中。

以下是一些常用的C语言头文件:

stdio.h:定义了输入输出的函数和宏定义,如printf、scanf、puts、gets等。
stdlib.h:定义了一些通用的函数和类型,如malloc、calloc、realloc、exit、rand、srand等。
string.h:定义了一些字符串处理函数和宏定义,如strcpy、strcat、strlen、strcmp、memset、memcpy等。
math.h:定义了数学运算相关的函数和常量,如sqrt、sin、cos、exp、PI等。
time.h:定义了和时间相关的函数和类型,如time、clock、strftime、tm等。
ctype.h:定义了一些字符处理函数和宏定义,如isalpha、isdigit、isspace、tolower、toupper等。

2.extern关键字

(1)extern的作用

extern是C语言中的一个关键字,用于声明一个变量或函数是在其他文件中定义的,可以在当前文件中使用。

具体来说,当我们在一个文件中使用另一个文件定义的变量或函数时,需要使用extern关键字进行声明。例如,如果我们在main.c文件中使用了一个变量x,而这个变量是在另一个文件func.c中定义的,那么我们需要在main.c文件中使用extern关键字进行声明:

// main.c
#include <stdio.h>

extern int x; // 声明变量x是在其他文件中定义的

int main() {
    
    
    printf("%d\n", x); // 使用变量x
    return 0;
}
// func.c
int x = 10; // 定义变量x

void func() {
    
    
    // ...
}

在上面的例子中,我们在main.c文件中使用了变量x,并使用extern关键字进行了声明。这样编译器就知道变量x是在其他文件中定义的,可以在链接时将其与实际定义联系起来。

除了变量外,extern关键字也可以用于声明函数是在其他文件中定义的,例如:

// main.c
#include <stdio.h>

extern void func(); // 声明函数func是在其他文件中定义的

int main() {
    
    
    func(); // 调用函数func
    return 0;
}
// func.c
void func() {
    
    
    // ...
}

在实际编程中,extern关键字经常与头文件一起使用,以便在多个文件中共享变量和函数定义。

(2)那变量可以定义在.h文件中吗?

答:在C语言中,通常不建议将变量定义在头文件(.h文件)中,因为头文件中的内容会被多个源文件包含,如果在头文件中定义变量,那么这些源文件都会有这个变量的定义,这可能会导致编译错误或者运行时不可预料的结果

通常,头文件中只包含函数声明、宏定义、类型定义等信息,也就是说,头文件中应该只有声明,而不应该有定义。

如果需要在多个源文件中共享变量,可以在一个源文件中定义这个变量,然后在其他源文件中使用extern关键字进行声明。这样,编译器就会将所有的引用都链接到同一个变量定义上,避免了定义重复和不一致的问题。

猜你喜欢

转载自blog.csdn.net/qq_53092944/article/details/131886369
今日推荐