零基础面试题1

01 请说明 C 语言中指针和数组的核心区别,以及数组名在哪些场景会退化为指针?

(1)总述

  • 数组 :相同类型元素连续存放的集合;数组名是数组类型的不可修改左值 ,标识整块内存,自身不额外占存储,不能赋值、不能 arr++。
  • 指针 :存储内存地址的独立变量,自身在 x86 下占 4 字节、x64 下占 8 字节,可随意修改指向。

(2)四个核心区别

  1. sizeof 不同 :sizeof(数组名) 得到整个数组的字节数(N × sizeof(T));sizeof(指针) 只得到指针自身大小。
  2. & 取地址类型不同 :&arr 是指向整个数组的指针 T(*)[N],加减 1 移动整个数组长度;&p 是二级指针 T**。
  3. 可修改性不同 :数组名不可赋值、不可自增;指针可以 p++、p = ...。
  4. 函数传参不同 :形参 T arr[] 会被编译器改写为 T* arr,数组传参必然退化,函数内无法用 sizeof 求原数组大小。

(3)退化规则

数组表达式求值时默认退化为指向首元素的指针,仅四种情况例外:

  1. 作为 sizeof 的操作数;
  2. 作为取地址运算符 & 的操作数;
  3. 作为 _Alignof 的操作数(C11);
  4. 字符串字面值初始化字符数组 ,如 char s[] = "abc";。

其余场景(表达式求值、算术运算、函数传参)全部退化。注意 char *p = "abc"; 中的字面值会退化,p 指向只读静态区。

复制代码
int arr[10];
int *p = arr;

sizeof(arr);   // 40:整个数组
sizeof(p);     // 8(x64):指针自身
&arr;          // 类型 int(*)[10]
arr++;         // 编译错误:数组名不可修改
p++;           // 合法:移动一个 int

02 结构体和联合体的内存布局有什么差异?各自的典型使用场景是什么?

