C/C++面试常见知识点

C/C++语言

C++内存分区

1.栈(Stack):由编译器自动分配和释放,存放函数参数、局部变量等。栈是连续的内存区域,从高地址向低地址增长。

2.堆(Heap):由程序员手动分配和释放,存放动态分配的内存数据。堆是不连续的内存区域,从低地址向高地址增长。

3.全局/静态存储区(Global/Static Storage Area):存放全局变量、静态变量等,在程序启动时由系统自动分配,在程序结束时由系统回收。全局/静态存储区是不连续的内存区域,从低地址向高地址增长。

4.常量存储区(Constant Storage Area):存放常量字符串、const变量等,不允许被修改。常量存储区在程序运行期间始终存在且只读,通常位于全局/静态存储区中。

5.程序代码区(Code Area):存放程序的二进制代码,通常位于内存的低地址处,只读。

malloc/free与new/delete的区别

malloc和free是C语言中的函数,用于动态内存的分配和释放。而new和delete是C++中的运算符,也用于动态内存的分配和释放。

以下是malloc/free和new/delete之间的区别:

  1. 语法:malloc和free是C语言函数,使用时需要包含头文件stdlib.h,并通过指针来操作内存;而new和delete是C++运算符,直接在对象上调用,不需要使用指针。
  2. 类型安全:malloc返回的是void指针,在使用时需要进行类型转换;而new可以根据类型自动分配正确大小的内存,并返回特定类型的指针,不需要进行类型转换。
  3. 构造函数和析构函数:new在分配内存后会自动调用对象的构造函数进行初始化,而malloc只是分配一块内存,不会调用构造函数;同样,delete在释放内存前会调用对象的析构函数进行清理,而free只是简单地释放内存。
  4. 内存管理:new/delete会维护对象的类型信息和内存的分配大小,便于内存管理和回收;而malloc/free只负责内存的分配和释放,无法自动管理对象的生命周期。
  5. 异常处理:new在内存分配失败时会抛出std::bad_alloc异常,可以进行异常处理;而malloc在内存分配失败时返回NULL,需要手动检查是否分配成功。

总的来说,使用C++时推荐使用new/delete,它们更符合面向对象的思维方式,提供了更好的类型安全和内存管理。而在需要与C语言兼容或特定场景下,可以使用malloc/free来进行动态内存分配和释放。

联合体

一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)

联合的特点是成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联 合至少得有能力保存最大的那个成员)

联合体大小的计算

联合的大小至少是最大成员的大小。

当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

结构体对齐

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  4. 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8·

为什么需要结构体内存对齐

以32位机器为例,结构体中的变量大小不固定,但是32位机器的数据总线就32位,,内部是以字节编址,但是是以四个字节为一组进行寻址的,如果内存对齐,可以保证四个字节的数据可以被CPU一次读取出来,但如果没有内存对齐,四个字节的数据就可以分布的紧凑,但是CPU需要两次才能提取到完成的数据,虽然节省了空间,但是,降低的命中率,严重拖慢CPU的执行速率。

结构体与联合体的区别

  • 结构体(struct)是一种按照一定顺序排列并能够包含不同数据类型的数据集合。结构体中的每个成员变量都有自己的内存空间,因此结构体的大小等于所有成员变量的大小之和。这也意味着,结构体中的不同成员变量可以同时存储不同的值
  • 联合体(union)也是一种数据集合,但它只能存储其中一个成员变量的值。不同成员变量共用同一块内存空间,它们的值会互相覆盖。联合体的大小等于它的最长成员变量的大小。

左值引用与右值引用

左值引用用于绑定到具名对象或者拥有持久存储的表达式,它要求被引用的对象必须是可修改的。通过左值引用,可以对被引用的对象进行读写操作,而且可以使用引用来修改原始对象的值。左值引用使用单个 & 符号表示。

cppCopy Codeint x = 10;
int& ref = x; // 左值引用绑定到变量x
ref = 20;    // 修改了原始对象x的值

