嵌入式软件开发笔试面试

12 篇文章 1 订阅
5 篇文章 0 订阅

C语言部分:

1.gcc的四步编译过程

1.预处理

展开头文件,删除注释、空行等无用内容,替换宏定义。

gcc -E hello.c -o hello.i

2.编译

检查语法错误,如果有错则报错,没有错误则生成汇编文件。

gcc -S hello.i -o hello.s

3.汇编

将汇编文件生成二进制目标文件(并非纯粹的二进制文件)

gcc -c hello.s -o hello.o

4. 链接

将目标文件链接库文件,最终生成机器能够运行的二进制可执行程序。

gcc hello.o -o hello

2.内存分区位置

18ab221317a4423db21e5df92a5eed80.png

 Linux的内存空间简单可以分为5个部分:

高地址

-------------------------------------------------

栈区:由系统自动开辟自动释放的空间,用于存放局部变量

-------------------------------------------------

堆区:由用户手动申请手动释放的空间,malloc和free

-------------------------------------------------

全 .bss 未初始化的全局变量和静态变量

局 ----------------------------------------------

区 .data 已初始化的全局变量和静态变量

-------------------------------------------------

常量区:存放常量

-------------------------------------------------

代码段:存放用户代码

-------------------------------------------------

低地址
————————————————
 

3.局部变量与全局变量

局部变量

定义位置:定义在函数体内部

存储位置:栈区

未初始化初值为:随机数

作用域:当前函数

生命周期:在函数执行结束后就被释放

全局变量

定义位置:定义在函数体外部

存储位置:全局区(.data和.bss段)

未初始化初值为:0

作用域:整个程序

生命周期:在整个程序执行结束后才会被释放

局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。 

4. auto:自动型

修饰的变量存放在栈区

修饰的变量特点:初值随机(如果变量不赋初值)

栈区:由系统自动开辟与释放

5.static的作用

本质是延长生命周期,同时限制其作用域。

static修饰的局部变量,只需初始化一次,未初始化初值为0。而且变量存储在全局数据段(静态存储区)中,而不是栈中,其生命周期持续到程序退出。

static修饰的全局变量、函数,仅当前文件内可用,其他文件不能引用。

6.extern的作用

(1)单个源文件的情况

对于单个源文件的程序,如果某个全局变量不是在文件开头定义,而是在中间某个位置,那么如果在定义位置之前的函数想使用这个全局变量,则可以采用extern来声明变量。

(2)多个源文件情况

如果某程序包含多个源文件(模块),一个源文件中定义了全局变量,其它多个源文件均需要使用该全局变量,只需要在各个使用此全局变量的文件中通过extern对全局变量进行声明即可使用。值得注意的是,这种情况下涉及到多个文件对一个变量的操作,某个文件修改了变量的值,可能会影响其他文件的使用,需谨慎使用。

(3)其它使用

此外extern也可用于函数的外部链接声明。我们知道函数的声明(定义)也可以包括存储类型,但只有extern/static两种。当函数声明为extern,说明函数具有外部链接,其它文件可以调用此函数;当函数声明为static,说明函数是内部链接,即只能在定义函数的文件内部调用函数;当不指明函数存储类型,则默认该函数具有外部链接。

注意:

在使用同一程序不同文件的全局变量,通常加extern进行外部引用。

extern引用语句放在全局,引用到该文件的所有的函数均可访问这个全局变量。

extern引用语句放在局部,仅能在所在函数体内部使用。

7.大小端(三种验证方法)

概念:多字节数据在存储的过程中会有不同的存储形式。

小端模式:高地址存高位数据,低地址存低位数据。

大端模式:高地址存低位数据,低地址存高位数据。

大小端验证方法:

方法一(强制类型转换):

#include<stdio.h>

int main(int argc, const char *argv[])

{

int num = 0x12345678;

char ch = (char)num;

printf("%#x\n",ch); //0x78 低地址存了低位数据,因此是小端存储模式。

return 0;

}

C语言规定,如果在printf()的格式控制符中使用了%#x,就表示在输出时是以带0x前缀的十六进制形式输出后面的对应参数。
也就是说,它的作用和%x相同,只不过%x不会输出前面的“0x",而这个会输出前面的“0x"

方法二( 利用共用体验证):

 利用联合体共用同一段内存的特性,申请一段大小为4个字节的空间,然后根据一个16进制占4位(bit),两个16进制占8位(bit)共1字节的原理,用0x12345678共8个16进制将联合体内的4个字节全部填充完整,最后直接取第一个字节的值,即取较低地址当中的值,根据我们上述说的小端模式中:较低的有效字节存放在较低的存储器地址,可知较低的有效字节为:0x78,如果该地址存放的值恰好等于0x78,那么即为小端模式,如果等于0x12,则为大端模式。
 

    #include<stdio.h>
          union data
          {
              char a;
              int b;
          };
          int main(int argc, const char *argv[])
          {
              union data s;
              s.b = 0x12345678;
              printf("%#x\n",s.a);
              return 0;
          }  


方法三(利用指针):                                    
     


          #include<stdio.h>
          int main(int argc, const char *argv[])
          {
              int num = 0x12345678;               
              void *p = &num;
              printf("%#x\n",*(char *)p);
              return 0;
          }

8. strlen和sizeof的区别

1. strlen 是函数,sizeof 是运算符。

2. strlen 测量的是字符的实际长度,以'\0' 结束(不包含'\0' )。而sizeof 测量的是字符的分配大小,如果未分配大小,则遇到'\0' 结束(包含'\0' ,也就是strlen测量的长度加1),如果已经分配内存大小,返回的就是分配的内存大小。

3.在子函数中,ziseof 会把从主函数中传进来的字符数组当作是指针来处理。指针的大小又是由机器来决定,而不是人为的来决定的。

4.strlen的结果要在运行的时候才能计算出来,是用来计算字符串的长度,不是类型占内存的大小。而大部分编译程序在编译的时候就把sizeof计算过了是类型或是变量的长度。

5.sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以''\0''结尾的。

9. 结构体(struct)和联合体(union)的区别

两者最大的区别在于内存利用

1. 结构体struct

    各成员各自拥有自己的内存,各自使用互不干涉,同时存在的,遵循内存对齐原则。一个struct变量的总长度等于所有成员的长度之和。

2. 联合体union

    各成员共用一块内存空间,并且同时只有一个成员可以得到这块内存的使用权(对该内存的读写),各变量共用一个内存首地址。因而,联合体比结构体更节约内存。一个union变量的总长度至少能容纳最大的成员变量,而且要满足是所有成员变量类型大小的整数倍。

10. 结构体字节对齐三原则

  结构体字节对齐的细节和具体编译器实现相关,但一般而言满足三个准则:

(1)首地址对齐(按最大):结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

(2)成员对齐(按成员类型)结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

(3)总大小对齐(按最大):结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。

对于以上规则的说明如下:

     第一条:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。

     第二条:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。

     第三条:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

11. 数组名和指针的区别与联系是什么


1. 数据保存方面
指针保存的是地址(保存目标数据地址,自身地址由编译器分配),内存访问偏移量为4个字节,无论其中保存的是何种数据均已地址类型进行解析。
数组保存的数据。数组名表示的是第一个元素的地址, 内存偏移量是保存数据类型的内存偏移量;只有对数组名取地址(&数组名)时数组名才表示整个数组,内存偏移量是整个数组的大小(sizeof(数组
名))。
2.数据访问方面
指针对数据的访问方式是间接访问,需要用到解引用符号(*数组名)。
数组对数据的访问则是直接访问,可通过下标访问或数组名+元素偏移量的方式
3.使用环境
指针多用于动态数据结构(如链表,等等)和动态内存开辟。
数组多用于存储固定个数且类型统一的数据结构(如线性表等等)和隐式分配。

12. const有什么作用

const意味着"只读"

(1)、修饰变量:

C语言中采用const修饰变量,功能是对变量声明为只读特性,并保护变量值以防被修改。举例说明如下:

const int i = 5;

上面这个例子表明,变量i具有只读特性,不能够被更改;若想对i重新赋值,如i = 10;则是错误的。

值得注意的是,定义变量的同时,必须初始化。定义形式也可以写成int const i=5,同样正确。

此外,const修饰变量还起到了节约空间的目的,通常编译器并不给普通const只读变量分配空间,而是将它们保存到符号表中,无需读写内存操作,程序执行效率也会提高。

(2)、修饰数组

C语言中const还可以修饰数组,举例如下:

const int array[5] = {1,2,3,4,5};

array[0] = array[0]+1; //错误

数组元素与变量类似,具有只读属性,不能被更改;一旦更改,如程序将会报错。

(3)、 修饰指针

C语言中const修饰指针要特别注意,共有两种形式,一种是用来限定指向空间的值不能修改;另一种是限定指针不可更改。举例说明如下:

int i = 5;

int j = 6;

int k = 7;

const int * p1 = &i; //定义1

int * const p2 =&j; //定义2

上面定义了两个指针p1和p2。

在定义1中const限定的是*p1,即其指向空间的值不可改变,若改变其指向空间的值如*p1=20,则程序会报错;但p1的值是可以改变的,对p1重新赋值如p1=&k是没有任何问题的。

在定义2中const限定的是指针p2,若改变p2的值如p2=&k,程序将会报错;但*p2,即其所指向空间的值可以改变,如*p2=80是没有问题的,程序正常执行。

(4)、修饰函数参数

const关键字修饰函数参数,对参数起限定作用,防止其在函数内部被修改。所限定的函数参数可以是普通变量,也可以是指针变量。举例如下:

void fun1(const int i)

{

其它语句

……

i++; //对i的值进行了修改,程序报错

其它语句

}

void fun2(const int *p)

{

其它语句

……

(*p)++; //对p指向空间的值进行了修改,程序报错

其它语句

}

13. POSIX是什么

POSIX(Portable Operating System Interface,便携式操作系统接口)是一个标准化的操作系统接口,旨在促进不同操作系统之间的可移植性和互操作性。POSIX标准由IEEE(Institute of Electrical and Electronics Engineers)制定,具体定义在IEEE POSIX标准化文件中。

POSIX标准定义了一组函数、系统调用、头文件和工具命令,用于编写可移植的应用程序和系统软件。这些标准化接口规范了操作系统的核心功能,包括文件操作、进程管理、线程、信号处理、网络通信等。

通过使用POSIX接口,开发者可以编写与特定操作系统无关的应用程序,使其能够在符合POSIX标准的不同操作系统上进行编译和运行,而无需进行大规模的修改和适应。POSIX的目标是提供一致的编程接口,使应用程序能够跨多个操作系统平台进行移植和部署。

POSIX接口最初是针对类UNIX操作系统的,但它也被其他操作系统(如Linux、macOS等)采纳,并在许多嵌入式系统和实时操作系统中得到广泛应用。通过POSIX接口,开发者可以编写可移植的、与操作系统无关的代码,提高代码的可重用性和可移植性,同时减少对特定操作系统的依赖性。

14. define 与typedef的区别

define 与typedef大体功能都是使用时给一个对象取一个别名,增强程序的可读性,但它们在使用时有以下几点区别:

(1)原理不同

#defineC语言中定义的语法,是预处理指令,在预处理时进行简单而机械的字符串替换,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。

typedef是关键字,在编译时处理,有类型检查功能。它在自己的作用域内给一个已经存在的类型一个别名,但不能在一个函数定义里面使用typedef。用typedef定义数组、指针、结构等类型会带来很大的方便,不仅使程序书写简单,也使意义明确,增强可读性。

(2)功能不同

typedef用来定义类型的别名,起到类型易于记忆的功能。

#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。

(3)作用域不同

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

15. Makefile 里有什么?

Makefile 里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。

1、显式规则。
显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile 的书写者明显指
出,要生成的文件,文件的依赖文件,生成的命令。

2、隐晦规则。
由于我们的 make 有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书
写 Makefile,这是由 make 所支持的。
3、变量的定义。
在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点你 C 语言中
的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
4、文件指示。
其包括了三个部分,一个是在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的
include 一样;另一个是指根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预
编译#if 一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中
讲述。
5、注释。
Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用“#”字符,这个就
像 C/C++中的“//”一样。如果你要在你的 Makefile 中使用“#”字符,可以用反斜框进行
转义,如:“#”。
最后,还值得一提的是,在 Makefile 中的命令,必须要以[Tab]键开始。

16. C语言中,修饰符volatile含义是什么?其应用场合有哪些?

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序
每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没
有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,
如果这个变量由别的程序更新了的话,将出现不一致的现象。

16. 关键字volatile有什么含义,并给出几个应用场合。


volatile的本意是“易变的”由于访问寄存器的速度要快过RAM,所以编译器一般都
会作减少存取外部RAM的优化,但有可能会读脏数据。当要求使用volatile声
明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指
令刚刚从该处读取过数据。而且读取的数据立刻被保存。
一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可
能由不同意义;

16. volatile作用

  1. 防止编译器优化:编译器常常会对代码进行优化,例如将变量存储在寄存器中,以提高程序的执行效率。然而,在某些情况下,变量的值可能会被程序之外的因素修改,例如多线程环境、硬件中断等。如果使用 volatile 关键字声明变量,编译器会禁止对该变量的优化,每次都会从内存中读取变量的最新值。

  2. 确保内存操作的可见性:在多线程或并发编程中,volatile 关键字还可以用于确保内存操作的可见性。当一个变量被声明为 volatile 时,每次对它的读写操作都会直接访问内存,而不会使用缓存。这样可以保证多个线程之间对该变量的操作是按顺序执行的,并且对一个线程的写入操作对其他线程是可见的。

17. 数组和链表的区别

1、顺序表在内存当中连续存储的(数组),但是链表在内存当中是不连续存储的,通过指针将数据链接在一起。

2、顺序表的长度是固定的,但是链表长度不固定。

3、顺序表查找方便(下标),但是插入和删除麻烦(post~last),链表,插入和删除方便,查找麻烦。

18. 对指针的理解

指针是一种数据类型,用于存储内存地址。它指向某个特定类型的数据,可以通过指针来访问和操作内存中的数据。指针的正确使用对于直接管理内存、处理复杂数据结构、减少内存占用等方面至关重要。指针的错误使用可能导致内存泄漏、野指针、访问非法内存等问题,引发程序崩溃或安全漏洞。因此,在使用指针时需要小心,并确保正确初始化、安全地引用和释放相关内存。

19. 堆和栈的区别

  1. 分配方式:

    • 栈:栈使用自动内存分配。在函数调用时,局部变量和函数参数被分配在栈上,并在函数返回时自动释放。栈的分配和释放是按照固定规则自动进行的,不需要额外的手动操作。
    • 堆:堆使用动态内存分配。程序员可以通过调用动态内存分配函数(如 malloc()new)在堆上显式地分配内存,同时还需要手动释放已分配的内存(使用 free()delete)。
  2. 空间大小:

    • 栈:栈的大小通常比较小,由编译器在编译时确定并在运行时分配。栈的大小限制取决于操作系统或编译器的设定。
    • 堆:堆的大小通常比较大,并且相对于栈来说更加灵活,可以根据需要动态分配和释放内存。
  3. 数据访问速度:

    • 栈:由于栈上的数据是按照固定规则分配和释放的,并且在访问上有较好的局部性,因此栈上的数据访问速度较快。
    • 堆:堆上的数据访问速度相对较慢,因为堆上的数据通常需要通过指针间接访问。

20. memcpy和strcpy的区别?

(1)功能不同

  • memcpy()memcpy()函数用于将指定内存区域的数据复制到另一个内存区域。它可以复制任意数量的字节数据,不仅限于字符串。
  • strcpy()strcpy()函数用于将一个字符串复制到另一个字符串。它复制字符直到遇到空字符(‘\0’)为止,表示字符串的结束。

(2)参数不同

  • memcpy()memcpy()函数接受三个参数:目标内存地址、源内存地址和要复制的字节数。它不会自动添加字符串结束符。
  • strcpy()strcpy()函数接受两个参数:目标字符串地址和源字符串地址。它会自动复制整个源字符串,并在末尾添加空字符(‘\0’)。

21. 什么是段错误?怎么解决段错误?

段错误(Segmentation Fault)是一种常见的运行时错误,它在程序访问无效的内存地址试图对只读内存进行写操作时发生。当程序产生段错误时,操作系统会终止程序的执行,并生成一个错误报告。

常见导致段错误的情况包括:

  1. 解引用空指针:当程序试图访问空指针所指向的内存区域时,由于空指针没有有效的内存地址,就会产生段错误。

  2. 访问越界:当程序访问数组、指针或缓冲区等数据结构时,超出了其有效范围,就会导致段错误。

  3. 写入只读内存:当程序试图对只读内存(如字符串常量)进行写操作时,就会引发段错误。

