小米C++开发,校招一面,纯C++
公众号:阿Q技术站
来源:https://www.nowcoder.com/feed/main/detail/e584c8b5d5e74f1faf8e8b9cc033dae2
1、说一下http和https的区别?
- 数据加密:
- HTTP:传输的数据是明文的,安全性较低,容易被窃听和篡改。
- HTTPS:通过 SSL/TLS 协议加密传输数据,可以保证数据的机密性,安全性更高。
- 数据完整性:
- HTTP:不提供数据完整性保护,数据在传输过程中可能被篡改。
- HTTPS:使用数字证书和加密算法来验证数据的完整性,保证数据在传输过程中不被篡改。
- 身份验证:
- HTTP:不提供身份验证机制,无法验证通信双方的身份。
- HTTPS:通过 SSL/TLS 协议验证服务器的身份,确保客户端连接的是真实的服务器,防止中间人攻击。
- 端口号:
- HTTP:默认端口号是 80。
- HTTPS:默认端口号是 443。
- SSL/TLS 握手过程:
- HTTPS 在建立连接时需要进行 SSL/TLS 握手过程,包括密钥交换、身份验证和协商加密算法等步骤,而 HTTP 不需要。
2、你用过lambda吗?
它是一种用来创建匿名函数的方式,可以在需要函数的地方直接定义函数,而不必显式地编写函数的名称。
[capture clause] (parameters) -> return_type {
// 函数体
};
其中:
-
capture clause
指定 lambda 表达式可以访问的外部变量。有两种形式:
[ ]
:不捕获任何外部变量。[var1, var2, ...]
:捕获指定的外部变量。捕获的变量可以是值传递或引用传递。
-
parameters
:lambda 函数的参数列表。 -
return_type
:返回类型,可以省略,编译器会根据返回语句自动推断。 -
{}
:函数体。
给个例子:
#include <iostream>
int main() {
// lambda 表达式,捕获外部变量 a,b
int a = 10, b = 20;
auto sum = [a, &b] () -> int {
b = 30; // 修改外部变量 b
return a + b;
};
std::cout << "Sum: " << sum() << std::endl; // 输出:40
std::cout << "b after lambda: " << b << std::endl; // 输出:30
return 0;
}
3、右值引用说一下?
右值引用是 C++11 引入的一种新的引用类型,用于标识临时对象(右值)的引用。右值引用的语法是使用双 && 符号,例如 int&&
。
右值引用的特点和用途:
- 绑定临时对象:右值引用可以绑定到临时对象(右值)上,延长其生命周期,避免临时对象在表达式结束后被销毁。
- 移动语义:右值引用是实现移动语义的基础。通过移动构造函数和移动赋值运算符,可以将资源(如内存、文件句柄等)从一个对象“移动”到另一个对象,避免不必要的资源拷贝,提高效率。
- 完美转发:右值引用可以与模板结合使用,实现完美转发(perfect forwarding),即在传递参数时保持参数的类型信息和引用性质。
例子:
#include <iostream>
void process(int&& value) {
std::cout << "Received rvalue: " << value << std::endl;
}
int main() {
int a = 10;
process(std::move(a)); // 此时 a 被转换为右值
return 0;
}
4、a和&a?
C++ 中,a
表示变量的值,而&a
表示变量的地址(即指针)。
给个例子一清二楚:
#include <iostream>
int main() {
int a = 10; // 定义一个整型变量 a,值为 10
int* ptr = &a; // 定义一个指针 ptr,指向变量 a 的地址
std::cout << "Value of a: " << a << std::endl; // 输出:Value of a: 10
std::cout << "Address of a: " << &a << std::endl; // 输出:Address of a: [a 的内存地址]
std::cout << "Value at address of a: " << *(&a) << std::endl; // 输出:Value at address of a: 10
std::cout << "Value at address of a using pointer: " << *ptr << std::endl; // 输出:Value at address of a using pointer: 10
return 0;
}
5、举列说三个STL?
这里给大家讲六大特性,下面有具体的容器
- 容器(Containers):STL 提供了多种容器,如向量(vector)、链表(list)、双端队列(deque)、集合(set)、映射(map)等,用于存储数据。每种容器都有不同的特性,可满足不同的需求。
- 算法(Algorithms):STL 包含了大量的算法,如排序、查找、遍历等,这些算法可以应用于各种容器,使得对数据的处理变得简单高效。
- 迭代器(Iterators):迭代器提供了一种统一的访问容器元素的方式,使得算法与容器的具体实现分离,增强了代码的可复用性和可移植性。
- 函数对象(Function Objects):函数对象是一种重载了函数调用操作符
()
的类对象,可以像函数一样调用。STL 中的很多算法可以接受函数对象作为参数,使得算法更加灵活。 - 适配器(Adapters):适配器是一种包装器,用于将一种容器或迭代器的接口转换成另一种接口,如栈(stack)、队列(queue)、优先队列(priority_queue)等。
- 空间配置器(Allocators):空间配置器用于管理内存分配和释放,可以根据需要自定义内存管理策略,使得 STL 中的容器可以在不同的内存环境下工作。
6、vector和list有什么区别?
- 底层数据结构:
vector
:使用动态数组(dynamic array)实现,内部是连续的内存空间,支持随机访问。list
:使用双向链表(doubly linked list)实现,每个元素存储当前元素的值以及指向前一个元素和后一个元素的指针,不支持随机访问,只能通过迭代器逐个访问元素。
- 内存分配:
vector
:由于使用动态数组,插入和删除操作可能涉及内存重新分配和元素移动,特别是在中间位置插入或删除元素时效率较低。list
:由于使用链表,插入和删除操作效率较高,不需要移动其他元素,但访问元素时需要遍历链表,效率较低。
- 插入和删除操作:
vector
:在末尾插入或删除元素的效率较高,时间复杂度为 O(1),但在中间位置插入或删除元素的效率较低,时间复杂度为 O(n)。list
:在任意位置插入或删除元素的效率都很高,时间复杂度为 O(1),因为只需要调整相邻节点的指针。
- 访问元素:
vector
:支持通过下标随机访问元素,时间复杂度为 O(1);支持迭代器逐个访问元素。list
:不支持随机访问,只能通过迭代器逐个访问元素,时间复杂度为 O(n)。
- 空间占用:
vector
:由于是动态数组,可能会预留额外的空间以支持后续的插入操作,因此可能会占用更多的内存。list
:由于是链表,每个节点都包含指针,可能会占用更多的内存。
7、map和set的区别?
- 元素特性:
map
:键值对形式的容器,每个元素包含一个键和一个值,键是唯一的。set
:集合形式的容器,每个元素只包含一个值,值是唯一的。
- 底层数据结构:
map
:通常使用红黑树(red-black tree)实现,保证了元素的有序性和高效的查找、插入和删除操作。set
:通常也使用红黑树实现,保证了元素的有序性和唯一性,与map
类似。
- 元素访问:
map
:通过键来访问元素,可以使用方括号[]
运算符或at()
成员函数。set
:直接访问元素,因为元素就是值本身。
- 插入操作:
map
:插入键值对使用insert()
成员函数。set
:插入值使用insert()
成员函数。
- 查找操作:
map
:查找元素使用find()
成员函数,返回一个指向该元素的迭代器。set
:查找元素使用find()
成员函数,返回一个指向该元素的迭代器。
- 内部元素顺序:
map
:元素按照键的顺序进行排序。set
:元素按照值的顺序进行排序。
- 内存占用:
map
和set
都是基于红黑树实现的,因此在内存占用上比较高,每个元素都需要存储额外的指针和颜色信息。
8、有没有遇到过迭代器失效的问题,怎么解决?
迭代器失效是指在对容器进行插入或删除操作后,原先获得的迭代器可能会失效,无法继续使用。这种情况通常发生在动态数组(如 vector
)和链表(如 list
)等容器中,因为这些容器在插入或删除元素时可能会导致内存重新分配或节点移动,从而使得原先的迭代器指向的位置不再有效。
解决办法:
- 使用插入和删除返回的迭代器:STL 的插入和删除操作通常会返回一个指向被插入或删除元素的迭代器,可以使用这个新的迭代器来继续操作。
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin() + 2; // 指向 3
it = vec.erase(it); // 删除 3,并返回指向 4 的迭代器
- 使用成员函数返回的迭代器:某些容器的成员函数会返回一个指向特定位置的迭代器,如
insert()
返回插入元素后的迭代器,可以使用这个迭代器来操作。
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
it = lst.insert(it, 0); // 在 1 前插入 0,并返回指向 0 的迭代器
-
避免在循环中直接使用迭代器:在对容器进行插入或删除操作时,最好避免在循环中直接使用迭代器,而是使用索引或其他方式来控制循环。
-
使用智能指针:如果可能,可以考虑使用智能指针(如
std::shared_ptr
或std::unique_ptr
)来管理元素,这样在容器重新分配内存时,智能指针会自动更新指向的对象。 -
重新获取迭代器:如果其他方法无法解决问题,可以在每次迭代前重新获取迭代器,确保它仍然有效。
9、给了一段代码,用迭代器修改了set的某一个值,问是否有问题?
10、构造函数为什么不能定义成虚函数?
构造函数不能定义成虚函数的主要原因是在对象构造过程中,虚函数机制并不适用。这是因为在调用构造函数时,对象的虚表(vtable)还没有被构造,无法确定正确的虚函数地址。
具体原因:
- 构造过程中的虚函数调用不安全:在对象的构造过程中,对象的虚表还没有被构造出来,此时如果调用虚函数,将无法找到正确的函数地址,可能导致程序崩溃或不可预料的行为。
- 虚函数表的构造时机:虚函数表是在对象构造完成后才会被构造的,构造函数负责初始化对象的数据成员,而虚函数表是由编译器生成的,包含了虚函数的地址。因此,构造函数无法在虚函数表构造之前定义虚函数。
- 派生类构造函数调用基类虚函数的问题:如果构造函数是虚函数,那么在派生类构造函数调用时,基类构造函数可能会被调用,而此时派生类的成员变量还没有被初始化,可能导致不正确的行为。
11、析构函数为什么一定要定义成虚函数?
析构函数如果不定义为虚函数,就会导致无法正确释放派生类对象的情况,这是因为在派生类对象被删除时,只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致派生类的资源无法正确释放,造成内存泄漏和不确定的行为。
详细解释:
- 多态性需要:如果基类的指针或引用指向派生类的对象,并且基类的析构函数不是虚函数,那么当删除基类指针时,只会调用基类的析构函数,而不会调用派生类的析构函数。这样就无法正确释放派生类对象的资源,造成资源泄漏。
- 虚函数表的作用:析构函数需要定义为虚函数,才能使得基类指针指向派生类对象时,通过虚函数表调用正确的析构函数。只有虚函数才会在运行时动态绑定,根据对象的实际类型调用正确的析构函数。
- 避免内存泄漏和未定义行为:如果派生类有自己的资源(如动态分配的内存、打开的文件等),没有定义虚析构函数,删除基类指针时不会调用派生类的析构函数,就无法正确释放这些资源,可能导致内存泄漏和未定义行为。
12、纯虚函数和虚函数的区别?
- 定义:
- 虚函数:在基类中用
virtual
关键字声明的成员函数,可以在派生类中被重写。 - 纯虚函数:在基类中用
virtual
关键字声明的没有函数体的虚函数,需要在派生类中重新实现,否则派生类也将是抽象类。
- 虚函数:在基类中用
- 实现:
- 虚函数:有函数体,可以在基类中提供默认实现,也可以在派生类中进行重写。
- 纯虚函数:没有函数体,需要在派生类中进行实现,否则派生类也将成为抽象类。
- 作用:
- 虚函数:用于实现运行时多态性,通过基类指针或引用调用虚函数时,会根据对象的实际类型调用相应的函数。
- 纯虚函数:用于定义接口,强制派生类实现特定的方法,以确保派生类具有某些功能。
- 含义:
- 虚函数:表示基类提供了默认实现,但允许派生类对其进行重写。
- 纯虚函数:表示基类只是声明了接口,不提供实现,要求派生类必须实现该函数。
- 语法:
- 虚函数:在基类中使用
virtual
关键字声明,并提供函数体。 - 纯虚函数:在基类中使用
virtual
关键字声明,但没有函数体,后面加上= 0
表示纯虚函数。
- 虚函数:在基类中使用
13、假设有一个类,他有自己的虚函数表,现在继承它,子类的虚函数表是否一样?
在 C++ 中,派生类会继承基类的虚函数表(vtable),但并不是简单地复制基类的虚函数表。子类的虚函数表可能与基类的虚函数表有所不同,具体取决于子类是否重写(覆盖)了基类的虚函数。
- 子类未重写基类的虚函数:
- 如果子类没有重写基类的虚函数,子类的虚函数表会继承基类的虚函数表,两者是相同的。
- 这意味着子类的虚函数表中的函数指针与基类的虚函数表中的相应函数指针相同。
- 子类重写基类的虚函数:
- 如果子类重写了基类的虚函数,子类的虚函数表中会有一个新的函数指针来指向子类的虚函数。
- 子类的虚函数表中只会包含那些被子类重写的虚函数,其余的虚函数仍然会继承自基类。
14、说说你对多态的理解?
多态性是指同一个操作可以作用于不同类型的对象,并且可以根据对象的类型执行不同的行为。多态性通过虚函数和函数重载实现。
- 编译时多态性(静态多态性): 通过函数重载实现,编译器在编译时根据函数参数的类型和数量来选择调用合适的函数。这种多态性是在编译时解析的。
- 运行时多态性(动态多态性): 通过虚函数和继承实现,允许在运行时根据对象的实际类型来调用适当的函数。这种多态性是在运行时解析的。
在C++中,虚函数的实现依赖于虚函数表(vtable)和虚函数表指针(vptr)。
- 虚函数表(vtable): 虚函数表是一个包含虚函数指针的数据结构,每个类(包括派生类)都有一个与之对应的虚函数表。虚函数表中存储了该类的虚函数地址,以及可能从基类继承的虚函数地址。每个虚函数表中的条目与一个虚函数对应。
- 虚函数表指针(vptr): 虚函数表指针是一个指向虚函数表的指针,它通常存储在每个类的对象的内部。如果一个类中包含虚函数,它的对象中会有一个虚函数表指针,指向该类的虚函数表。这个指针使程序能够在运行时找到正确的虚函数表,从而实现多态性。
15、重写的时候如何调用基类的函数?
在派生类中重写(覆盖)基类的虚函数时,可以使用作用域解析运算符 ::
来显式调用基类的函数。这样可以在派生类中调用基类的函数,即使该函数在派生类中被重写了。
给个例子说明:
假设有一个基类 Base
和一个派生类 Derived
,Base
中有一个虚函数 virtual void foo()
,Derived
中重写了这个函数。在 Derived
中如何调用 Base
的 foo
函数呢?可以使用作用域解析运算符 ::
来指定调用的是基类的函数。
参考代码:
#include <iostream>
class Base {
public:
virtual void foo() {
std::cout << "Base::foo()" << std::endl;
}
};
class Derived : public Base {
public:
void foo() override {
std::cout << "Derived::foo()" << std::endl;
// 调用基类的foo函数
Base::foo();
}
};
int main() {
Derived d;
d.foo(); // 输出 Derived::foo() Base::foo()
return 0;
}
16、说一说死锁问题,如何预防?
死锁是指两个或多个进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致它们都无法继续执行下去。
预防办法:
- 避免使用多个锁:尽可能减少使用多个锁来控制共享资源,这样可以减少死锁的可能性。如果必须使用多个锁,确保它们的获取顺序是一致的,避免循环等待。
- 使用锁的层次结构:如果需要使用多个锁,可以考虑使用锁的层次结构,即在获取锁的时候按照一定的顺序获取。这样可以避免循环等待的情况。
- 尽量减小锁的持有时间:在获取锁和释放锁之间的代码尽量保持简短,减小锁的持有时间,从而减少死锁的可能性。
- 使用超时机制:在获取锁的时候可以使用超时机制,即尝试获取锁一段时间后如果没有成功就放弃,避免长时间等待造成的死锁。
- 避免嵌套锁:尽量避免在持有一个锁的同时去获取另一个锁,这样容易造成死锁。如果必须嵌套锁,确保获取锁的顺序是一致的。
- 使用死锁检测工具:可以使用一些死锁检测工具来帮助检测和解决死锁问题,例如Valgrind、Helgrind等。
17、手撕题目:力扣142. 环形链表 II
问题描述
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
思路
- 使用两个指针
slow
和fast
,开始时它们都指向链表的头节点head
。 slow
每次移动一步,fast
每次移动两步,直到它们相遇或者fast
指向了null
。- 如果
fast
指向了null
,说明链表无环,直接返回null
。 - 如果
fast
和slow
相遇了,说明链表有环。此时将fast
指针重新指向头节点head
,然后fast
和slow
同时每次移动一步,直到它们再次相遇。 - 当它们再次相遇时,就是环的入口节点。
参考代码
C++
#include <iostream>
using namespace std;
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
ListNode *detectCycle(ListNode *head) {
if (!head || !head->next) {
return nullptr; // 链表为空或只有一个节点,无环
}
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,说明链表有环
if (slow == fast) {
fast = head; // 将快指针重新指向头节点
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
return fast; // 返回环的入口节点
}
}
return nullptr; // 快指针到达链表尾部,无环
}
int main() {
// 构建一个带环的链表
ListNode *head = new ListNode(3);
head->next = new ListNode(2);
head->next->next = new ListNode(0);
head->next->next->next = new ListNode(-4);
head->next->next->next->next = head->next; // 3->2->0->-4->2
ListNode *result = detectCycle(head);
if (result) {
cout << "环的入口节点值为:" << result->val << endl;
} else {
cout << "链表无环" << endl;
}
return 0;
}