【嵌入式八股21】C语言基础知识
一、数据类型说明
在 C 语言中,不同的数据类型在不同平台下占用的字节数有所不同。以下是常见数据类型在 16 位、32 位和 64 位平台下的字节数情况:
| char | 1 字节 | 1 字节 | 1 字节 |
| pointer(指针) | 2 字节 | 4 字节 | 8 字节 |
| short(短整型) | 2 字节 | 2 字节 | 2 字节 |
| int(整型) | 2 字节 | 4 字节 | 4 字节 |
| float(单精度浮点数) | 4 字节 | 4 字节 | 4 字节 |
| double(双精度浮点数) | 8 字节 | 8 字节 | 8 字节 |
| long(长整型) | 4 字节 | 4 字节 | 8 字节 |
| long long(长整型) | 8 字节 | 8 字节 | 8 字节 |
二、volatile 关键字
volatile 关键字用于指出变量的值是随时可能发生变化的,因此在每次使用该变量时,编译器都会强制从变量的内存地址中重新读取数据,而不是使用保存在寄存器中的缓存值。其常见的应用场景如下:
- 并行设备的硬件寄存器:例如,与外部设备进行交互时,硬件寄存器中的值可能会在程序执行过程中由外部设备自行改变,此时需要将访问该寄存器的变量声明为 volatile,以确保每次读取到的都是最新值。
- 中断服务子程序中会访问到的非自动变量(Non-automatic variables):在中断服务程序中,可能会访问到一些全局变量或静态变量。由于中断的发生是随机的,这些变量的值可能会在主程序执行过程中被中断服务程序修改。为了保证在主程序中能正确处理这些变量,应将其声明为 volatile。此外,对于这类变量的访问,可以使用关键区保护,以避免在多线程或中断环境下出现数据竞争。
- 多线程应用中被几个任务共享的变量:在多线程环境下,多个线程可能同时访问和修改同一个变量。为了确保每个线程都能读取到变量的最新值,避免因编译器优化导致的缓存数据不一致问题,需要将共享变量声明为 volatile。另外,在对共享变量进行操作时,可以考虑关闭系统调度(在某些特定情况下),以保证操作的原子性。
三、指针相关概念
(一)函数指针
函数指针是指向函数的指针变量,其声明格式为:int (*fun)(int *a);,表示 fun 是一个指针,它指向一个返回值为 int 类型,参数为 int * 类型的函数。
(二)函数指针数组
函数指针数组是一个数组,数组的每个元素都是一个函数指针。声明格式为:int (*fun[10])(int *data, int size);,表示 fun 是一个包含 10 个元素的数组,每个元素都是一个指向函数的指针,这些函数的返回值为 int 类型,参数为 int * 和 int 类型。
使用示例:
int (*sort_fun[5])(int *data, int size) = {
quick_sort, /* 快速排序 */
insert_sort, /* 插入排序 */
bubble_sort, /* 冒泡排序 */
heap_sort, /* 堆排序 */
selection_sort /* 选择排序 */
};
// 或者
sort_fun[0] = quick_sort;
sort_fun[1] = insert_sort;
(三)指针数组
指针数组是一个数组,数组的元素都是指针。声明格式为:int *a[10];,表示 a 是一个包含 10 个元素的数组,每个元素都是一个指向 int 类型的指针。
(四)数组指针
数组指针是指向数组的指针变量。声明格式为:int (*a)[10];,这里 a 是一个指针,它指向一个包含 10 个 int 类型元素的一维数组。当进行 a + 1 操作时,a 会跨过 10 个 int 型数据的长度。
(五)指针的指针
指针的指针是指向指针的指针变量。声明格式为:int **a;,表示 a 是一个指针,它指向的是一个 int * 类型的指针。
四、main 函数的返回值
在 C 语言中,main 函数的返回值用于向操作系统反馈程序的执行状态:
- 返回 0:表示程序正常退出,即程序在执行过程中没有遇到任何异常或错误情况,顺利完成了所有操作。
- 返回负数:表示程序异常退出,通常意味着程序在执行过程中遇到了某种错误,如内存分配失败、非法操作等。操作系统可以根据返回的负数来进一步分析程序出错的原因。
五、const 关键字
(一)const T
用于定义一个常量,在声明的同时必须进行初始化。一旦声明,该常量的值将不能被改变。
示例:
const int constInt = 10; // 正确,声明并初始化一个整型常量
constInt = 20; // 错误,常量值不可被改变
const int constInt3; // 错误,未被初始化
(二)const T*
表示指向常量的指针,该指针不能用于改变其所指向的对象的值。
示例:
const int i = 5;
const int i2 = 10;
const int* pInt = &i; // 正确,指向一个 const int 对象,即 i 的地址
//*pInt = 10; // 错误,不能改变其所指向的对象的值
pInt = &i2; // 正确,可以改变 pInt 指针本身的值,此时 pInt 指向的是 i2 的地址
const int* p2 = new int(8); // 正确,指向一个 new 出来的对象的地址
delete p2; // 正确
//int* pInt = &i; // 错误,i 是 const int 类型,类型不匹配,不能将 const int * 初始化为 int *
int nValue = 15;
const int * pConstInt = &nValue; // 正确,可以把 int * 赋给 const int *,但是 pConstInt 不能改变其所指向对象的值,即 nValue
*pConstInt = 40; // 错误,不能改变其所指向对象的值
(三)const int* 与 int* const 的区别
指针本身是一种对象,int* const 表示常量指针,即指针本身的值不可改变,声明时必须初始化。
示例:
int nValue = 10;
int* const p = &nValue;
int *const p2 = &nValue;
// const int* 指针指向的对象不可以改变,但指针本身的值可以改变;int* const 指针本身的值不可改变,但其指向的对象可以改变。
int nValue1 = 10;
int nValue2 = 20;
int* const constPoint = &nValue1;
//constPoint = & nValue2; // 错误,不能改变 constPoint 本身的值
*constPoint = 40; // 正确,可以改变 constPoint 所指向的对象,此时 nValue1 = 40
const int nConstValue1 = 5;
const int nConstValue2 = 15;
const int* pPoint = &nConstValue1;
//*pPoint = 55; // 错误,不能改变 pPoint 所指向对象的值
pPoint = &nConstValue2; // 正确,可以改变 pPoint 指针本身的值,此时 pPoint 绑定的是 nConstValue2 对象,即 pPoint 为 nConstValue2 的地址
(四)const int* const
表示一个指向常量对象的常量指针,即既不可以改变指针本身的值,也不可以改变指针指向的对象的值。
示例:
const int nConstValue1 = 5;
const int nConstValue2 = 15;
const int* const pPoint = &nConstValue1;
//*pPoint = 55; // 错误,不能改变 pPoint 所指向对象的值
//pPoint = &nConstValue2; // 错误,不能改变 pPoint 本身的值
(五)const 助记方法
- 把一个声明从右向左读:
char * const cp;应读成cp is a const pointer to char,即cp是一个指向char类型的常量指针;const char * p;读成p is a pointer to const char,即p是一个指向const char类型的指针;char const * p;与const char * p等价,因为在 C++ 中没有const*的运算符,所以const只能属于前面的类型。 - C++ 标准规定,const 关键字放在类型或变量名之前等价:
char * const cp:定义一个指向字符的指针常数,即const指针。const char* p:定义一个指向字符常数的指针。char const* p:等同于const char* p。const char **:是一个指向指针的指针,那个指针又指向一个字符串常量。char **:也是一个指向指针的指针,那个指针又指向一个字符串变量。
六、浮点数存储方式
(一)float 类型
float 类型占用 4 个字节(32 bits),其存储结构如下:
| 符号位 | 1 bit | 表示浮点数的正负,0 表示正数,1 表示负数。 |
| 指数位 | 8 bits | 用于表示浮点数的指数部分,以确定数值的大小范围。 |
| 尾数部分 | 23 bits | 表示浮点数的有效数字部分。 |
(二)double 类型
double 类型占用 8 个字节(64 bits),其存储结构如下:
| 符号位 | 1 bit | 表示浮点数的正负,0 表示正数,1 表示负数。 |
| 指数位 | 11 bits | 用于表示浮点数的指数部分,相比 float 类型,能表示更大的数值范围。 |
| 尾数部分 | 52 bits | 表示浮点数的有效数字部分,提供了更高的精度。 |
七、C 语言题目示例
(一)printf 返回值
#include <stdio.h>
int main() {
int i = 43;
printf("%d\n", printf("%d", printf("%d", i)));
return 0;
}
输出:4321
printf 函数的返回值是输出字符的个数(不包括字符串结尾 \x00)。在上述代码中,最内层的 printf("%d", i) 输出 43,返回值为 2;中间层的 printf("%d", 2) 输出 2,返回值为 1;最外层的 printf("%d\n", 1) 输出 1 并换行。
(二)enum 枚举类型
#include <stdio.h>
int main() {
enum color{
RED,
BLUE,
GREEN = -2,
YELLOW,
PINK
};
printf("%d %d\n", BLUE, PINK);
return 0;
}
输出:1 0
在 enum 枚举类型中,默认情况下,枚举常量从 0 开始依次赋值。所以在上述代码中,RED = 0,BLUE = 1,GREEN = -2,YELLOW 的值为 GREEN 的值加 1,即 YELLOW = -1,PINK 的值为 YELLOW 的值加 1,即 PINK = 0。
(三)可变参数函数
#include "stdarg.h"
char buf[512] = {0};
int func(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
vsprintf(buf, fmt, args);
va_end(args);
// 这里可以根据实际需求对 buf 进行处理,比如返回处理结果等
return 0;
}
上述代码展示了一个可变参数函数的基本结构。va_list 是用于存储可变参数列表的类型,va_start 用于初始化可变参数列表,vsprintf 函数根据格式化字符串 fmt 将可变参数列表中的值格式化到 buf 中,va_end 用于结束可变参数列表的使用。
(四)大小端区分
union data {
int a;
char b;
};
struct def {
union data mine;
};
struct def endian;
int main(int argc, char **argv)
{
endian.mine.a = 0x12345678;
printf("%02X\n", endian.mine.b);
return 0;
}
上述代码通过联合体(union)的特性来判断系统的字节序(大小端)。在小端模式下,低地址存储低字节,高地址存储高字节;在大端模式下,低地址存储高字节,高地址存储低字节。在上述代码中,如果打印结果为 78,说明是小端模式,因为 0x12345678 的低字节 78 存储在低地址处;如果打印结果为 12,则说明是大端模式。
八、链表相关代码示例
以下是一个单链表的实现,包括单链表的初始化、建立(头插法和尾插法)、插入、查找和删除操作。
//////////////////////////////////////////
// 单链表的初始化,建立,插入,查找,删除。//
// Author:sagima //
// Date: 2024.8.19 //
//////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
////////////////////////////////////////////
// 定义结点类型
typedef struct Node
{
ElemType data; // 单链表中的数据域
struct Node *next; // 单链表的指针域
} Node, *LinkedList;
////////////////////////////////////////////
// 单链表的初始化
LinkedList LinkedListInit()
{
Node *L;
L = (Node *)malloc(sizeof(Node)); // 申请结点空间
if (L == NULL) // 判断是否有足够的内存空间
{
printf("申请内存空间失败\n");
return NULL;
}
L->next = NULL; // 将 next 设置为 NULL, 初始长度为 0 的单链表
return L;
}
////////////////////////////////////////////
// 单链表的建立 1,头插法建立单链表
LinkedList LinkedListCreatH()
{
Node *L;
L = (Node *)malloc(sizeof(Node)); // 申请头结点空间
L->next = NULL; // 初始化一个空链表
ElemType x; // x 为链表数据域中的数据
while (scanf("%d", &x) != EOF)
{
Node *p;
p = (Node *)malloc(sizeof(Node)); // 申请新的结点
p->data = x; // 结点数据域赋值
p->next = L->next; // 将结点插入到表头 L-->|2|-->|1|-->NULL
L->next = p;
}
return L;
}
////////////////////////////////////////////
// 单链表的建立 2,尾插法建立单链表
LinkedList LinkedListCreatT()
{
Node *L;
L = (Node *)malloc(sizeof(Node)); // 申请头结点空间
L->next = NULL; // 初始化一个空链表
Node *r;
r = L; // r 始终指向终端结点,开始时指向头结点
ElemType x; // x 为链表数据域中的数据
while (scanf("%d", &x) != EOF)
{
Node *p;
p = (Node *)malloc(sizeof(Node)); // 申请新的结点
p->data = x; // 结点数据域赋值
r->next = p; // 将结点插入到表头 L-->|1|-->|2|-->NULL
r = p;
}
r->next = NULL;
return L;
}
////////////////////////////////////////////
// 单链表的插入,在链表的第 i 个位置插入 x 的元素
LinkedList LinkedListInsert(LinkedList L, int i, ElemType x)
{
// 检查插入位置的合法性
if (i < 1)
{
printf("插入位置不合法\n");
return L;
}
Node *pre; // pre 为前驱结点
pre = L;
int tempi = 1;
// 查找第 i 个位置的前驱结点
while (tempi < i && pre != NULL)
{
pre = pre->next;
tempi++;
}
if (pre == NULL)
{
printf("插入位置超出链表长度\n");
return L;
}
Node *p; // 插入的结点为 p
p = (Node *)malloc(sizeof(Node));
p->data = x;
p->next = pre->next;
pre->next = p;
return L;
}
////////////////////////////////////////////
// 单链表的查找,查找链表中值为 x 的元素的位置(这里返回第一个匹配的位置,从 1 开始计数)
int LinkedListFind(LinkedList L, ElemType x)
{
Node *p = L->next;
int position = 1;
while (p != NULL)
{
if (p->data == x)
{
return position;
}
p = p->next;
position++;
}
return -1; // 表示未找到
}
////////////////////////////////////////////
// 单链表的删除,在链表中删除值为 x 的元素
LinkedList LinkedListDelete(LinkedList L, ElemType x)
{
Node *p, *pre; // pre 为前驱结点,p 为查找的结点。
pre = L;
p = L->next;
while (p != NULL && p->data != x) // 查找值为 x 的元素
{
pre = p;
p = p->next;
}
if (p == NULL)
{
printf("未找到要删除的元素\n");
return L;
}
pre->next = p->next; // 删除操作,将其前驱 next 指向其后继。
free(p);
return L;
}
/////////////////////////////////////////////
int main()
{
LinkedList list, start;
// 使用头插法建立链表示例
/* printf("请输入单链表的数据(以文件结束符结束输入,如在 Linux 下为 Ctrl+D,在 Windows 下为 Ctrl+Z):");
list = LinkedListCreatH();
for(start = list->next; start != NULL; start = start->next)
printf("%d ",start->data);
printf("\n");
*/
// 使用尾插法建立链表示例
printf("请输入单链表的数据(以文件结束符结束输入,如在 Linux 下为 Ctrl+D,在 Windows 下为 Ctrl+Z):");
list = LinkedListCreatT();
for(start = list->next; start != NULL; start = start->next)
printf("%d ",start->data);
printf("\n");
int i;
ElemType x;
printf("请输入插入数据的位置:");
scanf("%d",&i);
printf("请输入插入数据的值:");
scanf("%d",&x);
LinkedListInsert(list,i,x);
for(start = list->next; start != NULL; start = start->next)
printf("%d ",start->data);
printf("\n");
int findPosition = LinkedListFind(list, x);
if (findPosition != -1)
{
printf("元素 %d 所在位置为:%d\n", x, findPosition);
}
else
{
printf("未找到元素 %d\n", x);
}
printf("请输入要删除的元素的值:");
scanf("%d",&x);
LinkedListDelete(list,x);
for(start = list->next; start != NULL; start = start->next)
printf("%d ",start->data);
printf("\n");
return 0;
}
以上代码完整地实现了单链表的一系列基本操作,并在 main 函数中提供了简单的测试示例,包括链表的建立、插入、查找和删除操作的调用及结果展示。
一些八股模拟拷打Point,万一有点用呢

查看13道真题和解析