解决段错误需要进行调试和修复代码。以下是一些常见的方法和技巧:

  1. 使用调试器:使用调试器(如GDB)可以帮助定位段错误的发生位置。通过在程序崩溃时运行调试器,并检查堆栈跟踪和变量状态,可以确定引发段错误的具体代码段。

  2. 检查空指针:确保程序中的指针在使用之前都进行了有效的初始化,避免解引用空指针。

  3. 检查数组和指针边界:确保程序中的数组和指针访问不会越界,超出有效范围。

  4. 避免写入只读内存:确保程序不会对只读内存进行写操作。如果需要修改字符串或数据,应该使用可写的内存。

  5. 使用内存管理工具:使用内存管理工具,如Valgrind,可以帮助检测内存访问错误和泄漏,并提供详细的错误报告。

  6. 编码规范和静态分析工具:遵循编码规范(如避免未初始化变量、正确使用指针等),并使用静态分析工具(如Clang静态分析器)可以帮助识别潜在的段错误问题。

注意,在解决段错误时,需要根据具体情况进行调试和修复。段错误可能是由于逻辑错误、内存管理错误或其他编程错误引起的。通过细致的调试和代码审查,可以找到并解决这些问题,以确保程序能够正确执行。

22. 什么是内存泄漏?什么是野指针?

内存泄漏(Memory Leak)指的是在程序运行过程中,动态分配的内存空间没有被正确释放或释放的时机不合适,导致这些内存无法再被程序使用,从而造成内存的浪费。当内存泄漏累积到一定程度时,会导致程序占用的内存越来越多,最终可能导致性能下降或程序崩溃。

内存泄漏的发生通常是因为程序没有及时调用 free() 或 delete 来释放已动态分配的内存,或者释放的顺序不正确,从而导致一部分内存无法被回收。这种情况经常发生在使用动态内存分配(如 malloc()new)的情况下。要解决内存泄漏问题,需要确保在不需要使用动态分配的内存时,及时释放它们,避免造成资源的浪费。

野指针(Dangling Pointer)指的是指向已释放或无效的内存空间的指针。当一个指针指向的内存被释放后,如果仍然使用该指针进行读写操作,就会导致 undefined behavior(未定义行为),可能产生程序崩溃或其他异常。野指针的发生通常是由于程序中未及时更新指针或释放指针后未将其置空引起的。

要避免野指针问题,应当养成良好的指针使用习惯,使用指针前确保其指向有效的内存空间,并在释放指针后将其置空或设置为合适的值。另外,可以使用一些编程实践,如避免在函数返回后返回指向局部变量的指针,或者使用智能指针等工具来自动管理指针的生命周期,减少野指针问题的发生。

总之,内存泄漏和野指针都是常见的内存管理问题,要写出高质量的程序,需要注意及时释放不再使用的内存,并确保指针的有效性。

23. 头文件< >和" "的区别

  1. #include <头文件>  : 编译器只会从系统配置的库环境中去寻找头文件,不会搜索当前文件夹。通常用于引用标准库头文件。
  2. #include "头文件"  : 编译器会先从当前文件夹中寻找头文件,如果找不到则到系统默认库环境中去寻找。一般用于引用用户自己定义使用的头文件。

24. #ifndef的作用

使用 #ifndef 可以有效防止头文件的重复包含,避免重复定义变量、结构体或函数等,并提高编译效率。一般情况下,我们会在头文件的开头使用 #ifndef 和 #define 配合,将整个头文件的内容包含在其中,确保头文件只会被编译一次

25. gdb调试

GDB调试方式:准确定位(行)

使用过的调试方式有哪些?

1:GDB

2:printf打印

调试工具(GDB)段错误(地址非法操作,指针)

gdb调试:

(在用GDB调试之前确定你的代码没有语法错误。设置断点的时候,必须让主函数运行起来,第一次断点设置,设置在主函数当中的某一行,这样编译器才能从入口函数进来。在gcc编译选项中一定要加入‘-g’。只有在代码处于“运行”或“暂停”状态时才能查看变量值。设置断点后程序在指定行之前停止

先制造一个产生段错误的代码:

1) gcc -g test.c

2) gdb a.out

3)(gdb)l:列出源文件内容

4)(gdb)b 10:设置断点在第10行

5)(gdb)r :运行(设置断点后一定要先运行,才能进行单步调试往下)

6)(gdb)n:单步调试(断点行是不被运行的,n单步调试的时候不进子函数)

7)(gdb)s:单步运行(断点行是不被运行的,s单步调试的时候进子函数)

单步调试时:(s进入子函数,16版本的虚拟机有问题,想进入系统函数,12版本的正常)

8) (gdb)p 变量名 :查看变量值

9)q:退出调试界面
 

26.  C语言中static函数与普通函数的区别是什么?

静态函数

在函数的返回类型前加上关键字static,函数就被定义成为静态函数。

函数的定义和声明默认情况下是extern的,但静态函数只是在声明他的文件当

中可见,不能被其他文件所用。

定义静态函数的好处:

<1>其他文件中可以定义相同名字的函数,不会发生冲突

<2>静态函数不能被其他文件所用。

局部静态变量

在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。

1)内存中的位置:静态存储区

2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的

值是任意的,除非他被显示初始化)

3)作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,

作用域随之结束。

全局静态变量

在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。

1)内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)

2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的

值是任意的,除非他被显示初始化)

3)作用域:全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之

处开始到文件结尾。

好处:

定义全局静态变量的好处:

<1>不会被其他文件所访问,修改

<2>其他文件中可以使用相同名字的变量,不会发生冲突

27. 写一个标准的宏 MINX,这个宏输入两个参数并返回较小的一个?

#define MIN(A, B) ((A)>(B)? (B) : (A))

28.  什么是MMU,MMU的作用

MMU是Memory Management Unit(内存管理单元)

1)虚拟内存。有了虚拟内存,可以在处理器上运行比实际物理内存大的应用程

序。为了使用虚拟内存,操作系统通常要设置一个交换分区(通常是硬盘),通

过将不活跃的内存中的数据放入交换分区,操作系统可以腾出其空间来为其它的

程序服务。虚拟内存是通过虚拟地址来实现的。

2)内存保护。根据需要对特定的内存区块的访问进行保护,通过这一功能,我

们可以将特定的内存块设置成只读、只写或是可同时读写。

29. 左值和右值是什么?

左值是指可以出现在等号左边的变量或表达式,它最重要的特点就是可写(可寻址)。也就是说,它的值可以被修改,如果一个变量或表达式的值不能被修改,那么它就不能作为左值。


右值是指只可以出现在等号右边的变量或表达式。它最重要的特点是可读。一般的使用场景都是把一个右值赋值给一个左值。


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

30. 什么是短路求值?

在短路求值中,当使用逻辑与操作时,如果第一个表达式为false,则不会对第二个表达式进行求值,因为无论第二个表达式的结果如何,整个逻辑与操作的结果都将为false。这意味着,如果第一个表达式的结果已经确定逻辑与的结果为false,进一步的计算就可以被跳过,以提高性能和效率。

类似地,当使用逻辑或操作时,如果第一个表达式为true,则不会对第二个表达式进行求值,因为无论第二个表达式的结果如何,整个逻辑或操作的结果都将为true。这意味着,如果第一个表达式的结果已经确定逻辑或的结果为true,进一步的计算就可以被跳过。

31. 数组指针与指针数组有什么区别

数组指针

93a3d081a4fb4bc3b8bead3080f1a2f0.png

 指针数组

 06895e1d073b4fb38b85165420939332.png

 32. 函数指针与指针函数有什么区别

函数指针

4e01867f49f04c1babfa4d459bed070b.png

 指针函数

60970933a3a3404296cf6f8cd6be6756.png

 ebd158bf1de84772a4eb22198db2bf46.png

33.  指针常量,常量指针,指向常量的常量指针有什么区别?

 6228765cb15a471497fdb29bb6ecf729.png

 34. 头文件的作用

头文件的作用主要表现为以下两个方面:
1.通过头文件来调用库功能。出于对源代码保密的考虑,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的。编译器会从库中提取相应的代码。
2.头文件能加强类型安全检查。当某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,大大减轻程序员调试、改错的负担。


35. C语言怎么进行函数调用的

大多数CPU上的程序实现使用栈来支持函数调用操作,栈被用来传递函数参数、存储返回信息、临时保存寄存器原有的值以备恢复以及用来存储局部变量。
函数调用操作所使用的栈部分叫做栈帧结构,每个函数调用都有属于自己的栈帧结构,栈帧结构由两个指针指定,帧指针(指向起始),栈指针(指向栈顶),函数对大多数数据的访问都是基于帧指针。下面是结构图:
94bbb62c025f468cb0bdf87691c58a10.png

 36. 如何求解整型数的二进制表示中1的个数?

4576651510e041ffa4941e76a915c5ff.png

 37. 如何求解二进制中0的个数

5937101a11794215a2b2b769562cbdff.png

38.  交换两个变量的值,不使用第三个变量。即a=3,b=5,交换之后a=5,b=3;

12a7fda9d5264407b4c54c8e97f09879.png

39.  给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a的bit 3。在以上两个操作中,要保持其它位不变。

9be0f21d40c042d783fab4e4ad7a5a29.png

void clearbit3(int a)
{
	a&=~(1<<3);
}
void setbit3(int a)
{
	a|=1<<3;
}

40. 以下没有直接使用ASIC C语言中malloc()函数,而是二次封装为void *Malloc(U16 size),其目的是什么


typedef struct {
S16 size;
U16 maxsize;
}ST_MEMPOOL,*PT_MEMPOOL;
static ST_MEMPOOL stMemInfo={0,8192};
void * Malloc(U16 size)
{
U16 newsize = stMemInfo.size +size;
if(newsize > = stMemInfo.maxsize)
return NULL;
stMemInfo.size = stMemInfo.size + size;
return malloc(size);
}

答:

这段代码是将malloc函数进行二次封装为Malloc(U16 size)函数。其目的是实现对内存分配的控制和限制。

在封装函数中,首先计算出新的内存大小newsize = stMemInfo.size + size,然后判断新的内存大小是否超过了预设的最大内存限制stMemInfo.maxsize。如果超过限制,则返回NULL,表示内存分配失败。

如果没有超过限制,则更新内存池的大小stMemInfo.size = stMemInfo.size + size,然后调用malloc函数进行实际的内存分配,并返回分配的内存地址。

这段代码的目的是在进行内存分配时,通过内存池的限制来控制和管理可用的内存大小。通过封装malloc函数,可以在每次分配内存前进行大小检查,以确保分配的内存不会超过预设的最大限制。这样可以避免内存溢出的问题,并提供更好的内存使用控制和管理。

41. 数据类型占用的空间(字节数)

除了long型,其他类型在32位(4字节)和64位(8字节)系统下占用字节数是一样的!

17f5baa4305b4848af82f408a89bdcb8.png

42. 运算符优先级


优先级从高向低:

单目运算符 ! ~ ++ --

算术运算符 * / % + -

移位运算符 << >>

关系运算符 < <= > >= == !=

位与运算符 &

异或运算符 ^

位或运算符 |

逻辑运算符 && ||

条件运算符 ?:

赋值运算符 = += *= /= ...

口诀:单算移关与,异或逻条赋

结合性从右向左:单条赋

43. 往内存0x12ff7c地址上存入一个整型数0x100

我们知道可以通过一个指针向其指向的内存地址写入数据,那么这里的内存地址0x12ff7c其本质不就是一个指针嘛。所以我们可以用下面的方法:

int  *p = ( int  *) 0x12ff7c ;
*p =  0x100 ;

数据结构部分:

1. 单链表的增删改查操作实现思路

2. 单链表的倒置思路

linklist.h文件:

#ifndef __LINKLIST__H_
#define __LINKLIST__H_
#include <stdio.h>
#include <stdlib.h>
typedef int datatype;
typedef struct node_t
{
    datatype data;
    struct node_t *next;
}link_node_t,*link_list_t;
 
link_list_t CreateEplist();
int InsertIntoPostLinkList(link_list_t p,int post,datatype data);
void ShowLinkList(link_list_t p);
int LengthLinkList(link_list_t p);
int DeleteLinkList(link_list_t p,int post);
int IsEpLinkList(link_list_t p);
int ChangePostLinkList(link_list_t p,int post,datatype data);
int SearchDataLinkList(link_list_t p,datatype data);
void ReverseLinkList(link_list_t p);
void ClearLinkList(link_list_t p);
int DeleteDataLinkList(link_list_t p,int data);  
 
#endif                                                                  

linklist.c文件:

#include "linklist.h"  
 
//创建一个空的单向链表(有头单向链表)                                                                       
link_list_t CreateEplist()                                                                    
{                                                                                             
    link_list_t h=(link_list_t)malloc(sizeof(link_node_t));                                   
    if(NULL==h)                                                                               
    {                                                                                         
        printf("error");                                                                      
        return NULL;                                                                          
                                                                                              
    }                                                                                         
    h->next=NULL;                                                                             
    return h;                                                                                 
}                                                                                             
                                                                                              
//向单链表的指定位置插入数据                                                                                              
int InsertIntoPostLinkList(link_list_t p,int post,datatype data)   /                        
{                                                                                             
                                                                                              
    if(post<0||post>LengthLinkList(p))                                                        
    {                                                                                         
        printf("erro\n");                                                                     
        return -1;                                                                            
    }                                                                                         
                                                                                              
    link_list_t pnew=(link_list_t)malloc(sizeof(link_node_t));                                
                                                                                              
    if(NULL==pnew)                                                                            
    {                                                                                         
        printf("error");                                                                      
        return -1;                                                                            
    }                                                                                         
                                                                                                                                                                                       
    pnew->data=data;                                                                          
    pnew->next=NULL;
                                                                          
    for(int i=0;i<post;i++)                                                                   
    {                                                                                         
        p=p->next;                                                                            
    }                                                                                         
    pnew->next=p->next;                                                                       
    p->next=pnew;                                                                             
    return 0;                                                                                 
}                                                                                             
 
//遍历单向链表                                                                                              
void ShowLinkList(link_list_t p)                                                              
{                                                                                             
    if(p->next==NULL)                                                                         
    {                                                                                         
      printf("empt LinkList\n");                                                              
    }                                                                                         
                                                                                              
    while(p->next!=NULL)                                                                      
    {                                                                                         
        p=p->next;                                                                            
        printf("%d",p->data);                                                                 
        putchar(10);                                                                          
    }                                                                                         
                                                                                                                                                                                         
}
                                                                                             
//求单链表长度的函数                                                                                             
int LengthLinkList(link_list_t p)                                                             
{                                                                                             
    int lenth=0;                                                                              
                                                                                              
    while(p->next!=NULL)                                                                      
    {                                                                                         
        p=p->next;                                                                            
        lenth++;                                                                              
    }                                                                                         
    return lenth;                                                                             
}                                                                                             
 
//删除单链表中指定位置的数据                                                                                             
int DeleteLinkList(link_list_t p,int post)                                                    
{                                                                                             
                                                                                              
    if(post<0||post>=LengthLinkList(p)||IsEpLinkList(p))                                      
    {                                                                                         
        printf("erro\n");                                                                     
        return -1;                                                                            
    }                                                                                         
    for(int i=0;i<post;i++)                                                                   
    {                                                                                         
        p=p->next;                                                                            
    }                                                                                         
    link_list_t pdel=p->next;                                                                 
    p->next=pdel->next;                                                                       
    free(pdel);                                                                               
    pdel=NULL;                                                                                
    return 0;                                                                                 
}                                                                                             
 
//判断单链表是否为空,1代表空,0代表非空                                                                                              
int IsEpLinkList(link_list_t p)                                                               
{                                                                                             
    return p->next==NULL;                                                                     
                                                                                              
}                                                                                             
 
// 修改指定位置上的数据                                                                                             
int ChangePostLinkList(link_list_t p,int post,datatype data)                                  
{                                                                                             
                                                                                              
    if(post<0||post>=LengthLinkList(p)||IsEpLinkList(p))                                      
    {                                                                                         
        printf("erro\n");                                                                     
        return -1;                                                                            
    }                                                                                         
    for(int i=0;i<=post;i++)                                                                  
    {                                                                                         
        p=p->next;                                                                            
    }                                                                                         
    p->data=data;                                                                             
    return 0;                                                                                 
                                                                                              
}                                                                                             
 
//查找指定数据出现的数据                                                                                              
int SearchDataLinkList(link_list_t p,datatype data)                                           
{                                                                                             
    if(IsEpLinkList(p))                                                                       
    {                                                                                         
        printf("error\n");                                                                    
        return -1;                                                                            
    }                                                                                         
    int i=0;                                                                                  
    while(p->next!=NULL){                                                                     
        p=p->next;                                                                            
        if(p->data==data)                                                                     
        {                                                                                     
            return i;                                                                         
        }                                                                                     
        i++;                                                                                  
    }                                                                                         
    return -1;                                                                                
}                                                                                             
                                                                                              
