C易忘知识点
宏定义
## 类似于拼接,如 12##34 = 1234,并且如果34是一个宏,##也会阻止其展开并拼接;
# 将后边的内容转换成字符串
宏定义可以嵌套
宏定义中可以使用三目运算符,因为被认为是表达式,最终返回的是一个结果。其实最重要就是记住宏的用法是直接展开,然后看符不符合语法,所以需要注意小括号。
- 宏定义是没有顺序的
#define M N+1
#define N 1
printf(" %d\n", M);
宏替换时,先将所有的宏定义收集起来,然后处理源代码,发现一个宏定义符就立即替换
- 宏定义是有顺序的
#include <stdio.h>
#define foo 10
#define __str(x) "my_"#x//将二者拼接成整体:my_##x
#define str(x) __str(x)
//第二步实参才会展开
//foo 遇到 # / ## 首先进行字符串化而不是展开宏
__str(foo) -1-> my_#foo -tips-> my_foo
//foo 没有遇到 # / ##,则进行宏展开
str(foo) -1-> __str(foo) -2-> __str(10) -3-> my_#10 --> my_10
如果我们希望支持参数本身是宏的场景下展开参数宏,然后再进行字符串化的话,我们需要定义两层宏。
展开顺序大致可以归结为:
第一步:首先用实参代替形参,将实参代入宏文本中
第二步:如果实参也是宏,则展开实参
第三步:最后继续处理宏替换后的宏文本,如果仍包含宏,则继续展开
tips:如果在第二步,实参代入宏文本后,实参之前或之后遇到 # 或 ##,实参不再展开
'##'还可以用来去除,前边的参数,例如:
#define Log(...) print(_FUNC_, _LINE_, ##args)
//__VA_ARGS__, args 是C99的宏,允许替换可变参数...
//这句代码中即使调用LOG()没传入前边的参数,也不会出问题,但是去掉##后则不行
函数名字其实是地址
#include <stdio.h>
void print_my(void){
printf("print\n");
return;
}
int main()
{
//void (*p)(void)函数类型的基本格式
((void (*)(void))0x00401340)();//第一种玩法
void (*p1)(void)=print_my;//第二种玩法
p1();
void (*p2)(void)=&print_my;//第三种玩法
(*p2)();
typedef void (*PF)(void);//第四种玩法
PF pf=print_my;
pf();
PF pf2=&print_my;//第五种玩法,所以对于函数名字加不加&都是地址
pf2();
return 0;
}
格式转换
int 左移 不溢出时符号位不变,其余位左移,右边补0;溢出时最左边的几位被丢弃。
右移 符号位向右移动后,正数的话补0,负数补1,补符号位。
unsigned int 左移 左移 最左边的几位被丢弃,右边多出来的几个空位则由0补齐。
右移 补0
32位x86编译器,等号两边类型不一致时,右边先强转再运算!例: a = (int)b + (int)c
int char 互相强制转换,只要不溢出都不用考虑很多,考虑溢出要看二进制(补码形式)。
uchar->int 正数,数值不变
char->int 数值不变,符号不变
int->char 不溢出则数值、符号不变,溢出则高位截断
int->uchar 正数不溢出则数值不变,溢出则高位截断;负数永远高位截断。
为什么printf(%f)输出int型会是0?
首先明确是跟编译器和硬件有关的。原则上 整数存储方式基本上 c语言通用 大序或者小序存储,只跟操作系统和处理器有关。 而浮点数没有严格规定,通常用 IEEE 754 ,我们就用IEEE 754格式来看看。
整数a = 1 二进制存储是 0000 0000 0000 0000 0000 0000 0000 0001
如果我们强行把他当成浮点数,(浮点数的指数位最高位必须是1,所以转换成2进制时要加127,转10进制则要减127),那么数值变为 2^(-126) * 2^(-22) = 2.8 * 10^(-45)
然后这个还是不对,原因在于,printf只接受double类型,所以应该是对 int 看成 double来输出,因为 int 字节数 和 double 字节数不匹配,具体如何实现不太确定,不过一点可以肯定的是,这个数值非常的小。
为简单起见,我们来看如果字节数和 double 字节数相同的整型数据: 整数1,会输出4.9* 10^(-324), 如果你用 "%f"的格式输出,当然结果就是 0 了。强转则会帮助你转换成正确的格式。
三目运算符
三目运算符可以用在宏定义中,这样就可以在宏中做判断,并且这个宏还能作为参数,至少看起来很好看。eg.
#define ICM_REG(reg, id)\
(\
id==ICM40607_CHIP_ID ? (ICM40607_##reg) : (ICM42670_##reg)\
)
linux中 read()、write()函数都可以在调用完之后直接打印errno,以此判断问题。
open函数的参数:O_RDOLY=0,O_WROLY=1,ORDWR=2。所以不能或运算赋值。
linux中可以用 查看函数是被谁调用的。
#include <execinfo.h>
int backtrace(void buffer, int size);
位域
所谓"位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。本质上是一种结构类型, 不过其成员是按二进位分配的。每个域有一个域名,在程序中可以按域名访问对应的二进制区域。位域可以把几个不同的对象用一个字节的二进制位域来表示,一般是用来作寄存器位配置。
1.如果一个字节所剩空间不够存放另一位域时,应从下一字节起存放该位域。也可以有意使某位域从下一单元开始
2.位域的长度不能大于数据类型本身的长度,比如int类型的位域不能能超过32位二进制位。
3.位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。
1》成员为同类型,且成员的长度之和小于等于类型的长度
2》成员为同类型,且成员的长度之和大于于类型的长度
3》成员为不同类型 ----长度由编译器决定,在实际使用中,一般很少出现不同类型的位域
struct <位域标签名>{
成员类型 成员名称 : 该成员占用的位数;
成员类型 : 该成员占用的位数;
.......
成员类型 成员名称 : 该成员占用的位数;
};
编译优化内存分配
众所周知结构体在编译时有内存对齐的优化,会补空字节方便存取。
但是有时候传输数据的时候我们会希望数据是按照结构传输。也就是直接做为有结构的buffer来用(注意arm如果想使用结构体的某个变量时必须要内存对齐,只有在作为buffer而不用取某个变量的地址时才能不对齐)。
防止内存对齐的方法:
#pragma pack(8) //八字节对齐
struct unAlign
{
char ch;
double j;
char k;
};
#pragma pack()
------------for arm------------
typedef struct unAlign
{
char is_ok;
float virtualCorner4X;
float virtualCorner4Y;
}__attribute__((packed)) CarportInfo_t;
volatile与static
volatile, 声明这个字段易变(可能被多个线程使用),Java内存模型负责各个线程的工作区与主存区的该字段的值保持同步,即一致性。
static, 声明这个字段是静态的(可能被多个实例共享),在主存区上该类的所有实例的该字段为同一个变量,即唯一性。
<>和""
<>先去系统路径找,再去工程路径找
""先去工程路径找,再去系统路径找
指针常量、常(量)指针
int * const P = &a; //指针常量,一个不可更改的指针,指向不可以更改,但是指向的变量可以更改;
* const int P = &a; //等价的指针常量
const int * P = &a; //常量指针,指向了常量的指针,指针可以更改指向,但是指向的变量不可以更改;
int const * p = &a; //等价的常指针
int const * const p = &a; //这个反倒没什么难度,常量指针常量,全都常就行了;
const int const * p //常量常量指针,虽然常的顺序不同,其实也是都常
- 由此可见,汉语的“指针”“常量”和代码的“*”“const”顺序是一一对应的
- 代码含义(指针类型)也仅与“*”“const”二者的顺序相关,与其他关键字无关
另一方面,到底是P不可改变指向还是a不可改变内容:
- 指针常量与常(量)指针结合记忆,汉语的定语越靠后越优先,指针作为常量很明显本身是常量,常量的指针很明显指向常量,常指针会给人误解成常量性质的指针,实际是指向常量的指针
- 直接看代码,“const”如果离着P更近,那么它的修饰性更优先,所以P首先是常量;“*”离着P更近那么说明P首先是一个指针,而“const”只是修饰“*P”这个整体,即“*P”指向的a。