右值引用用于绑定到临时对象、匿名对象或者即将被销毁的对象,它可以延长对象的生命周期或者实现移动语义。通过右值引用,可以获取临时对象的资源或者通过移动操作减少对象的拷贝开销。右值引用使用双 && 符号表示。

cppCopy Codeint&& rref = 30; // 右值引用绑定到临时对象
rref = 40;      // 修改了临时对象的值(虽然没有实际意义)

C++11引入了移动语义和完美转发,右值引用在这些新特性的实现中起到了重要的作用。通过右值引用,可以区分左值和右值,实现更高效的对象管理和转移,提高程序的性能。

需要注意的是,左值引用和右值引用在语法上是相同的,但根据绑定的对象类型不同,会有不同的语义和使用方式。

指针和引用的区别

  1. 指针是一个变量,存储的是一个地址,指向内存的一个存储单元;引用是原变量的一个别名,跟原来的变量实质上是同一个东西。
  2. 指针可以有多级,引用只能是一级
  3. 指针可以在定义的时候不初始化,引用必须在定义的时候初始化
  4. 指针可以指向NULL,引用不可以为NULL
  5. 指针初始化之后可以再改变,引用不可以
  6. sizeof 的运算结果不同,在32位机器上,指针是4个字节,引用是绑定类型的大小
  7. 自增运算意义不同,指针加一是移动一个指向类型的字节大小,而引用加一是绑定的对象加一

迭代器失效

  1. 指向意义改变
  2. 变成野指针了

由于插入元素,使得容器元素整体“迁移”导致存放原容器元素的空间不再有效,从而使得指向原空间的迭代器失效。

由于删除元素使得某些元素次序发生变化使得原本指向某元素的迭代器不再指向希望指向的元素。

static关键字在C语言的作用

  1. 限制变量作用域,虽然变量一直存在,但是只能在指定作用域中使用
  2. 声明静态变量,在进程地址空间的静态区中,只能在第一次初始化
  3. 声明静态函数,保证静态函数只能在当前源文件调用,其它文件不可见,保证安全性

进程地址空间的分布

  1. 代码段(Code Segment):
    • 也称为文本段(Text Segment)。
    • 存放程序的机器指令或可执行代码。
    • 通常是只读的,以确保程序的指令不会被修改。
  2. 数据段(Data Segment):
    • 存放全局变量和静态变量。
    • 包括已初始化的数据和全局/静态变量的初始值。
    • 在程序启动时进行初始化,并且可以被读写。
  3. BSS段(Block Started by Symbol):
    • 存放未初始化的全局变量和静态变量。
    • 它们在程序启动时会自动被初始化为0或者空指针。
    • 不需要保存具体的初始值,因此节省了存储空间。
  4. 堆(Heap):
    • 用于动态内存的分配和释放。
    • 在程序运行时,可以通过函数如malloc()和free()来管理堆内存。
    • 堆是从低地址向高地址扩展的,由操作系统动态分配和回收内存。
  5. 栈(Stack):
    • 存放函数调用时的局部变量、函数参数、返回地址等。
    • 栈是一种自动分配和释放内存的方式,遵循后进先出(LIFO)的原则。
  6. 内核空间(Kernel Space):
    • 用于操作系统内核执行和管理。
    • 存放操作系统核心数据结构、设备驱动程序等。
    • 用户程序无法直接访问内核空间,需要通过系统调用来与内核交互。

内联函数

内联函数是一种特殊的函数,它与普通函数最大的不同在于在编译时会被直接嵌入到调用该函数的地方,从而避免了函数调用的开销。内联函数可以帮助提升程序的运行效率。

在 C++ 中,一个函数可以通过关键字 inline 来声明为内联函数。下面是一个内联函数

当该函数被调用时,编译器将会将其内部代码复制到调用处,并且优化成一个表达式或语句。

相比于普通函数,在使用内联函数时需要注意以下几点:

  1. 内联函数的代码必须简单且短小,以便编译器能够将其嵌入到调用点中。否则,如果函数体过大,可能会导致代码可读性降低、代码体积增大以及编译时间增加等问题。
  2. 内联函数的声明通常需要放在头文件中,以便所有使用到该函数的源文件都能够获得其定义。否则,在链接时可能会出现找不到函数定义的错误提示。
  3. 内联函数不支持递归调用和函数指针调用,因为这些调用方式无法被简单地展开。