//转置链表                                                                                              
void ReverseLinkList(link_list_t p)                                                           
{                                                                                             
                                                                                              
    link_list_t q=p->next;                                                                    
    link_list_t temp=NULL;                                                                    
    p->next=NULL;                                                                             
    while(q!=NULL)                                                                            
    {                                                                                         
                                                                                              
        temp=q->next;                                                                         
                                                                                              
        q->next=p->next;                                                                      
        p->next=q;                                                                            
                                                                                              
        q=temp;                                                                               
    }                                                                                         
                                                                                              
                                                                                              
}                                                                                             
 
 
//清空单向链表                                                 
void ClearLinkList(link_list_t p)                
{                                                
    link_list_t pdel=NULL;                       
    while(p->next!=NULL)                         
    {                                            
        pdel=p->next;                            
        p->next=pdel->next;                      
        free(pdel);                              
        pdel=NULL;                               
    }                                            
                                                 
}                                                
                                                 
//删除单向链表中出现的指定数据
int DeleteDataLinkList(link_list_t p,int data)             
{                                                          
                                                           
    if(IsEpLinkList(p))                                    
    {                                                      
        printf("erro\n");                                  
        return -1;                                         
    }                                                      
                                                           
    link_list_t pdel=NULL;                                 
                                                           
    while(p->next!=NULL)                                   
        if(p->next->data==data)                            
        {                                                  
            pdel=p->next;                                  
            p->next=pdel->next;                            
            free(pdel);                                    
            pdel=NULL;                                     
        }                                                  
        else{                                              
            p=p->next;                                     
        }                                                  
                                                           
return 0;                                                  
}                                                          

main.c文件:

#include "linklist.h"                            
int main(int argc, const char *argv[])           
{                                                
    link_list_t p=CreateEplist();                
#if 1                                            
    InsertIntoPostLinkList(p,0,1);               
    InsertIntoPostLinkList(p,1,2);               
    InsertIntoPostLinkList(p,2,3);               
    InsertIntoPostLinkList(p,3,4);               
    ShowLinkList(p);                             
    putchar(10);                                 
#endif                                           
    DeleteLinkList(p,0);                         
    ShowLinkList(p);                             
    putchar(10);                                 
                                                 
    ChangePostLinkList(p,1,5);                   
    ShowLinkList(p);                             
    putchar(10);                                 
                                                 
    printf("%d\n",SearchDataLinkList(p,5));      
    putchar(10);                                 
                                                 
    ReverseLinkList(p);                          
    ShowLinkList(p);                             
    putchar(10);                                 
                               
    DeleteDataLinkList(p,5);  
    ShowLinkList(p);          
    putchar(10);              
                                             
    ClearLinkList(p); 
    free(p);
    p=NULL;                           
    ShowLinkList(p);                             
    return 0;                                    
}                                                

3. 单链表和双向链表的区别

单链表(Singly Linked List)和双向链表(Doubly Linked List)是两种常见的链表数据结构,它们之间的主要区别在于节点内部的指针个数以及操作的复杂性。

  1. 单链表(Singly Linked List):

    • 每个节点包含一个数据项和一个指向下一个节点的指针(通常称为"next")。
    • 单链表只支持单向遍历,即从头节点到尾节点。
    • 插入或删除节点时,需要更新相邻节点的链接。
    • 单链表通常需要更少的内存空间,因为每个节点只需要存储一个指针。
  2. 双向链表(Doubly Linked List):

    • 每个节点包含一个数据项、一个指向前一个节点的指针(通常称为"prev")和一个指向下一个节点的指针(通常称为"next")。
    • 双向链表支持双向遍历,可以从头节点到尾节点,也可以从尾节点到头节点。
    • 插入或删除节点时,不仅需要更新相邻节点的链接,还需要更新前一个节点的指针。
    • 双向链表相对于单链表来说,具有更强的操作灵活性,因为它可以更高效地在任意位置插入或删除节点。

总结:

  • 单链表只有一个指向下一个节点的指针,支持单向遍历,操作相对简单。
  • 双向链表有两个指针,分别指向前一个节点和后一个节点,支持双向遍历,操作相对复杂但更灵活。
  • 双向链表相比单链表占用更多的内存空间,因为每个节点需要存储两个指针。

4. 栈和队列的区别

栈的特点:

  • 栈是一种后进先出(Last In First Out,LIFO)的数据结构。
  • 元素的插入和删除操作都是在同一端进行,该端称为栈顶。
  • 只能在栈顶进行插入(压栈)和删除(弹栈)操作。
  • 最后进入栈的元素第一个被删除,即先进入的元素最后出栈。
  • 由于栈的特性,适合处理具有嵌套关系和程序调用等后进先出的场景。
  • 常见的栈的应用包括函数调用栈、表达式求值和回溯等。
  • 常见栈的应用场景包括括号问题的求解,表达式的转换和求值,函数调用和递归实现,深度优先搜索遍历等;

队列的特点:

  • 队列是一种先进先出(First In First Out,FIFO)的数据结构。
  • 元素的插入操作(入队)在一端进行,称为队尾;元素的删除操作(出队)在另一端进行,称为队首。
  • 最先进入队列的元素最先被删除。
  • 队列可以用于实现流程控制,例如任务调度、消息传递和缓冲区管理等。
  • 常见的队列的应用包括广度优先搜索算法、打印队列和消息队列等。
  • 常见的队列的应用场景包括计算机系统中各种资源的管理,消息缓冲器的管理和广度优先搜索遍历等。

简要总结:

  • 栈是后进先出(LIFO),适合具有嵌套关系和程序调用等场景。
  • 队列是先进先出(FIFO),适合流程控制和顺序处理等场景。

5. 两个栈怎实现一个队列,思路说一下

用两个栈s1和s2模拟一个队列时,s1作输入栈,逐个元素压栈,以此模拟队列元素的入队。
当需要出队时,将栈s1退栈并逐个压入栈s2中,s1中最先入栈的元素,在s2中处于栈顶。
s2退栈,相当于队列的出队,实现了先进先出。
显然,只有栈s2为空且s1也为空,才算是队列空。
 

6. 冒泡排序原理?时间复杂度多少?

通过不断交换相邻元素的位置来实现排序。其原理如下:

  1. 从待排序的数据中,依次比较相邻的两个元素,如果它们的顺序不正确(逆序),则交换它们的位置。
  2. 在第一轮比较结束后,最大的元素会被交换到数组的末尾(或是最小的元素被交换到数组的开头)。
  3. 在第二轮比较中,从剩余的元素中再次重复上述比较和交换过程,直到所有元素都按照从小到大(或从大到小)的顺序排序完成。

冒泡排序的关键在于每一轮比较都会确定一个当前轮次的最大(或最小)元素的位置,这样每一轮循环都将缩小待排序元素的范围。

冒泡排序的时间复杂度为 O(n^2),其中 n 是待排序元素的个数。在最坏情况下,需要进行 n-1 轮比较,每轮比较需要进行 n-i-1 次交换操作(i 表示当前轮次),因此总的时间复杂度为 O(n^2)。冒泡排序的空间复杂度为 O(1),因为只需要少量的额外空间来存储临时变量。

值得注意的是,冒泡排序是一种稳定的排序算法,即相等元素的相对顺序在排序后保持不变。另外,冒泡排序在实际应用中效率较低,对于大规模数据的排序不具备优势。

7. 什么是二叉树、满二叉树?

  1. 二叉树(Binary Tree)是一种特殊的树形数据结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。

  2. 满二叉树(Full Binary Tree)是一种特殊的二叉树,其中每个节点要么没有子节点(叶节点),要么同时有左子节点和右子节点。换句话说,满二叉树的每个非叶节点的度数都是 2,且所有叶节点都在同一层上。

二叉树的特点(重点):

(1)二叉树第k(k>=1)层上的节点最多为2的k-1次幂个。

(2)深度为k(k>=1)的二叉树最多有2的k次幂-1个节点。//满二叉树的时候

(3)在任意一棵二叉树中,树叶的数目比度数为2的节点的数目多一

满二叉树的特点:

  • 在满二叉树中,如果高度为 h,则总共有 2^h - 1 个节点。
  • 当给定满二叉树的高度时,可以直接计算出树中的节点数量。
  • 满二叉树是一种平衡二叉树,即左右子树的高度差不超过 1。

8. 二叉树的前序中序后序遍历

前序: 根----> 左 -----> 右 

中序: 左----> 根 -----> 右 

后序: 左----> 右 -----> 根

9.查找算法学过哪些?二分查找的时间复杂度多少?

顺序查找,折半查找(二分查找),分块查找,树表的查找。

二分查找的时间复杂度为O(log₂n)

10. 哈希表的原理?

哈希表(Hash table,也叫散列表),所谓hash表,就是以 键-值(key-indexed) 的形式存储的数据结构。可以根据key来快速的查找到value。也就是说,它通过把key值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

当要插入/查找一个键值对时,哈希表使用哈希函数计算键的哈希值,并据此确定存储位置。如果该位置已经被占用,就需要处理哈希冲突。哈希冲突发生在不同的键经过哈希函数计算后得到相同的哈希值的情况下。

常见的处理哈希冲突的方法有两种:

链地址法(Chaining):链地址法的基本思想是:把具有相同散列地址的记录放在同一个单链表中,称为同义词链表。有 m 个散列地址就有 m 个单链表,同时用数组 HT[O…m-1]存放各个链表的头指针,凡是散列地址为 l 的记录都以结点方式插入到以 HT[i]为头结点的单链表中。

开放地址法的基本思想是:把记录都存储在散列表数组中,当某一记录关键字 key的初始散列地址H0=H(key)发生冲突时,以凡为基础 ,采取合适方法计算得到另一个地址H1,如果凡仍然发生冲突,以凡为基础再求下一个地址H2,若仍然冲突,再求得H3。依次类推,直至Hk不发生冲突为止,则凡为该记录在表中的散列地址。常见的探测方法包括线性探测法,二次探测法,伪随机探测法,

11. 排序算法学过哪些?快速排序的实现原理?

407c24a028f24773bb7c20e4eecad903.jpg

 2f8378453df2440b976db213b126b3b1.jpg

12. 如何判断一个链表是不是环形链表

  1. 定义两个指针,一个称为慢指针(slow),另一个称为快指针(fast)。
  2. 初始化慢指针和快指针为链表的头节点(即指向链表的起始位置)。
  3. 遍历链表,每次移动慢指针一步,快指针两步。
  4. 检查快指针是否为空(即指向了链表的末尾节点或者超过了末尾节点)。如果为空,则说明链表不是环形链表,因为快指针已经到达链表的末尾,没有环。
  5. 如果快指针和慢指针相遇(即两个指针指向同一个节点),则说明链表是环形链表,因为快指针追上了慢指针。

    
    bool CircleInList(Link* pHead)
    {
    if(pHead = = NULL || pHead->next = = NULL)//无节点或只有一个节点并且无自环
    return (false);
    if(pHead->next = = pHead)//自环
    return (true);
    Link *pTemp1 = pHead;//step 1
    Link *pTemp = pHead->next;//step 2
    while(pTemp != pTemp1 && pTemp != NULL && pTemp->next != NULL)
    {
    pTemp1 = pTemp1->next;
    pTemp = pTemp->next->next;
    }
    if(pTemp = = pTemp1)
    return (true);
    return (false);
    }

网络编程部分:

当一个TCP连接长时间没有数据传输时,可能会出现网络故障或者其它原因导致连接失效。

1. 服务器如何知道客户端client 是否已断电

心跳包技术:心跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。

方法1:应用层自己实现的心跳包( 使用定时器-----适合有数据流动的情况))

思路一:由应用程序自己发送心跳包来检测连接是否正常。
        大致的方法是:服务器端在一个 定时事件中 定时向客户端发送一个短小的数据包,然后启动一个线程,在该线程当中不断检测客户端的ACK应答包。如果在定时时间内收到了客户端的ACK应答包,说明客户端与服务器端的TCP连接仍然是可用的。但是,如果定时器已经超时、而服务器仍然没有收到客户端的ACK应答包,即可以认为客户端已经断开。同样道理,如果客户端在一定时间内没有收到服务器的心跳包,则也会认为改TCP连接不可用了。

思路二:心跳包应该由客户端在一个定时事件中定时向客户端发送一个短小的数据包,如果服务端收到客户端的心跳包或正常报文,则服务端的计数器归零;服务端启动一个定时器定时累加计数器,当计数器的累加值超过一定值时,则认为客户端断开。

方法2:TCP协议的KeepAlive保活机制 (使用socket选项SO_KEEPALIVE------适合没有数据流动的情况)。

        因为要考虑到一个服务器通常会连接很多个客户端,因此,由用户在应用层自己实现心跳包,代码较多而且稍显复杂。而利用TCP/IP协议层的内置的KeepAlive功能来实现心跳功能则简单得多。不论是服务器端还是客户端,只要一端开启KeepAlive功能后,就会自动的在规定时间内向对端发送心跳包, 而另一端在收到心跳包后就会自动回复,以告诉对端主机我仍然在线。
        因为开启KeepAlive功能需要消耗额外的宽带和流量,所以TCP协议层默认是不开启KeepAlive功能的。尽管这微不足道,但是在按流量计费的环境下增加了费用,另一方面,KeepAlive设置不合理的话有可能会 因为短暂的网络波动而断开健康的TCP连接。并且,默认的KeepAlive超时需要即2小时,探测次数为5次。对于很多服务端应用程序来说,2小时的空闲时间太长。因此,我们需要手工开启KeepAlive功能并设置合理的KeepAlive参数。

2. 字节序

小端序(little-endian) - 低序字节存储在低地址

大端序(big-endian)- 高序字节存储在低地址

网络中传输的数据必须使用网络字节序,即大端字节序

(1)主机字节序到网络字节序

u_long htonl (u_long hostlong);
u_short htons (u_short short);  //掌握这个

(2)网络字节序到主机字节序

u_long ntohl (u_long hostlong);
u_short ntohs (u_short short);
 

2. 在网络应用中,函数htons,htonl,ntohs,ntohl的作用是什么?

小端序(little-endian) - 低序字节存储在低地址

大端序(big-endian)- 高序字节存储在低地址

网络中传输的数据必须使用网络字节序,即大端字节序

(1)主机字节序到网络字节序

uint32_t  htonl(uint32_t  hostlong);//32位的主机字节序转换到网络字节序

uint16_t  htons(uint16_t  hostshort);//16位的主机字节序转换到网络字节序

(2)网络字节序到主机字节序

uint32_t ntohl(uint32_t netlong);//32位的网络字节序转换到主机字节序

uint16_t ntohs(uint16_t netshort);//16位的网络字节序转换到主机字节序

(皆为大小端的改变)

3. IPv4把所有的IP地址分为A、B、C、D、E五类。请写出B类地址和C类地
址的范围和掩码,D类地址的用途是什么?

795b5e0e413a4560817cf32190025bd3.png主机号的第一个和最后一个都不能被使用,第一个作为网段号,最后一个最为广播地址。

A类:1.0.0.1~126.255.255.254
B类:128.0.0.1~~191.255.255.254
C类:192.0.0.1~~223.255.255.254
D类(组播地址):224.0.0.1~~239.255.255.254

0.0.0.0:在服务器中,0.0.0.0指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,192.168.1.1 和 10.1.2.1,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过两个ip地址都能够访问该服务。

127.0.0.1:回环地址/环路地址,所有发往该类地址的数据包都应该被loop back。

IP地址=网络号+主机号,使用子网掩码来进行区分

网络号:表示是否在一个网段内(局域网)

主机号:标识在本网段内的ID,同一局域网不能重复

子网掩码:是一个32位的整数,作用是将某一个IP划分成网络地址和主机地址;
子网掩码长度是和IP地址长度完全一样;
网络号全为1,主机号全为0;

D类地址用于多点播送
E类地址保留,仅作实验和开发用
全零(“0.0.0.0”)地址指任意网络。
全“1”的IP地址(“255.255.255.255”)是当前子网的广播地址。

4. TCP和UDP的区别

TCPUDP
名称传输控制协议(Transmission Control Protocol)用户数据报协议(User Datagram Protocol)
是否连接面向连接无连接
是否可靠可靠性通信(数据无误、无丢失、无失序、无重复到达))不可靠传输
传输方式面向字节流面向报文
连接对象个数一对一通信一对一、一对多、多对一、多对多交互通信
适用场景需要可靠数据传输的场合,对传输质量要求较高的通信即时通讯、小尺寸数据、广播/组播式通信。

TCP

TCP(即传输控制协议):是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)。

适用场景

适合于对传输质量要求较高的通信

在需要可靠数据传输的场合,通常使用TCP协议

MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议

UDP

UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。

适用场景

发送小尺寸数据(如对DNS服务器进行IP地址查询时)

适合于广播/组播式通信中。

MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
 

5. TCP的三次握手过程 

