C语言
1 B(byte 字节) = 8 bit(位)
类型空间
类型 | 32位 | 64位 |
---|---|---|
char | 1字节 | 1字节 |
int | 4字节 | 4字节 |
float | 4字节 | 4字节 |
double | 8字节 | 8字节 |
short | 2字节 | 2字节 |
long | 4字节 | 4字节 |
long long | 8字节 | 8字节 |
long double | 8字节 | 16字节 |
signed | 4字节 | 4字节 |
unsigned | 4字节 | 4字节 |
指针 | 4字节 | 8字节 |
占位符
类型 | 含义 |
---|---|
d | 整型 |
ld | 长整型 |
o | 八进制数形式整数 |
u | 十进制数unsigned(无符号)型数据 |
x | 十六进制数形式整数,或字符串的地址 |
X | 十六进制数形式整数 |
i | 十进制,八进制,十六进制整数 |
c | 一个字符 |
s | 一个字符串 |
f | 小数形式的实数,默认情况下保留小数点6位 |
e | 指数形式的实数 |
g | 根据大小自动选f格式或e格式,且去掉无意义的零 |
p | 地址 |
关键字
auto
解释为一个自动存储变量的关键字,也就是申明一块临时的变量内存。
自动存储期的变量在进入声明该变量的程序块时被建立,它在该程序块活动时存在,退出该程序块时撤销。在函数内部定义的变量成为局部变量。
register
用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个字),且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。
寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 ‘register’ 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。 让该变量的访问速度达到最快。
在使用寄存器变量时,请注意:
(1)待声明为寄存器变量的类型应该是CPU寄存器所能接受的类型,寄存器变量是单个变量,变量长度应该小于等于寄存器长度。
(2)不能对寄存器变量使用取地址符“&”,因为该变量没有内存地址。
(3)尽量在大量、频繁操作时使用寄存器变量,且声明的变量个数应该尽量少。
extern
对该变量作“外部变量声明”,即表示该变量是一个已经定义的外部变量。这样就可以从声明处其,使用该外部变量。 多个源文件之间的引用
static
使用 static 关键字修饰变量时,我们称此变量为静态变量。 隐藏与隔离的作用,文件中避免变量名冲突
当将局部变量声明为静态局部变量的时候,也就改变了局部变量的存储位置,即从原来的栈中存放改为静态存储区存放。 保持变量内容的持久性,在函数中定义变量,函数结束不会释放内存,保留到程序结束。
在静态数据区,内存中所有的字节默认值都是 0x00。静态变量与全局变量也一样,它们都存储在静态数据区中,因此其变量的值默认也为 0。 默认初始化为 0
const
声明只读变量
const修饰的数据类型是指常类型,常类型的变量或对象的值是不能被更新的。
- 定义const常量,具有不可变性。
- 便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。
- 可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。
- 可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。
- 可以节省空间,避免不必要的内存分配。
- 提高了效率。
sizeof
计算数据类型长度
一种单目操作符,如C语言的其他操作符++、–等。它并不是函数。sizeof操作符以字节形式给出了其操作数的存储大小。操作数可以是一个表达式或括在括号内的类型名。操作数的存储大小由操作数的类型决定。
strlen和sizeof的区别
strlen函数,运算时才能计算,计算字符串的具体长度,不包括字符串结束符,返回的是字符个数。
sizeof运算符,编译时就能计算,计算声明后所占的内存数,不是实际长度。返回字符个数*字符所占的字节数
typedef
- 为基本数据类型定义新的类型名
- 为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
- 为数组定义简洁的类型名称
- 为指针定义简洁的名称
volatile
说明变量在程序执行中可被隐含地改变
编译器对访问该变量的代码就不再进行优化,从而可以提供对其地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。
- 并行设备的硬件寄存器(如:状态寄存器)
- 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
- 多线程应用中被几个任务共享的变量
编译
预编译
不会检查语法错误
- 处理所有的注释,以空格代替
- 将所有的 #define 删除,并且展开所有的宏定义
- 处理条件编译指令
#if, #ifdef, #elif,#else,#endif
- 处理 #include,展开被包含的文件
- 保留编译器需要使用的 #pragma 指令
预处理指令示例:gcc -E file.c -o file.i
编译
对预处理文件进行词法分析,语法分析和语义分析。编译的过程实质上是把高级语言翻译成机器语言的过程。
- 词法分析:分析关键字,标示符,立即数等是否合法
- 语法分析:分析表达式是否遵循语法规则
- 语义分析:在语法分析的基础上进一步分析表达式是否合法
以字符流的形式进行处理,进行词法和语法的分析,然后通过汇编器将源代码指令转变成汇编指令、生成相应的汇编文件。
编译指令示例:gcc -S file.i -o file.s
汇编
- 汇编器将汇编代码转变为机器的可以执行指令
- 每条汇编语句几乎都对应一条机器指令
也就是把汇编码转换成机器所能识别的二进制
汇编指令示例:gcc -c file.s -o file.o
链接
由.o文件到可执行文件,这个过程叫链接。经过汇编之后生成的目标文件并不能立即被执行,还需要由链接器将代码在执行过程中用到的其他目标代码及库文件进行链接,最终生成一个可执行程序。
链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)
汇编指令示例:gcc hello.o -o hello.exe
假如.c文件中有用到printf函数,那么就需要找到包含该函数的标准库文件,对它进行链接。
预处理指令
预处理指令是以#号开头的代码行,# 号必须是该行除了任何空白字符外的第一个字符。# 后是指令关键字,在关键字和 # 号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。
指令 | 说明 |
---|---|
# | 空指令,无任何效果 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if……#else条件编译块 |
#include
文件包含命令,用来引入对应的头文件(.h文件)。#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。
用法:
1、使用尖括号< >,编译器会到系统路径下查找头文件;
2、使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
3、使用尖括号来引入标准头文件,使用双引号来引入自定义头文件
注意事项:
1、一个#include命令只能包含一个头文件,多个头文件需要多个#include命令。
2、同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。
3、文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
#define
#define
是最常见的预处理指令,用来将指定的词替换成另一个词。它的参数分成两个部分,第一个参数就是要被替换的部分,其余参数是替换后的内容。每条替换规则,称为一个宏(macro)。
#define MAX 100
#define HELLO "Hello, world"
带参数的宏
#define SQUARE(X) X*X //z = SQUARE(2);
#define SQUARE(X) X*X
// 输出19
printf("%d\n", SQUARE(3 + 4));
/*SQUARE(3 + 4)如果是函数,输出的应该是49(7*7);宏是原样替换,所以替换成3 + 4*3 + 4,最后输出19。
可以修改为: #define SQUARE(X) ((X) * (X))
*/
由于宏不涉及数据类型,所以替换以后可能为各种类型的值。如果希望替换后的值为字符串,可以在替换文本的参数前面加上#
#define STR(x) #x
// 等同于 printf("%s\n", "3.14159");
printf("%s\n", STR(3.14159));
如果替换后的文本里面,参数需要跟其他标识符连在一起,组成一个新的标识符,可以使用##
运算符。它起到粘合作用,将参数“嵌入”一个标识符之中。 ##
运算符的一个主要用途是批量生成变量名和标识符。
#define MK_ID(n) i##n
int MK_ID(1), MK_ID(2), MK_ID(3);
// 替换成
int i1, i2, i3;
如果整条指令过长,可以在折行处使用反斜杠,延续到下一行。
#define
指令可以出现在源码文件的任何地方,从指令出现的地方到该文件末尾都有效。宏的名称不允许有空格,而且必须遵守 C 语言的变量命名规则,只能使用字母、数字与下划线(
_
),且首字符不能是数字。宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束,如要终止其作用域可使用#undef命令。
代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替。
宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。
习惯上宏名用大写字母表示,以便于与变量区别。
宏定义只是简单的字符串替换,由预处理器来处理;typedef是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。
#undef
#undef
指令用来取消已经使用#define
定义的宏。
#define LIMIT 400
#undef LIMIT
上面示例的undef
指令取消已经定义的宏LIMIT
,后面就可以重新用 LIMIT 定义一个宏。
有时候想重新定义一个宏,但不确定是否以前定义过,就可以先用#undef
取消,然后再定义。因为同名的宏如果两次定义不一样,会报错,而#undef
的参数如果是不存在的宏,并不会报错。
GCC 的-U
选项可以在命令行取消宏的定义,相当于#undef
。
$ gcc -U LIMIT foo.c
宏定义#define与关键字typedef的区别
(1)与#define不同,typedef创建的符号名只受限于类型,不能用与值
(2)typedef由编译器解释,#define由预处理器处理
(3)typedef与#define的作用范围不同
typedef如果定义在函数中,就具有局部作用域,受限于定义所在的函数。如果定义在函数外面,就具有文件作用域。
#define可以出现在源文件的任何地方,其定义从指令出现的地方到该文件末尾有效。
条件编译
根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译,条件编译是预处理程序的功能,不是编译器的功能。
#if 整型常量表达式1
程序段1
#elif 整型常量表达式2
程序段2
#else
程序段3
#endif
#ifdef 宏名
程序段1
#else
程序段2
#endif
//如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。
#ifndef 宏名
程序段1
#else
程序段2
#endif
//如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。
-
预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。
-
宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。
-
为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
-
文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
-
#if后面跟的是“整型常量表达式”,而#ifdef和#ifndef后面跟的只能是一个宏名,不能是其他的。
-
条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
-
使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。
库文件
头文件(.h)
#ifndef 头文件名 //头文件名的格式为"_头文件名_",注意要大写
#define 头文件名
//头文件内容
#endif
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。所谓静态、动态是指链接。
静态库
在链接阶段,将汇编生成的目标文件.0与引用到的库一起打包到可执行文件中,此种链接方式称为静态链接。
静态库的链接是编译时期完成;
程序运行与函数库再无瓜葛,移植方便;
浪费空间与资源
创建静态库:
静态库命名规则:必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.a
gcc -c StaticMath.c
首先使用gcc -c只编译不连接,生成.o文件:然后使用ar工具进行打包成.a归档文件。
ar –crv StaticMath.a StaticMath.o
把.a和.h放在引用的文件夹下,然后再.c文件中包含库的.h,然后直接使用库函数。
gcc –o main main.c –I./StaticLib –L./StaticLib -lStaticMath
-I directory: 指定头文件搜索路径;
-L directory:指定库文件搜索路径;
-l lib:指定需要链接的库文件,省略前缀与后缀。
除了ar名另外,还有个nm命令也很有用,它可以用来查看一个.a文件中都有哪些符号
动态库
动态库把函数库的链接载入到程序运行时期;
可实现进程之间资源共享(共享库);
增量更新;
可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
创建动态库:
动态库命名形式:libxxx.so,前缀是lib,后缀名为".so"。
首先,生成目标文件,此时要加编译器选项-fpic:
gcc –fPIC –c DynamicMath.c
-fPIC 创建与地址无关的编译程序,是为了能够在多个应用程序间共享。
然后,生成动态库,此时要加链接器选项-shared
gcc –shared –o libdynamicmath.so DynamicMath.o
-shared指定生成动态链接库。
上面可合成一步:
gcc –fPIC –shared –o libdynamicmath.so DynamicMath.c
生成动态库文件后为了能让系统链接器在程序运行过程中能够成功载入该程序,需要将该动态库所在路径添加进/etc/ld.so.conf文件,或在/etc/ld.so.conf.d/目录下新建任何以.conf为后缀的文件,在该文件中加入库文件所在的目录,运行ldconfig,以更新/etc/ld.so.cache文件,否则会报error while loading shared libraries: libXXX.so.X: cannot open shared object file: No such file。