0%

C/C++面试题目整理(2)

1. move函数

在C++中,移动语义是为了解决传统的拷贝构造函数和赋值运算符在传递临时对象时效率低下的问题。通过使用move函数模板类,程序员可以明确地表达自己的意图,将资源的所有权从一个对象转移到另一个对象,避免不必要的数据拷贝,提高程序的性能。在实现move函数模板类时,需要考虑对象的生命周期管理、资源的移动和释放等问题,以确保程序的正确性和高效性。
std::move() 是 C++ 标准库中的一个函数,它位于 头文件中。它用于将对象的所有权从一个对象转移给另一个对象。
当我们需要将某个对象转移(而不是复制)到另一个对象时,可以使用 std::move() 函数来实现高效的资源管理。通过使用 std::move(),编译器将不再视图拷贝该对象,而是将其内部状态的所有权转移到新的目标对象上。
使用 std::move() 的一般步骤如下:
引入 头文件:#include
使用 std::move() 函数进行转移:target = std::move(source);
以下是一些关键点和注意事项:
std::move() 并不实际移动任何数据,它只是将传递给它的参数标记为可被移动(右值)。这意味着我们应该确保在调用 std::move() 之前,源对象不再被使用。
被标记为可移动的右值引用可以通过 Rvalue 引用来接收,并且可以使用该引用访问原始源对象的内容。
在转移完成后,源对象处于有效但未定义状态,通常会被设置为默认构造状态或 nullptr 等合理值。因此,在转移后,请不要假设源对象保持原有值。
对于支持移动语义的类,可以通过实现自定义的移动构造函数和移动赋值操作符来控制转移过程。

2. 类型转换

类型转换是将一个数据类型的值转换为另一个数据类型的过程。在C++中,有多种类型转换方式:
隐式类型转换(Implicit Conversion):编译器会自动进行的类型转换。例如,将一个整数赋值给一个浮点数变量,或者将一个较小范围的整数赋值给较大范围的整数变量。
显式类型转换(Explicit Conversion):需要使用特定的语法来指明要进行类型转换。
主要有以下几种形式:
C风格的强制类型转换:使用括号将目标类型写在被转换表达式之前。例如,(int) 3.14 将浮点数 3.14 转换为整数。
static_cast:用于基本数据类型之间的显式转换。例如:

1
int a = static_cast<int>(3.14);

将浮点数 3.14 转换为整数。
dynamic_cast:用于类层次间的向下转型和运行时检查。用于具有虚函数的类及其派生类之间的指针或引用。
const_cast:用于去除常量属性。例如,去除 const 修饰符后修改常量值。
reinterpret_cast:进行底层位模式重新解释的转型。

3. 如果new了之后出了问题直接return。会导致内存泄漏。怎么办(智能指针,raii)

正确的做法是使用智能指针来管理内存,通过RAII(资源获取即初始化)的机制来确保资源的及时释放,避免内存泄漏。
智能指针是C++中的一种数据类型,可以模拟原始指针的行为,但具有自动释放内存的功能。在面对可能出现异常导致提前返回的情况下,使用智能指针能够确保在任何情况下都能正确释放内存,从而避免内存泄漏。
内存泄漏是指程序中动态分配的内存在不再使用时没有被释放,导致系统内存耗尽的问题。使用智能指针可以有效避免内存泄漏,其中最常用的智能指针包括std::shared_ptr和std::unique_ptr。std::shared_ptr允许多个指针共享同一块内存,会维护一个引用计数,当引用计数为0时自动释放内存;std::unique_ptr则独占所指向的对象,保证了独占所有权的语义。在编程中,应该尽量使用智能指针来管理动态内存,以提高程序的健壮性和可维护性。

4. const引用和指针的区别?

const引用在声明时需要同时初始化,而且在使用时不能修改所引用变量的值,相当于给变量加上只读属性。指针可以通过解引用来修改所指向变量的值,但需要注意空指针和野指针的问题。
const引用和指针都是用于处理常量数据的方式,但它们有一些区别:
语法:const引用使用&符号表示,而指针使用符号表示。
初始化要求:const引用必须在定义时初始化,并且只能绑定到与其类型相同或可转换为其类型的对象。指针可以先声明再初始化,并且可以指向任意类型的对象。
空值(null):指针可以为空值(nullptr),即不指向任何对象。而const引用必须始终引用一个有效的对象。
可变性:通过const引用无法修改所引用的对象,这种约束是由编译器强制执行的。而通过指针,可以通过解引用操作符(
)来修改所指向的对象。
对象拷贝:对于大型对象来说,通过const引用传递参数会更高效,因为不需要进行拷贝操作。而指针作为函数参数时需要进行拷贝。

5. C++ 中struct 和class区别?

在C++中,struct和class在语法上是可以互换使用的,唯一的区别在于默认的访问权限。在struct中,默认的成员访问权限是public,而在class中,默认的成员访问权限是private。
在C++中,struct和class是用于定义自定义数据类型的关键字,它们有一些区别:
默认访问权限:在struct中,默认的成员访问权限为公共(public),而在class中,默认的成员访问权限为私有(private)。
继承方式:当使用关键字struct定义类时,继承默认为公共继承;而使用关键字class定义类时,默认为私有继承。
类型别名:在C++11及以后的标准中,可以使用关键字using给结构体或类定义别名。例如:

