结构体大小计算规则
结构体计算要遵循字节对齐原则。
结构体默认的字节对齐一般满足以下准则:
- 1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
- 2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
- 3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding
结构体按1字节对齐使用如下指令修饰,#pragma pack 和 __attribute__((packed))都可以,参考如下。
//#pragma pack(push, 1)
typedef struct
{
uint8_t v1;
uint8_t v2;
uint32_t v3;
}__attribute__((packed)) structDef_t;
//#pragma pack(pop)
const关键字
它限定一个变量不允许被改变,产生静态作用。在一定程度上可以提高程序的安全性和可靠性。
为什么会用const关键字:
const推出的初始目的,就是为了取代预编译指令。消除它的缺点,同时继承它的优点。
举例:
/*把一个变量定义成常量,因为常量在定义后就不能被修改。因此定义时必须初始化*/
const int buf=10; //ok
buf=20; //error
const int temp; //error 没有初始化
/*const 限定了p1指针指向的对象,但不限定指针p1本身。允许赋值给其他的const对象,但是不能通过p1改变所指向的对象*/
const char* p1; //定义了指向字符或字符串常量的指针变量,指向可变,但不可修改所指的值
char a,b;
p1=&a;
*p1=20; //error,不能通过指针改变它所指向对象a的值
p=&b; //ok,指向是可以改变的
/*注:由于没有办法分辨p1指向的对象是否为const,系统会自动把它指向的对象都视为const,所以可以指向const对象,也可以指向非const对象*/
char const *p1; //与const char *p1相同
/*const限定的是指针变量,任何试图给const指针变量赋值的行为都会导致编译出错。但可以通过该指针修改所指内存的值*/
char a=10,b=10;
char* const p1 = &a; //定义了指向字符或字符串变量的指针常量,必须在定义时初始化。
p1 = &b; //error,不能改变p1常指针的指向
*p1 = 20; //ok
使用场景
在实际使用中,常将形参定义为指向常量的指针变量,也就是 const int*p 类型,这样可以确保传递给函数的实际对象在函数中不会因为手贱而被修改
volatile 简介:
防止编译器对代码进行优化。字面意思是“易变”的,但这么说有点不好理解。“强制从原始地址操作数据”,这样比较好理解。
为什么有volatile关键字
程序运行时是存储在计算机的物理内存中的,但对内存的操作和CPU执行指令的速度相比是有很大差距的。这样就会造成交互时程序执行效率降低。因此就发明出了cache(高速缓存)。那么这种换取速度的方式肯定会存在一些问题。举个例子。
发薪水时,会计每次都会把员工叫过来登记他们的银行卡号,这样做是很麻烦。效率很低。但稳定不会出错。但有一次会计为了省事。没有即时登记。用了上次发工资时登记的银行卡号。假如刚好有个员工银行卡掉了。挂失了这张银行卡。这样,就会导致这个员工领不到工资。
举例:
int a;
a=10;
a=20;
a=30;
//我们本意是想让a先等于10,在等于20,最后再等于30.但是编译器先会为了“省事”。只执行了一条a=30.
for(int i=0; i<100000;i++);
//这个语句本意上就是用来测试空循环的速度的。但是编译器就有可能会把它优化掉。从而不执行
for(volatile int i=0; i<100000; i++);
//但是你写成这样,他就会执行了。
使用场景
多用在如下几个地方
1:中断服务函数中修改供其它程序检测的变量需要加上volatile
2:多任务环境下个任务间的共享标志也应该加上volatile,否者你读出来的flag就是先前存放在寄存器里的flag,上一个状态的flag。因为在另一个任务中修改后可能还没来的及更新。
3:存储器映射的硬件寄存器通常也要加volatile说明,我是这样理解的,假如你在程序中频繁的去访问某一个寄存器。那么编译器可能会为了“省事”,而将寄存器的值存放到一个通用的寄存器里面,也就是ARM里面的R0-R15.或者时高速缓冲的Cache中。那么当你在程序中再次去访问这个寄存器时。可能此刻这个寄存器的值已经改变。但是在cache中还没来的及去更新。这样。你就读错了。而加上volatile关键字之后。虽然会慢一些。但是可以确保你读到的值就是寄存器的当前值。
C语言中 -> 和 .
C语言中 -> 和 . 的区别可以理解为:性质不同、指向不同、访问不同
1:性质不同
"->" : 是指向结构体的成员运算符
"." : 是断点符号,不属于运算符
2:指向不同
"->" : 所指向的是结构体或者对象的首地址
"." : 所指向的是结构体或对象
3:访问不同
"-> " : 用处是使用一个指针以便访问结构体或对象其内成员
"." : 用处是使用一个指向以便于去访问结构体或对象
结构体变量用 "." 来访问结构体成员
指向结构体的指针用 -> 来访问其指向的结构体成员
按位定义结构体
按位定义结构体最直接的优势就是节省空间,同时将一系列相关功能的实现聚集到了一起。
在此基础之上联合体union,可以实现快速准确置位,清空相应 bit 位的操作。
举例
typedef union
{
struct
{
uint8_t v1:1;
uint8_t v2:1;
uint8_t v3:2;
uint8_t v4:4;
}tBit;
uint8_t Bits;
}structDef_t;
int main(void)
{
structDef_t tStruct = {0};
printf("sizeof:%d\r\n", sizeof(tStruct));
tStruct.tBit.v1 = 1;
tStruct.tBit.v3 = 3;
tStruct.tBit.v4 = 0XF;
printf("Bits:%#x\r\n", tStruct.Bits);
}
执行结果:
sizeof:1
Bits:0xfd
这里需要注意的是,按位定义是低位在前定义,高位在后定义。
数组指定元素赋值
在GNU C中,通过 数组元素索引,我们就可以直接给指定的几个元素赋值。也可以给数组中某一个索引范围的数组元素初始化。
int a[100] = {[3 ... 10]=1, [50 ... 99]=10};
int b[100] = {[10] = 1, [30] = 2};
C语言判断大小端
可以通过变量赋值截断的方式或联合体的方式判断。
int main(void)
{
// int a = 0X11223344;
// char b = a;
union u
{
int a;
char b;
}tUnion;
tUnion.a = 0X11223344;
if(tUnion.b == 0X44)
{
printf("little endian\r\n");
}else if(tUnion.b == 0X11)
{
printf("big endian\r\n");
}
return 0;
}
语句表达式
GNU C对C语言标准作了扩展,允许在一个表达式里内嵌语句,允许在表达式内部使用局部变量、for循环和goto跳转语句。这种类型的表达式,我们称为语句表达式。语句表达式的格式如下。
({表达式1;表达式2;表达式;})
举例如下:
int main(void)
{
int sum=0;
sum =
(
{
int s=0;
for(int i=0; i<10; i++)
s = s+i;
sum = s; //语句表达式的值等于最后一个表达式的值
}
);
printf("sum:%d\r\n", sum);
return 0;
}
宏定义语句表达式
语句表达式,作为GNU C对C标准的一个扩展,在linux 内核中,尤其在内核的宏定义中被大量使用。使用语句表达式定义宏,不仅可以实现复杂的功能,还可以避免宏定义带来的一些歧义和漏洞。举例如下:
/**
* @note 两数比较返回较小值
* @attention (void)(&_A1 == &_A2)是为了在两数类型不同时发出警报
*/
#define SMALLERVALUE(A1, A2) ({ \
typeof(A1) _A1 = (A1); \
typeof(A2) _A2 = (A2); \
(void)(&_A1 == &_A2); \
_A1 > _A2 ? _A2 : _A1;})
/**
* @note 两数比较返回最大值
*/
#define MAXERVALUE(A1, A2) ({ \
typeof(A1) _A1 = (A1); \
typeof(A2) _A2 = (A2); \
(void)(&_A1 == &_A2); \
_A1 > _A2 ? _A1 : _A2;})
typeof
GNU C扩展了一个关键字typeof,用来获取一个变量或表达式的类型。
int a=10;
typeof(a) b=20;
container_of
container_of了。这个宏在Linux内核中应用甚广, 它的主要作用就是,根据结构体某一成员的地址,获取这个结构体的首地址。
/**
* @note 根据结构体某一成员的地址,获取这个结构体的首地址
* @param ptr 结构体成员member的地址
* @param type 结构体类型
* @param member 结构体内的成员
*/
#define container_of(ptr, type, member) \
(type *)((char *)(ptr) - (char *) &((type *)0)->member)
offsetof
C 库宏 offsetof(type, member-designator) 会生成一个类型为 size_t 的整型常量,它是一个结构成员相对于结构开头的字节偏移量。成员是由 member-designator 给定的,结构的名称是在 type 中给定的。
零数组长度
C99标准规定:可以定义一个变长数组,数组的长度在编译时是未确定的,在程序运行的时候
才确定,甚至可以由用户指定大小。例如,我们可以定义一个数组,然后在程序运行时才指定这个数组的大小,还可以通过输入数据来初始化数组。GNU C 在此基础上,还支持零数组长度。
零长度数组有一个奇特的地方,就是它不占用内存存储空间。
零长度数组经常以变长结构体的形式,在某些特殊的应用场合使用。在一个变长结构体中,零长度数组不占用结构体的存储空间,但是我们可以通过使用结构体的成员a去访问内存,非常方便。变长结构体的使用示例如下。
typedef struct{
int len;
char a[0];
}zeroArray_t;
int main()
{
zeroArray_t zeroArray;
printf("sieof(zeroArray):%ld\r\n", sizeof(zeroArray));
zeroArray_t *pzeroArray = NULL;
pzeroArray = malloc(20 + sizeof(zeroArray_t));
pzeroArray->len = 20;
strcpy(pzeroArray->a, "hello zeroArray\r\n");
printf("\r\npzeroArray->a:%s\r\n", pzeroArray->a);
return 0;
}
__attribute__
GNU C增加了一个__attribute__关键字用来声明一个函数、变量或类型的特殊属性。_attribute__的使用非常简单,当我们定义一个函数、变量或类型时,直接在它们名字旁边添加下面的属性声明即可。
__attribute__((ATTRIBUTE))
/*需要注意的是,__attribute__后面是两对小括号*/
aligned
GNU C通过__attribute__来声明aligned和packed属性,指定一个变量或类型的对齐方式。这两个属性用来告诉编译器:在给变量分配存储空间时,要按指定的地址对齐方式给变量分配地址。
aligned有一个参数,表示要按几字节对齐,使用时要注意,地址对齐的字节数必须是2的幂次方。
int a __attribute__((aligned(8)));
注意,一个编译器,对每个基本数据类型都有默认的最大边界对齐字节数。如果超过了,则编译器只能按照它规定的最大对齐字节数来给变量分配地址。
packed
aligned属性一般用来增大变量的地址对齐,元素之间因为地址对齐会造成一定的内存空洞。而packed属性则与之相反,一般用来减少地址对齐,指定变量或类型使用最可能小的地址对齐方式。
typedef struct
{
uint8_t v1;
uint8_t v2;
uint32_t v3;
uint32_t v4;
}__attribute__((packed)) structDef_t;
format
format属性,来指定变参函数的参数格式检查。
在一些商业项目中,我们经常会实现一些自定义的打印调试函数,甚至实现一个独立的日志打印模块。这些自定义的打印函数往往是变参函数,用户在调用这些接口函数时参数往往不固定,那么编译器在编译程序时,怎么知道我们的参数格式对不对呢?如何对我们传进去的实参做格式检查呢?因为我们实现的是变参函数,参数的个数和格式都不确定,所以编译器表示压力很大,不知道该如何处理,直接举例。
void __attribute__((format(printf,1,2))) LOG(char *fmt, ...)
{
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
}
format(printf,1,2)有3个参数,第1个参数printf是告诉编译器,按照printf()函数的标准来检查;第2个参数表示在LOG()函数所有的参数列表中格式字符串的位置索引;第3个参数是告诉编译器要检查的参数的起始位置。
weak
GNU C通过weak属性声明,可以将一个强符号转换为弱符号。
void __attribute__((weak)) func(void);
int num __attribute__((weak));
used
该属性告诉编译器即使函数没有被使用,也要将其保留在目标文件中,以便链接器可以正确地找到它。
这通常用于编写一些特殊的函数,例如处理器的中断处理函数,这些函数不会直接从应有程序中调用,而是由处理器硬件在特定的条件下调用。在这种情况下,函数可能会看起来未被使用,但仍然需要包含在目标文件中以供链接器使用。
需要注意的是,如果使用 __attribute__((used)) 修饰过多的函数,可能会导致目标文件的大小增加,从而影响程序的性能,因此,应该仅在必要时使用它。
unused
该属性用于指示代码中的函数,变量或参数是有意未使用的,一般用于抑制编译器的警告,
当一个变量或函数在代码中声明但没有使用时,编译器可能会发送警告,用 __attribute__((unused)) 属性进行修饰可以告诉编译器不应该对未使用的变量或函数发出警告。
该属性修饰的函数或变量也是可以正常调用执行的。
可变参数宏
C99标准已经支持了这个特性,但是其他编译器不太给力,对C99标准的支持不是很好,只有GNU C标准支持这个功能。
可变参数宏的实现形式其实和变参函数差不多:用...表示变参列表,变参列表由不确定的参数组成,各个参数之间用逗号隔开。可变参数宏使用C99标准新增加的一个__VA_ARGS__预定义标识符来表示变参列表。
#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
宏连接符##的主要作用就是连接两个字符串,避免宏只传入了一个参数,当变参为空时,由于后面还有一个逗号,不符合语法规则而产生的语法错误。
使用预定义标识符__VA_ARGS__来定义一个变参宏,是C99标准规定的写法。而下面这种格式是GNU C扩展的一个新写法:可以不使用__VA_ARGS__,而是直接使用args...来表示一个变参列表,然后在后面的宏定义中,直接使用args代表变参列表就可以了。
#define LOG(fmt, args ...) printf(fmt, args)
指针
int *a[10]:定义一个元素个数为10的指针数组,数组元素类型为 int *;
int (*a)[10]:定义一个数组指针,指向的数组类型为 int a[10];
int *f (int):定义一个参数为 int 类型,返回值为 int * 的指针函数;
int (*f)(int):定义了一个函数指针,指向函数类型 int f(int);
int (*a[10])(int):定义一个元素个数为10的函数指针数组,指向的函数类型为 int a(int);
两个同类型的指针可以相减,但不能相加。相减的结果以数据类型的长度为单位,而非以字节为单位。指针相减一般用于同一个数组之中,用来计算两个元素之间的偏差。