C++ 面试准备 八股(一)夯实C++的基础体系

type right by Thomas Alan 光风霁月023 .XDU
发现一个方便看的小工具 如何生成目录索引

一、总览部分

1. 既然有C语言为什么有C++

  C++ 是在 C 语言基础上发展而来的一种编程语言。它继承了 C 语言的基本语法和特性,并且加入了很多新的特性和功能,比如类、继承、多态、模板等。C++ 语言的出现是为了弥补 C 语言在面向对象编程方面的不足,同时也为了支持更加复杂的程序设计和开发。

  C++ 语言具有比 C 语言更强的面向对象编程特性和更丰富的标准库。它可以更方便地进行软件开发,能够提高程序的可维护性、可靠性和可重用性。同时,C++ 还可以直接使用 C 语言编写的代码,具有很好的兼容性,这也是 C++ 得到广泛应用的原因之一。

  C++ 相对于 C 语言的一些优势:
  1) 面向对象编程:C++ 支持面向对象编程,包括封装、继承、多态等特性,使得程序设计更加模块化、灵活,易于扩展和维护。
  2) 类型安全:C++ 中的类型检查比 C 语言更为严格,避免了一些潜在的类型错误,提高了程序的稳定性和安全性。
  3) STL 库:C++ 标准库中提供了丰富的数据结构和算法,包括容器、迭代器、算法等,可以大大提高程序的开发效率和运行效率。
  4) 模板:C++ 中的模板是一种泛型编程技术,可以在不知道类型的情况下进行编程,实现代码的重用和泛化。
  5) 异常处理:C++ 支持异常处理机制,可以有效地处理程序中的异常情况,提高了程序的容错性和可靠性。
  6) 命名空间:C++ 中的命名空间可以有效地避免命名冲突,使得程序设计更加规范、清晰。
  7) 运算符重载:C++ 允许用户对运算符进行重载,可以方便地实现用户自定义类型的运算。

  总的来说,C++ 语言是 C 语言的一个超集,它继承了 C 语言的优点,并且加入了更多的特性和功能,使得程序设计更加方便、高效、灵活。

2. C++的编译过程

编译的过程一般包括预处理、编译、汇编、链接四个步骤。

1)预处理:预处理器会对源代码进行处理,包括对头文件的处理、宏替换等,生成一个经过预处理后的代码文件。
  预处理是在编译之前进行的一步操作,主要作用是对源代码进行一些宏替换和头文件的处理。预处理器会根据源代码中的宏定义和头文件,将相应的代码插入到源代码中,生成一个预处理后的代码文件。预处理后的代码不是最终的可执行文件,但是它包含了源代码中的所有宏定义和头文件。

2)编译:编译器会对预处理后的代码进行语法和语义的分析,生成相应的汇编代码。
  编译是将预处理后的代码转化成汇编语言的过程。编译器会将源代码翻译成汇编语言,然后进行语法和语义的检查,以保证程序的正确性和可靠性。编译器在编译的过程中,会对代码进行一定的优化,比如去除无用的代码,减少重复的代码等。

2.5)优化:在编译的过程中,C++ 编译器会进行优化,包括代码优化、数据优化等,以提高程序的执行效率和性能。
  编译器会对程序中的代码进行分析和重组,以减少程序的运行时间和空间占用。比如,编译器会进行循环展开、函数内联、死代码消除等操作,优化程序的执行效率。(详细见 3. 优化操作包括哪些内容)

3)汇编:汇编器会将编译器生成的汇编代码转换为机器码,并生成目标文件。
  汇编是将编译器生成的汇编代码转换成机器码的过程。汇编器会读取编译器生成的汇编代码,并将其翻译成机器码,生成目标文件。

4)链接:链接器会将目标文件和库文件链接起来,生成可执行文件。
  链接是将目标文件和库文件合并成可执行文件的过程。链接器会将目标文件中的符号和库文件中的符号进行匹配,然后将它们合并在一起,生成最终的可执行文件。链接器还会对可执行文件进行一些优化,包括压缩、去重、重定位等操作。

