c++篇
看到了一些关于游戏开发c++笔试、面试题,但是有题目没有答案,作为一个只会一点点c++的小菜鸡就记录一下,以下问题答案都是chatGPT回答以及百度答案
面试问题来源:游戏开发岗面试总结
构造函数(Constructor):C++中的构造函数用于初始化对象的数据成员。构造函数的名称与类名相同,没有返回类型,包括参数列表和函数体。构造函数在创建对象时自动调用,并且可以重载,即可以有多个构造函数。
复制构造函数(Copy Constructor):复制构造函数用于通过已有对象创建一个新对象。复制构造函数的参数是一个同类的对象引用,它用于初始化新对象的数据成员。如果没有显式定义复制构造函数,C++会自动生成一个默认的复制构造函数。复制构造函数通常使用深拷贝(deep copy)来避免浅拷贝(shallow copy)带来的问题。
析构函数(Destructor):析构函数用于在对象销毁时释放资源和做一些清理工作。析构函数的名称与类名相同,前面加上一个波浪号(~)作为前缀。析构函数没有返回类型,没有参数。当对象超出作用域、被删除或程序结束时,析构函数会被自动调用。如果没有显式定义析构函数,C++会自动生成一个默认的析构函数。
代码如下(示例):
#include
class MyClass {
public:
// 默认构造函数
MyClass() {
std::cout << "Default constructor called" << std::endl;
}
// 带参数的构造函数
MyClass(int value) {
std::cout << "Parameterized constructor called with value: " << value << std::endl;
}
// 复制构造函数
MyClass(const MyClass& other) {
std::cout << "Copy constructor called" << std::endl;
}
// 析构函数
~MyClass() {
std::cout << "Destructor called" << std::endl;
}
};
int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2(10); // 调用带参数的构造函数
MyClass obj3 = obj1; // 调用复制构造函数
obj3 = obj2; // 调用赋值运算符(非复制构造函数)
return 0;
}
输出结果:
Default constructor called
Parameterized constructor called with value: 10
Copy constructor called
Destructor called
Destructor called
Destructor called
深复制(deep copy)和浅复制(shallow copy)是在编程中常用的两种对象复制方法。
简而言之,浅复制只复制对象的引用,而深复制复制对象的内容。
析构函数可以被写成虚函数,而构造函数不能被写成虚函数。
总结:由于构造函数在对象创建时被调用,对象的实际类型还不确定,无法使用虚函数;而析构函数在对象销毁时被调用,对象的实际类型已经确定,可以使用虚函数实现多态。
数组的内存排列是连续的,即所有元素在内存中是相邻存储的。
链表的内存排列是非连续的,每个节点包含数据和指向下一个节点的指针,节点在内存中可以分布在不同的位置。
二叉树的内存排列是通过指针链接的,每个节点包含数据以及指向左右子节点的指针。
struct A {
char y; //char类型,1字节
char* z; //指针类型,在 32 位系统上为 4 字节,在 64 位系统上为 8 字节
int x; //int类型,4字节
};
cout<<sizeof(A)<<endl;
因此,整个结构体 A 的大小为 1 + 4 + 4 = 9 字节。但是,由于内存对齐的原因,编译器会将结构体的大小调整为 12 字节,以确保每个成员的地址都能够对齐到合适的内存边界。
这里是假设在32位系统上,所以为char* z为4字节,取所有成员变量中占内存最大的计算偏移量
char*(4字节)== int(4字节),所以偏移量为4
所以
struct A {
char y; //偏移量:0
char* z; //偏移量:4
int x; //偏移量:8
};
结构体大小 = 0 + 4 + 8
因此,输出语句 cout<<sizeof(A)<<endl; 将会输出 12。
不同类型变量对应的字节数
虚函数的原理是通过虚函数表(vtable)来实现的。
总结:虚函数通过虚函数表和虚函数表指针实现动态绑定,能够在运行时根据对象的实际类型来确定调用的虚函数。
内存泄漏通常发生在动态分配内存后没有正确释放的情况下,导致程序无法再次访问和释放这块内存。内存泄漏的原因可能有以下几种:
为了避免内存泄漏,可以采取以下几种方法:
指针是一个变量,其值为另一个变量的内存地址。在计算机中,每个变量都存储在内存中的某个位置,而指针则指向这个位置。通过指针,可以直接访问和修改存储在内存中的数据。
指针的工作原理可以简单描述为以下几个步骤:
函数的参数传递方式有两种:传值和传址。
传值和传址的选择取决于具体的需求和情况。一般来说,对于简单的数据类型和较小的数据量,可以选择传值;而对于复杂的数据类型和较大的数据量,可以选择传址。
new和delete是C++中用于动态内存管理的操作符,用于在堆上分配和释放内存。
new 类型名
或 new 类型名[数组大小]
。delete 指针
或 delete[] 指针
。与malloc和free的区别:
malloc和free是C语言中的函数,用于动态内存管理。与new和delete相比,它们有以下几个区别:
void* malloc(size_t size)
,返回一个void指针。void free(void* ptr)
,接受一个void指针作为参数。综上所述,new和delete是C++中用于动态内存管理的操作符,提供了更高的类型安全性和便利性;而malloc和free是C语言中的函数,需要手动管理内存的分配和释放。在C++中,推荐使用new和delete来进行动态内存的分配和释放。
C++程序在运行时使用的内存可以划分为以下几个区域:
栈(Stack):
栈是用于存储局部变量、函数参数、函数返回地址等短期数据的一块内存区域。栈是由编译器自动分配和释放的,具有自动管理的特性。每当函数被调用时,会在栈上分配存储函数的参数、局部变量和返回地址的内存空间;当函数调用结束时,这些内存空间会被自动释放。
堆(Heap):
堆是用于存储动态分配的内存的一块内存区域。堆是由程序员手动分配和释放的,具有手动管理的特性。通过new操作符在堆上分配内存,通过delete操作符释放堆上的内存。堆上分配的内存可以在程序的任何地方使用,并且在不同的函数调用之间保持有效。
全局/静态存储区(Global/Static Storage):
全局存储区用于存储全局变量和静态变量,这些变量在整个程序的执行过程中都是存在的。全局变量在程序启动时分配内存,在程序结束时释放内存;静态变量在定义时分配内存,在程序结束时释放内存。
常量区(Constant Area):
常量区用于存储字符串常量和其他常量数据。这些常量数据在程序运行期间是不可修改的,并且存储在只读内存区域。
代码区(Code Area):
代码区存储程序的执行代码,包括函数的二进制代码和其他指令。代码区也是只读的,程序无法修改自身的代码。
这些内存区域在程序运行期间可以根据需要进行动态分配和释放,其中栈和堆是最常用的内存管理方式。栈用于存储函数的局部变量和函数调用的上下文信息,而堆用于存储动态分配的内存,供程序在需要时进行使用和释放。全局/静态存储区、常量区和代码区则用于存储程序的静态数据和执行代码,不会在程序运行期间进行动态的内存分配和释放。
C++11引入了许多新特性,以下是其中一些常见的特性:
代码如下:
auto x = 10; // x被推断为int类型
auto str = "Hello World"; // str被推断为const char*类型
代码如下:
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto& num : vec) {
num *= 2;
}
代码如下:
std::vector<int> vec = {1, 2, 3, 4, 5};
std::map<std::string, int> map = {{"apple", 1}, {"banana", 2}, {"orange", 3}};
代码如下:
int* ptr = nullptr;
if (ptr == nullptr) {
// 指针为空
}
代码如下:
enum class Color {
RED,
GREEN,
BLUE
};
Color color = Color::GREEN;
代码如下:
std::vector<int> vec = {1, 2, 3, 4, 5};
int sum = 0;
std::for_each(vec.begin(), vec.end(), [&sum](int num) {
sum += num;
});
代码如下:
#include
#include
void printHello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(printHello);
t.join();
return 0;
}
代码如下:
std::vector<int> source = {1, 2, 3, 4, 5};
std::vector<int> destination = std::move(source); // source的内容被移动到destination
代码如下:
#include
std::shared_ptr<int> sptr = std::make_shared<int>(10);
std::unique_ptr<int> uptr = std::make_unique<int>(20);
四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是
10.右值引用:
右值引用是一种新的引用类型,可以绑定到临时对象或被移动的对象上,支持高效的移动语义。
代码如下:
//使用两个 && 表示这是一个右值引用的类型
void processData(std::vector<int>&& data) {
// 对右值引用的data进行处理
}
std::vector<int> getData() {
std::vector<int> vec;
// 获取数据
return vec;
}
processData(getData()); // 传递getData()的返回值(右值)给processData函数
C++语言提供了几种智能指针,包括:std::unique_ptr
、std::shared_ptr
和std::weak_ptr
。
std::unique_ptr
:
std::unique_ptr
指向同一个对象,当它过期(超出作用域)或被手动释放时,它自动删除所管理的对象。unique_ptr p3 (new string (“auto”));
unique_ptr p4;
p4 = p3; //此时会报错!!
//可以用
unique_ptr ps1, ps2;
ps1 = demo(“hello”);
ps2 = move(ps1);
ps1 = demo(“alexia”);
cout << *ps2 << *ps1 << endl;
std::shared_ptr
:
std::shared_ptr
共享同一个对象。std::shared_ptr
过期时,才会自动删除所管理的对象。成员函数 | 含义 |
---|---|
use_count | 返回引用计数的个数 |
unique | 返回是否是独占所有权( use_count 为 1) |
swap | 交换两个 shared_ptr 对象(即交换所拥有的对象) |
reset | 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少 |
get | 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的 |
std::weak_ptr
:
std::shared_ptr
来访问或转换。std::shared_ptr
之间的循环引用导致内存泄漏的情况。这些智能指针各自有不同的优缺点:
std::unique_ptr
的优点是轻量级且性能高,不需要维护引用计数;缺点是不能共享资源。std::shared_ptr
的优点是可以共享资源,线程安全,适用于复杂的拥有关系;缺点是增加了额外的开销,包括引用计数的维护和原子操作等。std::weak_ptr
的优点是避免了循环引用的内存泄漏问题;缺点是不能直接访问被管理的对象,需要转换为std::shared_ptr
使用。选择智能指针应根据具体的需求和场景来决定。如果你需要独占所有权并且不需要资源共享,则可以使用std::unique_ptr
。如果你需要共享资源,则可以使用std::shared_ptr
。如果你需要解决循环引用问题,则可以考虑使用std::weak_ptr
。
在使用auto声明变量时,编译器会根据变量的初始化表达式推断出其类型,并将其替换为具体的类型。这样可以简化代码,减少类型的显式声明,提高代码的可读性和可维护性。
虽然使用auto声明变量可以方便地进行类型推断,但并不意味着应该全部都使用auto来声明变量。以下情况不建议过度使用auto:
可读性:显式声明变量可以更清晰地传达代码的意图和目的。如果变量的类型对于代码的理解很重要,或者可以提高代码的可读性,建议显式声明变量。
复杂表达式:在一些复杂的表达式中,类型推断可能会导致难以理解的推断结果。在这种情况下,显式声明变量可以提高代码的可读性和可维护性。
模板编程:在模板编程中,由于模板的参数可能具有多种类型,使用auto可能无法满足需要。在这种情况下,需要显式指定类型。
lambda表达式是一种匿名函数,允许我们在需要函数对象的地方提供一个简洁、灵活和内联的函数定义。
使用lambda表达式可以带来以下好处:
简洁性:lambda表达式可以在不定义独立函数的情况下,直接在代码中定义函数功能。这样可以减少定义函数的代码量,并且更清晰地表达特定的功能。
内联性:lambda表达式是内联定义的,可以直接在需要的位置使用,无需额外的函数声明和定义。这对于某些仅在局部范围内使用的函数非常方便。
可读性:由于lambda表达式在使用时紧随其后,它们可以直接展示函数的功能和意图,使代码更加易读和易理解。
高度灵活性:lambda表达式可以自包含地捕获所需的变量,并且可以在需要时更改捕获方式。这使得lambda表达式在编写回调函数、排序算法、STL算法等场景中特别有用。
以下是一个lambda表达式的示例:
auto sum = [](int a, int b) { return a + b; };
int result = sum(3, 4); // 调用lambda表达式计算结果为7
lambda表达式使用方括号来指定捕获列表,可以选择按值或按引用来捕获变量。在括号内部,定义了函数参数和函数体。
需要注意的是,在某些情况下,lambda表达式可能会使代码变得复杂和难以理解。在这种情况下,考虑将其提取为命名函数可能更合适。合理地使用lambda表达式可以提高代码的灵活性和可读性。
不,override关键字并非必需,但在特定情况下使用它可以提高代码的清晰性和可维护性。
在C++中,override关键字用于显式地指示派生类中的成员函数覆盖基类中的虚函数。当我们希望确保派生类中的函数确实是对基类函数的重写时,使用override关键字是一个好的实践。
使用override关键字的好处包括:
明确表明意图:使用override关键字可以清楚地表示我们有意重写了基类中的函数。这可以提醒其他开发人员,并且可以在编译时检测到一些潜在的错误,比如函数签名不匹配或遗漏了const修饰符。
错误检测:当使用override关键字时,编译器会在派生类中检查是否存在与基类中的虚函数匹配的函数。如果没有找到匹配的函数,或者函数签名不正确,编译器将产生错误。这有助于捕获潜在的错误,避免在派生类中错误地定义了一个新的函数而不是重写基类函数。
可读性和可理解性:使用override关键字可以使代码更加清晰和易读。它提供了一种方式来快速识别哪些函数是虚函数的重写,从而提高代码的可维护性和可理解性。
需要注意的是,在以下情况下使用override关键字是无效或不必要的:
右值引用是C++11引入的一种新特性,它提供了一种高效的方式来管理临时对象和避免不必要的复制。
右值引用的语法是使用&&符号来定义一个引用,例如:
int&& rvalue_ref = 123; // 右值引用绑定字面量
右值引用最常见的应用场景是将临时对象传递给函数,从而避免不必要的复制或移动:
void foo(std::string&& str) // 右值引用参数
{
// 对str的操作,通常会将其转移(move)到其他地方
}
int main()
{
foo(std::string("Hello, world!")); // 传递临时字符串,避免了复制
std::string str = "Hello";
foo(std::move(str)); // 将str转移为右值引用,避免了复制
}
在这个例子中,我们可以看到右值引用的两种应用方式。首先,在函数foo中,我们将参数定义为一个右值引用,以接收一个临时对象。由于该对象只是暂时存在,并且我们不需要保留它的状态,因此使用右值引用可以更有效地处理它。
另外,在调用foo函数时,我们可以使用一个临时对象或通过std::move函数将一个左值对象转换为右值引用来传递参数。这样可以避免在传递参数时进行额外的复制,从而提高代码的效率。
除了上述示例中的用法之外,右值引用还有其他实际应用场景。例如:
移动语义:右值引用是实现移动语义的关键。通过允许对象的状态转移到新的对象中,移动语义可以避免不必要的复制,提高代码效率。
完美转发:右值引用也是实现完美转发的核心技术之一。通过在函数模板中使用右值引用,可以将参数按原样转发到另一个函数,从而实现参数类型的完全转移。
总之,右值引用是C++11中非常重要的一个特性,它可以帮助我们管理临时对象、提高代码效率和实现完美转发等。理解和掌握右值引用的使用方式对于编写高效和可维护的C++代码非常重要。
这里只是文章的一部分问题答案,不一定正确,我对C++的了解有限,如果有错误,会有后续修改