目录
typedef与define的区别
define:执行于预处理阶段,只进行简单的字符替换,没有作用域限制,可以对类型/变量/常量等进行替换。
用法: #define 被替换字符 替换字符
typedef:执行于编译阶段,对已经存在的数据类型取别名。只能在定义的作用域内使用,会进行类型检查。
用法: typedef 被替换的数据类型 替换字符
实例一
#define char* pstr;
则
const pstr => const char* //指针指向一个常量,可以更改指向的地址
typedef char* pstr;
则
const pstr => char* const //不能更改指向的地址(常量指针)
实例二:作用域#define没有作用域限制,typedef只能用在作用域
//正确使用
typedef int size
void main(){
size a = 3;
function ();
}
void function (){
size a = 3;
}
-------------------------------------------------------------------------------
//错误使用
int main(){
typedef int size;
size a = 3; //等同于int a = 3;
function();
}
int function(){
size a = 3; //报错
}
static、const、volatile的区别和作用
🍨static:定义静态变量或函数
1️⃣:在函数中声明变量时, static 关键字指定变量只初始化一次,并在之后调用该函数时保留其状态,此外相较于普通变量,静态变量在函数执行结束后并不会被销毁,而普通变量则相反,根据此条特性静态变量可以作为函数的计数器,记录函数被调用的次数。
2️⃣:在声明变量时,变量具有静态持续时间,并且除非您指定另一个值。
3️⃣ :在全局和/或命名空间范围 (在单个文件范围内声明变量或函数时) static 关键字指定变量或函数为内部链接,即外部文件无法引用该变量或函数,根据此条特性利用好static可以防止其他文件的变量或函数的命名重复而出现报错,增加了代码封装性,移植性。
4️⃣:static 关键字 没有赋值时,默认赋值为 05️⃣:static修饰局部变量时,会改变局部变量的存储位置,从而使得局部变量的生命周期变长。
6️⃣:和全局变量有些相似都是存储在静态数据区,但作用域不一样
用法:
static 数据类型 变量名\函数名()
static int a;
static void fun(){};
🍨 const:常用于定义常量
1️⃣:可以保护被修饰的东西,防止意外修改,增强程序的健壮性
2️⃣:编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高
3️⃣:用const修饰变量时,一定要给变量初始化,否则之后就不能再进行赋值了。
4️⃣:修饰函数的参数可以防止修改指针指向的内容、修改指针指向的地址、指针p1和指针p2指向的地址都不能修改
用法:
1️⃣定义常量:const int a 或者 int const a 两者意思一样
2️⃣指针常量: 指的是该指针自身是常量,指向的地址不可改变(int * const a)
3️⃣常量指针:指的是该指针指向的地址里面存放的数据是一个常量(const int *a)
4️⃣指向常量的常指针:是以上两种的结合,指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。
5️⃣修饰函数的参数:防止传进去修改指针指向的内容
void StringCopy(char *strDestination, const char *strSource);
其中 strSource 是输入参数,strDestination 是输出参数。给 strSource 加上 const 修饰后,如果函数体内的语句试图改动 strSource 的内容,编译器将指出错误。
6️⃣修饰全局变量:全局变量的作用域是整个文件,我们应该尽量避免使用全局变量,因为一旦有一个函数改变了全局变量的值,它也会影响到其他引用这个变量的函数,导致除了bug后很难发现,如果一定要用全局变量,我们应该尽量的使用const修饰符进行修饰,这样防止不必要的人为修改,使用的方法与局部变量是相同的。
🍨 volatile:是一种类型修饰符
遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存
#include <stdio.h>
void main()
{
volatile int i = 10;
int a = i;
printf("i = %d", a);
__asm {
mov dword ptr [ebp-4], 20h
}
int b = i;
printf("i = %d", b);
}
结果为
i = 10
i = 32
函数指针
函数指针是 C 语言中的一种强大特性,用于存储指向函数的地址,从而可以通过指针来调用函数。这使得可以实现回调函数、函数的动态选择等功能。以下是函数指针的基本概念和用法介绍:
🍨什么是函数指针?
函数指针是一个指针变量,它存储的是一个函数的地址。通过函数指针,我们可以间接地调用函数、将函数作为参数传递,以及实现更灵活的程序结构。
🍨如何声明和定义函数指针
要声明一个函数指针,首先要知道所指向的函数的返回类型和参数类型。以下是一个典型的函数指针声明示例:
// 声明一个函数指针类型,指向返回类型为 int,参数为两个 int 的函数
typedef int (*func_ptr_t)(int, int);
🍨示例
以下是一个完整的示例,演示了如何使用函数指针。
#include <stdio.h>
// 定义几个简单的函数
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
// 定义函数指针类型
typedef int (*operation_t)(int, int);
// 函数接受一个函数指针作为参数
void perform_operation(operation_t op, int x, int y) {
printf("Result: %d\n", op(x, y)); // 通过函数指针调用函数
}
int main() {
// 定义函数指针
operation_t op;
// 将指针指向 add 函数
op = add;
perform_operation(op, 5, 3); // 输出: Result: 8
// 将指针指向 subtract 函数
op = subtract;
perform_operation(op, 5, 3); // 输出: Result: 2
return 0;
}
🍨解释
-
🍉函数定义:定义了两个函数
add
和subtract
,它们都接受两个int
参数,并返回一个int
结果。 -
🍉函数指针类型:使用
typedef
声明了一个函数指针类型operation_t
,指向返回类型为int
,参数为两个int
的函数。 -
🍉函数参数:
perform_operation
函数接受一个函数指针op
和两个int
参数x
和y
,它通过指针调用传入的函数。 -
🍉函数指针赋值与调用:在
main
函数中,定义了operation_t
类型的指针op
,并分别将其指向add
和subtract
函数,通过perform_operation
调用相应的函数。
🍨应用场景
- 🍉回调函数:常用于事件处理、信号处理等。
- 🍉动态函数调用:可以根据条件选择调用不同的函数。
- 🍉实现策略模式:通过函数指针实现不同的算法或策略。
函数指针使得 C 语言的编程更加灵活和高效,但需要小心使用,以避免出现指向无效地址或调用不匹配函数签名的问题。
回调函数
回调函数是其中一种函数指针的应用,它是一种通过将一个函数(或方法)作为参数传递给另一个函数来实现的编程模式。被传递的函数(即回调函数)可以在未来的某个时间点被调用。这种机制在许多编程场景中非常有用,特别是在事件驱动的编程中。
回调函数的工作原理
-
🍉定义回调函数:首先,你需要定义一个将被回调的函数。这个函数通常具有一个特定的签名(即参数类型和返回类型)。
-
🍉将回调函数传递给其他函数:接着,这个回调函数被作为参数传递给另一个函数。
-
🍉在特定条件下调用回调:被接收的函数可以在适当的时机调用传递的回调函数。
🍨示例
下面是一个简单的 C 语言示例,演示了如何使用回调函数:
#include <stdio.h>
// 定义回调函数类型
typedef void (*Callback)();
// 回调函数实现
void sayHello() {
printf("Hello!\n");
}
void sayGoodbye() {
printf("Goodbye!\n");
}
// 接受回调的函数
void performAction(Callback callback) {
// 在某些条件下调用回调函数
callback();
}
int main() {
// 传递回调函数
performAction(sayHello); // 输出: Hello!
performAction(sayGoodbye); // 输出: Goodbye!
return 0;
}
🍨示例二
#include <stdio.h>
typedef void (*fun_callback_t)(int number);//声明回调函数指针
fun_callback_t g_fun_cb = NULL; //定义回调函数指针
int set_callback(fun_callback_t cb); //声明设置回调函数
void my_fun(int number )
{
number = number*100;
printf("In {my_fun} number:<%d>\n",number);
}
int main()
{
printf("callback test start !\n");
set_callback(my_fun); //设置一个名称为my_fun的回调函数
for(int i=0; i<5; i++ )
{
printf("In {main} i:<%d>\n",i);
g_fun_cb(i); //通过回调函数指针调用回调函数
printf("\n");
}
return 0;
}
int set_callback(fun_callback_t cb) //设置回调函数实现
{
if (g_fun_cb != NULL)
{
printf("g_fun_cb is not NULL!\n");
return 0;
}
else
{
g_fun_cb = cb;
return 1;
}
}
🍉解释
先看mian函数通过set_callback(my_fun)将函数my_fun()通过参数传进去,判断回调函数是否为空,如果为空将g_fun_cb的指针指向my_fun(),随后在for循环中调用
🍉结果
0
100
200
300
400
keil5编辑器的编译完整流程
Keil µVision 5(常称为 Keil 5)是一个广泛使用的嵌入式开发环境,主要用于开发基于 ARM 处理器的应用程序。以下是 Keil 5 编辑器编译的完整流程:
🍨创建项目
- 🍓新建项目:在 Keil 中创建一个新工程,选择目标微控制器(MCU),这会自动配置一些设置和启动文件。
- 🍓添加源文件:将 C 或汇编源文件添加到项目中。
🍨设置编译器选项
- 🍓打开“工程选项”(Project Options),配置编译器的选项,包括优化级别、包含路径、预处理器定义等。
🍨 预处理(Preprocessing)
- 🍓Keil 会执行源文件的预处理,处理
#include
和#define
等预处理器指令。这一步通常是自动进行的,Keil 会将源代码的预处理版本传递给编译器。
🍨编译(Compilation)
- 🍓在此阶段,Keil 会将 C 代码翻译成中间表示(IR)。它会进行:
- 🍅语法分析:检查代码的语法。
- 🍅语义分析:验证变量类型和作用域等。
- 🍅生成中间代码,准备生成汇编代码。
🍨 汇编(Assembly)
- 🍓将编译生成的中间表示转换为汇编语言,并生成汇编代码。
- 🍓最后,汇编器会将汇编代码转换为机器代码,生成目标文件(通常是
.obj
文件)。
🍨链接(Linking)
- 🍓Keil 会将生成的目标文件与库文件链接在一起,以生成最终的可执行文件(通常是
.axf
文件)。 - 🍓在此阶段,链接器进行符号解析、内存分配和地址重定位。
🍨生成输出
- 🍓生成最终的程序文件,Keil 还会创建一些帮助文件,如:
- 🍅Listing 文件(.lst):包含汇编代码和每行对应的机器代码。
- 🍅Map 文件(.map):显示代码和数据的存储位置。
- 🍅Hex 文件(.hex):用于编程微控制器的格式。
🍨 调试
- 🍓在 Keil 中,可以直接使用调试器进行代码调试。连接到微控制器后,可以设置断点、查看变量值、单步执行等。
🍨总结
- 🍓创建项目:建立新项目和添加源文件。
- 🍓设置选项:配置编译器和链接器选项。
- 🍓预处理:处理预处理指令。
- 🍓编译:编译源代码生成目标文件。
- 🍓汇编:将目标文件转换为机器码。
- 🍓链接:将各个目标文件链接成最终执行文件。
- 🍓输出和调试:生成需要的输出文件并进行调试。
通过了解这些步骤,可以更有效地使用 Keil 进行嵌入式开发。