另外,C++ 还支持多种编译方式,包括静态编译和动态编译等,用户可以根据自己的需要选择不同的编译方式。
在这个过程中,还会涉及到一些其他的操作,比如调试信息的生成、符号表的生成、库文件的加载等。总的来说,C++ 的编译过程比较复杂,但是它可以保证程序的正确性和可靠性,并且可以进行一些优化,提高程序的执行效率和性能。

3. 优化操作包括哪些内容

1)数据优化:编译器会对程序中的数据进行分析和优化,以减少数据访问的次数和数据传输的时间。比如,编译器会进行变量共享、数组优化、寄存器分配等操作,优化程序的内存访问效率。

2)并行化优化:编译器会对程序中的代码进行分析和重组,以便于在多核处理器上并行执行。比如,编译器会进行循环并行化、任务并行化等操作,优化程序的并行执行效率。

3)压缩优化:编译器会对可执行文件进行压缩,以减少文件的大小,提高文件的加载速度。比如,编译器会进行代码压缩、符号表压缩等操作,优化程序的文件大小。

4)缓存优化:编译器会对程序中的代码和数据进行分析和优化,以减少缓存的失效率。比如,编译器会进行局部性优化、数据对齐等操作,优化程序的缓存命中率。

总的来说,C++ 编译器在编译过程中进行的优化操作非常多,每个编译器都有其独特的优化算法和优化策略,以提高程序的执行效率和性能。

4. 面向对象的特点

1)封装性(Encapsulation):将对象的属性和方法封装在一起,只对外暴露必要的接口,隐藏了实现的细节,提高了代码的可读性和可维护性。

2)继承性(Inheritance):子类可以继承父类的属性和方法,避免了代码的重复,增加了代码的重用性。

3)多态性(Polymorphism):同一个方法在不同的对象上可以有不同的实现,使得代码具有更高的灵活性和可扩展性。

4)抽象性(Abstraction):从具体的实例中抽象出通用的概念,形成类的定义,使得代码具有更高的抽象层次和可复用性。

5)消息传递性(Message Passing):对象之间通过消息进行通信,封装了对象之间的关系,降低了代码的耦合度。

这些特点使得面向对象编程具有更好的可维护性、可扩展性和代码复用性。它也使得程序更易于理解和修改,使得软件开发更加高效和可靠。

5. C++ 的内存分区

一般的,C++ 内存分区为:数据段、代码段、BSS段、堆区、栈区
其中, BSS段相对于其他内存区域来说使用较少,而全局区(静态区)包括了BSS段和数据段中的已初始化静态变量,更符合实际编程中的使用情况,所以人们通常会直接把全局区(静态区)作为表示静态变量和全局变量的存储区域。

因此这里按照实用性总结为:栈区、堆区、全局区(静态区)、常量区、代码区。在描述完具体分区后,本文继续解释BSS段相关内容。

1)栈区:栈区用于存储函数的局部变量、函数的参数等数据,它的内存分配和释放是自动完成的,当函数调用结束时,栈区的内存会自动释放。栈区的空间相对较小,通常为几 MB,也不适合存储大量的数据。

2)堆区(自由存储区):堆区用于存储程序动态分配的内存,堆区的内存分配和释放需要手动完成,通过 new & delete (malloc & free) 关键字进行操作。堆区的空间相对较大,可以存储大量的数据,但是需要手动管理内存,否则容易出现内存泄漏和内存溢出等问题。很多编译器的new & delete都是以malloc & free为基础来实现的。

3)全局区(静态区):全局区用于存储全局变量和静态变量等数据,它的内存分配和释放在程序启动和结束时完成。全局区的空间相对较大,可以存储大量的数据,但是需要注意全局变量的作用域和生命周期等问题。

注:与普通局部变量不同的是,静态局部变量的生命周期与全局变量相同,即在程序开始时被创建,在程序结束时被销毁。
普通局部变量一般存在于栈区,如果使用new、malloc创建,那么自然在堆区。

