1.类对象的大小由哪些因素决定?

  • 空类或无数据成员类的默认大小:如果一个类不包含任何数据成员(包括继承来的数据成员),根据C++标准,即使是空类也会被分配至少1个字节的空间。这是为了确保每个对象都有唯一的地址。

  • 虚函数的影响(虚函数指针):当一个类包含虚函数时,编译器会为该类添加一个指向虚函数表(vtable)的指针,称为vptr。这个指针是每个对象的一部分,因此它增加了对象的大小。指针的大小取决于系统架构(例如,在32位系统上是4字节,在64位系统上是8字节)。

  • 虚继承的影响:当使用虚继承来解决多重继承下的菱形继承问题时,每个对象需要额外的指针来维护到虚基类子对象的引用,这被称为虚基表指针(vbptr)。这样的设计避免了基类部分的重复,但同时也增加了每个对象的大小。

  • 内存对齐:类对象的大小计算遵循结构体内存对齐的原则。这意味着编译器会在数据成员之间或者末尾插入填充字节,以满足特定硬件平台上的对齐要求。这种对齐可以提高访问速度,但同时可能增加对象的实际大小。

注:静态数据成员和成员函数不影响:无论是静态还是非静态成员函数,它们都不会影响类对象的大小。这是因为成员函数实际上是存储在代码段中的,所有对象共享同一份函数代码副本。同样,静态数据成员属于整个类,而非单个对象,因此它们也不会增加对象的大小。

2.虚基表指针是什么?有什么作用?

虚基表指针:编译器会为每个包含虚基类的类插入一个虚基表指针(vbptr),该指针指向一个虚基表(vbtable)。这个表包含了访问虚基类子对象所需的信息。

虚基表结构

  • 虚基类子对象相对于当前对象的偏移量
  • 当前对象相对于虚基表指针的偏移量

虚基表指针的作用:

虚基表指针的主要作用是在虚继承情况下,正确找到共享的虚基类子对象(虚基类子对象相对于当前对象的偏移量)

3.什么是内存对齐?内存对齐有什么作用?

内存对齐:要求数据的存储地址必须是某个特定值的整数倍。

eg:

1
2
3
4
5
6
struct Example1 {
char a; // 1字节(地址0)
int b; // 4字节(必须对齐到4,地址4)
double c; // 8字节(必须对齐到8,地址8)
};
// 大小:1 + 3(填充) + 4 + 8 = 16字节

为什么要内存对齐?

加快硬件访问效率,现代CPU通常以固定大小的块(如4字节、8字节)读取内存。如果数据未对齐,CPU可能需要多次访问内存才能读取完整数据。

4.std::move的作用和使用场景?

std::move: 将一个对象转换为右值引用,从而启用移动语义。

使用场景:如果类实现了移动构造函数移动赋值运算符,可以结合 std::move 使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <utility>
class MyClass {
public:
int* data;
// 构造函数
MyClass(int value) : data(new int(value)) {}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete data; // 释放当前资源
data = other.data; // 窃取资源
other.data = nullptr;// 将原对象置空
}
return *this;
}

~MyClass() {
delete data; // 释放资源
}
};
int main() {
MyClass obj1(42);
MyClass obj2 = std::move(obj1); // 使用 std::move 调用移动构造函数
if (obj1.data == nullptr) {
std::cout << "obj1's resource has been moved!" << std::endl;
}
std::cout << "obj2's data: " << *(obj2.data) << std::endl; // 输出 42
return 0;
}

5.std::forward的作用和使用场景?

std::forward: 主要作用是实现完美转发,即将函数模板的参数原封不动地转发给另一个函数,同时保留其值类别(lvalue 或 rvalue)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <utility>
void process(int& x) { std::cout << "Lvalue reference" << std::endl; }
void process(int&& x) { std::cout << "Rvalue reference" << std::endl; }
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // 根据 arg 的值类别转发
}
int main() {
int x = 42;
wrapper(x); // 左值,调用 process(int&)
wrapper(42); // 右值,调用 process(int&&)
return 0;
}

注:T&&是万能引用,既能接收左值又能接收右值。

6.构造函数和析构函数能抛出异常吗?

构造函数可以抛出异常,但可能会造成内存泄露。

  • 如果构造函数抛出异常,对象被视为未完全构造,析构函数不会被调用,可能导致资源泄漏。使用智能指针(如 std::unique_ptr)可以有效避免资源泄漏问题。

析构函数不能、也不应该抛出异常。

  • 如果析构函数抛出异常,则异常点之后的程序不会被执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会被执行,会造成诸如资源泄露的问题。
  • 通常异常发生时,C++机制会调用已经构造对象的析构函数来释放资源,此时如析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

7.左值和右值的区别?

左值有明确的内存地址、能够被引用和修改。

右值是指临时的、没有明确的内存地址的对象。

image-20250417150545277

8.类中默认构造函数有几个?

八个:

  • 默认构造函数
  • 默认拷贝构造函数
  • 默认析构函数
  • 默认拷贝赋值运算符
  • 默认移动构造函数
  • 默认移动赋值运算符
  • 默认取址运算符
  • 默认取址运算符 const 版本