三次握手是TCP用来确保建立可靠连接的方式:
1、客户端调用connect给服务器发送一个SYN同步包。

其中标志位SYN置为1,初始序列号为客户端端随机生成的一个值seq;

表示需要建立TCP连接。(SYN=1,seq=x,x为随机生成数值);


2、当服务器接收到客户端发送的SYN同步包,会回一个ACK确认包,同时给客户端发送一个SYN同步包。

其中标志位ACK置为1,ACK确认号数值是在客户端发送过来的序列号seq的基础上加1;

标志位SYN置为1,序列号数值为服务器端随机生成的一个值seq;

以便客户端收到信息时,知晓自己的TCP建立请求已得到验证。(SYN=1,ACK=1,ack=x+1,seq=y,y为随机生成数值)这里的ack加1可以理解为是确认和谁建立连接;

3、客户端接收到服务器的确认包和同步包,回一个ACK确认包,三次握手完成。

其中标志位ACK置为1,确认号数值为服务端发过来的序列号seq上加1;

seq序列号数值由客户端的的初始化序列号加1。

(ACK=1,ack=y+1,seq=x+1)。
服务器段accept返回,客户端connect返回,进行通信

aa324e0bd3c4250b0d0b09a23d29d774.jpeg

扩展:为什么需要三次握手,两次不行吗?

第一次握手:客户端发送网络包,服务端收到了。
这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了。
这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
第三次握手:客户端发包,服务端收到了。
这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
因此,需要三次握手才能确认双方的接收与发送能力是否正常。

如果是用两次握手,则会出现下面这种情况:

如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
 

6. TCP的四次挥手过程

四次挥手是TCP用来确关闭可靠连接的方式
1、客户端调用close关闭通信会给服务器发一个FIN结束包;

其中标志位FIN置为1,初始序列号为客户端随机生成的一个值seq;

表示需要断开TCP连接。(FIN=1,seq=x,x由客户端随机生成)


2、服务器端接收到客户端的FIN结束包会给客户端会一个ACK确认包。recv就会返回等于0.

其中标志位ACK置为1,ACK确认号数值是在客户端发送过来的序列号seq的基础上加1;

序列号数值为服务器端随机生成的一个值seq;

以便客户端收到信息时,知晓自己的TCP断开请求已经得到验证。(ACK=1,ack=x+1,seq=y,y由服务端随机生成);

3、服务器调用close关闭通信文件描述符,给客户端再发送一个FIN结束包。

其中标志位FIN置为1,序列号为服务器端随机生成的一个(因为是半关闭状态,服务器可能又发送了一些数据);

标志位ACK置为1,ACK确认号数值是在客户端发送过来的序列号seq的基础上加1;;

(FIN=1,ACK=1,ack=x+1,seq=z,z由服务端随机生成);


4、客户端接收到FIN结束包,给服务器回一个ACK确认包,断开连接。

标志位ACK置为1,ACK确认号数值是在服务器端端发送过来的序列号seq的基础上加1;

seq序列号数值由客户端的的初始化序列号加1。

(ACK=1,ack=z+1,seq=x+1)

b4e315865a2a2a7c5b47a5d0bcef63e8.jpeg

扩展:挥手为什么需要四次?
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

7. TCP是如何保证可靠传输的

首先,TCP 的连接是基于三次握手,而断开则是四次挥手。确保连接和断开的可靠性。
其次,TCP 的可靠性,还体现TCP 通过校验和ACK 应答超时重传来记录哪些数据发送了,哪些数据被接受了,哪些没有被接受,并且保证数据包按序到达,保证数据传输不出差错。
再次,TCP 的可靠性,还体现在通过流量控制(滑动窗口)拥塞控制来控制发送方发送速率。
 

数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组;
到达确认:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认包;
超时重发:发送方在发送分片后计时,若超时却没有收到相应的确认包,将会重发对应的分片;
滑动窗口:TCP连接双方的接收缓冲空间大小都固定,接收端只能接受缓冲区能接纳的数据。
失序处理:TCP的接收端需要重新排序接收到的数据。
重复处理:如果传输的TCP分片出现重复,TCP的接收端需要丢弃重复的数据。
数据校验:TCP通过保持它首部和数据的检验和来检测数据在传输过程中的任何变化。


8.  TCP的粘包问题

 原因:

TCP是面向流的, 流要说明就像河水一样, 只要有水, 就会一直流向低处, 不会间断。TCP为了提高传输效率, 发送数据的时候, 并不是直接发送数据到网路, 而是先暂存到系统缓冲, 超过时间或者缓冲满了, 才把缓冲区的内容发送出去, 这样, 就可以有效提高发送效率。所以会造成所谓的粘包, 即前一份Send的数据跟后一份Send的数据可能会暂存到缓冲当中, 然后一起发送。

粘包、拆包发生原因: 
    发生TCP粘包或拆包有很多原因,常见的几点: 
   1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。 
   2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。 
   3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。 
   4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
粘包、拆包解决办法: 
   解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下:
   1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。 
   2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。 
   3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。 等等。
   4.延时、效率低

9. UDP的丢包问题

UDP,是面向报文形式, 系统不会缓冲, 也不会做优化, Send的时候, 就会直接Send到网络上, 对方收不收到也不管, 所以会造成丢包问题。

原因:

1. 客户端发送过快,网络状况不好或者超过服务器接收速度,就会丢包。
2. 服务器收到包后,还要进行一些处理,而这段时间客户端发送的包没有去收,造成丢包。

解决方法:

1. 客户端降低发送速度,可以等待回包,或者加一些延迟。
2. 服务器部分单独开一个线程,去接收UDP数据,存放在一个缓冲区中,又另外的线程去处理收到的数据,尽量减少因为处理数据延时造成的丢包。

10. TCP编程框架

   ac4199904f7160bbbbb16148908fc4e9.png

服务器:
   1.创建流式套接字(socket())------------------------>  有手机
   2.指定本地的网络信息(struct sockaddr_in)----------> 有号码
   3.绑定套接字(bind())------------------------------>绑定手机
   4.监听套接字(listen())---------------------------->待机
   5.链接客户端的请求(accept())---------------------->接电话
   6.接收/发送数据(recv()/send())-------------------->通话
   7.关闭套接字(close())----------------------------->挂机
 
客户端:
   1.创建流式套接字(socket())----------------------->有手机
   2.指定服务器的网络信息(struct sockaddr_in)------->有对方号码
   3.请求链接服务器(connect())---------------------->打电话
   4.发送/接收数据(send()/recv())------------------->通话
   5.关闭套接字(close())--------------------------- >挂机
 

11.  TCP/IP网络模型分为几层?每一层什么作用?都有哪些协议?

c342202117f21dd65d4976c076373fcf.png

网络接口和物理层:屏蔽硬件差异(驱动),向上层提供统一的操作接口。

网络层:提供端对端的传输,可以理解为通过IP寻址机器。

传输:决定数据交给机器的哪个任务(进程)去处理,通过端口寻址。

应用层:应用协议和应用程序的集合。

12. OSI模型?

62cd4afca17df67b9d8a51536b216acf.png

 常见网络协议:

网络接口和物理层:
	ppp:拨号协议(老式电话线上网方式)
	ARP:地址解析协议  IP-->MAC
	RARP:反向地址转换协议 MAC-->IP
网络层:	
	IP(IPV4/IPV6):网间互连的协议
	ICMP:网络控制管理协议,ping命令使用
	IGMP:网络分组管理协议,广播和组播使用
传输层:
	TCP:传输控制协议
	UDP:用户数据报协议
应用层:
	SSH:加密协议
	telnet:远程登录协议
	FTP:文件传输协议
	HTTP:超文本传输协议
	DNS:地址解析协议
	SMTP/POP3:邮件传输协议

 注意:TCP和IP是属于不同协议栈层的,只是这两个协议属于协议族里最重要的协议,所以协议栈或者模型以之命名了。

13. 如何实现并发服务器 

同一个时刻可以响应多个客户端的请求,常用的模型有多进程模型/多线程模型/IO多路复用模型。

多进程和多线程实现的思想:每当有一个客户端连接成功就创建一个子进程或线程和这个客户端通信,父进程或主线程循环等待下一个客户端连接。

借助select、poll、epoll机制,将新连接的客户端描述符增加到描述符表中,只需要一个线程即可处理所有的客户端连接。

select、poll、epoll共同优点:
(1)占用资源少,因为是单进程处理。(相比于多进程、多线程
(2)性能好,可一次等待多个进程。

14. Select、poll、epoll的区别

select实现IO多路复用特点:

1. 一个进程最多只能监听1024个文件描述符 (千级别);
2. select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源);
3. select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低(一个进程0~4G,0~3G是用户态,3G~4G是内核态,拷贝是非常耗时的);

select优点:
( 1)select()的可移植性更好,在某些Unix系统上不支持poll()
(2)select() 对于超时值提供了更好的精度:微秒,而poll是毫秒。

poll实现IO多路复用的特点:

1. 优化文件描述符个数的限制;(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组元素个数为1,如果想监听100个,那么这个结构体数组的元素个数就为100,由程序员自己来决定)。
2. poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低。
3. poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可。

poll优点:
(1)poll() 不要求开发者计算最大文件描述符加一的大小。
(2)poll() 在应付大数目的文件描述符的时候相比于select速度更快
(3)它没有最大连接数的限制,原因是它是基于链表来存储的。

epoll实现IO多路复用的特点:

1. 监听的最大的文件描述符没有个数限制(理论上,取决与你自己的系统)。
2. 异步I/O,Epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高。
3. epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可。

epoll优点

1)支持一个进程打开大数目的socket描述符(FD)
2)IO效率不随FD数目增加而线性下降
3)使用mmap加速内核与用户空间的消息传递。

15. selet实现

实现步骤

1.创建表

2.将关心文件描述符添加到表中

3.进入循环,调用select函数检测

4.当有一个或多个事件产生select函数返回

5.判断是哪个或哪几个产生事件

6.处理事件

2595bb102aa74544a4dec8daa34b9ba0.png

       6. 优点

 6eeef5a41f9848149769cdad555d4228.png

16. poll实现

1.创建表

2.将关心文件描述符添加到表中

3.进入循环,调用poll函数监测

4.遍历数组,检查哪些文件描述符已经就绪。

5.根据就绪的文件描述符进行相应的操作,例如读取数据、写入数据或处理异常情况。

 17. epoll

1. 创建树

2. 将关心文件描述符添加到树上

3. 进入循环,在循环中使用 epoll_wait 函数等待事件发生,并阻塞等待。

4.当事件发生后,epoll_wait 函数返回就绪的文件描述符和对应的事件。根据就绪的文件描述符进行相应的操作。

34bd77477cc949e59f267387fc9aac6f.png

18. 广播和组播的区别

广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信。

组播是一个人发送,加入到多播组的人接收数据。多播方式既可以发给多个主机,又能避免象广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)。

19. 用过抓包工具么?抓过什么数据包?

 wireshark抓包工具。

TCP的三次握手和四次挥手,modbus,mqtt

20. 学过什么数据库?增删改查语句?

(1)SQLite。

 增加数据:INSERT INTO 表名[(列1,列2,...)] VALUES(值1,值2,...)

INSERT INTO myemp(empno,ename,job,mgr,hiredate,sal,deptno)
VALUES(8888,'张三','厨师',7839,DATETIME('now','localtime'),10000,40);

删除数据:DELETE FROM 表名 [WHERE 删除条件(s)]

DELETE FROM myemp WHERE deptno=30;

修改数据:UPDATE 表名 SET 列名1=值1,列名2=值2,...[WHERE 更新条件(s)]
 

UPDATE myemp SET sal=5000,comm=2000 WHERE ename='SMITH';

查询数据:

a9cb8c8cd16bb94aed89571a8206ebeb.png

创建表命令
CREATE TABLE

删除表命令

DROP TABLE

21. 如何判断在不同网络中的两台电脑是否可以通讯,用什么工具或者方法,

Ping对方的IP;用路由器实现两台电脑通信。

22. 请比较面向连接服务和无连接服务的异常点,TCP/UDP分别是什么服务,QQ聊天通信中使用的是哪种服务。

面向连接(connection-oriented),在发送任何数据之前,要求建立会话连接(与拨打电话类似),然后才能开始传送数据,传送完成后需要 释放连接。建立连接是需要分配相应的资源如缓冲区,以保证通信能正常进行。这种方法通常称为“可靠”的网络业务。它可以保证数据以相同的顺序到达。面向连 接的服务在端系统之间建立通过网络的虚链路。

无面向连接(connectiongless),不要求发送方和接收方之间的会话连接。发送方只是简单地开始向目的地发送数据分组(称为数据报)。这与现 在风行的手机短信非常相似:你在发短信的时候,只需要输入对方手机号就OK了。此业务不如面向连接的方法可靠,但对于周期性的突发传输很有用。系统不必为 它们发送传输到其中和从其中接收传输的系统保留状态信息。无连接网络提供最小的服务,仅仅是连接。无连接服务的优点是通信比较迅速,使用灵活方便,连接开 销小;但可靠性低,不能防止报文的丢失,重复或失序. 适合于传送少量零星的报文。 TCP是面向连接,UDP是无连接。

QQ是UDP。

23. 什么是域名解析及域名解析的过程,

英文缩写DNS(Domain Name System) 简单的说就是把你在浏览器地址栏输入的网页地址解析成IP地址.由于IP地址是用四段数字组成,相对英文来说不好记,通过域名解析就可以不用记IP地址,直接通过输入的英文或汉字链接到网站上了.

1.客户机提出域名解析请求,并将该请求发送给本地的域名服务器。

2.当本地的域名服务器收到请求后,就先查询本地的缓存,如果有该纪录项,则本地的域名服务器就直接把查询的结果返回。

3.如果本地的缓存中没有该纪录,则本地域名服务器就直接把请求发给根域名服务器,然后根域名服务器再返回给本地域名服务器一个所查询域(根的子域)的主域名服务器的地址。

4.本地服务器再向上一步返回的域名服务器发送请求,然后接受请求的服务器查询自己的缓存,如果没有该纪录,则返回相关的下级的域名服务器的地址。

5.重复第四步,直到找到正确的纪录。

6.本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时还将结果返回给客户机。
 

IO与进程、线程部分:

1. 标准IO和文件IO的区别

标准IO

文件IO

在C库中定义输入输出的函数

在posix中定义的输入输出的函数

有缓冲机制

围绕流操作,FILE*

默认打开三个流:stdin/stdout/stderr

只能操作普通文件

没有缓冲机制

围绕文件描述符操作,int非负整数

默认打开三个文件描述符:0/1/2

除d外其他任意类型文件

打开文件:fopen/freopen

读写文件:fgetc/fputc、fgets/fputs、fread/fwrite

关闭文件:fclose

文件定位:rewind、fseek、ftell

打开文件:open

读写文件:read、write

关闭文件:close

文件定位:lseek

2. 静态库和动态库的区别

静态库和动态库,本质区别是代码被载入时刻不同。

1) 静态库在程序编译时会被连接到目标代码中。

优点:程序运行时将不再需要该静态库;运行时无需加载库,运行速度更快

缺点:静态库中的代码复制到了程序中,因此体积较大;

静态库升级后,程序需要重新编译链接

2) 动态库是在程序运行时才被载入代码中。

优点:程序在执行时加载动态库,代码体积小;

程序升级更简单;

不同应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。

缺点:运行时还需要动态库的存在,移植性较差

3. 怎么创建进程

#include <stdio.h>
#include <unistd.h>
 
int main(int argc, char const *argv[])
{
    int num = 10;
    pid_t id;
    id = fork(); //创建子进程
    if(id < 0)
    {
        perror("fork err");
        return -1;
    }
    else if(id == 0)
    {
        //in the child
        printf("in the child\n");
    }
    else
    {
        // int s;
        // wait(&s); //回收子进程资源,阻塞函数
        // printf("%d\n", s);
        wait(NULL);
        //in the parent
        printf("in the parent\n");
        while(1);
    }
    // while(1);
    
    return 2;
}

4. 什么是守护进程

1、 特点:
守护进程是后台进程,不依赖于控制终端;

生命周期比较长,从运行时开启,系统关闭时结束;

它是脱离控制终端且周期执行的进程。

2. 步骤:
1) 创建子进程,父进程退出

让子进程变成孤儿进程,成为后台进程;fork()

2) 在子进程中创建新会话

让子进程成为会话组组长,为了让子进程完全脱离终端;setsid()

3) 改变进程运行路径为根目录

原因进程运行的路径不能被卸载;chdir("/")

4) 重设文件权限掩码

目的:增大进程创建文件时权限,提高灵活性;umask(0)

5) 关闭文件描述符

将不需要的文件关闭;close()
 

5. 什么是僵尸进程?什么是孤儿进程?

若父进程先结束,子进程成为孤儿进程,被init进程收养,子进程变成后台进程。