4)常量区:常量区用于存储程序中的常量数据,包括字符串常量和数值常量等。常量区的内存分配在程序编译时完成,通常是只读的,不能修改其中的数据。

5)代码区:代码区用于存储程序的代码段,包括函数体和程序指令等。代码区的内存分配在程序编译时完成,通常是只读的,不能修改其中的数据。

注:Block Started by Symbol(BSS)可以翻译为“以符号开始的块”,它是一种存储未初始化全局变量和静态变量的段。BSS段通常在程序加载时被清零,因此不需要在可执行文件中占用磁盘空间,只是在程序加载时分配内存。
数据段包含已初始化的全局、静态变量、常量
BSS段包含未初始化的全局、静态变量。因此,在对未初始化的 BSS 段变量进行赋值时,会将其从 BSS 段移动到数据段。由于数据段和 BSS 段在内存中的位置是不同的,会导致内存的重新分配。但是,这种内存的重新分配只会在变量进行赋值时才会发生,一旦赋值完成,变量的内存位置就不会再改变了。

在 C++ 程序中,了解内存分区的特点和使用方式,可以更好地管理内存,避免内存泄漏和内存溢出等问题,提高程序的性能和稳定性。

5.堆(Heap)和栈(Stack)的区别:

1)内存分配方式:
堆区的内存分配是动态的,需要手动分配和释放,通常使用 new 和 delete 关键字进行操作。
栈区的内存分配是静态的,在程序编译时就已经确定了,程序在运行时会自动分配和释放。

2)内存空间大小:
堆区的空间相对较大,可以存储大量的数据,但需要手动管理内存。几乎是没有什么限制的。
栈区的空间相对较小,通常只能存储一些局部变量和函数参数等数据。一般只有几M。

3)内存访问方式:
堆区的内存访问方式是通过指针进行的,需要进行动态内存分配和释放,并且访问速度较慢。
栈区的内存访问方式是通过栈指针进行的,速度较快。

4)内存分配效率:
堆区的内存分配效率较低,需要频繁地进行动态内存分配和释放,容易产生内存碎片等问题。
栈区的内存分配效率较高,因为它采用的是一种先进后出(LIFO)的数据结构。

5)生长方向:
堆区的生长方向是向上的,向着内存地址增加的方向增长。
栈区的生长方向是向下的,向着内存地址减小的方向增长。

在实际编程中,应根据程序的需求和特点,合理地选择堆区和栈区进行内存分配,避免出现内存泄漏和内存溢出等问题。

6. 常见的内存错误

1)内存泄漏:动态分配的内存没有被正确释放,导致内存一直被占用,最终导致程序崩溃或者消耗大量内存。

2)内存溢出:程序申请的内存超出了系统可分配的内存限制,导致程序崩溃或者出现未定义的行为。

3)访问未初始化的内存:使用未初始化的内存会导致未定义的行为,包括程序崩溃、产生随机结果等。

4)缓冲区溢出:在处理字符串、数组等数据结构时,往往会使用缓冲区,如果缓冲区的长度不够,就可能会发生缓冲区溢出,导致程序崩溃或者出现未定义的行为。

5)访问已经释放的内存:已经被释放的内存不能再次访问,否则可能导致程序崩溃或者出现未定义的行为。

6)重复释放内存:已经被释放的内存不能再次释放,否则可能导致程序崩溃或者出现未定义的行为。

7)内存越界:访问数组时超出了数组边界,或者使用指针操作时超出了指针所指向的内存空间,导致程序崩溃或者出现未定义的行为。

8)多线程内存竞争:多个线程同时访问同一个内存区域,导致数据不一致、程序崩溃等问题。