(1)结构体(struct):成员各自独立、按序排列

  • 每个成员拥有独立存储空间,按声明顺序排列;
  • 成员的有效对齐数 = min(类型自然对齐数, 编译器指定对齐数(#pragma pack));
  • 成员偏移量必须是其有效对齐数的整数倍,不满足则插入填充字节;
  • 结构体总大小必须是最大有效对齐数的整数倍,尾部也可能填充。

(2)联合体(union):成员共享内存、同时只有一个有效

  • 所有成员共享同一块从偏移 0 开始的内存;

  • 写入一个成员会覆盖其他成员,任一时刻只有一个成员有效;

  • 总大小 = 最大成员大小按最大对齐要求向上取。

    struct S {
    char c; // 偏移 0
    int i; // 偏移 4(1~3 填充)
    char d; // 偏移 8,尾部填充
    }; // sizeof(struct S) = 12

    union U {
    char c[9]; // 最大成员 9 字节
    int i; // 对齐要求 4
    }; // 9 向上取整为 4 的倍数 → 12

(3)典型使用场景

  • 结构体:组织需要同时存在的多个属性,如学生信息、网络协议首部、系统调用参数。
  • 联合体 :
    1. 配合枚举实现带标签联合体(tagged union),互斥字段、节省内存、手动多态;
    2. 协议解析 / 硬件寄存器视图,同一组字节按字节、半字、字访问;
    3. 类型双关(type punning),如判断大小端、查看浮点数位表示(属实现定义行为,GCC 支持)。

03 快速排序的复杂度、最坏情况与优化

问题:快速排序的平均、最好、最坏时间复杂度分别是多少?什么场景下出现最坏情况?常用优化手段有哪些?

(1)复杂度

  • 最好 / 平均:O(n log n)------每次划分接近均分,递归深度 log n,每层比较总量 O(n);
  • 最坏:O(n²)------每次划分极度不平衡(0 个与 n−1 个),递归深度退化为 n,比较总量 n + (n−1) + ... + 1。
  • 补充:空间复杂度平均 O(log n) 、最坏 O(n) ;快排不稳定。

(2)最坏情况的触发场景

每轮选取的基准值(pivot/key)都是区间极值。

最典型的情况是:对已经有序或完全逆序的数组,固定选取首元素(或尾元素)作为基准。

(3)六种常用优化手段

  1. 随机化基准值:使最坏情况无法被特定输入稳定触发;
  2. 三数取中(median-of-three):取首、中、尾的中位数,避免选到极值;
  3. 三路划分(Dutch National Flag) :分为 <key、=key、>key 三段,重复元素多时显著减少递归;
  4. 小区间切换插入排序:分区长度小于阈值(常取 5~20)时改用插入排序,常数因子更小;
  5. 尾递归优化:只递归小分区、大分区改迭代,栈深压缩到 O(log n);
  6. Introsort 兜底:递归深度超过 2 log n 时切换堆排序,保证最坏 O(n log n)

04 请说明进程和线程的本质区别,Linux 下常用的进程间通信(IPC)方式有哪些,各自的优缺点是什么?

(1)本质区别

  • 进程是资源分配的基本单位:拥有独立地址空间、页表、文件描述符表,进程间相互隔离;
  • 线程是 CPU 调度的基本单位 :一个进程至少含一个主线程;线程共享进程的代码、堆、全局变量、fd 表和信号处理函数,仅私有栈、寄存器上下文、线程 ID、errno、信号掩码和 TLS。

其他差异:

  1. 切换开销:进程切换要换页表、刷 TLB,开销大;同进程线程切换不换地址空间,只保存 PC、SP 和寄存器,开销小;
  2. 健壮性:进程间独立,一个崩溃不影响他人;一个线程崩溃通常拖垮整个进程;
  3. 通信方式:进程间必须用 IPC;线程间直接读写共享变量,但需互斥锁、条件变量同步。

(2)七种 IPC 方式及优缺点

  1. 匿名管道 pipe :内核环形缓冲区,半双工,靠 fork 继承 fd,用于亲缘进程。
    • 优点:简单、字节流可靠、内核自动同步;
    • 缺点:半双工、仅限亲缘进程、数据无格式、缓冲区有限(约 64KB)。
  2. 命名管道 FIFO :mkfifo 创建管道文件,任意本地进程可打开。
    • 优点:无亲缘要求、使用简单;
    • 缺点:半双工、不能跨主机、打开需读写双方同时在场、有文件创建删除开销。
  3. 信号 signal :异步事件通知。
    • 优点:开销极小、可异步触发;
    • 缺点:不能传数据(实时信号可携带少量数据)、会中断系统调用。
  4. 消息队列(System V / POSIX) :内核维护的消息链表,传递结构化消息。
    • 优点:按类型读取、支持优先级、自带同步、无亲缘要求;
    • 缺点:有内核拷贝、消息大小和数量受限、仅本地。
  5. 共享内存(System V / POSIX / mmap) :多进程映射同一物理页,适合大量数据。
    • 优点:速度最快、无内核拷贝;
    • 缺点:无同步机制,必须配合信号量/锁;仅本地;崩溃易残留脏数据。
  6. 信号量 semaphore:内核计数器,属同步机制而非数据传输,常与共享内存搭配。
  7. 套接字 socket :网络套接字可跨主机,AF_UNIX 用于本地。
    • 优点:全双工、可跨网络、协议标准、Unix 域套接字可传递 fd;
    • 缺点:有协议栈开销,本地速度不如共享内存。

选型口诀:大数据量 → 共享内存 + 信号量;结构化小消息 → 消息队列;异步通知 → 信号;跨主机 → socket。


05 什么是文件描述符(fd)?请对比 select、poll、epoll 三种 IO 多路复用机制的实现差异和使用场景。

(1)文件描述符

fd 是每个进程私有的、索引内核 I/O 对象的非负整数句柄。

文件、目录、管道、socket、设备等一切 I/O 对象在内核中统一抽象为 struct file,fd 即进程文件描述符表的下标;它是进程级概念,fork 时被子进程继承。默认 0 / 1 / 2 分别为标准输入、标准输出、标准错误。

(2)select

  • 基于 fd_set 位图,有 1024(FD_SETSIZE)硬上限;
  • 每次调用全量拷贝到内核并线性遍历 O(n);
  • 返回时 fd_set 被内核改写 ,下次调用前必须重新 FD_SET;用户态仍需 O(n) 遍历;
  • 仅支持水平触发 LT。

(3)poll

  • 基于 struct pollfd 数组,无 1024 上限(受 ulimit -n 和内存限制);
  • 每次仍需全量拷贝、线性遍历,O(n);
  • events / revents 输入输出分离,数组可复用,无需每次重设;
  • 仅支持水平触发 LT。

(4)epoll(Linux 专有)

  • 三个接口分工:epoll_create1() 创建实例 → epoll_ctl() 增删改 fd → epoll_wait() 等待就绪;
  • 内核用红黑树 管理注册 fd(只注册一次,无需每次全量拷贝),用就绪链表收集就绪 fd,并在设备等待队列上注册回调;
  • fd 就绪 → 回调把表项挂入就绪链表并唤醒进程 → epoll_wait 直接返回就绪项,复杂度 O(k)(k 为就绪数),与监听总数无关;
  • 支持 LT(默认)和 ET ,以及 EPOLLONESHOT;ET 必须搭配非阻塞 fd,并循环读到 EAGAIN,否则丢数据。

(5)使用场景

  • select:fd 数量少(<1024)、要求高可移植性(含 Windows);
  • poll:数量中等、需突破 1024 限制且要求 POSIX 跨平台;
  • epoll:Linux 高并发长连接(C10K / C100K),如 Nginx、Redis、高性能网络库------连接总数大、每次活跃数少时优势最大。
相关推荐
sogw-三叶草️1 小时前
LeetCode206. 反转链表
数据结构·链表
m0_739312871 小时前
【自动驾驶与机器人技术】之惯性导航与组合导航
算法·机器人·自动驾驶
驭渊的小故事1 小时前
牛客网算法刷题笔记:哈希表、贪心与动态规划实战
笔记·算法·散列表
careathers1 小时前
【数据结构】二叉树
数据结构·算法
轩轶子2 小时前
Python CLI Todo:一次数据结构错误引发的 Bug
数据结构·python·bug
Omics Pro2 小时前
空间组学!医学代谢显微工具
数据库·人工智能·算法·机器学习·自然语言处理
Coder-magician2 小时前
《代码随想录》刷题打卡day44:图论-part02
算法·深度优先·图论
克里斯蒂亚诺更新2 小时前
理解机器学习的各种算法
人工智能·算法·机器学习
小范的技术工坊2 小时前
大模型学习操作文档(13个核心概念串讲)
人工智能·学习·算法·大模型