宏定义为什么定义一个数据还有偏移

作者&投稿:秦购 (若有异议请与网页底部的电邮联系)
用宏定义来定义一个一年有多少秒~

宏定义
宏定义是C提供的三种预处理功能的其中一种,这三种预处理包括:宏定义、文件包含、条件编译
编辑本段1.不带参数的宏定义:
宏定义又称为宏代换、宏替换,简称“宏”。
格式:
#define 标识符 字符串
其中的标识符就是所谓的符号常量,也称为“宏名”。
预处理(预编译)工作也叫做宏展开:将宏名替换为字符串。
掌握"宏"概念的关键是“换”。一切以换为前提、做任何事情之前先要换,准确理解之前就要“换”。
即在对相关命令或语句的含义和功能作具体分析之前就要换:
例:
#define PI 3.1415926
把程序中出现的PI全部换成3.1415926

1) #define宏定义有一个特别的长处:可以使用 #ifdef ,#ifndef等来进行逻辑判断,还可以使用#undef来取消定义。

2) typedef也有一个特别的长处:它符合范围规则,使用typedef定义的变量类型其作用范围限制在所定义的函数或者文件内(取决于此变量定义的位置),而宏定义则没有这种特性。

3)typedef比#define更安全。举个例子:
typedef char* pStr1;

  #define pStr2 char *

  pStr1 s1, s2;

  pStr2 s3, s4;

  在上述的变量定义中,s1、s2、s3都被定义为char *,而s4则定义成了char,不是我们所预期的指针变量,根本原因就在于#define只是简单的字符串替换而typedef则是为一个类型起新名字。

