好多小伙伴问游戏后端现在都是C++吗?可以说现在大部分还是C++,虽然也有Golang,但是综合考虑起来,还得是C++。
这几年Java都卷成麻花了,在网上也看到好多帖子劝退Java。怎么说呢?各有特点吧,各有自己擅长的领域,也看个人的选择和爱好。
粉丝福利, 免费领取C/C++ 开发学习资料包、技术视频/项目代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发/音视频开发/Qt开发/游戏开发/Linuxn内核等进阶学习资料和最佳学习路线)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
给一个示例代码,静态成员函数的使用:
#include using namespace std; class MyClass { public: // 静态成员变量 static int staticVar; // 静态成员函数 static void staticFunction() { cout << "Static Function Called. staticVar = " << staticVar << endl; // 不能访问非静态成员变量或非静态成员函数 // nonStaticFunction(); // 错误 } // 非静态成员函数 void nonStaticFunction() { cout << "Non-Static Function Called." << endl; } }; // 静态成员变量的初始化 int MyClass::staticVar = 0; int main() { // 通过类名调用静态成员函数 MyClass::staticFunction(); // 修改静态成员变量 MyClass::staticVar = 10; // 通过类名再次调用静态成员函数 MyClass::staticFunction(); // 创建对象实例 MyClass obj; // 通过对象实例调用静态成员函数(不推荐) obj.staticFunction(); return 0; }
输出:
Static Function Called. staticVar = 0 Static Function Called. staticVar = 10 Static Function Called. staticVar = 10
虚函数表是一个函数指针数组,每个类都有一个虚函数表,用来存储该类的虚函数的地址。当一个类定义了虚函数时,编译器会为该类生成一个虚函数表。
每个含有虚函数的类的对象中,编译器会自动添加一个虚函数指针(vptr),该指针指向该类的虚函数表。通过这个指针,对象可以在运行时找到正确的函数实现,从而实现动态绑定。
虚函数的动态绑定是通过虚函数表和虚函数指针在运行时实现的。当调用虚函数时,程序会根据对象的vptr找到正确的函数地址,从而实现动态绑定。这使得C++可以在运行时根据实际对象类型调用相应的函数,实现多态性。
在单一继承的情况下,每个包含虚函数的类的对象会有一个vptr,这个vptr指向该类的虚函数表(vtable)。
class Base { public: virtual void show() { cout << "Base class show function" << endl; } virtual ~Base() = default; }; class Derived : public Base { public: void show() override { cout << "Derived class show function" << endl; } };
上述例子中,无论是Base类还是Derived类的对象,都会有一个vptr指向它们各自的虚函数表。
在多重继承的情况下,每个对象可能会有多个虚函数指针。这是因为每个基类都有自己的虚函数表,而每个继承自多个基类的类的对象必须维护多个虚函数指针,以指向各个基类的虚函数表。
class Base1 { public: virtual void show1() { cout << "Base1 show1 function" << endl; } virtual ~Base1() = default; }; class Base2 { public: virtual void show2() { cout << "Base2 show2 function" << endl; } virtual ~Base2() = default; }; class Derived : public Base1, public Base2 { public: void show1() override { cout << "Derived show1 function" << endl; } void show2() override { cout << "Derived show2 function" << endl; } };
上述例子中,Derived类从Base1和Base2继承而来。因此,Derived类的对象将包含两个vptr,一个指向Base1的虚函数表,另一个指向Base2的虚函数表。
对于单一继承,一个类只有一个基类,因此对象只需一个vptr。
Base* ptr = new Derived(); ptr->show(); // 调用Derived的show函数
对于多重继承,一个类有多个基类,每个基类都有自己的虚函数表,因此对象需要多个vptr。
Derived* d = new Derived(); Base1* b1 = d; Base2* b2 = d; b1->show1(); // 调用Derived的show1函数 b2->show2(); // 调用Derived的show2函数
这种情况下,d对象有两个vptr,一个指向Base1的虚函数表,另一个指向Base2的虚函数表。
友元函数是一种非成员函数,但它可以访问类的私有成员和保护成员。友元函数是通过在类内声明friend关键字来指定的。友元函数的主要目的是为了提供一种机制,使非成员函数可以访问类的私有数据。
class MyClass { private: int data; public: MyClass(int value) : data(value) {} // 声明友元函数 friend void showData(const MyClass& obj); }; // 友元函数定义 void showData(const MyClass& obj) { cout << "Data: " << obj.data << endl; } int main() { MyClass obj(10); showData(obj); // 调用友元函数,输出 "Data: 10" return 0; }
在这个例子中,showData函数是MyClass类的友元函数,它能够访问MyClass对象的私有成员data。
虚函数是类的成员函数,通过在基类中使用virtual关键字声明。虚函数的主要目的是实现多态性,使得通过基类指针或引用调用派生类的重写函数。
class Base { public: virtual void show() { cout << "Base class show function" << endl; } virtual ~Base() = default; }; class Derived : public Base { public: void show() override { cout << "Derived class show function" << endl; } }; int main() { Base* bptr = new Derived(); bptr->show(); // 输出 "Derived class show function" delete bptr; return 0; }
在这个例子中,通过基类指针bptr调用虚函数show,实现了运行时的多态性。
内联函数是一种提示编译器将函数调用展开为函数体内的代码,以避免函数调用的开销。内联函数通过在函数定义前加上inline关键字来声明。内联函数通常用于短小、频繁调用的函数。
虚函数是用于实现多态性的成员函数,允许通过基类指针或引用调用派生类的重写函数。虚函数通过在基类中使用virtual关键字声明。
内联函数可以是虚函数,但在实际使用中存在一些限制和考虑。以下是详细的解释:
#include class Base { public: virtual void show() { std::cout << "Base class show function" << std::endl; } }; class Derived : public Base { public: inline void show() override { std::cout << "Derived class show function" << std::endl; } }; int main() { Derived d; d.show(); // 可能被内联展开 Base* bptr = &d; bptr->show(); // 不会被内联展开,因为是通过基类指针调用 return 0; }
模板函数是一种函数,它允许我们在编写代码时使用泛型类型。模板函数在编译时会根据提供的具体类型生成具体的函数实例。
template void show(T value) { std::cout << value << std::endl; }
虚函数是用于实现运行时多态性的成员函数,允许基类指针或引用调用派生类的重写函数。虚函数依赖于虚函数表(vtable)和虚函数指针(vptr)来在运行时动态绑定函数调用。
class Base { public: virtual void show() { std::cout << "Base class show function" << std::endl; } }; class Derived : public Base { public: void show() override { std::cout << "Derived class show function" << std::endl; } };
模板函数的实现是通过模板实例化完成的。当我们定义一个模板函数时,并不会立即生成函数代码。只有在使用该模板函数时(即实例化模板函数时),编译器才会生成对应的具体函数代码。
template void show(T value) { std::cout << value << std::endl; }
当我们调用这个模板函数时,
show(10); // T 实例化为 int show(3.14); // T 实例化为 double show("Hello"); // T 实例化为 const char*
编译器会生成以下具体的函数实例:
void show(int value) { std::cout << value << std::endl; } void show(double value) { std::cout << value << std::endl; } void show(const char* value) { std::cout << value << std::endl; }
模板函数通过模板实例化实现编译时多态性,这意味着在编译期生成不同类型的函数实例。编译时多态性主要依赖于编译期的类型推导和模板实例化机制。
template T add(T a, T b) { return a + b; } int main() { int x = add(1, 2); // 生成 add(int, int) double y = add(1.1, 2.2); // 生成 add(double, double) return 0; }
std::unique_ptr是独占所有权的智能指针,确保一个对象在同一时间只有一个智能指针指向它。
std::shared_ptr是共享所有权的智能指针,可以有多个智能指针指向同一个对象。对象会在最后一个引用离开作用域时被销毁。
#include #include void sharedPtrExample() { std::shared_ptr p1(new int(20)); std::shared_ptr p2 = p1; // p1和p2共享所有权 std::cout << *p1 << ", " << *p2 << std::endl; std::cout << "use count: " << p1.use_count() << std::endl; }
std::weak_ptr是一种不控制对象生命周期的智能指针,与std::shared_ptr配合使用,解决循环引用的问题。
#include #include void weakPtrExample() { std::shared_ptr p1 = std::make_shared(30); std::weak_ptr wp = p1; // 创建弱引用 std::shared_ptr p2 = wp.lock(); // 提升为共享引用 if (p2) { std::cout << *p2 << std::endl; std::cout << "use count: " << p2.use_count() << std::endl; } }
noexcept可以用于声明一个函数不会抛出异常。有两种形式:
无条件形式的noexcept声明一个函数在任何情况下都不会抛出异常。
void func() noexcept { // This function is guaranteed not to throw an exception }
条件形式的noexcept根据一个布尔表达式来决定函数是否不会抛出异常。常见的用法是通过类型特性来判断。
template void func(T t) noexcept(noexcept(T())) { // This function will not throw an exception if T's constructor does not throw }
以下是一些示例代码,展示了如何使用noexcept关键字:
void doWork() noexcept { // Guaranteed not to throw an exception } int main() { doWork(); return 0; }
#include template void maybeThrow(T t) noexcept(std::is_nothrow_copy_constructible::value) { // Will not throw if T's copy constructor is noexcept } int main() { int a = 5; maybeThrow(a); // OK, int is nothrow copy constructible std::string s = "Hello"; maybeThrow(s); // OK, std::string's copy constructor may throw return 0; }
在实现移动构造函数和移动赋值运算符时,使用noexcept可以避免不必要的性能开销。
class MyClass { public: MyClass(MyClass&& other) noexcept { // Move constructor } MyClass& operator=(MyClass&& other) noexcept { // Move assignment operator return *this; } };
C++标准库中很多函数和运算符都使用了noexcept来优化性能和行为。例如,标准库容器在执行某些操作(如std::vector的移动操作)时会检查元素类型的构造函数和赋值运算符是否是noexcept的。
万能引用是指模板参数的类型推导时可以同时表示左值引用和右值引用的引用。万能引用的形式是T&&,其中T是模板参数。
template void func(T&& arg) { // arg 是万能引用 }
完美转发通过结合万能引用和std::forward来实现。std::forward根据传入的参数类型决定是将参数转发为左值引用还是右值引用。
std::forward是一个标准库函数,用于保留参数的左值或右值性质。以下是一个例子:
#include #include void process(int& lref) { std::cout << "Lvalue reference" << std::endl; } void process(int&& rref) { std::cout << "Rvalue reference" << std::endl; } template void wrapper(T&& arg) { process(std::forward(arg)); // 使用std::forward进行完美转发 } int main() { int x = 10; wrapper(x); // Lvalue reference wrapper(10); // Rvalue reference return 0; }
std::vector 的底层数据结构是一个连续的动态数组,元素在内存中排列成一系列的单元。这使得通过索引来访问元素非常高效,因为它可以通过内存指针和偏移量直接计算出元素的地址。
C++标准并没有规定std::vector必须是线程安全的。因此,在多线程环境中,如果有一个线程正在修改std::vector,而另一个线程同时对其进行访问(读取或修改),就有可能导致竞态条件(Race Condition)和数据竞争(Data Race)。为了确保在多线程环境下的安全使用,可以使用互斥锁(Mutex)或其他线程同步机制来保护std::vector的访问。
插入操作的时间复杂度为O(log n),其中n为堆中元素的个数。
删除操作的时间复杂度也是O(log n),其中n为堆中元素的个数。
#include #include class MinHeap { private: std::vector heap; // 上浮操作 void heapifyUp(int index) { int parent = (index - 1) / 2; while (index > 0 && heap[index] < heap[parent]) { std::swap(heap[index], heap[parent]); index = parent; parent = (index - 1) / 2; } } // 下沉操作 void heapifyDown(int index) { int left = 2 * index + 1; int right = 2 * index + 2; int smallest = index; if (left < heap.size() && heap[left] < heap[smallest]) { smallest = left; } if (right < heap.size() && heap[right] < heap[smallest]) { smallest = right; } if (smallest != index) { std::swap(heap[index], heap[smallest]); heapifyDown(smallest); } } public: // 插入操作 void insert(int value) { heap.push_back(value); heapifyUp(heap.size() - 1); } // 删除操作 int extractMin() { if (heap.empty()) { throw std::out_of_range("Heap is empty"); } int root = heap.front(); heap[0] = heap.back(); heap.pop_back(); heapifyDown(0); return root; } // 获取堆顶元素(最小元素) int getMin() { if (heap.empty()) { throw std::out_of_range("Heap is empty"); } return heap.front(); } // 获取堆的大小 size_t size() { return heap.size(); } // 判断堆是否为空 bool empty() { return heap.empty(); } }; int main() { MinHeap heap; heap.insert(3); heap.insert(2); heap.insert(1); heap.insert(15); heap.insert(5); std::cout << "Heap size: " << heap.size() << std::endl; while (!heap.empty()) { std::cout << heap.extractMin() << " "; } std::cout << std::endl; return 0; }
拉链法是一种开放地址法,它将多个映射到同一槽位的元素存储在一个链表(或其他数据结构)中。
当哈希冲突发生时,元素被添加到链表中。在查找、插入和删除元素时,首先找到对应的槽位,然后在链表中搜索或操作。
拉链法的优点是简单且易于实现,但在链表变得很长时,性能可能会下降。
线性探测法是一种封闭地址法,当哈希冲突发生时,它会查找下一个可用的槽位。
如果槽位被占用,就线性地查找下一个槽位,直到找到一个空槽位或达到表的末尾。
线性探测法可能导致聚集现象,即连续的槽位会被多次占用,降低性能。因此,通常需要解决二次聚集或更复杂的聚集问题。
二次探测法是线性探测法的变种,它使用二次函数来查找下一个可用的槽位,以减少聚集现象。
二次探测法的探测步长是一个二次函数的结果,通常是 c1 * i^2 + c2 * i,其中 c1 和 c2 是常数,i 是探测的次数。
二次探测法的性能通常比线性探测法好,但仍然可能出现聚集。
双重哈希是一种封闭地址法,它使用两个不同的哈希函数来确定下一个探测位置。
当哈希冲突发生时,它首先使用第一个哈希函数来计算下一个槽位,如果该槽位已被占用,则使用第二个哈希函数来计算下一个槽位。
双重哈希法可以减少聚集问题,但需要精心选择和设计哈希函数。
当哈希表中的负载因子(已占用槽位数与总槽位数之比)达到一定阈值时,可以进行再哈希。
再哈希是将哈希表的大小扩展一倍,并重新将所有元素插入新的表中。这可以减小负载因子,减少哈希冲突的可能性。
最好的方法是设计一个能够尽可能减少冲突的哈希函数。好的哈希函数应该均匀地分散键,使哈希表的槽位均匀利用。
这种方法是将哈希表的每个槽位都构建为一个独立的数据结构,如链表、树或其他哈希表。这样,即使发生冲突,也能够高效地存储多个值。
除了系统调用(System Call)以外,还有以下情况会触发用户态与内核态的切换:
切换过程涉及保存当前CPU状态(寄存器、程序计数器等),切换到新的上下文,并加载新状态:
在传统的数据传输过程中,数据可能会在用户态和内核态之间多次拷贝,这会导致性能开销。零拷贝技术旨在减少或消除这些不必要的拷贝操作,直接在内核态完成数据传输。
零拷贝的实现方式有多种,以下是几种常见的方法:
int fd = open("file.txt", O_RDONLY); struct stat file_stat; fstat(fd, &file_stat); void *mapped = mmap(NULL, file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0); write(socket_fd, mapped, file_stat.st_size); munmap(mapped, file_stat.st_size); close(fd);
int file_fd = open("file.txt", O_RDONLY); off_t offset = 0; sendfile(socket_fd, file_fd, &offset, file_size); close(file_fd);
splice 和 tee
splice系统调用允许在两个文件描述符之间移动数据,而无需将数据复制到用户态。tee系统调用可以复制数据流,而无需额外的数据拷贝。
int pipe_fds[2]; pipe(pipe_fds); splice(file_fd, NULL, pipe_fds[1], NULL, file_size, SPLICE_F_MOVE); splice(pipe_fds[0], NULL, socket_fd, NULL, file_size, SPLICE_F_MOVE);
写时拷贝的基本思想是,在创建数据的副本时,不立即复制数据,而是让副本和原始数据共享同一块内存区域。只有当其中一个副本试图修改数据时,系统才真正执行数据的复制操作。这种策略有助于节省内存和减少不必要的数据复制开销。
写时拷贝的实现通常涉及以下几个步骤:
写时拷贝广泛应用于操作系统和文件系统中,以下是几个典型的应用场景:
pid_t pid = fork(); if (pid == 0) { // 子进程 } else if (pid > 0) { // 父进程 } else { // 错误处理 }
2.虚拟内存分页:
在虚拟内存系统中,每个进程拥有一个虚拟地址空间,这个空间被分成固定大小的块,称为页(Page)。这些页被映射到物理内存中的物理页框(Frame)。当一个进程试图访问一个未映射到物理内存的虚拟页时,就会发生缺页中断。
客户端与服务器建立TCP连接:HTTPS在应用层使用SSL/TLS协议,而在传输层则使用TCP协议。首先,客户端和服务器通过三次握手建立一个TCP连接。
关闭TCP连接:通常在HTTP/1.1协议中,连接会保持一段时间以便处理后续请求。完成所有请求和响应后,客户端和服务器通过四次挥手关闭TCP连接。
对称加密算法使用相同的密钥进行加密和解密。
非对称加密算法使用不同的密钥进行加密和解密,通常包括公钥和私钥。
在实际应用中,对称加密和非对称加密常常结合使用,以利用两者的优点。
不同类型的网络有不同的MTU值。
MTU的大小对网络性能有显著影响:
MTU值可以在网络接口(如以太网接口、无线接口等)上进行配置。配置MTU值时需要考虑以下因素:
在不同操作系统上,可以使用不同的命令来查看和调整MTU值。例如:
#include struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; while (curr) { ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; } void reorderList(ListNode* head) { if (!head || !head->next) { return; } // 找到链表的中间节点 ListNode* slow = head; ListNode* fast = head; while (fast->next && fast->next->next) { slow = slow->next; fast = fast->next->next; } // 将链表分成两个部分 ListNode* secondHalf = slow->next; slow->next = nullptr; // 反转后半部分链表 secondHalf = reverseList(secondHalf); // 合并两个链表 ListNode* p1 = head; ListNode* p2 = secondHalf; while (p2) { ListNode* tmp1 = p1->next; ListNode* tmp2 = p2->next; p1->next = p2; p2->next = tmp1; p1 = tmp1; p2 = tmp2; } } void printList(ListNode* head) { ListNode* curr = head; while (curr) { std::cout << curr->val << " "; curr = curr->next; } std::cout << std::endl; } int main() { ListNode* head = new ListNode(1); head->next = new ListNode(2); head->next->next = new ListNode(3); head->next->next->next = new ListNode(4); head->next->next->next->next = new ListNode(5); std::cout << "Original list: "; printList(head); reorderList(head); std::cout << "Reordered list: "; printList(head); return 0; }
粉丝福利, 免费领取C/C++ 开发学习资料包、技术视频/项目代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发/音视频开发/Qt开发/游戏开发/Linuxn内核等进阶学习资料和最佳学习路线)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