嵌入式面试题 - C语言总结(一)

嵌入式面试题-C语言总结(一)


1.什么是嵌入式,什么又是嵌入式操作系统,这两者有什么区别呢

  • 嵌入式,准确点来说,我认为应该是指嵌入式系统,它是一个既包括软件又包括硬件的计算系统,也就是说是一个软硬结合体

    总的来说,嵌入式就是在已有的硬件平台上移植操作系统,用于降低软硬件之间的耦合度,提高移植性,

    使软件工程师不再需要考虑硬件结构就可以参与项目,这样不仅仅可以提高产品开发效率,还提高了用户体验率

    [!NOTE]

    嵌入式与传统开发的区别

    说到嵌入式,肯定会联想到传统开发,因为嵌入式系统就是为了弥补传统开发的一些缺陷而开发的

    • 传统开发一般都是软件与硬件之间的开发,在开发同一项目的时候,如果硬件发生了变化,那么软件也会发生变化,

      这样就会导致相当多的问题,软 / 硬件开发商就会去头疼软件之间耦合度的问题

    • 但是嵌入式开发就不太一样了,相较于传统的单片机开发,加入了操作系统

      通过操作系统来实现软件与硬件之间的功能实现,正因为如此,嵌入式开发又分为了分上层开发和底层开发

      总的来说,上层开发就是实现软件与操作系统的开发,而下层开发就是实现操作系统与硬件之间的开发

  • 而嵌入式操作系统,本质上是一种用途广泛的系统软件,负责系统全部的软硬件资源分配,任务管理以及进程控制协调

    其主要作用,个人认为是为了将软件与硬件分离,降低软件与硬件的耦合度

    换句话说,嵌入式操作系统就是为了嵌入式系统而存在的

  • 这两者之间的区别,前者是软件是硬件的结合体,后者是系统软件,也可以说嵌入式操作系统是嵌入式系统的软件部分


2.常用的操作系统和嵌入式处理器有什么,什么样的操作系统可做为嵌入式操作系统?

  • 常用的操作系统:Windows系统,嵌入式的 Linux;手机系统Android、IOS、鸿蒙等,实时操作系统VxWorks、QNX等

[!NOTE]

操作系统是一种内置的程序,本质上来说是一种软件,用于协作计算机的各种硬件,以与用户进行交互

它也是计算机系统的核心,负责管理和控制计算机的硬件与软件资源,并提供用户接口,使用户能够方便地使用计算机

  • 嵌入式处理器:x86(PC) 、ARM(基本上移动设备)、mips(汽车、工业)、powerPC(网络设备)、 RISC-V(开源)

  • 可移植,但是不一定开源的操作系统是可以作为嵌入式操作系统的

    然后,嵌入式系统联网不是必须的,部分嵌入式系统的设计是孤立运行的,例如一些家电,洗衣机啊,微波炉什么的

    他们的嵌入式系统只负责设备控制和用户交互,当然也有需要联网的家电,比如智能家居和物联网设备

    [!TIP]

    只不过,对于这一小部分,我还有一点额外的理解,就是孤立运行和网络连接

    孤立运行和网络连接并不是对立的,而是并行存在的

    对于现在的环境来说网络已经是嵌入式设计必备的需求了,个人认为网络也是嵌入式设计中最核心的模块

    孤立运行指的是它能够在本地执行任务,不需要网络的支持,但是不代表它就不能使用网络

    同样的孤立运行的设备也不代表它没有应用价值,孤立运行可以让它降低功耗,节约资源,拥有更好的安全性等

    两者总的来说,是并行操作,互不排斥的

    通俗点讲,就是你玩某款游戏游戏时,既可以玩单机模式(孤立运行)的又可以玩联机模式的


