该文章Github地址:https://github.com/AntonyCheng/c-notes
在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.csdn.net/AntonyCheng/article/details/136555245),该模板集成了最常见的开发组件,同时基于修改配置文件实现组件的装载,除了这些,模板中还有非常丰富的整合示例,同时单体架构也非常适合SpringBoot框架入门,如果觉得有意义或者有帮助,欢迎Star & Issues & PR!
上一章:由浅到深认识C语言(6)
6.预处理
6.1.C语言编译过程
-
预处理:头文件包含、宏替换、条件编译、删除注释,不做语法检查;
#include<stdio.h>
-
编译:将预处理后的文件生成汇编文件,并且进行语法检查;
-
汇编:将汇编文件编译成二进制文件;
-
链接:将“众多的二进制文件+库函数+启动代码”生成可执行文件(.exe等);
6.2.头文件包含
<>
用于包含系统的头文件
#include<hello.h>
上面代码表示在系统指定的目录下寻找 hello.h
;
""
用于包含用户自定义的头文件
#include"hello.h"
上面代码表示先从源文件所在的目录寻找 hello.h
,如果找不到再到系统指定的目录下寻找;
include
只能包含头文件.h
,不要去包含源文件.c
;
6.3.define 宏
定义:在源代码中允许一个标识符(宏名)来表示一个语言符号的字符串,用特定的符号来代表指定的信息;
优点:提高代码的可读性和移植性;
#include<stdio.h>
#define N 10
static void test() {int i = 0;for (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);}return;
}
int main(int argc, char *argv[]) {test();return 0;
}
打印效果如下:
过程:上例的过程是将代码中的 10
转变成了 N
,如果需要修改 10
这一个数值的话,仅需改变宏,该过程被称之为“宏展开”;
注意:宏的后方不能加分号,因为加了分号,编译器就会把分号给编进宏内,宏一般为大写,以便和普通变量区分开来;
分类:
-
无参数的宏:
#define 宏名 代替内容
例如:#define PI 3.14f
例如:
#include<stdio.h> #define N 10 static void test() {int i = 0;for (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);} #undef Nfor (i = 0; i < N; i++) {printf("%d ", i);}for (i = 0; i < N; i++) {printf("%d ", i);}return; } int main(int argc, char *argv[]) {test();return 0; }
注意:
- 宏只在当前源文件有效,当需要终止宏的作用范围时需要
#undef 宏名
,例如上; - 这种方法能使用户能以一个简单的名字代替一个长的内容(数字,字符,字符串)
- 宏只在当前源文件有效,当需要终止宏的作用范围时需要
-
带参数的宏(宏函数):
#define 宏名(形参表) 被替代内容;
例如:#define N(a,b) a+b;
注意:参数表中的参数不能写类型,即 N(int a,int b) 是错的;
例如:
#define MY_ADD(a,b) a+b //调用宏 MY_ADD(10,20); //这里等价于 10 + 20;
注意:千万不要擅自加括号,比如求
MY_ADD(10+10,20+20);
那么求得的过程就是10+10*20+20
案例一:两数相加;
#include<stdio.h> #define MY_ADD(a,b) a+b static void test() {int num = 0;num = MY_ADD(10 , 20);printf("num = %d\n", num);return; } int main(int argc, char *argv[]) {test();return 0; }
打印效果如下:
案例二:两数相乘;
#include<stdio.h> #define MY_MUL(a,b) a*b static void test() {int num = 0;num = MY_ADD(10 , 20);printf("num = %d\n", num);return; } int main(int argc, char *argv[]) {test();return 0; }
打印效果如下:
-
宏函数和普通函数的区别:
宏函数调用多少次,就会宏展开多少次,执行代码的时候没有函数调用的过程,也不需要函数的出入栈,所以带参数的宏会浪费空间,节省了时间;
普通函数代码只有一份,存在代码段,调用的时候去代码段读取函数指令,调用的时候要压栈(保存调用函数前的相关信息),调用完之后会出栈(恢复调用前的相关信息),所以函数浪费了时间,节省了空间;
-
案例:求出下例
printf
;#include<stdio.h> #define MY_MUL(a,b) a*b #define MY_ADD(a,b) a+b static void test() {printf("%d\n", MY_MUL(MY_ADD(10 + 10, 20 + 20), MY_MUL(10 + 10, 20 + 20)));//列算式出来就是 10 + 10 + 20 + 20 * 10 + 10 * 20 +20 = 460return; } int main(int argc, char *argv[]) {test();return 0; }
打印效果如下:
6.4.条件编译
一般情况下,源程序中所有行都参加编译,但有时希望对部分源程序行只在满足一定条件时才编译,即对这部分源程序行指定编译条件。
注意:以下案例在 Linux
下测试;
案例一:测试不存在;
测试预处理结果如下:
案例二:测试存在;
测试预处理结果如下:
案例三:判断表达式;
综合案例:通过条件编译来控制大小写的转换;
#include<stdio.h>
int main(int argc, char *argv[]) {char str[128] = { "" };int i = 0;printf("请输入一个字符串:");//fgets 会获取换行符“\n”fgets(str, sizeof(str), stdin);//去掉换行符,strlen返回的是字符串的实际长度,不包括“\0”str[strlen(str) - 1] = 0; //strlen[str] 是换行符的下标位置
#if 1//大写变小写while (str[i] != '\0') {if (str[i] >= 'A' && str[i] <= 'Z') {str[i] = str[i] + 32;}i++;}
#else//小写变大写while (str[i] != '\0') {if (str[i] >= 'a' && str[i] <= 'z') {str[i] -= 32;}i++;}
#endifprintf("str = %s\n", str);return 0;
}
6.5.防止头文件重复包含
-
方法一:编译器层面
#pragma once
;在所有头文件上方写上
#pragma once
,例如下:#pragma once #include"a.h" #include"b.h" #include"c.h"
-
方法二:C++层面
#ifndef 宏……#define 宏……头文件具体内容……#endif
;例如下:
main.c
#include<stdio.h> #include"a.h" #include "b.h" int main(int argc,char *argv[]){printf("num = %d\n",num);return 0; }
a.h
#ifndef __A_H__ #define __A_H__ #include "b.h" #endif
b.h
#ifndef __B_H__ #define __B_H__ int num = 10; #endif
-
总结:
#pragma once
编译器决定,强调的文件名;#ifndef
是C/C++标准制定,强调的是宏而不是文件;
7.二进制 原/反/补码
无符号数和正数 | 负数 | ||||
---|---|---|---|---|---|
概念 | 10 | 概念 | -10 | ||
原码 | 数据的二进制形式 | 0000 1010 | 原码 | 数据的二进制形式 | 1000 1010 |
反码 | 和原码相同 | 0000 1010 | 反码 | 原码的符号位不变,其他取反 | 1111 0101 |
补码 | 和原码相同 | 0000 1010 | 补码 | 反码 + 1 | 1111 0110 |
注意:
- 无符号数和正数——原码==反码==补码
- 负数——
- 反码==原码的符号位不变,其他位取反
- 补码=反码 + 1
- 任何数据在计算机中都是以补码形式存储
计算机为什么要补码?
补码将减法运算转换成加法运算:
计算下式:
6 - 10 == -4
6 + (-10) == -4
如果没有补码:0000 0110
+ 1000 1010
-------------1001 0000 == -16(错误)
如果有补码:0000 0110
+ 1111 0110
-------------1111 1100 --> 1000 0011 --> 1000 0100 == -4
补码统一了零的编码:
有符号数:1111 1111 ~ 1000 0000 ~ 0000 0000 ~ 0111 1111-127 ~ -0 ~ +0 ~ +127计算机为了扩大数据的表示范围,故意将 -0 看成了 -128所以范围是 -128 ~ 127
无符号数:0000 0000 ~ 1111 1111范围是 0 ~ 255
即:
+0 == 0000 0000(原) == 0000 0000(反) == 0000 0000(补)
-0 == 1000 0000(原) == 1111 1111(反) == 0000 0000(补)
补码意义:将减法运算变加法运算,同时统一了零的编码。