C++每日面经

1.如果new的时候内存不够了,操作系统会做什么

在C++中,如果new关键字无法分配所需的内存,它将抛出一个std::bad_alloc异常。

在C/C++,不管是用malloc还是new,我申请的都是虚拟内存上的堆内存(malloc有一个阈值128k,当大于他的时候,从文件映射区分配,也就是调用mmap),也就是申请的是虚拟内存,64位机器虚拟内存的大小是128T,申请的虚拟内存只有在真正使用的时候才会出发缺页中断,映射到物理内存。当内存不够的时候,liunx操作系统为例(windows不太熟),操作系统会开始进行内存回收,分为后台回收,直接回收,如果开启了swap机制(一般是默认开启),操作系统,会把不常访问的一部分内存换到磁盘中,空出一部分内存来供我们的程序使用,(这时候我们及时申请很大,100T的内存,按理说也能正常使用),当回收的速度跟不上我们使用的速度,操作系统就会开启OOM机制,强制杀内存(最糟糕的情况)。

2.C++中static的作用

在C++中,static是一个非常重要的关键字,它有多种不同的用途,如下所示:

  1. 静态成员变量(Static Member Variables)

在C++中,静态成员变量是类的成员变量,但与普通成员变量不同的是,静态成员变量只有一个副本,无论创建了多少个对象,它始终存在于类中,并且在任何对象创建之前就已经分配了内存空间。
静态成员变量在声明时必须初始化,可以在类定义中初始化,也可以在类外进行初始化

  1. 静态成员函数(Static Member Functions)

静态成员函数是类的成员函数,但与普通成员函数不同的是,它没有this指针,并且可以直接使用类的静态成员变量,而不需要创建任何对象。静态成员函数可以通过类名来调用,也可以通过对象名来调用。

  1. 局部静态变量(Local Static Variables)

局部静态变量是在函数内部定义的变量,但与普通局部变量不同的是,它们只在函数第一次调用时初始化,并且在之后的调用中保持其值。局部静态变量存储在静态存储区域中,生命周期与程序的生命周期相同。

  1. 静态函数(Static Functions)

静态函数是在文件作用域内定义的函数,也称为内部链接函数,它只在当前文件中可见。静态函数不能被其他文件中的函数调用,因此可以用来限制函数的作用域,提高程序的模块化程度。
总之,static关键字在C++中有多种用途,可以用来定义静态成员变量、静态成员函数、局部静态变量和静态函数,提高程序的模块化程度,增加程序的可读性和可维护性。

3.C++多态介绍一下

C++中的多态是面向对象编程的核心概念之一,它允许程序在运行时根据对象的实际类型来调用不同的函数实现,从而实现动态绑定。

在C++中,多态性通过虚函数来实现。虚函数是在基类中声明的函数,在派生类中可以被重写。在使用虚函数的情况下,通过基类指针或引用调用函数时,将根据实际对象的类型来调用正确的函数实现,即在运行时动态绑定函数调用。

例如,假设我们有一个基类Animal和两个派生类Dog和Cat,它们都实现了一个名为speak的虚函数。我们可以定义一个指向Animal对象的指针,然后通过该指针调用speak函数。如果该指针指向Dog对象,则调用Dog类的speak函数,如果该指针指向Cat对象,则调用Cat类的speak函数。

这种行为被称为多态性,它允许我们编写具有可扩展性和可维护性的程序。通过多态性,我们可以编写通用的代码,而不必关心将来添加新的派生类时如何处理它们。此外,多态性还允许我们编写抽象类,它定义了一组纯虚函数,由派生类实现,从而使得我们可以编写通用的代码,而不必关心派生类的具体实现。

需要注意的是,为了使用多态性,必须将虚函数声明为虚函数。这可以通过在函数声明前加上virtual关键字来实现。此外,多态性还有其他一些规则和限制,例如:

  • 构造函数不能是虚函数。
  • 友元函数不能是虚函数。
  • 静态成员函数不能是虚函数。
  • 虚函数可以是纯虚函数,但此时基类将成为抽象类。