3.什么语言可以代替C语言

  • 我个人认为目前是没有语言可以代替C语言的

  • 现在网络上对这个问题的讨论,争议性还是非常大的,我看过很多帖子,都在说Go语言和Rust语言可以代替C语言

    然后从什么性能对比啊,内存安全啊,并发支持啊,开发效率以及生态系统和工具支持去分析

    但是啊,很少有人去分析C语言的历史累积问题,C语言已经存在了很多年,生态系统和工具已经非常成熟了

    甚至还具有非常活跃的社区环境,我不清楚C语言的社区环境是不是最活跃的,但是我知道它一定是最活跃的那几个

    至于历史累积,总不可能让用了十几二十年C语言的企业换成Go语言之类的吧,成本太高了,也不一定可以看到更好的长期收益

    所以,C 语言依然不可替代,尤其是在 底层系统编程、嵌入式开发、高性能计算操作系统内核开发


4.编程的7个步骤

  • 哪有那么多固定的7个步骤,定死了,太死板就不好玩的
  • 但是编程的步骤也都是大同小异
  • 1.先看你接下来需要干什么(指定程序的目标)
  • 2.看看如何实现这个程序(设计程序)
  • 3.开始敲代码(编写代码)
  • 4.编译
  • 5.运行程序
  • 6.成功运行那就没有问题了,如果有bug就要调试(检测和调试程序)
  • 7.调试了之后肯定就要去修bug了,然后如果后期有其他功能还要进行相关的代码维护(维护和修改代码)

5.编译型语言(运行时语言) VS 解释语言 VS 静态语言

  • 编译型语言(运行时语言):编译出来的可执行文件是直接可以被识别执行的,不需要第三方插入的

    即:不依赖第三方环境的可执行文件,直接编译为可执行文件后可独立执行

    • 例如:C,C++
  • 解释语言:运行时解释给第三方(解释器),第三方再去做操作

    即:程序源代码在运行时由解释器解析执行

    • Python、java、shell脚本
  • 静态语言:编译时已经固定好了,不需要做太多变化

    即:编译时就确定了所有类型的语言

    • C、C++、Java、Rust、Go

    [!IMPORTANT]

    C语言是强类型还是弱类型语言 ?

    • C语言通常被认为是弱类型语言,但是也有不少人认为它是一个强类型语言

      • 强类型语言:编译或者运行时,严格检查数据类型的语言,换言之,不允许隐式类型转换

        变量和表达式中使用的数据类型必须明确且一致

        强制类型定义,例如:C++

      • 弱类型语言:弱类型语言则允许不同类型之间的隐式转换或混用

        弱类型定义语言,例如:C??????????????????????????

    • 但是呢,你说C语言是强类型吧,在变量声明、函数参数和返回值等方面,C语言要求明确的类型匹配

      你说C语言是弱类型吧,C语言支持隐式类型转换,并允许强制类型转换,甚至可以通过指针直接操作内存,绕过类型检查

      int类型和char类型的转换又是什么???

    • 我个人认为,这绝对绝对是个争议性非常大的题,对于初学者来说,这道题可能是非常简单的,毕竟不会想那么多

      也不会去质疑 强类型 和 弱类型这个概念

      但是对于老东西来说 —— 还在问,还在问,怎么还在问这个题啊,刚学的时候在问,现在还在问!!!

      这个问题绝对绝对是一个坑到无法再坑的问题????查了一堆资料,大家对这个问题要不避而不言,要不就开始吵起来了

      最后,查了一下维基百科,然后对照了一下英文维基百科和中文维基百科

      大致的意思是:在计算机编程中,有许多方式可以口语化地对编程语言进行分类,其中之一就是根据语言类型系统的特点,将其分为强类型或弱类型这两个术语,但是这两个东西并没有非常明确的技术定义(想让社区吵起来,放个这个问题就可以了吧.......),甚至有国外的编程语言专家花了很长一段时间都没有去搞明白这些东西是什么

      中文维基百科来个人多维护一下啊喂,放个中文链接在这里,希望有野生的大佬跳出来,指着我的鼻子说:萌新!看我表演

      强弱类型 - 维基百科,自由的百科全书

      翻了一下博客,有位博主在多年前用了一句非常精辟的话做出了总结:

      强类型是对编码的规范化而定制的标准,有利于程序员养成良好的编程习惯

      而弱类型则对这方面没有严格要求

      所以,去TMD强类型弱类型

      能编出好程序才是好类型!

    • 与其去思考什么强类型还是弱类型,不如多思考一下现在的bug怎么修,我想早点下班,晚上想多打会steam


