强制类型转换与内存对齐问题小结

本文深入探讨了C语言中的强制类型转换原理,包括基本数据类型间的转换、指针类型转换及结构体间的转换等内容,并详细解释了内存对齐的概念及其对程序效率的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C语言强制类型转换

 
  理解:不论是什么类型,在内存中存储的都是二进制,所以之间可以相互转换,而每个内存单元为一个字节,所以强制类型转换就是在分配这些字节。

概要:

C语言中,任何一个变量都必须占有一个地址,而这个地址空间内的0-1代码就是这个变量的值。不同的数据类型占有的空间大小不一,但是他们都必须有个地址,而这个地址就是硬件访问的依据,而名字只是提供给程序员的一种记住这个地址的方便一点的方法。但是,不同的变量在机器中都是0-1代码,所以,我们不能简单的通过检查一个值的位来判断它的类型。

例如,定义如下:

int a;

 float b;

double c;

 long double d;

(假设它们所占的字节分别是48810,而且连续存储于某个地址空间,起始地址是100,则我们可以得到如下内存分布)

a变量就是由以地址100开始到103结束的4个字节内存空间内的0-1代码组成。b变量则是由以地址104开始到112结束的8个字节内存空间内的0-1代码组成。而在机器中,这些内存都是连续的0-1代码,机器并不知道100~103是整型而104~111是float型,所有这些类型都是编译器告知的。当我们用a时,由于前面把a定义为int型,则编译器知道从a的地址开始向后取4个字节再把它解释成int型。那么(float)a,就是先按照int类型取出该数值,再将该数值按照int to float的规则转换成float型。所以强制类型转换就是按照某个变量的类型取出该变量的值,再按照***to***的规则进行强制转转换。如果是(类型名)常数,则是将该常数按照常数to类型 的规则进行强制转换。

指针也是一个变量,它自己占据一个4个字节的地址空间(由于程序的寻址空间是2^32次方,即4GB,所以用4个字节表示指针就已经能指向任何程序能够寻址到的空间了,所以指针的大小为4字节),他的值是另一个东西的地址,这个东西可以是普通变量,结构体,还可以是个函数等等。由于,指针的大小是4字节,所以,我们可以将指针强制转换成int型或者其他类型。同样,我们也可以将任何一个常数转换成int型再赋值给指针。所有的指针所占的空间大小都是4字节,他们只是声明的类型不同,他们的值都是地址指向某个东西,他们对于机器来说没有本质差别,他们之间可以进行强制类型转换。
指针 to 指针的强制类型转换是指将指针所指的内容的类型由原先的类型转换为后面的类型。

 

int a = 1;

int *p = &a;

float *p1 = (float*)p;

pp1的值都是&a,但是*p是将&a地址中的值按照int型变量进行解释,而*p1则是将&a地址中的值按照float型变量进行解释。


 

鉴于指针之间这种灵活的强制类型转换的需求和出于简化代码的考虑,ANSI C引入了空指针即void*void指针又名万能指针,在现在的很多程序中,当参数不确定时就用万能指针代替,这一类的指针在线程\进程函数里特别常见。

ANSI C规定,void指针可以复制给其他任意类型的指针,其他任意类型的指针也可以复制给void指针,他们之间复制不需要强制类型转换。当然任何地址也可以复制给void型指针。我们在《网络编程》中经常会看到accept(socket, (struct sockaddr *)&saddr_c, &lenth)之类的语句在&saddr_c之前需要增加代码(struct sockaddr *)是因为当此函数被设计的时候ANSI C还没有提出void*的概念。所有的地址统一用struct sockaddr类型标识,该函数的第二个参数也是指向struct sockaddr类型的指针,此处是强制类型转换。

当然,在某些编译器中不同类型的指针也可以进行直接赋值,但一般情况下会给出类型不匹配的警告。要求程序员显示的给出指针强制类型转换可以提醒程序员小心使用指针,对于明确程序目的具有一定的好处。