#define OFFSETOF(type, field) ((size_t)&(((type *)0)->field))
(type *)0:把0地址当成type类型的指针。
((type *)0)->field:对应域的变量。
&((type *)0)->field:取该变量的地址,其实就等于该域相对于0地址的偏移量。
(size_t)&(((type *)0)->field):将该地址(偏移量)转化为size_t型数据。
ANSI C标准允许任何值为0的常量被强制转换成任何一种类型的指针,并且转换结果是一个NULL指针,因此((s*)0)的结果就是一个类型为s*的NULL指针。如果利用这个NULL指针来访问s的成员当然是非法的,但&(((s*)0)->m)的意图并非想存取s字段内容,而仅仅是计算当结构体实例的首址为((s*)0)时m字段的地址。聪明的编译器根本就不生成访问m的代码,而仅仅是根据s的内存布局和结构体实例首址在编译期计算这个(常量)地址,这样就完全避免了通过NULL指针访问内存的问题。
注:
1.
有人这样表达:
#define OFFSETOF(type, field) ((size_t) \
((char *)&((type *)0)->field - (char *)(type *)0))
我认为效果是一样的,多增加的那部分就是0地址,相减后就是偏移量。
2.
为什么要增加size_t呢?
首先size_t的定义是什么呢,在文件stddef.h中可以找到答案。
typedef unsigned int size_t; /*mine is 32bit machine*/
可见就是将偏移量转化为无符整型,其实32位机器的地址就是无符号的32位整数。一般情况下,不进行size_t类型转化也是没有问题的(后面的实验可证)。我认为,只有偏移量足够大,当大于0x80000000时才有影响,因为这时候的偏移量最高位是1,机器默认为是负数了。似乎上面宏定义OFFSETOF中更能说明这个问题,因为这个宏定义是一个差值,最高位是1就肯定是负数了。使用printf("%d", &var);打印一个变量的地址就是个负数。这只是我的看法,网上基本没有什么人分析为什么添加size_t的强制类型转化。因为系统对数组长度的大小是有限制的,所以也不能实验得到数据。
插一句数组长度的问题(引述):
理论上来说没有限制,但是内核一般配置允许每个进程拥有有限的内存空间,可以用系统调用函数getrlimit(int resource, struct rlimit *rlim)
获得系统的资源限制。系统的资源限制分为软件限制和硬件限制,软件限制最大值不能超过硬件限制。数组静态获得的存储空间是分配在stack,只要知道stack的限制就知道答案了。可以使用如下代码获得:
struct rlimit resource_limit;
getrlimit(RLIMIT_STACK, &resource_limit);
printf("STACK: soft_limit - %ld hard_limit - %ld\n", resource_limit.rlim_cur, resource_limit.rlim_max);
分配大数量的数组,若是系统找不到该大小的一段连续的存储空间,系统就会产生一个SIGSEGV信号,这时调用函数int sigaltstack(const stack_t *ss, stack_t *oss)来处理这个信号。sigaltstack储存信号SIGSEGV到一个alternate stack结构ss中,内核会先于进程运行前检查这个信号。
3.
由此,到一个结构体中field所占用的字节数就很简单了。
#define FIELD_SIZE(type, field) sizeof(((type *)0)->field)
4.
其实,系统给提供了一个相同的宏定义,在文件stddef.h中:
在嵌入式系统里,不同开发商,不同架构处理器和编译器都有不同的offsetof定义形式:
/* Keil 8051 */
#define offsetof(s,m) (size_t)&(((s *)0)->m)
/* Microsoft x86 */
#define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m)
/* Motorola coldfire */
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))
/* GNU GCC 4.0.2 */
#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)
虽然定义形式不同,但功能都是返回成员在数据结构中的偏移量,都是为了提高代码的可移植性。
5.
offsetof虽然同样适用于union结构,但它不能用于计算位域(bitfield)成员在数据结构中的偏移量。
typedef struct
{
unsigned int a:3;
unsigned int b:13;
unsigned int c:16;
}foo;
使用offset(foo,a)计算a在foo中的偏移量,编译器会报错。
6.应用(引述)
offsetof与EEPROM
我们许多人可能都使用过一些非挥发性的存储器,如常见的EEPROM。我们经常使用它们在存储一些系统的配置参数和设备信息。在所有的EEPROM中,通串过口访问的占了大多数。一般来说,对串口的访问都是按字节进行的,这使得我们不可避免会设计出下面的接口去访问EEPROM的信息:
/*从EEPROM 偏移量offset处读取nBytes到RAM地址dest*/
ee_rd(uint16_t offset, uint16_t nBytes, uint8_t * dest);
然而,这种接口必须要知道偏移量offset和读取字节数nBytes。可能你会采用下面的方法解决方法解决这个问题:
定义一个数据结构和一个指向这个数据结构的指针,并初始化这个指针为EEPROM的起始地址EEPROM_BASE.
#define EEPROM_BASE 0x0000000/*配置信息的起始地址*/
typedef struct
{
int i;
float f;
char c;
} EEPROM;
EEPROM * const pEE = EEPROM_BASE
ee_rd(&(pEE->f), sizeof(pEE->f), dest);
没错,这种方法的确可以达到访问指定地址的信息。不过这种方法也存在下面的问题:
a.容易使代码维护人员人误以为在ee_rd接口内部也存在EEPROM的数据结构。
b.当你编写一些自己感觉良好编译器不报错的代码,比如pEE->f = 3.2,你可能意想不到灾难将要来临。
c.这个接口没有很好地体现EEPROM所隐含的硬件特性。
到这里,有人可能会想到offsetof来解决这个问题:
#define offsetof(type, f) ((size_t) \
((char *)&((type *)0)->f - (char *)(type *)0))
typedef struct
{
int i;
float f;
char c;
} EEPROM;
ee_rd(offsetof(EEPROM,f), 4, dest);
如果让编译器来计算nBytes而不是我们自己给出那就更好了。这时,一定有人会马上提到sizeof。可是怎么使用呢,我们不能用sizeof(EEPROM.f)来计算nBytes吧?!因为EEPROM是数据类型,不是对象,没有办法操作f域呀。
/*类似于offsetof的定义*/
#define SIZEOF(s,m) ((size_t) sizeof(((s *)0)->m))
ee_rd(offsetof(EEPROM, f), SIZEOF(EEPROM, f), &dest);
其实还可以精简为下面的最终形式:
#define EE_RD(M,D) ee_rd(offsetof(EEPROM,M), SIZEOF(EEPROM,M), D)
EE_RD(f, &dest);
哈哈,这样我们只用传递两个参数,不用再考虑应该从那里读取数据以及读取多少的问题。
有人会说这种简化都是建立在EEPROM_BASE为0x0000000基础之上的,可能会反问,如果配置信息不是从0地址开始的呢?
其实我们可以通过下面的方法解决。
#define EEPROM_BASE 0x00000a10
typedef struct
{
char pad[EEPROM_BASE];/*使数据结构的前EEPROM_BASE个字节填"空"*/
int i;
float f;
char c;
} EEPROM;
使用offsetof简化EEPROM的串口访问的确很妙。这里还有一个很好的例子。在嵌入式应用中,我们时常将一些I/O寄存器映射到内存地址空间进行访问。这种映射使原本复杂的寄存器访问变得象访问普通的RAM地址一样方便。PowerPC 8250访问外部的ROM控制器(ROM controller)的寄存器就是通过这种方式实现的。ROM控制器所有的寄存器被映射到从I/O寄存器空间基地址0x10000000(IO_BASE)偏移0x60000(ROMCONOffset)字节的一段内存。每个寄存器占用四个字节,并有一个数据结构与它们对应。比如控制ROM控制器工作状态的寄存器对应数据结构ROMCON_ROM_CONTROL,配置PCI总线A的寄存器对应数据结构ROMCON_CONFIG_A,下面先看看这些数据结构的定义:
#define IO_BASE 0x10000000
#define ROMCONOffset 0x60000
typedef unsigned int NW_UINT32;
typedef struct _ROMCON_CONFIG_A {
union {
struct {
UINT32 pad4:21; /* unused */
UINT32 pad3:2; /* reserved */
UINT32 pad2:5; /* unused */
UINT32 EnablePCIA:1;
UINT32 pad1:1; /* reserved */
UINT32 EnableBoot:1;
UINT32 EnableCpu:1; /*bit to enable cpu*/
} nlstruct;
struct {
UINT32 ConfigA;
} nlstruct4;
} nlunion;
} ROMCON_CONFIG_A, *PROMCON_CONFIG_A;
typedef struct _ROMCON_ROM_CONTROL {
union {
struct {
UINT32 TransferComplete:1;
UINT32 pad3:1; /* unused */
UINT32 BondPad3To2:2;
UINT32 Advance:3;
UINT32 VersaPortDisable:1;
UINT32 pad2:1; /* unused */
UINT32 FastClks:1;
UINT32 pad1:7; /* unused */
UINT32 CsToFinClks:2;
UINT32 OeToCsClks:2;
UINT32 DataToOeClks:2;
UINT32 OeToDataClks:3;
UINT32 CsToOeClks:2;
UINT32 AddrToCsClks:2;
UINT32 AleWidth:2;
} nlstruct;
struct {
UINT32 RomControl;
} nlstruct4;
} nlunion;
} ROMCON_ROM_CONTROL, *PROMCON_ROM_CONTROL;
typedef struct
{
ROMCON_CONFIG_A ConfigA;
ROMCON_CONFIG_B ConfigB;
ROMCON_ROM_CONTROL RomControl;
...
}ROMCON, *PROMCON;
---------------------------- <-IO_BASE:0x10000000
| | | | | | |...
----------------------------
| | | | | | |...
...
---------------------------- <-ROMCONOffset(ROMCON):0x60000
| | | | | | |...
---------------------------- <-ROMCON_ROM_CONTROL
...
----------------------------
那么如何访问ROMCON_ROM_CONTROL对应寄存器呢,比如ROMCON_ROM_CONTROL对应寄存器的VersaPortDisable位?
估计有人可能会这样做:
事先定义成员RomControl(ROMCON中用ROMCON_ROM_CONTROL定义的实例)相对于ROMCON的偏移量,
#define ROMCONRomControlOffset 0x8
然后设计访问ROM的接口如下:
/*读取ROM控制器位于src位置的寄存器数据到dest*/
typedef unsigned long dword_t;
void rom_read(dword_t* src, uint32_t* dest);
void rom_write(dword_t* src, uint32_t* dest);
最后利用这个偏移量做下面的操作:
ROMCON_ROM_CONTROL tRomCtrl={0};
dword_t* pReg=(dword_t*)(IO_BASE+ROMCONOffset+ROMCONRomControlOffset);
rom_read(pReg,(uint32_t)*(&tRomCtrl));
/*查看寄存器的VersaPortDisable位,如果该位没有启用就启用它*/
if(!tRomCtrl.nlunion.nlstruct.VersaPortDisable)
{
tRomCtrl.nlunion.nlstruct.VersaPortDisable = 1;
rom_write(pReg,(uint32_t)*(&tRomCtrl));
}
这样做确实可以达到访问相应寄存器的目的。但是,如果和ROM相关的寄存器很多,那么定义、记忆和管理那么多偏移量不是很不方便吗?到这里,如果你对前面关于offsetof还有印象的话,我想你可能会作下面的优化:
#define ROMCON_ADDR(m) (((size_t)IO_BASE+\
(size_t)ROMCONOffset+\
(size_t)offsetof(ROMCON,m))
ROMCON_ROM_CONTROL tRomCtrl={0};
dword_t* pReg=(dword_t*)ROMCON_ADDR(ConfigA);
rom_read(pReg,(uint32_t)*(&tRomCtrl));
/*查看寄存器的VersaPortDisable位,如果没有启动就启动它*/
if(!tRomCtrl.nlunion.nlstruct.VersaPortDisable)
{
tRomCtrl.nlunion.nlstruct.VersaPortDisable = 1;
rom_write(pReg,(uint32_t)*(&tRomCtrl));
}

