2.1 c和c++区别、概念相关面试题
2.1.1 new和malloc的区别⭐⭐⭐⭐⭐
- 函数来源:new/delete是C++特有的操作符,malloc/free是c/c++标准库函数。new在创建对象分配内存时会自动调用构造函数,同时完成对象的初始化,同理delete也可以自动调用析构函数。而malloc/free单纯为对象分配和分配内存。
- 操作返回:new返回指定类型的内存指针,并自动计算所申请内存的大小。而malloc返回的是void*类型,我们需要强行将其转换为实际类型的指针,并且需要指定好要申请内存的大小,malloc不会自动计算的。 new内存分配失败时,会抛出bad_alloc异常。malloc分配内存失败时返回NULL。
- 重载特性:C++允许重载new/delete操作符,而malloc和free是一个函数,并不能重载。
- 内存分配:先了解自由存储区和堆,两者不相等于的。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配。new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
2.1.2 malloc的底层实现⭐⭐⭐⭐
嵌入式系统裸机环境:使用 malloc(),没有专门的申请和释放,很容易产生碎片,再申请一大片连续的大内存时就会失败。
- 一般不建议使用堆内存,遇到使用大块内存的地方,可以使用一个全局数组代替。
- 实现自己的堆内存管理,如采用内存池。将堆内存空间划分为固定大小的内存块,自己管理与维护内存的申请和释放来避免内存碎片的产生。为了节省内存资源,甚至可以将堆内存划分成不同大小的内存块,根据用户申请内存的大小选择合适的内存块,进一步提高内存利用率。
Linux环境下堆内存管理:
malloc() / free() 函数的底层实现,是通过系统调用brk()向内核内存管理系统申请内存,内核批准后,就会在BSS段的后面留出一片内存空间,允许用户进行读写操作。申请的内存使用完毕后要通过free()函数释放,free()函数的底层实现也是通过系统调用来归还这块内存的。

内存分配器通过系统调用 brk()/mmap() 向 Linux 内存管理子系统“批发”内存,同时实现了malloc()/free() 等API函数给用户使用,满足用户动态内存的申请与释放请求。

