没志青年
发布于 2025-12-04 / 17 阅读
0

面试题 - C语言

关键字 volatile、const、static 分别有什么用?

编写一个宏,可以清除或设置pos位

已知:val 要设置的值 pos 偏移量 bit:1置位0清位

#define SET bit ? (val |= a << pos) : (val &=~ 1 << pos)

已知一个数组Table,写一个宏得出数组元素个数。

#define ARR_SIZE (sizeof(table)/sizeof(table[0]))

什么是回调函数?与普通的直接函数调用比,回调的方式好处在哪里(可以从功能性、模块化两个方面讲)

从代码的安全方面上,指针在使用时应注意什么?

列出C语言常用的数据类型和占用字节数?

C语言中常用的数据结构?

局部变量、全局变量、静态变量的存储位置在哪?

什么是预编译?


一、预处理 & 关键字

在C语言中,“以#开头的都是预处理命令,预处理命令都是以#开头的”,这句话是正确的吗?

正确。

C语言宏中"#“和”##"的用法

#:将宏函数中传入的参数转换为字符串

示例:

#define TO_STRING(x) #x
​
int main() {
    int num = 10;
    printf("%s\n", TO_STRING(num));  // 输出:num
    printf("%s\n", TO_STRING(123));  // 输出:123
}

##:拼接宏函数中的参数,得到一个新的标识符。

注意:

  • 前后的空格可有可无

  • 如果前后的参数是宏定义的话,会阻止展开,也就是说如果参数为宏A,那么就当做是A,不会把它展开

  • 拼接得到的标识符可以是变量也可以是宏定义,但是这个标识符不存在的话编译会报错

示例:

#define CONCAT(a, b) a##b
#deinfe N(a) num##a
​
int main() {
    int xy = 100;
    int num9 = 120;
    printf("%d\n", NUM(9));   // 输出:120
    printf("%d\n", CONCAT(x, y));  // 输出:100
}

如何实现代码的条件编译?

通过 #ifdef 或 #if 实现条件编译

#ifdef DEBUG
    printf("Debug mode\n");
#endif

预处理器标识#error的作用是什么?

#error 用于在预处理阶段生成一个错误消息并终止编译过程。

通常结合 #ifdef 等使用,防止程序在不满足某些条件时继续编译,确保编译环境或配置符合特定要求。

#ifdef XXX  
...
#error "XXX has been defined"
#else
#endif

volatile 关键字

volatile 关键字的作用是什么?有哪些使用场景?

volatile 在编译阶段起作用

作用:

volatile 告诉编译器,变量的值可能会在程序的其他地方(例如硬件或另一个线程)发生变化,因此每次访问该变量时,都必须从内存中读取其值,而不是使用CPU寄存器中的缓存值。

使用场景:

(1)防止编译器优化,编译器在编译的时候可能会优化程序代码,导致代码原有的功能改变。

比如下面这个,我们操作寄存器进行两次操作,但是编译器会进行不必要优化,导致只有一次硬件操作。

int *p = ioremap(xxxx, 4); // GPIO 寄存器的地址
*p = 0; // 点灯,但是这句话被优化掉了
*p = 1; // 灭灯

(2)声明硬件寄存器,确保能获取到寄存器的实际值,并且保证每次操作都能执行。

volatile uint32_t *status_register = (uint32_t *)0x40000000;
while (*status_register != 0x01) {
    // 等待硬件寄存器的状态变化
}

(3)中断服务程序中的全局变量,不使用 volatile 中断程序可能无法获取 flag 的最新值

(4)处理多线程共享变量,防止死锁

函数的参数可以是volatile吗?

指针可以是 volatile 吗?

可以,因为指针和普通变量一样,有时也有变化程序的不可控性。

常见例子:子中断服务子程序修改 一个指向一个 buffer 的指针时,必须用 volatile 来修饰这个指针。

#define 宏定义

define 是在编译的哪个阶段被处理的?

预处理阶段。

编译器在预处理阶段会处理所有预处理命令,包括宏定义、条件编译、⽂件包含等。

写一个宏,返回两个参数中最小的一个

#define MIN(A, B) ((A) <= (B) ? (A) : (B)) 

使用三目运算符实现,加括号为了防止替换时出错。

写一个宏,计算数组元素个数

#define COUNT(table) (sizeof(table) / sizeof(table[0])) 

sizeof(table) 得到数组长度,sizeof(table[0]) 得到数组元素长度,两者相除即可得到数组元素个数。

写一个宏,表明1年中有多少秒(忽略闰年问题)

#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL 

解读: (1)注意预处理器将为你计算常数表达式的值,并且整个宏体要用括号括起来。 (2)注意这个表达式将使一个16位机的整型数溢出,因此要用到无符号长整型符号UL,告诉编译器这个常数是的无符号长整型数。

带参数宏和函数的区别?

(1)带参宏只是在编译预处理阶段进行简单的字符替换;而函数则是在运行时进行调用和返回。

(2)宏替换不占运行时间,只占编译时间;而函数调用则占运行时间(分配单元、保留现场、值传递、返回)。

(3)带参宏在处理时不分配内存;而函数调用会分配临时内存。

(4)宏不存在类型问题,宏名无类型,它的参数也是无类型的;而函数中的实参和形参都要定义类型,二者的类型要求一致。

(5)而使用宏定义次数多时,宏替换后源程序会变长;而函数调用不使源程序变长。

下面代码能不能编译通过?

#define c 3 
c++;

不能。自增运算符++只能用于变量,而3是常量。

define 和枚举的区别?

  • 枚举是在编译器生效,define 是在预处理

  • 枚举有类型,具有类型检查

  • 枚举是实体,宏不是

  • 枚举可以定义常量和变量,宏只能定义常量

  • 枚举可以调试

const 关键字

关键字 const 的作用是什么?

const 更恰当的理解是【只读】,而不是意味着【常数】。

const 可以放在数据类型前面,也可以放在后面。

(1)修饰变量,定义变量为常量

const int N = 100;//定义一个常量N
N = 50; //错误,常量的值不能被修改
const int n; //错误,常量在定义的时候必须初始化

注意:使用 const 定义了常量,也不一定放在常量区:

  • 在函数内部定义的 const 常量,还是放在栈区的,因为函数被调用的时候才会创建

  • 修饰变量,这取决于指针指向数据的定义

  • 有些const变量,如果编译器能够在编译时确定它的值(比如const int a = 10;),则编译器可能根本不会为它分配存储空间,而是直接在需要使用的地方内联这个常量。

(2)修饰数组

数组中每个元素都不能修改

int const a[8] = {1,2,3,4,5,6,7,8};
const int a[8] = {1,2,3,4,5,6,7,8};

(3)修饰函数参数和返回值