7. 怎么检查内存泄露,泄露了多少

  在 C++ 中,可以通过一些工具来检查内存泄漏,其中比较常用的工具有 Valgrind 和 AddressSanitizer。
  Valgrind 是一款常用的开源内存检查工具,可以检查内存泄漏、内存越界、使用未初始化的内存等。它支持多种编程语言,包括 C++、C、Java 等,并且支持多种操作系统,包括 Linux、macOS 等。

  1)使用 Valgrind 检查内存泄漏的方法如下:
  在编译时添加 -g 选项,以便 Valgrind 能够读取可执行文件的调试符号信息。
  运行可执行文件时,使用 Valgrind 工具进行检查:

valgrind --leak-check=full ./executable

  其中,--leak-check=full 表示要检查内存泄漏,并打印出详细信息。
  如果 Valgrind 检测到内存泄漏,它会打印出相关的信息,包括泄漏的内存块地址、大小、分配位置等。可以根据这些信息来定位内存泄漏的代码位置。
  另外,有一些第三方工具,如 LeakSanitizer、AddressSanitizer 等,也可以用来检测内存泄漏。

  2)在程序中,可以使用一些代码来记录内存的分配和释放情况,从而计算内存泄漏的大小。一种常见的方法是使用计数器,每当分配一块内存时,将计数器加 1,每当释放一块内存时,将计数器减 1。最终,如果计数器不为 0,则说明有内存泄漏。这种方法的缺点是需要修改代码,并且对于复杂的程序可能会出现误报或漏报的情况。

二、零散部分

1. 定义一个对象与new一个对象的区别

1)定义一个对象:定义一个对象时,编译器会在栈(stack)上为对象分配内存空间,并在程序执行到离开该对象的作用域时,自动调用对象的析构函数来释放内存。

2)new 一个对象:使用 new 运算符动态地在堆(heap)上分配对象的内存空间,并返回指向该对象的指针。需要手动使用 delete 运算符来释放对象的内存空间,否则会导致内存泄漏问题。

  因此,定义一个对象比 new 一个对象更加方便和安全,但是 new 一个对象能够动态地分配内存空间,适用于在程序运行时需要动态创建对象的情况。需要注意的是,在使用 new 运算符创建对象后,一定要记得使用 delete 运算符来释放内存空间,否则会导致内存泄漏问题。

2. 指针跟引用的区别

1)定义方式不同:
指针变量使用 * 号来定义。
引用变量使用 & 号来定义。

2)空指针:
指针可以是空指针(nullptr),表示它不指向任何有效的内存地址。
引用必须始终指向一个有效的对象,否则会导致编译错误。

3)可修改性:
指针可以被重新赋值指向另一个内存地址。
引用在定义时必须初始化,并且不能被重新赋值指向另一个对象。

4)对象的访问方式:
指针需要通过解引用(*)符号来访问对象的值。
引用不需要解引用符号,可以直接访问对象的值。

5)空间占用:
指针变量需要占用一定的内存空间来存储内存地址。
引用变量不需要占用内存空间,只是一个别名。

总的来说,指针更加灵活,可以指向任意类型的对象,并且可以被重新赋值指向另一个对象,但是需要注意空指针的问题和解引用的操作。而引用更加简洁和易读,并且不需要考虑空指针和解引用的问题,但是需要在定义时初始化,并且不能被重新赋值。

3. define、const、inline 的区别和联系(它们都是用于定义常量的关键字)

1)定义方式不同:
·define 是预处理指令,使用 #define 命令来定义常量。
·const 是 C++ 中的关键字,使用 const 关键字来定义常量。
·inline 也是 C++ 中的关键字,使用 inline 关键字来定义内联函数和内联变量。

2)作用不同:
·define 定义的常量是预处理时进行文本替换的,没有类型检查,容易出错,可以定义任何类型的常量,也可以定义宏函数。
·const 定义的常量是在程序运行时进行类型检查的,类型安全,只能定义基本类型和对象类型的常量,不能定义宏函数。
·inline 定义的内联函数和内联变量可以在编译时进行替换,以减少函数调用和内存开销。