1、指针类型强制转换:

int m;

int *pm = &m;

char *cp = (char *)&m;

pm指向一个整型,cp指向整型数的第一个字节


2、结构体之间的强制转换

struct str1 a;

 

struct str2 b;

a=(struct str1) b;                  //this is wrong

a=*((struct str1*)&b);         //this is correct


 

3、关于一个程序的解释


 

int main(void)

{

    int a[4] = {1, 2, 3, 4};

    int *ptr1=(int *)(&a+1);

    int *ptr2=(int *)((int)a+1);

    int *c = *(a + 1);

    printf("%x, %x,%x\n", ptr1[-1], *ptr2,*c);

    return 0;

}

输出分别为4 和2000000,2

 

式子&a+1表示的是指针加法运算,而不是普通的数值加法运算


1.&a+1

   &a表示数组指针,&a+1表示指向下一个数组,强制转换为int型指针,减一即指向a[3]=4;在前面两篇文章中有详细解释。而ptr1[-1]= *(ptr1 1) 由于ptr1是指针,指向数组a后面的下一个元素,而ptr1-1就是ptr1这个指针往前移动一个单位,移动之后这个指针指向了数组a的最后一个元素。所以就有*(ptr1-1)=4


2.(int *)((int)a+1)

       指针ptr2的处理是把数组名a强制转换成整型变量,然后再加1,然后再强制转换成整型指针,即是让ptr2指向a[0]的第二个字节,此时打印的内容就是ptr2所指向的往后4个字节的内容,也就是a{0}的后三个字节和a[1]的第一个字节上面的图没有画出里面的内容。此时需要考虑大小端问题。

    

    字节序分两种,大端字节序(big-endian) ,小端字节序(little-endian)   

    1、所谓大端(big-endian)序,就是高优先位对应高有效位。就是读取或者存放数据时,最低位 对应高地址 。

    2、所谓小端(little-endian)序,就是高优先位对应低有效位 。就是读取或者存放数据时,最低 位对应低地址 。

       由于x86平台是小端序的,因此将会打印出0200 0000,如果题目中没有说明在x86平台,那答案是不确定的,取决于具体的平台,例如ARM平台就是大端序的。 


 3.*(a + 1)  

      *(a + 1)  此时的a已经是一个常指针了,这个表达式计算出a所指向元素后面的第2个元素的地址,然后对它解引用得到相应的值。这个表达式等价于int last = a[1]






结构体类型的强制类型转换


结构体和int等类型一样,都是数据类型。其他类型怎么转换,结构体就怎么转换,没有什么特殊的地方。
楼主可能想知道的不是结构体怎样强制转换这个问题吧,猜测,楼主想知道如下几个问题:
如果将一个结构体强制类型转换为另一个结构体(或者类型),那这个结构体的成员会怎样了?
如果将一个结构体强制类型转换为另一个结构体(或者类型),那么这个结构体成员的数值又会是什么了?
解答:
1、结构体的本质是:我们和C语言约定了一段内存空间的长短,及其内容的安排。假设下面两个结构体:
struct A1
{
    int a;
    char b;
};

struct A2
{
    char a;
    int b;
};
接着,用struct A1和struct A2定义变量,并赋初值:
struct A1 x = {10, 'A'};
struct A2 y = {'A', 10};
现在最重要的是,要知道x和y的内存情况:
x的内存安排是:前4B,后1B;
y的内存安排是:前1B,后4B。
如果有struct A2 z;
z.a = ((struct A2)x).a;
那么,C语言会对x的空间,按照struct A2的格局进行解释:

也就是说,将x的第一个字节看成第一个成员,且按ASCII码处理数据,而将后面的4B看成第二个成员,并按补码格式解释数据。





内存对齐小结:


在C语言面试和考试中经常会遇到内存字节对齐的问题。今天就来对字节对齐的知识进行小结一下。