修饰参数,表示函数内不能修改参数的值。

修饰返回值,表示函数的返回值不能被修改。

(4)修饰指针

常量指针:指向一个常量的指针,可以修改指向,但是指向的内容不能修改。

const int*p;
int const*p;

指针常量:指针是常量,因此不能修改指向,但是指向的内容可以修改。

int*const p;

指向常量的常量指针,都不可以修改。

const int* const p;

函数的参数既可以是const还可以是volatile吗?

一个指针可以是volatile吗?下面

的函数有什么问题? (1)是的。一个例子是只读的状态寄存器,它是volatile因为它可能被意想不到地改 变,它是const因为程序不应该试图去修改它。

/ (2)是的。一个例子是当一个中服务子程序修改一个指向一个缓冲区的指针时。 (3)这个函数的目的是用来返指针ptr指向值的平方,但是,由于ptr指向一个 volatile型参数,编译器将产生类似下面的代码: 由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能 返不是你所期望的平方值!正确的代码如下:

const 和 #define 的异同?

  • 类型安全:#define只是在预处理阶段进行简单的文本替换,没有数据类型的概念,不会对类型安全检查。

  • 内存占用:#define不分配内存,存在于程序的代码段,在程序中表现为一个常数;而 const 常量存在于常量区,可以在程序中传递和被调用。

  • 作用域:#define 的作用范围是全局的,且不受命名空间的约束;const 定义的常量有它的作用域。

const 和 #define 定义常量谁更好?

因此,推荐使用 const 来定义常量类型,宏定义常量常用于条件编译。

sizeof 关键字

sizeof() 和 strlen() 有什么区别?

  • 作用不同:

    • sizeof() 可用于计算任意数据类型占用的字节数

    • strlen() 只能用于计算字符串占用的字节数(不包括 \0),也就是字符个数

  • 作用时机不同:

    • sizeof() 为编译时运算符,在编译时由编译器计算确定结果

    • strlen() 为函数,是在程序运行期间计算的

数组做 sizeof 的参数不退化,传递给 strlen 就退化为指针了。

不使用 sizeof,如何求int占用的字节数?

	#include <stdio.h>
	#define MySizeof(Value)	(char *)(&value+1)-(char*)&value
	int main() 4	{
	int i ;
	double f;
	double *q;
	printf("%d\r\n",MySizeof(i));
	printf("%d\r\n",MySizeof(f));
	printf("%d\r\n",MySizeof(a));
	printf("%d\r\n",MySizeof(q));
	return 0; 	}

上例中, (char)& Value 返回 Value的地址的第一个字节, (char)(& Value+1) 返回 value的地址的下一个地址的第一个字节,所以它们之差为它所占的字节数。

关键字 static 的作用是什么?

static 可用于声明静态变量和静态函数。

静态全局变量:

  • 作用域被修改,其它文件无法通过 extern 关键字去访问

静态局部变量:

  • 在函数中,无论函数调用多少次,静态局部变量只会初始化一次

  • 局部变量的生命周期被延长,和全局变量一样

静态函数:

  • 作用域被修改,其它文件无法通过 extern 关键字去调用

  • 在大项目中经常用到,不用担心同名函数的干扰,同时也是函数本身的一种保护机制。

extern 关键字

关键字 extern 的作用是什么?

用于访问其它文件中的全局变量和函数,函数默认具有外部链接属性,因此可以不用加 extern。

通常在头文件中使用 extern 声明变量,这样引用该头文件,就可以直接使用其它文件中的变量。

注意:

  • extern 在【链接阶段】起作用

  • extern 只能用于声明变量,不能进行初始化

extern ”C” 的作用是什么?

用于 C++ 和 C 语言在代码中互相调用函数。????????

在C++中,函数的名称在编译时会经过名称修饰,以支持函数重载和其他C++特性,而C语言不支持名称修饰,函数的名称在编译后保持原样。因此使用 extern "C" 告诉C++编译器按照C语言的方式来处理,以确保C++编译器正确地链接C语言函数。

注意:extern ”C”只能写在 C++ 文件中。????????????

#ifdef __cplusplus   // 预处理指令,检测当前的编译器是否是C++编译器
extern "C" {
#endif

void foo(int);

#ifdef __cplusplus
}
#endif

typedef 关键字

typedef 和 define 有什么区别?

相同:都可以用于给数据类型起别名。

(1)功能不同

  • typedef 用于定义数据类型的别名,如int、char、struct等,增强程序的可读性。

  • define 不仅可以为类型取别名,还可以定义常量、代码段等

(2)原理不同

  • define 是预处理指令,在预处理时只进行简单的替换,不做类型检査,只有在编译已被展开的源程序时,才会发现可能的错误并报错。

  • typedef 是关键字,是编译过程的一部分,具有类型检查的功能。

(3)作用域不同

  • define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用

  • 而 typedef 像变量一样有作用域

(4)对指针的操作不同

typedef 和 define 定义的指针时有很大的区别

#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1	pl, p2;      ==>   int*p1,p2;      定义一个指针,一个整型
INTPTR2 p3, p4;      ==>   int*pl, *p2;    定义两个指针变量

注意:typedef 定义是语句,因为句尾要加上分号。而 define 不是语句,千万不能在句尾加分号。

typedef 和 define 给数据类型起别名哪个更好?

typedef 更好

  • 灵活

  • 定义指针不会出错

头文件

使用 <> 和 "" 引入头文件有什么区别?

使用 <>:编译器先从系统标准库路径开始搜索,找不到再搜索用户的工作路径。

使用 "":编译器先从用户的工作路径开始搜索,找不到再去系统路径下找。

因此,对于系统库使用<>,对于自定义头文件使用 ""

头文件的作用有哪些?

(1)通过头文件来调用库功能

一些第三方库可能不希望开放源代码,因此只提供头文件和库文件。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的。

(2)加强类型安全检查

虽然在其它文件中也能声明函数然后调用,但是没有安全检查。而在头文件中声明的话,编译器既会检查实现的也会检查调用的,如果不一致就会指出错误,大大减轻程序员调试、改错的负担。

文件为什么要防止重复定义?

(1)当多个文件包含头文件的时候,防止头文件中的变量、函数和类型等重复定义,导致编译出错。

(2)同一代码只需要编译一次,提高编译速度。

头文件如何防止重复定义?

(1)使用宏保护

适用于所有C\C++编译器

#ifndef HEADER_FILENAME_H
#define HEADER_FILENAME_H

// 头文件的内容

#endif

(2)使用 #pragma once

编译器的拓展指令,更加简洁,大多数编译器都支持

#pragma once

// 头文件的内容
void myFunction();

头文件中是否可以定义全局变量?

不可以。