3)使用方式不同:
·define 定义的常量没有作用域限制,可以在整个程序中使用,容易产生命名冲突。
·const 定义的常量有作用域限制,只能在定义它的作用域内使用,具有较好的封装性和安全性。
·inline 定义的内联函数和内联变量可以直接在头文件中定义,可以被多个源文件共享使用,但需要注意内联函数的代码不能过于复杂,否则可能会降低程序的性能。

综上所述,define、const、inline 都是用于定义常量的关键字,它们在使用方式、作用范围、类型安全、性能等方面有所不同,根据具体的使用场景选择合适的关键字可以提高程序的性能和安全性。

4. 什么是字节对齐

  字节对齐(Byte Alignment)是指在存储器中,对于某些数据类型,按照一定的规则将其存储在某个地址处的一种技术。

  在计算机存储器中,每个地址都对应着一个字节(Byte),而不同的数据类型需要占用不同的字节数。为了提高内存读写的效率,计算机通常会对数据进行字节对齐,即按照一定的规则将数据放置在内存中,使得数据的起始地址能够被一个固定的值整除。

  例如,32位的计算机中,一个 int 类型的变量通常需要占用 4 个字节,如果按照自然对齐的规则将它放在地址为 0x1000 的位置,那么下一个 int 类型的变量应该放置在地址为 0x1004 的位置,这样可以保证数据对齐并提高内存读写的效率。如果没有进行字节对齐,数据的读写可能需要多次访问内存,从而影响程序的性能。

  在 C++ 中,可以使用预编译指令 #pragma pack(n)(其中 n 表示字节对齐的值)来指定结构体的字节对齐方式。但需要注意的是,字节对齐可能会导致结构体的大小变大,从而增加内存的使用量,因此在编写程序时应根据实际情况进行选择。

5. 虚函数、虚指针、虚表

1)虚函数:
  虚函数是C++中实现多态的重要机制之一。在C++中,如果一个函数被声明为虚函数,它的派生类可以重新定义这个函数并且实现多态。这意味着,如果派生类重新定义了虚函数,那么调用该函数时会根据实际的对象类型调用对应的函数实现,而不是根据指针或者引用类型来调用函数。
  在类中将函数声明为虚函数,需要在函数声明前加上virtual关键字

class A {
public:
    virtual void foo() {}
};

注:
  只有将基类中的成员函数声明为虚函数,才可以在派生类中进行重写(覆盖)。如果在派生类中定义了一个与基类中同名、同参数列表的函数,但没有使用 virtual 关键字来声明该函数为虚函数,那么这个函数就不会被当做是虚函数来处理,也就无法实现多态性,即使基类指针指向派生类对象,也只会调用基类中定义的该函数。因此,如果希望在派生类中对基类中的函数进行重写,必须在基类中将该函数声明为虚函数。
  在子类中重写一个父类的虚函数时,子类可以不标记virtual关键字。如果父类的虚函数被标记为virtual,那么这个函数在父类和子类中都是虚函数,即使子类中没有显式地使用virtual关键字。重写父类的虚函数,子类可以选择是否使用virtual关键字来标记它,但是为了代码的可读性和可维护性,建议在子类中显式地标记虚函数为virtual。
  后面带 = 0的虚函数称为纯虚函数,纯虚函数的类不能被实例化,子类必须实现父类纯虚函数,才能实例化出来

void virtual foo() = 0;

2)虚函数指针:
  虚函数指针是一个指向虚函数表的指针,每个对象都有一个虚函数指针。虚函数表是一个包含了虚函数地址的数组,每个类都有一个对应的虚函数表,用来存储该类中所有虚函数的地址。当一个类定义了虚函数时,编译器会在该类中添加一个虚函数表,并在对象中添加一个指向该表的指针。
  在调用一个虚函数时,编译器会通过对象的虚函数指针找到该对象的虚函数表,然后在虚函数表中查找该函数的地址,并调用该函数。这种方式能够实现动态绑定,即在运行时决定调用哪个函数,而不是在编译时就确定了。
  虚函数指针和虚函数表的实现是由编译器和链接器完成的,对于程序员来说,只需要声明和定义虚函数即可,编译器和链接器会自动处理虚函数指针和虚函数表的生成。

