1. 结构体大小如何计算?
结构体的大小是根据它的成员变量的大小以及内存对齐规则来计算的。编译器通常会根据目标平台的架构,将结构体的成员按其数据类型的对齐要求进行排列。结构体的大小是所有成员变量的大小总和,再加上可能由内存对齐导致的填充字节(padding)。
计算步骤:
- 计算每个成员变量的大小。
- 考虑每个成员的对齐要求,将成员按对齐要求排列,填补可能的内存空隙。
- 最终的结构体大小应是最大对齐单位的倍数。
例如:
struct Example {
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
};
如果 int
类型要求4字节对齐,那么 a
之后会有3个填充字节,b
之后没有填充字节,c
之后还会有2个填充字节,最终大小为 12
字节。
2. 宏 OFFSET
的作用是什么?如何求出结构体中一个成员的内存偏移量?
宏 offsetof
通常用于计算结构体中某个成员的偏移量。它的作用是在不实例化结构体的情况下,计算结构体中某个成员相对于结构体首地址的偏移量。标准库中提供了 offsetof
宏,定义在 stddef.h
中。
实现方式:
#include <stddef.h>
#define offsetof(type, member) ((size_t) &(((type *)0)->member))
通过将结构体指针设为0,然后访问成员的地址,计算得出的值即为该成员的偏移量。
3. 结构体内存对齐问题,如何解决因为对齐产生的内存碎片?
内存对齐是为了提高处理器访问内存的效率,但这会产生填充字节,造成内存碎片。为了解决内存碎片问题,可以:
- 重新排列结构体成员:将较大的成员放在一起,减小填充字节。例如,将
int
类型的成员放在一起可以减少对齐产生的填充字节。 - 使用
#pragma pack
指令:在编译器中调整对齐方式。例如,通过#pragma pack(1)
指令强制结构体按1字节对齐,但这可能会降低访问效率。 - 使用内存池:对于大量相同结构体的内存分配,可以使用内存池管理器,减少内存碎片的影响。
4. C++容器:vector
和 map
的了解。
-
vector
:vector
是一个动态数组,可以根据需要自动调整其大小。它提供了连续的内存存储,支持快速随机访问,适合存储数量可变且需要频繁访问的元素。常见操作包括添加、删除、访问元素,时间复杂度为 O(1) 访问和 O(n) 插入或删除。 -
map
:map
是一个关联容器,存储键值对,键是唯一的。map
使用红黑树(或其他平衡树)实现,支持 O(log n) 的插入、删除和查找操作,适合需要高效查找、插入和删除的场景。
5. C++构造函数和析构函数的理解,析构函数的作用,构造函数的种类,移动构造函数。
-
构造函数:构造函数用于对象的初始化。当创建对象时,构造函数会自动调用。构造函数可以有多个重载,C++11 引入了移动构造函数,用于处理临时对象或右值。
- 默认构造函数:无参数,用于创建空对象。
- 参数化构造函数:带参数,用于根据输入值初始化对象。
- 复制构造函数:用已有对象来初始化新对象。
- 移动构造函数:使用右值引用,用于移动资源而非复制,避免不必要的复制,提高效率。
-
析构函数:析构函数在对象生命周期结束时调用,用于释放资源(如内存、文件描述符等)。析构函数的名字与类名相同,但前面带有
~
,且无返回类型和参数。
6. 虚函数、纯虚函数、虚函数表。
-
虚函数:通过
virtual
关键字定义的函数,允许派生类重写,支持多态性。调用虚函数时,根据对象的实际类型调用相应的函数版本。 -
纯虚函数:在基类中声明但不实现的虚函数,用于定义接口。类中包含纯虚函数,该类就是抽象类,不能实例化。
-
虚函数表(vtable):编译器为包含虚函数的类生成的一个指针表,表中包含类的虚函数指针。每个对象都有一个指向该表的指针,通过该指针实现动态绑定。
7. C++虚函数表的具体内容。
虚函数表包含类的所有虚函数指针,按声明顺序存放。在继承链中,派生类的虚函数表继承基类的虚函数表,并覆盖重写的虚函数指针。虚函数表实现了多态性,通过虚函数表指针(vptr)实现动态绑定和函数调用。
8. 函数回调的实现原理。
函数回调是指将一个函数的指针作为参数传递给另一个函数,并在适当的时机通过这个指针调用该函数。回调函数用于实现灵活的函数调用和事件处理。在C++中,可以通过函数指针、仿函数(函数对象)、std::function
、std::bind
或者 Lambda 表达式实现回调。
9. C++的四种类型转换。
static_cast
:用于非多态类型的转换,如基本类型之间的转换,类层次结构中向上或向下转换(需确保安全)。dynamic_cast
:用于多态类型的转换,通常在继承层次中向下转换,运行时检查类型安全性,失败时返回nullptr
(指针)或抛出异常(引用)。const_cast
:用于去除或添加const
限定符。reinterpret_cast
:强制转换类型,用于不相关类型之间的转换,极具危险性,需谨慎使用。
10. C++智能指针。
智能指针是C++提供的自动管理动态内存的工具,避免手动 new
/delete
导致的内存泄漏。C++11 标准引入了三种智能指针:
std::unique_ptr
:独占所有权,不可复制,适用于唯一所有权场景。std::shared_ptr
:共享所有权,引用计数管理内存,适用于多对象共享同一资源的场景。std::weak_ptr
:弱引用,不影响引用计数,通常与std::shared_ptr
配合使用,解决循环引用问题。
11. strcpy
和 strncpy
的区别,手写 strcmp
,实现 memcpy
。
strcpy
:复制字符串,复制到目标缓冲区,假设目标缓冲区足够大,可能引发缓冲区溢出。strncpy
:复制指定长度的字符串,确保不会超过目标缓冲区大小,但需要手动添加终止符。
手写 strcmp
:
int strcmp(const char *str1, const char *str2) {
while (*str1 && (*str1 == *str2)) {
str1++;
str2++;
}
return *(unsigned char*)str1 - *(unsigned char*)str2;
}
实现 memcpy
:
void *memcpy(void *dest, const void *src, size_t n) {
char *d = (char*)dest;
const char *s = (const char*)src;
while (n--) {
*d++ = *s++;
}
return dest;
}
12. 堆栈溢出和内存泄漏,排查和避免方法。
-
堆栈溢出:通常由于递归过深或分配了过大的局部变量导致。避免方法包括优化递归、使用堆代替栈、增加栈大小。
-
内存泄漏:动态分配的内存没有正确释放。避免方法包括使用智能指针自动管理内存、确保每个
new
都有相应的delete
、使用工具如 Valgrind 进行检测。
13. 数据结构的介绍。
常见数据结构包括:
- 数组:
连续内存块存储相同类型的数据,支持快速随机访问,插入删除效率低。
- 链表:非连续存储,支持高效插入和删除,访问效率低。
- 栈:后进先出(LIFO)结构,常用于递归和回溯算法。
- 队列:先进先出(FIFO)结构,常用于任务调度和广度优先搜索。
- 哈希表:基于哈希函数实现的键值对存储,支持快速插入、删除和查找。
- 树:分层结构,常用于搜索(如二叉搜索树)和表达式解析。
- 图:节点和边的集合,常用于网络结构、路径查找等。
14. 迭代器的作用,常见容器的底层实现。
-
迭代器:用于遍历容器内的元素,提供统一的接口,支持不同类型容器的遍历操作。迭代器类似指针,可以移动、比较、解引用。
-
常见容器的底层实现:
vector
:动态数组,连续内存,支持随机访问。list
:双向链表,不连续内存,支持高效插入删除。map
:红黑树实现,支持有序键值对存储。unordered_map
:哈希表实现,支持无序键值对存储,插入删除效率高。
15. 平衡二叉树的特点。
平衡二叉树是一种保持平衡的二叉搜索树,其左右子树的高度差不超过1。常见的平衡二叉树有AVL树和红黑树。平衡二叉树的查找、插入、删除操作的时间复杂度为O(log n),通过旋转操作维持平衡。
16. 变量声明和定义的区别,extern
关键字。
- 声明:告知编译器变量的类型和名字,但不分配内存。如:
extern int x;
- 定义:为变量分配内存空间。如:
int x = 10;
extern
:用于声明全局变量或函数,指向其他文件中的定义,避免重复定义错误。
17. 多态的概念和实现。
多态是面向对象编程中的概念,允许同一接口表现不同的行为。C++ 中通过继承和虚函数实现多态。基类指针或引用可以指向派生类对象,调用虚函数时,根据对象的实际类型调用相应版本的函数。
18. C++继承关系。
C++ 支持单继承和多继承,允许派生类继承基类的属性和方法。继承关系可以是公共、私有或保护的,影响基类成员在派生类中的访问权限。继承用于代码复用和多态实现。
19. C/C++区别。
- C:面向过程,适合系统编程和嵌入式开发,代码简洁高效。
- C++:在C的基础上增加了面向对象特性,支持类和对象、继承、多态等,适合大型复杂软件开发。
20. 动态链接和静态链接。
- 静态链接:在编译时将所有依赖库的代码直接包含到可执行文件中,生成独立的二进制文件。
- 动态链接:在运行时加载库,程序依赖的库不包含在可执行文件中,而是通过共享库动态加载。
21. STL容器的使用。
STL(标准模板库)提供了丰富的容器类,如 vector
, list
, map
, set
,以及算法和迭代器。使用 STL 容器可以提高开发效率和代码可维护性,适用于各种常见的数据存储和处理需求。
22. 虚函数实现多态的原理。
虚函数通过虚函数表(vtable)和虚函数表指针(vptr)实现多态性。每个包含虚函数的类都有一个虚函数表,表中存储着指向虚函数的指针。对象的虚函数表指针指向相应的虚函数表,调用虚函数时通过该指针实现动态绑定。
23. 内存管理:如何管理1G内存?如何实现动态内存分配?
-
内存管理:分配1G内存可以使用内存池管理器,将内存划分为多个块,根据需要分配和释放内存块。可以使用链表、位图等数据结构管理内存的分配状态。
-
动态内存分配:通过
malloc
/free
或 C++ 中的new
/delete
实现。动态分配需要注意内存对齐、碎片和内存泄漏问题。
24. 共用体的作用。
共用体(union)是一种特殊的结构体,允许不同类型的成员共用同一块内存。共用体的大小由最大的成员决定。共用体常用于节省内存空间,但需谨慎使用,确保正确管理不同成员的访问。
25. 类定义在64位上占多少字节?加入虚析构函数后的情况。
类的大小取决于其成员变量和内存对齐规则。64位系统下指针大小为8字节,如果类没有虚函数,其大小是所有成员的大小总和。加入虚析构函数后,类会有一个额外的指向虚函数表的指针,增加8字节。
26. 头文件重复包含解决方法。
头文件重复包含问题可以通过预处理指令防护,使用 #ifndef
, #define
, #endif
宏定义来防止重复包含:
#ifndef HEADER_FILE_NAME_H
#define HEADER_FILE_NAME_H
// 头文件内容
#endif // HEADER_FILE_NAME_H
27. 深拷贝和浅拷贝的区别。
- 浅拷贝:复制对象的所有成员,但不复制指向的资源,复制后两个对象共享同一资源。
- 深拷贝:不仅复制对象的成员,还复制指向的资源,确保两个对象互不影响。
28. 多线程如何保证线程安全。
线程安全性通过同步机制来保证,常见的同步工具包括互斥锁(mutex)、信号量(semaphore)、条件变量(condition variable)和原子操作(atomic operations)。合理使用这些工具可以避免竞态条件和数据不一致。
29. 链接过程中涉及的文件类型。
- 目标文件(.o/.obj):编译源代码生成的中间文件,包含机器代码和符号表。
- 静态库(.a/.lib):多个目标文件的集合,静态链接时将其合并到可执行文件中。
- 动态库(.so/.dll):共享库,动态链接时加载,减少可执行文件的大小。
30. 如何将左值强制转换成右值。
左值可以通过 std::move
转换为右值引用,std::move
是一个标准库函数,通常用于传递给函数或移动构造函数,减少不必要的拷贝,提高效率。
int a = 10;
int &&r = std::move(a); // 将左值 a 转换为右值引用 r