全局变量的定义在整个项目中只能出现一次,每个包含该头文件的源文件都会定义这个全局变量,导致编译器在链接阶段出现“重复定义”的错误。

头文件中是否可以定义静态全局变量?

可以但是不推荐。

在头文件中定义静态全局变量不会引起编译器错误,但是这样做是无意义的。

所有包含该头文件的源文件中,都会为这个静态变量分配空间,不管有没有用到,造成资源的浪费。

关键字 auto 的作用是什么?

auto 关键字用于指定局部变量的自动存储类型,并没有什么实际的用途,局部变量默认就是自动局部变量,省略了 auto 关键字。

关键字 register 的作用是什么?使用时需要注意什么?

(1)作用:编译器会将register修饰的变量尽可能地放在CPU的寄存器中,以加快其 存取速度,一般用于频繁使用的变量。

(2)注意:register变量可能不存放在内存中,所以不能用&来获取该变量的地址; 只有局部变量和形参可以作为register变量;寄存器数量有限,不能定义过多register 变量。

存在寄存器里面,即 cpu,该值不能取地址操作,并且是整数,不能是浮点数

%操作必须两边都是整数

C 语言中不能用来表示整常数的进制是二进制

二、变量与数据类型

优先级

()[].-> !~ ++ -- &* /%* +- << >>== >< &|^ &&|| ?: = += -= , 1. 优先级,简单记就是:! > 算术运算符 > 关系运算符 > && > || > 赋值运算符 2. 单目优于双目 3. 单目、条件、赋值是右左结合 4. !表示取非,对于整形变量,只要不为 0,使用 ! 取反都是 0,0 取反就是 1。就 像 bool 只有真假一样 5. ~取反,代表位的取反,对于整形变量,对每一个二进制位进行取反,0 变 1,1 变 0。

\6. ? : 三目运算, 7. 源码、补码、反码

在32和64位系统中数据类型的大小?

gets 和 scanf 函数的区别?

  • 空格:

    • gets 函数可以接受空格

    • scanf 遇到空格就会结束

  • 输入类型:

    • gets 函数仅用于读入字符串

    • scanf 为格式化输出函数,可以读入任意 C 语言基础类型的变量值,而不是仅限于字符串(char*)类型

  • 返回值:

    • gets 的返回值为 char*型,当读入成功时会返回输入的字符串指针地址,出错时返回 NULL

    • scanf 返回值为 int 型,返回实际成功赋值的变量个数,当遇到文件结尾标识时返回 EOF

无符号数和有符号数的运算

有符号数和无符号数运算时,有符号数转无符号数,运算的时候是补码运算,而不是原码。

void foo(void) 
{ 
 unsigned int a = 6; 
 int b = -20; 
 (a + b > 6)? printf("> 6") : printf(" <= 6"); 
} 

-20变成了一个非常大的正整数,所以该表达式计算出的结果 ”>6”

unsigned int a = 1; 
int b = 0; 
int c = 0; 
c = a + b > 0 ? 1 : 2; 

b的补码还是0,a+b=1,因此c = 1

数据类型的转换

  • 隐式转换:在某些情况下,编译器会自动进行转换。

  • 显式转换:使用强制类型转换来转换。

也可以这样问你:不同数据类型之间的赋值规则?

(1)整数与整数(char、short、int、long)

  • 长 -> 短:

  • 短 -> 长:

(2)整数与浮点数

  • 浮点数 -> 整数:截取整数部分,丢弃小数部分。

  • 整数 -> 浮点数:小数部分为0

(3)float 与 double

  • double -> float:丢失精度

  • float -> double:不会丢失精度

①长度相等:内存中的数据不变,只是按不同的编码格式来解析。 ②长赋值给短:截取低位,然后按短整数的数据类型解析。 ③短赋值给长:如果都是无符号数,短整数高位补0;如果都是有符号数,短整数高位 补符号数;如果一个有符号一个无符号,那么先将短整数进行位数扩展,过程中保持数

据不变,然后按照长整数的数据类型解析数据。

字符与 ASCII 之间的转换

字符转 ASCII:

char ch = 'A';
int ascii_value = (int)ch;  // 将字符转换为对应的ASCII值

ASCII 转字符:

 int ascii_value = 65;
 char ch = (char)ascii_value;  

记住常用的 ASCII 码:

  • 0:48

  • A:65

  • a:97

字符、字符串与整数的转换

#include <stdio.h>
#include <stdlib.h>  // 包含 atoi 函数的头文件

int main() {
    const char *str = "1234";    // 也可以是负数;也可以是1234abc,到a就停止了,因此1234
    int num = atoi(str);
    printf("The converted integer is %d\n", num);
    return 0;
}

字符 0 ~ 9 和数字之间的转换

char ch = '5';
int num = ch - '0';  // '5' 的 ASCII 值减去 '0' 的 ASCII 值

int num = 7;
char ch = num + '0';  // 数字加上 '0' 的 ASCII 值

交换两个变量的值,要求不使用第三个变量

(1)算术运算:

a = a + b;
b = a - b;
a = a - b;

(2)异或(只能是int、char)

a = a^b;
b = a^b;
a = a^b;

或者

a ^= b ^= a;

全局变量和局部变量的区别是什么?

  1. 作用域不同:全局变量的作用域为整个程序,局部变量的作用域为当前函数。

  2. 内存分配不同:全局变量分配在全局数据区,局部变量分配在栈区。

  3. 生命周期不同:全局变量从程序开始执行到程序结束,局部变量在函数或代码块执行完毕后就会被释放。

  4. 初始值不同:全局变量具有默认值,局部变量没有默认值,如果没有初始化它的值是不定的,可能是栈上的垃圾值。

局部变量能否和全局变量重名?

可以。

在局部变量的作用域内(局部变量所在的代码块)会屏蔽全局变量。

但是不建议这么做,写代码时可能会混淆。

在C语言中,使用 {extern int num} 这种形式可以在代码块中访问全局变量,在C++中可以使用“::”。

a++和++a的运算过程?

a++

int temp = a;
a=a+1;
return temp;

后置自增运算符需要把原来变量的值复制到一个临时的存储空间,等运算结束后才会返回这个临时变量的值。

++a

a=a+1;
return a;

所以前置自增运算符效率比后置自增要高。

bool布尔类型包含在哪个头文件中?

在 C99 标准中位于 stdbool.h 文件中

C89 标准中则不支持,需要自己定义:

#define TRUE 1 
#define FALSE 0 
typedef int bool; 

结构体和联合体的区别?

相同:结构体和联合体都是用户可自定义的数据结构,内部可包含不同数据类型的成员。

不同:

  • 结构体中每个成员都有独立的内存空间,可以同时访问和修改;联合体的所有成员共享同一块内存空间,一次只能访问一个成员,并且修改一个成员的值会影响其他成员。

  • 结构体的总大小由于内存对齐,可能不等于所有成员大小之和,联合体的总大小等于其最大成员的大小。

