C语言做题知识点总结
目录[*]C语言做题易错及知识点总结
[*]
[*]重要!!!负数在计算机中的存储:
[*]重要!!!计算机内部的存储
[*]特殊情况128
[*]变量范围
[*]man手册的使用
[*]重要!!!格式化输出
[*](1) 标志字符 (必须放在%说明符后面)
[*](2) 转换精度
[*](3)长度修饰
[*](4) 转换说明
[*]转义字符的输出需要加\
[*]1. 用双反斜杠 \ 输出单个反斜杠
[*]2. 完整示例代码
[*]格式化输入
[*](1)匹配字符[]
[*]重要!!!运算符
[*]1. 左值的定义
[*](1)位操作运算符
[*](2)逻辑运算符
[*](3)强制类型转换需要直接截断 不会四舍五入。
[*]最重要!!!置位复位操作
[*]各种语句
[*]Switch语句注意事项
[*]break与continue区别
[*]for()循环语句
[*]数组
[*]结论: *(E1+E2)==E1
[*]数组的两种例外情况
[*]字符数组
[*]二维数组
[*]二维数组的访问
[*]匿名数组
[*]零长数组
[*]段错误产生的原因
[*]重要!!!指针
[*]指针表示的含义
[*]二维指针的含义
[*]进阶题目
[*]函数
[*]函数的注释
[*]函数的传递
[*]值传递
[*]地址传递
[*]Static关键字
[*]重要!!!Linux内存分布
[*](1) 保留区
[*](2) 代码段
[*](3) 数据段
[*](4) 堆空间
[*](5) 栈空间
[*](6) 内核区域
[*]重要!!!时间换空间,空间换间
[*]Const关键字
[*]修饰指针
[*]指针常量
[*]常量指针
[*]结构体struct
[*]结构体的访问
[*]重要!!!字节对齐(空间换时间)
[*]预处理指令 #pragma
[*]typedef关键字
[*]联合体union
[*]联合体用来判断机器的大小段问题
[*]枚举enum
[*]宏定义
[*]#define
[*](1)无参数的宏定义
[*](2)带参数的宏定义
[*](3)无替换列表的宏
[*]#if
[*]#ifdef
[*]#if和 #elif
[*]#ifndef和#endif
[*]编译过程
[*]预处理:
[*]编译:
[*]汇编:
[*]链接:
C语言做题易错及知识点总结
[*]int main(int argc, char *argv[]) {}其中argc代表了命令行参数的数量 argv[]代表一个字符串数值 存储所有命令行参数的具体值.
[*]头文件的包含方式有两种:
< xxx.h > :编译器只去系统指定路径中查找该头文件,如果未找到,则编译器进行错误提示。
“ xxx.h ” :先去当前路径中查找头文件,未找到再去系统路径中查找,还未找到则报错提示。
重要!!!负数在计算机中的存储:
计算机用补码存负数,规则是:
正数:直接存二进制。
比如 5 → 0000 0101(8 位)。
负数:绝对值取反加 1。(也可以直接反码+1)
比如 -5:
先写 5 的二进制:0000 0101
每一位取反:1111 1010
加 1:1111 1011
→ -5 存成 1111 1011(补码)。
2. 右移时为什么补符号位(比如补 1)?
右移操作分两种:
1.无符号数:右移补 0(逻辑右移)。 比如 0010 1000 → 右移 2 位 → 0000 1010(相当于除以 4)。
2.有符号数:右移补符号位(算术右移)。比如 -5 的补码是 1111 1011,右移 1 位:
补符号位 1 → 1111 1101(还是负数)。如果补 0 → 0111 1101(变成正数,错误!)。
补符号位的目的:
保证右移后还是负数,并且数值正确。比如 -5 ÷ 2 = -3,右移后 1111 1101 刚好是 -3 的补码。
3. 原题例子:-2147483648 >> 31 为啥是 -1?
-2147483648 的 32 位补码:1000 0000 ... 0000(最高位是 1,表示负数)。
右移 31 位:
每次右移补符号位 1 → 最终全变成 1 → 1111 1111 ... 1111。
而 1111...1111 是 -1 的补码(取反加 1 验证:0000...0001 → -1)。
总结:补符号位是为了让负数右移后仍然是负数,并且结果符合数学规律(除以 2)。
一句话解释;
右移时补符号位(比如补 1),是为了让负数在右移后还是负数,并且数值变化和数学里的除法一致。如果补 0,负数会变成正数,结果就错了。
[*]总结:只需要记住有符号的数在右移时补符号位的数。
[*]注意:计算机处理数据的基本单位是byte,计算机处理数据的最小单位是bit,1byte = 8bit。
[*]具体如何计算存储单元地址大小:一个地址是8位16进制=32位2进制=4字节 (一般来说大小都是换算到二进制来看的)
[*]变量和常量也被称为*标识符*,对于标识符的命名必须遵循规则:标识符是由字母、数字、下划线以及美元符号$组成,并且标识符的第一个字符只能是字母或者下划线。
重要!!!计算机内部的存储
计算机内部是以二进制的补码方式来存储数据的,所以需要把二进制数的原码转换为补码形式。一定是先转化在储存
练习:用户打算定义一个字符型变量,命名叫做data,准备把一个正整数存储到变量data中,并设计了一段程序,请问这段程序输出的结果是多少?
char data; data = 129; printf(“%d\n”,data);--> 请问输出的结果是多少??答案:-127
0 1000 0001(129)–> 1000 0001 原码 因为只有8个字节 只能截断 错误的因为这是常量有4个字节 并且遵循先转(转是说转换位补码存放)再放的原则 而上面则是先放再转(截断就是因为先放进去的 只有8个字节 所以才会截断)
正确处理是:
这里的0000 0000 0000 0000 0000 0000 1000 0001这里是补码 因为129为正数原码=补码
注意!!这种是有符号且超过范围的情况
[*]特殊情况128
练习:用户打算定义一个字符型变量,命名叫做data,准备把一个正整数存储到变量data中,并设计了一段程序,请问这段程序输出的结果是多少?
char data; data = 128; printf(“%d\n”,data);--> 请问输出的结果是多少??答案:-128
128--0000 0000 0000 0000 0000 0000 1000 0000原码--正数的原码和补码是相同的 1000 0000
1000 0000补码 -- 属于数据类型中非常特殊的存在,可以计算是 -0,但是0不分正负,所以 把-0的位置用-128进行替换。
[*]变量范围
字符型: char1字节 -- 有符号 -- 数值范围 -128 ~ 127--无符号 -- 数值范围 0 ~ 255
短整型:short2字节 -- 有符号 -- 数值范围 -32768 ~ 32767 无符号 --数值范围 0 ~ 65535
[*]man手册的使用
[*] man手册:man -f function
如果想要针对性查找某些接口,可以指定man手册章节,终端输入命令: mann(1-9)xxx
重要!!!格式化输出
[*]注意:printf的返回值是输出字符的数量(不包括”\0”),发生错误的时候返回的是一个负整数
(1) 标志字符 (必须放在%说明符后面)
[*]#0则可以把八进制的前导符输出
#x则可以把十六进制的前导符输出 如果只是写printf(“%xd”)则不显示16进制的前导符0x
- 用于左对齐 c语言中默认的是右对齐。
(2) 转换精度
转换精度是可选的 使用. 来表示 .的后面紧跟一个十进制的数 如果只有. 则表示精度为0
(3)长度修饰
注意:计算机内部存储多字节的数据时会涉及到大端小端,不同的处理器架构采用的模式是不同的,一般X86架构采用小端模式,ARM架构一般采用大端模式(但是并不绝对)!!!!!!
(4) 转换说明
十进制 decimal-constant
八进制 octal-constant
十六进制 hexadecimal--constant
o u x 是把无符号的整数转换成八进制,十进制,十六进制来输出
c 是以字符的形式输出
s 指针指向一个字符数组,指针指向字符穿的首地址,以字符串形式输出
p 可以把存储单元编号以16进制形式输出
% 想输出 % 需要使用`%%`
[*]转义字符的输出需要加\
1. 用双反斜杠 \ 输出单个反斜杠
在 C 语言中,\\ 会被解析为一个普通的反斜杠字符。因此:
printf("\\n");// 输出:\n(而非换行)
printf("\\t");// 输出:\t(而非制表符)2. 完整示例代码
下面的程序展示了如何将常见转义字符作为普通文本输出:
#include <stdio.h>
int main() {
printf("将转义字符作为普通字符输出:\n\n");
printf("\\n 表示换行符\n"); // 输出:\n 表示换行符
printf("\\t 表示制表符\n"); // 输出:\t 表示制表符
printf("\\\" 表示双引号\n"); // 输出:\" 表示双引号
printf("\\\' 表示单引号\n"); // 输出:\' 表示单引号
printf("\\\\ 表示反斜杠本身\n");// 输出:\\ 表示反斜杠本身
printf("\\a 表示响铃符\n"); // 输出:\a 表示响铃符
printf("\\b 表示退格符\n"); // 输出:\b 表示退格符
printf("\\r 表示回车符\n"); // 输出:\r 表示回车符
printf("\\v 表示垂直制表符\n");// 输出:\v 表示垂直制表符
return 0;
}格式化输入
(1)匹配字符[]
需要匹配的字符用[]括起来 如果是[^]则表示不匹配括号中的字符。
重要!!!运算符
[*]运算符的优先级 很重要
运算符的优先级:基本表达式 > 单目运算符(++ --) >算术运算符(+ -* /) > 按位移位运算符 (>> <<) > 关系运算符 (> < == !=)>位操作运算符(~ & | ^) > 逻辑运算符 (&& || !)> 条件运算符(三目运算符) > 赋值运算符(= +=) >逗号运算符注意:C语言中的单目运算符 ++ -- 和三目运算符都是遵循右结合性,当然也包含双目运算符中的赋值运算符=,其他的运算符都遵循左结合性。
思考:前缀增量和后缀增量都属于单目运算符,如果一个表达式中同时出现两种运算符,那应该如何进行解释?比如 表达式 ++i++ 如何解释?
回答:++(i++)遵循右结合性 但是这个写法是有问题的
错误原因
表达式展开:依据运算符优先级,++i++ 会被解析成 ++(i++)。
左值要求:前缀自增(++)运算要求操作数必须是可修改的左值,也就是能放在赋值语句左边的变量。
后缀自增的返回值:后缀自增运算返回的是变量未自增之前的值,这是一个临时的右值,并非可修改的左值。
编译错误:++(i++) 这种形式是不合法的,因为它试图对一个右值进行前缀自增操作。1. 左值的定义
[*]左值(lvalue):是一个表示内存中特定位置的表达式,它可以出现在赋值语句的左侧(即可以被赋值)。
[*]右值(rvalue):是一个临时的值或表达式结果,只能出现在赋值语句的右侧。
总结
特性左值(lvalue)右值(rvalue)能否被赋值能(大多数情况)不能能否取地址能(&a 合法)不能(&(a+1) 非法)示例变量、数组元素、结构体成员字面量、表达式结果、函数返回值注意:``sizeof在C语言中是一个操作符,作用是计算数据类型的大小,结果以字节为单位,sizeof`括号中的表达式是不会实现运算和处理的。
(1)位操作运算符
~:按位取反,对于二进制数而言,0变为1,1变为0~ 1101_1110 = 0010_0001
&:按位与,对于二进制而言,当两个bit同时为1,则结果为1,如果bit存在0,结果为0
|:按位或,对于二进制而言,当两个bit同时为0,则结果为0,如果bit存在1,结果为1
^:按位异或,对于二进制而言,当两个bit相同,则结果为0,两个bit不同,则结果为1
<<:左移运算符,对于二进制而言原则:高位舍弃、低位补0 0111_1010 << 3 -- 1101 0000
>>:右移运算符,对于二进制而言原则:低位舍弃、高位补0 0111_1010 >> 3 -- 0000 1111
注意带符号的数右移动是要补齐符号位的(2)逻辑运算符
无符号和有符号整数进行运算时,有符号整数会被提升为无符号整数
&&具有短路现象当前面一个为假的时候后面的操作符就算是个运算表达式则不会进行计算
||具有短路现象 当前面一个为真的时候后面的操作符就算是个运算表达式则不会进行计算
! (3)强制类型转换需要直接截断 不会四舍五入。
最重要!!!置位复位操作
各种语句
Switch语句注意事项
[*]case以及default只能用于switch语句
[*]注意switch( )括号中可以是枚举类型 字符类型 整数类型.
break与continue区别
[*]break可以用于``switch`语句或者循环语句
for()循环语句
for(;;;)代表一直循环等价于while(1)
数组
结论: *(E1+E2)==E1
*( E1 + E2 )的解释:E1是一个数组对象,E1也就是数组名称,C语言标准中规定数组名可以作为数组中第一个元素的地址,所以相当于E1是数组中第一个元素的地址,而E2是一个整数,所以 E1 + E2 相当于从E1这个地址向后偏移E2个单位(以元素为单位,所以需要考虑元素的类型),所以E1+E2的结果还是一个地址, *( E1 + E2) 相当于间接访问该地址,相当于得到了(E1+E2)这个地址下的值。 总结: *(E1+E2)==E1
数组的两种例外情况
[*](1) 当数组名 和 &地址运算符一起使用时,数组名就不表示数组首元素的地址,而表示数组本身
练习
[*](2) 当 数组名 和 sizeof()运算符 单独使用的时候,数组名就不表示数组首元素地址,而表示数组本身。
练习
在64bit系统下
字符数组
思考:用户定义了一个字符数组 char buf; 用户想要把一个字符序列abcde这5个字符存储到字符数组中,提供两种方案: char buf = “abcde”;char buf ={‘a’,’b’,’c’,’d’,’e’}; 请问两种方案有什么区别?
回答:如果数组的容量刚好和字符串常量中的有效字符的数量一致时,就会导致数组越界,因为字符串常量的末尾有一个转义字符’\0’,也是需要占用1个字节的存储单元。(可能会越界段错误)
二维数组
注意:不管是几维数组,数组的定义规则:元素类型+数组名[元素数量]比如 int buf 就是找到数组名以及数组名后面的这个方括号 方括号里面有元素数量之外剩下的都是元素类型!!!
二维数组的访问
[*]下标访问
int buf; 则如果打算访问buf,
[*]地址访问
int buf; 则如果打算访问 buf ==>* ( ( *(buf + 1) ) + 1 )
练习
PS:
[*]代表数组a的长度
[*]数组中某个元素
[*]a有三个匿名数组 代表大数组里第一个小数组的地址 (最开始是第0个数组) 可以当做小数组的名字
[*]这个不是sizeof单独与数组名使用 则代表了地址
匿名数组
C99标准中支持匿名数组,但是匿名数组一般都是在函数参数中或者在多维数组中使用,很少单独使用。
比如二维数组 int buf--->buf 数组的每个元素的类型是 int ,就是匿名数组。 有三个匿名数组
零长数组
int buf 就是数组的长度可以是0,但是由于数组长度是0,所以操作系统是不会提供内存单元给数组的!
段错误产生的原因
1.段错误就是访问了不可访问的内存,出现了运行时出现了segmentation fault的报错
2.产生的原因:访问不存在的内存地址、访问系统保护的内存地址 、访问只读的内存地址、空指针废弃(eg:malloc与free释放后,继续使用)、堆栈溢出、内存越界(数组越界,变量类型不一致等)
重要!!!指针
指针表示的含义
int * p p表示一个指针变量,存储地址,该地址在存储的数据的类型是int型
int ** p 表示一个二级指针变量,存储的是一个指向 int 类型的一级指针的地址
[*]*p:表示取出 p 存储的地址处的值,即得到一个 int * 类型的一级指针。
[*]**p:对 *p 再解引用一次,得到该一级指针指向的 int 值。
注意:对于指针变量的偏移,要考虑到变量中存储的地址的数据类型,所以 地址 + 1 不表示存储单元向后偏移1个字节,应该是向后偏移 (1 * 数据类型)个字节。
[*]注意
指针数组int *a
数组指针int (*a)
指针函数int* func(int a, int b)
函数指针int (*a)(int)---->一个有 10 个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
函数指针数组 int(*a)(char)二维指针的含义
二级指针定义格式: intdata; //整型变量int*p1 = &data; //指针变量int **p2 = &p1;
进阶题目
练习:用户现在定义一个int buf = {1,2,3,4,5}; 现在用户定义一个数组指针来存储数组的地址, int*p = buf; 请问 printf(“%d\n”,*p++); printf(“%d\n”,(*p)++);请问两句话的输出结果?
1 //p++ 后缀表达式
2
函数
注意:c语言不支持函数的嵌套定义,但是可以嵌套调用 。
在函数设计的时候应该满足低耦合 高内聚
耦合:指的是各个函数之间的互相影响。
内聚:指的是各部分的紧密程度,越高越好。
函数的注释
/******************************************************
*file name:demo.c
*author :cecelmx@126.com
*date :2024/04/02
*function :设计函数,实现传递两个整数并输出其中较大的数
*note :None
*
*CopyRight (c) 2023 - 2024cecelmx@126.comAll Right Reseverd
*******************************************************/
***上面的是整个文件的注释,下面的是函数的注释***
/******************************************************
*Function Name:func
*Function:Output the larger of two integers
*Function Parameters:
* @a :
* @b :
*Return Value:
*Notes:None
*Author:cecelmx@126.com
*Creation Date:2024/04/02
*Modification History:
*Version:V1.0
*******************************************************/ 定义函数的时候函数参数列表中的形参是不占内存的,只是为了提醒用户参数的数量和类型!
函数的传递
值传递
[*]值传递都是单向的 注意看下面的代码
*** 值传递 ***
void f(double *p) {
p = (double *) malloc(10 * sizeof(double));// 错误:只修改了副本p
}
// 主函数
int main() {
double *ptr = NULL;
f(ptr);// 传递ptr的值(NULL)给p
// 此时ptr仍然是NULL,因为函数内部只修改了副本p
}
*** 地址传递 ***
void f(double **p) {
*p = (double *) malloc(10 * sizeof(double));// 正确:修改了p指向的地址(即ptr)
}
// 主函数
int main() {
double *ptr = NULL;
f(&ptr);// 传递ptr的地址(double**类型)
// 此时ptr指向了新分配的内存
}地址传递
二维数组的传递
Static关键字
如果函数中定义的局部变量使用static关键字进行修饰,则系统会从全局数据区分配内存空间给该局部变量,全局数据区的生命周期是跟随程序的,不会因为函数结束而释放。
注意:一个函数中的static修饰的变量在多次调用时,其值不是最开始初始化的值,其会保留上一次调用结束的值。
重要!!!Linux内存分布
如果采用的是32bit的linux系统,则每个运行的程序都会得到4G大小的内存空间,只不过每个程序得到的4G大小的内存都是虚拟内存,而物理内存才只有4G,物理内存是真实存在的,而虚拟内存是通过映射得到的。
虚拟内存是由物理内存映射而来,所以都需要计算机中的重要部件:MMU内存管理单元!
(1) 保留区
[*]保留区的地址范围是0x0000_0000 ~ 0x0804_8000 大小是128M
[*]宏定义NULL其实就是指向0x0000_0000
(2) 代码段
[*].text段 用于存储用户程序生成的指令
[*].init段 用于存储系统初始化的指令
嵌入式两个处理器架构:冯诺依曼架构;哈弗架构
[*]冯诺依曼架构:将程序指令存储器和数据存储器合并在一起的架构。
[*]哈弗架构 :将程序指令存储器和数据存储器合分开的架构。
(3) 数据段
[*].rodata段:被称为只读常量区,程序中的常量(整型常量、字符串常量)都是存储在该区域,对于该区域的属性是只读的,当程序结束后该区域的内存会被释放。
[*] .data段:用于存储程序中的已经被初始化的全局变量和已经被初始化的静态局部变量,另外注意初始化的值不能为0!
[*].bss段:用于存储程序中未被初始化的全局变量以及未被初始化的静态局部变量以及初始化为0的全局变量和初始化为0的静态局部变量。
(4) 堆空间
[*]堆空间是匿名内存,只能通过指针访问,一般都需要手动进行申请。
(1) malloc()
利用malloc申请堆内存,则申请成功的堆内存是未被初始化的,所以用户应该对申请的堆内存进行初始化,同样,malloc只需要一个参数,该参数指的是需要申请的堆内存的大小,以字节为单位。
函数的返回值是申请成功的堆内存的首地址,但是该地址的类型是void*,则用户应该对该地址进行强制转换,如果申请失败,则函数返回NULL,所以用户应该进行错误处理!
(2) calloc()
calloc函数可以申请堆内存,calloc有两个参数,第一个参数是要申请的内存块的数量,第二个参数是内存块的大小,所以申请的内存的总大小 = 内存块数量 * 内存块大小,相当于是数组结构。
记得使用之后进行free操作 不然会导致内存泄漏。
(5) 栈空间
大小为8M向下增长
栈空间主要用于存储程序的命令行参数、局部变量、函数的参数值、函数的返回地址
使用ulimit命令来查看修改栈空间的大小 ,但是是临时性的,只针对当前终端。
(6) 内核区域
略
重要!!!时间换空间,空间换间
*时间效率*:时间效率被称为时间复杂度,指的是算法执行过程耗费的时间。
*空间效率*:空间效率被称为空间复杂度,指的是算法执行过程所耗费的最大的存储空间。
*用空间换时间*: 当内存空间充足的时候,如果我们更加追求代码的执行速度,就可以选择空间复杂度相对较高、但时间复杂度相对很低的算法或者数据结构。
案例:1.循环队列满的情况 空一格然后进行(下标+1)%容量=队头2.结构体对齐
*用时间换空间*:当内存比较紧缺时,例如代码跑在手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路。
Const关键字
举例: int data = 10; data = 20; //变量是可读可写的
constintdata;//只读变量修饰指针
指针常量
(1) int *const p;
可以看到const离变量名称更近,const是修饰变量p的,而变量p是一个指针变量,变量p用于存储一个地址,但是变量p本身也可以得到存储单元,相当于降低了变量p的存储单元的属性,也就是变量p中存储的地址就不能发生变化,所以这个指针变量就称为指针常量。
常量指针
(2)const int *p;
可以看到const关键字离指针变量p指向的地址下的数据类型更近,所以const是用于修饰指针变量p指向的地址的,所以也可以写成 int const *p;变量p的存储单元是没有受到影响的,而是变量p中存储的地址的权限降低为只读了,可以为变量p是美杜莎之眼,被称为常量指针。
注意:只有通过指针变量p来间接访问地址下的值时,才会出现错误,因为变量p中存储的地址被变量p影响了,权限降低为只读了,但是只要不通过变量p来间接访问,则就不会受到变量p的影响。
结构体struct
[*]结构体中不应该包含不完整的类型以及函数类型。但是可以包含指向该结构体的指针
[*]定义格式:
struct结构体名称
{
数据类型成员名称;
数据类型成员名称;
数据类型成员名称;
....
}; //注意:复合语句后的分号不可以省略注意:构造的结构体类型是不占内存的,只有使用该类型创建的变量才会得到内存空间!!
结构体的访问
重要!!!字节对齐(空间换时间)
32位移4字节对齐,64位8字节对齐。
验证技巧:计算出的结构体的大小应该是结构体类型中字节宽度最大的成员的整数倍!!!!!
预处理指令 #pragma
[*]#pragmapack(n)可以用于进行字节对齐以及取消字节对齐,n的值可以是1、2、4、8.......
[*]#pragmapack(1)是取消字节对齐 按照原本所占字节大小来计算
typedef关键字
typedef关键字对数据类型起别名,应该是针对整个程序而言,所以不应该在某个复合语句内进行声明。
使用格式: typedef数据类型别名; 举例 : typedef unsigned intuint32_t;
潜规则:如果打算把某个数据类型起一个新的别名,在使用typedef的时候,新的别名应该在命名的时候末尾添加 _t
举例 : typedef unsigned intuint32_t;//为了提高可读性,用户可以知道这个是别名!!
联合体union
定义格式:
union联合体名称
{
数据类型成员名称;
数据类型成员名称;
数据类型成员名称;
....
}; //注意:复合语句后的分号不可以省略
[*]注意:联合体变量中的成员由于是共用一块内存,所以每个成员的起始地址都是相同的,只要修改联合体中任何一个成员的值,都会影响其他成员的值,另外,也不应该同时对联合体中的多个成员进行赋值。
联合体用来判断机器的大小段问题
+++
枚举enum
[*]枚举就是把一些没有意义的数字(整数)起一个有意义的名称,利用该名称就相当使用该整数常量,是为了提高程序可读性!!
注意:枚举列表中第一个元素未被初始化,则默认是0,下一个元素的值等于前一个+1.
宏定义
注意:1.宏定义是单纯的文本替换,不检查语法是否合法。
2.一般实际开发中宏名称都采用大写(潜规则)。
3.宏不是语句,在末尾并不需要加分号,如果添加分号,则分号也会被一起替换。
Compile -------编译
Assemble ----- 汇编
Link-----------链接
Identifer------标识符
macro ---------宏#define
定义格式:#define宏名称(大写)替换列表 换行(一般就是用户按下回车)
(1)无参数的宏定义
__DATE__ __FILE__ __LINE__ __TIME__
(2)带参数的宏定义
略
(3)无替换列表的宏
[*]C语言中也允许只定义一个宏,这个宏可以没有替换列表,一般实际开发中都是对程序进行条件编译的情况下来使用
#if
#if用于判断常量表达式是否成立,遵循“非0即真”原则,#if预处理指令作为条件编译
一般#if和#endif是结合一起使用的,经常用于程序中的调试,可以选择保留或注释代码块,如果是真则保留,如果是假(0)则注释!
+++
#ifdef
#ifdef 用于判断宏是否被定义,如果宏是提前定义好的,则(#ifdef 与#endif之间的语句是有效的)该预处理指令是有效的,也需要和#endif一起使用
+++
#if和 #elif
#if和#elif和#else和#endif 用于条件编译,可以通过常量表达式的多种状态来选择保留或者删除某些代码块
+++
#ifndef和#endif
#ifndef和#endif 用于判断宏是否未定义,如果宏定义,则该代码块会被删除,如果宏未被定义,则该代码块可以保留(这个跟#ifdef正好相反 那个是定义则该预处理指令是有效的)
编译过程
思考:什么叫做预处理阶段?预处理阶段和编译阶段有什么不同?源文件转换为可执行文件一共需要经历几个阶段?
预处理:
对源码进行简单的加工,GCC编译器会调用预处理器cpp对程序进行预处理,其实就是解释源程序中所有的预处理指令,如#include(文件包含)、#define(宏定义)、#if(条件编译)等以#号开头的预处理语句。
这些预处理指令将会在预处理阶段被解释掉,如会把被包含的文件拷贝进来,覆盖掉原来的#include语句,把所有的宏定义展开,所有的条件编译语句被执行,GCC还会把所有的注释删掉,添加必要的调试信息。
预处理指令: gcc-Exxx.c-oxxx.i 会生成预处理文件xxx.i
编译:
就是对经过预处理之后的.i文件进行进一步翻译,也就是对语法、词法的分析,最终生成对应硬件平台的汇编文件,具体生成什么平台的汇编文件取决于编译器,比如X86平台使用gcc编译器,而ARM平台使用交叉编译工具arm-linux-gcc。
编译指令: gcc-Sxxx.i-oxxx.s 会生成汇编文件xxx.s
汇编:
GCC编译器会调用汇编器as将汇编文件翻译成可重定位文件,其实就是把.s文件的汇编代码翻译为相应的指令。
编译指令: gcc-cxxx.s-oxxx.o 会生成目标文件xxx.o
链接:
经过汇编步骤后生成的.o文件其实是ELF格式的可重定位文件,虽然已经生成了指令流,但是需要重定位函数地址等,所以需要链接系统提供的标准C库和其他的gcc基本库文件等,并且还要把其他的.o文件一起进行链接。-lc-lgcc是默认的,可以省略
编译指令:gcchello.o-ohello-lc-lgcc会生成可执行文件xxx// l是lib的缩写
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]