总之,内联函数可以提高程序的运行效率,但需要注意函数代码的复杂度和调用方式等因素。对于一些频繁调用、简单且短小的函数,可以使用内联函数来优化程序性能。

三大特性

构造函数不能是虚函数

构造函数不能是虚函数,因为派生类不能继承基类的构造函数,将构造函数声明为虚函数没有意义。

vbtl构造函数调用建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次不是对象的动态行为,也必要成为虚函数

析构函数得是虚函数

如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而调用派生类析构函数。那么在这种情况下,派生类中申请的空间就得释放从而产生内存泄漏

多态中的虚函数表

虚函数表的位置:代码段

在编译时,派生类就会拷贝一份基类的虚函数指针数组,放进派生类为自己的虚表准备的空间中,然后,看看自己重写了哪个虚函数,就按照其相应的位置去将元素赋为自己重写的虚函数的函数指针。

如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。*这里的数组就是虚函数表(Virtual function table),简写为vtable。

基类的虚函数在 vtable 中的索引(下标)是固定的,不会随着继承层次的增加而改变,派生类新增的虚函数放在 vtable 的最后。如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable 中只会出现一次。

重载重写重定义

  • 重载:同一个作用域下,函数名相同,参数列表不同,在编译时确定-静态的多态
  • 重写(覆盖):两个函数分别在基类与派生类的作用域中,函数必须完全一样,函数都得是虚函数
  • 重定义(隐藏):函数名相同,在同一个类中进行,不涉及继承关系

动态的多态

通过基类的指针或者引用调用派生类的虚函数,在运行时确定,通过指向的对象的类不同,而查找的虚函数表不同,这样实现的动态的多态,通过这个也是实现的一种接口继承。

虚表指针是在什么阶段完成初始化的呢?虚表又是在什么阶段生成的?

虚表在编译阶段完成,虚表指针在构造函数的列表初始化时完成

虚表指针是存在于对象中的,所以在对象还没生成时,是不会有虚表指针的,而对象完成构造后,虚表指针又是正常出现了,所以虚表指针是在对象在构造函数中的初始化列表中完成初始化的。

而虚表是要早于虚表指针的,不然虚表指针就拿不到虚函数指针数组的首地址。而在一个程序的编译阶段会处理程序中的函数,对函数进行分析,所以虚表是在编译阶段生成的。当对象构造时,直接将地址赋给指针即可

网络

TCP三次握手

TCP报文中有SYN表示发起一个连接,FIN表示结束一个连接,ACK表示对这个报文的确认有效

  1. 客户端先向服务端发起建立连接的请求,报文中是不携带任何数据,报文中的SYN=1,表示建立连接,同时生成一个随机的32位的序号,这是表示后面的数据报文的序号都从则会个初始序号开始 ,表明客户端可以向服务端发送信息
  2. 服务端收到建立连接的报文段后,也会向客户端发送一个SYN=1,同时随机初始化序号的报文段,报文段中还要有ACK=1,表示确认收到客户端的请求,报文段中的确认号要加1回复,表示这个确认号之前的报文段都受到了,期待收到这个确认号的报文段,这是表示服务端可以接收与回复客户端的响应
  3. 客户端收到服务端的报文后,向服务端再次发送一个信息,表示客户端可以收到服务端的消息。这个报文段可以携带一些数据报文,其中消息确认段ACK=1,确认号根据上次服务端发送的序号加1

这三次握手之后,可以确保TCP全双工的协议正常,双方可以互相收发。

TCP四次挥手-断开连接

客户端断开连接时,FIN=1,表示断开连接,其它的如报文序号是需要正常推进,这样是在基于可靠传输协议上进行的。

服务端收到了FIN=1的报文段后,需要应答 ACK=1,确认收到了客户端的断开连接的请求。

