1. 单继承与多继承的虚函数表结构
单继承的虚函数表结构
在单继承中,每个类都有一个虚函数表(vtable),它是一个指向虚函数的指针数组。虚函数表中包含该类及其父类的所有虚函数指针。对象中会有一个指向该虚函数表的指针,称为虚表指针(vptr)。
class Base {
public:
virtual void foo() { std::cout << "Base::foo" << std::endl; }
virtual void bar() { std::cout << "Base::bar" << std::endl; }
};
class Derived : public Base {
public:
void foo() override { std::cout << "Derived::foo" << std::endl; }
};
对于 Base
类,其虚表会包含 foo
和 bar
的指针。对于 Derived
类,其虚表中 foo
指针会指向 Derived::foo
,而 bar
指针则继承自 Base
。
多继承的虚函数表结构
在多继承中,情况更复杂。如果一个类从多个基类继承,通常每个基类部分都会有自己的虚函数表,这些虚表指针通常放在类的实例对象中。
class Base1 {
public:
virtual void foo1() { std::cout << "Base1::foo1" << std::endl; }
};
class Base2 {
public:
virtual void foo2() { std::cout << "Base2::foo2" << std::endl; }
};
class Derived : public Base1, public Base2 {
public:
void foo1() override { std::cout << "Derived::foo1" << std::endl; }
void foo2() override { std::cout << "Derived::foo2" << std::endl; }
};
Derived
类会有两个虚表指针,一个对应 Base1
的虚表,另一个对应 Base2
的虚表。
2. C++ 程序编译过程
C++ 程序的编译过程包括以下几个步骤:
-
预处理(Preprocessing)
预处理器处理#include
、#define
等指令,生成预处理后的文件。此步骤替换宏、处理条件编译指令等。 -
编译(Compilation)
编译器将预处理后的代码翻译为汇编代码。这个过程会将高级语言转换为目标处理器可以理解的低级指令。 -
汇编(Assembly)
汇编器将汇编代码转换为机器码,生成目标文件(.obj
或.o
文件)。 -
链接(Linking)
链接器将多个目标文件及其依赖的库文件链接为最终的可执行文件。这一步解决符号引用,合并代码段和数据段,并最终生成一个完整的可执行程序。
3. C++ 内存管理
C++ 提供了两种主要的内存管理方式:
-
静态内存分配
编译时分配,大小和生命周期固定,如全局变量、局部静态变量。 -
动态内存分配
通过new
和delete
操作符在堆上动态分配和释放内存。动态内存分配允许在运行时决定内存的大小,但需要手动管理内存的释放,避免内存泄漏。
int* ptr = new int(5); // 动态分配内存
delete ptr; // 释放内存
4. 栈和堆的区别
-
栈(Stack)
栈是自动管理的内存区域,函数调用时自动分配和释放。栈内存管理快,但大小有限。 -
堆(Heap)
堆是用于动态内存分配的区域,内存由程序员手动管理。堆内存管理灵活但较慢,容易导致内存泄漏或碎片化。
使用场景:
栈用于局部变量、函数调用等生命周期明确的数据。堆用于需要动态分配内存的数据,如大型数组或对象。
5. 变量的区别
-
局部变量
在函数或代码块中定义,生命周期在函数或块结束时终止。 -
全局变量
在所有函数外部定义,生命周期随程序运行期始终存在,作用域为整个文件或通过extern
扩展到其他文件。 -
静态变量
局部静态变量在函数内部定义,但其生命周期贯穿整个程序运行期。全局静态变量则只能在定义它的文件中可见。 -
动态变量
通过new
分配的变量,其生命周期由程序员管理,需手动释放。
6. 全局变量定义在头文件中的问题
将全局变量定义在头文件中会导致在多个文件中重复定义同一变量,从而导致链接错误(重复定义)。解决方法是将全局变量的定义放在一个源文件中,而在其他需要使用的文件中通过 extern
声明。
// 头文件.h
extern int globalVar;
// 源文件.cpp
int globalVar = 0;
7. 内存对齐
内存对齐是为了提高内存访问效率。处理器通常要求数据在内存中的地址是某个特定倍数(如 4 或 8)的对齐地址。如果数据没有对齐,处理器可能需要多次内存访问才能读取完整数据,影响性能。
struct MyStruct {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};
在默认对齐下,b
可能会被放在 4 字节边界处,导致 MyStruct
的总大小为 8 字节而非 6 字节。
8. 什么是内存泄漏
内存泄漏是指程序运行时分配的内存未被正确释放,导致这部分内存无法再被使用,最终可能导致系统内存耗尽。常见原因包括忘记释放 new
分配的内存或资源未被正确清理。
9. 如何防止内存泄漏?内存泄漏检测工具的原理
防止内存泄漏的方法:
- 使用智能指针,如
std::unique_ptr
和std::shared_ptr
。 - 确保所有
new
的对象都有相应的delete
。 - 在析构函数中释放动态分配的资源。
内存泄漏检测工具:
- Valgrind:通过监控内存分配和释放检测泄漏。
- AddressSanitizer:编译时插入额外检查代码,运行时检测非法内存访问和泄漏。
10. 智能指针的种类及实现原理
unique_ptr
:独占所有权,不能复制,转移所有权使用std::move
。shared_ptr
:共享所有权,内部使用引用计数管理内存,引用计数为零时释放资源。weak_ptr
:不控制资源,不增加引用计数,用于解决shared_ptr
的循环引用问题。
11. 智能指针应用举例
std::unique_ptr<int> p1(new int(10));
std::shared_ptr<int> p2 = std::make_shared<int>(20);
void func() {
std::shared_ptr<int> p3 = p2; // p2, p3 共享同一内存
}
优点: 自动管理内存,防止泄漏。
缺点: 可能导致循环引用问题。
12. 如何将 unique_ptr
赋值给另一个 unique_ptr
对象
unique_ptr
不能复制,但可以通过移动语义转移所有权。
std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = std::move(p1); // p1 失去所有权,p2 获得所有权
13. 使用智能指针可能出现的问题及解决方法
循环引用:shared_ptr
可能导致循环引用,导致内存泄漏。
解决方法:使用 weak_ptr
打破循环引用。
struct A;
struct B;
struct A {
std::shared_ptr<B> b_ptr;
};
struct B {
std::weak_ptr<A> a_ptr;
};
14. 在 Visual Studio 中检测内存泄漏的方法
Visual Studio 提供内存泄漏检测功能,通过在代码中插入 _CrtDumpMemoryLeaks()
函数,程序结束时自动检测泄漏。
#define _CRTDBG
_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
int main() {
_CrtDumpMemoryLeaks();
return 0;
}
15. 深拷贝与浅拷贝
- 浅拷贝:复制对象时,仅复制指针或引用,两个对象共享同一资源。
- 深拷贝:复制对象时,同时复制指向的资源,两个对象独立存在。
class MyClass {
int* data;
public:
MyClass(int val) : data(new int(val)) {}
MyClass(const MyClass& other) : data(new int(*other.data)) {} // 深拷贝
~MyClass() { delete data; }
};
16. 虚拟内存
虚拟内存是操作系统管理内存的技术,提供一个逻辑上连续的地址空间,并将物理内存和磁盘空间结合。它允许程序使用比实际物理内存更多的内存,并隔离各程序的内存空间。
17. 语言对比
C++ 与 Java:
- 内存管理:C++ 需要手动管理内存,Java 有垃圾回收。
- 语法:C++ 支持多重继承,Java 不支持,但有接口机制。
- 性能:C++ 性能更高,因为它编译为本地代码,而 Java 运行在 JVM 上。
C++ 与 Python:
- 性能:C++ 性能优于 Python。
- 语法:Python 语法更简洁,适合快速开发。
- 内存管理:Python 有自动垃圾回收,C++ 需要手动管理内存。
18. C++ 11 新特性
- 自动类型推导:
auto
关键字。 - 智能指针:
unique_ptr
、shared_ptr
。 - lambda 表达式:简化回调和函数对象的使用。
- 右值引用和移动语义:提高性能,避免不必要的拷贝。
19. C 和 C++ 的区别
- 面向对象:C 是面向过程的语言,C++ 支持面向对象编程。
- 标准库:C++ 提供更丰富的标准库,如 STL。
- 类型检查:C++ 类型检查更严格。
20. Python 和 C++ 的区别
- 语法:Python 更简洁,适合快速开发;C++ 语法复杂,但灵活性更高。
- 性能:C++ 性能更优,Python 适合编写脚本和快速原型。
- 内存管理:C++ 手动管理内存,Python 自动垃圾回收。
21. 面向对象
面向对象编程(OOP)是一种编程范式,通过类和对象封装数据和操作。其优势在于提高代码的可复用性、可扩展性和可维护性。
22. 面向对象的三大特性
- 封装:将数据和操作封装在类中,隐藏内部实现。
- 继承:通过继承复用代码,创建层次结构。
- 多态:允许不同类型对象通过同一接口进行操作,分为静态多态和动态多态。
23. 重载、重写、隐藏的区别
- 重载:同一作用域内,函数名称相同,但参数不同。
- 重写:子类重写父类的虚函数,实现不同的功能。
- 隐藏:子类定义了与父类同名但不同参数的函数,隐藏父类同名函数。
24. 如何理解 C++ 是面向对象编程
C++ 通过类、继承和多态等特性支持面向对象编程,允许程序员创建和操作对象,并利用封装、继承和多态实现代码复用和扩展。
25. 什么是多态?多态如何实现?
多态是指不同对象可以通过同一接口进行操作。C++ 中多态通过虚函数实现,允许子类重写父类的虚函数,从而在运行时决定调用哪个函数。
26. 静态多态与动态多态
- 静态多态:编译时决定调用哪个函数,如函数重载、模板。
- 动态多态:运行时决定调用哪个函数,通过虚函数实现。
27. 类相关
类是 C++ 中的基本构造,包含数据成员和成员函数。构造函数用于初始化对象,析构函数用于清理资源。
28. 什么是虚函数?什么是纯虚函数?
- 虚函数:基类中通过
virtual
关键字声明,允许子类重写。 - 纯虚函数:在基类中定义为纯虚函数,要求子类必须实现,表示类是抽象类。
29. 虚函数与纯虚函数的区别
虚函数可以有默认实现,而纯虚函数没有实现,必须由子类实现。纯虚函数使得类成为抽象类,不能实例化。
30. 虚函数的实现机制
虚函数通过虚函数表(vtable)实现。每个含有虚函数的类都有一个虚函数表,表中存储了该类及其继承链上所有虚函数的地址。对象中存储一个指向该表的指针(vptr),运行时通过这个指针找到并调用正确的函数。