unity客户端面试高频2(自用未完持续更新)
标题是我
- 1.构造函数为什么不能为虚函数?析构函数为什么要虚函数?
- 2.C++智能指针
- 3.左值和右值
- 完美转发
- 4.深拷贝与浅拷贝
- 5.malloc VS new 你们知道吗
- 6.C++虚函数多态虚函数指针虚函数表
- 7.点乘和叉乘
- 点乘:
- 叉乘:
- 8.异步编程、Task、Async、协程
- 9.协程底层原理
- 10.事件跟委托的区别、Func、Action
- 11.资源动态加载了解吗?
- Resource
- 异步
- 同步
- AssetBundle
- 异步
- 同步
- 12.UI界面点击Button无效,会有多少种情况?
1.构造函数为什么不能为虚函数?析构函数为什么要虚函数?
构造函数不能定义为虚函数的原因是因为虚函数是用于实现动态多态性的机制,而构造函数的调用是在对象创建的过程中完成的。在对象创建时,其类型是已知的,不需要通过动态绑定来确定调用哪个构造函数。因此,构造函数不需要被定义为虚函数。
析构函数需要定义为虚函数的主要原因是为了在使用基类指针指向派生类对象并通过该指针删除对象时,可以正确地调用派生类对象的析构函数,以防止内存泄漏。如果不将析构函数定义为虚函数,当使用基类指针指向派生类对象并通过该指针删除对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致派生类对象的资源无法正确释放,造成内存泄漏。
2.C++智能指针
图片来源程序员陈子青
具体的底层实现
计数器类
template <typename T>
class RefCount {
public:
T* ptr; // 指向实际管理的对象
size_t count; // 引用计数
RefCount(T* p) : ptr(p), count(1) {}
~RefCount() {
delete ptr; // 当引用计数为0时,释放管理的对象
}
};
share_ptr类
template <typename T>
class MySharedPtr {
private:
RefCount<T>* ref; // 指向引用计数管理对象
public:
// 构造函数,初始化指向对象并设置引用计数为1
MySharedPtr(T* p = nullptr) {
if (p) {
ref = new RefCount<T>(p);
} else {
ref = nullptr;
}
}
// 拷贝构造函数
MySharedPtr(const MySharedPtr& other) {
ref = other.ref;
if (ref) {
++(ref->count); // 增加引用计数
}
}
// 移动构造函数
MySharedPtr(MySharedPtr&& other) noexcept {
ref = other.ref;
other.ref = nullptr; // 转移所有权后,原对象的引用计数指针置空
}
// 赋值运算符重载
MySharedPtr& operator=(const MySharedPtr& other) {
if (this != &other) {
release(); // 先释放当前对象的资源(若有)
ref = other.ref;
if (ref) {
++(ref->count); // 增加引用计数
}
}
return *this;
}
// 移动赋值运算符重载
MySharedPtr& operator=(MySharedPtr&& other) noexcept {
if (this != &other) {
release();
ref = other.ref;
other.ref = nullptr;
}
return *this;
}
// 获取管理的对象指针
T* get() const {
return ref? ref->ptr : nullptr;
}
// 解引用操作
T& operator*() const {
return *(ref->ptr);
}
// 箭头操作符,用于访问对象成员
T* operator->() const {
return ref->ptr;
}
// 获取引用计数
size_t use_count() const {
return ref? ref->count : 0;
}
// 析构函数
~MySharedPtr() {
release();
}
private:
// 释放资源的辅助函数
void release() {
if (ref) {
--(ref->count);
if (ref->count == 0) {
delete ref; // 引用计数为0时,删除引用计数管理对象
}
}
}
};
3.左值和右值
-
左值(lvalue):左值指的是可以出现在赋值运算符左边的表达式。它代表一个具名的、有确定内存地址的对象,其生命周期相对较长,在表达式结束后依然存在。
-
右值(rvalue):右值指的是只能出现在赋值运算符右边的表达式。它代表临时对象、字面量或即将销毁的对象,其生命周期较短,在表达式结束后就会被销毁。
右值引用会给右值一个临时的地址,并且延长它的生命周期。
左值作为参数的函数只可以接受左值,强调不要复制。void print(int& a);
右值作为参数的函数传入只能接受右值,可以修改右值。void print(int&& a);
左值作为参数传入很常见,避免复制影响空间内存和性能。
那为什么要用右值引用呢?
- 右值引用可以避免深拷贝
假设A是一个类实例,类内有拷贝构造函数。
如果使用ClassName B = A;那么会复制一份A的内容到B上,堆内就有两份相同的数据了。
然而如果使用ClassName&& B = move(A);就只有一份数据,将A的所有权转移给B,避免了深拷贝。前提是类中必须有移动构造函数。为什么不用左值引用?
只是为了转移资源,避免拷贝,把原来的A指向空,B把A的内容偷过来了,后续的修改也不会影响A的内容,这使得A能被重新赋值。
右值引用在实现移动语义时,通常能在不影响原变量(更准确地说是源对象)的前提下(除了初次的使它置空),在不拷贝的情况下进行资源转移。
#include <iostream>
class MyArray {
private:
int* data;
size_t size;
public:
MyArray(size_t s) : size(s) {
data = new int[s];
for (size_t i = 0; i < s; ++i) {
data[i] = i;
}
}
// 拷贝构造函数
MyArray(const MyArray& other) : size(other.size) {
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i];
}
}
// 移动构造函数
MyArray(MyArray&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
~MyArray() {
delete[] data;
}
};
完美转发
通过forward(arg)这个API能让函数传入的是左值或者右值的属性保留传递下去。
去掉forward直接传递arg的话会导致,第一次传入forwardExample这个函数时,在这函数生命周期中是右值,然而第二次传递进去的是一个arg的参数会被识别为左值。
图片来源程序员陈子青
4.深拷贝与浅拷贝
C++默认的拷贝构造函数就是浅拷贝,只是将新的指针指向同一块内存地址,这会导致某一个指针被释放的时候,另外一个成为野指针。
深拷贝需要重写,会在堆上分配一份新的内容并且将指针指向这个内容的地址。
而C++11后的智能指针shared_ptr可以完美解决这个问题,通过引用计数器的方式判断是否需要释放指向的空间。
5.malloc VS new 你们知道吗
malloc菜,不会自动分配内存,要自己sizeof分配,不知道类型,要自己转换。
new强,会自己分配内存,自己类型转换。
malloc不安全,new更安全。
free不会调用析构函数,delete会调用析构函数。
6.C++虚函数多态虚函数指针虚函数表
父类有虚函数,子类继承它并且对它进行了覆写。
父类的指针,指向某一个类(包括自己和子类),虚函数指针会根据每个类内的虚函数表来确定调用的到底是哪个函数。
多个同类型的实例指向的是同一个虚函数表,对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数。
它们的虚函数表的地址指向详细看这篇文章。
我是下面的文章
7.点乘和叉乘
点乘:
三维点乘类似
叉乘:
公式及其几何意义
8.异步编程、Task、Async、协程
Async:异步任务声明关键字
await:交出目前所在的Async方法的控制权防止线程阻塞。
Task《T》:声明一个异步方法所需的返回值,也就是异步函数只有void 和 Task《T》和 Task三种返回值。
Task的定义决定了异步方法的返回值。
void返回一般用在事件处理程序(按钮,窗口加载等自动调用的方法)
协程: StartCoroutine() IEnmurator
using System;
using System.Threading.Tasks;
using UnityEngine;
public class TaskExample : MonoBehaviour
{
private async void Start()
{
Debug.Log("开始异步任务");
// 创建并启动一个 Task
Task<int> task = Task.Run(() =>
{
// 模拟一个耗时操作
System.Threading.Thread.Sleep(2000);
return 42;
});
// 等待 Task 完成并获取结果
int result = await task;
Debug.Log($"异步任务完成,结果是: {result}");
}
}
async关键字声明一个异步方法。
Task声明一个任务,使用.Run(接收一个方法,类似委托那样)运行这个任务.
await等待task结束继续往下。
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
public class NetworkAsyncExample : MonoBehaviour
{
private async void Start()
{
try
{
string result = await FetchDataAsync("https://jsonplaceholder.typicode.com/todos/1");
Debug.Log(result);
}
catch (Exception e)
{
Debug.LogError($"请求出错: {e.Message}");
}
}
private async Task<string> FetchDataAsync(string url)
{
using (UnityWebRequest webRequest = UnityWebRequest.Get(url))
{
// 发送请求并等待完成
var asyncOperation = webRequest.SendWebRequest();
while (!asyncOperation.isDone)
{
await Task.Yield();
}
if (webRequest.result == UnityWebRequest.Result.Success)
{
return webRequest.downloadHandler.text;
}
else
{
throw new Exception($"请求失败: {webRequest.error}");
}
}
}
}
异步网络请求
await 交出当前的控制权并且,等待一个返回值为string的task任务结束,并将结果返回给result。
9.协程底层原理
10.事件跟委托的区别、Func、Action
11.资源动态加载了解吗?
Resource
异步
同步
AssetBundle
异步
同步
12.UI界面点击Button无效,会有多少种情况?
- OnClick中没有订阅函数
- EventSystem缺失
- 按钮组件有 Interactable(是否可交互)属性,该属性被设置为 false
- 如果有其他 UI 元素(如透明的 Image 或者 Panel)覆盖在按钮之上,点击操作可能被上层元素拦截,导致按钮无法响应
- Unity 的 UI 交互依赖射线检测。若射线检测出现问题,按钮可能接收不到点击事件。比如 Canvas 的 Graphic Raycaster(UI专用的射线检测) 组件被禁用或者配置有误,就会影响射线检测。