总之,C++的多态性是面向对象编程的核心概念之一,它通过虚函数来实现,在运行时动态绑定函数调用,允许我们编写具有可扩展性和可维护性的程序。

4.介绍一下进程的调度

进程的调度过程 - 搜索结果 - 知乎 (zhihu.com)()

5.知道进程的加载吗

在进程加载过程中,操作系统会分配一段内存空间给进程,并将进程所需的代码和数据加载到该内存空间中。具体的加载过程包括以下几个步骤:

  1. 空间分配:操作系统会为进程分配一段连续的内存空间,这个空间包括进程的代码段、数据段、堆栈等。
  2. 装入:将程序的可执行文件从硬盘中读取到内存中,通常包括代码段、数据段、符号表、重定位信息等。
  3. 初始化:对进程的代码段、数据段进行初始化,包括全局变量的初始化、静态变量的初始化、堆栈指针的初始化等。
  4. 动态链接:对于需要链接的库文件,操作系统会在运行时进行动态链接,将库文件中的函数和变量链接到进程的地址空间中。
  5. 启动进程:最后,操作系统会将程序的入口地址作为程序计数器的值,开始执行进程的代码。

6.进程上下文切换会保存哪些数据

在操作系统中,当CPU从一个进程切换到另一个进程时,需要保存当前进程的上下文信息,以便在下次切换回该进程时能够继续执行。上下文切换会保存以下一些数据:

  1. 寄存器值:包括通用寄存器、程序计数器、状态寄存器、堆栈指针等。这些值保存了当前进程的执行状态,以便在下次切换回来时能够继续执行。
  2. 进程控制块(PCB):进程控制块是操作系统中用于管理进程的数据结构,其中保存了该进程的所有状态信息,包括进程标识符、进程状态、进程优先级、进程的资源使用情况等。上下文切换时需要保存当前进程的 PCB,以便在下次切换回该进程时能够继续管理该进程。
  3. 内存映像信息:包括代码、数据、堆栈等内存区域的信息。这些信息保存了当前进程所使用的内存区域的状态,以便在下次切换回来时能够继续使用这些内存区域。
  4. 浮点寄存器值(可选):对于需要使用浮点运算的进程,操作系统需要保存其浮点寄存器的状态,以便在下次切换回来时能够继续执行浮点运算。

总之,上下文切换需要保存当前进程的执行状态以及与进程相关的一些数据,以便在下次切换回来时能够继续执行。这是操作系统中非常重要的一个机制,也是实现多任务处理和进程间切换的关键。

7.C++虚函数表如何生成的

C++的虚函数是通过虚函数表来实现的。在C++编译器编译一个类时,如果它包含虚函数,编译器会为该类生成一个虚函数表(Virtual Table)。

虚函数表是一个指向函数地址的指针数组,每个虚函数都在虚函数表中占用一个指针位置。这个指针指向了实际的虚函数的地址,因此当调用虚函数时,程序可以通过虚函数表找到正确的函数地址,并执行该函数。

虚函数表通常被放置在对象的内存空间的开头或结尾,或者在类的元数据中作为一个单独的数据成员。

当一个对象被创建时,编译器会将虚函数表的地址存储在对象的内存空间中,以便在调用虚函数时可以通过该地址找到正确的虚函数表,并从中获取正确的函数地址。

同时,对于每个派生类,编译器也会生成一个新的虚函数表,并在其中添加新增的虚函数,覆盖或继承基类的虚函数表中的虚函数。

8.是一个类一个虚函数表指针吗,还是一个对象

在C++中,一个类通常只有一个虚函数表,而不是每个对象都有一个虚函数表指针。

每个类都有一个虚函数表,其中包含该类的所有虚函数的指针。这个虚函数表是在编译时由编译器生成的,并存储在静态内存中。当类的一个实例(对象)被创建时,该对象的内存空间中会包含一个指向该类虚函数表的指针。这个指针通常被称为虚函数表指针,或简称为vptr。

因此,如果一个类有多个实例,它们都会共享同一个虚函数表,而每个实例的虚函数表指针会指向这个虚函数表。

