355. C++生成子类需要虚析构么?
是的。如果你的C++类设计为基类,并且你打算通过基类指针来删除派生类的对象,那么你应该为基类提供一个虚析构函数。这样做可以确保在删除基类指针时,派生类的析构函数会被正确调用,从而避免资源泄露或其他析构相关的问题。
356. C++虚函数底层是如何实现的?
C++中虚函数的底层实现通常是通过虚函数表(虚表,vtable)实现的。每个包含虚函数的类都有一个虚表,此表是一个包含指向类的虚函数的函数指针的数组。每个对象都包含了一个指针(虚指针,vptr),指向基类的虚表。当调用虚函数时,实际上是通过对象的虚指针查找虚表,然后通过虚表调用对应的函数实现的,这允许在运行时进行多态行为。
357. 虚表是存放在哪里的?
虚表(vtable)通常存放在程序的只读数据段(.rodata section)中,这是因为虚表包含的函数地址不会在运行时改变。每个对象的虚指针(vptr)指向基类的虚表,确保正确调用对应类的虚函数。
358. 什么是结构体对齐?结构体对齐的规则?怎么判断结构体大小?
结构体对齐是指在结构体中,每个成员的起始地址相对于结构体起始地址的偏移量是该成员大小的整数倍,这样做是为了满足某些平台对数据存储对齐的要求,提高内存的访问效率。
结构体对齐规则通常遵循以下原则:
- 结构体的起始地址能够被其最宽基本类型成员的大小所整除。
- 结构体每个成员相对于结构体起始地址的偏移量应是该成员大小的整数倍,不是的话会进行填充。
- 结构体的总大小为其最宽基本类型成员大小的整数倍。
判断结构体大小的方法:
- 确定每个成员的大小和对齐要求。
- 根据成员声明顺序,为每个成员分配地址空间,必要时进行填充。
- 按最大成员对齐要求在结构体末尾可能添加填充。
- 结构体总大小就是最后一个成员末尾地址加上末尾填充(如果有的话)。
使用sizeof(结构体类型名)表达式可以直接得到结构体的大小。
解释:
这里不知道你有没有疑问,我开始的时候有个疑问:“结构体每个成员相对于结构体起始地址的偏移量应是该成员大小的整数倍,不是的话会进行填充”这句话,不应该是最宽基本类型成员大小的整数倍吗?
然后我试了一下,发现不是这样的。
解释
每个成员的对齐要求是基于它自身的大小。例如:
char
通常对齐到 1 字节。int
通常对齐到 4 字节。double
通常对齐到 8 字节。
对齐要求是指每个成员的起始地址必须是它对齐大小的整数倍。
示例
考虑下面的结构体:
struct Example {
char a; // 对齐要求是 1 字节
int b; // 对齐要求是 4 字节
char c; // 对齐要求是 1 字节
};
计算偏移量和填充
-
char a
:- 大小:1 字节
- 对齐要求:1 字节
- 偏移量:0
- 不需要填充
-
int b
:- 大小:4 字节
- 对齐要求:4 字节
- 当前偏移量:1(因为
char a
占据了 1 字节) - 为了满足
int
的 4 字节对齐要求,需要在char a
后添加 3 个填充字节 - 新的偏移量:4
int b
放置在偏移量 4
-
char c
:- 大小:1 字节
- 对齐要求:1 字节
- 当前偏移量:8(因为
int b
占据了 4 字节) char c
放置在偏移量 8
总大小
为了满足结构体对齐规则,结构体的总大小需要是其最宽基本类型成员大小(即 int
的 4 字节)的整数倍。当前结构体的大小是 9 字节(1 字节 char a
+ 3 字节填充 + 4 字节 int b
+ 1 字节 char c
)。需要再添加 3 个填充字节使总大小成为 12 字节。
总结
- 每个成员相对于结构体起始地址的偏移量应是该成员对齐要求的整数倍,这与结构体中最宽基本类型成员的大小无关。
- 如果当前偏移量不满足该成员的对齐要求,就需要添加填充字节。
- 结构体的总大小需要是结构体中最宽基本类型成员的大小的整数倍。
struct Example {
char a; // 偏移量 0
// 3 字节填充
int b; // 偏移量 4
char c; // 偏移量 8
// 3 字节填充
};
最终总大小:12字节
359. 线程间怎么同步?同步是解决什么问题?
主要的目的是为了防止多个线程同时操作同一片内存空间,导致数据的不一致。
线程间的同步可以通过多种机制实现,包括但不限于以下几种:
- 互斥锁(Mutex):当一个线程使用互斥锁保护的资源时,其他需要这些资源的线程将被阻塞,直到该线程释放资源。
- 信号量(Semaphore):他是一个计数器,用来保护一个或者多个相同的资源。当没有可用资源时,需要这些资源的线程将被阻塞。
- 条件变量(Condition Variables):它可以用来让一个线程等待某个条件成立,而不是忙等。
- Event/Message Queue:线程通过发送和接收事件或消息进行通信。接收线程将被阻塞,直到有事件或消息可接收。
360. 如何避免死锁?死锁的条件是什么?
避免死锁的一些常见策略包括:
- 破坏互斥条件:尽可能减少对资源的互斥访问。
- 破环请求并等待条件:一次性申请所有资源,而不是分步申请。
- 破坏不可剥夺条件:使资源可以被抢占,当某个线程需要资源时,已经分配的资源可以被回收。
- 破坏循环等待条件:对资源请求进行排序,按照一定顺序获取资源。
死锁发生的条件有四个,通常被称为死锁的必要条件:
-
互斥:资源不能被共享,只能由一个线程同时使用。
-
持有和等待:线程已经持有至少一个资源,但又提出了新的资源请求,而该资源被其他线程持有。
-
不可抢占:线程持有的资源在未完成其任务前,不能被其他线程抢占。
-
循环等待:存在一种线程资源的循环等待关系。
361. vector底层是什么数据结构?
vector底层是使用连续内存空间来实现的动态数组。
362. vector空间不够了底层会怎么做?
当vector空间不够时,他的底层实现会分配一个更大的连续内存块,将现有的元素复制到新的内存块中,然后释放原来的内存块,并更新内部状态以反映新的容量。
363. vector resize是怎么做的?
vector的resize操作主要完成以下步骤:
- 判断新的大小是否大于当前的容量,如果是,则需要额外分配内存空间。
- 如果新的大小比当前元素数量少,将会销毁多余的元素。
- 如果新的大小比当前元素数量多,将会在容器尾部创建新的元素。
- 更新内部的元素数量为新的大小。
364. C++里面的成员函数和普通函数有什么区别?
主要区别如下:
- 成员函数是类的组成部分,他们可以访问类的私有、保护和公共成员。而普通函数不是类的一部分,不可以直接访问类的私有和保护成员。
- 成员函数在调用时需要使用对象或者指向对象的指针或引用来调用,而普通函数则不需要。
- 成员函数可以被声明为虚函数,形成多态性。而普通函数不能。
- 成员函数可以被重载,但是不能被重定义。
365. C++成员函数是怎么访问到成员变量的?
C++中,成员函数可以直接访问属于相同类的成员变量,因为他们在调用时隐含的接收一个指向调用对象的指针(通常命名为this)。this指针提供了对调用对象成员变量的直接访问方式。
解释:
成员函数和普通函数的重载和重定义
重载(Overloading)
重载指的是在同一个作用域内,函数名相同但参数列表不同的多个函数。重载函数可以具有不同的参数类型或数量,返回类型可以相同或不同。重载主要通过参数列表来区分不同的函数。
- 成员函数重载:
class MyClass {
public:
void func(int a) {}
void func(double a) {}
};
普通函数重载:
void func(int a) {}
void func(double a) {}
重定义(Redefinition)
重定义通常指的是在同一个作用域内定义两个完全相同的函数,包括函数名、参数列表和返回类型。C++ 中不允许这种情况。
例如,以下代码会导致编译错误:
void func(int a) {}
void func(int a) {} // 错误,函数重定义
成员函数与普通函数的重定义和重载
- 成员函数可以被重载,但不能被重定义。
- 普通函数可以被重载,但不能被重定义。
这两点的含义如下:
-
重载:无论是成员函数还是普通函数,都可以重载,即在同一个作用域内定义多个具有相同名称但参数列表不同的函数。
-
重定义:无论是成员函数还是普通函数,都不能重定义,即不能在同一个作用域内定义两个完全相同的函数。
特别注意
需要注意的是,成员函数的重定义与普通函数的重定义在继承和作用域中有不同的处理方式:
- 成员函数重定义: 在继承关系中,派生类可以重定义基类的成员函数。这个过程叫做函数覆盖(overriding),它与函数重定义的概念不同。在覆盖中,派生类重新定义了基类的虚函数。
class Base {
public:
virtual void func() {}
};
class Derived : public Base {
public:
void func() override {} // 覆盖基类的虚函数
};
- 普通函数重定义: 普通函数没有类似成员函数覆盖的机制。在同一作用域内定义两个完全相同的普通函数会导致编译错误。
总结:
- 成员函数和普通函数都可以被重载。
- 在同一作用域内,成员函数和普通函数都不能被重定义。
- 在继承关系中,派生类可以覆盖基类的虚函数。
366. unity脚本如何与引擎绑定的?
在Unity中,脚本与具体的游戏对象(引擎实体)绑定的方式一般如下:
- 首先,创建一个脚本。这可以通过Unity编辑器的“Assets”菜单,选择“Create”-> “C# Script”
- 接着,将这个脚本拖放到游戏对象上,或者在游戏对象被选中的情况下,直接在检查器窗口点击“Add Component”,然后选择你的脚本。
这样,你的脚本就与指定的游戏对象绑定了。然后脚本内的函数就可以通过this.gameObject来访问和控制绑定的游戏对象。
367. 讲一下 noexcept如果出现了异常会怎么办?
如果在声明为noexcept的函数中出现了异常,程序会调用 std::terminate()来立即终止程序执行。
解释:
在C++中,noexcept
是一个关键字,用于指示一个函数不会抛出任何异常。其主要作用是帮助编译器优化代码,并且在某些情况下可以提高程序的性能和稳定性。你提到的std::terminate
是C++标准库中的一个函数,用于在程序遇到无法处理的异常或其他致命错误时立即终止程序的执行。
详细解释
1. noexcept
关键字
当你声明一个函数为noexcept
时,你告诉编译器和程序的使用者,这个函数在任何情况下都不会抛出异常。例如:
void myFunction() noexcept {
// Function implementation
}
2. 异常安全性检查
如果一个函数被声明为noexcept
,但在函数体内抛出了异常,程序的行为将会非常明确——它会立即调用std::terminate
函数来终止程序。这是因为抛出异常的行为与noexcept
的承诺相违背。
3. std::terminate
函数
std::terminate
是C++标准库中的一个函数。当程序遇到以下情况之一时,会调用std::terminate
:
- 一个未捕获的异常到达了程序的顶层(即,没有任何
catch
块可以处理它)。 - 一个构造函数或析构函数在处理异常时抛出了另一个异常。
- 在一个
noexcept
函数中抛出了异常。
默认情况下,std::terminate
会调用std::abort
,导致程序立即终止,不会进行任何清理操作,也不会调用任何析构函数或atexit
注册的函数。
示例
以下是一个简单示例,展示了在noexcept
函数中抛出异常时会发生什么:
#include <iostream>
#include <stdexcept>
void myFunction() noexcept {
throw std::runtime_error("Exception in noexcept function");
}
int main() {
try {
myFunction();
} catch (const std::exception& e) {
std::cout << "Caught exception: " << e.what() << std::endl;
}
std::cout << "This line will not be executed." << std::endl;
return 0;
}
在这个例子中,myFunction
被声明为noexcept
,但它抛出了一个std::runtime_error
异常。因为这是在一个noexcept
函数中发生的异常,程序会立即调用std::terminate
,而不会执行main
函数中的catch
块。
总结
noexcept
声明:用于指示函数不会抛出异常。- 违反
noexcept
:如果在noexcept
函数中抛出异常,程序会调用std::terminate
。 std::terminate
行为:std::terminate
会立即终止程序的执行,不进行任何清理。
通过使用noexcept
,程序员可以在编译时和运行时获得更好的异常安全性保证,同时也能帮助编译器进行优化。但需要谨慎使用,确保在noexcept
函数中确实不会有任何异常抛出。
368. vector中emplace back是什么?
emplace_back是一种方法,用于在向量尾部直接构造和添加一个新元素,而无需额外的复制或移动操作。他接受的参数是将要添加的元素的构造函数参数,而不是已构造的元素本身。这个方法可以提高效率,因为省去了不必要的构造和析构过程。
369. 右值引用是怎么样的?如果没写右值引用的函数呢?
右值引用是C++引入的一种引用类型用于绑定到临时对象(右值)。使用右值引用可以避免不必要的对象拷贝,从而提高效率。
如果某个函数未提供右值引用版本,当需要传递临时对象作为参数时,会按照传统的拷贝或移动的方式处理,这可能会导致额外的拷贝开销。右值引用通过允许直接修改临时对象(通过移动语义),减少了这种不必要的拷贝,优化了性能。
370. 在写webserver的时候,socket编程是阻塞的还是非阻塞的,边缘触发和水平触发的区别是什么?
在编写webserver的时候,socket编程可以是阻塞的,也可以是非阻塞的,具体取决于你的需求和设计。阻塞socket会在没有数据可读的情况或者无法立即写入数据时使调用的线程阻塞,而非阻塞socket则不会。
边缘触发和水平触发是IO多路复用中的概念。水平触发(LT)模式下,只要socket处于可读或可写状态,无论是否有新的IO事件,都会触发通知。而边缘触发(ET)模式下,只有在状态发生改变时才会触发通知,比如从不可读写变为可读写时。