文章目录
前言
笔者在学习时发觉自己的C语言很久没有系统性重温一遍了,本期主要是对于嵌入式中常用的C语言语法进行一个汇总。次内容对于着急刷嵌入式八股文的同学也有一定帮助,详细可以去看:
https://www.bilibili.com/video/BV1VM4y137Pm?p=3&vd_source=937f3264dce76f586bc0a69ee24dfafa
一、指针和变量
变量和指针是编程语言中的两个核心概念,它们在程序设计和执行中各自扮演着重要的角色:
- 变量是编程语言中用于存储数据的标识符,它代表了一个可以存储数据值或引用(即内存地址)的内存位置。变量具有一个唯一的名称(即变量名),它允许程序在需要时访问和操作存储在该内存位置的数据。变量的值可以在程序执行过程中改变,这使得变量成为跟踪和操作程序数据的关键工具。
- 指针(若为32位机,默认在内存占4字节)则是编程语言中的一个特殊类型的变量,它存储的是另一个变量的内存地址,而不是直接的数据值。指针提供了一种间接访问和操作内存的方式,通过指针,程序可以获取和操作指定内存地址的数据。指针在编程中有很多应用,例如动态内存分配、数据结构操作以及函数参数传递等。
变量和指针在编程中密切相关,但又有明显的区别。变量直接存储数据值,而指针存储的是指向数据值的内存地址。在使用上,变量可以直接通过其名称来访问和操作存储的数据,而指针则需要通过解引用操作(如使用星号*操作符)来获取指向的数据。
通过指针赋值
在进行一个赋值操作时,其流程为“CPU从flash上读取i=123指令→CPU执行指令(从指令中解析得到i的地址和要输入的数据,再输入)”,这一个过程中实际上隐藏的对地址的操作,也可以通过指针复现这个过程,这二者是等价的。例如:
//1.直接写入
int i;
i=123;
//2.通过地址操作写入
int i;
int *p;
p = & i;
*p = 123;
二、关键字
1. Volatile
volatile关键字可以用于告诉编译器不要对变量进行优化,即不要将变量缓存在寄存器中,应该直接从内存中读取或写入变量。这在多线程访问共享变量和中断处理函数中的变量时特别有用,能确保数据的正确性和实时性。
代码如下(示例):
int i;
i=0;
i=1;
此时i=0可以省略,为了进一步优化速度,通常这部分内容会放到CPU中,从而使得读取不通过内存,此时如果我们想使其每次都必须从内存读取防止数据被篡改就可以加该关键字。通常情况下,在访问硬件寄存器时需要使用该关键字。
2. const
简单来说,const关键字就是表明所有人都不能改变此变量,常用于设置常量,例如圆周率等。
3. static
在大型项目中,我们常常会出现变量名命名重复的问题,此时可以增加static关键字表示该定义只能在该文件下使用,不可传递到其他目录下。
4. extern
在对同一变量名进行引用时,常常会出现重复定义的情况,此时便可以在一个文件中正常定义,而在其他文件加入extern关键字进行定义,告诉cpu在处理这部分内容时,变量被定义在别处。例如:
//子文件
int temp;
i=1;
//主函数
extern int temp;
三、结构体
1. 结构体基本内容
结构体(struct)是编程语言中用于组合不同类型的数据到一个单独的数据类型的一种构造。结构体允许你创建复合数据类型,这些数据类型由多个基本数据类型(如整数、浮点数、字符等)或其他结构体组合而成。通过使用结构体,你可以将相关的数据元素组合在一起,形成一个有意义的整体,从而更方便地管理和操作这些数据。结构体通常具有如下特性:
- 组成元素:结构体可以包含多个不同类型的成员(或称为字段),这些成员可以是基本数据类型、其他结构体类型、指针类型等。
- 命名:每个结构体都有一个唯一的名称,用于在代码中引用该结构体类型。
- 定义与初始化:在使用结构体之前,需要先进行定义,即声明结构体的名称以及它所包含的成员类型和名称。之后,可以创建结构体的实例(即变量),并为其成员赋值。
- 访问成员:通过结构体变量和点运算符(在C/C++中)或箭头运算符(在通过指针访问结构体成员时),可以访问和修改结构体的成员。
#include <stdio.h>
// 定义一个结构体类型
struct Student {
char name[50];
int age;
float score;
};
int main() {
// 创建一个结构体的实例(变量)
struct Student student1 = {"张三",20,90.5f};
// 访问结构体的成员并打印
printf("姓名: %s\n", student1.name);
printf("年龄: %d\n", student1.age);
printf("分数: %.1f\n", student1.score);
return 0;
}
struct只有在实例化后才会分配空间,这里在32的硬件配置时经常使用(例如GPIOX),感兴趣可以打开了解一下。
2. 通过指针对结构体赋值
在项目中,我们同样可以利用指针实现结构体赋值,这种方式的好处在于具有更高的灵活性和安全性(大项目中,如果使用全局变量进行数据传输可能导致数据跑飞)。具体实现如下所示:
#include <stdio.h>
#include <string.h>
// 定义结构体类型
struct Person {
char name[50];
int age;
};
int main() {
// 创建两个结构体的实例
struct Person person1 = {"Alice", 30};
struct Person person2;
// 创建指向结构体类型的指针,并让它指向person1
struct Person *ptr = &person1;
// 通过指针访问结构体的成员
printf("Name: %s, Age: %d\n", ptr->name, ptr->age);
// 通过指针对结构体进行赋值
// 将person1的值赋给person2
person2 = *ptr; // 解引用指针,获取其指向的结构体的值,并赋给person2
// 直接访问person2的成员,验证赋值是否成功
printf("person2 Name: %s, Age: %d\n", person2.name, person2.age);
// 也可以通过指针直接修改结构体的成员
strcpy(ptr->name, "Bob");
ptr->age = 25;
// 再次输出修改后的值
printf("Modified Name: %s, Age: %d\n", ptr->name, ptr->age);
return 0;
}
- struct Person *ptr = &person1; 创建了一个指向 person1 的结构体指针 ptr。
- printf(“Name through pointer: %s\n”, ptr->name); 通过指针 ptr 访问 person1的name 成员。
- person2 = *ptr; 通过解引用指针 ptr(即 *ptr),获取 person1 的值,并将其赋给person2。
通过指针给结构体赋值则是使用这个地址来获取结构体的值,并将这个值赋给另一个结构体变量。
3. 结构体指针
在C语言中,结构体指针是一个特殊的指针变量,它指向一个结构体类型的变量或内存区域。通过使用结构体指针,你可以间接地访问和操作结构体的成员。这里我们对上例进行修改:
#include <stdio.h>
#include <string.h>
// 定义结构体类型
struct Person {
char name[50];
int age;
};
int main() {
// 创建两个结构体的实例
struct Person person1 = {"Alice", 30};
struct Person person2;
// 创建指向结构体类型的指针,并让它指向person1
struct Person *ptr = &person1;
// 通过指针访问person1的成员
printf("Name through pointer: %s\n", ptr->name);
printf("Age through pointer: %d\n", ptr->age);
// 通过结构体指针给结构体赋值
// 将ptr指向的结构体(即person1)的值赋给person2
memcpy(&person2, ptr, sizeof(struct Person)); // 使用memcpy通过指针复制结构体内容
// 直接访问person2的成员,验证赋值是否成功
printf("person2 Name: %s\n", person2.name);
printf("person2 Age: %d\n", person2.age);
return 0;
}
在这个修改后的例子中,我使用了 memcpy 函数来通过结构体指针 ptr 复制 person1 的内容到 person2。memcpy 函数从 ptr 指向的地址开始,复制 sizeof(struct Person) 字节到 &person2 的地址。这样就实现了通过结构体指针给另一个结构体赋值的效果。此外,结构体指针最主要的功能在于你可以通过“ptr->XXX = ?”的形式来实现对某设定好的结构体的成员的改动,而普通的结构体变量则做不到这一点。
注:这时候你可能会感觉到很懵,这不是一样的吗,实际上结构体指针和通过指针给结构体赋值不是同一个意思,但它们之间有关联。结构体指针是一个变量,它存储了一个结构体的内存地址。通过这个地址,你可以间接地访问和修改结构体的成员。而通过指针给结构体赋值,是指使用结构体指针来将一个结构体的值赋给另一个结构体。这通常涉及到解引用指针来获取其指向的结构体的值,然后将这个值赋给另一个结构体变量。
扩展----结构体中的成员也可以是函数指针,在使用时的功能和正常函数一样,赋值时和其他成员一样,例如:
#include <stdio.h>
// 定义两个简单的函数
void print_hello() {
printf("Hello, World!\n");
}
void print_goodbye() {
printf("Goodbye, World!\n");
}
// 定义结构体,其中包含一个函数指针成员
typedef struct {
void (*print_func)(); // 函数指针成员,指向无参数无返回值的函数
} FunctionStruct;
int main() {
// 创建两个结构体实例,并分别初始化它们的函数指针成员
FunctionStruct fs_hello = {print_hello};
FunctionStruct fs_goodbye = {print_goodbye};
// 通过结构体中的函数指针调用函数
fs_hello.print_func(); // 输出: Hello, World!
fs_goodbye.print_func(); // 输出: Goodbye, World!
return 0;
}
四、链表
1. 链表的基本内容
链表(Linked List)是一种常见的数据结构,它由一系列节点(Node)组成,每个节点包含两个部分:数据域和指针域。数据域用于存储数据元素,指针域则用于指向链表中的下一个节点。通过指针的连接,链表可以动态地存储数据,并且可以根据需要进行扩展或缩减。链表有多种类型,最常见的有单向链表、双向链表和循环链表。
-
单向链表:每个节点包含一个数据元素和一个指向下一个节点的指针。第一个节点(头节点)的指针指向链表中的第一个数据节点,最后一个节点的指针通常指向 NULL 表示链表的结束。只能从头节点开始顺序访问链表中的元素。
-
双向链表:每个节点除了包含一个数据元素外,还包含两个指针:一个指向前一个节点,一个指向下一个节点。双向链表可以从任意节点向前或向后遍历,第一个节点的前指针通常指向 NULL,最后一个节点的后指针也指向 NULL。
-
循环链表:循环链表与单向链表类似,但最后一个节点的指针指向头节点,形成一个环。循环链表可以循环遍历整个链表。
接下来是一个简单的单向链表节点定义:
typedef struct Node {
int data; // 数据域
struct Node* next; // 指针域,指向下一个节点
} Node;
2. 链表的插入和删除
一般在插入时,我们选择传递指针(节省内存资源),示例如下:
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
// 创建新节点
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode) {
printf("Memory allocation failed.\n");
exit(1);
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// 在链表末尾插入节点
void insertNode(Node** head, int data) {
Node* newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
} else {
Node* current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = newNode;
}
}
// 遍历链表并打印节点数据
void printList(Node* head) {
Node* current = head;
while (current != NULL) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
int main() {
Node* head = NULL; // 初始化空链表
// 插入节点
insertNode(&head, 1);
insertNode(&head, 2);
insertNode(&head, 3);
// 打印链表
printList(head); // 输出: 1 2 3
// 释放链表内存(这里省略了释放内存的代码)
return 0;
}
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点
typedef struct Node {
int data;
struct Node* next;
} Node;
// 创建新节点
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode) {
printf("Memory allocation failed.\n");
exit(1);
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// 删除指定值的节点
void deleteNode(Node** head, int value) {
// 处理头节点的情况
if (*head != NULL && (*head)->data == value) {
Node* temp = *head;
*head = (*head)->next;
free(temp);
return;
}
// 处理链表其他节点的情况
Node* current = *head;
while (current->next != NULL) {
if (current->next->data == value) {
Node* temp = current->next;
current->next = current->next->next;
free(temp);
return;
}
current = current->next;
}
// 如果没有找到要删除的节点
printf("Node with value %d not found in the list.\n", value);
}
// 遍历链表并打印节点数据
void printList(Node* head) {
Node* current = head;
while (current != NULL) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
// 主函数
int main() {
Node* head = NULL; // 初始化空链表
// 插入节点
head = createNode(1);
insertNode(&head, 2);
insertNode(&head, 3);
insertNode(&head, 4);
insertNode(&head, 5);
// 打印原始链表
printf("Original list: ");
printList(head);
// 删除节点
int valueToDelete = 3;
deleteNode(&head, valueToDelete);
// 打印删除节点后的链表
printf("List after deleting node with value %d: ", valueToDelete);
printList(head);
// 释放链表内存
Node* current = head;
while (current != NULL) {
Node* temp = current;
current = current->next;
free(temp);
}
return 0;
}
这部分代码的核心在于判断当前是否是最后一项(或者是要删除项的前一项),可以简单理解为排火车,插入时需要考虑是否插入的地方后面没有人,如果没有就可以插进去了。同理,在删除时也需要知道被删除的人在“谁后边”,确定之后才能进行操作。
五、堆、栈和队列
1. 栈(Stack)
栈是一种后进先出(LIFO)的数据结构,用于存储局部变量和函数调用的信息。在程序执行时,栈会自动分配和释放内存空间。每次函数调用时,都会在栈上创建一个新的栈帧(stack frame),用于存储该函数的局部变量和返回地址等信息。函数执行完毕后,对应的栈帧会被销毁,其占用的内存空间会被自动释放。栈的大小通常有限,并且栈上的数据访问速度非常快。
2. 堆(Heap)
堆是用于动态内存分配的区域,程序员可以在运行时显式地请求分配和释放内存。在C和C++等语言中,通常使用malloc、calloc、realloc和free等函数来在堆上分配和释放内存。堆的大小通常比栈大得多,且分配和释放内存的操作相对较慢(因为需要查找可用的内存块并进行内存管理)。堆上的数据可以在程序的整个生命周期内存在,直到显式地释放它们。
注 :栈是自动管理内存的,主要用于存储局部变量和函数调用信息,其大小有限且访问速度快;堆是手动管理内存的,用于动态内存分配,其大小较大且分配和释放操作相对较慢。
3. 队列
队列(Queue) 是一种先进先出(FIFO,First In First Out)的数据结构。它就像日常生活中的排队一样,新加入的元素会被放到队尾,而访问元素时总是从队头开始取,队列不允许在中间位置进行插入或删除操作。
队列的基本操作通常包括:
- 入队(Enqueue):在队列的尾部添加一个新元素。
- 出队(Dequeue):移除队列的头部元素,并返回该元素的值。
- 查看队头(Peek):返回队列头部元素的值,但不移除它。
- 判断队列是否为空:检查队列中是否还有元素。
注:队列与栈和堆在内存管理上有本质的区别,栈和堆是内存管理的两种方式,而队列则是一种数据结构,用于组织和管理数据。
标签:Node,current,head,八股文,嵌入式,链表,结构,C语言,指针 From: https://blog.csdn.net/sincerelover/article/details/137131847