1.7 多阶指针指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链;有两个概念需要区分,一个是指针变量本身,一个是指针变量指向(存储)的地址。比如:
int a = 100; int *pa = &a; - 定义一个整型(int)变量a,值是100。
- 定义一个整型指针(int *)变量pa,值是a的地址。
pa是一个整型指针变量,指向的地址是整型变量a的地址,而*pa是地址里面的内容,也就是变量a的值100。 由此看一个程序: char *类型指针p1,指向常量区内容为"1234567890"的地址; char **指向指针的指针p2,指向p1地址,因此*p2就是p1,依次类推 定义char ***p3存放p2地址,*p3就是p2,**p3相当于*p2也就是p1; 定义char ****p4存放p3地址,*p4就是p3,**p4相当于*p3也就是p2, ***p4相当于**p3也就是*p2即p1。 上述程序中printf的输出应该都是一样的,编译测试: 1.8 通用类型指针a. 类型转换。
所谓通用类型指针,其实就是void *;该指针可以用来转换不同类型参数指针变量,如下例子(main_3.c),我们定义一个用于输出任意类型变量内容的函数,函数要求传入参数时指定该参数的类型(比如uint16_t,string等)来指示输出: 函数将void *类型格式化为想要类型的指针,然后取值输出,调用方式: 编译输出: b. 代理传参调用。
下面这个例子会比较抽象(涉及到指针和地址比较抽象的转换关系,了解即可,不要求能理解里面的转换逻辑),源码放在main_4.c中,需要很好理解地址和指针变量的关系,我们借助void *的列表来存储一段空间,并用来做回调函数的调用,从而达到代理调用的过程。 思考一下:int *pa = (int *)malloc(sizeof(int)); 这个代码中,指针变量本身的地址在哪,指针变量指向的地址在哪,指针变量指向的地址里面的内容是什么,先清这几个概念再继续往下看,首先指针变量本身的地址是&pa;指针指向的地址是pa(准确来说是(uint64_t)pa);指针指向的地址里面的内容是*pa。 声明几个函数如下: 然后有一定测试代码如下: 首先我们申请了2个参数(args),然后给两个参数进行赋值,第一个值是一个函数的地址,第二个值是一个uint16_t类型的数值(12345);然后调用tester进行测试,函数tester里面把args的内容(函数和数值)取出来,进行调用,也就是把第一值强制转换成函数指针(第21行),然后传入数值完成调用。 再看args这几个声明的函数实现: 在函数args_add_func里面,首先我们把指针变量func所指向的地址保存了起来(loader),然后loader被放在args中,由于index是0,因此是放在第一个位置;在函数args_add_uint16里面,我们则是把变量value的数值保存起来(pvalue),然后放在args第二个位置(index是1)。再回过头看tester里面的代码大概就能看懂了。 编译运行,结果等价于直接调用show函数:show(12345): 1.9 主函数(main)参数列表默认情况下,main函数是一个程序的入口函数,而我们可以给程序传入一系列的参数列表,这个列表可以在main函数中被解析;这就需要main函数是带参的方式使用了,基本写法:int main(int argc, char *argv[])。 main函数参数列表的解析,需要包含一个头文件:#include <getopt.h>,利用该头文件提供的接口,我们需要理解几个地方即可, a. 结构体:
struct option {
const char *name; // 选项(参数)名 int has_arg; // 是否选项后有参数,常用取值如下: // required_argument,no_argument int *flag; // NULL时返回val,使用时保持NULL就可以了。 int val; // 返回值,需和flag配合。 }; b. 外部变量: extern char *optarg; // 当前选项对应的参数值。 c. 函数(结合例子看): int getopt_long(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex); d. 还有就是选项字符串:
字符加冒号,比如:"p:n:v::th",说明: - 字符加一个冒号表示选项后面必须加一个参数,选项和参数用空格隔开。
- 单个字符后跟两个冒号,表示该选项后可以跟一个参数,也可以不跟,如果后边跟一个参数,参数必须紧跟在选项后不能以空格隔开。
- 单个字符,表示选项,后面不能带参数。
例子(模板),选项必须再opts中找得到: 编译运行如下: 1.10 可变参数基本用法C语言的可变参数解析需要包含头文件:#include <stdarg.h>,直接上例子,我们实现一个加法函数add,用来求和: add第一个参数n,表示可变参数的长度,有几个地方说明下: va_list 为可变参数类型。 va_start 是一个宏,计算起始地址。 va_end 是一个宏,计算结束地址。 va_arg 是一个宏,它返回当前取值指针指向的参数值并将取值指针指向形参列表的下一个参数的首地址。 结果如上图,基本是固定的,我们通过va_arg拿出可变参数的每个数据,然后进行加法运算。 main函数中调用的不是函数add,而是宏ADD,这里主要是说明宏如何进行可变参数的处理,可以用__VA_ARGS__来表示可变参数。 编译运行: 1.10.1 扩展用法之日志输出利用可变参数的特性,我们可以实现一个日志功能,输出带有文件名,函数名,行号的一个接口来代替printf的功能;再linux c中,可以用三个宏来分别表示文件名:__FILE__;函数名:__func__;行号:__LINE__;有了这三个宏,我们再把用户输出的内容拼接进入,就可以完成一个带格式的日志功能了,首先声明并实现一个函数logd如下: 这个函数的参数列表包括:文件名file,函数名func,行号line,剩下的format和一个可变参数;可变参数的解析结构和上节课一样,多出一个新的接口函数是vsprintf,这个接口是把可变参数的内容格式化为一个字符串。 为了简化调用,我们定义一个宏LOG,这个宏主要的作用是自己填充文件名,函数名,行号,简化调用过程: 最后是使用的方法: 编译运行: 1.11 头文件兼容C++定义.h文件时,可以按照下面的声明方式,从而完成对c++的兼容: 头文件应符合几个基本要求: - 尽可能少的包含其他头文件。
- 不带具体的代码实现,纯粹的做API声明。
- 简洁明了。
- 除了需要extern声明的外部变量,不能出现变量的定义。
1.11.2 日志功能实例我们把3.5.2中的日志接口单独抽取出来,放在log.h中去声明接口函数,然后在log.c中去实现,最后在main.c中去包括log.h并通过宏来调用日志输出功能。 接口实现源码log.c如下: 然后main.c中的调用如下: 编译运行(gcc log.c main.c -o tester): 1.11.3 C++源文件测试上一节C语言测试了log.h,现在我们创建一个main.cpp,在C++源码中来测试,测试内容如下(嵌入式C++基础后续在基础篇中加入,这里了解即可): logger::instance()是为了取得一个类logger的单例,类logger中有一个函数debug对log.h中的接口进行了一次封装;调用如下: 编译运行(g++ log.c main.cpp -o tester):
================================================================================================================================== 这样整个开发环境及交叉编译链就搭建好啦!!如果觉得对您有帮助并想进一步深入学习交流可以扫描以下微信二维码或加入QQ群:928840648
欢迎共同学习成长,有一群爱学习的小伙伴一起勉励!!加油!!也可点击
|