首先说说为什么要对齐。为了提高效率,计算机从内存中取数据是按照一个固定长度的。以32位机为例,它每次取32个位,也就是4个字节(每字节8个位,计算机基础知识,别说不知道)。字节对齐有什么好处?以int型数据为例,如果它在内存中存放的位置按4字节对齐,也就是说1个int的数据全部落在计算机一次取数的区间内,那么只需要取一次就可以了。如图a-1。如果不对齐,很不巧,这个int数据刚好跨越了取数的边界,这样就需要取两次才能把这个int的数据全部取到,这样效率也就降低了。

                          

图:a-1                                    

  

    图:a-2

内存对齐是会浪费一些空间的。但是这种空间上得浪费却可以减少取数的时间。这是典型的一种以空间换时间的做法。空间与时间孰优孰略这个每个人都有自己的看法,但是C语言既然采取了这种以空间换时间的策略,就必然有它的道理。况且,在存储器越来越便宜的今天,这一点点的空间上的浪费就不算什么了。

需要说明的是,字节对齐不同的编译器可能会采用不同的优化策略,以下以GCC为例讲解结构体的对齐.

一、原则:

1.结构体内成员按自身按自身长度自对齐。

自身长度,如char=1,short=2,int=4,double=8,。所谓自对齐,指的是该成员的起始位置的内存地址必须是它自身长度的整数倍。如int只能以0,4,8这类的地址开始

2.结构体的总大小为结构体的有效对齐值的整数倍

结构体的有效对齐值的确定:

1)当未明确指定时,以结构体中最长的成员的长度为其有效值

2)当用#pragma pack(n)指定时,以n和结构体中最长的成员的长度中较小者为其值。

3)当用__attribute__ ((__packed__))指定长度时,强制按照此值为结构体的有效对齐值

二、例子

1。

struct AA{

    char a;

    int b;

    char c; 

}aa

结果,sizeof(aa)=12

何解?首先假设结构体内存起始地址为0,那么地址的分布如下

0  a

1  

2

3

4  b

5  b

6  b

7  b

8  c

9

10

11

char的字对齐长度为1,所以可以在任何地址开始,但是,int自对齐长度为4,必须以4的倍数地址开始。所以,尽管1-3空着,但b也只能从4开始。再加上c后,整个结构体的总长度为9,结构体的有效对齐值为其中最大的成员即int的长度4,所以,结构体的大小向上扩展到12,即9-11的地址空着。

2.

struct AA{

    char a;

char c; 

    int b;    

}aa

sizeof(aa)=8,为什么呢

0  a

1  c

2

3

4  b

5  b

6  b

7  b

因为c为char类型,字对齐长度为1,所以可以有效的利用1-3间的空格。看见了吧,变量定义的位置的不同时有可能影响结构体的大小的哦!


3.

#pragma pack(2)

struct AA{

    char a;

    int b;

    char c; 

}aa

sizeof(aa)=10,

为什么呢?a到c只占9字节长度,因为结构体的有效对齐长度在pack指定的2和int的4中取

较小的值2。故取2的倍数10。

如果当pack指定为8呢?那就仍然按4来对齐,结果仍然是12。


4.

struct AA{

    char a;

    int b;

    char c; 

}__attribute__((__8__))aa

sizeof(aa)=16,)

为咩?其实a到c仍然只占9字节长度,但结构体以8对齐,故取8的倍数16.

如果其指定2,则结果为10


如果pragma pack和__attribute__ 同时指定呢?以__attribute__ 的为准。

需要说明的是,不管pragma pack和__attribute__如何指定,结构体内部成员的自对齐仍然按照其自身的对齐值。


另外,不同的编译器可能会对内存的分布进行优化,

例如有些编译器会把立体1中的程序优化成例题2的样子。但这属于编译器的问题,

这里不做详细讨论。如果要作为编程的参考的话,最好当做编译器不会做优化,