若子进程先结束,父进程如果没有及时回收,子进程变成僵尸进程(要避免僵尸进程产生)

6. 时间片了解么?

       时间片(timeslice)又称为 “量子”(quantum)或 “处理器片”(processor slice),是分时操作系统分配给每个正在运行的进程微观上的一段 CPU 时间(在抢占内核中是:从进程开始运行直到被抢占的时间)。

       简单来说,时间片就是 CPU 分配给各个程序的时间,即该进程允许运行的时间。如果进程在时间片结束时还在运行,则 CPU 将被强制剥夺并分配给另一个进程;如果进程在时间片结束前就阻塞或结束,则 CPU 会在阻塞或结束时当即进行切换。

7. 进程和线程区别?

共性:都为操作系统提供了并发执行能力

不同点:

调度和资源:线程是系统调度的最小单位,进程是资源分配的最小单位

地址空间方面:同一个进程创建的多个线程共享进程的资源;进程的地址空间相互独立

通信方面:线程通信相对简单,只需要通过全局变量可以实现,但是需要考虑临界资源保护的问题;进程通信比较复杂,需要借助进程间的通信机制(借助3g-4g内核空间)

安全性方面:线程安全性差一些,当进程结束时会导致所有线程退出;进程相对安全
 

8. 线程的同步怎么实现

通过信号量实现线程间同步。

信号量:由信号量来决定线程是继续运行还是阻塞等待,信号量代表某一类资源,其值表示系统中该资源的数量

信号量是一个受保护的变量,只能通过三种操作来访问:初始化、P操作(申请资源)、V操作(释放资源)

信号量的值为非负整数

9. 线程的互斥怎么实现

临界资源:一次仅允许一个进程所使用的资源

临界区:指的是一个访问共享资源的程序片段

互斥:多个线程在访问临界资源时,同一时间只能一个线程访问

     互斥锁:通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。
 

10. 进程间通信方式有哪些?哪种效率最高

无名管道、有名管道、信号,消息队列,共享内存,信号灯集、套接字(socket)

共享内存效率最高

11. 线程同步(通讯)的方法

(1)信号量(2)读写锁(3)条件变量(4)互斥锁(5)自旋锁

12. 什么时候用进程,什么时候用线程

1)创建和销毁较频繁使用线程,因为创建进程花销大。

2)需要大量数据传送使用线程,因为多线程切换速度快,不需要跨越进程边界。

3)安全稳定选进程;快速频繁选线程;

13. 说明什么是上下文切换?


有很多角度,有进程上下文,有中断上下文。

进程上下文:一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

中断上下文:由于触发信号,导致CPU中断当前进程,转而去执行另外的程序。那么当前进程的所有资源要保存,比如堆栈和指针。保存过后转而去执行中断处理程序,快读执行完毕返回,返回后恢复上一个进程的资源,继续执行。这就是中断的上下文。
 

14. 通信方式的优缺点对比

  1. 无名管道(Unnamed Pipe):

    • 优点:
      • 简单易用,是最基本的进程间通信机制。
      • 仅限于具有亲缘关系的进程间通信。
      • 只能实现单向通信,即数据流只能从一个进程传递给另一个进程。
    • 缺点:
      • 容量有限,传输数据量受限。
      • 只能在同一个主机上进行通信。
      • 只能用于单个读者和单个写者之间的通信。
  2. 有名管道(Named Pipe):

    • 优点:
      • 允许无关进程间的通信,不仅限于具有亲缘关系的进程。
      • 具有独立的读写权限,可以在同一管道上进行双向通信。
    • 缺点:
      • 容量有限,传输数据量受限。
      • 需要通过文件操作系统来进行配置和使用。
      • 与管道相比,有名管道的设置和管理较为繁琐。
  3. 信号(Signal):

    • 优点:
      • 提供异步通信机制,用于进程间的通知和事件处理。
      • 可以响应外部事件,例如按下Ctrl+C键等。
    • 缺点:
      • 信号通信的灵活性有限,只能传递简单的信号值,不能传输大量数据。
      • 信号可能导致竞争条件和不可预测的行为。
      • 不适用于高速数据传输和复杂的通信需求。
  4. 消息队列(Message Queue):

    • 优点:
      • 提供异步通信机制,发送和接收进程无需同时存在。
      • 具有缓冲区,使得消息的发送和接收可以彼此独立进行,提高了系统的灵活性和容错性。
    • 缺点:
      • 通信开销较大,因为需要将数据从用户空间复制到内核空间。
      • 需要定义特定的消息格式和协议,以保持消息的一致性和正确性。
  5. 共享内存(Shared Memory):

    • 优点:
      • 最快的进程间通信方式,进程可以直接访问共享内存区域,无需数据复制。
      • 适合大量数据的高速传输。
      • 允许进程间异步通信。
    • 缺点:
      • 需要额外的同步机制,如信号量或互斥锁,来控制对共享内存的并发访问。
      • 可能引发一些资源管理和数据一致性的问题。
  6. 信号灯集(Semaphore):

    • 优点:
      • 用于进程间同步和互斥操作,实现进程间的资源共享和互斥访问。
    • 缺点:
      • 使用复杂,需控制信号灯的值和操作。
      • 可能导致竞争条件和死锁问题。
  7. 套接字(Socket):

    • 优点:
      • 提供网络通信的能力,允许不同主机上的进程进行通信。
      • 提供可靠的、面向连接的通信,适用于传输大量数据和实时通信。
      • 支持多种通信协议,如TCP/IP和UDP/IP。
    • 缺点:
      • 编程复杂,需处理网络通信的细节。
      • 通信开销较大,需要进行数据的序列化和反序列化。

15. 有名管道和无名管道的区别?

d41de30ff8b74404b2f71d8cbb618e99.png

16. 共享内存的实现方式

0)创建key值 // 两个进程key相同,

1)创建或打开共享内存   shmget

2)映射                 shmat

3)取消映射             shmdt

4)删除共享内存         shmctl

17. 消息队列的实现方式

1)创建key值  ftok

2)创建或打开消息队列  msgget  

3)添加消息/读取消息   msgsnd/msgrcv

4)删除消息队列  msgctl
 

18. fork和vfork区别

1. fork( )的子进程拷贝父进程的数据段和代码段; vfork( )的子进程与父进程共享数据段
2.fork()的父子进程的执行次序不确定;vfork( )保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。
3. vfork( )保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进—步动作,则会导致死锁。
4.当需要改变共享数据段中变量的值,则拷贝父进程。
 

fork()vfork()都是在UNIX和类UNIX系统中使用的系统调用,用于创建一个新的进程。它们之间的主要区别在于它们对父子进程之间的共享资源的处理方式。

  1. fork():

    • fork()调用通过创建一个父进程的副本来创建一个新的子进程。
    • 父进程和子进程各自拥有独立的虚拟地址空间和内存映像。
    • 父进程的所有资源(包括打开的文件、信号处理器、锁等)都会被复制到子进程中。
    • 父进程和子进程之间共享的唯一资源是打开的文件描述符的引用计数,也就是说它们引用相同的文件表项。但是,它们使用不同的文件描述符表和虚拟内存地址空间。
    • 子进程将从fork()调用之后的下一行代码开始执行。
    • fork()调用返回两次,在父进程中返回子进程的进程ID(PID),在子进程中返回0。这样父子进程可以根据返回值进行不同的操作。
  2. vfork():

    • vfork()调用是为了解决fork()的性能问题而引入的。
    • vfork()调用创建一个新的子进程,但是它会暂停父进程的执行,直到子进程调用exec()系统调用或者_exit()系统调用,或者父进程调用exit()系统调用。
    • 父进程和子进程共享同一个虚拟地址空间,也就意味着它们共享同一份代码和数据段。
    • 子进程在执行期间不能访问任何变量,否则可能导致意外的行为。
    • vfork()调用返回两次,与fork()类似,但是父进程会等待子进程调用exec()_exit()或者exit()之后才恢复执行。

总结:

  • fork()创建父子进程的完全副本,拥有独立的资源和虚拟地址空间。
  • vfork()创建共享虚拟地址空间的子进程,父进程暂停执行直到子进程调用exec()_exit()或者exit()
  • 通常情况下,fork()更常见且更安全,因为它避免了在共享虚拟地址空间中对变量的意外修改。

19. 线程的死锁,怎么避免?

是指两个或两个以上的进程/线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去

死锁产生的四个必要条件:

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

20. sem_ wait(sem_ *sem)的功能是什么,和sem_trywait(sem_ *sem)有什么区别?

sem_wait(sem_t *sem): 这个函数用于对信号量进行等待操作。当调用sem_wait时,如果信号量的值大于0,则将信号量的值减1,并继续执行。如果信号量的值为0,表示没有可用资源,则会阻塞当前线程,直到有资源可用。当其他线程或进程通过sem_post增加信号量的值时,被阻塞的线程将解除阻塞并继续执行。

sem_trywait(sem_t *sem): 这个函数也用于对信号量进行等待操作。调用sem_trywait时,它会尝试对信号量进行等待,如果信号量的值大于0,则将信号量的值减1,并继续执行。如果信号量的值为0,sem_trywait函数并不会阻塞当前线程,而是直接返回,不对信号量进行等待。

所以,sem_wait函数会阻塞当前线程并等待信号量的值变为非零,而sem_trywait函数会立即返回,不对信号量进行等待。使用这两个函数可以根据实际情况选择阻塞等待或立即返回的方式来处理信号量。

21. 程序和进程的区别

(1)程序是编译好的可执行文件,存放在磁盘上的指令和数据的有序集合(文件);

         进程是一个独立的可调度的任务,它是执行一个程序所分配的资源的总称

(2)程序是静态的观念,进程是动态的观念,包括创建、调度、执行和消亡;

(3)程序是永存的;进程是暂时的,是程序在数据集上的一次执行,有创建有撤销,存在是暂时            的;

(4)进程具有并发性,而程序没有;

(5)进程是竞争计算机资源的基本单位,程序不是。

(6)进程是程序的一次执行过程

C++部分:

1. 指针和引用相同点和不同点,相互转换

相同点:

1.都是地址的概念,指针指向某一内存、它的内容是所指内存的地址;引用则是某块内存的别名。

2.从内存分配上看:两者都占内存,程序为指针会分配内存,一般是4个字节;而引用的本质是指针常量,指向对象不能变,但指向对象的值可以变。两者都是地址概念,所以本身都会占用内存。
 

不同点:
1.指针是实体,而引用是别名。
2.指针和引用的自增(++)运算符意义不同,指针是对内存地址自增,而引用是对值的自增。

3.引用使用时无需解引用(*),指针需要解引用;(关于解引用大家可以看看这篇博客,传送门)。

4.引用只能在定义时被初始化一次,之后不可变;指针可变。
5.引用不能为空,指针可以为空。
6."sizeof 引用"得到的是所指向的变量(对象)的大小,而"sizeof 指针"得到的是指针本身的大小,在32位系统指针变量一般占用4字节内存。
 

转换

a906f58fc56f44e5b4c7b87ea31de2b3.png

2. 类和结构体区别

1.类型不同:结构体是一种值类型,而类是引用类型。值类型用于存储数据的值,引用类型用于存储对实际数据的引用。那么结构体就是当成值来使用的,类则通过引用来对实际数据操作。

2.存储不同:结构使用栈存储,而类使用堆存储。栈的空间相对较小.但是存储在栈中的数据访问效率相对较高.堆的空间相对较大.但是存储在堆中的数据的访问效率相对较低。

3.作用不同:类是反映现实事物的一种抽象,而结构体的作用只是一种包含了具体不同类别数据的一种包装,结构体不具备类的继承多态特性

4.初始化不同:类可以在声明的时候初始化,结构不能在申明的时候初始化(不能在结构中初始化字段),否则报错。

3. C++中Static、const作用

static作用:

类中的static主要用于创建静态成员变量静态成员函数静态局部变量。

普通的成员变量使用static修饰就是静态成员变量。这种静态成员变量需要类内声明,类外初始化。

静态成员函数没有this指针,因此不能调用非静态成员(非静态成员变量和成员函数)。

使用static修饰的局部变量就是静态局部变量。静态局部变量所在的代码块被第一次调用时,静态局部变量内存开辟,与非静态局部变量不同的是,代码块调用完成后,静态局部变量不会被销毁,下次代码被调用时,继续使用之前的静态局部变量,直到程序运行终止后才会被自动销毁。

const作用:

const修饰的成员函数,称为常成员函数。

const修饰对象,表示该对象是一个常量对象。

const修饰的成员变量,表示常成员变量。

const修饰局部变量,表示局部变量不可变,通常用于引用类型的函数参数。

4. C和C++的区别

1.  C是面向过程的语言,而C++是面向对象的语言

2. C和C++动态管理内存的方法不一样,C是使用malloc/free函数,而C++除此之外还有new/delete关键字

3. C中的struct和C++的类,C++的类是C所没有的,但是C中的struct是可以在C++中正常使用的,并且C++对struct进行了进一步的扩展,使struct在C++中可以和class一样当做类使用,而唯一和class不同的地方在于struct的成员默认访问修饰符是public,而class默认的是private;

4. C++支持函数重载,而C不支持函数重载,而C++支持重载的依仗就在于C++的名字修饰与C不同,例如在C++中函数int fun(int ,int)经过名字修饰之后变为 _fun_int_int ,而C是 _fun,一般是这样的,所以C++才会支持不同的参数调用不同的函数;

5. C++中有引用,而C没有;

6. C++全部变量的默认链接属性是外链接,而C是内连接;

7. C 中用const修饰的变量不可以用在定义数组时的大小,但是C++用const修饰的变量可以

8. C语言和C++的最大区别在于它们解决问题的思想方法不一样。C语言主要用于嵌入式领域,驱动开发等与硬件直接打交道的领域, C++可以用于应用层开发,用户界面开发等于操作系统打交道的领域。

5. 什么是构造函数、析构函数?

构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载

析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。

6. New、delete、malloc、free区别

1、new/delete是C++的操作符,而malloc/free是C中的函数。

2、new做两件事,一是分配内存,二是调用类的构造函数;同样,delete会调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。

3、new建立的是一个对象,而malloc分配的是一块内存;new建立的对象可以用成员函数访问,不要直接访问它的地址空间;malloc分配的是一块内存区域,用指针访问,可以在里面移动指针;new出来的指针是带有类型信息的,而malloc返回的是void指针。

4、new/delete是保留字,不需要头文件支持;malloc/free需要头文件库函数支持。

7. 虚函数的作用?

指向基类的指针在操作它的多态类对象时,可以根据指向的不同类对象调用其相应的函数,这个函数就是虚函数。
虚函数的作用:在基类定义了虚函数后,可以在派生类中对虚函数进行重新定义,并且可以通过基类指针或引用,在程序的运行阶段动态地选择调用基类和不同派生类中的同名函数。(如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。)

411816a31ec74e8baeee315f29b227fe.png

8. 对多态的理解?多态的原理?

多态可以被概括为“一个接口,多重状态”,只写一个函数接口,在程序运行时才决定调用类型对应的代码。

多态的使用需要有以下条件:

  • 必须使用公有继承
  • 派生类要有函数覆盖(重写)
  • 基类引用/指针指向派生类对象

多态性的原理可以总结为以下步骤:

  1. 声明一个基类,并在其中定义一个或多个虚函数。
  2. 派生类继承基类,并重写(覆盖)基类中的虚函数。
  3. 创建基类的指针或引用,并将其指向派生类的对象。
  4. 在运行时,通过基类指针或引用调用虚函数。
  5. 运行时系统根据对象的实际类型,动态确定调用的函数实现。
  6. 根据对象的实际类型,在虚函数表中查找对应的函数地址,并进行调用。

多态性使得代码具备更高的灵活性,可以通过统一的接口处理不同类型的对象,而不需要显式地针对每种类型编写不同的代码逻辑。它提供了代码的可扩展性和可维护性,同时也是面向对象编程中的一项重要特性。

9. 为什么在继承中将析构函数定义为虚析构函数?

在C++中将析构函数定义为虚析构函数是为了解决基类指针指向派生类对象时的内存释放问题。当一个类通过继承关系被作为基类使用时,通常会使用基类指针或引用指向派生类的对象。如果基类的析构函数不是虚析构函数(没有加上virtual关键字),那么在通过基类指针或引用释放派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致派生类中动态分配的资源没有被正确释放,造成内存泄漏。

使用虚析构函数可以解决这个问题。当基类的析构函数被声明为虚析构函数时,在删除派生类对象时,会先调用派生类的析构函数,然后再调用基类的析构函数。这样可以确保在释放对象时正确调用派生类和基类的析构函数,从而正确释放对象中的资源。

10. vector与list的区别,vector、list、map是什么存储结构

vector内部靠数组实现,随机存取(固定位置读写)比较高效,相对插入删除操作效率较低,支持下标操作。