C++ 中define(宏定义) 各种用法(含特殊),小举个例子,谢谢!
答:1、define是宏定义,程序在预处理阶段将用define定义的内容进行了替换。因此在程序运行时,常量表中并没有用define定义的常量,系统不为它分配内存。而const定义的常量,在程序运行时,存在常量表中,且系统为它分配内存。2、define定义的常量,预处理时只是直接进行了替换,因此在编译时不能进行数据类型...

C语言的宏定义是什么意思啊?
答:简单地说就是,如果你定义了一个#define a abc的话,那么程序在编译的时候,你程序里所有单个的a都将被替换为abc。说到底,“宏”就是“置换”。

...宏定义不是可以实现和函数相同功能 那为什么还要分函数和宏定义两种...
答:普通函数 :MAX(a,b) { return a>b?a:b;} (1)函数式宏定义的参数没有类型,预处理器只负责做形式上的替换,而不做参数类型检查,所以传参时要格外小心。(2)调用真正函数的代码和调用函数式宏定义的代码编译生成的指令不同。如果MAX是个普通函数,那么它的函数体return a > b ? a : b...

C语言中的宏定义有什么用?
答:比如我要写一个计算圆相关信息(周长、面积等等)的程序,需要定义π(3.14):define PI 3.14 那么势必PI这个标识可以在程序中代替所有的3.14,如果我发现计算的精度不够,需要π的更多有效位时,则只需将宏定义修改为:define PI 3.1415926 那么程序中的所有PI都会变成3.1415926 可想而知,没有...