malloc() 函数是内存分配器调用底层 brk() 和 mmap() 向 Linux 内存管理子系统“批发”内存,方便用户进行申请和释放内存。采用双链表结构链接不同大小的内存块 chunk,并根据内存块大小不同,将这些chunk分类为 unsorted bin, small 和 large bins。
- 申请内存时,根据内存大小依次在unsorted bins、fast bins 、small bins、large bins中查找。若未正好的匹配的内存,会将一些大的内存块将会被分割成两部分:一部分返回给用户使用,剩余部分则放到unsortedbin中。
- 释放内存时,先存放在unsorted bins,对于物理内存上相邻的内存,会在合适的时机(存在零散的内存,分配的空间没有合适大小),根据内存大小迁移到对应的bins。
2.1.3 在1G内存的计算机中能否malloc(1.2G)?为什么?⭐⭐
答:是有可能申请1.2G的内存的。
解析:回答这个问题前需要知道malloc的作用和原理,应用程序通过malloc函数可以向程序的虚拟空间申请一块虚拟地址空间,与物理内存没有直接关系,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。
我们要申请空间的大小为
1.2G=(1024*1024*1024)*1.2 Byte ,转换为十六进制约为 4CCC CCCC ,这个数值还在 unsigned int 的表示范围。 malloc 函数要求的参数正是unsigned int。在当前正在使用的Windows环境中,可申请的最大空间超过1.9G。实际上,具体的数值会受到操作系统版本、程序本身的大小、用到的动态/共享库数量、大小、程序栈数量、大小等的影响,甚至每次运行的结果都可能存在差异,因为有些操作系统使用了一种叫做随机地址分布的技术,使得进程的堆空间变小。感兴趣的读者可以去研究操作系统中的相关内容。综上,是有可能通过malloc( size_t ) 函数调用申请超过该机器物理内存大小的内存块的。
2.1.4 指针与引用的相同和区别;如何相互转换?⭐⭐⭐⭐⭐
c++中,引用和指针的区别是什么? - 编程指北的回答 - 知乎 https://www.zhihu.com/question/37608201/answer/1601079930
本质(还是指针)→ 差别→ 指针的指针 → 指针运算(自加)→ const修饰
- 引用只是c++语法糖,可以看作编译器自动完成取地址、解引用的指针常量。引用区别于指针的特性都是编译器约束完成的,一旦编译成汇编就和指针一样
- 引用无需解引用,而指针需要解引用
- 引用只能在定义时初始化一次,之后不可变,指针可变
- 引用由编译器保证初始化,使用起来较为方便(如不用检查空指针等)
- 引用时内存地址的一个别名,它是依赖于变量的存在,因此引用不能为空,而指针可以为空。
由于引用只是指针包装了下,所以也存在风险,比如如下代码:
- 有指针引用--是引用,绑定到指针, 但是没有引用指针--这很显然,因为很多时候指针存在的意义就是间接改变对象的值,但是引用本身的值我们上面说过了是所引用对象的地址,但是引用不能更改所引用的对象,也就当然不能有引用指针了。
- 指针和引用的自增(++)和自减含义不同,指针是指针运算, 而引用是代表所指向的对象对象执行++或 --
- “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小,在32位系统指针变量一般占用4字节内存。
- Const 的修饰:引用本身不可变,没有
int & const,加顶层const也没有意义; 但有底层const即const int&,这表示引用所引用的对象本身是常量。指针既有顶层const(int * const-指针本身不可变),也有底层const(int const *-指针所指向的对象不可变)
指针和引用之间怎么转换:
1)指针转引用:把指针用*就可以转换成对象,可以用在引用参数当中。
2)引用转指针:把引用类型的对象用&取地址就获得指针了。
此时调用: fun(*pA);
pA是指针,加个*号后可以转换成该指针指向的对象,此时fun的形参是一个引用值,pA指针指向的对象会转换成引用va。
2.1.5 C语言检索内存情况内存分配的方式⭐⭐⭐
1、从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
2、在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3、从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
2.1.8 函数参数压栈顺序,即关于stdcall和cdecl调用方式的理解⭐⭐⭐
1 stdcall 和 cdecl 区别
1.1 stdcall
__stdcall 是 StandardCall 的缩写,是C++的标准调用方式。stdcall 调用方式又被称为 Pascal 调用方式。在Microsoft C++系列的C/C++编译器中,使用 PASCAL 宏,WINAPI 宏和 CALLBACK 宏来指定函数的调用方式为 stdcall。
其声明语法为:
stdcall 的调用方式意味着:
(1)参数从右向左依次压入堆栈。
(2)由被调用函数自己来恢复堆栈,称为自动清栈。
(3)函数名自动加前导下划线,后面紧跟着一个@,其后紧跟着参数的大小。
1.2 cdecl
__cdecl 是C Declaration的缩写(declaration,声明),cdecl调用方式又称为C调用方式,是C语言缺省的调用方式。
其声明语法为:
cdecl 的调用方式意味着:
(1) 参数从右向左依次压入堆栈。
(2) 由调用者恢复堆栈,称为手动清栈。
(3) 函数名自动加前导下划线。
__stdcall还是 __cdecl 函数参数都是从可向左入栈的,并且由调用者完成入栈操作。对于 __stdcall 方式被调用者自身在函数返回前清空堆栈;__cdecl 则由调用者维护内存堆栈,所以调用者函数生成的汇编代码比前一种方式长。由__cdecl 约定的函数只能被C/C++调用。2 windows 和 linux区别
2.1. windows 上
不管 C 还是 C++,默认使用的都是__stdcall方式。
2.2. Linux上
Linux上使用
__stdcall 和 __cdecl 的方式比较麻烦一些。__stdcall 和 __cdecl 没有区别,有区别的是编程语言。2.1.9 重写memcpy()函数需要注意哪些问题⭐⭐
内存对齐
在重写memcpy函数时,我们需要确保处理内存对齐的问题。一些嵌入式平台对于特定类型的数据可能有内存对齐的要求,如果不满足这些要求,可能会导致内存访问异常或者性能下降。因此,重写的memcpy函数需要处理好内存对齐的情况,以确保在不同平台上都能正确工作。
指针溢出
在memcpy函数中,我们通常会使用指针进行内存的复制操作。在重写memcpy函数时,需要确保对源地址和目的地址进行指针溢出的检查,以避免发生内存访问越界的情况。这可以通过对源地址和目的地址进行范围检查来实现。
内存重叠
在标准的memcpy函数中,源地址和目的地址可以重叠,但是在重写memcpy函数时,需要考虑到内存重叠的情况。如果源地址和目的地址发生重叠,需要确保复制操作能够正确进行,而不会导致数据的损坏或丢失。
性能优化
重写memcpy函数的一个常见目的是为了优化性能。在进行性能优化时,需要考虑到不同平台的特性和限制,以确保优化后的memcpy函数在各种情况下都能够提供更好的性能表现。
可移植性
重写memcpy函数时,需要考虑到函数的可移植性。这意味着函数需要在不同的编译器和平台上都能够正确编译和运行。因此,需要遵循标准的C/C++语法和规范,以确保函数能够在不同环境下都能够正确工作。
测试和验证
最后,重写memcpy函数后,需要进行充分的测试和验证。这包括对不同情况下的内存复制操作进行测试,以确保函数能够正确处理各种情况下的内存复制需求。
2.1.10 数组到底存放在哪里⭐⭐⭐
- 栈上:当数组在函数内部声明时,数组通常存储在栈上。栈是一种后进先出(LIFO)的内存分配方式,用于存储函数的局部变量和临时数据。当函数调用结束时,栈上的局部变量会被自动销毁。
- 堆上:如果使用动态内存分配方式(例如
new运算符)来创建数组,数组将存储在堆上。堆是一种由程序员手动分配和释放内存的内存池,用于存储动态分配的数据。需要注意的是,堆上的内存必须在不再需要时手动释放,否则可能导致内存泄漏。
- 全局/静态存储区:如果数组在全局作用域或使用
static关键字声明,数组将存储在全局/静态存储区。全局/静态存储区用于存储全局变量和静态变量,它们在程序启动时分配,在程序结束时释放。
- 常量区:如果数组是一个常量数组(例如字符串常量),则存储在常量区。常量区是存储常量数据的区域,通常存储在只读内存中,以防止意外修改
2.1.11 struct 和 class 的区别 ⭐⭐⭐⭐⭐
总结:C++ 中存在 struct 的唯一意义就是为了让 C 语言程序员有归属感,是为了让 C++ 编译器兼容以前用C语言开发的项目。
答:两者最大区别是struct 里面默认的访问控制是 public,而class中的默认访问控制是private。
2.1.12 char和 int 之间的转换;⭐⭐⭐
1)首先 char 与 int 都分为 signed 与 unsigned 类型,默认情况下都是 signed 类型。
2)从长字节数据类型转换为短字节数据类型,会产生截断:
如从 4 字节的int类型转换成 1 个字节的char类型,则取int数据的最低的一个字节,将这个字节的数据赋给char型数据,且是有符号的,即首位为符号位;而如果是从int转换成unsigned char类型,则整个一个字节都是数据,没有符号位。
3)从短字节类型转换为长字节类型,从char转换为int:则在前面的三个字节补符号位,即补上
0xffffff(char的首位为1),或0x000000(char的首位为0)。从 unsigned char 转换为 int,则前面补上0x000000 。2.1.13 static 和extern 的用法(定义和用途)⭐⭐⭐⭐⭐
static
在C语言中,static作用:“改变生命周期” 或者 “改变作用域”。有以下特性:
1)static 局部变量:局部变量为动态存储,即指令执行到定义处才分配内存,将一个变量声明为函数的局部变量,使其变为静态存储方式(静态数据区),那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中。
2)static 全局变量:全局变量即定义{}外面,其本身就是静态变量,编译时就分配内存,这只会改变其连接方式,使其只在本文件内部有效,而其他文件不可连接或引用该变量。
3)static 函数:对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的。这样的函数又叫作
静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。到了C++的时候,static多了几个其他的作用:
4)static 类成员变量:表示这个成员为全类所共有,对类的所有对象只有一份拷贝,可以借助类名直接访问。
5)static 类成员函数:表示这个函数为全类所共有,而且只能访问静态成员变量,因为这个函数不接收 this 指针。
extern
在C语言中,修饰符extern用在变量或者函数的声明前,用来说明此函数/变量是在别处定义,但是要再此处引用。注意extern不能用来修饰局部变量。具体应用示例:
1)头文件声明时加,extern 定义时不要加,因为 extern 可以多次声明,但只有一个定义。
2)extern 在链接阶段起作用(四大阶段:预处理--编译--汇编--链接)。
test.h如下:
test.c如下:
主函数所在的源文件为main.c,其代码如下:
其中,a,b的声明也可放在main.c中,代码如下:
此时,就不需要包含test.h这个头文件了。
2.1.14 const 的用法(定义和用途)⭐⭐⭐⭐⭐
- 修饰函数的入口参数:防止传入的参数代表的内容在函数体内被改变,但仅对指针和引用有意义。因为如果是按值传递,传给参数的仅仅是实参的副本,即使在函数体内改变了形参,实参也不会得到影响。
- 修饰变量,数组:被修饰的变量和数组只具有只读属性,不可被更改(也就是变成了常量数组)。例子
- 修饰指针:const修饰指针的时候有三种情况:
第一种情况const用来修饰*p,(如下代码),const和int可以互换位置。此时const修饰的是*p,*p代表指针变量所指向内存单元的值,即*p的值不可被更改,但p所代表的地址可以被更改。
第二种情况const用来修饰p,此时p所存放的内存单元地址不可变,但所指向内存单元的内容可变。
第三种情况是*p和p均被const修饰,则p存放的内存单元的地址和*p存放的内存单元的内容均不可变。
2.1.15 const 常量和 #define的区别(编译阶段、安全性、内存占用等) ⭐⭐⭐⭐
编译阶段和安全性:
- 宏定义定义的常量
没有类型安全检查,只是字符串替换,在预编译阶段进行替换- 程序中用到常量的地方都要进行拷贝替换
- const 定义常量
编译时会进行类型检查,存放在静态区域,编译时赋值。
作用域:
- 宏定义的作用范围仅限于当前文件。 (区分为头文件和源文件,源文件直接和包含头文件中宏定义即可生效,而源文件中含有的宏定义只在该源文件中生效)
- const对象只在文件内有效,当多个文件中出现了同名的const变量时,等同于在不同文件中分别定义了独立的变量。 如果想在多个文件之间共享 const 对象,必须在变量定义之前添加extern关键字(在声明和定义时都要加)。
存储和内存占用:
- # define 所定义的宏变量却有多个拷贝,所以宏定义在程序运行过程中所消耗的 内存 要比 const 变量的大得多;
指针作用:
用 define 定义的常量是不可以用指针变量去指向的,用const定义的常量是可以用指针去指向该常量的地址的。
定义后可否取消定义
- 宏定义可以通过 #undef 来使之前的宏定义失效
- const 常量定义后将在定义域内永久有效