list内部由双向链表实现,因此元素的内存空间不连续,不能通过下标进行元素访问,但是能高效地进行插入和删除操作。

  • Vector的存储结构是连续的数组,元素在内存中依次排列。
  • List的存储结构是双向链表,通过指针将元素连接起来。
  • Map是一种关联容器,它将键和值配对存储。内部实现通常是基于红黑树,以保持键的有序性,并提供高效的查找和插入操作。

11. 迭代器的作用

迭代器(Iterator)是一种抽象的数据访问方式,它提供了一种统一的方法遍历容器或容器类似的数据结构中的元素,而不需要了解底层的存储结构。迭代器可以在容器中前进、后退,以及访问当前位置的元素,使得对容器的遍历和操作更加方便和灵活。

迭代器的主要作用如下:

  1. 遍历容器:迭代器提供了对容器中元素的顺序访问,可以遍历容器中的每一个元素,而不需要使用特定的循环结构,如for循环或while循环。

  2. 访问元素:通过迭代器,可以直接访问容器中当前位置的元素,获取或修改其值。

  3. 插入和删除元素:某些类型的迭代器(如双向迭代器和随机访问迭代器)还支持在容器中插入和删除元素,而不会影响其他迭代器的有效性。

  4. 支持泛型编程:迭代器的设计是为了支持泛型编程,使得可以使用相同的算法和操作来处理不同类型的容器,只需改变迭代器的类型即可。

总的来说,迭代器提供了一种统一的、通用的方式来访问和操作容器中的元素,使得代码更加简洁、可读性更高,并且增加了代码的灵活性和可重用性。

12. 智能指针有哪些?

首先,我们C++中有四种智能指针,都在标准名字空间下,使用时都需要引入头文件*include ,这四种指针分别是:

auto_ptr(自动指针,C++98中引入,目前推荐不使用)
unique_ptr (唯一指针,C++11引入)
shared_ptr(共享指针,C++11引入)
weak_ptr(虚指针,C++11中引入)
 

13. C++11新特性有哪些?

  1. 自动类型推导(auto):使用auto关键字可以自动推导变量的类型。

  2. 基于范围的for循环:使用范围(range)迭代器遍历容器中的元素。

  3. 列表初始化(Initializer lists):可以使用初始化列表语法对变量和容器进行初始化。

  4. 初始化列表构造函数:类可以定义初始化列表构造函数来接受和处理初始化列表。

  5. 空指针(nullptr):引入了nullptr关键字来表示空指针,替代了NULL和0的模棱两可的语义。

  6. 强类型枚举类(strongly typed enums):增强了枚举类型的类型安全性,可以指定底层类型以及作用域。

  7. Lambda表达式:允许在代码中定义匿名函数,提供了更方便的函数对象编程方式。

  8. 函数对象(Function objects):新增了函数对象std::function和std::bind,方便地封装函数和成员函数。

  9. 智能指针(Smart pointers):引入了std::shared_ptr、std::unique_ptr和std::weak_ptr等智能指针类,用于管理动态分配的内存,提供更安全和方便的内存管理机制。

  10. 新增关键字和特殊标识符:引入了关键字如constexpr、decltype、static_assert等,以及特殊标识符如override和final等,用于增强语言的表达能力和类型检查。

  11. 移动语义(Move semantics):引入了右值引用(rvalue references)和移动构造函数、移动赋值运算符等机制,提供了更高效的资源管理和对象移动操作。

  12. 并发编程库(Concurrency library):引入了std::thread、std::mutex和std::atomic等线程和原子操作相关的库,方便进行并发编程。

这只是C++11引入的一些主要特性,还有其他的一些特性,如正则表达式、统一的初始化语法、类型推导等等,它们一起显著改进了C++语言的功能和表达能力。

14. 深拷贝和浅拷贝区别

深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在对象复制过程中常用的两种方式,它们具有以下区别:

  1. 拷贝的内容:

    • 浅拷贝:执行浅拷贝时,只复制对象本身的值,包括基本数据类型的成员变量的值,以及指针成员变量的地址。这意味着原对象和拷贝对象会共享指针指向的内存区域。
    • 深拷贝:执行深拷贝时,除了复制对象本身的值外,还会复制指针指向的内存区域,使得原对象和拷贝对象拥有彼此独立的内存空间。
  2. 对象间的关联:

    • 浅拷贝:在浅拷贝中,原对象和拷贝对象共享指针指向的内存,如果其中一个对象修改了共享内存的值,另一个对象也会受到影响。
    • 深拷贝:由于深拷贝会为指针成员变量分配独立的内存空间,所以原对象和拷贝对象之间不存在共享内存的情况。修改一个对象的数据不会影响另一个对象。
  3. 对象生命周期:

    • 浅拷贝:当原对象被销毁时,共享的指针将成为野指针,并且如果拷贝对象也没有进行释放,就会导致内存泄漏或访问非法内存的问题。
    • 深拷贝:拷贝对象拥有独立的内存空间,所以在对象销毁时能够正确释放自己的资源,不会导致野指针或内存泄漏问题。

在使用自定义的类或结构体时,需要根据具体需求来选择深拷贝或浅拷贝。如果对象的成员变量不包含指针或其他动态分配的资源,浅拷贝可以满足需求。但如果对象包含指针成员或动态分配的资源,为了避免潜在的问题,深拷贝是更安全和可靠的方式。

需要注意的是,执行深拷贝需要自定义拷贝构造函数和赋值运算符重载,确保正确地进行内存分配和释放,以避免内存泄漏或重复释放的问题。

15. This指针的作用

  1. 解决成员变量与参数名冲突:当类的成员函数中存在与参数名同名的成员变量时,可以使用this指针来区分它们。通过使用this指针,可以显式地访问当前对象的成员变量,而不会与同名的函数参数产生混淆。
  2. 在类的成员函数中访问当前对象:this指针可以在类的成员函数内部获取当前对象的地址,使得成员函数能够访问和操作当前对象的成员变量和成员函数。

总的来说,this指针的作用是在类的成员函数中指向当前对象,使得可以在函数内部访问和操作当前对象的成员变量和成员函数。它解决了成员变量与参数名冲突的问题,并提供了方便的方式来操作当前对象。

16. 重载、覆盖、隐藏的区别?

  1. 重载(Overloading):

    • 定义:是指在同一个作用域内,根据函数参数的类型、个数或顺序,定义具有相同名称但不同参数的多个函数。
    • 特点:
      • 函数重载是静态多态性(编译时多态),在编译时根据函数调用的参数类型或个数匹配对应的函数。
      • 函数重载与函数的返回类型无关。
      • 函数重载可以在同一个类中或不同的类之间进行。
  2. 覆盖(Override):

    • 定义:是指子类重新定义(或实现)了从父类继承的同名、同参数列表的成员函数。
    • 特点:
      • 覆盖是动态多态性(运行时多态),在运行时根据对象的实际类型调用对应的函数。
      • 函数覆盖要求函数名、参数类型和个数相同,且返回类型也必须兼容。
      • 覆盖只能存在于继承关系中,即子类可以覆盖父类的成员函数。
  3. 隐藏(Hiding):

    • 定义:是指子类定义了与父类同名的成员函数,但参数列表不同。
    • 特点:
      • 隐藏是静态多态性(编译时多态),在编译时根据函数调用时的静态类型(声明类型)进行函数匹配。
      • 函数隐藏要求函数名相同但参数类型和个数不同。
      • 函数隐藏可以在继承关系中以及在同一个类中进行。

总结来说,重载是根据函数参数的类型、个数或顺序定义多个同名函数,覆盖是子类重新定义父类的同名函数,而隐藏是同时定义了相同函数名但不同参数的函数。重载是编译时多态,覆盖和隐藏是运行时多态。重载和隐藏可以在同一个类中进行,而覆盖只能存在于继承关系中。

17. 访问权限private、protected、public区别?

762385ad8d484d628f32b5f4d1251c6b.png

18. 在 C++ 程序中调用被 C 编译器编译后的函数,为什么要加 extern “C”

 答:C++语言支持函数重载,C 语言不支持函数重载。函数被 C++编译后在库中的名字与 C 语言的不同。假设某个函数的原型为: void foo(int x, int y);

该函数被 C 编译器编译后在库中的名字为_foo,而 C++编译器则会产生像_foo_int_int 之类 的名字。

C++提供了 C 连接交换指定符号 extern“C”来解决名字匹配问题

19. Public继承、protected继承、private继承的区别?

1、public继承就是公有继承完还是公有,保护还是保护,私有还是私有

2、protected继承就是公有变保护,保护还是保护,私有还是私有

3、private继承就是所有变成私有

20. extern”C” 的作用

我们可以在C++中使用C的已编译好的函数模块,这时候就需要用到extern”C”。也就是extern“C” 都是在c++文件里添加的。

extern在链接阶段起作用(四大阶段:预处理--编译--汇编--链接)。

stm32单片机部分:

1. RTOS是什么?

实时操作系统(Real Time Operating System,简称RTOS)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统做出快速响应,调度一切可利用的资源完成实时任务,并控制所有实时任务协调一致运行的操作系统。提供及时响应和高可靠性是其主要特点。

实时操作系统(RTOS)是一种专门设计用于实时应用的操作系统。RTOS旨在满足实时应用对于可靠性、可预测性和响应性的需求,并提供任务调度、中断处理、资源管理和通信机制等功能。

RTOS与通用操作系统(如Windows、Linux等)的主要区别在于其专注于实时性能和可靠性。实时应用要求系统能够在严格的时间限制内响应和处理事件,因此RTOS需要提供可靠的任务调度和中断处理机制,以确保关键任务能够及时执行,并保持适当的响应时间。

RTOS通常具有以下特点:

  1. 实时性:RTOS要能够在严格的时间限制内响应事件,并提供最小的延迟和最大的确定性。

  2. 可预测性:RTOS需要提供可预测的任务执行和中断处理时间,以便应用程序能够按照需求进行时间规划和调度。

  3. 资源管理:RTOS需要提供有效的资源管理机制,如任务管理、内存管理和设备管理,以便应用程序能够合理地使用系统资源。

  4. 通信和同步:RTOS需要提供通信和同步机制,以便多个任务之间能够安全地共享数据和协调操作。

  5. 中断处理:RTOS需要提供可靠的中断处理机制,以便及时响应外部事件和设备请求。

RTOS广泛应用于各种实时应用领域,如航空航天、汽车电子、医疗设备、工业自动化、嵌入式系统等,这些领域对于实时性和可靠性的要求较高。常见的RTOS有FreeRTOS、RTOS-32、QNX等,它们提供了丰富的功能和工具,帮助开发人员构建可靠的实时系统。

2. 谈谈你对中断的理解?

在处理器中,中断是一个过程,即CPU在正常执行程序的过程中,遇到外部/内部的紧急事件需要处理,暂时中止当前程序的执行,转而去为处理紧急的事件,待处理完毕后再返回被打断的程序处继续往下执行。

中断在计算机多任务处理,尤其是即时系统中尤为重要。比如uCOS,FreeRTOS等。

中断能提高CPU的效率,同时能对突发事件做出实时处理

实现程序的并行化,实现嵌入式系统进程之间的切换

3. Linux操作系统和RTOS的区别?

  1. 实时性要求:RTOS专注于满足实时应用对于响应时间和可预测性的严格要求。它们提供了可靠的任务调度和中断处理机制,以确保关键任务能够及时执行,并保持适当的响应时间。而Linux操作系统通常是一种通用用途的操作系统,优先考虑资源利用率和多任务处理,而不一定满足实时性需求。

  2. 内核大小和复杂性:RTOS的内核通常较小而精简,专注于提供实时性能和简单的任务调度。反之,Linux内核较大且功能丰富,它需要支持多种硬件设备和文件系统,提供了广泛的功能和驱动程序支持。

  3. 资源管理:RTOS提供了更严格和精确的资源管理机制,以确保对处理器、内存和其他设备资源的可预测性和有效利用。Linux操作系统则更侧重于通用资源管理和多任务处理。

  4. 网络和图形界面:Linux操作系统在网络通信和图形界面方面具有强大的支持,它提供了丰富的网络协议栈和图形用户界面(GUI)功能。而RTOS通常专注于实时控制任务,这些功能可能没有如此广泛的支持。

  5. 可定制性:RTOS通常提供了可定制和裁剪的机制,以便满足特定应用需求和资源限制。开发人员可以根据需求选择和配置所需的功能,以减小内核大小和优化性能。相比之下,定制Linux内核的过程较为复杂。

总体而言,RTOS适用于对实时性能要求较高的应用,如航空航天、汽车电子、医疗设备等。而Linux操作系统则适用于更通用的计算机应用,如服务器、个人计算机、嵌入式系统等,在这些应用中,实时性能可能不是首要关注点,而更注重通用性和功能的丰富性。

4. NVIC在哪里?

NVIC,提供中断控制器,用于总体管理异常,称之为“内嵌向量中断控制器:Nested Vectored Interrupt Controller (NVIC)”

4a94a74ed91544f0a56c736cd32ba8c8.png

在Cortex-M系列处理器中,NVIC通常是集成在处理器的内部,作为其核心的一部分。具体而言,在ARM Cortex-M0内核中,NVIC位于内部,与处理器的内核一起工作。ARM Cortex-M3、M4和M7等内核也拥有类似的NVIC实现。 

5. 简述一下冯诺依曼架构和哈佛架构

解释冯诺依曼体系结构:
(1) 一律用二进制数表示数据和指令。

(2) 顺序执行程序。执行前,将需要的程序和数据先放入存储器(PC为内存)。当执行时把要执行的程序和要处理的数据按顺序从存储器中取出指令一条一条地执行,称作顺序执行程序。

(3) 计算机硬件由运算器、控制器、存储器、输入设备和输出设备五大部分组成。

format,png

定义
冯诺依曼结构采用指令和数据统一编址,使用同条总线传输,CPU读取指令和数据的操作无法重叠。

哈佛结构采用指令和数据独立编址,使用两条独立的总线传输,CPU读取指令和数据的操作可以重叠。

利弊
冯诺依曼结构主要用于通用计算机领域,需要对存储器中的代码和数据频繁的进行修改,统一编址有利于节约资源。
哈佛结构主要用于嵌入式计算机,程序固化在硬件中,有较高的可靠性、运算速度和较大的吞吐。
 

区别:

1、存储器结构不同

(1)、冯诺依曼结构:冯诺依曼结构是一种将程序指令存储器和数据存储器合并在一起的存储器结构。

(2)、哈佛结构:哈佛结构使用两个独立的存储器模块,分别存储指令和数据,每个存储模块都不允许指令和数据并存。

2、总线不同

(1)、冯诺依曼结构:冯诺依曼结构没有总线,CPU与存储器直接关联。

(2)、哈佛结构:哈佛结构使用独立的两条总线,分别作为CPU与每个存储器之间的专用通信路径,而这两条总线之间毫无关联。

3、执行效率不同

(1)、冯诺依曼结构:冯诺依曼结构其程序指令和数据指令执行时不可以预先读取下一条指令,需要依次读取,执行效率较低。

(2)、哈佛结构:哈佛结构其程序指令和数据指令执行时可以预先读取下一条指令,具有较高的执行效率。

6. M0内核有多少个内部中断?多少个外部中断?

Cortex-M0内核 可以处理15个内部异常和32个外部中断

7. 简述外部中断事件框图

72e864041e994167833ca9638c515d64.png

外部中断/事件处理过程框图分析:
    编号1是信号输入线,EXTI支持产生多达28个外部事件/中断请求。
    编号2是边沿检测电路,用于监测上升沿或下降沿信号。
    
    编号3是一个或门电路,信号来源是外部事件或者软件中断/事件寄存器产生。
    
    编号4是一个与门电路,信号来源是编号3送来的信号和中断屏蔽寄存器的值,
    如果中断屏蔽寄存器为0,也不会将信号送到NVIC,
    只有编号3送来了中断信号且中断屏蔽寄存器允许产生中断,才会将中断信号送入NVIC.
    
    编号5是一个与门电路,信号来源是编号3送来的信号和事件屏蔽寄存器的值,
    如果事件屏蔽寄存器为0,不会将信号送到脉冲发生器,
    只有编号3送来了信号且事件屏蔽寄存器允许产生事件,才会将信号送入脉冲发生器(编号            6), 进而产生脉冲来控制外部设备做出动作。    
 

8. 简述单片机中的前后台操作(什么是前台,什么是后台)

前台操作指的是主要的任务或功能,通常是基于事件驱动的,需要及时响应和处理。这些前台任务是优先级较高的任务,它们负责实时性较高的任务处理,例如处理外部中断、采集传感器数据、控制外围设备等。前台任务需要在最短的时间内完成,并及时响应外部事件或请求。