使用场景:

  • 结构体:用于描述一个具有多个属性的实体,适用于需要同时存储和访问多个相关数据的情况。

  • 联合体:用于节省内存,适合存储不同类型但不会同时使用的数据。

结构体如何对齐?为什么要对齐?

C语言——结构体(struct)对齐_结构体对齐-CSDN博客

结构体对齐规则:

  • 总大小:

  • 成员:

    • 数组类型:

    • 结构体:

结构体总大小的对齐规则:

一个结构体的总共内存大小是其类型最大成员大小的整数倍。如果结构体的最后一个成员之后没有足够的空间来满足对齐要求,编译器会在结构体末尾添加填充字节。

结构体成员的对齐规则:

结构体的每个成员相对于结构体开头的偏移量是该成员大小的整数倍。如果成员的大小小于对齐要求,编译器会在成员之间插入填充字节以满足对齐要求。

数组和结构体的对齐规则:

数组中的每个元素都要满足上述对齐规则,即数组的每个元素都相对于数组开头的偏移量是其大小的整数倍。对于嵌套的结构体,嵌套的结构体本身也要满足其内部的对齐规则,并且嵌套结构体相对于外部结构体的偏移量也要满足对齐要求。

举个例子:

struct A 
{
 char t : 4; // 4位
 char k : 4; // 4位
 unsigned short i : 8; // 8位
 unsigned long m; // 4字节 
}; 

如果使用 pack() 指令,则结果不同,下面的占用 4+8+1 个字节。

#pragma pack(1) 
struct fun 
{ 
 int i; // 4字节 
 double d; // 8字节 
 char c; // 1字节 
}; 

联合体

两大准则:

结构体对齐的原因?

内存对齐是指数据在内存中存储时,其地址必须是某个特定的边界值(比如 4 字节或 8 字节)的整数倍。内 存对齐非常重要,因为许多处理器在访问未对齐的内存时效率会降低,甚至会引发异常。对齐可以提高内存访问 的效率,确保系统的稳定性。