3)虚函数表:
虚函数表(Virtual Function Table,简称 vtable)是用于实现 C++ 中的多态性的一种机制。它是一个包含虚函数地址的数组,每个类(包括抽象类)都有自己的一张虚函数表。
  在 C++ 中,如果一个类声明了虚函数,编译器会在该类的对象中添加一个指向虚函数表的指针(通常称为虚指针),并将虚函数表放在程序的数据段中。当调用一个虚函数时,实际上是通过该对象的虚指针来访问虚函数表,并根据函数在虚函数表中的索引来调用相应的虚函数。
  子类继承父类时,会继承父类的虚函数表,并在自己的虚函数表中添加自己新的虚函数。如果子类重写了父类的虚函数,那么子类的虚函数表中的相应位置将被替换为子类的虚函数地址。
  虚函数表的具体实现可能因编译器不同而有所区别,但其基本原理是一致的。

6. 晚绑定

  晚绑定(Late Binding)是指在运行时才确定实际调用的函数,与之相对的是早绑定(Early Binding),是指在编译时就能够确定调用的函数。在C++中,虚函数机制是实现晚绑定的重要手段之一,它能够实现运行时多态,即在运行时动态地决定调用哪个实现。这种机制可以提高代码的灵活性和可扩展性,因为子类可以重写父类的虚函数,从而实现不同的行为。
  函数指针数组:另外一种实现晚绑定的方式是使用函数指针数组,这种方式通常用于C语言中的结构体中,由于C语言没有虚函数的概念。在这种方式下,结构体中会定义一个函数指针数组,数组中存储了结构体中的所有成员函数的地址。在调用成员函数时,通过传入结构体对象的指针,找到该对象对应的函数指针数组,然后根据函数在数组中的下标找到相应的函数指针,再进行调用。同样地,由于是在运行时才进行查找和调用,因此实现了晚绑定。

7. 如何获取虚函数表以及虚函数地址

1)通过对象指针获取虚函数表指针:由于对象的第一个成员是虚函数表指针,所以可以通过对象指针访问到虚函数表指针,然后进行偏移以获取虚函数地址。

class Base {
public:
    virtual void func() {}
};

int main() {
    Base b;
    Base* p = &b;
    uintptr_t* vtablePtr = *(uintptr_t**)(&b);
    uintptr_t vfuncPtr = vtablePtr[0];
    void (*vfunc)() = (void (*)())(vfuncPtr);
    vfunc();
    return 0;
}

2)通过虚函数指针获取虚函数地址:在具体调用虚函数时,编译器会在对象内存中查找虚函数指针,然后通过虚函数指针的偏移量找到对应的虚函数地址。

class Base {
public:
    virtual void func() {}
};

int main() {
    Base b;
    void (*vfunc)() = *(void (**)())(&b);
    vfunc();
    return 0;
}

8. placement new

  placement new 是 C++ 中的一种内存管理机制,用于在已分配的一块内存上构造对象,而不是通过 new 运算符在堆上动态分配内存。placement new 可以手动管理对象的内存分配和释放,这对于一些特殊情况下需要手动管理内存的场景非常有用。
  使用 placement new 可以将对象的构造与内存分配分开,在内存已经被分配的情况下,构造对象并在指定的内存位置上返回指向该对象的指针。这使得程序员能够更加细粒度地控制内存的使用,并可以有效地避免不必要的内存分配和释放开销。

语法格式为:

void* operator new(std::size_t size, void* ptr);

其中第一个参数表示要分配的内存大小,第二个参数表示分配的内存位置。在使用 placement new 时,需要手动调用 operator new 来分配内存,并在分配的内存位置上构造对象,示例代码如下:

#include 
#include 

struct MyStruct {
    int a;
    float b;
};