C语言宏定义为何不用分号结束?如果一行写不下怎么办?
答:1.C语言宏定义为何不用分号结束?为什么要做这样的规定?答:因为#define语句属于编译预处理,它不属于c语言语句。c语言规定每一条语句后面要加上分号,但编译预处理不是c语言语句,所以不必遵守此规定。2. 一行多长?80字节?255字节?如果仍然不够怎么办?答:一行的长度与具体的操作系统和编译器的...

C语言,宏名有类型,其参数也有类型?对嘛?判断
答:C语言中,宏名没有类型,其参数也没有类型。宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时代入指定的字符串即可。宏定义时,字符串可以是任意类型的数据。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换。

C语言中,宏替换与定义全局变量的区别是什么?
答:而变量在运行时要为其分配内存。3 宏定义不可以被赋值,即其值一旦定义不可修改,而变量在运行过程中可以被修改。4 宏定义只有在定义所在文件,或引用所在文件的其它文件中使用。 而全局变量可以在工程所有文件中使用,只要再使用前加一个声明就可以了。换句话说,宏定义不支持extern。

使用宏定义时,对宏定义不正确概念的描述
答:宏替换时先求出实参表达式的值,然后代入形参数运算求值。宏其实就是替换,这种替换不是在运行时进行,而是自编译时就完成的。替换时仅仅将表达式展开.不进行运算求值。宏定义是高级语言编译器提供的常用语法,其目的是利用某一标识符标识某个文本字符串。在编写程序时,如果程序中反复地使用某个数据或某...

一个C语言的宏定义语句
答:这个宏定义的主要功能是 在0x0000开头的内存中写入DATATYPE数据。例如:WRITE(char,128);宏展开之后是:do{*(char*)(save_offset)=128;save_offset +=sizeof(char);}while(0)执行过后 在内存地址为0x0000处写入数据128占据sizeof(char)个字节,并在0x0000 + sizeof(char)处等待下一个数据的写入...

单片机中宏定义与重新定义数据类型(typedef)区别,并且各自的优势(初 ...
答:1) #define宏定义有一个特别的长处:可以使用 #ifdef ,#ifndef等来进行逻辑判断,还可以使用#undef来取消定义。2) typedef也有一个特别的长处:它符合范围规则,使用typedef定义的变量类型其作用范围限制在所定义的函数或者文件内(取决于此变量定义的位置),而宏定义则没有这种特性。3)typedef比#define...