CVTE一面面经
公众号:阿Q技术站
1、public、protected、private三者有什么联系、区别?
-
public
(公有):-
成员可以在类的外部和内部访问。
-
成员在类的继承链中保持其可见性,派生类可以访问基类的公有成员。
-
成员通常用于定义类的公共接口,即其他代码可以访问的类成员。
-
-
protected
(保护):-
成员可以在类的内部和派生类中访问,但不能在类的外部访问。
-
成员通常用于实现继承的接口和内部实现细节,以便在派生类中重用。
-
成员对于类的用户来说是不可见的,只有派生类可以访问它们。
-
-
private
(私有):-
成员只能在类的内部访问,不能在类的外部或派生类中访问。
-
成员通常用于类的实现细节,以隐藏内部数据和实现细节。
-
成员对于类的用户和派生类都是不可见的。
-
2、类的静态成员函数有什么特性?
- 静态成员函数不依赖于类的实例,因此可以在没有类对象的情况下调用。它们属于类本身而不是类的实例。这意味着你可以通过类名直接调用静态成员函数,而不需要创建类的对象。
- 静态成员函数只能访问类的静态成员(静态数据成员和静态函数),不能访问非静态成员(普通数据成员和普通成员函数)。这是因为静态成员函数不关联于任何特定的类对象,因此无法访问实例相关的数据。
- 静态成员函数可以具有公有、保护或私有的访问级别,就像普通成员函数一样。这意味着它们可以被外部代码访问,也可以在派生类中被重写。
- 静态成员函数通常与静态数据成员一起使用,因为它们都与类本身相关,而不是与类的实例相关。静态成员函数可以用于操作和管理静态数据成员。
3、空类的大小?
空类的话就是类中没有任何数据成员和成员函数。空类的大小取决于编译器和平台,但通常情况下,空类的大小为1字节。
这1字节的大小通常用于标识对象在内存中的位置,以确保不同的对象具有不同的地址。
所以即使是空类,也必须在内存中占用至少一个字节。
4、空类默认生成哪几个成员函数?
- 默认构造函数(Default Constructor): 如果你没有显式定义构造函数,编译器会为你生成一个默认构造函数。这个构造函数不接受任何参数,用于创建类的对象。默认构造函数会执行默认的对象初始化。
- 析构函数(Destructor): 如果你没有显式定义析构函数,编译器会为你生成一个默认的析构函数。这个析构函数用于销毁类的对象,释放对象所占用的资源。默认析构函数通常是空的,不执行特定的清理工作。
- 拷贝构造函数(Copy Constructor): 如果你没有显式定义拷贝构造函数,编译器会为你生成一个默认的拷贝构造函数。这个构造函数用于创建一个对象作为另一个对象的副本。默认的拷贝构造函数执行成员逐一拷贝,适用于大多数情况,但可能不适用于包含动态分配内存的类。
- 拷贝赋值运算符(Copy Assignment Operator): 如果你没有显式定义拷贝赋值运算符,编译器会为你生成一个默认的拷贝赋值运算符。这个运算符用于将一个对象的内容复制到另一个对象,通常与拷贝构造函数类似,执行成员逐一拷贝。
5、结构体和类有什么区别?
- 默认访问控制:
- 结构体:结构体的成员默认为公有(public),这意味着结构体的数据成员可以在结构体外部直接访问。
- 类:类的成员默认为私有(private),这意味着类的数据成员默认情况下不能在类外部直接访问。
- 成员函数的默认属性:
- 结构体:结构体可以包含成员函数,但这些成员函数默认为公有(public)。
- 类:类中的成员函数默认为私有(private)。
- 设计目的:
- 结构体:结构体通常用于组织一组相关的数据,以便轻松地进行数据的打包和传递。结构体的主要目的是数据的聚合。
- 类:类用于封装数据和操作数据的方法,实现面向对象编程的概念。类的主要目的是数据和行为的封装,支持数据隐藏和信息隐藏。
- 默认构造函数:
- 结构体:如果你没有显式定义构造函数,结构体会有一个默认构造函数,但不会初始化成员变量。
- 类:如果你没有显式定义构造函数,类会有一个默认构造函数,但不会初始化成员变量。不过,如果类包含了非静态的 const 数据成员,那么默认构造函数会对这些成员进行初始化。
- 默认析构函数:
- 结构体:如果你没有显式定义析构函数,结构体会有一个默认析构函数,但不执行任何清理工作。
- 类:如果你没有显式定义析构函数,类会有一个默认析构函数,但不执行任何清理工作。不过,如果类包含了资源(如动态分配内存),你可能需要自定义析构函数来正确释放这些资源。
- 继承:
- 结构体:结构体可以用于继承,但默认继承访问控制为公有继承。
- 类:类也可以用于继承,但默认继承访问控制为私有继承。
6、父类和子类构造和释放的顺序?
构造顺序:
- 父类构造函数:当创建子类对象时,首先会调用父类(基类)的构造函数。父类的构造函数负责初始化父类的数据成员和执行父类的构造函数体。
- 子类构造函数:接下来,子类的构造函数被调用。子类的构造函数负责初始化子类的数据成员和执行子类的构造函数体。
释放顺序:
- 子类析构函数:当子类对象离开其作用域或被显式销毁时,首先会调用子类的析构函数。子类的析构函数负责释放子类特定的资源和执行子类的析构函数体。
- 父类析构函数:接下来,父类的析构函数被调用。父类的析构函数负责释放父类特定的资源和执行父类的析构函数体。
需要注意的是,构造和析构的顺序总是相反的:先构造父类,再构造子类;先析构子类,再析构父类。这个顺序是为了确保在子类的构造函数中可以正确初始化继承自父类的成员,而在析构函数中可以正确清理这些资源。
下边我用一个例子给大家展示一下:
#include <iostream>
class Parent {
public:
Parent() {
std::cout << "Parent constructor" << std::endl;
}
~Parent() {
std::cout << "Parent destructor" << std::endl;
}
};
class Child : public Parent {
public:
Child() {
std::cout << "Child constructor" << std::endl;
}
~Child() {
std::cout << "Child destructor" << std::endl;
}
};
int main() {
Child childObj;
return 0;
}
7、有用过stl吗?里面有哪些内容?
巴拉巴拉。。。
这里我就不详说了,可以去看这篇:
8、vector如何实现内部动态扩容的?
上一个里面有哦~
9、map的[]和find两者有什么区别?
- 返回值类型:
[]
运算符:返回与指定键相关联的值,如果键不存在,则会插入一个新键值对,该值的类型为该map
的值类型,并且可以用于读取和写入值。find
函数:返回一个迭代器,指向与指定键关联的元素(键值对)或者指向map
的end()
迭代器,如果键不存在。
- 行为差异:
[]
运算符:如果使用[]
访问一个不存在的键,则会在map
中插入一个新键值对,键的值由值类型的默认构造函数确定(通常为零初始化)。如果键已存在,则会返回与该键相关联的值,并且可以使用[]
来修改该值。find
函数:find
函数只用于查找元素,如果键不存在,它会返回map
的end()
迭代器,不会插入新元素。
- 异常差异:
[]
运算符:不会引发异常,如果键不存在,它会插入一个新元素。find
函数:不会引发异常,如果键不存在,它会返回map
的end()
迭代器。
- 使用场景:
[]
运算符通常用于在明确知道键存在的情况下访问元素,或者在需要插入新元素并设置其值时使用。find
函数通常用于检查键是否存在,然后根据情况采取进一步的操作。
下边演示一下“[]”运算符和“find”函数的不同行为:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap;
// 使用[]运算符,插入新元素
myMap[1] = "One";
myMap[2] = "Two";
// 使用find函数检查键是否存在
std::map<int, std::string>::iterator it = myMap.find(3);
if (it != myMap.end()) {
std::cout << "Key 3 found: " << it->second << std::endl;
} else {
std::cout << "Key 3 not found." << std::endl;
}
return 0;
}
10、迭代器中如何删除一个迭代器又能保证后续迭代器不失效呢?
- 使用迭代器的返回值: 在执行删除操作后,将迭代器的返回值(指向被删除元素的下一个元素的迭代器)赋值给原始迭代器,从而更新它。这样做可以确保后续迭代器不会失效。
std::map<int, std::string> myMap;
// 填充map
std::map<int, std::string>::iterator it = myMap.begin();
while (it != myMap.end()) {
if (some_condition) {
// 删除满足条件的元素,并更新迭代器
it = myMap.erase(it);
} else {
++it; // 移动到下一个元素
}
}
示例中,如果满足某个条件,我们使用 erase
函数删除了元素,并将返回的迭代器赋值给 it
,以确保后续迭代器仍然有效。
- 使用后缀自增运算符: 可以使用后缀自增运算符
it++
而不是前缀自增运算符++it
来移动迭代器,因为后缀自增运算符返回的是迭代器的副本,而不是引用,这样可以避免潜在的问题。
std::map<int, std::string> myMap;
// 填充map
std::map<int, std::string>::iterator it = myMap.begin();
while (it != myMap.end()) {
if (some_condition) {
// 删除满足条件的元素,并继续使用后缀自增运算符
myMap.erase(it++);
} else {
++it; // 移动到下一个元素
}
}
示例中,我们在删除元素之后使用 it++
来继续移动迭代器,确保后续迭代器仍然有效。
11、有用过智能指针吗?用过哪些?shared_ptr内部的实现原理是怎么样的?
智能指针在C++学习中,应该是必不可少的,所以说我们都应该好好学习学习。。
目前就是auto_ptr(已启用)、shared_ptr、weak_ptr、unique_ptr。
shared_ptr内部的实现原理:
- 引用计数:
shared_ptr
内部维护一个引用计数,用于跟踪有多少个shared_ptr
共享同一个堆内存资源。每当创建一个新的shared_ptr
对象来管理特定的堆内存资源时,引用计数就会增加1。当shared_ptr
被销毁或者不再引用这个堆内存资源时,引用计数会减少1。当引用计数变为0时,表示没有任何shared_ptr
对象在管理这个堆内存资源,此时会自动释放(delete
)这个资源,避免内存泄漏。 - 资源管理:
shared_ptr
不仅维护了引用计数,还在内部持有一个指向堆内存资源的指针。这个指针指向被管理的对象或数组。当shared_ptr
的构造函数被调用时,它会接收一个裸指针(通常是通过new
创建的)或另一个shared_ptr
,并将这个指针保存在内部。 - 拷贝构造和赋值操作: 当你将一个
shared_ptr
赋值给另一个shared_ptr
或者使用拷贝构造函数创建一个新的shared_ptr
时,引用计数会增加1,并且这两个shared_ptr
对象都指向相同的堆内存资源。这是因为它们共享同一个引用计数。当其中一个shared_ptr
被销毁或不再引用资源时,引用计数会减少1。 - 释放资源: 当
shared_ptr
的引用计数减少到0时,表示没有任何shared_ptr
对象再引用这个堆内存资源。此时,shared_ptr
内部会自动释放这个资源,调用delete
(或者适用于数组的delete[]
)来销毁对象或释放数组。这确保了资源的正确释放,避免了内存泄漏。 - 弱引用计数: 除了引用计数,
shared_ptr
还维护了一个弱引用计数。弱引用计数用于weak_ptr
,它是另一种智能指针,允许观察(但不拥有)与shared_ptr
共享的资源。弱引用计数跟踪有多少个weak_ptr
共享相同的资源,但不影响资源的生命周期。只有当所有的shared_ptr
都销毁时,资源才会被释放。
更详细点的可以看另外一篇文章:
12、分配内存时,malloc和new有什么区别?
- 语法和类型安全性:
malloc
是C语言中的函数,而new
是C++中的操作符。malloc
返回void*
类型的指针,需要进行显式的类型转换,将其转换为所需的指针类型,这可能导致类型错误。new
返回所需类型的指针,不需要显式类型转换,因此更类型安全。
// 使用malloc
int *p1 = (int *)malloc(sizeof(int));
// 使用new
int *p2 = new int;
-
构造函数和析构函数的调用:
-
malloc
只是分配内存,不会调用对象的构造函数。 -
new
不仅分配内存,还会调用对象的构造函数进行初始化。这使得new
更适合在C++中用于动态分配自定义类型的对象。
-
// 使用malloc,不会调用构造函数
MyClass *obj1 = (MyClass *)malloc(sizeof(MyClass));
// 使用new,调用构造函数
MyClass *obj2 = new MyClass;
-
内存分配失败处理:
-
malloc
在分配失败时返回NULL
指针,需要显式检查。 -
new
在分配失败时引发std::bad_alloc
异常,可以使用异常处理机制来处理。
-
int *p = new(std::nothrow) int; // 使用new,分配失败时返回nullptr,而不抛出异常
if (!p) {
// 处理分配失败情况
}
-
内存分配数量:
-
malloc
接受一个表示要分配的字节数的参数,可以分配任意数量的字节。 -
new
用于分配特定类型的对象,它的大小由编译器自动计算,因此你不需要显式指定分配多少字节。
-
MyClass *obj = new MyClass; // 编译器计算对象大小
-
内存释放:
-
使用
malloc
分配的内存应该使用free
函数来释放。 -
使用
new
分配的内存应该使用delete
运算符来释放,如果分配时使用了[]
运算符(用于数组),则应该使用delete[]
来释放。
-
// 使用malloc释放内存
free(p1);
// 使用new释放内存
delete p2;
13、引用和指针有什么区别?
-
语法和声明:
-
引用:引用是一个别名,使用
&
符号声明,通常在初始化时就要指定引用的目标对象,且一旦引用被初始化,就无法再重新绑定到其他对象。 -
int x = 10; int &ref = x; // 引用ref绑定到x
-
指针:指针是一个变量,用于存储另一个对象的内存地址,使用
*
符号声明,可以在任何时候改变指针所指向的对象。 -
int x = 10; int *ptr = &x; // ptr指向x的地址
-
-
空引用和空指针:
- 引用:引用在声明时必须初始化,不存在空引用的概念。如果试图创建一个未初始化的引用,会导致编译错误。
- 指针:指针可以声明而不初始化,形成空指针。空指针的值通常为
nullptr
或NULL
。
-
对象绑定:
- 引用:引用在初始化时必须绑定到一个对象,并且一旦绑定,无法再更改其绑定对象。这使得引用在函数参数传递和返回值上非常有用,因为它们可以提供更直观的语法。
- 指针:指针可以在任何时候指向不同的对象,因此更灵活。这也使得指针在动态内存分配和数组操作等场景中非常有用。
-
操作符和语法:
- 引用:引用使用
.
运算符来访问对象的成员(对于类和结构体),并且不需要使用间接解引用运算符*
。 - 指针:指针使用
->
运算符来访问对象的成员,或者使用*
运算符来间接访问指向的对象。
- 引用:引用使用
-
空值处理:
- 引用:引用不能表示空值或空对象,因为引用必须在初始化时绑定到一个有效的对象。
- 指针:指针可以为空(
nullptr
或NULL
),表示不指向任何有效的对象,这在处理可能不存在的对象时很有用。
-
传递方式:
- 引用:通过引用传递参数可以实现按引用传递,允许函数修改原始对象的值。这通常用于避免拷贝大型对象。
- 指针:通过指针传递参数可以实现按指针传递,允许函数修改原始对象的值。指针传递需要显式地使用指针运算符
*
来访问对象。
14、有用过lambda表达式吗?它和函数指针有什么区别?
-
语法和可读性:
-
Lambda表达式:Lambda表达式使用一种更简洁的语法,允许你在代码中定义匿名函数。Lambda表达式通常更容易阅读和编写。
-
auto add = [](int a, int b) { return a + b; };
-
函数指针:函数指针需要显式声明一个函数,并将其地址赋给指针,这通常需要更多的代码,并且可读性较差。
-
int add(int a, int b) { return a + b; } int (*functionPtr)(int, int) = add;
-
-
捕获外部变量:
-
Lambda表达式:Lambda表达式可以捕获其作用域内的外部变量,使得在Lambda内部可以访问这些变量。
-
int x = 10; auto lambda = [x](int a) { return a + x; };
-
函数指针:函数指针无法轻松地捕获外部变量,通常需要通过函数参数传递外部数据。
-
-
类型推导:
-
Lambda表达式:Lambda表达式可以使用
auto
来推导返回值类型,使得代码更加简洁和灵活。 -
auto add = [](int a, int b) { return a + b; };
-
函数指针:函数指针必须显式指定返回类型,这可能会增加代码的复杂性。
-
-
内联和优化:
- Lambda表达式:编译器通常可以对Lambda表达式进行内联优化,将Lambda的代码嵌入到调用处,提高性能。
- 函数指针:函数指针的调用通常需要跳转到函数的地址,可能不如Lambda表达式那么高效。
更多的也可以看我最近刚总结的那一篇可调用对象。
15、有用过多线程吗?用过哪些线程库?
C++多线程大家可以去另一篇去了解了解:
目前常见的线程库有下边几个,供大家参考,详细一点的知识可以去一些官方的地方学习。
- C++11标准线程库(std::thread): C++11引入了标准线程库,允许你创建、控制和同步线程。它提供了
std::thread
类,以及一系列的线程管理和同步工具,如std::mutex
、std::condition_variable
等。这是C++标准库的一部分,因此可以在支持C++11标准的编译器上使用。 - POSIX线程库(pthread): POSIX线程库是一种跨平台的线程库,可以在多种操作系统上使用。它提供了一套C函数,用于创建和管理线程。虽然它不是C++标准库的一部分,但在许多系统上具有广泛的支持。
- Boost.Thread库: Boost.Thread是Boost C++库的一部分,提供了类似于C++11标准线程库的功能,但可以在较早的C++标准和编译器上使用。它包括
boost::thread
类和其他线程相关的组件。 - Intel Threading Building Blocks(TBB): TBB是Intel开发的一款并行编程库,提供高级的并行算法和数据结构,以便利用多核处理器。它可以用于C++编程,并提供了一些并行模式,如并行循环和并行任务。
- OpenMP: OpenMP是一种用于共享内存并行编程的API,它使用编译器指令来指定并行区域。虽然不是线程库,但它可以让程序员方便地并行化循环和任务。
16、什么时候用条件变量,什么时候用线程锁?
条件变量(Condition Variable): 条件变量用于线程之间的协作,它允许线程等待某个条件的发生,然后在条件满足时被唤醒。条件变量通常与线程锁一起使用,以实现更复杂的同步需求。条件变量主要用于以下情况:
- 等待事件发生: 当一个线程需要等待某个条件变为真时,它可以进入等待状态,释放锁,然后等待其他线程通过条件变量发出信号通知它条件已经满足。
- 通知其他线程: 当一个线程完成某个任务并且条件已满足时,它可以通过条件变量发送信号通知其他线程,以唤醒它们并使它们能够执行相关操作。
- 避免忙等待: 条件变量允许线程进入休眠状态,而不是忙等待某个条件的满足,这可以节省CPU资源。
- 线程间的协作: 条件变量常用于多线程之间的协作,例如生产者-消费者问题,其中生产者线程通知消费者线程何时可以消费数据。
线程锁(Mutex): 线程锁是一种互斥机制,用于保护共享资源,确保一次只有一个线程可以访问共享资源。线程锁主要用于以下情况:
- 保护共享资源: 当多个线程需要访问共享资源(如共享变量或数据结构)时,可以使用线程锁来确保在任何给定时间只有一个线程可以修改或访问资源,从而防止竞态条件。
- 临界区保护: 临界区是一段代码,只能由一个线程同时执行。线程锁可以用来标记临界区,以确保同一时刻只有一个线程可以进入临界区执行代码。
- 避免数据竞争: 线程锁可以防止数据竞争,当多个线程尝试同时写入或读取共享数据时,可以使用锁来协调它们的操作,避免数据不一致性。
区别和使用场景:
- 使用线程锁是为了保护共享资源的互斥访问,以防止数据竞争和并发问题。线程锁通常与临界区一起使用,以限制同时访问共享资源的线程数量。
- 使用条件变量是为了实现线程之间的协作和等待,以满足某个特定条件。条件变量通常与线程锁一起使用,以确保在等待条件时释放锁,以允许其他线程修改共享资源。
17、进程间的通信方式?
- 管道(Pipe): 管道是一种半双工的IPC方式,用于在父进程和子进程之间传输数据。管道通常用于具有父子关系的进程之间,其中一个进程将数据写入管道,而另一个进程从管道中读取数据。
- 命名管道(Named Pipe,FIFO): 命名管道是一种允许不具有亲缘关系的进程之间通信的方式。它是一种特殊的文件,可以在文件系统中创建,并通过文件名来访问。
- 消息队列(Message Queue): 消息队列是一种进程间通信方式,允许不同进程之间发送和接收消息。消息队列通常用于实现进程之间的异步通信。
- 共享内存(Shared Memory): 共享内存允许多个进程访问相同的物理内存区域,因此它是一种高效的IPC方式。但需要谨慎处理同步问题,以避免数据竞争。
- 信号(Signal): 信号是一种轻量级的IPC方式,用于通知进程发生了某个事件。信号通常用于处理异步事件,如进程终止或某个条件的发生。
- 套接字(Socket): 套接字是一种网络编程中常用的IPC方式,允许不同主机上的进程之间进行通信。套接字通常用于实现分布式应用程序。
- 文件锁(File Lock): 文件锁是一种通过文件系统进行IPC的方式,允许多个进程协调对共享文件的访问。
- 信号量(Semaphore): 信号量是一种计数器,用于控制多个进程对共享资源的访问。它可以用于解决资源分配和同步的问题。
- 共享文件(Shared File): 多个进程可以通过共享文件来进行通信。这通常需要一种协议来确保对文件的互斥访问。
- RPC(远程过程调用)和RMI(远程方法调用): RPC和RMI允许一个进程调用另一个进程中的函数或方法,从而实现远程通信。
这里简单说说这些进程间的通信方式,这些也都是我之前有总结过的笔记,可以点击去看看:
18、有学过数据结构课程吗?怎么判断一个链表是否为环形链表?
可以使用快慢指针。
- 定义两个指针,一个称为慢指针(slow),初始时指向链表的头节点;另一个称为快指针(fast),初始时也指向链表的头节点。
- 使用一个循环,不断迭代链表中的节点,每次循环中,慢指针移动一步,而快指针移动两步。
- 如果链表是环形的,那么快指针和慢指针最终会相遇在某个节点上。如果链表不是环形的,那么快指针会在某一时刻到达链表的末尾(即快指针指向nullptr),此时可以确定链表不是环形的。
- 判断是否为环形链表的条件是,如果快指针和慢指针相遇了,就说明链表是环形的;如果快指针到达末尾了,就说明链表不是环形的。
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
bool hasCycle(ListNode *head) {
if (!head || !head->next) {
// 链表为空或只有一个节点,肯定不是环形的
return false;
}
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
slow = slow->next; // 慢指针移动一步
fast = fast->next->next; // 快指针移动两步
if (slow == fast) {
// 快慢指针相遇,链表是环形的
return true;
}
}
// 快指针到达末尾,链表不是环形的
return false;
}
19、怎么判断循环队列是空的还是满的?
判断循环队列是否为空或满,通常需要使用两个指针(或索引):一个用于指示队列的头部,另一个用于指示队列的尾部。
- 判断队列为空: 循环队列为空的条件是队列中没有任何元素。可以使用两种方法来判断队列是否为空:
- 方法一:使用一个额外的计数器变量,记录队列中的元素数量。当计数器为0时,队列为空。
- 方法二:使用两个指针(或索引),一个指向队列的头部(front),另一个指向队列的尾部(rear)。如果这两个指针指向相同的位置,则队列为空。
以下是方法二的示例代码:
class MyCircularQueue {
public:
MyCircularQueue(int k) {
data.resize(k);
size = k;
front = 0;
rear = 0;
count = 0;
}
bool isEmpty() {
return count == 0;
}
bool isFull() {
return count == size;
}
// 其他队列操作的实现
// ...
private:
vector<int> data;
int size;
int front;
int rear;
int count;
};
-
判断队列为满: 循环队列为满的条件是队列中元素的数量等于队列的容量。同样,可以使用两种方法来判断队列是否为满:
-
方法一:使用一个额外的计数器变量,记录队列中的元素数量。当计数器等于队列的容量时,队列为满。
-
方法二:使用两个指针(或索引),一个指向队列的头部(front),另一个指向队列的尾部(rear)。如果在插入元素后,rear 指针的下一个位置等于 front 指针,队列为满。
-
以下是方法二的示例代码:
class MyCircularQueue {
public:
MyCircularQueue(int k) {
data.resize(k);
size = k;
front = 0;
rear = 0;
count = 0;
}
bool isEmpty() {
return count == 0;
}
bool isFull() {
return count == size;
}
// 其他队列操作的实现
// ...
private:
vector<int> data;
int size;
int front;
int rear;
int count;
};
20、简单描述一个冒泡排序算法的实现原理?
- 遍历待排序的元素列表,从第一个元素开始。
- 比较当前元素和下一个元素的大小,如果当前元素大于下一个元素(升序排序),则交换它们的位置,使得较大的元素向后移动。
- 继续比较和交换相邻元素,直到遍历到列表的末尾。此时,列表中的最大元素已经被移动到了末尾位置。
- 重复步骤1至步骤3,但不包括已经排好序的末尾元素。每次遍历都会找到一个未排序部分的最大元素,并将其移动到末尾。
- 继续进行多次遍历,每次遍历都会将一个最大的元素移动到未排序部分的末尾,直到整个列表都已排序。
- 当没有发生交换操作时,可以提前结束排序,因为列表已经完全有序。
冒泡排序的时间复杂度为O(n^2),其中n是待排序元素的数量。
21、如果给你一串英文单词,如何将单词都翻转过来?
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
// 函数用于翻转一个单词
std::string reverseWord(const std::string& word) {
std::string reversedWord = "";
for (int i = word.length() - 1; i >= 0; i--) {
reversedWord += word[i];
}
return reversedWord;
}
// 函数用于翻转一串英文单词中的每个单词
std::string reverseWords(const std::string& text) {
std::stringstream ss(text);
std::string word;
std::vector<std::string> words;
// 使用空格分割输入文本中的单词
while (ss >> word) {
words.push_back(reverseWord(word));
}
// 重新组合翻转后的单词
std::string reversedText = "";
for (const std::string& reversedWord : words) {
if (!reversedText.empty()) {
reversedText += " ";
}
reversedText += reversedWord;
}
return reversedText;
}
int main() {
std::string inputText = "Hello World";
std::string result = reverseWords(inputText);
std::cout << result << std::endl; // 输出 "olleH dlroW"
return 0;
}
以上代码算是一个逻辑比较简单的算法实现了,大家可以参考参考,这也是LeetCode上必刷的一道题,面试也是一道常考题,所以大家在学习过程中,多留意,多总结方法。
21、计算机组成原理有了解吗?(可能是因为我本科非科班所以这么问)计算机的基本组件有哪些?
- 输入设备
借助输入设备,用户可以将要处理的数据输入到计算机中。也就是说,输入设备使得用户和计算机之间创建了连接。 输入设备的功能是接收用户输入的数据,并将其转换为计算机能够处理的二进制形式。
- 存储器
计算机中存储器可分为 2 种,分别为主存储器(又称主存、内存)和辅助存储器(又称外存),它们都可以存储数据和指令。
- CPU(中央处理器)
CPU 被视为计算机的“大脑”。从上图可以看到,CPU 主要由运算器和控制器构成:
- 控制器作为计算机的指挥中枢,它可以确保计算机以正确的方式和顺序执行所有操作。
- 运算器又称算术逻辑单元,简称 ALU。CPU 负责从存储器获取用户输入的原始数据,然后交由运算器对原始数据进行加工、处理,使其转换为对用户有用的数据,最后再由 CPU 将处理后的数据发送到存储器中。
实际上 CPU 内部也包含有存储数据的介质,分别为寄存器和高速缓存,我们会在后续章节给您做详细介绍。
- 输出设备
借助输出设备,我们从计算机处获取到处理后的有用数据。 和输入设备一样,输出设备也构建起了用户和计算机之间的连接,该设备会先将计算机处理后的结果转换为用户可以理解的形式,然后反馈给用户。
22、有了解什么是大端存储,什么是小端存储吗?为什么这么设计呢?
- 大端存储(Big-Endian): 在大端存储中,最高有效字节(Most Significant Byte,MSB)位于最低地址处,而最低有效字节(Least Significant Byte,LSB)位于最高地址处。这意味着在内存中,数据的高位字节存储在低地址中,依次排列到低位字节存储在高地址中。这类似于阅读书籍时,从左到右依次阅读。
- 小端存储(Little-Endian): 在小端存储中,最低有效字节(LSB)位于最低地址处,而最高有效字节(MSB)位于最高地址处。这意味着在内存中,数据的低位字节存储在低地址中,依次排列到高位字节存储在高地址中。这类似于阅读书籍时,从右到左依次阅读。
- 大端存储的优点: 大端存储的主要优点是直观性。在大端存储中,内存中的数据排列方式与人类阅读习惯一致,因此在调试和理解数据时更容易。此外,大端存储在网络通信中的应用更为广泛,因为大多数网络协议都要求数据以大端字节序传输。
- 小端存储的优点: 小端存储在某些情况下可以提供更高的性能。例如,x86 架构的处理器采用小端存储方式,而且在访问多字节数据时,它可以更高效地进行处理。此外,小端存储在一些嵌入式系统和移动设备中得到广泛采用。
23、路由器和交换机有什么区别?
- 路由器(Router):
- 路由器是一种网络设备,用于连接不同的网络,并在这些网络之间传递数据包。
- 它工作在网络层(第三层)和传输层(第四层)之间,具有路由和转发数据包的功能。
- 路由器可以识别不同网络上的设备,并根据目标 IP 地址将数据包从一个网络传递到另一个网络。
- 路由器通常用于连接不同的局域网(LAN)或广域网(WAN),并在它们之间进行数据路由,允许不同网络上的设备进行通信。
- 交换机(Switch):
- 交换机是一种网络设备,用于在局域网(LAN)内部进行数据交换。
- 它工作在数据链路层(第二层),负责处理局域网上的数据帧,并根据目标 MAC 地址将数据帧从一个端口转发到另一个端口。
- 交换机通常用于构建局域网,以提供高速和低延迟的数据交换,以便局域网内的设备之间可以快速通信。
- 交换机通常用于内部网络,而路由器用于连接不同网络之间。
24、tcp和udp有什么区别?
- 连接性:
- TCP是面向连接的协议。在建立通信之前,TCP会建立一个连接,确保数据的可靠传输,然后在通信结束后关闭连接。这种连接性保证了数据的可靠性,但会引入一定的延迟。
- UDP是无连接的协议。它不会建立连接,直接将数据包发送到目标,不保证数据的可靠性,但具有低延迟的优势。
- 数据可靠性:
- TCP提供数据的可靠传输。它使用确认机制和重传来确保数据的完整性和可靠性。如果数据包在传输过程中丢失或损坏,TCP会重新发送丢失的数据。
- UDP不提供数据的可靠性。它将数据包发送出去,但不保证它们的到达。丢失、重复或无序的数据包在UDP中是常见的,应用程序需要自行处理。
- 流量控制:
- TCP具有流量控制机制,可以根据接收端的处理能力来调整数据的发送速率,以避免过载。
- UDP没有流量控制机制,发送方将数据包发送出去,不考虑接收方的处理速度,可能导致网络拥塞。
- 顺序性:
- TCP保持数据的顺序性。发送的数据包将按照发送顺序在接收端被重建。
- UDP不保证数据包的顺序性。数据包可能以不同的顺序到达接收端。
- 头部开销:
- TCP头部较大,包含许多控制信息,这增加了数据包的大小。
- UDP头部较小,只包含少量的必要信息。
- 适用场景:
- TCP适用于需要可靠数据传输的应用程序,如网页浏览、电子邮件、文件传输等。
- UDP适用于对数据传输延迟要求较高的应用程序,如音频和视频流、在线游戏等。
25、当网络不稳定的时候对网络数据包有什么影响?
- 丢包(Packet Loss): 网络不稳定时,数据包可能会丢失,这意味着它们在传输过程中消失了。丢包可以导致数据的不完整性,需要在应用层进行处理,通常通过协议的重传机制来恢复丢失的数据。
- 延迟(Latency): 网络不稳定可能导致数据包的传输延迟增加。延迟是数据从发送端到接收端所需的时间,高延迟可能会影响实时性要求高的应用,如实时通信或在线游戏。
- 抖动(Jitter): 抖动是指数据包在传输过程中的延迟不稳定,导致数据包以不规则的时间间隔到达。抖动对音视频流或实时通信应用尤其有害,因为它会导致声音或图像的不连贯性。
- 带宽限制(Bandwidth Limitation): 网络不稳定可能导致带宽限制,即可用带宽变小。这会影响数据传输速度,导致数据包传输变得更慢。
- 重排序(Packet Reordering): 在不稳定的网络中,数据包的到达顺序可能会被打乱,需要在接收端进行重排序,以确保数据的正确顺序。
- 连接中断(Connection Drops): 网络不稳定可能导致连接中断,即通信双方之间的连接被中断。这需要重新建立连接,并可能导致数据丢失。
- 冲突和碰撞(Collisions): 在某些情况下,网络不稳定可能导致数据包冲突和碰撞,这会损坏数据包并导致丢失。
- 协议调整(Protocol Adaptation): 有些应用程序和协议可以通过自适应机制来适应网络不稳定,例如调整数据传输速率或采用错误纠正机制来处理丢失的数据包。
26、基于udp怎么设计出相对可靠的传输呢?
- 应用层重传(Application-Level Retransmission): 在发送方,你可以实现一个应用层的重传机制。当接收方未收到数据包的确认或者收到的数据包损坏时,发送方可以周期性地重传丢失或损坏的数据包。这样可以确保数据的可靠传输,但会增加网络延迟。
- 数据包编号和确认(Packet Numbering and Acknowledgment): 给每个发送的数据包分配一个唯一的序号,并要求接收方发送确认消息,指示它已成功接收到数据包。发送方可以通过确认消息来确定哪些数据包需要重传。
- 超时和重传(Timeout and Retransmission): 发送方可以设置一个超时时间,如果在超时时间内没有收到确认消息,就认为数据包丢失,然后进行重传。超时时间的选择需要根据网络延迟和丢包率来调整。
- 冗余数据(Redundant Data): 发送方可以发送冗余数据,例如发送多个副本,以增加数据包的到达概率。接收方可以使用这些副本中的一个来恢复丢失的数据。
- 错误检测和纠正(Error Detection and Correction): 添加校验和或纠错码来检测和纠正损坏的数据包。这可以在一定程度上提高数据的可靠性。
- 流量控制和拥塞控制(Flow Control and Congestion Control): 如果数据包在网络中拥塞或丢失,可以实现流量控制和拥塞控制机制来降低发送速率,以减少数据包丢失的可能性。
27、网络协议体系有几层?http属于哪一层?
- 物理层(Physical Layer): 负责传输原始比特流,控制物理介质的特性,例如电压、频率等。
- 数据链路层(Data Link Layer): 主要处理数据包的帧化、物理地址寻址、错误检测和纠正等功能。它将原始比特流组织成帧,以便在物理介质上传输。
- 网络层(Network Layer): 负责路由数据包,将数据包从源主机传输到目标主机。它使用逻辑地址(如IP地址)来确定数据包的下一个跳。
- 传输层(Transport Layer): 提供端到端的通信,负责数据的分段、传输控制和错误恢复。TCP和UDP是在传输层工作的协议。
- 会话层(Session Layer): 负责建立、维护和终止会话,以便应用程序之间的通信。通常在应用程序中实现。
- 表示层(Presentation Layer): 负责数据的编码、压缩、加密和解密,以确保数据在不同系统间的兼容性。
- 应用层(Application Layer): 最高层,提供各种应用程序的服务和协议。HTTP、FTP、SMTP等应用层协议都属于这一层。
HTTP(超文本传输协议)属于应用层。它是一种用于在Web上传输数据的协议,用于请求和响应Web页面、图像、视频和其他资源。HTTP建立在传输层的TCP协议之上,通过浏览器与Web服务器之间的通信来实现。虽然HTTP本身不处理数据的传输和路由,但它定义了Web上数据的请求和响应的规范,使Web应用能够运作。
28、最近在学习哪些新的技术
29、学校课题中遇到的最难的技术难点是什么
30、你对未来几年的规划是什么样子的
31、如果入职一家公司会考虑哪些因素
反问:
1、接下还有来几面 不确定(据说一般一面技术面,二面问项目,三面hr)
2、什么时候出结果 过几天在官网可查
3、入职了做哪方面工作 c++岗位和部门很多,具体工作内容还得等待分配
4、询问面试评价和建议 优点:基础扎实 缺点:需要深入了解
自我感受:
面试纯八股文,有些内容在上一个问题中提到了下个问题还会问让我觉得对方并不是很认真听我说了什么,对方口音有些重,我两三次没听明白问题,没开摄像头,语气较温和,觉得我答差不多了就ok问下一个。
这是我第二次面试,感觉比第一次还是进步很多了,没有怯场,虽然半夜紧张到睡不着,后来想到“我只要把我知道的表达出来告诉对方就好了”就没那么紧张了,大家也可以试试。紧张的部分原因也是cvte27号晚上七点才发邮件约我28号早上面试,觉得准备时间好短,担心自己准备不够充分,所以大家在秋招期间平常就要好好准备,多多锻炼自己的表达能力,相信自己,只要把自己知道的全部表达完整、清晰就好!
来源:https://www.nowcoder.com/discuss/536876532724256768