int main() {
    // 分配一块内存
    void* mem = operator new(sizeof(MyStruct));

    // 在已分配的内存上构造对象
    MyStruct* p = new (mem) MyStruct{42, 3.14f};

    // 对象的成员变量可以直接访问
    std::cout << p->a << ", " << p->b << std::endl;

    // 手动调用析构函数
    p->~MyStruct();

    // 释放内存
    operator delete(mem);
    return 0;
}

9. class 的内存布局

  在C++中,每个类的对象在内存中都是按照一定的布局存储的,通常包括以下几个部分:
  1)对象的数据成员:即在类定义中声明的成员变量,按照定义顺序依次存储在内存中。
  2)虚函数表指针(vptr):如果类中有虚函数,编译器会为该类生成一个虚函数表(vtable),并在对象的内存布局中添加一个指向该表的指针,即虚函数表指针。该指针通常位于对象的内存布局的起始位置或者末尾位置。同时,如果类有虚继承,还会有一个指向虚基类表(vbtable)的指针,这个表存放了所有虚基类子对象在对象中的偏移量。
  3)对齐填充:为了满足对齐要求,编译器可能会在数据成员之间插入一些没有意义的填充字节,使得数据成员的偏移量都是对齐字节的倍数。
  4)一些编译器可能会在对象的末尾添加一些额外的信息,如 RTTI(Run-Time Type Information)等。

  因此,存储顺序一般是vptr - vbptr - values - functions。需要注意的是,由于内存对齐的原因,实际存储的顺序可能会被调整,但是总体的存储结构和原则是一致的。

注:即使某些方法在类定义时声明在变量之前,它们实际上也会被存储在变量之后。这是因为在 C++ 中,类的成员函数和变量都是存储在同一个类对象的内存空间中的,它们的存储顺序是由编译器决定的。编译器通常会按照一定的规则对成员函数和变量进行排列,以便优化内存存储和访问效率。在多数情况下,非虚函数的存储顺序会放在变量之后,但这并不是绝对的。

10. Run-Time Type Information 有什么作用

  Run-Time Type Information (RTTI)是 C++ 的一个特性,允许程序在运行时获取类型信息。RTTI 主要有两个作用:

  1)让程序在运行时识别对象的类型,可以使用 dynamic_cast 运算符进行安全的类型转换。
  2)让程序在运行时获取对象的类型信息,以便进行相应的处理。

  例如,可以使用 typeid 运算符获取一个对象的类型信息,也可以使用 dynamic_cast 运算符在类型转换时检查是否允许该转换。
  RTTI 主要涉及到两个关键字:typeid 和 dynamic_cast。其中,typeid 返回一个类型信息对象,可以与其他类型信息对象进行比较,也可以使用 type_info 对象的 name() 函数获取类型的名称。而 dynamic_cast 运算符则用于将一个指向基类对象的指针或引用转换为指向派生类对象的指针或引用。如果类型转换不安全,dynamic_cast 会返回空指针或引用。

11. 在 C++ 中显示的类型转换有哪些

  1)static_cast:用于执行非动态类型的转换,比如基本数据类型之间的转换,还可以将父类指针转换为子类指针,但是这种转换有潜在的不安全性,需要在使用时自行进行检查。

  2)dynamic_cast:用于执行动态类型转换,即基类指针向派生类指针的转换。如果该指针指向的对象类型不是所转换的类型,那么转换失败,返回一个空指针。在使用该转换时需要注意,它只能用于包含虚函数的类之间的类型转换。

  3)reinterpret_cast:用于将一个指针转换成另一个类型的指针,或者将一个整数类型的值转换成一个指针类型。这种转换非常危险,需要极其谨慎地使用,一般只用于与硬件交互或特定的系统编程。

  4)const_cast:用于去除 const 属性。通常用于函数参数传递时,因为有时候函数中需要修改传入的参数,但参数是 const 类型,这时就需要用到 const_cast。

你可能感兴趣的:(C++ 面试准备 八股(一)夯实C++的基础体系)