后台操作指的是次要的任务或功能,通常是基于轮询方式的,可以延迟响应或周期性执行。这些后台任务是优先级较低的任务,它们执行的频率相对较低,例如处理数据存储、更新界面显示、执行周期性的系统检测等。后台任务的执行时间较长,不需要实时响应,可以在需要时进行处理。

前台和后台操作可以通过任务优先级的设置来实现。前台任务的优先级较高,使其在任务调度时具有更高的执行优先级,以确保及时响应和处理。而后台任务的优先级较低,将在前台任务没有紧急处理需求时执行。

使用前后台操作的优势在于能够灵活管理和调度多个任务,提高系统的有效利用率和响应能力。通过合理设置前后台任务之间的优先级关系,可以满足不同任务的实时性要求,提升系统性能和稳定性。

需要注意的是,前后台操作的具体实现方式可能因单片机的架构和使用的实时操作系统(RTOS)的不同而有所差异。因此,在使用单片机进行任务管理时,根据具体的硬件和软件环境,开发人员需要合理设计任务优先级和任务调度策略,以实现前后台操作的有效管理。

9. 如何保障串口通信过程中数据的安全性?

  1. 数据完整性:使用校验机制来确保数据在传输过程中的完整性。常见的校验方式包括奇偶校验、CRC校验等。发送端在将数据发送前计算校验值,并将其附加在数据中,接收端在接收到数据后进行校验,如果校验失败则表示数据损坏或篡改。

  2. 数据加密:对敏感数据进行加密处理,确保只有合法的接收方能够解密并获取到原始数据。常见的加密算法包括对称加密(如AES算法)和非对称加密(如RSA算法)。发送方在发送数据前对其进行加密,接收方在接收到数据后进行解密。

  3. 访问控制:通过访问控制机制确保只有授权的设备或用户能够访问串口通信。可以使用密码、加密锁等方式来实现访问控制,限制非授权设备或用户的接入。

  4. 数据的时效性:在一些对实时性要求较高的应用场景中,可以使用时间戳或序列号等机制来确保数据的时效性。接收方可以根据时间戳或序列号来判断是否接收到最新的数据,并进行相应处理。

  5. 物理安全性:在物理层面上采取措施来保障串口通信的安全性,例如使用加密传输线路、采取防护措施以避免串口的物理接触受到破坏或篡改。

需要根据具体的应用场景和安全需求选择适当的安全措施来保障串口通信的安全性。以上只是一些常见的安全保障措施,具体的实现可以根据实际情况进行定制。

10. 简述同步通信和异步通信的区别

  同步通信:通信双发根据同步信号进行通信(IIC\SPI)
  异步通信:双方都有各自的独立的时钟,约定好通信速度进行通信。(UART)
  通信速度:单位时间内发送或接收的数据位数

同步通信:

时间同步:同步通信是基于时钟同步的方式进行数据传输。

传输速率稳定:在同步通信中,发送方和接收方的时钟频率需要相同,且传输速率(数据传输速度)是固定的,

需要专用的时钟信号线:同步通信还需要额外的时钟信号线,用于保持发送方和接收方的时钟同步,以控制数据的传输。

异步通信:

时间异步:异步通信不依赖于时钟同步,数据的传输不需要时钟信号进行同步。

传输速率灵活:在异步通信中,数据的传输速率可以是可变的,发送方和接收方可以以不同的速率进行数据传输。

无需额外的时钟信号线:异步通信不需要额外的时钟信号线。

11. 谈谈你对ARM的理解

1-ARM是一家公司,ARM公司是一家芯片知识产权(IP)供应商,它与一般的半导体公司最大的不同就是不制造芯片且不向终端用户出售芯片,而是通过转让设计方案,由合作伙伴生产出各具特色的芯片。

 2 - ARM处理器,ARM处理器是英国Acorn有限公司设计的低功耗低成本的第一款RISC微处理器。

            经典处理器 ARM7\ARM9\ARM11,后续处理器开始以cortex命名

            Cortex-A 高性能

            Cortex-R 汽车电子 

            Cortex-M 低成本、低功耗

3 - ARM代表一种技术。具有性能高、成本低和能耗省的特点。在智能机、平板电脑、嵌入控制、多媒体数字等处理器领域拥有主导地位。
 

12. ARM指令集和RISC之间的关系?RISC-V和RISC之间的关系?

ARM指令集和RISC之间的关系

ARM指令集与RISC(Reduced Instruction Set Computer,精简指令集计算机)之间有着密切的关系。事实上,ARM是一种基于RISC原则设计的指令集架构。

RISC是一种计算机体系结构设计原则,旨在通过使用较少且简单的指令来提高指令执行的效率和性能。RISC的设计原则包括指令集精简、采用固定长度的指令、使用寄存器操作等。RISC架构的优点包括执行速度快、指令编码简单、资源利用高等。

ARM(Advanced RISC Machines)是一家英国半导体公司,它开发了自己的处理器架构和指令集,也称为ARM架构或ARM指令集。ARM架构是基于RISC原则设计的,旨在提供低功耗和高性能的解决方案,广泛应用于移动设备、嵌入式系统和其他领域。

ARM指令集特点如下:

  1. 精简指令集:ARM指令集采用了精简的指令集设计,使得指令编码简单且易于解码,有助于提高指令执行的效率和性能。
  2. 可扩展性:ARM指令集具有可扩展性,支持不同的指令集扩展和指令集特性,以满足不同应用的需求。
  3. 接口标准化:ARM指令集和架构的接口是标准化的,这意味着可以通过使用ARM架构的不同处理器和核心,实现对不同系统进行复用和兼容性。

总结起来,ARM指令集是基于RISC原则设计的一种指令集架构。它的设计目标是通过精简的指令集和高性能的处理器核心实现低功耗、高效率的计算。ARM架构在移动设备、嵌入式系统等领域得到了广泛应用,并成为当前最流行的指令集架构之一。

RISC-V和RISC之间的关系

RISC-V(RISC-Five)是一种开放指令集架构(ISA),它基于RISC(Reduced Instruction Set Computer,精简指令集计算机)原则。RISC-V的设计目标是提供一个免费、开放和可扩展的指令集架构,适用于广泛的应用领域。

RISC-V与传统的商业指令集架构(如ARM、x86等)以及其他RISC架构(如MIPS、PowerPC等)相比有一定的区别:

  1. 开放性:RISC-V是开放的指令集架构,其设计和规范是公开可用的,任何人都可以免费使用和实现RISC-V架构。这使得RISC-V成为一个开放的生态系统,吸引了大量的研究、创新和社区参与。

  2. 可扩展性:RISC-V的设计非常灵活,支持可扩展的指令集。它提供了基本的核心指令集(RV32I、RV64I等),并以模块化的方式支持标准扩展指令集(如乘法、浮点运算、向量处理等),以及自定义指令集扩展。

  3. 简洁性:与某些商业指令集架构相比,RISC-V采用了相对简洁的指令编码,使得指令解码和执行效率得到提高。

  4. 应用范围:RISC-V旨在满足广泛的应用需求,从嵌入式系统和物联网设备到高性能计算和云计算等领域。

总之,RISC-V是基于RISC原则设计的一种开放指令集架构。它提供了自由、灵活和可扩展的设计,成为了一个受到广泛关注的开源生态系统,并在学术界、工业界和社区中获得了越来越多的应用和支持。需要注意的是,尽管RISC-V和传统RISC架构有相似之处,但RISC-V是一种独立的指令集架构,与其他RISC架构(如MIPS、PowerPC等)之间并没有直接的关联。

046cc6b2e81e63dcf48dcb31ce28cca5.jpeg

13. DHT11使用的是什么协议

DHT11传感器使用的是一种基于单总线(One-Wire)协议。该协议允许通过一个单一的数据线来进行通信,包括发送控制命令和接收数据。基于单总线协议的设备可以通过对数据线的电平变化来进行通信,从而实现数据的传输和控制。

14. 简述DMA作用

DMA的传输方式无需CPU参与,可以直接控制传输

DMA给外部设备和内存开辟了一条直接数据传输的通道

15. SPI是同步通信还是异步通信

fc4cb1acad804cb8a5f0d6dfa03a6064.png

16. 简述单片机中断触发过程

b3f2ad34186741b89607f32611208ad3.png

进入中断 

处理器自动保存现场到堆栈里

{PC, xPSR, R0-R3, R12, LR}

一旦入栈结束,ISR(中断服务程序)便可开始执行

退出中断

中断前的现场被自动从堆栈中恢复

一旦出栈完成,继续执行被中断打断的指令

出栈的过程也可被打断,使得随时可以响应新的中断而不再进行 现场保存
 

17. 在PWM中,占空比是什么

在脉冲宽度调制(PWM)中,占空比用于描述高电平(脉冲信号上电平为逻辑高状态)在一个周期内所占的时间比例。

一般情况下,周期指的是PWM信号的完整周期,即一个高电平和一个低电平的总时间。

占空比的计算公式为:    占空比 = (高电平时间 / 周期) * 100%

占空比可以用来控制PWM信号产生的平均功率或电压。通过改变占空比,可以调节输出信号的平均电平和能量,从而实现对电机速度、LED亮度等的控制。

18. 简述几个PWM的应用场景

呼吸灯

PWM常用于电机速度控制,温度控制。

PWM还可用于音频信号的数字到模拟转换(DAC)

在无线通信中,PWM可用于产生特定频率的调制信号

19. 单片机IO框图中,保护二极管如何实现的保护功能,保护的谁?

在单片机的IO框图中,保护二极管用于保护IO引脚免受外部电压或电流的损坏。

保护二极管一般采用反向并联的结构,通常是PN结构(二极管的一种)。当外部电压或电流在正向电压范围内施加在IO引脚上时,保护二极管将开始导通,将多余的电流通过二极管导向地(或电源),以保护IO引脚。

20. ARM内核的单片机通常情况下使用的是精简指令集还是复杂指令集

精简指令集

21. 单片机I/O口有什么作用?上拉电阻与下拉电阻的作用?

输入功能:作为输入口,I/O口可以接收外部设备发送的信号。

输出功能:作为输出口,I/O口可以向外部设备发送信号。

上拉电阻:上当I/O口为输入模式时,外部没有提供足够的信号,上拉电阻会将I/O口电平拉高至Vcc。保证在没有外部信号输入时,I/O口维持高电平状态。当外部信号输入时,会引起电平变化,从而使I/O口能够检测到外部信号。

下拉电阻:下当I/O口为输入模式时,如果外部没有提供足够的信号,下拉电阻会将I/O口电平拉低至地。保证在没有外部信号输入时,I/O口维持低电平状态。当外部信号输入时,会引起电平变化,从而使I/O口能够检测到外部信号。

22. 单片机的寻址方式有哪些?

直接寻址:直接寻址是最常见的寻址方式,在程序指令中直接给出操作数的地址。

立即寻址:立即寻址是将操作数直接包含在指令中,而不是传递地址。

间接寻址:间接寻址是使用一个存储在寄存器或内存中的地址来访问数据。

寄存器寻址:寄存器寻址是将寄存器作为操作数的地址。指令中直接指定使用特定的寄存器来存储或访问数据。

间接寄存器寻址:间接寄存器寻址是使用一个寄存器中的值作为地址,然后在该地址上进行数据存取。

相对寻址:相对寻址是将操作数与当前指令指针(程序计数器)中的值进行相对偏移,从而得到最终的地址。

基址寻址:基址寻址是通过将基址寄存器与偏移值相加来计算最终的地址

23. 看门狗的原理?

看门狗的工作原理就是通过定期喂狗操作来防止看门狗定时器达到超时值。如果系统未能按时喂狗,看门狗定时器将会触发复位信号,使系统重新启动或采取其他应对措施。通过这种方式,看门狗能够监视系统的运行状态,并在出现故障或异常情况下采取自动纠正措施,提高系统的可靠性和稳定性。

24. 复位时单片机有什么动作?

  1. 初始化寄存器:复位时,单片机的寄存器通常会被初始化为默认值。这包括各种控制寄存器、状态寄存器以及通用寄存器等,以确保单片机处于预定义的初始状态。

  2. 清除中断状态:复位操作通常会清除所有中断状态。这意味着在复位后,所有中断请求被清除,并且中断向量指针回到预定义的初始值。

  3. 执行复位向量:复位向量是一个特殊的程序入口地址,指示单片机在复位时首先执行的指令。该向量可以是内部存储器中的特定地址,也可以是外部引脚上的输入信号。执行复位向量的目的是在复位后执行一些特定的初始化操作,例如设置堆栈指针、配置时钟和外设等。

  4. 关闭外设:复位时,可能会关闭某些外设或模块,以确保它们处于初始状态或低功耗状态。具体关闭哪些外设或模块取决于单片机的设计和配置。

  5. 启动时钟:复位后,需要启动适当的时钟源和时钟分频器,以确保单片机的时钟系统正常运行。时钟系统对于单片机的正常工作至关重要,因为它提供了指令执行、外设操作和时序同步等功能。

25. 中断嵌套的好处与坏处?

中断嵌套是指在一个中断处理程序中,又触发了另一个中断。

好处:

(1)更高的灵活性:中断嵌套可以允许在一个中断处理程序中处理更紧急或更高优先级的中断请求。

(2)减少延迟:

(3)确保关键任务的执行:

坏处:

(1)复杂性增加:

(2)可能引发竞态条件:如果多个中断处理程序并发地访问共享资源,可能会引发数据竞争和不确定的行为。

(3)响应时间不确定性:中断嵌套会增加中断被延迟处理的可能性。

26. 什么是可重入型函数,使用时需要注意哪些问题?

可重入性是指一个函数在同一时间可以被多个执行线程并发调用,而不会出现竞争条件或导致不可预期的结果。

使用可重入型函数时需要注意的重点是避免对共享资源的竞争访问,保证数据一致性,考虑上下文切换和选择合适的同步机制。这样可以确保函数在多线程环境下的安全可用性。

27. 单片机片内外设和外设

在单片机中,有片内外设和外部外设两种类型的设备。

(1)片内外设(On-chip Peripherals)

片内外设是指嵌入在单片机芯片内部,与核心处理器(CPU)集成在一起的功能模块。这些模块通常与单片机的核心处理器通过总线或专用接口进行连接,共享片内资源。一些常见的片内外设包括:

  •  GPIO(通用输入输出口):用于接收和输出数字信号。

  • 定时器/计数器(Timer/Counter):用于生成定时、计数、脉冲宽度调制(PWM)等信号。

  • 串行通信接口(Serial Communication Interface):如UART、SPI、I2C等,用于与其他设备进行串行通信。

  • ADC/DAC(模数转换器/数模转换器):用于模拟信号的转换与处理。

  • 中断控制器(Interrupt Controller):用于处理外部中断信号,实现异步事件的处理。

  • 存储器(Memory):片内存储器,如闪存(Flash)用于存储程序代码和数据。

  • 时钟模块(Clock Module):用于产生系统时钟信号。 

(2)外部外设(External Peripherals)

外部外设是指通过单片机芯片的引脚或专用接口与单片机连接的外部设备或模块。这些外设通常是单独的硬件组件,通过与单片机的引脚进行连接来实现与单片机的通信和交互。外部外设的选择取决于应用需求,可以是传感器、执行器、通信模块、显示器、存储设备等等。

  • 传感器(Sensors):如温度传感器、压力传感器、光敏传感器等,用于获取环境或物理量的数据。

  • 执行器(Actuators):如电机、继电器、LED等,用于控制物理动作或输出信号。
  • 通信模块(Communication Modules):如以太网模块、Wi-Fi模块、无线射频模块等,用于实现与网络或其他设备的通信。
  • 显示器(Display):如液晶显示器(LCD)、数码管等,用于显示文本、图形、数值等信息。
  • 存储设备(Storage Devices):如SD卡、EEPROM、外部闪存等,用于扩展单片机的存储容量。

28.MOS管和三极管的区别

1. MOS管是电压控制的元件,而三级管是电流控制的元件。

2. ​ 三极管比MOS管的功耗大。

3.  MOS管常用于功率开关和大电流开关电路,三极管常用于数字电路的开关控制。

29. IIC主机和从机的区别

  1. 控制能力:主机可以控制通信的启动、停止、时序和速度,而从机没有这些控制能力,它只能被动响应主机的命令。

  2. 地址选择:主机通过发送地址来选择要与之通信的从机,而从机被动等待主机的命令,并根据接收到的地址判断是否是自己需要响应的通信。

  3. 通信顺序:主机与从机是一对多的通信关系,主机可以与多个从机进行通信,但每个时刻只能与一个从机进行通信。从机只能在被选择时才能进行通信。

30. 常见的嵌入式文件系统

嵌入式系统中比较常用的文件系统有:cramfs、JFFS2、NFS、initrd、yaffs2、Ext4、squashfs

linux底层部分:

1. 设计或开发 Linux 操作系统的内核时,你会考虑哪些基本的管理模块,并描述其作用。

