知识列表
- 内存对齐
- extern “C”
- C++编译流程
- 编译器在底层如何实现重载
- C++函数调用的压栈过程
- 回调函数和其使用场景
- 内联函数和宏定义的区别
- 浅拷贝和深拷贝的区别
- 如何消除隐式转换
- 零拷贝
- strlen和sizeof的区别
- 头文件如何避免出现重定义的问题
- 静态多态和动态多态
- 重载和重写的区别
- 临时对象在什么时候产生
- C++的返回值优化
- 局部static变量为什么线程安全
- memcpy和strcpy什么区别
1.内存对齐
结构体基本规则
对齐:结构体成员的分配的内存空间的起始地址是自身大小的整数倍,比如int,起始大小是4的整数倍
补齐:结构体的大小是最大的成员变量的整数倍
内存对齐的必要性
提高内存的访问速度
。对齐的数据CPU能以最快的方式访问内存,数据对齐到特定的内存地址,使得CPU可以在单个或最少的内存访问周期内完成数据的读写防止硬件错误
。某些处理器(如ARM,SPARC)对未对齐的数据访问可能引发硬件异常。所以内存对齐能够确保程序的稳定运行。遵守硬件和编译器约定
。大多数现代编译器和CPU架构都有内存对齐的要求。遵守这些约定可以优化代码执行。平台兼容性
。在不同的平台之间共享数据时,正确的内存对齐确保数据结构的一致性和正确解析,特别是在网络通信和文件格式标准中。提高缓存利用率
。正确对齐的数据结构可能与缓存行边界对齐,减少缓存行加载次数,提高缓存利用率,从而减少CPU缓存未命中的情况。
2.extern ”C“ 的作用
- extern “C”。c++为支持函数重载,会对函数名进行复杂的的修饰。extern “C”告诉C++ 编译器按C语言的简单名称修饰处理,避免c++的复杂名称修饰。C++ 支持函数重载,这意味着可以定义多个同名但参数不同的函数。为了区分这些函数,C++ 编译器会对函数名称进行修饰,生成独特的名称(即名称修饰)。而 C 语言不支持函数重载,因此 C 编译器不会对函数名称进行修饰。使用
extern "C"
可以告诉 C++ 编译器不要对函数进行名称修饰,从而使得 C++ 中定义的函数可以被 C 语言代码正确地调用。 - 链接兼容性。如果你有一个 C 语言的库,并希望在 C++ 中使用它,或者反之,你可以使用
extern "C"
来确保函数能够被正确链接。例如:
extern "C" { void c_function(int a); }
这样,c_function
函数可以在 C++ 中被调用,并且可以在 C 代码中找到。 - 避免冲突。如果不使用
extern "C"
,而直接在 C++ 中声明 C 语言的函数,可能会导致链接错误,因为 C++ 编译器会对这些函数的名称进行修饰,导致找不到对应的函数实现。 - C 语言头文件。在包含 C 语言的头文件时,通常会看到如下的使用方式:
#ifdef __cplusplus extern "C" { #endif // C function declarations void c_function(int a); #ifdef __cplusplus } #endif
这种方式确保了在 C++ 编译时,C 函数的声明不会被名称修饰,从而可以在 C++ 代码中正确使用。
3.C++编译流程
预处理:处理宏定义,头文件包含,条件编译指令等。
编译:(词法分析,语法分析,语义分析,中间代码生成和优化),将预处理后的代码转换为汇编代码
汇编:将汇编代码转换为机器码。
链接:将多个目标文件和库文件链接生成可执行文件
4.编译器在底层如何实现重载
编译器通过 名称改编 实现函数重载,每个函数在编译时,编译器会将其名称和参数组合成一个唯一的符号,以区别重载的不同函数
5.C++函数调用的压栈过程
- 参数压栈。从右到左将参数压入栈中。
- 返回地址压栈。将函数的返回地址压入栈中。
- 跳转到函数代码。程序跳转到函数代码中执行。
- 函数体执行。在函数体中执行具体代码。
- 返回值处理。将返回值存储在寄存器或栈中
- 栈清理和返回。清理栈并返回到调用位置
6.回调函数和其使用场景
回调函数是一种委托机制,通过把函数的指针作为参数传递给另一个函数,从而在该函数内调用指针指向的函数。所以回调函数是由其他代码在适当的时间点调用的函数。
- 事件驱动编程 。在图形用户界面(GUI)编程中,回调函数用于处理按钮点击、鼠标移动等事件。例如,点击按钮时调用一个特定的函数来处理该点击事件
- 异步编程 。在网络编程中,回调函数用于处理异步 I/O 操作。例如,当数据从服务器返回时,回调函数被调用来处理这些数据。
- 定时器。定时器到期时调用回调函数执行某些操作。例如,设置一个定时器,每隔一段时间调用一个函数
- 函数式编程。许多函数式编程语言或库中,回调函数用于高阶函数。例如,map、filter 等函数接受回调函数作为参数,以对集合中的每个元素进行操作
- 排序和搜索。在排序算法(如 qsort)中,回调函数用于比较元素。用户可以定义自己的比较函数,从而定制排序行为。
7.内联函数和宏定义的区别
内联函数支持类型检查和调试,适用于短小的函数;宏定义通过预处理器进行文本替换,缺乏类型检查,容易导致难以发现的问题。
内联函数的原理
内联函数的原理主要是通过编译器在编译阶段将函数调用替换为函数体的代码,从而避免了常规函数调用的开销。以下是内联函数的具体原理和工作方式:
- 函数调用的开销:
- 在常规函数调用中,程序需要进行一系列操作,包括保存当前执行上下文(如寄存器、栈指针等)、跳转到函数的地址、执行函数体、然后再返回到调用点。这些操作会消耗时间和资源,尤其是在函数调用频繁的情况下。
- 代码替换:
- 当编译器遇到内联函数的调用时,它会尝试将该调用替换为内联函数的函数体。这样,程序在执行时就不需要进行常规的函数调用操作,直接执行内联函数的代码。
- 编译器的决策:
- 使用
inline
关键字只是一个建议,最终是否将函数视为内联函数由编译器决定。编译器会根据函数的复杂性、大小、调用频率等因素来决定是否内联。如果函数体较大或复杂,编译器可能会选择不进行内联。
- 使用
- 代码膨胀:
- 内联函数的一个潜在缺点是代码膨胀(code bloat)。如果一个内联函数被多次调用,编译器会在每个调用点插入函数体的代码,这可能导致生成的可执行文件变得庞大,尤其是在函数体较大或调用次数非常频繁的情况下。
使用内联函数的好处
- 提高性能:减少函数调用的开销,特别是对于小型、频繁调用的函数。
- 代码可读性:与宏(macro)相比,内联函数具有类型安全和作用域限制,增强了代码的可读性和可维护性。
如何正确的使用内联函数
调用频繁和短小的函数,需要类型检查,调试和设置断点的 函数
8.浅拷贝和深拷贝的区别
浅拷贝:只复制对象的指针,不复制指针指向的内存,导致多个对象共享同一块内存资源
深拷贝:复制对象及其指针指向的所有内存内容
9.如何消除隐式转换
隐式转换:编译器自动将一种类型转换成另一种类型
消除方法:使用explicit关键字防止隐式类型转换,确保只能显示的调用构造函数
10.零拷贝
零拷贝技术在数据传输过程中避免了内存拷贝,直接在不同模块(如用户空间和内核空间)之间传递数据,从而提高了系统性能,减少了CPU和内存带宽的消耗。常用于高性能网络传输和文件I/O操作,典型技术:sendfile,mmap,splice,DMA技术
原理
使得数据在处理过程中不需要从一个缓冲区拷贝到另一个缓冲区,而是通过指针或引用传递,从而减少了内存总线的使用,减少了数据在内存的拷贝操作,减少了CPU的使用率,提高了整体性能
sendfile
在Linux中,将文件数据直接从文件描述符发送到网络套接字
mmap
内存映射文件,允许文件的内容直接映射到进程的地址空间
splice
将数据在两个文件描述符之间移动,而无需将数据拷贝到内核空间
DMA(Direct Memory Access)
允许设备直接访问主内存,减少CPU参与数据传输
11.strlen和sizeof的区别
strlen是用于计算C风格(以nullptr结尾的字符数组)的长度,不包括结尾的NULL;返回类型是size_t,运行是计算字符串的实际长度,直到遇到第一个NULL字符
sizeof是一个编译时运算符,用于获取常量,数据类型或数据结构占用的内存大小,以字节为单位;编译时确定,不依赖与程序执行的状态
12.头文件如何避免出现重定义的问题
使用#ifndef #define #endif
13.静态多态和动态多态
静态多态就是编译时多态,通过函数重载和模版(泛型编程)来实现;解析时间,在编译时进行,也就是说编译器已经确定了函数的哪个版本将被调用;速度更快一些
动态多态也就是运行时多态,通过虚函数和继承来实现;解析时间,在运行时运行,在运行时决定调用哪个方法,速度比静态多态可能稍慢,因为需要再运行时查找适当的函数
14.重载和重写的区别
重载就是在同一个作用域的范围下,根据同名函数不同参数来实现,无需一些关键字
重写是基于派生类和基类的关系下,参数列表相同,要使用virtual关键字
15.临时对象在什么时候产生
- 返回时优化(RVO):当函数返回一个对象时,编译器通常会生成一个临时对象,然后通过优化直接将这个临时对象转移给接收对象,从而避免不必要的拷贝和移动操作
- 隐式类型转换:当函数参数或返回值需要进行类型转换时,编译器会创建一个临时对象来存放转换后的值
- 运算符重载:在重载运算符时,尤其是涉及到操作结果返回的情况,临时对象会产生
- 按值传递和返回:当函数参数按值传递或返回值按值返回时,临时对象在拷贝或移动中产生
- 临时对象的延长生命周期:当一个临时对象被绑定到一个const引用时,其生命周期会被延长
16.C++的返回值优化
- 返回值优化(RVO):适用于函数值直接返回一个临时对象的情况 : return MyClass();
- 命名返回值优化(NRVO):适用于函数返回一个命名的局部对象的情况 MyClass obj ; return obj;
17.局部static变量为什么线程安全
static变量在程序启动时(全局static变量)或第一次使用时(局部static变量)进行初始化。初始化过程由编译器和运行时环境控制,确保在多线程环境下只进行一次初始化。
static变量存储在全局/静态存储区,该区域在程序的整个生命周期内存在,且位置固定,这意味着不同线程访问static变量时,访问的是同一个内存位置
只初始化一次的具体原因:根据c++11标准,局部静态变量的初始化将在第一个进入该变量所在作用域的线程完成,并且是线程安全的。这意味着多个线程同时访问一个局部静态变量时,只有一个线程会进行初始化,而其他线程会被阻塞,直到初始化完成。
编译器实现:编译器在生成代码时,会在局部静态变量的初始化代码周围插入同步机制,确保只有一个线程能执行初始化代码,而其他线程会等待该初始化过程完成
实现方式:加锁,原子操作,双重检查锁定:减少不必要的锁开销,先检查变量是否已经初始化,如果没有再加锁进行初始化
18.memcpy和strcpy什么区别
memcpy用于内存块的拷贝,可以拷贝任意类型的数据,需指定拷贝的字节数,适用于二进制数据
strcpy:用于拷贝字符串,拷贝直到遇到\0结束符,适合字符串拷贝