[八股] C/C++进阶八股
点个小赞关注一波,持续更新……
[专栏]嵌入式软件校招笔记(点击跳转)
C/C++进阶概念
C++怎么从源码到可执行程序的过程
C++从源码到可执行程序的过程主要包括以下四个步骤:
- 预处理(Preprocessing):预处理器会处理C++源代码文件中的预处理指令,如#include和#define。它会替换#include指令为相应文件的内容(通常只是声明),替换宏定义(#define),并根据#if、#ifdef和#ifndef指令选择不同的文本部分。预处理器对一个C++源文件进行操作,生成一个单一的输出,这是一个由上述转换产生的标记流。
- 编译(Compilation):编译步骤在每个预处理器的输出上执行。编译器解析纯C++源代码(现在没有任何预处理指令),并将其转换为汇编代码。然后调用底层后端(工具链中的汇编器),将该代码编译为机器语言,生成实际的二进制文件。
- 汇编(Assembling):汇编器将源代码转换为目标代码1。在UNIX系统上,你可以看到扩展名为.o的文件,这些是目标代码文件(MSDOS上为.OBJ)。汇编器处理目标文件,将其汇编代码转换为机器语言指令。
- 链接(Linking):链接器接收由编译器产生的目标文件,并生成库或可执行文件。链接过程中,链接器会解析目标文件中的符号引用,并将这些引用与其他目标文件或库中定义的符号进行连接。
为什么局部变量未赋值时,每次初始化的结果是不确定的?
定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的,上次用完没清零的,所以说使用栈来实现的局部变量定义时如果不显式初始化,值就是脏的,是不确定的。
C语言中,static关键字的作用?
在C中,static主要定义全局静态变量、定义局部静态变量、定义静态函数。
1、定义全局静态变量:在全局变量前面加上关键字static,该全局变量变成了全局静态变量。全局静态变量有以下特点。a.在全局区分配内存。b.如果没有初始化,其默认值为0.c.该变量在本文件内从定义开始到文件结束可见。
2、定义局部静态变量:在局部变量前面加上关键字static,其特点如下:a.该变量在全局数据区分配内存。b.它始终驻留在全局数据区,直到程序运行结束。c. 其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。
memcpy和strcpy什么区别
memcpy
和strcpy
都是C++中用于复制数据的函数,但它们之间存在一些重要的区别:
- 数据类型:memcpy函数在内存中复制指定数量的字节,它对数据的类型并不关心,可以用于任何类型的数据1234。而strcpy函数则专门用于复制字符串1234。
- 复制方式:memcpy会复制你指定的所有字符12。而strcpy则会复制源字符串中的字符,直到遇到第一个空字符(‘\0’),或者复制了你指定的字符数为止12。
- 性能:通常来说,memcpy的效率会比strcpy更高,因为strcpy在复制数据时需要检查每个字符是否为’\0’1。
总的来说,如果你需要复制的是字符串,并且希望在遇到’\0’时自动停止复制,那么应该使用strcpy
。如果你需要复制特定数量的字节,或者复制的数据不是字符串,那么应该使用memcpy
。
介绍下常用的gdb命令
quit:退出gdb,结束调试
list:查看程序源代码
reverse-search:字符串用来从当前行向前查找第一个匹配的字符串
run:程序开始执行
help list/all:查看帮助信息
break:设置断点
break get_sum:以函数名设置断点
break 行号或者函数名 if 条件:以条件表达式设置断点
watch 条件表达式:条件表达式发生改变时程序就会停下来
next:继续执行下一条语句 ,会把函数当作一条语句执行
step:继续执行下一条语句,会跟踪进入函数,一次一条的执行函数内的代码
struct结构体内存对齐以及原因
为什么存在内存对齐?
大部分的参考资料都是这样说的:
1、平台原因(移植原因)不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存仅需要一次访问。
进一步解释一下cpu在取数据的时候一般都是4字节或8字节的取(32位cpu指的是cpu一次能处理的最大位数是32,并且它的内存寻址能力是32位,此时指针变量的大小为4字节。)
没有内存对齐的话,取出来的数据就是一个char和3/4个int,不是一个完整的int,为了取出一个完整的int,还得再取一次。
总体来说,结构体的内存对齐是拿空间换取时间的做法。那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?让占用空间小的成员尽量集中在一起
struct S1 { char c1; int i; char c2; }; //改为 struct S2 { char c1; char c2; int i; };
#pragma pack(1/2/4/8/16)//修改编译器的默认对齐数//如果参数不是1/2/4/8/16,在VS08下:warning C4086: 杂注参数应为“1”、“2”、“4”、“8”或者“16”#pragma pack()//把修改后的对齐数再改回来
struct联合体的内存对齐规则:
1.数据成员对齐规则:结构(struct或联合union)的数据成员,第一个成员在与结构体变量偏移量为0的地址处,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。
2.结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
3.结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)
union联合体的内存对齐规则
1、联合体的大小至少是最大成员的大小;2、当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。和struct类似,直接举例:
//在VS08下测试 #pragma pack(4) union A { char a[10]; double b; };//大小是12,虽然联合体中所有成员都在 与联合体变量偏移量为0 的位置,但是联合体A最终也要整体对齐一下,所以是12。 #pragma pack() union B { char a[10]; double b; };//大小是16 struct C { int a; union B b;//16,b的对齐数是8 };//大小为24
还有,枚举类型的大小就是4字节,相当于int。
C语言结构体怎么定义节省内存
1.在保证值域足够的情况下,用小字节变量代替大字节变量,如用short替代int 2.将各成员按其所占字节数从小到大声明,以尽量减少中间的填补空间(字节对齐)。 3.可以取消字节对齐,#pragma pack(1),当然这会牺牲效率,谨慎采用。
C/C++中大端小端是什么?能否介绍如何判断
https://zhuanlan.zhihu.com/p/477207022
void judge_bigend_littleend2() { int i = 1; char c = (*(char*)&i); if (c) printf("小端\n"); else printf("大端\n"); }
在小端模式(Little-Endian)和大端模式(Big-Endian)下,*(char*)&i 的值是不同的。
1. 小端模式(Little-Endian):在小端模式下,数据的低字节保存在内存的低地址中,而高字节保存在高地址中。
所以,对于 int i = 1;,其在内存中的存储形式将是 0x01 0x00 0x00 0x00。当你执行 *(char*)&i,你实际上是获取 i 的第一个字节。
在小端模式下,这将是低字节,即 0x01。所以,在小端模式下,*(char*)&i 的值将是 1。
2. 大端模式(Big-Endian):在大端模式下,数据的高字节保存在内存的低地址中,而低字节保存在高地址中。
所以,对于 int i = 1;,其在内存中的存储形式将是 0x00 0x00 0x00 0x01。
当你执行 *(char*)&i,你实际上是获取 i 的第一个字节。在大端模式下,这将是高字节,即 0x00。所以,在大端模式下,*(char*)&i 的值将是 0。
动态库和静态库的区别,后缀格式,以及函数的相对地址区别
区别:
命名方式不同:静态库libxxx.a:库名前加”lib”,后缀用”.a”,“xxx”为静态库名。动态库libxxx.so:库名前加”lib”,后缀变为“.so”。
链接时间不同: 静态库的代码是在编译过程中被载入程序中。 动态库的代码是当程序运行到相关函数才调用动态库的相应函数。
链接方式不同: 静态库的链接是将整个函数库的所有数据在编译时都整合进了目标代码。 动态库的链接是程序执行到哪个函数链接哪个函数的库。(用哪个链接哪个)
优缺点?
静态库: 优点是,在编译后的执行程序不再需要外部的函数库支持,运行速度相对快些; 缺点是,如果所使用的静态库发生更新改变,你的程序必须重新编译。
动态库 : 优点是,动态库的改变并不影响你的程序,所以动态函数库升级比较方便; 缺点是,因为函数库并没有整合进程序,所以程序的运行环境必须提供相应的库。
宏函数和内联函数的区别
内联函数是代码被插入到调用者代码处的函数。如同#define宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。 宏定义不检查函数参数和返回值,只是展开,相对来说,内联函数会检查参数类型,所以更安全。 内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以像调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
简述C++的内存管理
C++的内存管理主要涉及到以下几个方面:
- 堆(Heap):堆是由程序在运行时动态申请的内存空间。在C++中,可以使用new关键字来申请堆内存,并使用delete关键字来释放已申请的堆内存。
- 栈(Stack):栈内存用于存储局部变量和函数调用的信息。当函数被调用时,其参数和局部变量会被压入栈中。当函数执行完毕后,这些信息会被自动从栈中弹出,释放内存。
- 全局/静态存储区:全局变量和静态变量被存储在全局/静态存储区。这部分内存在程序的生命周期内一直存在。
- 常量存储区:常量存储区用于存储常量,这部分内存也是在程序的生命周期内一直存在。
- 自由存储区:这是一块由malloc等函数分配的内存区域,结束使用后要用free等函数释放。
C++提供了对内存管理的细粒度控制,但同时也带来了责任。程序员需要确保正确地分配和释放内存,以避免内存泄漏和其他相关问题。如果不正确地管理内存,可能会导致程序中出现严重的错误,如段错误、内存泄漏或者未定义的行为。
堆和栈的区别是什么?哪个更快一些?
堆和栈是两种不同类型的内存,它们在计算机程序中起着不同的作用。以下是它们的主要区别:
- 内存管理:堆内存是动态分配的,也就是说,你可以在运行时决定分配多少内存。而栈内存是在编译时就已经确定的。
- 生命周期:堆内存的生命周期取决于程序员的管理,当你使用new关键字分配了一块堆内存后,它会一直存在,直到你使用delete关键字将其释放。而栈内存的生命周期则取决于其作用域,当变量的作用域结束时,它占用的栈内存就会被自动释放。
- 性能:栈内存的分配和释放速度通常要比堆内存快。这是因为栈内存是以连续的、固定大小的块来管理的,所以分配和释放都非常快速。而堆内存则需要在运行时查找足够大的内存块来分配,所以速度相对较慢。
- 空间大小:栈空间相对较小,如果你尝试在栈上分配大量内存(例如,几兆或更多),可能会导致栈溢出。而堆空间通常要大得多,受限于计算机系统中可用的总内存。
- 碎片化:频繁地在堆上分配和释放小块内存可能会导致内存碎片化,这可能会降低程序的性能。而栈则不会有这个问题。
总的来说,堆和栈各有优势,选择使用哪种类型的内存取决于你的具体需求。如果你需要动态分配大量内存,并且愿意负责管理这些内存的生命周期,那么堆可能是一个好选择。如果你需要快速分配小块内存,并且希望这些内存能够在不再需要时自动释放,那么栈可能更适合。
C++如何保证申请内存成功?
在C++中,当你使用new运算符申请内存时,如果内存分配成功,new会返回分配的内存的地址。如果内存分配失败(例如,因为系统内存不足),则new会抛出一个std::bad_alloc异常或返回NULL。
因此,你可以通过检查new的返回值来判断内存是否已成功分配。以下是一个示例:
int* p = new(std::nothrow) int; if (!p) { std::cout << "Memory allocation failed.\n"; } else { // 使用p delete p; }
在这个例子中,我们使用了new(std::nothrow)。这将导致在内存分配失败时返回NULL,而不是抛出异常。然后我们检查p是否为NULL,如果是,那么我们知道内存分配失败。
注意:在使用完通过new分配的内存后,一定要记得使用delete来释放这块内存,以防止内存泄漏。同时,在释放完内存后,最好将指针设置为NULL,以防止产生野指针。
volatile关键字作用?
volatile是一种类型修饰符,用于声明可以被某些编译器未知的因素(如操作系统、硬件或其他线程等)更改的变量。当遇到这个关键字声明的变量时,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
例如,当一个变量被定义为volatile类型时,任何对它的修改都会立即刷新到主内存中,而不是等到线程结束或者锁被释放。同时,当其他线程需要访问这个变量时,它们会从主内存中读取最新的值。这样就保证了线程间数据的一致性。
在C/C++中,volatile关键字通常用于以下几个场合:
- 中断服务程序中修改的供其它程序检测的变量需要加volatile;
- 多任务环境下各任务间共享的标志应该加volatile;
- 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义。
总的来说,volatile关键字用于指示一个字段可以由多个同时执行的线程修改。它能防止编译器进行某些类型的优化,并确保对特殊地址的稳定访问。
C语言中如果一个代码段不想被编译器优化怎么进行处理
在C语言中,如果你不希望某个代码段被编译器优化,可以使用volatile关键字。volatile关键字告诉编译器,这个变量可能会被编译器未知的因素更改,比如操作系统、硬件或其他线程等。因此,编译器会对访问这个变量的代码进行特殊处理,不再进行优化。
例如:
volatile int x = 0;
在这个例子中,x是一个volatile变量。每次访问x时,都会从内存中重新读取数据,而不是使用寄存器中的缓存值。
另外,你也可以通过调整编译器的优化级别来防止代码被优化。例如,在GCC中,你可以使用-O0选项来关闭优化。
如果你不希望一个函数被编译器优化,可以使用特定的编译器指令或者属性来禁止优化。这些指令或属性依赖于你使用的具体编译器。
例如,在GCC和Clang中,你可以使用__attribute__((optimize("O0")))来禁止对特定函数的优化。下面是一个例子:
void __attribute__((optimize("O0"))) foo() { // your code here }
另外,你也可以在编译命令行中使用特定的选项来禁止优化。例如,在GCC和Clang中,你可以使用-O0选项来关闭优化。
请注意,禁止优化可能会影响程序的性能。因此,除非有特殊的理由,否则通常建议让编译器进行优化。
malloc申请的存储空间能用delete释放吗
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
记录本人校招过程中遇到的问题及笔记整理!后续会持续更新