对C语言更高阶的认识,从野生到正统,从杂牌军到正规军。
1、printf函数的理解
printf函数的原型:int printf(const char*format,…);
format:字符串,
…:代表输入的参数是变参,类型通过format里面来指定。
返回值:代表输出的字符个数。
返回值示例代码:
int a=123;
printf("%d\n",printf("%d\n",printf("%d\n",a)));
变参的处理:会用到一组函数来进行变参的获取,主要有以下几种类型:
va_list 类型:变参的结构类型,首先定义变量,其是指向参数的指针;
va_start():初始化该变量,即获取变参的地址
va_end():销毁变参,结束变参数据的获取。
va_arg():获取变参数据,如果是多个变参,可以通过多次调用来依次回去参数数据;
示例代码如下:
int demo(char*msg,...)
{
va_list argp;
int arg_num=0;
char *para;
va_start(argp,msg);
while(1)
{
para = va_arg(argp,char*);
if(strcmp(para," ") == 0) //为了退出而写
break;
printf("Para #%d is %s\n",arg_num,para);
arg_num++;
}
va_end(argp);
return 0;
}
demo("msg","this","is","a","demo!"," ");
标准库中的printf函数理解:
格式:%[flags][width][.precision][length] spcification
- flags:
-:左对齐,默认识右对齐
+:强制数据带±号
(space):不足空格补齐
#:与十六进制联合使用,显示0x,0X
0:不足位数补0,而不是部空格 - widths:
(number):number指示显示的位宽占多少个字符,
(*):表示位数可以通过参数指定,是变参,而上面的是固定参数 - precision:
(.number):控制小数点精度位数,number指示位数 - length:不是指具体数字
h:short Int
hh:short char
d:int
l:long int
ll:long long int
u/o/x/x :(unsigned 十进制、八进制、十六进制) 示例代码:u64 temp_data = 0x123456789a; printf("####### startup log system! %lld %#x %lld %p %#p ######",temp_data,temp_data,&temp_data,temp_data,temp_data );
结果如下图所示:

2、指针分配地址
指针需要知道几个概念:指针指向、指针内容、指针地址
指针指向:就是指针指向的一块内存。
指针内容:是指的地址里面的值。
指针的地址:指的是指针本身的地址,指针指向的也是一个地址,所以这边有点绕。
void myGetMemory(char* p)
{
p = (char*)malloc(100);
}
int main()
{
char *str=NULL;
myGetMemory(str);
strcpy(str,"hello world");
printf("%s",str);
return 0;
}
上述程序的问题,就是指针的用法错误,
分析一下问题:
- 第一步 p-> str,所以p的值和str的值均为空,因为这是参数传值,会有两个变量。
- 下一步 指针p指向分配的空间,但是str并没有分配空间,
- 再一下内存拷贝会出错(段错误)。
这里错误的地方在于:本身想修改str的指向,但是没有用对方法。
- 正确修改指针的指向应该是用二级指针,因为二级指针指向是一级指针的地址,
- 那么二级指针内容即是一级指针的指向,
- 通过这种方法可以修改一级指针的指向,就可以分配到空间。
具体实现如下:
void myGetMemory(char** p)
{
(*p) = (char*)malloc(100);
}
int main()
{
char *str=NULL;
myGetMemory(&str);
strcpy(str,"hello world");
printf("%s",str);
return 0;
}
还有种方法也可以,
- 将二级地址强转成整形,
- 然后传递进来,
- 再强转成指针,
- 最后其指针内容就是str指向的空间,给其空间赋值,后面就可以拷贝。
void myGetMemory(int p)
{
*(int *)(p) = (int)(char*)malloc(100);
}
int main()
{
char *str=NULL;
myGetMemory((int)&str);
strcpy(str,"hello world");
printf("%s",str);
return 0;
}
3、宏的使用
define 宏定义常量、函数,
高阶用法:
- 实现“重载”,动态过滤提供效率
#if (VERSION == 1)
MY_PRINT(a) my_one_log_print(a)
#elif (VERSION == 2)
MY_PRINT(a) you_own_log_print(a)
#endif
- 多内容替换,假如说宏内容中有需要通过if条件来进行是否选择执行,可以选用这种方法。
#if (VERSION == 1)
#define MY_PRINT(a) \
do \
{\
if(LEVEL == DEBUG)\
{\
my_own_log_print(a);\
}\
}while(0)
#elif(VERSION == 2)
#define MY_PRINT(a,b) \
do \
{\
if(LEVEL == DEBUG)\
{\
my_own_log_print(a,b);\
}\
}while(0)
#endif
- 名字空间,担心宏与其他宏进行重复,但是又不想宏名字太长,可以选用这种方法,
void my_own_log_print(u8 _level,fmt,...);
#define LOG_DEBUG 0
#define LOG_INFO 1
#define LOG_WARN 2
#define LOG_CRIT 3
#define MY_PRINT(_level,_fmt,...) my_own_log_print(LOG_##_level,_fmt,##_VA_ARGS__)
int main()
{
my_own_log_print(INFO,"this is my log!\n");
return 0;
}
- 编译器自带的宏,无需自己定义,可以直接使用。
__FILE__:显示当前文件路径及名称
__FUNCTION__:当前所在函数
__LINE__:所在行号
__DATE__:当前日前
__TIME__:当前时间