服务端发送一个FIN=1的报文段,表示服务端做好了断开连接的准备,然后客户端发送一个ACK=1的确认报文段

表示断开连接

can通信的原理

串行通信协议,具有可靠灵活,实时的特点

  1. 以广播的形式发送报文.当CAN总线上的某个节点需要给其他节点发送消息时,会以广播的形式发送给总线上所有的节点,因为总线上的节点不适用地址来进行配置CAN系统,而是根据报文的开头的11位标识符决定是否要接受其他节点发来的报文.(面向内容的编制方案)

  2. 每个节点都有自己的处理器和CAN总线接口控制器;

  3. 当一个节点需要发送数据到另一个节点时,自身节点的处理器需要将要发送的数据和自己的标识符传给自身的总线控制接口,处于准备状态;当获取到总线的使用权后,将数据和标识符组装成报文,将报文以一定格式发出,此时其他的节点处于接收状态.至于其他节点是否接收,由其他节点决定,是都会对某些报文进行过滤.

  4. 当新增的节点仅仅是纯粹的数据接收设备时,只需要该设备直接从总线上接收数据即可.

多线程

互斥锁和信号量,条件变量的区别

功能不同:

  • 互斥锁是一种用于保护共享资源的锁机制,它确保在同一时刻只有一个线程可以访问被保护的资源。
  • 信号量是一种计数器,用于控制对资源的并发访问。它可以实现进程间的同步和互斥。
  • 条件变量用于线程之间的等待和通知,用于等待某个特定条件的发生。

使用方式不同:

  • 互斥锁提供了两个基本操作:上锁(Lock)和解锁(Unlock)。线程在访问共享资源之前需要先获取互斥锁,如果锁已经被其他线程占用,那么获取操作将被阻塞。一旦线程完成对共享资源的访问,就释放互斥锁,以便其他线程可以获取锁并访问资源。
  • 信号量包含一个计数器和两个原子操作:P(wait)和V(signal)。P 操作会将计数器减一,如果计数器小于零,则线程被阻塞。V 操作会将计数器加一,并唤醒等待的线程。线程在访问共享资源之前必须执行 P 操作,而在访问完成后执行 V 操作。
  • 条件变量通常结合互斥锁一起使用。线程可以在条件变量上等待某个条件的发生,并在满足条件时被唤醒。条件变量提供了 wait(阻塞等待条件)、signal(唤醒一个等待线程)和 broadcast(唤醒所有等待线程)等操作。

适用场景不同:

  • 互斥锁主要用于保护共享资源的互斥访问,防止多个线程同时修改共享数据造成数据不一致或冲突的问题。
  • 信号量可用于控制对有限数量资源的并发访问,例如限制同时访问某个文件的线程数量。
  • 条件变量通常与互斥锁一起使用,用于线程间的等待和通知机制,例如一个线程等待另一个线程完成某项任务后才能继续执行。

总结来说,互斥锁用于提供对共享资源的互斥访问信号量用于控制资源的并发访问,而条件变量则用于线程之间的等待和通知。它们各自有不同的功能和应用场景,可以根据具体需求选择使用。

进程间的通信

  1. 管道(Pipe):管道是一种半双工的通信方式,可以实现两个进程之间的通信。管道分为匿名管道和命名管道两种,匿名管道只适用于有亲缘关系的进程,而命名管道则可以跨越不同主机和无亲缘关系的进程进行通信。
  2. 信号(Signal):信号是一种异步的通信方式,主要用于处理进程间的同步和协作。某个进程可以向另一个进程发送信号,让其响应相应的操作。
  3. 共享内存(Shared Memory):共享内存是一种可以被多个进程同时访问的内存区域,在该内存区域中的数据可以被多个进程读写,从而实现了进程之间的通信和数据共享。由于多个进程可以同时访问共享内存,因此必须采用同步机制(如互斥锁、信号量等)来保证共享内存的正确性。
  4. 消息队列(Message Queue):消息队列是一种有一端写入消息、另一端读取消息的通信方式。消息队列不同于管道和共享内存,它们都只能进行一对一的通信,而消息队列可以实现一对多的通信。
  5. 套接字(Socket):套接字是用于不同主机之间通信的一种进程间通信方式,在网络编程中应用广泛。套接字提供了一种标准的接口,使得进程可以通过网络发送和接收数据。