在调用虚函数时,程序会通过对象的虚函数表指针找到对应的虚函数表,并从中获取正确的虚函数地址进行调用。这个过程是由编译器和链接器自动完成的,程序员无需手动干预。

需要注意的是,由于虚函数表和虚函数表指针都是编译器和链接器生成和管理的,它们的具体实现可能会因编译器和操作系统的不同而有所差异。

9.友元函数和友元类了解吗

在C++中,友元(friend)是一种访问控制机制,它允许其他类或函数访问一个类的私有成员。具体来说,C++中的友元有两种形式:友元函数和友元类。

  1. 友元函数

友元函数是一个被声明为该类友元的非成员函数。这意味着该函数可以访问该类的私有成员和受保护成员。在类中声明友元函数的语法如下:

c++Copy code
class MyClass {
    friend void MyFriendFunction();
};
  1. 友元类

友元类是一个被声明为该类友元的其他类。这意味着该类可以访问该类的私有成员和受保护成员。在类中声明友元类的语法如下:

c++Copy code
class MyClass {
    friend class MyFriendClass;
};

使用友元函数或友元类可以提高程序的灵活性和可扩展性,但也会降低程序的封装性。因此,使用友元应该慎重,只在必要的情况下使用。

需要注意的是,友元函数和友元类是声明在类内部的,但它们并不是该类的成员函数或成员类。它们可以在类外定义和实现,并且它们的访问权限不受类的访问控制符(public、private、protected)的限制。

10.为什么需要友元,它不是会破坏封装吗

确实,友元机制会破坏类的封装性,因为它允许外部实体(如函数或类)直接访问一个类的私有成员。因此,使用友元应该慎重,只在必要的情况下使用。

那么为什么需要友元呢?其主要原因是提高程序的灵活性和可扩展性。以下是一些常见的使用友元的场景:

  1. 友元函数或友元类可以访问类的私有成员或受保护成员,这使得它们可以提供一些更高效或更方便的实现方式,而不必通过公共接口来访问这些成员。比如,一个类的成员函数可能需要访问另一个类的私有成员,这时可以把这个成员函数声明为另一个类的友元函数。
  2. 友元可以用于实现一些设计模式,比如单例模式、工厂模式等。
  3. 友元可以提供类似于“友好”(friendship)的机制,使得一些相关的类或函数可以互相协作,而不必暴露类的私有成员给外部。

11.为什么需要异步日志,同步不行吗

在软件开发中,日志是一项重要的功能。日志可以记录应用程序的运行状态、异常信息、性能数据等,对于问题排查和性能优化都非常有帮助。在日志功能的实现中,同步写入和异步写入是两种常见的方式。

同步写入指的是在应用程序中直接将日志写入磁盘或其他存储介质中,它的实现方式是在日志写入过程中使用文件锁或者互斥量来保证线程安全。这种方式的优点是实现简单,易于调试,但是在高并发的情况下,会因为频繁地竞争文件锁或互斥量而导致性能下降。

异步写入则是在应用程序中将日志消息发送到一个缓冲区中,由专门的线程负责将缓冲区中的日志写入磁盘或其他存储介质中。这种方式的优点是可以提高日志的写入效率,避免了同步写入的性能问题,而且可以通过合理的调整缓冲区大小和写入线程的数量来平衡性能和资源的消耗。

另外,异步日志还有以下一些优点:

  1. 异步写入可以避免因为文件锁或互斥量竞争而导致的线程阻塞,提高了应用程序的响应能力和吞吐量。
  2. 异步写入可以将日志写入和应用程序的主线程分离,减轻了应用程序的负担,提高了应用程序的稳定性。
  3. 异步写入可以在写入线程中实现一些高级功能,比如日志滚动、压缩、上传等,进一步提高了日志系统的可靠性和可扩展性。

综上所述,异步日志相比同步日志具有更高的写入效率、更好的稳定性和可扩展性。当应用程序需要高性能和高可靠性的日志功能时,异步日志是更好的选择。

12.同步日志就没优点吗,什么时候选择同步日志

