嵌入式开发工程师笔试面试指南-C++
一 .简述C++从代码到可执行二进制文件的过程⭐⭐⭐⭐⭐
C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接。
预编译:这个过程主要的处理操作如下:
(1) 将所有的#define删除,并且展开所有的宏定义
(2) 处理所有的条件预编译指令,如#if、#ifdef
(3) 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
(4) 过滤所有的注释
(5) 添加行号和文件名标识。
编译:这个过程主要的处理操作如下:
(1) 词法分析:将源代码的字符序列分割成一系列的记号。
(2) 语法分析:对记号进行语法分析,产生语法树。
(3) 语义分析:判断表达式是否有意义。
(4) 代码优化:
(5) 目标代码生成:生成汇编代码。
(6) 目标代码优化:
汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。
链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。
二 知道动态链接与静态链接吗?两者有什么区别⭐⭐⭐⭐
链接分为静态链接和动态链接。
1 静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。
2 而动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。
区别
1 静态链接是将各个模块的obj和库链接成一个完整的可执行程序;而动态链接是程序在运行的时候寻找动态库的函数符号(重定位)
2 静态链接运行快、可独立运行;动态链接运行较慢(事实上,动态库被广泛使用,这个缺点可以忽略)、不可独立运行。
3 静态链接浪费空间,存在多个副本,同一个函数的多次调用会被多次链接进可执行程序,当库和模块修改时,main也需要重编译;动态链接节省空间,相同的函数只有一份,当库和模块修改时,main不需要重编译。
三 导入C函数的关键字是什么,C++编译时和C有什么不同?⭐⭐⭐⭐
1 关键字:在C++中,导入C函数的关键字是extern,表达形式为extern “C”, extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
2 编译区别:由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
四 请你说说什么是宏?⭐⭐⭐⭐⭐
#define命令是一个宏命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。
该命令有两种格式:一种是不带参数的宏定义,另一种是带参数的宏定义。
五 为什么要少使用宏?C++有什么解决方案?⭐⭐⭐⭐⭐
由程序编译的四个过程,知道宏是在预编译阶段被展开的。在预编译阶段是不会进行语法检查、语义分析的,宏被暴力替换,正是因为如此,如果不注意细节,宏的使用很容易出现问题。比如在表达式中忘记加括号等问题。
正因为如此,在C++中为了安全性,我们就要少用宏。
不带参数的宏命令我们可以用常量const来替代,比如const int PI = 3.1415,可以起到同样的效果,而且还比宏安全,因为这条语句会在编译阶段进行语法检查。
而带参数的宏命令有点类似函数的功能,在C++中可以使用内联函数或模板来替代,内联函数与宏命令功能相似,是在调用函数的地方,用函数体直接替换。但是内联函数比宏命令安全,因为内联函数的替换发生在编译阶段,同样会进行语法检查、语义分析等,而宏命令发生在预编译阶段,属于暴力替换,并不安全。
六 请你说说内联函数,为什么使用内联函数?需要注意什么?⭐⭐⭐⭐⭐
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline。
为什么使用内联函数?
函数调用是有调用开销的,执行速度要慢很多,调用函数要先保存寄存器,返回时再恢复,复制实参等等。
如果本身函数体很简单,那么函数调用的开销将远大于函数体执行的开销。为了减少这种开销,我们才使用内联函数。
内联函数使用的条件
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的内联。
内联函数和宏函数的区别
1 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率;而内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。
2 宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率。
3 宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等。
七 关键字volatile的作用⭐⭐⭐⭐⭐
1 并行设备的硬件寄存器。存储器的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地址的数据进行假设。
2 一个中断服务程序中修改的供其他程序检测的变量。volatile提醒编译器,它后面定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序跟新的话,将出现不一致的现象。
3 多线程应用中被多个任务共享的变量。单地说就是防止编译器对代码进行优化。
八 关键字static的作用⭐⭐⭐⭐⭐
1 在函数体中,只会被初始化一次,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
2 在模块内(函数体外),一个被声明为静态变量可以在模块内所用函数访问,但不能被模块外其他函数访问。它是一个本地的全局变量(只能在当前文件使用)。
3 在模块内,一个被声明为静态的函数只可被这一模块内的其他函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用(只能被当前文件使用)。
其中对于所有的对象(不仅仅是静态对象),初始化都只有一次,而由于静态变量具有记忆功能,初始化后,一直没有被销毁,都会保存在内存区域中,所以不会再次初始化。存放在静态区的变量的生命周期一般比较长,他与整个程序同生死,同存亡,所以只需要初始化一次。
而auto变量,即自动变量,由于它存放在栈区,一旦函数调用结束,就会立即被销毁。
九 关键字const的作用⭐⭐⭐⭐⭐
1 定义变量(全局变量或者局部变量)为常量。
2 修饰函数的参数,表示在函数体内不能改变这个参数的值。
3 修饰函数的返回值。
a 如果给用const修饰返回值的类型为指针,那么函数返回值的内容是不能被修改的,而这个返回值只能赋给被const修饰的指针。
b 如果用const修饰普通的返回值,如返回int变量,由于这个返回值是一个临时变量,在函数调用结束后这个临时变量的生命周期也就结束了,因此这个返回值修饰为const是没有意义的。
4 节省空间,避免不必要的内存分配。
在啥情况下使用const关键字
1 修饰一般变量。一般常量是指简单类型的常量。
2 修饰常数值。
3 修饰常对象。常对象是指对象常量。
4 修饰指针。
const int*p; //常量指针,指向常量的指针。即p指向的内存可以变,p指向的数值内容不可变 int const*p; //同上 int*const p;//指针常量,本质是一个常量,而用指针修饰它。 即p指向的内存不可以变,但是p内存位置的数值可以变 const int* const p;//指向常量的常量指针。即p指向的内存和数值都不可变
5 修饰常引用。被const修饰的引用变量为常引用,一旦被初始化,就不能指向其他对象了。
6 修饰函数的常参数。const修饰符也可以修饰函数的传递参数
7 修饰函数的返回值。const修饰符也可以修饰函数的返回值,表明该返回值不可被改变。
8 在另一连接文件中引用const常量。
说说const和define的区别
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
const生效于编译的阶段;define生效于预处理阶段。
const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的;define定义的常量,运行时是直接的操作数,并不会存放在内存中。
const定义的常量是带类型的;define定义的常量不带类型。因此define定义的常量不利于类型检查。
十 什么是字节对齐?为什么要字节对齐?⭐⭐⭐⭐⭐
1 为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对齐”跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。
2 为什么要字节对齐?
(1)需要字节对齐的根本原因在于CPU访问数据的效率问题。
(2)一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,而在x86上就不会出现错误,只是效率下降。
(3)各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些平台每次读都是从偶地址开始
十一 内联函数和函数的区别,内联函数的作用。⭐⭐⭐⭐⭐
1 内联函数比普通函数多了关键字inline
2 内联函数避免了函数调用的开销;普通函数有调用的开销
3 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址。
内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句;普通函数没有这个要求。
内联函数的作用:内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。
十二 静态局部变量,全局变量,局部变量的特点,以及使用场景⭐⭐⭐⭐⭐
1 首先从作用域考虑:C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域。
全局变量:全局作用域,可以通过extern作用于其他非定义的源文件。
静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。
局部变量:局部作用域,比如函数的参数,函数内的局部变量等等。
静态局部变量 :局部作用域,只被初始化一次,直到程序结束。
2 从所在空间考虑:除了局部变量在栈上外,其他都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。
3 生命周期:局部变量在栈上,出了作用域就回收内存;而全局变量、静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。
4 使用场景:从它们各自特点就可以看出各自的应用场景,不再赘述。
十三 原子操作⭐⭐⭐⭐
原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
原子操作类似互斥锁,但是原子操作比锁效率更高,这是因为原子操作更加接近底层,它的实现原理是基于总线加锁和缓存加锁的方式。
十四 引用和指针的区别⭐⭐⭐⭐⭐
(1)指针是实体,占用内存空间;引用是别名,与变量共享内存空间。
(2)指针不用初始化或初始化为NULL;引用定义时必须初始化。
(3)指针中途可以修改指向;引用不可以。
(4)指针可以为NULL;引用不能为空。
(5)sizeof(指针)计算的是指针本身的大小;而sizeof(引用)计算的是它引用的对象的大小。
(6)如果返回的是动态分配的内存或对象,必须使用指针,使用引用会产生内存泄漏。
(7)指针使用时需要解引用;引用使用时不需要解引用‘*’。
(8)有二级指针;没有二级引用。
十五 多线程编程修改全局变量需要注意什么⭐⭐⭐⭐⭐
多线程编程中,变量的值在内存中可能已经被修改,而编译器优化优先从寄存器里读值,读取的并不是最新值。
解决办法:
1 全局变量加关键字volatile
2 使用原子操作,效率比锁高
3 使用互斥锁
十六 strlen和sizeof的区别⭐⭐⭐⭐⭐
1 sizeof 是运算符,strlen是函数。
2 sizeof运算符的结果类型是size_t,他在头文件的typedef为unsigned_int类型。
3 sizeof可以用类型作为参数,strlen只能用char* 做参数,而且需要以”\0“来结尾。
4 大部分编译程序的sizeof都是在编译的时候计算的;而strlen大小则是在运行期确定的。
5 当数组作为参数传给函数时,传递的是指针而不是数组,即传递的是数值的首地址。
strlen(“\0”)=? 和sizeof(“\0”)=?
strlen(“\0”)=0,
sizeof(“\0”)=2.
strlen用来计算字符串的长度(字符串是以"\0"作为结束符的),而sizeof是以字节的形式给出了操作数的存储大小,操作数的存储大小由操作数的类型决定的。
十七 ++a和a++的区别?⭐⭐⭐⭐⭐
++a的运算过程是
a=a+1; return a;
而a++的运算过程是
int temp=a; a=a+1; return temp;
后置自增运算符需要把原来的值复制到一个临时的存储空间,等运算结束后才会返回这个临时变量的值。所以前置自增运算符效率比后置自增要高。
十八 内存分配的方式⭐⭐⭐⭐⭐
1 静态存储区域分配
内存分配在程序编译之前完成,并在程序的整个运行期间都存在,例如全局变量、静态变量等。
2 栈上分配
在函数执行时,函数内的局部变量的存储单元在栈上创建,函数执行结束后这些存储单元自动释放。
3 堆上分配
堆和栈的区别
1 申请方式
栈的空间由操作系统自动分配/释放,堆上的空间手动分配/释放。
2 申请大小的限制
栈空间有限,栈是向低地址拓展的数据结构,是一块连续的内存区域。
堆有很大的自由存储区。堆是向高地址拓展的数据结构,是不连续的内存区域。
3 申请效率
栈是由系统自动分配的,速度比较快。但程序员是无法控制的。
堆是由new分配的的内存,一般速度比较慢,而且容易产生内存碎片。用起来很方便。
栈在C++中的作用
1 c++中栈是用来存储临时变量,临时变量包括函数参数和函数内部定义的临时变量。函数中调用中函数调用相关的函数返回地址。
2 多线程编程的基础是栈,栈是多线程编程的基础,每一个线程都最少有一个自己专属的栈,用来存储本线程运行时各个函数的临时变量和函数调用和函数返回值。操作系统最基本的功能是支持多线程编程,支持中断和异常处理,每一个线程都有专属的栈,中断和异常也有专属的栈,栈是操作系统多线程管理的基础。
十九 c++的内存管理介绍⭐⭐⭐⭐⭐
在c++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区及栈区。
代码段:包括制度存储区、文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量。
BSS段:存储未出啥的全局变量和静态变量(局部+全局),以及所有被初始化为的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射。
栈区:使用栈空间存储函数的返回地址、参数、局部变量、返回值。
二十 内存泄漏⭐⭐⭐⭐⭐
内存泄漏是什么?
内存泄漏是指在计算机程序运行时,内存动态分配后,由于疏忽或者错误导致内存无法被正确释放,最终导致内存被占用过多,从而程序出现错误甚至崩溃的问题。内存泄漏是一种非常严重的程序问题,需要引起足够的重视。
通常,内存泄漏是由于程序员在动态分配内存时,没有正确的释放内存导致的。如果没有进行内存释放操作,就会导致一个不断增长的内存空间,最终用完系统内存资源从而导致程序崩溃。一些常见的内存泄漏情况包括:
1 动态分配内存后,忘记释放掉。
2 对已经释放的内存空间进行读写操作。
3 循环引用:在程序中存在两个或多个对象相互引用,且没有正确地释放它们的内存。
内存泄漏对计算机程序的性能和稳定性有很大的影响。虽然内存泄漏问题可能不太容易引起程序崩溃,但长时间运行时却会导致系统的稳定性受到很大的影响。因此,在程序设计中需要仔细检查内存的分配和释放是否正确,并对内存泄漏进行相应的处理,以保证程序的稳定性和可靠性。
如何判断内存泄漏?
内存泄漏是一种程序错误,因此要找到内存泄漏并修复它并不是一件容易的事情。下面介绍一些常见的用于判断内存泄漏的方法:
1 内存泄漏检测工具:如Valgrind、Purify、Electric Fence等等。这些工具可以检测出内存泄漏和其他一些内存问题。这些工具可以在代码运行时查找内存泄漏问题,具有比较高的准确率,并且可以帮助确定出导致内存泄漏的代码位置。
2 使用编译器中的内存泄漏检测选项:GCC编译器中有一些相应的内存泄漏检测选项,可以在编译时检测出内存泄漏等问题。在使用这些选项时,需要将程序和编译器一起使用。
3 使用程序的逻辑和代码跟踪来确定内存泄漏:通过程序的逻辑和代码跟踪可以确定一些即使不能证明内存泄漏的代码问题,也可以追踪程序执行过程中出现的问题,从而可以进行进一步的分析和改进。
4 手动追踪代码:手动追踪代码并检查内存泄漏是一种较为原始的方法,可以通过添加打印语句或记录日志文件的方式来跟踪程序的执行过程,以确保代码能够正确的释放内存。
需要提醒的是,在应用程序中查找和解决内存泄漏问题是一项比较复杂的工作,需要仔细的分析每一段代码,定位代码中的潜在问题,并且通过测试验证解决方案是否有效。
二十一 指针⭐⭐⭐⭐⭐
数组指针和指针数组的区别
数组指针和指针数组是两种常用的C语言指针类型,它们的区别如下:
1 定义方式不同:数组指针是一个指向数组的指针,定义方式为:类型 (*指针变量名)[数组长度];而指针数组是一个数组,数组中每个元素都是指针,定义方式为:类型 *指针数组名[数组长度]。
2 存储的数据不同:数组指针存储的是整个数组的地址,而指针数组则存储的是多个指针变量的地址,每个指针变量又可以指向另一数据区域的地址。
3 操作方式不同:对于数组指针,通常需要使用解引用运算符和下标运算符[]来访问数组中的元素。而对于指针数组,可以通过下标运算符来访问特定的指针变量,然后使用解引用运算符来访问该指针所指向的数据。
4 分配空间方式不同:对于数组指针,必须先定义数组并为其分配内存空间,才能将其指针传递给数组指针变量。而对于指针数组,可以直接定义并初始化一个数组中的指针变量。
在实际编程中,如何选择数组指针或指针数组取决于具体问题的实现需要以及代码的简洁性等因素。如果需要动态地处理数组元素,应该使用指针数组;如果需要传递整个数组,并使函数能够更改数组元素的值,则应该使用数组指针。
函数指针和指针函数的区别
函数指针和指针函数是两个非常常见的C语言概念,它们的区别如下:
1 定义和声明方式不同:函数指针是指向函数的指针,需要先定义函数类型,再声明函数指针,定义方式为:返回类型 (*指针名)(参数列表);而指针函数是返回值为指针类型的函数,定义方式为:返回类型 *函数名(参数列表)。
2 存储的数据不同:函数指针存储的是函数的地址,可以通过它来调用该函数;而指针函数返回的是指针类型的数据,可以通过该数据来访问其他数据。
3 调用方式不同:函数指针可以通过使用解引用运算符*和函数调用运算符()来调用它所指向的函数;而指针函数只需要按照普通函数的调用方式来调用即可。
4 使用场景不同:函数指针常常用于回调函数和动态库中函数的加载等场景,可以更灵活地实现不同的函数调用;而指针函数则适用于需要返回复杂数据类型的函数,例如返回一个指向数据结构的指针。
总之,函数指针和指针函数虽然都涉及到指针的使用,但是它们的应用场景、定义方式、存储的数据和调用方式等具有较大的差异,需要根据实际编程需求选择使用。
指针和数组名的区别和联系
指针和数组名是C语言中的两个重要概念,它们在以下方面有联系和区别:
1 定义方式:数组名表示的是一个数组的名称,它是一个常量指针,即一个不可更改的地址常量,而指针是一个变量,可以被赋新值来指向内存中的不同位置。
2 数据存储:数组名是一个常量指针,表示的是该数组的首地址,指向整个数组;而指针则是一种变量类型,存储的是指向的数据的地址,指向一个单独的数据。
3 访问方式:数组名和指针都可以用于数组的访问。数组名在访问数组时,采用下标的方式(如:arr[2]),指针在访问数组时,需要先将指针指向该数组首地址后,再使用相似的下标方式去访问该数组元素(如:*(p+2))。
4 操作方式:指针可以像普通变量一样进行赋值、递增、递减等操作;而数组名是一个常量指针,只能用来表示该数组的首地址,不可以进行赋值、递增、递减等操作。
5 传递方式:将数组名作为函数参数时,传递的是该数组的首地址,而指针则可以作为函数参数来传递指向不同数据类型的数据。
二十二 野指针⭐⭐⭐⭐⭐
什么是野指针?
野指针是指一个指针变量指向的内存空间被释放或者未初始化,或者是指向了一个无效的内存地址。野指针的存在会引起一些不可预料的后果,如程序崩溃、数据损坏等。
常见的产生野指针的原因有:
1 指针变量未初始化,指向一个不确定的内存地址;
2 已经释放了的内存空间,指针变量仍然指向该地址;
3 使用野指针进行写操作,导致系统或者其他变量的内存数据被损坏;
4 跨函数、跨模块使用指针时,由于指针变量所指向的内存空间可能会被释放,导致出现野指针访问。
如何避免野指针?
为有效防止野指针的产生,可以在指针变量初始化后,使用动态内存分配函数(如 malloc()、calloc()、realloc() 等)为指针变量申请相应的内存空间,并注意释放指针在使用结束后相应的内存空间。当指针变量不再使用时,最好将指针变量置为NULL。
总之,为防止野指针的出现,应当注意指针变量的初始化、正确的内存动态分配与释放等问题。尽可能避免使用野指针,为编写高质量的程序提供保障。
二十三 智能指针⭐⭐⭐⭐⭐
什么是智能指针?
智能指针是一个C++语言的类模板,通过将指针的管理与使用分离,实现自动的内存管理,有效地避免了程序中野指针的出现。使用智能指针,可以帮助程序员降低代码的出错率、提高程序的可维护性。
智能指针的主要优点:
1 通过智能指针的自动内存管理,避免了资源的泄露;
2 智能指针能够提供指针的部分或者完全定位能力,降低了程序员的工作量;
3 智能指针提供了强大的函数库,程序员可以更加方便地操作指针;
4 智能指针提供了更高的程序效率和更好的程序性能。
智能指针的不足之处:
1 对于复杂的数据结构,智能指针的使用可能会有一定的复杂度;
2 智能指针不能够解决循环引用的问题;
3 对于不确定的指针赋值,使用智能指针可能会引起内存开销等问题。
总之,智能指针是在程序开发过程中避免野指针、提高程序可维护性的重要工具。虽然智能指针在某些情况下存在一些局限性,但它的优点使得它在程序开发中有广泛的应用。
指南指针的内存泄漏如何解决?
指针的内存泄漏是指在程序中分配的某个内存地址没有被释放的情况,如果频繁出现这种问题,会导致程序占用过多的内存资源,从而导致程序变得不稳定或崩溃。
为了避免指针的内存泄漏,可以使用智能指针来自动管理内存,这样就不需要手动释放内存了。智能指针使用引用计数来跟踪指向对象的引用次数,并在引用次数为0时自动释放对象所占用的内存空间,从而避免内存泄漏。
此外,还可以使用RAII技术(资源获取即初始化)来管理指针。RAII技术是指,在程序中使用对象管理资源,通过对象的构造和析构函数来释放相应的资源。使用RAII技术可以禁止手动管理内存,这将有助于避免指针的内存泄漏。
最后,还可以通过一些静态检查工具来检测程序中的内存泄漏问题。例如,可以使用Valgrind工具进行内存泄漏检查,或者使用C++的智能指针等功能。
总之,内存泄漏是一个非常严重的问题,它可能导致程序变得极其不稳定或崩溃。为了避免指针的内存泄漏,需要采用合适的资源管理技术,例如:智能指针、RAII技术等。同时,静态检查工具也是处理内存泄漏问题的一个有力工具。
二十四 typedef和define有什么区别 ⭐⭐⭐⭐⭐
typedef和#define是C语言中用于定义别名的两种方式,它们的主要区别如下:
1 typedef是在编译时处理的,而define是在预处理时处理的。因此,typedef可以接受类型名称作为参数,而define不能;
2 typedef只能用于定义类型,而define可以定义常量、变量、函数等;
3 typedef定义的别名会导致新的类型定义,而define定义的别名只是文本替换,不能创建新的类型;
4 使用typedef定义的别名比使用#define定义的别名更加安全和易读,因为typedef定义的别名可以避免意想不到的替换,并且可以使用更加直观的语言来描述类型。
例如,使用typedef定义一个新的类型:
typedef unsigned int uint32_t;
这个定义会将“unsigned int”类型重命名为“uint32_t”类型。现在,我们可以在程序中使用“uint32_t”类型,就像使用其他类型一样。
而使用#define定义一个常量:
#define PI 3.1415926
这个定义会将所有的“PI”替换为“3.1415926”,这是一个简单的替换规则。
总之,typedef和#define都是定义别名的方式,但typedef定义类型别名更加安全、易读,而define定义常量、变量、函数等不同类型的别名。
二十五 析构函数 ⭐⭐⭐⭐⭐
什么是析构函数?
在C++中,一个类的析构函数是用来在对象销毁时执行清除和资源回收操作的函数,它与该类的构造函数成对存在。析构函数名与类名相同,但在函数名前面加一个波浪号(~)。
当某个对象超出其作用域时(通常是函数的末尾),C++会自动调用该对象的析构函数。析构函数通常用来清理该对象的资源,例如释放动态分配的内存、关闭文件或网络连接、销毁子对象等。
如果一个类没有定义析构函数,C++会自动生成一个默认的析构函数,这个默认析构函数不做任何操作,因此并不一定能处理某些需要特定操作的资源清理。
析构函数的声明方式如下:
class ClassName { public: ClassName(); // 构造函数 ~ClassName(); // 析构函数 }
例如:
class MyClass { public: MyClass(); // 构造函数 ~MyClass(); // 析构函数 } MyClass::~MyClass() { // 执行资源清理操作... }
需要注意的是,虚析构函数是指在继承中对父类指针进行释放时需要使用的析构函数,用来确保释放子类对象时使用的是正确的析构函数。例如:
class BaseClass { public: BaseClass(); virtual ~BaseClass(); }; class DerivedClass : public BaseClass { public: DerivedClass(); virtual ~DerivedClass(); }; BaseClass *p = new DerivedClass; delete p;
上面的代码中,由于“p”指向的是DerivedClass对象,所以应该使用DerivedClass的析构函数。因此需要在基类BaseClass中设置虚析构函数以确保正确的析构函数被调用。
二十六 虚函数 ⭐⭐⭐⭐⭐
什么是虚函数?
在 C++ 中,虚函数是通过在函数声明前加上关键字“virtual”来定义的。虚函数的作用是让派生类可以根据自己的需要重写基类的函数实现。
当基类的指针或引用指向一个派生类的对象时,如果基类中的成员函数被声明为虚函数,那么该成员函数在运行时将根据实际对象类型调用相应的覆盖函数。这个过程称为“动态绑定”或“后期绑定”。
一个类只有在需要作为基类使用时才将其成员函数声明为虚函数,因为虚函数是带有一定开销的,如果没有必要,可以不使用虚函数来避免多余的开销。
虚函数的声明方法如下:
class ClassName { public: virtual ReturnType FunctionName(ParameterList); }; 例如: class Shape { public: virtual float getArea() { return 0; } }; class Circle: public Shape { public: Circle(float r) { radius = r; } virtual float getArea() { // 圆形面积的计算公式 return 3.14 * radius * radius; } private: float radius; };
在上面的例子中,getArea()函数是一个虚函数。在派生类Circle中,我们可以用与基类完全不同的方式重新实现它,但在派生类的对象中调用getArea()函数时总是会根据对象类型选择相应的函数实现。
总之,虚函数是允许派生类改写基类函数的一种特殊的函数。使用虚函数可以方便地实现多态性,使得程序更加灵活和可读。
二十七 const和define 的区别 ⭐⭐⭐⭐⭐
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
1 const生效于编译的阶段;define生效于预处理阶段。
2 const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的;define定义的常量,运行时是直接的操作数,并不会存放在内存中。
3 const定义的常量是带类型的;define定义的常量不带类型。因此define定义的常量不利于类型检查。
二十八 类的权限有几种? ⭐⭐⭐⭐⭐
类中成员访问属性有三种:
(1)私有成员(变量和函数)只限于类成员访问,由private限定;
(2)公有成员(变量和函数)允许类成员和类外的任何访问,由public限定;
(3)受保护成员(变量和函数)允许类成员和派生类成员访问,不允许类外的任何访问。所以protected对外封闭,对派生类开放。
二十九 对象是值传递还是引用传递? ⭐⭐⭐⭐⭐
引用传递对象
通常,使用对象作为参数的函数时,应按引用而不是按值来传递对象,这样可以有效的提高效率。
原因
因为按值传递的时候,将会涉及到调用拷贝构造函数生成临时的拷贝,然后又调用析构函数,这在大型的对象上要比传递引用花费的时间多的多。当我们不修改对象的时候,应当将参数声明为const引用。
三十 拷贝构造函数的参数类型为什么必须是引用? ⭐⭐⭐⭐⭐
如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。否则无法完成拷贝,而且栈也会满。
三十一 深拷贝和浅拷贝的区别? ⭐⭐⭐⭐⭐
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
三十一 C++中 struct 和 class 的区别? ⭐⭐⭐⭐⭐
在C++中,struct和class都可以用来定义自定义数据类型。它们的基本功能是完全一样的,唯一的区别是默认的成员访问权限和继承方式不同。
struct 中声明的成员默认是公共的(public),而 class 中声明的成员默认是私有的(private)。这就是 struct 和 class 的主要区别。因此,在定义类的时候,如果只需要公共的成员,可以使用 struct;如果需要对成员进行更精细的控制,可以使用 class。
此外,struct 与 class 在继承方式上也有所不同。使用 struct 进行继承时,默认的继承方式是 public 继承,而使用 class 进行继承时默认的继承方式是 private 继承。因此,与继承相关的某些概念,如派生类中的成员访问控制和继承链中访问控制的继承,也不完全相同。
另外还有一些其他的区别,例如创建结构时不需要使用 new 关键字分配内存,而在创建类对象时需要使用 new。但在实际使用过程中,这些区别并不会对程序的功能产生重大影响,只是在一些细节上的区别。
总的来说,struct 和 class 的区别并不是很大,主要是在默认的成员访问权限和继承方式上有所不同。除此之外,大部分情况下建议根据需要使用恰当的关键字 struct 或 class 来定义类或模板类型。
三十二 C++结构体和C结构体的区别? ⭐⭐⭐⭐⭐
(1)C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数。
(2)C的结构体对内部成员变量的访问权限只能是public,而C++允许public,protected,private三种。
(3)C语言的结构体是不可以继承的,C++的结构体是可以从其他的结构体或者类继承过来的。
(4)C 中使用结构体需要加上 struct 关键字,或者对结构体使用 typedef 取别名,而 C++ 中可以省略 struct 关键字直接使用。
三十三 存储类型? ⭐⭐⭐⭐⭐
在C++中,存储类型(Storage class)是指用于限定变量和函数对象的可见性、生命周期和存储方式等特点的关键字和修饰符。常见的存储类型包括以下几种:
自动存储类型(auto):表示变量具有自动存储期,在其所在函数或语句块中被创建,在函数或语句块执行结束后自动销毁。
静态存储类型(static):表示变量的生命周期与程序的运行时间相同,在程序启动时被创建,在程序结束后销毁。静态存储类型还可以被用于限定变量或函数的作用域和可见性。
寄存器存储类型(register):表示变量存放在寄存器中,以提高访问速度。由于寄存器数量有限,编译器不一定能够将所有变量都存放在寄存器中,因此register修饰符并不能保证变量一定会存入寄存器中。
外部存储类型(extern):表示变量或函数对象存储在其他文件中,在本文件中只是声明,在链接时才会被实际定义和分配空间。
动态存储类型(dynamic):表示变量的存储空间动态分配,并在适当的时候释放。动态存储类型通常与new和delete运算符一起使用。
以上是常见的存储类型,不同的存储类型适用于不同的场景,可以帮助我们实现更高效、更灵活的编程。
三十四 new、delete、malloc、free关系⭐⭐⭐⭐⭐
功能相似:new和malloc都用于在堆上动态分配内存,delete和free都用于释放动态分配的内存,以防止内存泄漏。
用法差异:new是 C++ 操作符,malloc是 C 语言标准库函数。new在分配内存时会调用构造函数进行对象初始化,malloc仅分配指定字节数的内存空间,不会进行初始化。delete会调用析构函数释放对象资源,free只是单纯释放内存。
返回值不同:new返回的是正确类型的指针,无需进行类型转换。malloc返回的是void*类型的指针,在使用时通常需要进行类型转换。
在 C++ 中,尽量使用new和delete进行内存管理,以利用 C++ 的面向对象特性和类型安全机制;在 C 语言中,则使用malloc和free来进行动态内存分配和释放。
三十五 子类析构时要调用父类的析构函数吗?⭐⭐⭐⭐
在 C++ 中,子类析构时通常要调用父类的析构函数,原因如下:
资源释放完整性:父类可能在构造函数中分配了一些资源,如动态内存、文件句柄等,在析构函数中负责释放这些资源。如果子类析构时不调用父类析构函数,那么父类中分配的资源就无法被正确释放,会导致资源泄漏,影响程序的稳定性和性能。
对象清理顺序:C++ 规定,在销毁一个派生类对象时,会先调用子类的析构函数,然后自动调用父类的析构函数,以确保对象的所有部分都能被正确清理。这是按照对象构造的相反顺序进行的,保证了对象内部状态的一致性和完整性。
虚函数机制支持:当存在虚析构函数时,通过基类指针删除派生类对象,会根据对象的实际类型调用正确的析构函数,实现多态的析构行为,确保子类和父类的资源都能被正确释放。
三十六 结构与联合有和区别?⭐⭐⭐⭐
结构(struct)和联合(union)是 C 和 C++ 等编程语言中用于组织数据的两种不同方式,它们的区别主要如下:
内存分配:结构中的每个成员都有独立的内存空间,整个结构的大小是所有成员大小之和(考虑内存对齐)。联合的所有成员共享同一块内存空间,其大小取决于最大成员的大小。
数据存储:结构可以同时存储多个成员的值,各成员相互独立。联合在同一时刻只能存储一个成员的值,新成员赋值会覆盖旧成员的值。
用途:结构常用于将不同类型但相关的数据组合在一起,如表示一个学生的信息。联合常用于需要在不同时刻存储不同类型数据的场景,如实现通用数据类型的存储和解析。
三十七 引用与指针有什么区别?⭐⭐⭐⭐
定义与性质:引用是已存在变量的别名,不占用额外内存空间,定义时必须初始化且不能再绑定到其他变量。指针是一个变量,存储的是另一个变量的地址,本身占用一定内存空间,定义时可不初始化。
使用方式:引用使用时就像原变量一样,直接访问。指针需要通过解引用操作符*来访问其所指向的变量。
安全性:引用在初始化后就保证指向一个有效的对象,不存在空引用,相对更安全。指针可以为NULL,若对空指针进行操作会导致程序错误。
三十八 全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?⭐⭐⭐
全局变量和局部变量的区别、实现方式以及操作系统和编译器的识别方式如下:
区别
作用域:全局变量作用域是整个程序,可在各个函数及代码块中访问;局部变量作用域仅在定义它的函数或代码块内,出了该范围就无法访问。
生命周期:全局变量从程序启动开始存在,到程序结束才销毁;局部变量在进入其所在函数或代码块时创建,离开时销毁。
存储位置:全局变量一般存于静态存储区,局部变量通常在栈区,若为动态分配的局部变量则在堆区。
初始化:全局变量若未初始化,会自动初始化为 0 或空值;局部变量若未初始化,其值是不确定的随机值。
实现方式
全局变量:编译器为其在静态存储区分配固定内存空间,在可执行文件中有专门存储段。程序运行时,操作系统根据可执行文件信息将其加载到对应内存区域,通过变量名访问内存地址。
局部变量:函数调用时,编译器在栈上为局部变量分配内存,其地址是相对于栈指针的偏移量,函数执行中通过栈指针和偏移量访问,函数返回时栈指针移动释放空间。
识别方式
编译器:通过语法分析,全局变量在函数外定义有文件作用域,局部变量在函数或代码块内有块作用域,编译器为它们生成不同符号表记录相关信息用于编译和链接。
操作系统:加载可执行文件时,依据文件格式信息,识别全局变量所在段和局部变量的栈区等内存区域,运行时管理进程内存空间,为变量提供正确访问环境。
#嵌入式#该专栏面向嵌入式开发工程师,包括C语言、C++,操作系统,ARM架构、RTOS、Linux基础、Linux驱动、Linux系统移植、计算机网络、数据结构与算法、5篇面试题目、HR面试常见问题汇总和嵌入式面试简历模板等18篇文章。超全的嵌入式软件工程师笔试面试题目和高频知识点总结!招聘so easy。