嵌入式c自我修养

开始记录一些在内核里看见的奇怪语法,慢慢认清,学会他们的写法

宏定义

写一个比较大小的宏定义

差劲
#define MAX(x,y) x > y ? x : y

会因为传入的宏带有其他符号(考虑优先级)而改变我们的想法
比如 : printf(“max=%d”,MAX(1!=1,1!=2));

中等
#define MAX(x,y) (x) > (y) ? (x) : (y)

括号外的符号也会有类似的优先级
比如 : printf(“max=%d”,MAX(1!=1,1!=2));

良好
#define MAX(x,y) ((x) > (y) ? (x) : (y))

会因为传入的值产生变化
比如 : printf(“max=%d”,MAX(i++,j++));

优秀
#define MAX(x,y)({ \
 int _x = x; \
  int _y = y; \
   _x > _y ? _x : _y; \
    })

没什么大问题了,使用局部变量来存储值,但是有时候因为前面加了类型,所以可以更好

完美

传入一个type类型,可以让这个宏通用

#define MAX(type,x,y)({ \
 type _x = x; \
  type _y = y; \
   _x > _y ? _x : _y; \
    })
牛逼

1.让typeof直接获取参数里面的类型
2.(void) (&_x == &_y);
a. 编译器会因为类型不同给出警告
b.两个值比较后,结果没有用到,可以用这个消除warning

#define max(x, y) ({ \
 typeof(x) _x = (x); \
 typeof(y) _y = (y); \
 (void) (&_x == &_y);\
 _x > _y ? _x : _y; })

详细解释 (&_x == &_y)
在这里就是判断两个值是否相等,
但是如果两个值的类型不一样,那么判断的时候,使用两个值的地址能让其编译通过,但编译器会警告有问题

container_of 内核第一宏

container_of 得到一个结构体的变量,可以得出这个结构体

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 
#define container_of(ptr, type, member) ({ \ 
	const typeof( ((type *)0)->member ) *__mptr = (ptr); \
	(type *)( (char *)__mptr - offsetof(type,member) );})

1 用 offsetof 来确定这个变量在结构体里面的偏移值
原理 : 假设结构体的地址是0,那么这个成员变量的偏移值就是当前地址
2 container_of 用之前的偏移值 - 现在结构体这个成员的地址 = 当前结构体的地址
a. 开始构造了一个 __mptr 来防止变量的改变
b. 这句话返回宏出来的值

动态数组 ->0长度数组的运用

0长度数组的概念

我们使用gcc 编译器的时候,就能使用0长度数组
比如说下面这个结构体,他的sizeof只有4,因为0长度数组不占内存

struct buffer{ 
	int len;
	int a[0]; 
};

有时候可以对这个结构体 malloc的时候多增加内存,使用这个0长度数组来控制
分配内存的数值

struct buffer{ 
	int len;
	int a[0];
};
int main(void) { 
	struct buffer *buf;
	buf = (struct buffer *)malloc \
	(sizeof(struct buffer)+ 20);
	buf->len = 20; 
	strcpy(buf->a, "hello wanglitao!\n"); 
	puts(buf->a); 
	free(buf); 
	return 0;
}
使用场景

比如说一个usb的驱动,usb结构体有着usb各种各样的描述,但结构体最后要放数据包
数据包的长度由于很多种usb设备所以长度不固定,使用一个0长度数组,描述特定的usb设备再分配buffer

struct urb { 
	struct kref kref; void *hcpriv;
	atomic_t use_count; atomic_t reject;
	int unlinked; struct list_head urb_list;
	struct list_head anchor_list; struct usb_anchor *anchor;
	struct usb_device *dev; struct usb_host_endpoint *ep;
	unsigned int pipe; unsigned int stream_id; int status;
	unsigned int transfer_flags; void *transfer_buffer;
	dma_addr_t transfer_dma; struct scatterlist *sg;
	int num_mapped_sgs; int num_sgs; u32 transfer_buffer_length;
	u32 actual_length; unsigned char *setup_packet;
	dma_addr_t setup_dma; int start_frame; int number_of_packets;
	int interval; int error_count; void *context; usb_complete_t complete;
	struct usb_iso_packet_descriptor iso_frame_desc[0];
};
附加: 为啥不用指针代替数组
  1. 指针和数组也有不同的地方,数组名不占空间,表示一段连续的地址
    指针是变量,是占空间的
  2. 0长度数组更加巧妙,不会对结构体造成冗余