1
2
3
4
5
6
7
8
9
10
struct MyStruct {
// ...
};

class MyClass {
// ...
};

using AliasStruct = MyStruct; // 结构体别名
using AliasClass = MyClass; // 类别名

使用习惯:传统上,人们倾向于使用 struct 来表示简单的数据结构,其成员通常都是公共的,并且不包含复杂的方法。而 class 则更多用于设计复杂的对象和类层次结构,并且强调封装性、隐藏内部实现细节等。

6. 如何防止一个头文件 include 多次?

头文件被多次包含可能会导致重复定义,造成编译错误。使用头文件保护宏可以有效避免这种情况发生。头文件保护宏的机制是在预处理阶段检查是否已经定义了特定的宏,如果已经定义则跳过后续内容,否则继续包含头文件内容。这种机制可以确保头文件只被包含一次,避免了重复定义的问题。以下是一个简单的示例:

1
2
3
4
5
6
7
plaintext
#ifndef EXAMPLE_H
#define EXAMPLE_H

// 头文件内容

#endif

7. C++中的map和unordered_map的区别和使用场景

C++中的map和unordered_map的区别在于底层数据结构不同,map是基于红黑树实现的有序映射,而unordered_map是基于哈希表实现的无序映射。map适合有序性要求高的场景,而unordered_map适合查找速度要求高的场景。 它们之间有以下区别和不同的使用场景:
存储方式:map 是基于红黑树实现的有序容器,而 unordered_map 则是基于哈希表实现的无序容器。
查找速度:由于哈希表具有 O(1) 的平均查找复杂度,相比之下,红黑树具有 O(log n) 的查找复杂度。因此,在大多数情况下,unordered_map 的查找速度更快。
顺序性:map 中的元素按照键值进行排序,并保持着固定的顺序;而 unordered_map 中元素的顺序是根据哈希函数计算得到的结果,并没有特定的顺序。
内存占用:由于哈希表需要额外存储桶来处理碰撞冲突,所以通常情况下 unordered_map 占用的内存会更多一些,而 map 只需要额外存储节点即可。
在选择使用哪种容器时可以考虑以下几点:
如果需要按照键值排序并且能够保持稳定顺序,则选择 map
如果对元素插入和查询速度要求较高,并且不关心顺序,则选择 unordered_map
如果对内存使用量非常敏感,并且可以接受较低的查询性能,可以选择 map

8. lambda表达式的理解,它可以捕获哪些类型?

lambda表达式是一种匿名函数,它可以捕获其所在作用域中的局部变量和全局变量,是C++11引入的一种函数对象创建方式,它可以在需要函数对象的地方使用,例如作为参数传递给算法函数、用于定义函数对象变量等。
Lambda表达式的基本语法如下:

1
2
3
[capture list](parameters) -> return_type { 
// 函数体
}

其中:
capture list 是可选项,用于捕获外部变量。
parameters 是参数列表。
return_type 是返回类型。
{} 包含了函数体。
Lambda表达式可以捕获以下几种类型的外部变量:
值捕获(value capture):通过在捕获列表中以值方式进行捕获。例如:[x],表示以值方式捕获变量x。
引用捕获(reference capture):通过在捕获列表中以引用方式进行捕获。例如:[&y],表示以引用方式捕获变量y。
隐式捕获(implicit capture):省略了具体的变量名,在编译器根据上下文推断要进行哪种类型的捕获。例如:[=] 表示按值方式隐式捕获所有外部变量; [&] 表示按引用方式隐式捕获所有外部变量。
在lambda表达式内部可以使用被捕获的外部变量,并且根据捕获方式的不同,有不同的语义。需要注意的是,被值捕获的变量在lambda表达式内部是不可修改的(除非使用mutable修饰符);而被引用捕获的变量可以修改。

9. gcc编译的过程

GCC(GNU Compiler Collection)是一款流行的开源编译器,用于将源代码转换为可执行程序。
下面是GCC编译过程的简要步骤:
预处理(Preprocessing):预处理阶段通过处理预处理指令,如#include和宏定义等,对源文件进行展开和替换。结果生成一个扩展名为”.i”的中间文件。
编译(Compilation):编译阶段将预处理后的代码翻译成汇编语言代码。这个阶段主要包括词法分析、语法分析、语义分析和优化等步骤。结果生成一个扩展名为”.s”的汇编代码文件。
汇编(Assembly):汇编阶段将汇编语言代码转化为机器码指令,并生成一个扩展名为”.o”的目标文件。
链接(Linking):链接阶段将目标文件与必要的库文件进行合并,以及解决符号引用和地址重定位等问题。最终生成可执行程序或者共享库文件。
在链接阶段,还可以包含以下几个子过程:
符号解析(Symbol resolution):确定所有符号引用的实际地址。
地址重定位(Address relocation):根据实际地址修改目标文件中的地址值。

C++面试题目整理(1)