虽然异步日志相比同步日志有更多的优点,但在一些特殊的场景下,同步日志仍然是一个不错的选择。

比如,当应用程序的并发量比较小,日志量比较少时,同步日志的性能表现往往比异步日志好,因为异步日志需要维护缓冲区和写入线程等额外的开销。此外,当应用程序需要立即将日志写入磁盘时,同步日志也是一个更好的选择,因为异步日志的缓冲区中的日志并不是实时写入磁盘的。

另外,在一些对日志性能和可靠性要求不高的场景下,同步日志也是一种不错的选择。比如在测试环境下进行简单的单元测试时,同步日志的实现比异步日志更加简单明了,方便调试和排错。还有一些小型应用程序或工具,使用同步日志可以避免引入额外的复杂性和资源消耗,从而更加轻便和高效。

综上所述,同步日志和异步日志都有各自的优缺点,需要根据实际需求和场景来进行选择。如果应用程序需要高性能和高可靠性的日志功能,异步日志是更好的选择;如果应用程序对日志性能和可靠性要求不高,或者并发量比较小,同步日志也是一个不错的选择。

13.说一下你对抽象编程的理解,为什么要搞那么多特性去抽象编程

抽象编程是一种编程范式,它的目的是通过将代码中与具体实现相关的细节抽象出来,从而使代码更加通用、灵活、可复用。抽象编程通常涉及到面向对象编程、泛型编程、函数式编程等多种技术。

抽象编程的好处在于它能够将问题领域的概念和程序中的实现分离开来,从而使程序更加清晰、简单和易于维护。抽象编程可以使程序员专注于问题的本质,而不必关心实现的具体细节。同时,抽象编程还可以提高代码的重用性和可扩展性,从而减少开发时间和维护成本。

抽象编程通常会涉及到多种特性,比如:

  1. 接口和抽象类:通过定义接口和抽象类来规范类之间的交互方式,从而提高代码的灵活性和可扩展性。
  2. 泛型编程:通过泛型编程技术,可以将代码中与数据类型相关的细节抽象出来,从而使代码更加通用和灵活。
  3. 模板元编程:通过模板元编程技术,可以在编译期间进行计算和处理,从而提高代码的性能和灵活性。
  4. Lambda表达式和函数对象:通过使用Lambda表达式和函数对象,可以将函数作为一等公民来处理,从而提高代码的灵活性和可读性。

总之,抽象编程是一种非常有用的编程范式,它可以使程序更加通用、灵活和可维护。虽然抽象编程涉及到多种特性和技术,但它们都是为了实现代码的抽象和规范化,从而提高代码的质量和可维护性。

14.你了解哪些C++17/20的实用新特性,说一下

C++17和C++20都是最近几年推出的C++标准,其中包含了很多新的特性和改进。以下是我了解的一些C++17/20的实用新特性:

  1. 结构化绑定(Structured bindings):可以将一个复合类型的元素分解成若干个独立的变量。
  2. if语句中的初始化:可以在if语句中初始化变量。
  3. constexpr if语句:可以在编译时根据条件判断是否执行某些代码。
  4. inline变量:可以在头文件中定义内联变量,从而避免多重定义的问题。
  5. 常量表达式函数:可以定义常量表达式函数,从而在编译期间执行一些简单的计算。
  6. 变量模板:可以定义模板变量,从而实现类型无关的编程。
  7. 增强型enum:可以定义枚举类,从而提高枚举类型的类型安全性。
  8. 增强型类型推导(Class template argument deduction):可以自动推导模板类的类型参数。
  9. Lambda表达式的改进:Lambda表达式的参数支持按值捕获、按引用捕获、移动语义捕获等。
  10. Coroutines:协程是C++20的一个新特性,可以实现轻量级的用户空间线程,从而提高程序的并发性能和可读性。

15.C++类的内存对齐了解多少

C++类的内存对齐是为了优化内存访问效率和提高数据存取速度,特别是在涉及到结构体或类成员变量类型不同时。内存对齐可以保证数据在内存中的对齐方式,使得 CPU 可以快速地访问内存,提高程序的运行效率。