(1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据。 (2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐,因为访问未 对齐的内存,处理器需要做两次内存访问,而访问对齐的内存仅需要一次。如下图所 示,访问对齐的short变量只需要一次,而访问未对齐的int变量则需要访问两次。

  • 提高性能:以空间换时间,许多处理器更高效地访问对齐的数据。

  • 硬件限制:一些处理器不允许未对齐的访问,否则会引发错误。

表达式与流程控制

什么是左值和右值?

左值是指可以出现在等号左边的变量或表达式,它的值可以被修改。

右值是指只可以出现在等号右边的变量或表达式,它最重要的特点是可读。

一般的使用场景都是把一个右值赋值给一个左值。

通常,左值可以作为右值,但是右值不一定可以作为左值。

什么是短路求值?

短路求值通常用于条件判断语句中的表达式,是逻辑运算中的一种优化策略,它使得在布尔表达式的计算过程中,当结果可以提前确定时,跳过不必要的计算。

它不仅提高了程序的效率,还能避免一些潜在的运行时错误。

逻辑与(&&):如果左侧为false,那结果必然为false,右侧就不用计算了。

逻辑或(||):如果左侧为true,那结果必然为true,右侧就不用计算了。

举例,避免一些潜在的运行时错误。

struct Node {
    int value;
    struct Node* next;
};

void printNodeValue(struct Node* node) {
    if (node != NULL && node->value > 0) {
        printf("Node value is %d\n", node->value);
    } else {
        printf("Node is NULL or value is not greater than 0\n");
    }
}

运算符的优先级

sum = a & b << c + a^c; 其中a=3,b=5,c=4,则sum = ?

先加再移位再&再异或,答案为4

介绍一下while和do while的区别

相同条件下,do while 至少运行一次。

break和continue的作用以及区别?

continue

语句的作用是跳过本次循环体中余下尚未执行的语句,立即进行下一次的循环条件判定,可 以理解为仅结束本次循环。 注意:continue 语句并没有使整个循环终止。 只能用在循环语句

break

只能用于循环语句或者开关语句 会使最近包含 break 语句跳出 在 while for、循环中 跳出本层循环 在 swich-case 中跳出对应的 case 如果 for(){switch case: break},for 循环不受影响

死循环的实现方式

for(;;) {}
while (1) {}

写出 float x 与零值的比较语句

if(x > -0.000001 && x < 0.000001);

计算机在处理浮点数的时候是有误差的,所以不能直接与0进行等于的判断,而是和阈值比较。

如果是 double 类型,则阈值应该更小。

函数

函数指针怎么定义的?

void (*pFunc)(int, int);
作用:
1.回调函数:函数指针可以作为参数传递给另一个函数,以便在需要时回调该函数。
2.函数的动态调用:函数指针可以根据不同的条件指向不同的函数,以便在运行时动态地调用不同的函
数。

int (*pf)(float);
函数指针调用函数事注意以下几点:
1. 函数类型必须和函数指针的类型一样,比如参数类型,返回值
2. 给函数指针赋值是可以&也可以不要
比如:

main 函数怎么接收传入的参数?

main 函数可以从命令行获取参数,从而提高代码的复用性。

int main(int argc , char* argv[],char* envp[]);
  • argc:传入参数的个数

  • argv:参数字符串数组

    • argv[0]:指向程序运行的绝对路径

    • argv[1]:第一个参数

    • argv[2]:第二个参数

    • argv[n] 为 NULL 时,表示参数结束

  • envp:与用户环境变量有关

如何在 main 函数之前执行函数?

attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。

gnu对于函数属性主要设置的关键字如下:

  • alias:设置函数别名

  • aligned:设置函数对齐方式

  • always_inline/gnu_inline:函数是否是内联函数。

  • constructor/destructor:主函数执行之前、之后执行的函数。

  • format:指定变参函数的格式输入字符串所在函数位置以及对应格式输出的位置。

  • noreturn:指定这个函数没有返回值。

    请注意,这里的没有返回值,并不是返回值是void。而是像 _exit/exit/abord 那样执行完函数之后进程就结束的函数。

  • weak:指定函数属性为弱属性,而不是全局属性,一旦全局函数名称和指定的函数名称 命名有冲突,使用全局函数名称。

#include <stdio.h>

void before() attribute ((constructor)); void after() attribute ((destructor));

void before() {
printf("this is function %s\n", func ); return;
}

void after(){
printf("this is function %s\n", func ); return;
}

int main(){
printf("this is function %s\n", func ); return 0;
}

// 输出结果
// this is function before
// this is function main
// this is function after

不能做 switch() 的参数类型是?

switch() 语句的参数类型只能是整型数据(int 、 char 和 enum枚举等),不能是浮点型( float 、 double )、指针或其他类型。

内联函数的优缺点和适用场景是什么?

在C语言中,函数调用是一个复杂的过程,比如调用者参数入栈、返回地址入栈、基址地址保存、栈指针移动和修改程序计数器等。

如果一些函数被频繁调用,不仅会造成栈空间的大量消耗,效率还不高。

(1)优点:内联函数与宏定义一样会在使用的地方展开,省去了函数调用开销,从而提高函数的执行效率,同时又能做类型检查。

(2)缺点:它会使程序的代码量增大,消耗更多内存空间。

适用场景:函数体内没有循环(执行时间短)且代码简短(占用内存空间小)。

宏函数和内联函数的区别

内联函数: 由编译器处理,避免了函数调用的开销。 支持类型检查和调试。 宏: 由预处理器处理,直接进行文本替换。 不支持类型检查,可能导致错误和调试困难。

1.宏函数不是函数,是宏定义,只是使用起来像函数,宏函数是在预编译的时候把 所有的宏名用宏体来替换,简单的说就是字符串替换 2.内联函数,内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简 单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不 能直接调用自身

3.宏函数和内联函数如何提高效率? 内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地 方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率 宏函数则是在预处理的时候把宏体展开减去了参数压栈,函数调用,返回值等 操作 4.宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型 的检查,内联函数满足函数的性质,比如有返回值、参数列表

函数参数传递的方式?

  • 值传递

  • 指针传递

(1)两种:值传递、指针传递。

(2)严格来看,只有一种传递,值传递,指针传递也是按值传递的,复制的是地 址。

C语言是怎么进行函数调用的?

EBP(基址指针)用于维护栈帧的边界,指向当前函数的栈帧起始位置,参数在 EBP 的正偏移处,而局部变量在 EBP 的负偏移处。

ESP(栈指针)用于指示栈的当前顶端。

函数调用步骤:

  1. 调用者:参数由右向左入栈、返回地址入栈

  2. 被调用者入栈:

    • 将调用者的 EBP 入栈,将当前的 ESP 保存到 EBP 中,形成新的栈帧基址。

    • 被调用函数的局部变量、临时值等入栈

  3. 修改程序计数器的值,跳转到被调用的函数,执行函数

  4. 函数执行完毕,如果有返回值,将返回值保存在寄存器中

  5. 恢复调用者的EBP,调整ESP

  6. 根据返回地址跳转,继续执行调用后的代码

一般来说,参数的传递规则是前 4 个参数通过寄存器传递,后面的参数使用堆栈传递。

但实际编程中,依据环境的不同会有所出入,如在函数中需要引用变量的地址,此时就不可能放在寄存器中了。

还是比较复杂的。

数组

数组的定义和初始化

对于二维数组可以 Int num 第一个【】可以不填,但是第二个必须填,但是这个数组必须初始化 对于一维数组 Int num[] 这【】可以不填但是这个数组必须初始化 总结: 数组的最左边的[]可以不填,这个时候数组必须初始化,二维数组的第二个[]必须 填,不管有没有初始化 int a; //不允许 int b={1,2,3,4};//可以 int c[] = {1,2,3};可以 Int c[]; //不可以 Int d//不允许,第二个[]必须填,不管有没有初始化 对于二维数组来说,第一维就是最左边的[],也就是行数

变长数组是什么?

在C99标准中,定义数组时[]中的值可以是整型变量或整型表达式:

#include<stdio.h> 
void main() 
{ 
     int n; 
     scanf(“%d”, &n); 
     int array[n]; 
} 

数组的指针表示

在二维数组中:

  • *(a[1]+1) : a[1]是第2行的地址,a[1]+1第2行第2列,指针解引用得到 a[1] [1]

  • *(&a[1][1]) : []优先级高,a[1] [1]取地址再取值,即 a[1] [1]

  • (*(a+1))[1] : * (a+1)=a[1],即 a[1] [1]

数组首元素地址和数组地址的异同?

不同:

  • (a)数组首元素地址,表示的是数组第一个元素的地址,地址+1得到的是第2个元素的地址

  • (&a)数组地址,表示的是数组占用的这块内存的开始地址,地址+1得到的是结束地址后面的地址。

相同:

数组首元素地址和数组地址的值是相等的,都可以获取到第一个元素。

字符串和字符数组的区别

数组的下标可以为负数吗?

可以,下标是指与当前地址的偏移量,只要根据这个偏移量能定位得到目标地址即可。

当指针指向数组的最后一个元素时,就可以使用负数下标遍历数组。

#include <stdio.h>
int main()
{
	int i:
	int a[5]={0,1,2,3,4};
	int *p=&a[4]
	for(i=-4;i<=0;i++)
	printf("%d %x\n", p[i], &p[i]);
		return O. 10
}

//输出结果为
//0 b3ecf480
//1 b3ecf484
//2 b3ecf488
//3 b3ecf48c
//4 b3ecf490---------

指针

使用指针可能发生的错误?

(1)空指针解引用:当指针未初始化或指向无效的内存地址时,对该指针进行解引用操作会导致程序崩溃。例如:

int* ptr;
*ptr = 10; // 错误,ptr 未初始化

解决方法是在使用指针之前确保它指向有效的内存地址。可以通过初始化指针、动态分配内存或从函数中返回有效的指针来解决这个问题。

(2)内存泄漏:当动态分配的内存没有被正确释放时,会导致内存泄漏。例如:

int* ptr = (int*)malloc(sizeof(int));
// 没有释放 ptr 所指向的内存

解决方法是不再需要动态分配的内存时,使用 free 函数释放内存。可以在适当的位置添加释放内存的代码,以确保不会发生内存泄漏。

(3)多次释放:当对同一个动态分配的内存进行多次释放时,会导致程序崩溃。例如:

int* ptr = (int*)malloc(sizeof(int));
free(ptr);
free(ptr); // 错误,双重释放

解决方法是确保每个动态分配的内存只被释放一次。可以使用标志或其他机制来跟踪已释放的内存,以避免重复释放。

(4)指针越界访问:当指针访问超出其所指向的内存范围时,会导致未定义的行为。例如:

int arr[5];
int* ptr = arr;
for (int i = 0; i < 10; i++) {
    ptr[i] = i; // 错误,指针越界访问
}

解决方法是确保指针的访问在合法的内存范围内。可以通过检查数组的边界、使用指针的偏移量时进行边界检查等方法来避免指针越界访问。

什么是野指针?产生的原因和解决方法?

野指针不是 NULL 指针,而是指向不可用内存地址的指针,对野指针进行操作的话,会导致程序发生不可预知的错误。

产生野指针原因:

  • 指针变量没有初始化,就会随机的指向一个地址

  • 指针变量指向的内存释放后,没有指向NULL,还是指向原来的地址

  • 指针变量的操作超出了作用范围

避免野指针:

  • 创建指针变量的时候初始化为NULL

  • 申请内存的时候检查是否申请成功,如果申请失败就赋值为NULL

  • 指针用完后释放内存,并设置为NULL

注:malloc函数分配完内存后需注意

  • 检查是否分配成功

  • 清空内存中的数据(使用 memset 或 bzero 函数)

指针能否加减运算?

指针相加是没有任何意义的,而且容易得到非法的地址,导致程序异常,因此C语言不允许此操作。

在数组或块内存(使用malloc申请的堆内存)中,指针变量相减得到的是元素的个数。

数组指针和指针数组有什么区别

(1)数组指针

区别于将数组直接赋给给指针,那样指针拿到的是数组的首地址。

数组指针指向的是完整的数组,而不仅仅是数组的首元素,可以使用数组指针操作数组。

感觉数组指针就像是给数组取了个别名,arr = (*p)

int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr;  // p 是指向包含 5 个 int 的数组的指针,注意需要取地址符

// 通过数组指针访问数组元素
printf("%d\n", (*p)[0]); // 输出 1
printf("%d\n", (*p)[1]); // 输出 2

// 获取元素的地址:&(*p)[0]

(2)指针数组

指针数组是指一个数组中的元素类型是指针。

int a = 1, b = 2, c = 3;
int *arr[3] = {&a, &b, &c};  // 指针数组,包含 3 个 int 指针

// 通过指针数组访问变量
printf("%d\n", *arr[0]); // 输出 1
printf("%d\n", *arr[1]); // 输出 2

函数指针和指针函数有什么区别

函数指针:指向函数的指针变量,可以使用这个指针调用函数。

使用场景:

  • 实现函数的动态调用。在程序运行过程中,可以根据不同的条件选择不同的函数来执行。

  • 作为参数传递

  • 用于回调函数

使函数的调用更加灵活,提高程序的灵活性和可扩展性。

(int (*)(int))a;

使用场景:比如驱动开发中,字符设备驱动的操作函数集。

#include <stdio.h>

void sayHello() {
    printf("Hello, World!\n");
}

int main() {
    // 定义一个函数指针,该指针可以指向一个返回值为 void 且无参数的函数
    void (*funcPtr)();
    
    // 将函数的地址赋值给函数指针
    funcPtr = sayHello;
    
    // 通过函数指针调用函数
    funcPtr();
    
    return 0;
}

指针函数:函数的返回值为指针类型。

使用场景:堆区内存分配 malloc

数组名和指针的区别和联系?

数组名指向的是数组中第一个元素的地址,&数组名表示整个数组,使用一个指针指向数组名,就可以以指针的方式遍历和修改元素。

  • 访问元素方式不同:

    • 可使用数组名+偏移量的方式访问元素。

    • 指针解引用后才能访问元素。

  • 指针能进行算术运算,比如自增和自减,但是数组名是常量,无法修改。

位运算

如何不影响其它位,置1和清0?

#define BIT3 (0x1<<3)

static int a;

// 置 1
void set_bit3(void) 4	{
	a |= BIT3;
}

// 清 0
void clear_bit3(void) 8	{
	a &= ~BIT3;
}

字符串操作

操作字符串的函数:

  • 字符串比较:

  • 字符串拷贝:

size_t strlen(const char *s);
char *strcpy(char *dest, const char *src)
int strcmp(const char *s1, const char *s2);
char *strcat(char *dest, const char *src);
void *memset(void *s, int c, size_t n);
int atoi(const char *nptr)

C语言中如何处理内存泄漏?
答案:内存泄漏发生在程序分配了内存但未能释放或丢失对已分配内存的引用。处理内存泄漏
的方法包括:
1. 正确使用内存分配和释放函数:如 malloc 、 calloc 、 realloc 和 free 。
2. 使用工具进行内存检测:如Valgrind、AddressSanitizer等。
3. 保持良好的编码习惯:如确保每个 malloc 有对应的 free ,避免忘记释放内存。
4. 分析代码:检查代码路径是否确保释放每个分配的内存块。

5.char *p="abcd"和char p[]="abcd"一样吗?有啥区别;

字符串的几种表示方法?

(1)指向字符串字面量的指针

只有这种方式,字符串的值无法修改。

char *str1 = "hello";

(2)字符数组

char str2[] = "abcd";

字符数组的其他:

char str3[10] = "hello\0";  // 后面的都被填充\0

手动赋值

char str[6];
str[0] = 'H';
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';
str[5] = '\0';

动态分配方式

char *str = (char *)malloc(20 * sizeof(char));
strcpy(str, "Hello, world!");

strcat、strncat、strcmp、strcpy 哪些函数会导致内存溢出?如何改进?no

(1)字符串拷贝:strcpy、strncpy、strcpy_s

1、strcpy 会导致内存溢出,函数原型:char *strcpy(char *strDest,const char *strSrc)

不会对目标地址的内存大小进行判断,没有任何检查措施。

2、strncpy_s 是安全的

(2)字符串比较:strcmp

strcmp(str1,str2),若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数。

(3)字符串连接:strcat、strncat

strcpy 和 memcpy 的区别

memcpy 可以拷贝任意类型的数据,strcpy 只能拷贝字符串类型。

简述 strcpy、sprintf 与 memcpy 的区别

三者主要有以下不同之处: (1)操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型, 目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。 (2)执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。 (3)实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字 符串的转化,memcpy 主要是内存块间的拷贝。 说明:strcpy、sprintf 与 memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来 选择合适的函数实现拷贝功能。

short i = 0; i = i + 1L;这两句有错吗

代码一是错的,代码二是正确的。 说明:在数据安全的情况下大类型的数据向小类型的数据转换一定要显示的强制类型转换。

&&和&、||和|有什么区别

(1)&和|对操作数进行求值运算,&&和||只是判断逻辑关系

(2)&&和||在在判断左侧操作数就能确定结果的情况下就不再对右侧操作数求值。

内存管理

C语言中如何申请和释放内存?

使用 malloc 函数申请

使用 free 函数释放

malloc 的底层实现?

(1)malloc函数的底层实现是操作系统有一个由可用内存块连接成的空闲链表。调 用malloc时,它将遍历该链表寻找足够大的内存空间,将该块一分为二(一块与用户申请的大小相等,另一块为剩下来的碎片,会返回链表),调用free函数时,内存块 重新连接回链表。

(2)若内存块过于琐碎无法满足用户需求,则操作系统会合并相邻的内存块。

内存拷贝

什么是深拷贝和浅拷贝?

什么是指针引用和值引用?

如何拷贝内存?

使用 memcpy

memcpy 和 memmove 的区别?

相同:都是用来拷贝n个字节到目标地址上

不同:

(2)不同的是,当src和dest所指的内存区域重叠时,memcpy可能无法正确处理, 而memmove()仍然可以正确处理,不过执行效率上略慢些。

(1)memcpy()无论什么情况下,都是从前往后拷贝内存。当源地址在前,目的地址 在后,且两个区域有重叠时,会造成拷贝错误,达不到理想中的效果。

(2)memmove()则分两种情况:目的地址在前,源地址在后的情况下,从前往后拷 贝内容。否则从后往前拷贝内容。无论什么情况都能达到理想中的效果。

内存分配的方式有几种?

  • 静态内存分配:内存分配在程序编译时已经完成,并且在程序整个生命周期内都存在,例如全局变量、静态变量等。

  • 栈内存分配:栈区内存用于存储函数的局部变量和参数等,函数调用时分配,函数返回时释放。

  • 动态内存分配(堆内存分配):堆内存用于动态内存分配,在程序中可随时申请和释放。

  • 内存映射:使用 mmap 函数将文件或设备映射到内存中,适用于大文件的访问或共享内存。映射区 域可以被读取、写入或执行。

静态内存和动态内存的区别?

  • 静态内存分配在编译时完成,不占用CPU资源; 动态内存分配在运行时,分配与释放都占用CPU资源。

  • 静态内存在栈上分配; 动态内存在堆上分配。

  • 动态内存分配需要配合指针和引用使用,静态不需要。

  • 静态内存分配是按计划分配,由编译器负责; 动态内存分配是按需分配,由程序员负责。

内存分布

C语言程序的内存分布?

内存四区,gcc 编译的C语言程序的内存分布。

(1)栈区 stack

栈区由高地址向低地址增长,存储函数的局部变量、函数参数、返回地址等。

发生函数调用时,栈指针向低地址方向移动;函数返回时,栈指针回移。

(2)堆区 heap

堆区由低地址向高地址增长,可在程序中动态的申请和释放。

(3)全局(静态)区 static

  • (.data)已初始化数据区:存放初始化不为0的全局变量和静态变量、const型常量。

  • (.bss)未初始化数据区:存放未初始化或初始化为0的全局变量和静态变量。

(4)代码区

存储程序的二进制可执行代码和字符串常量等。

只读区域,防止程序运行时修改代码。

常量区只是一个逻辑上的区域,是代码区的一部分,存储字符串常量和一些使用 const 关键字修饰的变量等,程序在运行时无法修改这些常量。

#include <stdio.h> int a = 0;	// 数据段
char *p1;                       // BSS段

int main() {
    int b; // 栈
    char s[] = "abc";//	栈
    char *p2;//	栈
    char *p3 = "123456"; // 123456\0在常量区,p3在栈上。static int c =0; // BSS段
    Class c1 = new Class();//new出的对象就在堆区
    strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
    return 0;
}

堆与栈有什么区别?

注意:栈经常被叫做堆栈,跟堆没有关系。

  1. 申请方式

    栈的空间由操作系统自动分配和释放,堆上的空间需要手动申请和释放。

  2. 申请大小的限制

    1. 栈空间有限。栈是向低地址扩展的一块连续的内存区域。即栈的最大容量是预先规定好,如果程序运行时栈空间不足(比如递归调用过深),将提示栈溢出。

    2. 堆是向高地址扩展的不连续的内存区域(内存中的空闲地址是不连续的)。堆的大小受限于计算机系统中有效的虚拟内存。

  3. 申请效率

    栈内存的分配和释放速度非常快,因为它遵循后进先出原则。每次分配内存只需调整栈指针即可,无需复杂的管理。

    堆内存的分配和释放速度相对较慢。由于堆内存是非连续的,内存分配涉及更多的管理工作,如查找空闲块、合并或拆分内存块等。此外,频繁的内存分配和释放可能导致内存碎片,从而影响性能。

栈有什么作用?

  • 栈用于处理函数的调用和返回,保存函数的参数和局部变量、返回地址、寄存器值等。使用栈能很好的解决临时值存取和现场保存和恢复的场景。

  • 栈是多线程编程的基础,每个线程都有自己独立的栈空间,操作系统中的中断处理也有自己栈空间,使用场景广泛。

栈为什么由高地址向低地址?

假如栈往高涨的话,那么要确定堆和栈的分界线,每一个程序的堆和栈都一样,所以这个分界线不好确定,所以一个往上涨, 一个往下长可以最大化的利用内存的空间。

堆溢出原因?

堆内存溢出可能是堆的尺寸设置得过小/动态申请的内存没有释放。

栈溢出原因?

(3)栈内存溢出可能是栈的尺寸设置得过小/递归层次太深/函数调用层次过深/分配了 过大的局部变量。

堆栈溢出是指超过了系统的堆栈容量,常由于递归过深或局部变量过多导致。

解决办法:避免过深的递归和过大的局部变量。

函数参数压栈顺序?

从右往左。

主要原因是为了支持可变长参数形式,否则第一个参数被压入了栈底,无法根据栈指针的相对位移找到它。

什么是内存泄漏?产生的原因?

申请的内存使用后没有释放掉,导致这块内存一直被占用,其它程序使用不了。

内存泄露的危害是程序运行时间越长,占用内存越多,最终用尽全部内存,甚至会导致系统崩溃。

产生的原因:

  • 内存使用完后没有释放

  • 申请内存的时候没有指针指向它

如何避免内存泄漏?

  • 养成良好的编码习惯,申请内存的时候使用指针接收它的返回值,使用完毕一定要释放内存(特别是在函数返回前)。

  • 可以使用内存调试工具帮助检测内存泄漏和其他内存管理错误。

  • 使用一些第三方库,支持垃圾收集库。

什么是内存溢出?

内存溢出(Out Of Memory)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于系统能提供的最大内存。

此时程序无法 运行,系统提示内存溢出。有时候会自动关闭软件。

原因:

  • 内存泄漏的堆积最终导致内存溢出。

  • 需要保存多个耗用内存过大的对象或加载单个超大的对象时,其大小超过了当前剩 余的可用内存空间。

内存溢出和内存越界的区别?

(1)内存溢出:要求分配的内存超出了系统所能给予的,于是产生溢出。

(2)内存越界:向系统申请了一块内存,而在使用时超出了申请的范围,常见的是 数组访问越界。

malloc 能申请多大的空间?

  • 受限于进程的地址空间,比如32位系统上是4G。

  • 受限于虚拟地址的最大容量,而虚拟地址又由物理内存和交换空间等决定。

  • 受限于内存碎片化,可能碎片化很严重,导致无法找到足够大的连续内存块。

  • 其他进程可能占用了过多空间,导致可申请的减少。

  • ......

因此,malloc 能申请的最大空间是无法预估的。

\4. 串口UART与RS232、RS485的区别?

\6. FreeRTOS和RT-Thread有什么区别?

\9. 在FreeRTOS中若是配置为非礼让+非抢占,则当前任务会一直得到执行,为什么? \10. 项目让你最难受的地方,分析思路和解决思路? \11. 串口中断中数据是怎么处理的? \12. 串口数据接收,如果一个较大的数据包发送过来(1K字节以上,带帧头、帧长和校验码)你怎么解析和处理?

\15. 冒泡排序的思路是什么?解释一下时间复杂度的计算?为什么是O(N^2)? \16. 链表有二分查找吗?一般什么情况下用二分查找? \17. DFS, BFS算法解释一下。 \18. 裸机开发的怎么实现一个软件定时器?如何定时处理100个任务?

2.两个有序链表怎么合并(归并排序的子部分呗就是);

7.进程同步有啥手段?那进程通信呢?

8.Socket编程了解过没,信号了解过没?

9.说说你对信号量的理解;

10.介绍一下你对计算机网络的理解;

11.TCP的拥塞控制算法懂吗,说说;

链接:https://www.nowcoder.com/interview/center?entranceType=%E5%AF%BC%E8%88%AA%E6%A0%8F

围绕项目问加八股

介绍一下使用到的去雾算法

定义一个指向存放十个整形指针的数组的指针

堆栈的区别

链接:https://www.nowcoder.com/interview/center?entranceType=%E5%AF%BC%E8%88%AA%E6%A0%8F

叙述一下链表

  • STM32的具体型号与频率;

还有pwm是怎么产生出来的,由什么参数决定的,可以解释一下各个参数嘛(就psc,ccr,arr那些)

从代码安全角度分析memcpy函数存在的问题?给出修改方案

其它

原码、反码、补码分别是什么?

整型数值在计算机的存储里,最左边的一位代表符号位,0代表正数,1代表负数。

整数在内存中都是以补码的形式存储的。

  • 正数:原码、反码、补码都一样。

  • 负数:

    • 原码:10001010 表示 -10

    • 补码:除了符号位,其余取反。11110101

    • 反码:补码+1。11110110

程序编译的步骤

四个阶段:

  • 预处理:处理所有的预处理命令,如宏定义、条件编译指令、⽂件包含指令(头文件复制拷贝)等

  • 编译:进⾏词法分析、语法分析、语义分析后,将代码翻译成汇编指令

  • 汇编:将汇编指令翻译成机器指令,得到二进制目标文件

  • 链接:将多个二进制⽬标⽂件进⾏链接,得到最终的可执⾏⽂件

编译和链接有什么不同?

  • 编译+汇编是为了生成目标文件(*.o)。编译过程中对于外部符号(如用extern 跨文件引用的全局变量)不做任何解释和处理,外部符号对应的就是“符号”。

  • 链接生成的是可执行程序。链接将会解释和处理外部符号,外部符号对应的是 地址。

如何判断大小端?

(1)使用联合体判断

原理:

  • 利用结构体从低地址开始存,且同一时间内只有一个成员占有内存的特性。

  • 结构体中定义 int 和 char 成员,然后给 int 成员赋值为1,即0x00000001

  • 如果是小端模式,则得到的是0x01,否则得到的是0x00

#include <stdio.h>

int main(){
    union w
    {
        int a;
        char b;
    }c;
	c.a = 1;
    if(c.b == 1)
        printf("小端存储\n");
    else
        printf("大端存储\n");
    return 0;
}

(2)使用指针判断

原理:

  • 指针指向的是变量的起始地址

  • 使用 char 指针指向 int 的地址

  • 如果得到的指针值是0x01说明是从低地址开始存的,即小端模式,反之则为大端模式。

#include <stdio.h>
int main ()
{
    int a = 1;
    char *p = (char *)&a;
    if(*p == 1)
        printf("小端存储\n");
    else
        printf("大端存储\n");
    return 0;
}

C语言如何表示一个寄存器地址

如何使用地址操作寄存器?

(1)C语言

volatile uint32_t* address = (*(volatile uint32_t *)0x100000) 
*address = value;

0x100000是寄存器地址,但是仅仅是一个数字而已,因此需要进行强制类型转换,转换成指针,这样C语言才能在解引用后对寄存器操作。

(2)汇编

LDR R0, =0x100000 ; 将地址 0x100000 加载到寄存器 R0
LDR R1, =value ; 将 value 加载到寄存器 R1 
STR R1, [R0] ; 将 R1 的值存储到 R0 指向的地址

哪些函数不能在中断中使用?

答:动态内存分配函数:例如 malloc()、free()等函数,因为在中断中动态分配 和释放内存可能导致内存碎片问题,甚至造成内存泄漏。 阻塞函数:包括像 sleep(),delay()等需要等待一段时间的函数,因为在中断服 务程序中阻塞会影响系统的实时性。 信号量操作函数:如 sem_wait()、sem_post()等,因为这些函数可能会导致死 锁或竞争条件。 文件操作函数:例如 fopen()、fclose()等,因为在中断服务程序中访问文件可能 会引起不可预测的结果。

什么叫不可重入函数?

答:不可重入函数是指在同一时间只能被一个任务或线程执行的函数,不可重入 函数会使用一些全局变量或静态变量来保存状态信息,如果在该函数执行过程中 被中断,并在中断处理程序或其他任务中再次调用该函数,那么全局状态可能会 被破坏,导致函数无法正确执行。

如何使用C语言实现一个简单的状态机?请给出代码示例。

状态机用于描述系统状态及状态转移:

typedef enum { STATE_IDLE, STATE_RUN, STATE_STOP } State;
void processState(State *currentState) {
switch (*currentState) {
case STATE_IDLE:
// Transition logic
*currentState = STATE_RUN;
break;
case STATE_RUN:
// Transition logic
*currentState = STATE_STOP;
break;
case STATE_STOP:
// Transition logic
*currentState = STATE_IDLE;
break;
}
}

Linux内核malloc()背后的实现原理——内存池_linux c++ alloc-CSDN博客

嵌软笔试面试——基础 (coucoublog.netlify.app)

嵌软笔试面试——经典题 (coucoublog.netlify.app)

【嵌入式面试】嵌入式经典面试题汇总(C语言)-CSDN博客

嵌入式 C语言常见面试试题集锦_嵌入式c语言面试常见问题-CSDN博客

2021秋招嵌入式笔试面试题目笔经面经牛客网 (nowcoder.com)

结构体 https://blog.csdn.net/Hush_H/article/details/127169690

详解main函数参数argc、argv及如何传参-CSDN博客


数据结构、算法

简述一下冒泡排序?