c语言精粹01
c语言精粹01
0x00 这篇博客的目的
很久没写博客,在面试前一天,决定把c的比较偏,容易错的地方梳理下,拾遗补漏。博客内容结合了《c primer plus 第六版》及自己的思考经验。
0x01 c语言初体验
为什么选c?易懂,高性能,易于移植。缺点?底层,少库,指针容易出错。
0x02 编译环境
GNU是一个开源项目,包含了很多软件,GCC是一个编译器集合。LLVM项目也是一个编译器相关的软件集合,通过clang调用c编译器。在linux上都可以方便的安装(一般自带gcc 使用gcc -v来查看版本)
0x03 可移植类型与浮点数初探
头文件stdint.h中有int16_t int32_t等与平台无关的整数类型,方便程序移植。浮点数有float double long double 前者是 1位符号 8位阶码 23位尾数 double则是11位阶码 52位尾数,long double只是规定不低于double的精度。
0x04 printf何时现实?
好像已经习惯了执行printf就能立马显示,实际上这是错觉,不信?执行如下main函数:
printf("a");
write(STDIN_FILENO,"b",1);
while(1);
return 0;
可以发现屏幕只显示 b。实际上,printf函数调用了write,往stdin写,但使用了应用层缓冲,(write在内核层还有缓冲)。printf如果遇到\n或者调用scanf或者应用层缓冲区满,才写入stdin。
0x05 常量和c预处理
对于一些不变的量,使用有名字的符号常量,比直接使用字面量(magic number)好,方便更换和理解,但是为了防止中途被修改,可以加上const前缀。或者使用#define name value ,前者有些编译器或直接编译成字面量到汇编指令中,想要替换成实际的变量需要加volatile 在const前,而后者则是预处理阶段替换。
0x06 printf与scanf初探
printf底层调用了sprintf,将字符串格式化,再调用write写到stdin里,使用转换符标识待替换的参数的类型,常见如下:
%c 单个字符 %d 有符号十进制整数
%e %E 浮点数 科学计数法 %f 浮点数 10进制
%o 无符号8进制 %p指针 %s 字符串 %u 无符号十进制
%x %X 无符号16进制 %% 打印%
另外有转换符修饰符,用来组合
标记 - + 空格 # 和 0 //表示如果带有这些标记,就显示他们,比如%+d,10
//那么会显示+10
数字 表示最小宽度 //如%4d,10 则是 10,前面有两个空格
.数字 表示精度,如果是字符串则是最大宽度 //如%.4d,10则是 0010
printf字符串过长时,可以①使用 \ +换行 ② 分成两个字符串(都用双引号括起来) 中间不需要连接符
scanf输入 转换符,scanf也有缓冲区,每次从缓冲区里取,如果这次有多余的,就给下次用,scanf回返回正确读取的参数的个数
%c 输入字符 包括空白和换行符
%d 解释成10进制无符号数字
%e %f %a 解释称浮点数
%i 解释成10进制有符号
%o 八进制
%u 10进制无符号
%x 16进制有符号
%p 指针
%s 字符串 遇见空白或者换行就停止(不包含换行符),结尾'\0'
0x07 隐式类型转换
①在传递参数时,整数与浮点数可以互相转换。
②在传递参数时,int与short直接也会转换,小转大不丢失信息,大转小,可能丢失,原则就是二进制不变,解释变化,从低地址解释起(小端字节)。
③在传递参数时,数组会转换成指针。
0x08 getchar与缓冲区
现在的终端输入,都有输入缓冲,需要按下enter,才送到用户程序,缓冲的好处是,便于用户更改输入。缓冲有两种,一种完全缓冲,等到缓冲区满了,才送往目的地,比如读文件,内核缓冲区读满了,才送到应用程序,另一种是行缓冲,输入enter,才送到应用。
由于行缓存刷新时需要用enter,并且该换行符会被读入缓冲区,所以需要处理这个换行符,可以用getchar()拿出来。
关于文件中的换行,linux上直接用换行符 \n 来记录,而windows上则用 \r\n,不过经过c库的封装,可以用 a==’\n’来在两个平台使用,是没问题的,可以先看到下图windows上的换行是0x0d 0x0a \r\n
使用crtl+z可以结束输入
0x09 重定向
执行应用时,命令行 使用 > 可以重定向输出 < 重定向输入
0x0a 尾递归
尾递归总是可以被优化成迭代的(循环的),原因在于,尾递归是函数已经执行完了,准备返回了,因此没有需要保存的上下文了,所以不需要入栈,而相当于直接执行了新的函数。
0x0b 多文件编译
gcc可以将多个.c源文件分别 预处理 汇编 编译成.o文件,最后链接,链接时可以链接静态库即.a文件(静态库实质是很多.o文件里部分函数的组合),而动态链接则是基于共享内存实现的,同一份代码,映入不同程序的地址空间,即.so库
0x0c 数组初始化
①列表初始化
int a[]={1,2,4};//如果没定义维度,则根据列表长度定义
int a[3]={1,2,4};//如果定义了,则正常
int a[3]={1,2};//如果列表长度小于维度,其余初始化为0 gcc是这样
②指定初始化器
int a[100]={[87]=87,88,89,[90]=8,7}//c99开始支持初始指定位置的元素
//而不需要数逗号
0x0d 多维数组与指针
须知,多维数组,实际上是数组的数组,而如果单独使用数组的名字,那么实际上是用的数组的第一个元素的地址。
另外,数组在作为实参传入函数时,会被转换成指针,因此数组的数组,传入,则是指向数组的指针。因此最外层的维度,会被忽略,而内层的维度则不会。
因此,以下代码可以正常编译 运行
void func(int p[3][4][5]){//等价于int (*p)[4][5]
return;
}
int b[2][4][5];
func(b);
0x0e 变长数组(VLA)
variable length array是指的,定义时可用非字面值作为维度的数组,在C11以前,只能用字面量,但是现在,不仅可以使用const变量,甚至可以使用变量,甚至可以等到运行时才知道数组的维度(动态分配内存实现的,也自动释放)。但是定义后维度还是不可修改。