在C++中,编译器会根据数据类型自动进行内存对齐。对于一个类对象,如果成员变量是按照声明顺序依次存储的,那么其内存布局也会按照声明顺序进行排列。但是,为了保证内存对齐,编译器可能会在成员变量之间填充空白区域,从而使得每个成员变量都按照一定的字节对齐。

内存对齐的方式和字节数是由编译器和操作系统决定的,并且可能会因为不同的编译器、不同的编译选项和不同的平台而有所不同。例如,一般情况下,x86架构的CPU对于基本数据类型的内存对齐要求为4字节,而对于SSE指令集要求对齐到16字节。此外,一些编译器还提供了指定对齐字节数的选项。

16.malloc申请的内存free后还存在吗

当使用malloc()函数在堆上申请一段内存时,系统会为这段内存分配一块虚拟地址空间,并将其标记为“未分配状态”,并返回该内存块的起始地址。当调用free()函数释放这段内存时,系统将该内存块标记为“未使用状态”,但并不会立即将该内存块的虚拟地址空间返回给操作系统,而是将其标记为“空闲状态”,以便后续的malloc()函数可以再次使用该内存块。因此,在释放内存后,该内存块的虚拟地址空间仍然属于当前进程,但是可以被重新分配给其他内存使用。

需要注意的是,当程序结束时,操作系统会自动回收当前进程所占用的所有虚拟地址空间,包括已经释放但未返回给操作系统的空闲内存块。此时,这些内存块的虚拟地址空间会被释放并返回给系统。因此,在程序结束前,如果没有及时释放内存,会导致内存泄漏,影响系统性能和稳定性。

17.C++类的内存分布是怎样的

在C++中,类的内存分布由两部分组成:对象头和对象体。

对象头通常包含虚函数表指针以及其他一些元数据,例如RTTI信息、虚继承表指针等等,这些信息用于实现多态和其他高级特性。

对象体是指类的成员变量所占用的内存区域,按照成员变量的定义顺序依次排列。类的成员变量包括普通成员变量、静态成员变量、成员函数指针等等。

类的内存分布遵循一些规则,其中最重要的是内存对齐。内存对齐是指将数据存储在内存中时,为了提高访问速度和效率,要求数据的起始地址是某个特定值的倍数。在C++中,每个成员变量都有一个对齐要求,通常是变量的大小或者操作系统的内存对齐要求,类的内存对齐要求是其所有成员变量中对齐要求最大的那个。

对于派生类,其内存分布是由基类和派生类成员变量共同组成的。基类子对象的内存分布在派生类对象的前部,派生类的成员变量则按照定义顺序依次排列在基类子对象之后。

需要注意的是,C++并不保证类的成员变量的顺序与定义顺序一致,因此不要依赖于成员变量的顺序。

18.你说到了虚继承指针,展开说一下

虚继承是C++中用于解决菱形继承问题的一种机制。在菱形继承中,一个派生类继承了同一个基类的两个或多个直接或间接基类,从而导致基类的数据成员在派生类对象中出现了多次,造成了内存空间的浪费和访问歧义。

为了解决这个问题,C++提供了虚继承的机制。在虚继承中,共享基类的子对象只在派生类对象中存在一份,不论这个基类在派生类的继承层次中出现了多少次。这样可以避免内存浪费和访问歧义,但是需要在内存布局上增加额外的开销。

虚继承通过在派生类对象中增加一个虚继承指针来实现。虚继承指针是一个指向共享基类子对象的指针,它指向了派生类对象中该共享子对象的地址。虚继承指针通常放置在派生类对象内存布局的开始位置,这样就可以方便地获取到该共享子对象的地址。在C++的内存模型中,虚继承指针通常是通过虚表来实现的。由于虚表是每个类只有一个,因此虚继承指针只会出现在虚继承的基类中,而不会出现在普通基类中。

转载至:https://zhuanlan.zhihu.com/p/621161187

你可能感兴趣的:(C++学习记录,开发语言,c++)