【常问基础】04.总结,其他
【嵌入式八股】一、语言篇(本专栏)https://www.nowcoder.com/creation/manager/columnDetail/mwQPeM
【嵌入式八股】二、计算机基础篇https://www.nowcoder.com/creation/manager/columnDetail/Mg5Lym
【嵌入式八股】三、硬件篇https://www.nowcoder.com/creation/manager/columnDetail/MRVDlM
【嵌入式八股】四、嵌入式Linux篇https://www.nowcoder.com/creation/manager/columnDetail/MQ2bb0
【一、编程语言-C/C++】01.预处理、基本函数、宏定义总结
04.其他
变量
71.变量声明和定义区别?
- 声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。
- 相同变量可以在多处声明(外部变量extern),但只能在一处定义。
72.定义和声明的区别,未定义在编译哪个阶段报错
如果是指变量的声明和定义: 从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。
如果是指函数的声明和定义: 声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。 定义:一般在源文件里,具体就是函数的实现过程写明函数体。
未定义在编译哪个阶段报错
函数的声明和定义_c++未声明和未定义_明朗晨光的博客-CSDN博客
变量未声明的错误产生于 “编译” 阶段,编译阶段检查的是语法错误 变量未定义的错误产生于 “链接” 阶段,链接阶段关心的是怎么实现
函数未声明的错误产生于 “编译” 阶段,编译阶段检查的是语法错误 函数未定义的错误产生于 “链接” 阶段,链接阶段关心的是怎么实现
73.局部变量能否和全局变量重名?
能,局部会屏蔽全局。要用全局变量,需要使用“::”。
对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。
74.如何引用一个已经定义过的全局变量?
可以用引用头文件的方式(不建议,可能会造成重复定义),也可以用extern关键字。
(1)如果用引用头文件方式来引用某个在头文件中声明的全局变理,假定你将那个变量写错了,那么在编译期间会报错。
(2)如果你用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在链接期间报错。
75.全局变量可不可以定义在可被多个.C文件包含的头文件中?
可以,但要将该全局变量定义为static全局变量,否则会重复定义,但此时没有达到一般意义上的全局变量的效果,而是静态全局变量。
建议不要在头文件中定义变量,只做变量的声明。
76.交换两个变量的值,不使用第三个变量。
可以使用算术运算符和位运算符来实现不使用第三个变量交换两个变量的值。
使用算术运算符:
void swap(int* a, int* b) {
*a = *a + *b;
*b = *a - *b;
*a = *a - *b;
}
使用位运算符:
void swap(int* a, int* b) {
*a = *a ^ *b;
*b = *a ^ *b;
*a = *a ^ *b;
}
这两种方法都是通过异或运算实现交换的,其中使用算术运算符的方法需要注意数据类型的范围,避免溢出。
77.C语言没定义的变量使用了,报错的根本原因是什么
在C语言中,使用未定义的变量会导致编译器错误。这是因为当程序中使用一个变量时,编译器需要知道该变量的类型和大小以及在内存中的位置。如果变量未定义,编译器就无法确定这些信息,因此会报错。
具体来说,未定义的变量可能会导致以下几种错误:
- 编译错误:编译器无法找到变量的定义,因此无法生成可执行代码,会产生编译错误。
- 运行时错误:如果变量在程序运行时被使用但未定义,则程序可能会崩溃或产生其他异常行为。
- 逻辑错误:如果程序中未定义的变量被错误地用于某个计算或逻辑判断中,则可能会导致程序逻辑上的错误。
简单来说就是没有分配内存,编译器不知道类型和大小
78.如何预防变量的重定义
在C语言中,可以使用以下几种方法来预防变量的重定义:
- 头文件保护(Header Guards):在头文件中使用条件编译指令来防止头文件的重复包含。在头文件的开头和结尾分别加上预处理指令,如下所示:
#ifndef HEADER_FILE_NAME_H
#define HEADER_FILE_NAME_H
// 头文件内容
#endif /* HEADER_FILE_NAME_H */
这样,如果多个源文件引用了同一个头文件,在编译过程中只会包含一次,避免了变量重定义的问题。
- 静态变量(Static Variables):将变量声明为静态变量,限定其作用域在当前源文件中。静态变量只能在声明它的源文件中访问,其他源文件无法直接访问该变量,从而避免了变量的重定义问题。
// file1.c
static int variable; // 声明静态变量
// file2.c
extern int variable; // 引用外部的静态变量
- extern 关键字:在多个源文件中共享变量时,可以使用 extern 关键字来声明变量,而不是在每个源文件中重新定义变量。
// file1.c
int variable; // 声明变量
// file2.c
extern int variable; // 声明外部变量,表示该变量在其他源文件中定义
// main.c
extern int variable; // 引用外部变量
通过在一个源文件中定义变量,然后在其他源文件中使用 extern 来声明该变量,可以确保变量只被定义一次,避免了重定义问题。
以上是常用的预防变量重定义的方法。使用这些方法可以确保在C语言程序中避免变量重定义的问题,同时保证变量在需要的地方被正确引用和共享。
位操作
79.如何求解整型数的二进制表示中1的个数?
求解整型数二进制表示中1的个数可以使用以下两种方法:
方法一:位运算 这种方法使用位运算的技巧来计算1的个数。具体思路是:将该整型数与1做与运算,如果结果是1,则表示最低位是1,累加器加1;然后将该整型数右移一位,继续做与运算和累加操作。重复以上操作直到该整型数变成0。最后累加器中的值即为该整型数二进制表示中1的个数。
int count_ones(int num) {
int count = 0;
while (num != 0) {
count += num & 1;
num >>= 1;
}
return count;
}
方法二:查表法 这种方法预处理一个包含所有256个8位二进制数中1的个数的查找表。然后将该整型数按8位一组分成4组,分别在查找表中查找对应的1的个数,并累加起来。最后得到的结果即为该整型数二进制表示中1的个数。
int count_ones(int num) {
// 预处理查找表
int table[256];
for (int i = 0; i < 256; i++) {
int count = 0;
for (int j = 0; j < 8; j++) {
if ((i >> j) & 1) {
count++;
}
}
table[i] = count;
}
// 按8位一组分组,并在查找表中查找1的个数并累加
int count = 0;
for (int i = 0; i < 4; i++) {
count += table[num & 0xFF];
num >>= 8;
}
return count;
}
方法三:Brian Kernighan算法
例如下题
#include <stdio.h>
int func(int x)
{
int countx = 0;
while(x)
{
countx++;
x = x&(x-1);
}
return countx;
}
int main()
{
printf("%d\n",func(9999));
return 0;
}
这段代码使用了一种叫做"Brian Kernighan算法"的位运算技巧,用于快速计算一个整数二进制表示中1的个数。它的基本思路是通过不断地将整数减去1并与原整数做位与运算来消去最右边的一个1,直到整数变成0为止。过程中,每消去一个1,就可以将1的个数加1。
程序输出的结果为8。 在上例中,函数func()的功能是将x转化为二进制数,然后计算该二进制数中含有的1的个数。首先以9为例来分析,9的二进制表示为1001,8的二进制表示为1000,两者执行&操作之后结果为1000,此时1000再与0111(7的二进制位)执行&操作之后结果为0。 为了理解这个算法的核心,需要理解以下两个操作: 1)当一个数被减1时,它最右边的那个值为1的bit将变为0,同时其右边的所有的bit都会变成1。 2)每次执行x&(x-1)的作用是把ⅹ对应的二进制数中的最后一位1去掉。因此,循环执行这个操作直到ⅹ等于0的时候,循环的次数就是x对应的二进制数中1的个数。
80.如何求解二进制中0的个数
int CountZeroBit(int num)
{
int count = 0;
while (num + 1)
{
count++;
num |= (num + 1);
}
return count;
}
int main()
{
int value = 25;
int ret = CountZeroBit(value);
printf("%d的二进制位中0的个数为%d\n",value, ret);
system("pause");
return 0;
}
这段代码的思路是先将二进制数num
加1,然后将其与原数进行或运算,以将原数最低位的0变为1,循环计数,直到num
变为0。然后返回计数器中的值,即0的个数。
但是这段代码存在一个问题,即如果输入的二进制数最高位是0,那么循环就会一直进行下去,因为加1后最高位会变成1,然后或运算会使原数最高位变为1,这样就会导致循环无法结束。因此,这段代码只适用于无符号整数。
另外,这段代码的逻辑有些复杂,可以简化为以下代码:
int CountZeroBit(int num) {
int count = 0;
while (num) {
if ((num & 1) ==0)count++;
num >>= 1;
}
return count;
}
这段代码也是通过逐位检查二进制数来统计0的个数,但是更加直观易懂。
使用一个计数器,通过循环检查每一位是否为0来统计0的个数。可以用右移运算符和位与运算符来逐位检查二进制数。
首先定义一个计数器count
,然后进入一个循环,循环条件为二进制数num
不为0。每次循环,将计数器加上当前位是否为0的结果,即(num & 1) == 0
,如果当前位是0,计数器就加1,否则不加。然后将二进制数右移一位,以便检查下一位。当二进制数为0时,循环结束,返回计数器中的值,即0的个数。
注意,上述代码中的unsigned int
类型可以保证不会出现负数,因此在右移操作时不会出现符号扩展。
81.指定位置1清零
给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。
#define BIT3 (0x1<<3)
static int a;
void set_bit3(void)
{
a |= BIT3;
}
void clear_bit3(void)
{
a &= ~BIT3;
}
82.指定位反转
在一个多任务嵌入式系统中,有一个CPU可直接寻址的32位寄存器REGn,地址为0x1F000010,编写一个安全的函数将寄存器REGn的指定位反转?
void bit_reverse(uint32_t nbit)
{
*((volatile unsigned int *)0x1F000010) ^= (0x01 << nbit);
}
-
指定位反转用异或^。
-
由于是寄存器地址,因此强制类型转换的时候要加上volatile。
83.位翻转
思路:目标数初始化为0,用&0x01的方式获得原始数的第1位,然后左移7位再与目标数按位或,接着原始数右移一位;再用&0x01的方式获得原始数的第2位,然后左移6位……如此循环8次即可。最后返回目标数。
代码:
unsigned char bit_reverse(unsigned char input)
{
unsigned char result = 0;
int bit = 8;
while(bit--)
{
result |= ((input & 0x01) << bit);
input >>= 1;
}
return result;
}
可变参数函数
84.简介可变参数函数
在 C 语言中,我们可以使用 stdarg.h 头文件提供的宏来实现可变参数函数。主要使用的宏有 va_list、va_start、va_arg 和 va_end。
下面是一个使用可变参数的示例,它实现了一个求和函数:
#include <stdio.h>
#include <stdarg.h>
double sum(int count, ...) {
double result = 0;
va_list args;
va_start(args, count);
for (int i = 0; i < count; i++) {
result += va_arg(args, double);
}
va_end(args);
return result;
}
int main() {
double result = sum(3, 1.2, 3.4, 5.6);
printf("Sum = %f\n", result);
return 0;
}
在上面的代码中,sum 函数接受一个整数 count 和任意数量的 double 类型参数,它的实现方式如下:
- 首先,声明一个 va_list 类型的变量 args。【va是可变参数的意思variable argument】
- 然后,调用 va_start 宏,它接受两个参数:一个是 va_list 变量,一个是最后一个固定参数的地址(在这里是 count)。这个宏的作用是初始化 va_list 变量,使其指向第一个可变参数的地址。
- 接下来,使用 va_arg 宏来访问可变参数。这个宏接受两个参数:一个是 va_list 变量,另一个是参数的类型。它的作用是返回当前参数的值,并将 va_list 变量指向下一个参数的地址。
- 最后,调用va_end 宏释放 va_list 变量所占用的资源。
需要注意的是,可变参数函数的参数类型和数量都应该由调用者来确定,否则会导致类型错误和内存访问错误等问题。
其他
85.为什么一般C程序中不用goto
- 容易产生混乱的代码:goto语句可能会使代码流程变得难以理解,因为它们允许程序跳转到程序的任何位置。这可能会导致代码的阅读和理解变得困难,特别是当程序规模较大时。
- 可能导致错误:goto语句容易导致一些常见的错误,例如使用未初始化的变量或无限循环。
- 可能难以维护:由于goto语句可以在程序中跳转到任意位置,因此在修改程序时可能需要在多个位置进行修改,这可能导致代码难以维护。
86.如果从一个函数中goto到函数外,会有什么影响
从函数中跳转到函数外部是一种非常不安全的行为,因为它可能会导致未定义的行为或错误。实际上,C语言标准规定不允许在函数内部跳转到函数外部。
当使用goto语句从函数内部跳转到函数外部时,程序的执行流程将跳过函数的结尾部分,直接到达目标标签所在的位置。这可能导致函数结束前未完成的任务或资源未被正确释放,例如未释放的内存、未关闭的文件等。
从函数内部跳转到函数外部也可能破坏了程序的栈帧结构,导致内存泄漏、崩溃等错误。因此,应该避免从函数内部跳转到函数外部,而是使用其他控制流语句来组织程序的逻辑。
87.死循环有几种方式来写
在C语言中,可以使用while(1)
或for(;;)
语句来创建无限循环。
例如:
while(1) {
// 循环体语句
}
// 或者
for(;;) {
// 循环体语句
}
这两种方式都可以创建一个不会停止的循环,程序将一直在循环体内执行,直到出现某些特殊情况(比如程序异常终止、用户强制退出等)。在无限循环中,通常会添加一些条件判断语句,以便在特定条件下跳出循环。
除了使用while(1)
或for(;;)
语句外,还有其他一些实现无限循环的方式,例如:
- 使用
do-while
循环:
do {
// 循环体语句
} while(1);
do-while
循环与while
循环的区别在于,do-while
循环至少会执行一次循环体语句,然后再判断循环条件是否为真。
- 使用递归函数:
void loop() {
// 循环体语句
loop();
}
int main() {
loop();
return 0;
}
递归函数调用自身,可以实现类似于无限循环的效果。需要注意的是,递归函数调用层数过多可能会导致栈溢出等问题。
无论是使用哪种方式实现无限循环,都应该注意程序的安全性和健壮性,避免出现死循环、内存泄漏等问题。
另外还有一种实现无限循环的方式是使用goto
语句,这种方式不太常用,也容易出现代码混乱的情况,不推荐使用。示例如下:
loop:
// 循环体语句
goto loop;
这种方式利用了goto
语句的特性,将代码跳转到标记位置进行循环。然而,使用goto
语句容易出现代码混乱、可读性差等问题,不建议使用。
总的来说,使用while(1)
或for(;;)
语句是实现无限循环的常用方式,使用其他方式时应当注意代码的可读性和安全性。
88.运算符优先级
C语言运算符按照优先级从高到低排列如下:
- ():括号运算符,用于改变运算次序。
- []、.、->:数组下标、结构体成员、结构体指针成员运算符。
- !、~、++、--:逻辑非、按位取反、自增、自减运算符。
- *、/、%:乘法、除法、取余运算符。
- +、-:加法、减法运算符。
- <<、>>:左移、右移运算符。
- <、<=、>、>=:小于、小于等于、大于、大于等于运算符。
- ==、!=:等于、不等于运算符。
- &:按位与运算符。
- ^:按位异或运算符。
- |:按位或运算符。
- &&:逻辑与运算符。
- ||:逻辑或运算符。
- ?::三目运算符。
- =、+=、-=、*=、/=、%=、<<=、>>=、&=、^=、|=:赋值、复合赋值运算符。
当表达式中有多个运算符时,优先级高的运算符先进行计算。如果有相同优先级的运算符,则按照从左到右的顺序计算。在表达式中使用括号可以改变运算次序。
89.算数右移和逻辑右移的区别
算数右移和逻辑右移的区别及逻辑运算的窍门_算术右移和逻辑右移区别_夏志121的博客-CSDN博客
左移时,无论是图形是数值,移位后,只需要将低位补0即可;右移时,需要根据情况判断是逻辑右移还是算数右移。
90.怎样判断两个浮点数是否相等?
对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。与浮点数的表示方式有关
一种常见的方法是判断两个浮点数之差的绝对值是否小于一个非常小的阈值。这个阈值通常被称为“机器精度(machine epsilon)”,是由计算机所使用的浮点数类型的精度决定的。
在C语言中,可以使用以下代码来实现浮点数的相等性比较:
#include <math.h>
int equal(double a, double b) {
const double EPSILON = 1E-8; // 机器精度
return fabs(a - b) < EPSILON;
}
其中,fabs函数用于计算一个双精度浮点数的绝对值。如果a和b之差的绝对值小于机器精度,则认为它们相等,返回1;否则返回0。
91.cout和printf有什么区别?
cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已存在针对各种类型数据的重载,所以会自动识别数据的类型。
输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。
cout是有缓冲输出:
cout < < "abc" < <endl;
或cout < < "abc\n "; cout < <flush; 这两个才是一样的.
flush立即强迫缓冲输出。
printf是行缓冲输出,不是无缓冲输出
- 使用方式不同
cout是C++中的标准输出流,使用起来与其他C++标准库函数类似,通过"<<"运算符将数据插入到输出流中
而printf则是C语言中的函数,使用时需要指定格式化字符串和相应的参数
- 类型安全性不同
cout在编译时会检查输出数据的类型是否与指定的格式匹配,从而提高了类型安全性。而printf则没有这种类型检查,如果使用错误的格式控制符,就可能导致输出的结果不正确。
- 输出速度和效率不同
在某些情况下,printf可能比cout更快更高效,尤其是当需要进行复杂的格式控制时。然而,cout在输出简单数据类型时通常更快,且更易于使用和理解。
#C++##嵌入式##校招##面试##八股#查阅整理上千份嵌入式面经,将相关资料汇集于此,主要包括: 0.简历面试 1.语言篇【本专栏】 2.计算机基础 3.硬件篇 4.嵌入式Linux (建议PC端查看)