关键字

typeof

通过使用typeof可以知道 我们的参数类型

int i ;
typeof(i) j = 20;
typeof(int *) a;
int f();
typeof(f()) k;

attribute

attribute指导编译器在编译程序时进行特定方面的优化或代码检查
下面是attribute支持填写的属性

section
aligned
packed
format
weak
alias
noinline
always_inline
……
attribute -> 变量对齐: aligned packed

举个例子 让一个变量进行4字节对齐 有下面的一些写法

char c2 __attribute__((packed,aligned(4))); 
char c2 __attribute__((packed,aligned(4))) = 4; 
__attribute__((packed,aligned(4))) char c2 = 4;
int a __attribute__((aligned(8));
attribute-> 属性声明:section

使用atttribute 来声明一个 section 属性,主要用途是在程序编译时,将一个函数或变
量放到指定的段,即 section 中。

怎么使用setion呢

先看看代码分段(不是进程空间模型)

section 组成
代码段( .text) 函数定义、程序语句
数据段( .data) 初始化的全局变量、初始化的静态局部变量
BSS段( .bss) 未初始化的全局变量、未初始化的静态局部变量

这样就把本来在 .bss段的 未初始化变量 uniit_val 放在
数据段 .data中

int global_val = 8;
int uninit_val __attribute__((section(".data")));
int main(void) { 
	return 0;
 }

attribute-> 属性声明:format

大概就是支持 变参函数的检查
比如说下面的自定义了一个 打印函数 LOG 我们希望检查参数像printf一样检查

__attribute__(( format (archetype, string-index, first-to-check))) void LOG(const char *fmt, ...) __attribute__((format(printf,1,2)));
变参函数的设计和实现1.0

通过指针打印出每个变量

void print_num(int count, ...) 
{ 
	int *args;
	args = &count + 1;
	for( int i = 0; i < count; i++)
	 { 
	 	printf("*args: %d\n", *args);
	 	args++;
	 	} 
}
int main(void) 
{ 
	print_num(5,1,2,3,4,5);
	return 0; 
}
变参函数的设计和实现2.0

这次通过char* 来实现这个功能,就可以兼容更多的参数类型

void print_num(int count, ...) 
{ 
	char *args;
	args = (char*)&count + 4;
	for( int i = 0; i < count; i++)
	 { 
	 	printf("*args: %d\n",  *(int *)args);
	 	args +=4;
	 	} 
}
int main(void) 
{ 
	print_num(5,1,2,3,4,5);
	return 0; 
}
变参函数的设计和实现3.0

对于变参函数,编译器或计算机系统一般会提供一些宏给程序员使用
用这些宏上面的函数就能简化

va_list:定义在编译器头文件中 typedef char* va_list; 。
va_start(args,fmt):根据参数 fmt 的地址,获取 fmt 后面参数的地址,并保存在 args 指针变量中。
va_end(args):释放 args 指针,将其赋值为 NULL。有了这些宏,我们的工作就简化了很多。我们就不用撸起袖子,自己解析了。
void print_num3(int count,...) 
{ 
	va_list args;
	va_start(args,count); 
	for(int i = 0; i < count; i++)
	{ 
		printf("*args: %d\n", *(int *)args);
		args += 4;
	}
	va_end(args); 
}
int main(void) 
{ 
	print_num3(5,1,2,3,4,5);
	return 0;
}
变参函数的设计和实现4.0

在 V3.0 版本中,我们使用编译器提供的三个宏,省去了解析参数的麻烦。但打印的时候,我们还必须自己实现。在 V4.0 版本中,我们继续改进,使用 vprintf 函数实现我们的打印功能。vprintf 函数的声明在 stdio.h 头文件中。

CRTIMP int __cdecl __MINGW_NOTHROW \ 
	vprintf (const char*, __VALIST);

vprintf 函数有2个参数,一个是格式字符串指针,一个是变参列表。在下面的程序里,我们可以将,使用 va_start 解析后的变参列表,直接传递给 vprintf 函数,实现打印功能。

void my_printf(char *fmt,...)
{ 
	va_list args; 
	va_start(args,fmt); 
	vprintf(fmt,args); 
	va_end(args);
 }
 int main(void) 
 { 
 	int num = 0;
 	my_printf("I am litao, I have %d car\n", num);
 	 return 0;
  }
到底有什么用呢??

实现自己的日志打印函数
有这个东西,我们用宏就可以整体调整要打印,debug等快速打开关闭

#define DEBUG //打印开关 
void __attribute__((format(printf,1,2))) LOG(char *fmt,...) 
{
	#ifdef DEBUG va_list args;
	va_start(args,fmt);
	vprintf(fmt,args); 
	va_end(args); 
	#endif 
}
int main(void) 
{ 
	int num = 0;
	LOG("I am litao, I have %d car\n", num);
	return 0;
 }
attribute-> 属性声明:weak

基本使用方法如下

void __attribute__((weak)) func(void); 
int num __attribte__((weak);

对于变量,有效拒绝重定义等问题
对于函数,在没有这个函数的时候,编译不会报错,运行用到这个函数才会报错
可以在开发库的时候预留很多接口

attribute-> 属性声明:alias

相当于给函数重新定义了一个别名

void __f(void) 
{
	printf("__f\n");
}
 void f() __attribute__((alias("__f"))); 
 int main(void) {
 	f(); 
 	return 0;
 }

内联函数

attribute-> 属性声明:noinline & always_inline

大致就是这个函数需不需要展开
如果函数短,而且还多次使用,使用过内联函数可以加快运行速度

static inline __attribute__((noinline)) int func(); 
static inline __attribute__((always_inline)) int func();

内建函数

意思是编译器给我们提供的自带函数 比如:
用来处理变长参数列表;
用来处理程序运行异常;
程序的编译优化、性能优化;
查看函数运行中的底层信息、堆栈信息等;
C 标准库函数的内建版本。
下面列一些好用的

__builtin_return_address(LEVEL) 函数调用层级

这个函数用来返回当前函数或调用者的返回地址。函数的参数 LEVEl 表示函数调用链中的不同层次的函数

/*0:返回当前函数的返回地址;
  1:返回当前函数调用者的返回地址;
  2:返回当前函数调用者的调用者的返回地址;
……
*/

void f(void) {
    int *p;
    p = __builtin_return_address(0);
    printf("f return address: %p\n", p);
    p = __builtin_return_address(1);;
    printf("func return address: %p\n", p);
    p = __builtin_return_address(2);;
    printf("main return address: %p\n", p);
    printf("\n");
}

void func(void) {
    int *p;
    p = __builtin_return_address(0);
    printf("func return address: %p\n", p);
    p = __builtin_return_address(1);;
    printf("main return address: %p\n", p);
    printf("\n");
    f();
}

int main(void) {
    int *p;
    p = __builtin_return_address(0);
    printf("main return address: %p\n", p);
    printf("\n");
    func();
    printf("goodbye!\n");
    return 0;
}


内建函数:__builtin_constant_p(n)

编译器内部还有一些内建函数,主要用来编译优化、性能优化,如 __builtinconstantp(n) 函数。该函数主要用来判断参数 n 在编译时是否
为常量,是常量的话,函数返回1;否则函数返回0。该函数常用于宏定义中,用于编译优化。

内建函数:__builtin_expect(exp,c)

这个函数的意义主要就是告诉编译器:参数 exp 的值为 c 的可能性很大。然后编译器可能就会根据这个提示信息,做一些分支预测上的代码优化。
这个函数有两个参数,返回值就是其中一个参数,仍是 exp
参数 c 跟这个函数的返回值无关,无论 c 为何值,函数的返回值都是 exp。


int main(void) {
    int a;
    a = __builtin_expect(3, 1);
    printf("a = %d\n", a);
    a = __builtin_expect(3, 10);
    printf("a = %d\n", a);
    a = __builtin_expect(3, 100);
    printf("a = %d\n", a);
    return 0;
}

你可能感兴趣的:(c学习,c语言)