C语言
为了方便编译器分配内存,C89规定变量必须定义在函数开始的位置,在定义变量之前不能有其他表达式。C99取消了这个限制。
变量的默认初始值:
- 对于全局变量,默认初始值始终为0,因为全局变量储存在内存分区中的全局数据区,这个区域中的数据在程序载入内存后会被初始化为0。
- 对于局部变量,c语言并没有规定默认初始值为多少。有的编译器会初始化为0,有的编译器会放任不管。
无论是强制类型转换还是自动类型转换,都是为了本次运算而进行的临时性转换,转换的结果也会保存到临时的内存空间,不会改变数据本来的类型或者值。
缓冲区
缓冲区(buffer)也叫缓存(cache),是内存空间的一部分,计算机在内存中预留了一定的存储空间,用来暂时保存输入或者输出的数据。
缓冲区是为了让低速的输入输出设备和高速的用户程序能够协调工作,并降低输入输出设备的读写次数。
缓冲区的类型
缓冲区可以分为输入缓冲区和输出缓冲区。也可以分为全缓冲区、行缓冲区、不带缓冲。
全缓冲区:当缓冲区被填满以后才会进行真正的输入输出操作,缓冲区的大小都有限制,数据量达到最大时就清空缓冲区。在实际的开发过程中,将数据写入文件以后,打开文件并不能立即看到内容,只有清空缓冲区、关闭文件或者关闭程序后才能在文件中看到内容。
行缓冲区:当输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作。典型代表就是键盘和显示器。
不带缓冲:不带缓冲区,数据就没地方缓存,必须立即进行输入输出。
刷新缓冲区
缓冲区的刷新规则:
- 缓冲区满时会自动刷新。
- 行缓冲遇到换行符的时候会刷新。
- 关闭文件时会刷新缓冲区。
- 程序关闭时,也会刷新缓冲区。
- 使用特定的函数也可以刷新缓冲区。
对于输出操作,清空缓存区会使得缓冲区的所有数据立即显示在屏幕上;对于输入操作,清空缓冲区就是丢弃残余字符。
清空输出缓冲区
fflush(stdout)是一个专门用来清空缓冲区的函数,stdout是标准输出设备。
#include <srdio.h>
int fflush(FILE *stream)
调用fflush会将缓冲区中的内容写入到stream所指向的文件中去,若stream为NULL,则会将所有打开的文件进行数据更新。
fflush(stdin):刷新缓冲区,将缓冲区中的数据清空并丢弃。
fflush(stdout):刷新缓冲区,将缓冲区内的数据输出到设备。
fflush(stdin)不常用,可以使用while(getchar() != '/n')来代替
清空输入缓冲区
使用**getchar()**清空缓冲区:每次从缓冲区中读取一个字符,while(getchar() != '/n')
。
使用**scanf()**清空缓冲区:使用正则表达式,可以读取所有的字符。scanf(“%*[^\n]”);scanf(“%*c”);
第一个 scanf() 将逐个读取缓冲区中\n
之前的其它字符,% 后面的 * 表示将读取的这些字符丢弃,遇到\n
字符时便停止读取。
scanf()
指定读取长度
- %2d表示最多读取两位整数。
- %10s表示读取的字符串的最大长度为10。
匹配特定长度的字符
%[xxx]
,[]包围起来的是需要读取的字符集合,遇到[]之外的字符就结束。为了简化制度集合的写法,也支持使用-
连字符表示一个范围的字符,如 %[a-z]、%[0-9]。不匹配某些字符的方式就是在前面加上^,如%[^\n]
表示匹配除换行符以外的所有字符。
丢弃读取到的字符
- 具体的方法就是在%后面加上一个*,
%*d
表示读取一个整数并丢弃,%*[a-z]
表示读取小写字母并丢弃,%*[^\n]
表示将换行符以外的字符全部丢弃。
预处理
#include叫做文件包含命令,就是将头文件的内容插入到该命令所在的位置。使用<>编译器会在系统路径下查找头文件,使用“”编译器会首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。使用双引号比尖括号多了一个查找路径,就是当前路径。
#error 指令用于在编译期间产生错误信息,阻止程序的编译。
字符#的使用
#
是预处理作用。#
是字符串运算符,会把宏调用时的实参转化为字符串。##
是连接符号,功能是带参数的宏定义中将两个子串联起来,从而形成一个新的子串。##
在可变参数宏中的作用,例如printf(fmt,##arg)
,##
的作用是当可变参数为空或者被忽略的时候,使预处理器去掉前面的逗号。
指针
指针变量可以指向计算机的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限。
数组和指针不等价,大多数情况下数组名可以当做指针使用。比如在求数组长度的时候,必须使用数组名,而不能使用指针。
- 数组是一系列数据的集合,没有开始和结束标志。指针仅仅是指向一个数据类型,编译器不知道指向的是一个整数还是一堆整数。
- 数组也有类型,int a[6]中的数组a的类型就是int[6],表示这是一个拥有6个int数据的集合。
- 在int a[6]中**&a**,表示的是指向整个数组的指针,而a是指向数组的首个元素的指针。**(&a)++**移动的单位是数组的大小。
- 对于指针变量
int *p
,指针的数据类型就是int*
。 - 所以说指针和数组的数据类型是不同的,指代的数据也不同。
C语言规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序一次解析。所以,编译器是从名字开始读取的,而不是从开头或者结尾开始的。
int (*(*(*pfunc)(int *))[5])(int *)
表示pfunc是一个函数指针,该函数的返回值是一个指针,它指向一个指针数组,指针数组中的指针指向原型为int func(int *)
的函数。
运算符优先级
[]
()
.
->
-
(type)
++
--
*
&
!
~
sizeof()
/
*
%
+
-
<<
>>
>
>=
<
<=
==
!=
&
^
|
&&
||
?:
=
/=
*=
%=
+=
-=
<<=
>>=
&=
^=
|=
,
结构体
大小端
大端模式:将数据的低位放在内存的高地址,数据的高位放在内存的低地址。(高位低地址)
小端模式:将数据的低位放在内存的低地址,数据的高位放在内存的高地址。(高位高地址)
计算机中的数据都是以字节为单位存储的,每个字节都有不同的地址。
确定大小端的方式:
union duan{
int a;
char b;
};
int is_little_endian(void)
{
union duan u1;
u1.a = 0x12345678;
if(u1.b == 0x12)
return FAUSE;
else if(u1.b == 0x78)
return TRUE;
}
int is_big_endian(void)
{
union duan u1;
u1.a = 0x12345678;
if(u1.b == 0x12)
return TRUE;
else if(u1.b == 0x78)
return FAUSE;
}
位域
在定义结构体时,可以指定某个成员变量占用的二进制位数。使用位域定义的格式为类型说明符 位域名:位域长度
。位域只能是int、unsigned int、signed int类型。
位域的宽度不能超过所依附的数据类型的长度。位域技术就是在成员变量占用的内存中选出一部分位宽来存储数据。
位域成员不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的。
struct abc{
unsigned int a: 10;
unsigned int b: 10;
unsigned int c: 10;
};
- 一个位域存放在同一个字节中,如果一个字节所剩空间不够存放另一个位域的时候,就会从下一个单元起开始存放该位域。
- 位域的宽度不能超过它所依赖的数据类型长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度。
- 位域可以是无名位域,这时它只能用来作填充或者调整位置,无名位域是不能使用的。
struct k{
int a:1;
int :2;/*无名位域,不能使用*/
int b:3;
int c:2;
}
零长度数组
想要分配一个不定长度的数组,可以使用零长度数组,即柔性数组(flexible array),如下所示:
struct line{
int length;
char contents[0];
};
零长度数组是存在于结构体内部的,但是不占用结构体的size。可以简单地理解为一个没有内容的占位标志,直到给结构体分配内存,这个占位标志才变成一个有长度的数组。
为什么不直接使用指针而是使用零长度数组来表示?因为想要给结构体内的数据分配一个连续的内存。
- 方便内存释放:在结构体中使用指针,就需要做二次内存分配,当需要free结构体的时候,很容易忽略结构体中二次分配的内存,导致内存泄漏。如果使用零长度数组,就可以一次分配内存,并且在free的时候,free一次就行。
- 有利于访问速度:连续的内存有利于提高访问速度,同时也有利于减少内存碎片。
const
const修饰指针的时候,const离变量名近就是用来修饰指针变量,离变量名远就是用来修饰指针指向的数据。
const通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修饰指针指向的数据,就可以使用const来限制。
结构体赋值和初始化
可以使用struct = {xx,xx,xx}的方式,结构体成员变量赋值完后用逗号而不是分号,初始化语句的元素是以固定的顺序出现的,和被初始化的数组或者结构体中的元素顺序一样。
使用在成员变量前面加上小数点,可以不用按照成员顺序赋值。struct = {.member1 = xx, .member2 = xx, .member3 = xx}
内嵌汇编
在内嵌汇编中,可以将c语言表达式指定为汇编指令的操作数,不用管如何将c语言表达式的值读入到哪个寄存器,以及如何将计算结果返回,只需要告诉c语言表达式和汇编指定操作数之间的关系。
c语言内嵌汇编举例为:__asm__ __volatile__("hlt");
__asm__
表示后面的代码为内嵌汇编,asm
是__asm__
的别名。__volatile__
表示编译器不要优化代码,后面的指令保持原样。volatile
是__volatile
的别名。- 括号中
"hlt"
是汇编指令。
内嵌汇编举例:__asm_- __volatile__("mov %1, %0" : "=r" (result) : "m" (input));
"move %1, %0"
是指令模板;"%0"
和"%1"
代表指令的操作数,称为占位符,内嵌汇编靠它们将c语言表达式与指令操作数对应起来。result
和input
是c语言表达式,即指令模板后面括号里面的是c语言表达式。按照出现的顺序分别和指令操作数"%0"
和"%1"
对应。操作数最多有10个,按照c语言表达式的出现顺序,分别和"%0"
…"%9"
对应。"=r"
和"m"
是对操作数的限制和要求,"="
表示"result"
是输出操作数。
c语言内嵌汇编语法:__asm__(汇编语句模板:输出部分:输入部分:破坏描述部分)
-
汇编语句模板:由汇编语句组成,语句之间使用
;
、\n
、\n\t
来分开。指令中的操作数可以使用站位符来引用c语言变量,操作数站位符最多10个。指令中使用占位符表示的操作数,被视为long
型,当把操作数当做字节来使用的时候,默认是操作低字节,可以显式的指定低字节或者高字节。方法是在%
和序号之间插入l
或者h
用来表示低字节或者高字节,如%h1
。 -
输出部分:输出部分描述输出操作数,不同的操作数描述符之间用逗号隔开,每个操作数由限定字符串和c语言变量组成。每个输出操作数的限制字符串必须包含
=
用来表示输出操作数。 -
输入部分:输入部分描述输入操作数,不同的操作数描述符之间使用逗号隔开,每个操作数描述符由限定字符串和c语言表达式或者c语言变量组成。
破坏描述部分:破坏描述符用于通知编译器,我们使用了哪些寄存器或者内存,由逗号隔开的字符串组成。