6.C语言特点

  • 1.C语言的最大特点之一是能够直接访问硬件,它通过指针实现对内存硬件寄存器地址直接操作
  • 2.C语言常被认为是汇编语言的高级抽象,它在提供高层语言便利性的同时,保持了接近硬件的特性,能够进行底层操作
  • 3.C语言具有较高的运行效率,因为它是直接编译为机器码,且能够被CPU直接执行
  • 4.C语言具有较好的移植性,既能在不同硬件平台上运行,也能支持跨平台的软件开发
  • 5.在实现相同功能的情况下,C语言通常能以最少的代码量完成任务
  • 6.C语言是面向过程的,支持函数内部调用其他函数(函数套函数)和结构体嵌套结构体(结构套结构),这种设计让程序的组织和复用更为灵活

7.🍁什么是面向过程编程?什么又是面向结构编程?他们的区别是什么

  • 面对过程是具体化的,就是一步一步去分析去解决去实现这个问题(自顶向下,逐步细化

    有一道数学题,你现在要一步一步去分析,应该怎么把这一道题给解释出来

  • 面对对象是抽象化的,功能都有了,需要什么功能直接用就可以了(面向对象三大基本特征:封装,继承,多态

    我们先让李华限时回归一下,李华喜欢玩某款二次元手游,但是天天肝觉得想吐了
    于是他写了一个脚本,每天上线,只需要选择对应功能的执行就可以了
    例如,今天脚本执行的功能顺序是【日常任务 - 公会战 - 活动】
    你不需要去管是怎么执行的,你就说今天的游戏肝完了没有


  • 面向过程(POP):函数套函数, 结构套结构面向过程就是面向解决问题的过程进行编程

    • 强联系,耦合度高,关联性强,复用性差,维护性差,扩展性差

    • C语言也可以写面向对象的编程(利用函数指针),但是C中的函数不属于对象

    • C语言本身是面向过程的,但可以通过函数指针结构体等技术来模拟面向对象编程的一些特性
      通过将函数指针作为结构体的一部分,模拟对象的行为,实现某些面向对象的设计模式
      但是C中的函数本身并不属于对象,因此它并不具备完整的面向对象特性,如封装、继承和多态等


  • 面向对象(OOP)

    • 低耦合,复用性高
    • 强调对象、封装、继承和多态,注重模型化和复用
    • 面对对象主要的思想:封装,抽象
      • 抽象类:含有成员函数的类,继承和多态来支撑抽象

看过一堆人总结分析面向对象,面向过程,都是类似,毫无创意,毫无理解,照搬教科书的,新人能理解吗?它理解不了啊
人家编程小白也有理由说的,我才什么水平啊,我才学一个月啊,你这个东西都抽象成什么样子了啊,你叫我理解
你现在什么水平?我什么水平,你不管什么问题都在哇哇哇哇一堆高大上的术语解释,能解释吗?
解释不了,萌新没这个能力知道吗?过了一会你又要说萌新理解能力不行........(好吧,实在是编不下去了......)
编程最重要的其实不是面向过程,也不是什么面向结构,而是面向老板,面向¥


8.为什么嵌入式开发选择C语言(嵌入式开发中为什么选择C语言)?

嵌入式开发就是在特定硬件平台上引入操作系统后进行软件开发,从而降低软硬件的耦合度,提高移植性,产品的开发效率和用户体验

在嵌入式开发中,引入的操作系统本质是一个软件,这个软件通常是用C语言实现的,而操作系统的主要任务是将硬件接口抽象为对应的地址和指令,方便软件的调用(人话:操作系统就是把对应的硬件接口转换成对应的地址)

C语言的最大优势之一是其能够直接访问硬件。C语言可以通过指针直接操作寄存器地址,这使得C语言能在多种不同的硬件平台上运行,其次它还具备高效的运行效率和较好的移植性。因此,C语言相较于其他编程语言在嵌入式开发中具有明显的优势


9.C语言的标准语法有哪些?C语言主流开发环境有哪些?

  • C 语言的语法标准分为两派,VC派和GNU派

  • VC派包括C89、C99和C11,属于Microsoft Visual C++(VC)编译器使用的标准

  • GNU派是由Linux在1983年启动的GNU计划提出的C语言标准,基于GCC编译器。Linux的底层内核就是按照GNU C的标准实现的。GNU C扩展了C语言的语法,引入了如container_of宏、typeof关键字、扩展的case语法、内联函数、零长度数组等特性

  • C 语言的主流开发环境有VScode、VisualStudio、QT、Vi、Vim等

  • 我主要使用的是VScode环境,VScode是微软开发的一款免费的开源跨平台代码编辑器,支持多种编程语言,并拥有丰富的插件和扩展。它可以用于编辑C、C++、C#、Python等几十种语言(国人做的小说摸鱼插件和追番插件有一说一,真不错,果然中国人最懂中国人,下次去看看有没有什么小游戏插件


10.🍁sizeof、strlen区别(指针数组、数组做形参)

  • sizeof是运算符(编译时运算符),strlen是函数
  • sizeof值在编译时确定,因此无法用于获取动态分配的内存(运行时分配)的大小
    • sizeof的功能是获得数据类型所占的字节长度
    • sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化)
  • strlen用于**计算字符串的长度,但不包括字符串末尾的 '\0' **
    • strlen 的参数只能是字符指针(char*),且该字符串必须以'\0'结尾

11.🍁为什么数组做形参会被退化成对应的指针

  • 在C语言中,当数组作为函数参数传递时,会发生数组退化成指针的现象

  • 因为C语言中的函数参数是通过值传递的,而数组名表示数组的首元素的地址,当数组作为参数传递时,编译器会自动将数组退化为指向该数组首元素的指针

  • void func(int arr[], int size) 
    {
        for (int i = 0; i < size; i++) 
        {
            printf("%d\n", arr[i]);
        }
    }
    
  • 为了验证这一特性,我们可以使用 sizeof

    在函数内部使用 sizeof 获取数组大小时,只对静态数组有效,即数组的大小在编译时是已知的。如果数组作为参数传递给函数,sizeof 不再有效,因为传递给函数的是指针,指针并没有保存数组的大小信息

    这个时候sizeof获取到的值实际上是一个指针大小而不是数组的大小,我们常用的64位计算机下,一般是8个字节长度

  • 人话:将数组传递给一个函数时,传递的实际上是指向数组的第一个元素的指针,从而丢失数组长度的信息

    一开始的时候,我知道数组有这样的特性,但是,并不知道这种特性会导致什么样子的后果

    理论知识阶段,哪怕我的老师讲的再好,也get不到点上,这会给我的实际运用照成什么问题吗,我不造啊

    直到在做一个魔塔游戏项目的时,我将一个数组传递给了一个函数,但是并没有传递长度,然后试图用sizeof 获取数组大小

    然后函数的预期和实际结果必然不相符了,找了很久的bug才发现是函数传参的问题,获取的值一直是8,8是什么?指针大小吗?

    查了许久的bug,才发现我忘记了数组的这一特性,

    为了解决这个bug,我又传入一个数组的长度,这样做虽然可以规避这个特性带来的一些问题,却也让函数更容易出错

    有什么更好的办法解决这个问题呢,除了传递数组长度,还可以使用使用指针与动态内存以及可变长度数组

    在C++中,我还会使用std::vector 动态数组容器来解决这个问题,如果允许使用的C++环境版本比较高(C++20及其以上版本)

    可以使用std::span来处理这个问题(std::span 提供了一种轻量级的方式来传递数组视图,同时保留大小信息)


12.局部变量和全局变量的区别

[!IMPORTANT]

生命周期作用域的区别:

  • 生命周期是变量或对象在内存中存在的时间段

  • 作用域是变量或对象在程序中的可见性或访问范围

  • 1.作用域不同

    局部变量作用域限定为它所在的代码块或函数中,也就是常说的在函数内部声明的变量只能在该函数内部使用

    全局变量的作用域为整个源文件,也就是说可以在文件任何地方被访问

  • 2.生命周期不同

    局部变量的生命周期通常与函数或代码块的执行周期相关

    局部变量函数调用时创建,函数调用结束后销毁,局部变量的生命周期仅限于其所在的函数或代码块的执行期间

    所以我们常说每次函数调用时,局部变量都会重新分配内存

    全局变量的生命周期程序的执行周期相同

    全局变量从程序启动时创建,直到程序结束时销毁,整个程序运行期间,全局变量都存在

    全局变量在程序执行期间始终存在,并且在程序的任何地方都可以访问(在其作用域内)

  • 3.存储空间不同

    局部变量存储在栈空间,而全局变量存储在静态存储区(通常称为数据段)

  • 4.初始化不同

    未初始化的局部变量是一个垃圾值,因为局部变量通常不会被自动初始化

    如果在声明时没有给局部变量赋初值,使用它时会出现未定义行为

    例如,栈上未初始化的局部变量含有垃圾值

    未初始化的全局变量初始值是0,编译器会自动将它们初始化为零

  • 5.命名冲突

    局部变量和全局变量重名时,(在函数内)局部变量会屏蔽(隐藏)全局变量


13.C语言内存空间布局?存储空间

内核空间、栈空间、堆空间、数据区和代码段

假设有一个进程,其虚拟地址空间为 4GB,通常分为两大部分用户空间内核空间,用户空间有3GB,内核空间有1GB

内核空间(1GB):存储操作系统内核的相关数据和代码,供操作系统使用

用户空间(3GB)

  • 栈空间(Stack):存储局部变量、函数参数、函数返回地址

    遵循先进后出(先分配空间 的最后释放),由操作系统自动管理内存分配和释放

    未初始化的变量在栈上是未定义值,也就是常说,未初始化局部变量会产生垃圾值

  • 堆空间(Heap):用于动态分配内存(如通过 malloccallocrealloc

    堆空间遵循先进先出(先分配空间的优先释放),堆内存的分配由程序员控制,程序员需要手动释放内存

    未初始化的堆空间对于malloc是随机值,对于calloc是0,因为calloc 会初始化内存为零,而 malloc 分配的内存不一定被初始化

  • 数据区

    • BSS 段:存储未初始化的全局变量(和静态变量),这些变量在程序启动时自动初始化为零
    • 只读数据段(RO段):存储常量数据,如字符串字面量和常量数组
    • 静态数据区存储静态变量
  • 代码段代码段保存的是可执行文件代码,它是只读的,在运行期间是不可以进行修改的

ps:老师啊,你给的资料好老啊,啊啊啊啊啊啊啊,查了一下,全是零几年和一几年的帖子了啊喂,我整理的好累啊


14.🍁关键字(static,const,extern,typedef)

  • static

    • 修饰局部变量:修饰局部变量变为静态局部变量,延长局部变量的生命周期

      • 静态局部变量:函数结束时不会消失,每次函数调用时,不会重新分配空间,程序结束之后才会释放
    • 修饰全局变量:修饰全局变量变为静态全局变量

      • 静态全局变量:仅在当前文件访问,无法在其他文件访问
    • 修饰函数:修饰函数变为静态函数

      • 静态函数:仅在当前文件访问,无法在其他文件访问

    • 由此可总结出static的三个主要作用:

      • 1.隐藏修饰对象,一定程度上提高了封装性,避免了外部干扰
      • 2.保持变量内容的持久,因为static变量存放在静态存储区,所以具备持久性
      • 3.默认初始化为0,static变量存放在静态存储区,所以默认值为0

  • const

    • C语言中的const

      • 1.修饰常量:用于声明一个常量,表示常量的值不可修改(必须初始化)

      • 2.修饰指针:可以限制指针本身或者指针指向的内容不可修改

        • const char *s  VS 	char *(const  s)
          常量指针           	 指针常量
          前者const修饰的是  *s    ,后者const修饰的是s
          所以前者不能修改的是值    ,后者不能修改的是指针朝向
          反之前者可以修改指针朝向  ,后者可以修改数值
          
        • const修饰指针常量(修饰的是一个常量):不能修改指针本身指向的地址 ,但可以修改指针指向的内容(值)

        • const修饰常量指针(修饰的是一个指针):不能修改指针指向的内容(值),但可以修改指针本身指向的地址

      • 3.修饰函数参数:保护函数内部的参数数据,确保函数参数在函数内部不可被修改


    • C++中的const

      • 比起C语言中的const关键字,还追加了一些功能

        • // 1.const修饰成员函数
          class MyClass 
          {
          public:
              void AfterWork() const 
              {  
                  // const 成员函数,不会修改成员变量
                  cout << "已下班,勿扰" << endl;
              }
          };
          --------------------------------------------
          // 2.const修饰常量引用
          void NoOverTime(const int &work) 
          {
              // x 不能被修改,避免了不必要拷贝
          }   
          --------------------------------------------
          // 3.const修饰类成员变量
          class MyClass {
          public:
              const int My_purse = 100;  // 你钱包里面的钱(My_purse) 是常量,无法修改
          };
          
        • const修饰成员函数:C++ 中 const 修饰成员函数,表示该函数不会修改调用对象的状态(不会修改成员变量)

        • const修饰常量引用:C++ 中常常使用 const 引用来避免不必要的拷贝,保证数据不被修改

        • const修饰类成员变量const 还可以修饰类的成员变量,使其在对象生命周期内不可变


    • 总结:const 关键字用于声明常量或不可修改的对象,防止其值在程序执行过程中被改变

    • 人话:说白了就是让某个被声明的对象或者常量不能被修改,查出来的东西巴拉巴拉一堆,实际都是一个意思

      你猜猜看,教程和术语这种东西有没有想让你明白他们在讲什么


  • extern:起到外部声明的作用,表示全局变量在其他文件中或其他位置已经被定义,当前函数或者文件可以访问该全局变量
  • typedef数据类型重命名(提高移植性、可读性、编码效率)
    • 结构体类型,函数指针(你也不想让你的智能指针长达好几十个字符吧),数组类型,复杂类型等

15.typedef vs #define(宏定义)

typedef
主要用于数据类型重定义,作用于编译阶段,且typedef会检查数据类型

#define
宏定义,主要用于定义数据类型,常量(包括特点数值,比如0和1008611)和函数
作用于预处理阶段,但是宏定义不会检查数据类型

[!CAUTION]

ps:连续定义多个指针变量的时候,#define只有第一个是指针,typedef类型作用于所有定义的变量

#include <stdio.h>

#define INT int*
typedef int* TypeInt;

int main()
{
 INT a, b;		// 你以为这里的a是int*,b也是int *,实际上b是int类型
 TypeInt c, d;
}

最后再叠个甲:博主只是一个即将成为社畜的苦逼大学生,水平有限,
以上问题全是根据自己现有的理解,结合以前的笔记,然后查询大量的资料
最后自己一个人整理,总结出来的,可能会有些许错误,如果存在一些问题,还望各位可以指出
最后再祝愿看的各位可以在来年找到好工作

posted @ 2024-12-19 20:53  想不到ID暂时就这样了  阅读(323)  评论(0)    收藏  举报