进程管理:进程的创建,销毁,调度等功能
注:可中断,不可中断,就是是否被信号打断。从运行状态怎样改到可中断等待态,和不可中断等待态操作系统开始会对每个进程分配一个时间片,当进程里面写了sleep函数,进程由运行到休眠态,但是此时CPU不可能等着。有两种方法,1:根据时间片,CPU自动跳转,2:程序里面自己写能引起CPU调度的代码就可以

文件管理:通过文件系统ext2/ext3/ext4 yaff jiffs等来组织管理文件


网络管理:通过网络协议栈(OSI,TCP)对数据进程封装和拆解过程(数据发送和接收是通过网卡驱动完成的,网卡驱动不会产生文件(在Linux系统dev下面没有相应的文件),所以不能用open等函数,而是使用的socket)。


内存管理:通过内存管理器对用户空间和内核空间内存的申请和释放


设备管理: 设备驱动的管理(驱动工程师所对应的)


字符设备驱动: (led 鼠标  键盘 lcd touchscreen(触摸屏))
1.按照字节为单位进行访问,顺序访问(有先后顺序去访问)

2.会创建设备文件,open read  write close来访问

块设备驱动  :(camera  u盘  emmc)
1.按照块(512字节)(扇区)来访问,可以顺序访问,可以无序访问

2.会创建设备文件,open read  write close来访问

网卡设备驱动:(猫)
1.按照网络数据包来收发的。
 

2.  bootloader、内核 、根文件的关系


启动顺序:bootloader->linux kernel->rootfile->app
Bootloader全名为启动引导程序,是第一段代码,它主要用来初始化处理器及外设,然后调用Linux内核。

Linux内核在完成系统的初始化之后需要挂载某个文件系统作为根文件系统(RootFilesystem),然后加载必要的内核模块,启动应用程序。

(一个嵌入式Linux系统从软件角度看可以分为四个部分:引导加载程序(Bootloader),Linux内核,文件系统,应用程序。)

3. 中断和异常的区别

内中断:同步中断(异常)是由cpu内部的电信号产生的中断,其特点为当前执行的指令结束后才转而产生中断,由于有cpu主动产生,其执行点必然是可控的。

外中断:异步中断是由cpu的外设产生的电信号引起的中断,其发生的时间点不可预期。

4. linux命令

1、改变文件属性的命令:chmod (chmod 777 /etc/squid
运行命令后,squid文件夹(目录)的权限就被修改为777(可读可写可执行))
2、查找文件中匹配字符串的命令:grep
3、查找当前目录:pwd
4、删除目录:rm -rf 目录名
5、删除文件:rm 文件名
6、创建目录(文件夹):mkdir
7、创建文件:touch
8、vi和vim 文件名也可以创建
9、打包:tar -cvzf 目录(文件夹)解压:tar -xvzf 压缩包
10、查看进程对应的端口号

5. 硬链接与软链接


链接操作实际上是给系统中已有的某个文件指定另外一个可用于访问它的名称。对于这个新的文件名,我们可以为之指定不同的访问权限,以控制对信息的共享和安全性的问题。如果链接指向目录,用户就可以利用该链接直接进入被链接的目录而不用打一大堆的路径名。而且,即使我们删除这个链接,也不会破坏原来的目录。

1)硬链接

硬链接只能引用同一文件系统中的文件。它引用的是文件在文件系统中的物理索引(也称为inode)。当您移动或删除原始文件时,硬链接不会被破坏,因为它所引用的是文件的物理数据而不是文件在文件结构中的位置。硬链接的文件不需要用户有访问原始文件的权限,也不会显示原始文件的位置,这样有助于文件的安全。如果您删除的文件有相应的硬链接,那么这个文件依然会保留,直到所有对它的引用都被删除。

2)软链接(符号链接)

软连接,其实就是新建立一个文件,这个文件就是专门用来指向别的文件的(那就和windows 下的快捷方式的那个文件有很接近的意味)。软连接产生的是一个新的文件,但这个文件的作用就是专门指向某个文件的,删了这个软连接文件,那就等于不需要这个连接,和原来的存在的实体原文件没有任何关系,但删除原来的文件,则相应的软连接不可用。
 

QT部分:

1. Qt的跨平台特性是如何体现的?

2. 上位机与下位机的关系是什么,你做过哪些相关项目?

3. 如何使用样式表更改一个进度条的外观?

4. 如何提升组件?

5. QApplication的功能是什么?

6. 信号槽连接的参数是什么?参数之间有什么关系?当多个信号连接到同一个槽的时候如何区分?

7. (随便找一个电脑软件)请问这个软件界面的布局思路是什么?

8. 定时器是做什么的,能举出一些在实际项目中的使用场景吗?

9. 如果要写一个飞机大战,请简述一些编写的思路?

10. 如果要写一个notepad++,请简述一下编写的思路?

11. 一个父对象与一个子对象互相传值,如何处理?

12. 描述一下异步刷新的原理,为什么使用?

13. 数据库预处理的意义是什么,如何操作?

14. 请简述事件机制?

简历部分

1. Modbus协议

modbusTCP协议是主从式协议,属于基于TCP协议的应用层高层协议,主要用于工业上工业设备之间的网络通信。有四类寄存器,线圈寄存器、离散输入寄存器、保持寄存器、输入寄存器,其中线圈一般表示开关型设备,保持和输入表示数值型设备,输入寄存器只能读不能写,保持寄存器可读可写。不同的寄存器要基于不同的功能码来操作,我都用过XXX功能码。1,5,15   3,6,16    2    4   

94bce66b6ae64c111ce018d435c5a6c7.png

 2. Mqtt 协议

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于客户端-服务器的消息发布/订阅的"轻量级"通讯协议,该协议构建于TCP/IP协议上,是基于TCP协议实现的一种应用层协议,同时具有TCP可靠性的特点与和UDP的分包协议特点,点对点的特点以及灵活的特点。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。

7cad5cf87474429e9263d48c1d191f73.png

3. http协议

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于Web Browser(浏览器)到Web Server(服务器)进行数据交互的传输协议。

HTTP是一个基于TCP通信协议传输来传递数据(HTML 文件, 图片文件, 查询结果等)的应用层协议。浏览器请求的本质其实是:

  • 浏览器作为TCP的客户端,按照http协议的要求,组合一个字符串,通过网络把数据发送给TCP服务器(webserver),这里要注意TCP服务器指定的端口,如果默认是80,浏览器不需要写端口,如果其它端口,需要我们在浏览器地址手动写端口。
  • TCP服务器通过recv收到浏览器请求字符串后,判断请求的类型(GET/POST)和请求内容,然后根据这两者做出回应,回应的格式需要遵循http协议的规范。
  • 浏览器收到响应后,会根据相应内容进行渲染页面。

4. wifi连接方式

我当时使用的单片机开发板自带wifi透传模组(ESP12F),本质上就是ESP8266芯片的封装。它实现了wifi和串口的透明传输:当需要通过网络发送数据时候,只需要将要发送的数据发给模块的串口,模组即可自动转换为wifi协议发送出去。同理,从网络过来的数据,模组会自动接收,并通过串口转发给单片机的主控。

5.  TCP/IP网络模型(几层,作用,协议)

TCP/IP协议实际上是一个协议族,主要分为4层,分别是网络接口和物理层,作用是屏蔽硬件差异(驱动),向上层提供统一的操作接口(以太网);网络层,作用是提供端对端的传输,可以理解为通过IP寻址机器(ip);传输层,作用是决定数据交给机器的哪个任务(进程)去处理,通过端口寻址(TCP,UDP);应用层:应用协议和应用程序的集合(FTP,HTTP)。

c342202117f21dd65d4976c076373fcf.png

6.Socket 套接字

(1)定义

套接字是一种通信机制(通信的两方的一种约定),socket屏蔽了各个协议的通信细节,提供了tcp/ip协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用tcp/ip协议的功能。这使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。我们可以用套接字中的相关函数来完成通信过程。

a339da01d8404727987a070e023bb687.png

发送方的发送数据的处理流程大致为:用户空间 -> 内核 -> 网卡 -> 网络

在用户态空间,调用发送数据接口 send/sento/wirte 等写数据包,在内核空间会根据不同的协议走不同的流程。以TCP为例,TCP是一种流协议,内核只是将数据包追加到套接字的发送队列中,真正发送数据的时刻,则是由TCP协议来控制的。TCP协议处理完成之后会交给IP协议继续处理,最后会调用网卡的发送函数,将数据包发送到网卡。

接收方的接收数据的处理流程大致为:网络 -> 网卡 -> 内核(epoll等) -> 进程(业务处理逻辑)

网卡会通过轮询或通知的方式接收数据,Linux做了优化,组合了通知和轮询的机制,简单来说,在CPU响应网卡中断时,不再仅仅是处理一个数据包就退出,而是使用轮询的方式继续尝试处理新数据包,直到没有新数据包到来,或者达到设置的一次中断最多处理的数据包个数。数据离开网卡驱动之后就进入到了协议栈,经过IP层、网络层协议的处理,就会触发IO读事件,比如epoll的reactor模型中,就会触发对应的读事件,然后回调对应的IO处理函数,数据之后会交给业务线程来处理,比如Netty的数据接收处理流程就是这样的。
 

(2)特性

套接字的特性有三个属性确定,它们是:域(domain),类型(type),和协议(protocol)。

域:指定套接字通信中使用的网络介质。最常见的套接字域是 AF_INET(IPv4)或者AF_INET6(IPV6),它是指 Internet 网络。


类型:

流套接字(SOCK_STREAM):
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP
数据报套接字(SOCK_DGRAM):
数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP(User Datagram Protocol)协议进行数据的传输。
原始套接字(SOCK_RAW):
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。


协议:IPPROTO_TCP,IPPROTO_UDP
 

(3)缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
6abd02955db14b75beceae83eac19da2.png

 6. GPIO

GPIO(general porpose intput output):通用输入输出接口的简称。可以通过软件控制其输出和输入。stm32芯片的GPIO引脚与外部设备连接起来,从而实现与外部通信,控制以及数据采集的功能。

GPIO是嵌入式系统、单片机开发过程中最常用的接口,用户可以通过编程灵活的对接口进行控制,实现对电路板上LED、数码管、按键等常用设备控制驱动,也可以作为串口的数据收发管脚,或AD的接口等复用功能使用。因此其作用和功能是非常重要的。

7. USART (通用同步异步收发器)

Universal Synchonous Asynchronous receiver transmitter 

USART(Universal Synchronous/Asynchronous Receiver Transmitter)是一种通用的串行通信接口,常用于微控制器和其他电子设备之间的数据通信。

USART 提供了同时支持同步和异步通信的能力,因此它可以在多种串行通信模式下操作。它可以用来传输数据和与外部设备进行通信,例如调试终端、传感器、无线模块等。

e6861a797ccd4845ad1a1f3fbabe9cf7.png

8. SPI

SPI接口是Motorola 首先提出的全双工三线/四线同步串行外设接口(Serial Peripheral Interface),采用主从模式(Master Slave)架构。

时钟由Master控制,在时钟移位脉冲下,数据按位传输,高位在前,低位在后(MSB first);SPI接口有2根单向数据线,为全双工通信。 

 SPI总线被广泛地使用在FLASH、ADC、LCD等设备与MCU间,要求通讯速率较高的场合。

四线制:

(1)MOSI(数据线):主器件数据输出,从器件数据输入

(2)MISO(数据线):主器件数据输入,从器件数据输出

(3)SCLK (时钟线):主器件产生时钟信号

(4)/SS(片选线):从器件使能信号,由主器件控制

9. I2C(集成电路总线)

I2C(集成电路总线),由Philips公司(2006年迁移到NXP)在1980年代初开发的一种简单、双线双向的同步串行总线,它利用一根时钟线和一根数据线在连接总线的两个器件之间进行信息的传递,为设备之间数据交换提供了一种简单高效的方法。每个连接到总线上的器件都有唯一的地址,任何器件既可以作为主机也可以作为从机,但同一时刻只允许有一个主机。

ce453d7e7e074cccb4d5df154c114da1.png

I2C 标准是一个具有冲突检测机制和仲裁机制的真正意义上的多主机总线,它能在多个主机同时请求控制总线时利用仲裁机制避免数据冲突并保护数据。作为嵌入式开发者,使用I2C总线通信的场景有很多,例如驱动FRAM、E2PROM、传感器等。

总结来说,I2C总线具有以下特点:

只需要SDA、SCL两条总线;
没有严格的波特率要求;
所有组件之间都存在简单的主/从关系,连接到总线的每个设备均可通过唯一地址进行软件寻址;
I2C是真正的多主设备总线,可提供仲裁和冲突检测;
传输速度分为四种模式:
标准模式(Standard Mode):100 Kbps
快速模式(Fast Mode):400 Kbps
高速模式(High speed mode):3.4 Mbps
超快速模式(Ultra fast mode):5 Mbps
最大主设备数:无限制;
最大从机数:理论上,1008个从节点,寻址模式的最大节点数为2的7次方或2的10次方,但有16个地址保留用于特殊用途。
I2C有16个保留I2C地址。这些地址对应于以下两种模式之一:0000 XXX或1111 XXX。下表显示了为特殊目的而保留的I2C地址。

I2C 节点地址    R/W    位功能描述
0000 000    0    广播地址
0000 000    1    起始字节
0000 001    X    CBUS 地址
0000 010    X    保留用于不同总线格式
0000 011    X    保留供未来使用
0000 1XX    X    高速模式主代码
1111 1XX    X    保留供未来使用
1111 0XX    X    10位节点地址
I2C还有两个变体,分别专注于系统和电源应用,称为系统管理总线(SMBus)和电源管理总线(PMBus)。

10. 中断

在处理器中,中断是一个过程,即CPU在正常执行程序的过程中,遇到外部/内部的紧急事件需要处理,暂时中止当前程序的执行,转而去为处理紧急的事件,待处理完毕后再返回被打断的程序处继续往下执行。

中断在计算机多任务处理,尤其是即时系统中尤为重要。比如uCOS,FreeRTOS等。

1.2 意义
中断能提高CPU的效率,同时能对突发事件做出实时处理

实现程序的并行化,实现嵌入式系统进程之间的切换

1.3 中断处理过程
进入中断 

处理器自动保存现场到堆栈里

{PC, xPSR, R0-R3, R12, LR}

一旦入栈结束,ISR(中断服务程序)便可开始执行

退出中断

中断前的现场被自动从堆栈中恢复

一旦出栈完成,继续执行被中断打断的指令

出栈的过程也可被打断,使得随时可以响应新的中断而不再进行 现场保存
 

7. ADC

Analog-to-Digital Converter模数转换器(将模拟信号转换为数字信号的转换器)

1. ADC作用
ADC是一个逐次逼近型的模数转换器,可以将连续的模拟信号(电压、温度、光照、压力....),转换成离散的数字信号

(传感器与之不同:将非电学量转换成电学量)

最直观的体现:模拟信号是连续变化的曲线,而数字量是不连续的一个个离散的点

8. DMA - 数据的搬运工


1. DMA作用
DMA的传输方式无需CPU参与,可以直接控制传输

DMA给外部设备和内存开辟了一条直接数据传输的通道

2. 目的
给CPU节省资源,使CPU的工作效率提高

3. DMA特性
    1)同一个DMA模块可以有多个优先级请求:很高 高 中等 低

    2)每个通道有3个事件标志: DMA半传输 DMA传输完成 DMA传输出错

    3)数据源 目标源 数据传输宽度对齐

    4)传输数据 字节8位 半字16位 全字32位 

    5)存储器<->存储器 外设<->存储器 外设<->外设

    6)闪存(flash) SRAM APB AHB 外设均可以作为源或者目标

    7)搬移数据的最大长度为65535字节


9. PWM

PWM是脉冲宽度调制(Pulse Width Modulation)的缩写。它是一种在电子设备中常用的调制技术,用于控制电平或信号的占空比。

脉冲宽度调制通过改变脉冲信号的脉宽来调节输出信号的平均功率或电平。一般情况下,脉冲信号是由一个周期性重复的方波信号构成,其中脉冲的宽度(高电平的持续时间)可以根据需要进行调节。

工作原理如下:当脉冲信号的脉宽增加时,脉冲信号的平均值也相应增加,从而增大了输出信号的功率或电平。相反,当脉宽减小时,输出信号的功率或电平就会减小。

PWM广泛应用于多个领域,包括电力电子、电机控制、数码产品、通信等。它的主要优势在于能够高效地调节输出信号的功率而不会产生大量的能量损耗。例如,PWM在直流电机速度调节、LED亮度控制、音频放大器等方面得到广泛应用。

通过调节脉冲信号的脉宽,PWM技术可以实现精确的控制和调节,使得电子设备可以根据需求实现不同的输出功率或电平,从而满足各种应用的要求。

  • 75
    点赞
  • 100
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 75
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 75
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码大魔王ㅤ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值