- 几个符号常见的用法
,##,VA_ARGS,##VA_ARGS
- [x] #:将后面紧跟的符号转化为字符串
#define PRINT_VAL(n, val) printf("%s = %d\n", #n, val)
int x1 = 3;
PRINT_VAL(x1, x1);

- [x] ##:将##左右两边的符号连接在一起变成一个符号
#define VAR_NAME(n) x##n
int VAR_NAME(0) = 0; //定义整型变量 x0=0;

- [x] VA_ARGS:表示该部分允许输入可变参数,必须有参数才行
#define LOGD(format, ...) printf("debug: " format, __VA_ARGS__)
- [x] ##__VA_ARGS:支持可变参数的输入或者无参数输入(参考上面变参宏替换)
#define LOGD(format, …) printf("debug: " format, ##VA_ARGS__)
- 几个预编译的符号认识
- [x] #error “msg”
如果执行到该行,则停止编译,并打印msg字符串,字符串可以任意定义。常用来做编译检查,如果代码被修改,则会编译报错; - [x] #ifdef、#ifndef、#elif、#else、#endif
- [x] #if、#if defined、#if !defined
4、C/C++对重载的支持
C语言不支持重载,C++支持重载
- C语言是因为函数名即是函数地址,如果函数名一样,则地址一样,则调用时无法区分,
- C++是因为编译器会对函数名进行修饰,按照参数的个数、顺序等,所以即使重载,最后编译器认识的函数名均不同,则分配了不同的地址,所以支持重载。
- C如果调用C++的库,则会用出现找不到函数名的情况,所以这个时候用到了“extern C”,将其按照C语言的方法进行编译,则函数名不会进行修饰,具体参考博客:extern C :静态库与动态库
5、头文件的包含
- 头文件的认识:
- 只放接口,其他(内部函数、依赖、不相关宏、结构体 放在c文件中,不要暴露出来,减少接口数量)
- 包含头文件在用到的文件中包含,不用到的不包含,减少依赖,减少编译时间
比如:三个文件A、B、C。其.c文件包含其.h文件,有依赖关系。
- A依赖于B,如果A.h包含了B.h,则A.c中也有B.h,可以正常编译
- 此时C依赖于A,则C.h包含A.h,
- 如果B.h发生变化,则重新编译时,ABC的.c文件都需要重新编译,但是C本身与B并没有关系,但是也重新编译,岂不是浪费时间。
- 如果A.c包含B.h,那么重新编译时,则只需要编译A与B,则C无需编译。
前者包含关系:
后者包含关系:
- 函数的认识:
- 变参(… ##VA_ARGS) va_list,
- 内部函数static修饰,接口放头文件
- 入参加const,不想被改变的返回值加const
- 简单高频次的函数声明为内联函数
- 注意层次关系,一般下层不调上层函数