2.1.16 volatile 作用和用法 ⭐⭐⭐⭐⭐
编译器优化,会将内存数据存入寄存器,并从寄存器取值,减少从内存存取,以提高效率(非原子操作,先读入寄存器,再操作变量内存,导致数据没有及时更新)。加上volatile,编译器对访问该变量的代码就不再进行优化,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
volatile关键词的作用是影响编译器编译的结果,用 volatile 声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错。
案例:我们来看看以下几个使用volatile的案例:
(1)中断服务程序中状态位或者状态变量
程序的本意是希望ISR_2中断产生时,在main函数中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过 i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致 dosomething 永远也不会被调用。如果将变量加上volatile 修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中 i 也应该是volatile int i;
(2)并行编程中的共享变量
(3)访问硬件寄存器(外部事件修改)
例如:假设要对一个设备进行初始化,此设备的某一个寄存器为 0xff800000。
经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为9,所以编译器最后给你编译编译的代码结果相当于:
如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化过程并不能达到目的。反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作只做了一次。然而从代码角度看是没有任何问题的。这时候就该使用volatile通知编译器这个变量是一个不稳定的(
volatile int *output=(volatile unsigned int *)0xff800000;//定义一个I/O端口),在遇到此变量时候不要优化。Elab中常用示例
全局变量,多个任务和中断都会访问和共享
中断处理程序中变量 pxTopOfStack会在Pensv 和SVC中断中被修改。
用于多任务同步标志,用来做判断
硬件寄存器
2.1.17 有常量指针 指针常量 常量引用 没有 引用常量⭐⭐⭐
(1)常量指针:也叫常指针,最后两个字是“指针”,代表这是一个指针,但指向的是一个常量,如下:
int a = 0;const int *p = &a; //不可以通过p改变a(2)指针常量:后面两个字是“常量”,代表这是个常量,不过是指针类型的常量,
int a = 0;int *const p = &a; //从后往前看,这是个指针常量,指向的 a 的值可以改变,但 p 本身不可改变注意:如果从代码来区分常量指针指针常量,那么可以从后往前看const的位置,
const int *p = &a //从后往前看,const修饰的是*p,所以指针p指向的数值不可变int *const p = &a; //从后往前看,const修饰的是p,所以指针p本身不可变(3)常量引用:后两个字是“引用”,那么这个是引用,并且是常量的引用,那么就有两个性质,如下:
double a;const int &r = a; //正确 性质1:不可通过常量引用r来改变aconst int &r = 10;//正确 性质2:常量引用可以直接引用具体数值(4)没有引用常量:后面两个字代表这个是常量,前面代表这个是引用类型的常量,然而常量就是常量了,并没有引用类型的常量。
2.1.18 没有指向引用的指针,因为引用是没有地址的,但是有指针的引用⭐⭐⭐
解析:如何理解这句话呢,首先,没有指向引用的指针,因为指针是本质上是指向某一块内存空间的,而引用只是一个变量的别名,本身是没有地址的,如果要创建一个指针指向某个引用,那么其实指向的是这个引用所引用的对象,看下面代码:
int v = 1;int &ri = v; //整型变量v的引用int *p = &ri; //指针p其实指向的是变量v其次,有指针的引用,我们直接看代码:
int v = 1;int *p = &v;int *&rp = p;第一个是要理解int *&rp = p; 这是定义了一个变量rp,还是从后往前看,距离rp左边最近的修饰符决定rp是个什么东东,剩下的就是rp的具体值。因此我们发现距离rp左边最近的是&,代表rp是个引用,所以int *&rp = p; 可以看作int *(&rp )= p; 如果我们把(&rp)当作一个整体,又可以看作int * RP = p;到此为止,我们就可以很明显的知道这句话其实就是定义了一个引用rp指向指针p。
2.1.19 c/c++中变量的作用域⭐⭐⭐⭐⭐
(1) 全局变量:
全局变量是在所有函数体的外部定义的,程序的所在部分(甚至其它文件中的代码)都可以使用。全局变量不受作用域的影响(也就是说,全局变量的生命期一直到程序的结束)。如果在一个文件中使用extern关键字来声明另一个文件中存在的全局变量,那么这个文件可以使用这个数据。
(2) 局部变量:
局部变量出现在一个作用域内,它们是局限于一个函数的。局部变量经常被称为自动变量,因为它们在进入作用域时自动生成,离开作用域时自动消失。关键字auto可以显式地说明这个问题,但是局部变量默认为auto,所以没有必要声明为auto。
(3) 寄存器变量
寄存器变量是一种局部变量。关键字register告诉编译器“尽可能快地访问这个变量”。加快访问速度取决于现实,但是,正如名字所暗示的那样,这经常是通过在寄存器中放置变量来做到的。这并不能保证将变置在寄存器中,甚至也不能保证提高访问速度。这只是对编译器的一个暗示。
注意:
使用register变量是有限制的:(1)不可能得到或计算register变量的地址; (2) register变量只能在一个块中声明(不可能有全局的或静态的register变量(c语言里register关键字可以在全局中定义变))。然而可以在一个函数中(即在参数表中)使用register变量作为一个形式参数。
一般地,不应当推测编译器的优化器,因为它可能比我们做得更好。因此,最好避免使用关键字register。
(4) 静态变量
关键字static有一些独特的意义。通常,函数中定义局部变量在函数中作用域结束时消失。当再次调用这个函数时,会重新创建变量的存储空间,其值会被重新初始化。如果想使局部变量的值在程序的整个生命期里仍然存在,我们可以定义函数的局部变量为static(静态的),并给它一个初始化。初始化只在函数第一次调用时执行,函数调用之间变量的值保持不变,这种方式,函数可以“记住”函数调用之间的一些信息片断。这也就是所谓的静态局部变量,具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只在定义自己的函数体内始终可见。
我们可能奇怪为什么不使用全局变量。static局部变量的优点是在函数范围之外它是不可用的,所以它不可能被轻易改变。这会使错误局部化。
此外同样存在静态全局变量,具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
(5) 外部变量
extern告诉编译器存在着一个变量和函数,即使编译器在当前的文件中没有看到它。这个变量或函数可能在一个文件或者在当前文件的后面定义。例如extern int i; 编译器会知道i肯定作为全局变量存在于某处。当编译器看到变量 i 的定义时,并没有看到别的声明,所以知道它在文件的前面已经找到了同样声明的 i。
(6) const常量
const告诉编译器这个名字表示常量,不管是内部的还是用户定义的数据类型都可以定义为const。如果定义了某对象为常量,然后试图改变它,编译器将会产生错误。在C++中一个const必须有初始值。
(7) volatile变量
限定词const告诉编译器“这是不会改变的”(这就是允许编译器执行额外的优化);而限定词volatile则告诉编译器“不知道何时变化”,防止编译器依据变量的稳定性作任何优化。
2.1.20 c++中类型转换机制?各适用什么环境?dynamic_cast转换失败时,会出现什么情况?⭐⭐⭐
2.1.22 描述一下普通局部变量、普通全局变量、静态局部变量、静态全局变量的区别
- 存取位置:全局变量和静态变量被分配到同一块内存中,即静态存储区。而普通局部变量的存储区为栈。
- 生存周期:
普通全局变量对整个源程序都有效,当此源程序包含多于一个文件的程序时,对其他文件依然有效。使用时,需用 extern 修饰;static 全局变量只在声明此static全局变量的文件中有效。普通局部变量的生存周期为声明其函数的周期,超过特定的范围其值会被重新初始化;static 局部变量生存周期为整个源程序,但是只能在声明其的函数中调用,并且其值与上一次的结果有关;
- 初始化:
普通全局变量、static局部变量、静态全局变量如果未初始化其值默认为0,普通局部变量存在栈区、不初始化内容随机。
2.1.23 头文件中双引号和尖括号的区别是什么?
- 使用双引号时:先搜索源文件所在的工作目录>编译器设置的头文件查找途径->系统变量C_INCLUDE_PATH指定的头文件路径。
- 使用尖括号时:先搜索编译器设置的头文件查找路径->系统变量C_INCLUDE_PATH指定的头文件路径。
2.1.24 #pragma once和#ifndef的区别
在C/C++中,为了避免了同一个头文件被包含(include)多次有两种宏实现方式:
- 第一种是#ifdef的方式,这种方式受C/C++语言标准支持,它可以保证同一个文件或者内容完全相同的文件/代码片段被包含多次。但这种定义方式依赖于宏名字不能冲突,如果宏名冲突,会导致头文件存在缺报错找不到声明。#ifndef的示例如下:
- 第二种方式采用#pragma的方式,这种方式由编译器保证同一个文件(物理意义上的)不会被包含多次,也不会出现因为宏名冲撞引发的问题。使用示例:
2.1.25 C语言中的声明和定义
- 函数和变量的声明不会分配内存,但定义会分配相应的内存空间。
- 函数和变量的声明可以有很多次,但定义只有一次。
- 函数的定义和声明方式都是默认extern的,即函数默认是全局的。可以采用static实现对函数的隐藏。
- 变量的声明和定义默认都是局部的,在当前编译单元或者文件中可用。
2.1.26 什么是预处理,何时要预处理?
C预处理器不是编译器的组成部分,而是编译过程的一个单独步骤,主要指的是程序执行前
#开头的指令的一些工作。
主要包括:#include, #define, #ifdef, #ifndef, #if/#elif/ #else/ #endif, #error, #pragma。简言之,C 预处理器(C Preprocessor) 只不过是一个文本替换的工具,其目的是指示编译器在实际编译之前完成所需的预处理。
2.1.27 C语言如何实现一个频繁使用短小函数,C++如何实现?
C语言可以使用宏定义实现一个短小函数,如下面例子所示。但是宏定义语句不会进行检查,并且对书写格式有过分的讲究。比如MAX和括号之间不能有空格,每个参数都要放在括号里。尽管如此,仍然会因为没有参数类型检查造成错误。
C++使用内联函数来实现,内联函数被编译器自动的以函数的形式添加进代码,因此会正常的进行参数检查。并且内联函数可以提高函数的使用效率。但是要注意内联函数不允许使用循环语句和开关语句,通常情况下,内联函数都是1-5行的小函数。
2.1.28 i++和++i去别,哪个速度更快?
++/--属于C/C++中的自增运算符,表示操作数增/减一。一般自增运算符有两种写法:i++ / ++i。区别:
- 用在独立的语句中:i++和++i没有区别。
- 用在赋值表达式中:y=i++表示先给y赋值,i自身再加一;y=++i表示先i自身加一再赋值给y。
速度上:++i的速度比较快,详细的流程如下:
2.1.29 描述一下gcc的编译过程?
gcc 编译过程分为4个阶段:预处理、编译、汇编、链接。
▪ 预处理:头文件包含、宏替换、条件编译、删除注释
▪ 编译:主要进行词法、语法、语义分析等,检查无误后将预处理好的文件编译成汇编文件。
▪ 汇编:将汇编文件转换成二进制目标文件
▪ 链接:将项目中的各个二进制文件+所需的库+启动代码链接成可执行文件
2.1.30 描述一下一维数组的不初始化、部分初始化、完全初始化的不同点
▪ 不初始化:如果是局部数组,数组元素的内容随机,如果是全局数组,数组的元素内容为 0
▪ 部分初始化:未被初始化的部分自动补 0
▪ 完全初始化:如果一个数组全部初始化 可以省略元素的个数数组的大小由初始化的个数确定
2.1.31 描述一下结构体对齐规则
1. 结构体成员对齐规则。第一个结构体成员应该放在offffset(偏移量)为0的地方,以后每个数组成员应该放在offffset 为min(当前成员的大小,#pargama pack(n))整数倍的地方开始(比如int在32位机器为4字节,#pargama pack(2),那么从2的倍数地方开始存储)。
2. 结构体总的大小,也就是sizeof的结果,必须是min(结构体内部最大成员,#pargama pack(n))的整数倍,不足要补⻬。
3. 结构体做为成员的对齐规则。如果一个结构体B里嵌套另⼀个结构体A,还是以最大成员类型的大小对齐,但是结构体A的起点为A内部最大成员的整数倍的地方。(struct B里存有struct A,A里有char,int,double等成员,那A应该从8的整数倍开始存储。),结构体A中的成员的对齐规则仍满足原则1、原则2。
2.1.32 请问以下代码有什么问题:
答:没有为str分配内存空间,将会发生异常,问题出在将一个字符串复制进一个字符变量指针所指地址。虽然可以正确输出结果,但因为越界进行内在读写而导致程序崩溃。
要初始化并分配内存给指向字符的指针
char *str,你可以使用标准库中的函数 malloc 或 calloc,或者使用C++中的 new 运算符。这取决于你的代码是C还是C++。在C中,你可以这样初始化和分配内存:
在C++中,你可以这样做:
在C中,你也可以使用
calloc 来初始化并分配内存,它会将分配的内存块中的所有字节都初始化为零:无论使用哪种方法,都要记得在使用完内存后释放它,以避免内存泄漏。
2.1.33 写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。
答:#define MIN(A,B) (A) <= (B) ? (A) : (B))
考点:
- 标识#define在宏中应用的基本知识。这是很重要的。因为在 嵌入(inline)操作符 变为标准C的一部分之前,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
- 三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优的代码,了解这个用法是很重要的。
- 懂得在宏中小心地把参数用括号括起来。
2.1.34 printf背后怎样过程
printf() 函数是C语言中用于格式化输出的一个标准库函数,其背后的过程可以简要概括为以下几个步骤:- 格式解析:
当程序调用
printf()函数时,首先会解析格式字符串,该字符串包含了要输出的文本以及用于格式化输出的控制字符(例如%d、%s等)。printf()函数根据格式字符串的内容来确定如何格式化输出。
- 参数处理:
printf()函数允许将多个参数传递给它,这些参数将按照格式字符串中指定的顺序进行输出。在格式化输出时,printf()函数会根据格式字符串中的格式控制符,从参数列表中获取对应的值,并将其插入到格式化输出中。
- 输出流处理:
一旦解析了格式字符串并处理了参数,
printf()函数就会将格式化后的文本输出到标准输出流(通常是屏幕)。这个过程涉及到将格式化后的文本逐个字符地写入输出流中。
- 字符流缓冲:
标准输出流通常会使用缓冲区来提高输出效率。在
printf()函数输出文本时,输出的字符首先被存储在输出缓冲区中,直到缓冲区被填满或者被显示清空。
- 刷新输出流:
当输出缓冲区被填满、遇到换行符
\n、或者程序显式地调用fflush(stdout)函数时,输出流会被刷新,缓冲区中的内容被写入到实际的输出设备中。这个过程将使得输出的文本实际显示在屏幕上。
- 返回值:
printf()函数通常会返回输出的字符数(不包括结尾的空字符\0),以便程序能够了解到实际输出了多少字符。
总的来说,
printf() 函数通过解析格式字符串、处理参数、输出文本、缓冲输出以及刷新输出流等步骤,实现了对输入参数的格式化输出。2.2 继承、多态相关面试题 ⭐⭐⭐⭐⭐
2.2.1 继承和虚继承 ⭐⭐⭐⭐⭐
2.2.2 多态的类,内存布局是怎么样的 ⭐⭐⭐⭐⭐
2.2.3 被隐藏的基类函数如何调用或者子类调用父类的同名函数和父类成员变量 ⭐⭐⭐⭐⭐
2.2.4 多态实现的三个条件、实现的原理 ⭐⭐⭐⭐⭐
2.2.5 对拷贝构造函数 深浅拷贝 的理解 拷贝构造函数作用及用途?什么时候需要自定义拷贝构造函数?⭐⭐⭐
2.2.6 析构函数可以抛出异常吗?为什么不能抛出异常?除了资源泄露,还有其他需考虑的因素吗?⭐⭐⭐
2.2.7 什么情况下会调用拷贝构造函数(三种情况)⭐⭐⭐
2.2.8 析构函数一般写成虚函数的原因⭐⭐⭐⭐⭐
2.2.9 构造函数为什么一般不定义为虚函数⭐⭐⭐⭐⭐
2.2.10 什么是纯虚函数⭐⭐⭐⭐⭐
2.2.11 静态绑定和动态绑定的介绍⭐⭐⭐⭐
2.2.12 C++所有的构造函数 ⭐⭐⭐
2.2.13 重写、重载、覆盖的区别⭐⭐⭐⭐⭐
2.2.14 成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?⭐⭐⭐⭐
2.2.15 如何避免编译器进行的隐式类型转换;(explicit)⭐⭐⭐⭐