线程间通信

  1. 共享内存:多个线程可以访问同一块内存区域进行数据交换。需要注意的是,由于共享内存可以被多个线程同时访问,因此需要使用同步机制(如互斥锁、条件变量等)来保证线程的正确同步和互斥。
  2. 消息队列:一个线程将消息放入队列,另外一个线程从队列中取出消息进行处理。消息队列可以提供异步通信和缓存功能,不过也需要使用同步机制来保证多个线程之间的数据安全。
  3. 线程信号量:一种特殊的计数器,用于控制多个线程对共享资源的访问。当某个线程获取到信号量时,可以继续执行特定操作;而当某个线程释放信号量时,其它线程可以获取到信号量并继续执行。线程信号量通常用于限制同时访问某个共享资源的线程数量。
  4. 管道(Pipe):管道是进程间通信的方式,但也可以用于线程间通信。管道提供了一个通信的通道,一个线程将数据写入管道,另一个线程从管道中读取数据进行处理。需要注意的是,由于管道是单向的,因此通常需要使用两个管道来实现双向通信。
  5. 信号(Signal):一种基于异步事件的通信方式,通常用于进程间通信,但也可以用于线程间通信。当某个线程执行特定事件(如段错误、中断等)时,可以向其它线程发送信号,以触发相应的操作。

排序

  1. 冒泡排序(Bubble Sort):重复地比较相邻的元素,将较大(或较小)的元素逐渐交换到右侧,直到整个数组有序。时间复杂度为O(n^2)。
  2. 插入排序(Insertion Sort):将待排序的元素依次插入已经排好序的序列中的合适位置,直到整个数组有序。时间复杂度为O(n^2),但在部分有序的情况下有较好的性能。
  3. 选择排序(Selection Sort):每次从无序区选择最小(或最大)的元素,放到有序区的末尾,直到整个数组有序。时间复杂度为O(n^2)。
  4. 快速排序(Quick Sort):通过一趟划分,将待排序序列分成两部分,左边部分都小于等于枢轴元素,右边部分都大于枢轴元素。然后对两部分分别进行快速排序,直到整个数组有序。平均时间复杂度为O(nlogn),但在最坏情况下可能达到O(n^2)。
  5. 归并排序(Merge Sort):将待排序序列分成两个子序列,分别对子序列进行归并排序,然后将两个有序的子序列合并成一个有序序列,直到整个数组有序。时间复杂度为O(nlogn),但需要使用额外的存储空间。
  6. 堆排序(Heap Sort):利用堆这种数据结构进行排序,构建最大堆或最小堆,然后每次取出堆顶元素并调整堆,直到整个数组有序。时间复杂度为O(nlogn),且不需要额外的存储空间。
  7. 希尔排序(Shell Sort):将待排序序列按照一定的步长进行分组,对每组进行插入排序,然后逐渐缩小步长,直到步长为1,最后进行一次完整的插入排序。时间复杂度取决于步长序列的选择,一般在O(nlogn)到O(n^2)之间。

快排的实现逻辑

挖坑法

对原始数组进行三数取中进行交换,然后选取首元素作为目标,存放在一个临时变量中,left指针指向第一个位置,然后right指针从数组最后一个元素向前移动,直到找到一个比目标元素小的,然后放到left指向的位置,right不动,left指针右移,寻找比目标元素大的放到right的位置。直到两个指针相遇,把目标元素放在相遇的位置,然后将数组从这个位置分成两个子数组,开始递归的执行逻辑

交换法

三数取中,选择一个目标元素存放在一个临时变量,设置左右两指针,left指针找到比目标元素大的就停止,right指针找到比目标元素小的就停止,然后左右指针的元素交换,然后继续移动直到相遇,把目标元素放在相遇的位置,然后以这个位置为分界分词两个子数组,递归执行逻辑。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值