尽量在保持代码清晰的情况下,自己手动将例题1优化成例题2的样子。

如果是做题的话,按照以上原则做就可以了,不用考虑不同编译器的特性。



/按键实验***********/ // 公司名称 :保定飞凌嵌入式技术有限公司 // 描 述 :按键控制蜂鸣器 // 版 权 :保定飞凌嵌入式技术有限公司 // 网 址 :www.witech.com.cn /***************************************************************/ /* 本实验接口说明 GPB5 ------ LED0 GPB6 ------ LED1 GPB8 ------ LED2 GPB10 ------ LED3 */ /------------------------地址声明---------------------------/ #define GPBCON (*(volatile unsigned )0x56000010) #define GPBDAT ((volatile unsigned )0x56000014) #define GPBUP ((volatile unsigned *)0x56000018) #define uchar unsigned char #define uint unsigned int /-----------------------定义全局变量------------------------/ /-----------------------函数声明----------------------------/ void Delay(int x); /------------------------------------------------------------/ 函数名称: Delay 功能描述: 延时函数 传 参: int x 返 回 值: 无 -------------------------------------------------------------/ void Delay(int x) { int k, j; while(x) { for (k=0;k<=0xff;k++) for(j=0;j<=0xff;j++); x--; } } /------------------------------------------------------------- 函数名称: ledMain 功能描述: 入口程序 初始化后,进入跑马灯死循环 传 参: 无 返 回 值: int 0 -------------------------------------------------------------/ int ledMain(void) { GPBCON = 0x1dd7fc; // GPB5,GPB6,GPB8,GPB10设置为输出 GPBDAT = ((1<<5)|(1<<6)|(1<<8)|(1<<10)); //使LED全灭 GPBDAT&=0xffe; //关闭蜂鸣器 GPBUP = 0x00; while (1) // 死循环 { GPBDAT = ~(1<<5); //LED0亮 Delay(500); GPBDAT = ~(1<<6); //LED1亮 Delay(500); GPBDAT = ~(1<<8); //LED2亮 Delay(500); GPBDAT = ~(1<<10); //LED3亮 Delay(500); } return 0; } ; init.s AREA |DATA|,CODE,READONLY ENTRY ldr r13, =0x1000 ;设置堆栈栈顶指针 IMPORT ledMain b ledMain END 预定义语句#define rGPBCON (*(volatile unsigned )0x56000010)中,关键字volatile的作用是什么?unsigned * 的作用的是什么? 第一个的作用是什么?rGPBCON代表的是什么特殊功能寄存器?其功能是什么? volatile 不稳定的,易变的,告诉编译器不要对volatile 修饰的变量或存储单元进行优化,其值随时可变,每次都直接访问存储单元。 unsigned * 的作用是强制类型转换,把0x56000010的本来的整数类型强制转换成无符号指针类型 第一个*的作用在地址0x56000010上,是指针运算符,访问地址为0x56000010的特殊功能寄存器 GPBCON是 GPIO的B端口的配置寄存器,为每一个GPIO B组的引脚定义功能(用2位定义一个引脚的功能) 00 - 输入 01-输出 10-定义具体硬件模块引脚功能 11 - 保留 把十六进制数0x1dd7fc转换成二进制,配置管脚GPB5的功能是什么?是GPBCON的哪些位段以及什么数值决定的? 语句rGPBDAT = ((1<<5)|(1<<6)|(1<<8)|(1<<10)); 执行后,GPB5、GPB6、GPB8、GPB10分别输出高电平还是低电平? 高电平, 对应的LED灯亮还是灭? 都处于熄灭状态(初始状态) 语句rGPBDAT = ~(1<<8); 执行后,GPB5、GPB6、GPB8、GPB10分别输出高电平还是低电平?对应的LED灯亮还是灭? GPB5、GPB6、GPB10输出高电平,对应的LED灯熄灭;GPB8输出低电平,对应的LED灯点亮 /按键实验***********/ // 公司名称 :保定飞凌嵌入式技术有限公司 // 描 述 :蜂鸣器 // 版 权 :保定飞凌嵌入式技术有限公司 // 网 址 :www.witech.com.cn /***************************************************************/ /* 本实验接口说明 GPB0 ------ 蜂鸣器控制口 */ void Delay(unsigned int); /------------------------地址声明-----------------------------/ #define GPBCON (*(volatile unsigned )0x56000010) #define GPBDAT ((volatile unsigned )0x56000014) #define GPBUP ((volatile unsigned *)0x56000018) /------------------------------------------------------------- 函数名称: BellMain 功能描述: 入口程序 初始化后,进入按键扫描死循环 传 参: 无 返 回 值: int 0 -------------------------------------------------------------/ int BellMain() { GPBUP &= 0XFFFFFFFE; //上拉使能GPB0 GPBCON &= 0XFFFFFFC; //GPB0设为输出 GPBCON |= 0X0000001; while(1) { GPBDAT &= 0xfe; //不能用!(非) Delay(40); GPBDAT |=0x1; //用或方式,不影响其他位 Delay(40); } return(0); } /------------------------------------------------------------/ 函数名称: Delay 功能描述: 延时函数 延时count毫秒 传 参: int count 返 回 值: 无 -------------------------------------------------------------/ void Delay(unsigned int x ) { unsigned int i,j,k; for(i=0;i<=x;i++) for(j=0;j<=0xff;j++) for(k=0;k<=0xff;k++); } AREA |DATA|,CODE,READONLY ENTRY ldr r13,=0x1000 IMPORT BellMain b BellMain END1. BellMain ()函数中的两条语句rGPBCON &= 0xFFFFFFC; rGPBCON |= 0x0000001;的具体作用是什么? //上拉电阻使能GPB0 2. GPB0端口的上拉电阻有没有使能?判断依据是什么? 3. 语句rGPBDAT &= 0xFE; rGPBDAT |= 0x1; 的作用是什么? 写出实验小结
最新发布
06-09
<think>我们首先需要理解问题中提到的代码和问题。代码主要涉及嵌入式系统中的GPIO配置,用于控制LED和蜂鸣器。问题包括:1.volatile关键字的作用2.unsigned*的作用3.第一个*的作用4.rGPBCON寄存器的功能和定义5.将十六进制数0x1dd7fc转换为二进制,并分析GPB5的配置6.执行rGPBDAT=((1<<5)|(1<<6)|(1<<8)|(1<<10));后各引脚电平状态及LED状态7.执行rGPBDAT=~(1<<8);后各引脚电平状态及LED状态此外,在另一个代码片段中,针对蜂鸣器控制的问题:1.两条配置语句的作用2.GPB0上拉电阻是否使能及判断依据3.控制蜂鸣器的两条语句的作用最后要求写实验小结。下面我将逐一解答。</think> ### 嵌入式GPIO配置控制详解 #### 1. `volatile`关键字的作用 `volatile`关键字指示编译器每次访问该变量时都必须直接从内存读取/写入,不得进行任何优化。在嵌入式系统中,这确保了对硬件寄存器的实时访问: ```c #define GPBCON (*(volatile unsigned *)0x56000010) ``` - **必要性**:GPIO寄存器的值可能被硬件(如中断)随时改变 - **避免优化**:防止编译器将寄存器访问优化为寄存器缓存 - **内存屏障**:确保指令执行顺序代码一致 #### 2. `unsigned *`的类型转换 `(volatile unsigned *)0x56000010` 实现: - **地址转换**:将整数值 `0x56000010` 转换为指向无符号整数的指针 - **硬件映射**:声明特定内存地址对应硬件寄存器 - **内存对齐**:强制32位访问(假设系统为32位架构) #### 3. 解引用操作符`*`的作用 第一个`*`是解引用操作符: ```c *(volatile unsigned *)0x56000010 ``` - **寄存器访问**:直接操作 `0x56000010` 地址对应的32位寄存器 - **完整访问**:一次性读写整个32位寄存器 - **等效操作**:等同于直接操作硬件寄存器实体 --- ### rGPBCON寄存器分析 **物理地址**:`0x56000010` **功能**:GPIO Port B 配置寄存器 **位结构**: | Bit Range | 功能 | 描述 | |----------|------|------| | [31:22] | Reserved | 保留位 | | [21:20] | GPB10 | 00=输入, 01=输出, 10=外设功能 | | [19:18] | GPB9 | 同上 | | [17:16] | GPB8 | 同上 | | [15:14] | GPB7 | 同上 | | [13:12] | GPB6 | 同上 | | [11:10] | GPB5 | 同上 | | [9:8] | GPB4 | 同上 | | [7:2] | Reserved | 保留位 | | [1:0] | GPB0 | 同上 | --- ### 十六进制值 `0x1dd7fc` 分析 **二进制转换**: ``` 0000 0000 0001 1101 1101 0111 1111 1100 0 0 1 d d 7 f c ``` **GPB5配置解析** (Bits [11:10]): ``` Position: 31........20.18.16.14.12.10.08.06.04.02.00 Value: 000000000001 11 01 11 01 01 11 11 11 11 00 GPB5 bits[11:10]: 01 → 输出模式 ``` --- ### LED控制状态分析 ```c // 初始化语句 GPBDAT = ((1<<5)|(1<<6)|(1<<8)|(1<<10)); // LED控制语句 GPBDAT = ~(1<<8); ``` **执行后状态对比**: | 引脚 | 初始状态 | 执行后状态 | LED状态变化 | |------|----------|------------|-------------| | GPB5 | 高电平 | 高电平 | 保持熄灭 | | GPB6 | 高电平 | 高电平 | 保持熄灭 | | GPB8 | 高电平 | 低电平 | 点亮 | | GPB10| 高电平 | 高电平 | 保持熄灭 | --- ### 蜂鸣器控制代码分析 ```c // 配置语句 GPBCON &= 0xFFFFFFFC; // 清除GPB0配置位 GPBCON |= 0x00000001; // 设置GPB0为输出 // 上拉电阻配置 GPBUP &= 0XFFFFFFFE; // 使能GPB0上拉 ``` **上拉电阻判断**: `GPBUP`寄存器bit0=0明确启用上拉电阻(0=使能,1=禁用) **蜂鸣器控制**: ```c GPBDAT &= 0xFE; // GPB0低电平 → 蜂鸣器响 GPBDAT |= 0x1; // GPB0高电平 → 蜂鸣器静音 ``` --- ### 实验小结 #### 1. 核心知识点总结 - **寄存器配置**:每个GPIO引脚由2位控制功能模式 - **位操作技巧**:使用`&=`清位和`|=`置位实现精准控制 - **电压逻辑**:高电平=LED熄灭/蜂鸣器关闭,低电平=点亮/启动 - **硬件抽象**:通过内存映射访问硬件寄存器 #### 2. 关键问题解决方案 | 问题 | 解决方案 | 核心原理 | |------|----------|----------| | 寄存器访问 | `volatile`关键字 | 阻止编译器优化 | | 引脚配置 | 位段操作 | 2位控制引脚模式 | | 外设控制 | 电平控制 | 低电平驱动外设 | | 上拉配置 | GPBUP寄存器 | bit=0启用上拉 | #### 3. 优化建议 1. **宏定义引脚**: ```c #define LED5 (1 << 5) #define BUZZER (1 << 0) ``` 2. **状态切换函数**: ```c void toggle_pin(int pin, int state) { if(state) GPBDAT |= pin; else GPBDAT &= ~pin; } ``` 3. **位带操作**:使用Cortex-M的位带特性实现原子操作 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值