富途后端C++一面
总体时间1h多几分钟
面试官人感觉挺不错的,全程语气都挺温和,不会为难
1、手撕 字符串去除相邻重复的字母
#include <iostream>
#include <string>
std::string removeAdjacentDuplicates(const std::string& input) {
std::string result;
for (char currentChar : input) {
// 如果结果字符串为空或者当前字符与结果字符串的最后一个字符不同,将当前字符添加到结果字符串中
if (result.empty() || currentChar != result.back()) {
result += currentChar;
}
}
return result;
}
int main() {
std::string input = "aaabbcdddeefffgghhh";
std::string result = removeAdjacentDuplicates(input);
std::cout << "Original String: " << input << std::endl;
std::cout << "Processed String: " << result << std::endl;
return 0;
}2、简单问了点项目
3、问动态库?
动态库(Dynamic Link Library,DLL)是一种包含可执行代码和数据的文件,它可以在运行时被加载到内存中并供多个程序共享使用。与静态库不同,动态库在程序运行时动态加载,这意味着多个程序可以共享同一个动态库文件,从而节省磁盘空间和内存,并使库的更新更加方便。
4、怎么找到动态库?
要找到动态库,需要告诉编译器和链接器库的位置。
- 在Linux系统上,动态库通常位于
/lib、/usr/lib或自定义的库路径下。如果你自己编译了一个动态库,库文件通常会在当前目录下或自定义的库路径下。 - 在Windows系统上,动态库通常具有
.dll扩展名,它们可以位于系统目录、应用程序目录或自定义路径下。系统目录通常是C:\Windows\System32。应用程序目录是包含可执行文件的目录。
5、找不到动态库怎么办?
- 确保库文件位于你指定的搜索路径中。你可以使用绝对路径或相对路径,但要确保路径是正确的。
- 确保你使用了正确的库文件名。在
-l选项中,通常不需要包含库文件的前缀(如lib)或扩展名(如.dll或.so)。 - 在Windows系统中,确保动态库文件位于系统的 PATH 环境变量指定的路径中。这样可以使操作系统能够找到库文件。
- 确保库文件实际存在于指定的路径中。你可以使用文件浏览器或命令行来验证。
- 如果你的库依赖于其他库,确保这些依赖库也可用,并且路径正确。如果依赖关系出现问题,可能导致库加载失败。
- 注意不同操作系统下,库文件的扩展名和路径可能会有所不同。在Linux中,通常使用
.so扩展名,而在Windows中使用.dll扩展名。
6、Makefile?
Makefile是一种用于构建和管理项目的文本文件,通常用于编译源代码并生成可执行文件、动态库、静态库等。Makefile使用一种特定的语法,其中包含了目标(target)、依赖关系(dependencies)、命令等信息,用于描述如何构建项目的各个组件。
Makefile的基本结构:
Makefile由一系列规则(rules)组成,每个规则通常包含以下部分:
- 目标(target): 指定要构建的结果的名称。可以是可执行文件、库文件等。
- 依赖关系(dependencies): 指定构建目标所依赖的文件或其他目标。如果依赖文件发生变化,目标需要重新构建。
- 命令(commands): 描述如何从依赖文件构建目标文件的具体步骤。通常使用TAB键缩进来表示命令。
一个简单的Makefile示例,用于构建一个名为 myprogram 的可执行文件:
myprogram: main.o utils.o
g++ -o myprogram main.o utils.o
main.o: main.cpp
g++ -c main.cpp
utils.o: utils.cpp
g++ -c utils.cppMakefile的使用:
要使用Makefile来构建项目,可以在项目目录中运行以下命令:
makeMake工具将自动查找名为 Makefile 或 makefile 的文件,并根据文件中的规则执行构建。如果要构建特定的目标,使用:
make target_name例如,要构建上述示例中的 myprogram,可以运行:
make myprogram变量和条件:
Makefile还支持变量的定义和条件判断,以便更灵活地管理项目。你可以定义变量来存储编译器选项、源文件列表等信息,并使用条件语句来根据不同情况执行不同的构建步骤。
示例:
CC = g++
CFLAGS = -Wall -O2
SRC = main.cpp utils.cpp
myprogram: main.o utils.o
$(CC) -o myprogram main.o utils.o
main.o: main.cpp
$(CC) $(CFLAGS) -c main.cpp
utils.o: utils.cpp
$(CC) $(CFLAGS) -c utils.cpp7、两个动态库有同样的函数,调用的是哪一个动态库的?(感觉是编译链接的顺序,但是怕不懂装懂还是说了不知道)
在C++中,如果有两个动态库(例如两个DLL文件)中都包含相同名称的函数,并且你的程序链接了这两个动态库,那么会出现函数冲突的问题。在这种情况下,调用哪一个动态库的函数取决于链接顺序和动态链接器的规则。
通常情况下,链接器会从左到右按照库的顺序搜索函数,一旦找到第一个匹配的函数,就会停止搜索。这意味着,如果你的程序链接了两个具有相同函数名的动态库,而且第一个库在命令行中出现在第二个库之前,那么程序将调用第一个库中的函数。
示例:
假设有两个动态库:libA.dll 和 libB.dll,它们都包含一个名为 myFunction 的函数。
如果你的编译命令如下:
g++ -o myprogram myprogram.cpp -L/path/to/libA -lA -L/path/to/libB -lB那么 libA.dll 中的 myFunction 将被调用,因为它在命令行中出现在 libB.dll 之前。
如果你想要调用 libB.dll 中的 myFunction,可以将链接顺序颠倒:
g++ -o myprogram myprogram.cpp -L/path/to/libB -lB -L/path/to/libA -lA这会导致 libB.dll 中的 myFunction 被调用。
要解决这个问题,一种方法是重命名其中一个动态库中的函数,以避免冲突。另一种方法是在链接时使用 -Bsymbolic 选项,这将使得在动态库中搜索函数的顺序不受影响,但这种方法可能会引入其他问题,因此需要谨慎使用。
总之,动态库中函数的调用顺序取决于链接顺序和动态链接器的规则。为了避免函数冲突问题,建议在设计动态库时使用唯一的函数名,或者使用命名空间来隔离函数。
8、问HTTP和TCP的区别?
- 协议层次:
- TCP(传输控制协议): TCP位于 OSI 模型的传输层,负责提供可靠的、面向连接的数据传输。它确保数据可靠地从一个节点传输到另一个节点,处理数据的分段、重传、流控制等问题。
- HTTP(超文本传输协议): HTTP位于 OSI 模型的应用层,是一种用于传输超文本(例如网页)的协议。HTTP依赖于底层的传输协议,通常是TCP,但也可以是其他协议(如HTTP/2使用的是TCP或TLS)。
- 目的和职责:
- TCP: TCP的主要目的是提供端到端的可靠数据传输,确保数据的可靠性和顺序性。它处理数据的分段、重传、流量控制等,但不关心数据的内容。
- HTTP: HTTP的主要目的是在客户端和服务器之间传输和交互超文本资源。它定义了请求-响应模型,允许客户端发送HTTP请求来获取服务器上的资源(例如网页、图像、视频等),并接收服务器的响应。
- 数据格式:
- TCP: TCP传输的是原始二进制数据,没有对数据的结构和语义进行解释。它只关心数据的可靠传输。
- HTTP: HTTP传输的是文本数据,通常使用ASCII字符集编码。HTTP定义了请求和响应的格式,包括请求方法、头部字段、正文等。HTTP的语义规则定义了如何解释这些数据。
- 连接性:
- TCP: TCP是面向连接的协议,它在通信开始前建立连接,然后进行数据传输,最后关闭连接。这种连接需要维护一些状态信息,如源端口、目标端口、序列号等。
- HTTP: HTTP是无状态的协议,每个HTTP请求都是独立的,服务器不会保留关于客户端的状态信息。为了保持状态,HTTP使用机制如Cookie来在请求之间传递状态信息。
- 端口:
- TCP: TCP使用端口来标识不同的服务和应用程序。每个端口都有一个特定的编号,用于将数据传递给正确的应用程序。
- HTTP: HTTP通常使用TCP的80端口(HTTP)或443端口(HTTPS)来进行通信,但可以使用其他端口,只要客户端和服务器都知道。
- 安全性:
- TCP: TCP本身不提供任何安全性保障,数据传输是明文的。但可以通过其他机制(如TLS/SSL)来在TCP上实现安全性。
- HTTP: HTTP也是明文传输的协议,但可以通过使用HTTPS(HTTP Secure)来加密通信内容,提供安全性。
9、那HTTP发送的是什么?
一种应用层协议,它用于在客户端和服务器之间传输和交互超文本资源,例如网页、图像、音频文件等。
发送的主要内容:
- 请求(Request): 客户端使用HTTP请求来获取服务器上的资源。HTTP请求通常包括以下信息:
- 请求方法(HTTP Method): 定义了客户端的操作类型,如GET、POST、PUT、DELETE等。
- 请求URL(Uniform Resource Locator): 指定了要访问的资源的地址。
- 请求头(Request Headers): 包含了请求的元信息,如用户代理、接受的内容类型、认证信息等。
- 请求体(Request Body): 通常包含了客户端发送给服务器的数据,如表单数据、JSON数据等。在GET请求中,请求体通常为空。
- 响应(Response): 服务器使用HTTP响应来回应客户端的请求。HTTP响应通常包括以下信息:
- 状态码(Status Code): 用于指示请求的结果状态,如200 OK表示成功,404 Not Found表示资源不存在,500 Internal Server Error表示服务器内部错误等。
- 响应头(Response Headers): 包含了响应的元信息,如服务器信息、响应的内容类型、响应时间等。
- 响应体(Response Body): 包含了服务器返回给客户端的实际数据,如网页内容、JSON数据等。
- 其他元信息: HTTP还可以传输其他的元信息,如Cookies、缓存控制指令、认证信息等,以控制和管理通信的各个方面。
10、HTTP报文有什么内容?(忘了)
请求报文(Request):
- 请求行(Request Line): 请求行包含了请求的方法、请求的URL和HTTP协议版本。例如:
GET /index.html HTTP/1.1请求头部(Request Headers): 请求头部包含了关于请求的附加信息,以键值对的形式表示。常见的请求头部包括:
Host:指定服务器的主机名和端口号。User-Agent:包含了客户端的信息,如浏览器类型和版本。Accept:指定客户端可接受的响应内容类型。Cookie:包含了客户端的会话信息。Authorization:用于进行身份验证的信息。
空行(Blank Line): 空行用于分隔请求头部和请求体,通常只包含一个回车换行符。
请求体(Request Body): 请求体包含了客户端向服务器发送的数据。这部分在一些请求中可能为空,例如GET请求。在POST请求中,请求体通常包含表单数据、JSON数据或其他内容。
响应报文(Response):
- 状态行(Status Line): 状态行包含了响应的HTTP协议版本、状态码和状态消息。例如:
HTTP/1.1 200 OK响应头部(Response Headers): 响应头部包含了关于响应的附加信息,以键值对的形式表示。常见的响应头部包括:
Content-Type:指定响应的内容类型,如text/html、application/json等。Content-Length:指定响应内容的长度(字节数)。Set-Cookie:设置在客户端的Cookie。Location:用于重定向,指定新的URL。
空行(Blank Line): 空行用于分隔响应头部和响应体,通常只包含一个回车换行符。
响应体(Response Body): 响应体包含了服务器返回给客户端的数据,例如HTML页面、JSON数据、图片等。
11、问哈希表(不太了解)
于存储键值对(Key-Value Pairs)并支持高效的插入、删除和查找操作。它通过将键(key)映射到数组的索引来实现这些操作,使得查找的时间复杂度通常为常数时间 O(1)。
- 哈希表的核心是哈希函数,它接受一个键作为输入,然后将该键映射到数组的一个索引位置。这个映射的过程是快速的,通常需要常数时间。哈希函数的设计和性能直接影响了哈希表的性能。
- 哈希表通常由一个数组和一组哈希函数组成。每个数组位置称为“桶”,每个桶可以存储一个或多个键值对。当多个键映射到同一个桶时,通常会使用链表、红黑树等数据结构来解决冲突。
- 哈希冲突:
- 开放地址法(Open Addressing): 尝试将键放入其他空闲桶,直到找到一个空桶或遍历完所有桶。
- 链地址法(Chaining): 在每个桶中使用链表或其他数据结构存储多个键值对,具有相同哈希值的键都存储在同一个桶中。
- 哈希表的操作
- 插入(Insertion): 将一个键值对插入哈希表。
- 查找(Lookup): 根据键查找对应的值。
- 删除(Deletion): 根据键删除对应的键值对。
- 哈希表的应用
- 数据库中的索引结构。
- 缓存数据的存储。
- 编程语言中的集合和字典数据结构(如Python中的字典)。
- 分布式系统中的数据分片和路由。
- 哈希表还用于密码学中的散列函数,如SHA-256、MD5等。
12、哈希表怎么实现的?
手动实现哈希表步骤:
- 选择合适的哈希函数:哈希函数应该将关键字映射到一个特定的桶中,最好是均匀地分布在哈希表中。常见的哈希函数有除余法(mod)、乘法散列等。在实现哈希函数时,应该考虑到哈希表的大小和负载因子等因素。
- 选择适当的存储结构:哈希表通常使用数组来实现,每个数组元素对应一个桶,存储与该桶对应的关键字。如果桶中有多个关键字,可以使用链表或二叉搜索树等结构来解决哈希冲突。
- 实现哈希表的基本操作:哈希表的基本操作包括插入、查找和删除。在实现这些操作时,需要先通过哈希函数计算出关键字对应的桶,然后根据具体的存储结构进行操作。
示例:
#include <iostream>
#include <list>
using namespace std;
class HashTable {
private:
int tableSize;
list<int> *table;
int hashFunction(int key) {
return key % tableSize;
}
public:
HashTable(int size) {
tableSize = size;
table = new list<int>[tableSize];
}
void insert(int key) {
int index = hashFunction(key);
table[index].push_back(key);
}
bool search(int key) {
int index = hashFunction(key);
for (auto it = table[index].begin(); it != table[index].end(); it++) {
if (*it == key) {
return true;
}
}
return false;
}
void remove(int key) {
int index = hashFunction(key);
table[index].remove(key);
}
~HashTable() {
delete[] table;
}
};
int main() {
HashTable ht(7);
ht.insert(10);
ht.insert(20);
ht.insert(15);
cout << ht.search(10) << endl; // 输出1,表示存在关键字10
cout << ht.search(30) << endl; // 输出0,表示不存在关键字30
ht.remove(10);
cout << ht.search(10) << endl; // 输出0,表示关键字10已被删除
return 0;
}14、为什么用哈希表不用链表和数组?
- 快速查找和插入: 哈希表在查找和插入操作上具有常数时间复杂度 O(1) 的优势,这意味着无论哈希表的大小如何,操作的时间开销都是可接受的。相比之下,链表和数组的查找操作通常需要线性时间复杂度 O(n),其中 n 是元素的数量。
- 空间利用率: 哈希表通常具有较高的空间利用率。虽然哈希表的大小可能会动态调整以适应元素的增长,但由于哈希函数的作用,它不会像数组一样需要提前分配大块内存。链表通常需要额外的指针来维护节点之间的关系,因此在存储大量元素时可能会浪费一些内存。
- 键值对的关联性: 哈希表适用于键值对(Key-Value Pairs)的存储和检索。每个键都通过哈希函数映射到一个唯一的位置,因此可以根据键快速找到对应的值。这在许多实际应用中非常有用,如数据库索引、缓存和字典数据结构。
但是,链表和数组也有它们的优点和适用场景。例如,链表适用于需要频繁的插入和删除操作的情况,因为在链表中插入和删除节点的时间开销较小。数组适用于元素数量固定或者大小已知的情况,因为它们可以在内存中连续存储元素,具有较好的内存局部性。
15、哈希表快还是数组快,为什么?
哈希表和数组各有其优点和适用场景,它们的性能快慢取决于具体的操作和使用情境。
哈希表的优点:
- 快速查找和插入: 哈希表在查找和插入操作上具有常数时间复杂度 O(1) 的性能。这意味着无论哈希表的大小如何,操作的时间开销都是恒定的。
- 适应动态数据: 哈希表可以根据数据的动态增长自动扩展,而无需提前分配大块内存。这使得它适用于需要频繁插入和删除的情况。
- 键值对关联: 哈希表适用于存储键值对,并能够根据键快速查找对应的值。这在许多应用中非常有用,如数据库索引、缓存、字典数据结构等。
数组的优点:
- 直接访问元素: 数组具有连续的内存存储,可以通过索引直接访问元素。因此,在已知索引的情况下,访问元素非常快速,具有常数时间复杂度 O(1)。
- 节省内存: 数组通常比哈希表占用更少的内存,因为它们不需要额外的指针来维护元素之间的关系。
性能比较的考虑因素:
- 查找操作: 哈希表通常在查找操作上更快,因为它们使用哈希函数将键映射到索引,而无需遍历整个数组。
- 插入和删除操作: 哈希表在动态插入和删除操作上更快,因为数组需要移动元素来维持连续存储。
- 内存占用: 数组通常占用更少的内存,因为哈希表需要额外的内存来存储哈希函数的索引和指针。
- 数据访问模式: 如果访问模式是随机访问,并且可以通过索引快速定位元素,数组更有优势。如果是根据键进行查找和插入,哈希表更合适。
总的来说,哈希表在查找、插入和删除操作上通常更快,特别是在动态数据和大量键值对的情况下。然而,数组在直接访问元素和节省内存方面具有优势。
16、场景题1
#include <iostream>
class A {
public:
A() {
func();
}
virtual void func() {
std::cout << "A::func()" << std::endl;
}
virtual void test() {
func();
}
};
class B : public A {
public:
B() {
func();
}
virtual void func(){
std::cout << "B::func()" << std::endl;
}
};
int main() {
B b;
b.test();
A* a = &b;
a->test();
return 0;
}- 问b对象调用的是父类的func还是子类的func
b 对象调用的是子类 B 的 func()。这是因为在 B 类中重写了虚拟函数 func(),并且在 B 类的构造函数中调用了 func()。所以,当创建 B 对象 b 时,首先调用 B 类构造函数,该构造函数调用了 B 类的 func()。
- 问b.test调用的是父类的func还是子类的func
b.test() 调用的是子类 B 的 func()。虽然 test() 函数在父类 A 中声明,但由于在子类 B 中重写了虚拟函数 func(),在 test() 函数中调用的是子类 B 的版本。
- 问a->test调用的是父类的func还是子类的func
a->test() 调用的是子类 B 的 func()。虽然 a 是一个指向父类 A 的指针,但在运行时它指向的是子类 B 的对象。因此,调用 a->test() 时会根据对象的实际类型调用子类 B 的版本。
17、场景题2:
class B {
virtual void funcB(){};
};
class C {
virtual void funcC(){};
}
class A : public B, public C {
virtual void funcB(){};
virtual void funcC(){};
};
void func() {
A* a = new A();
B* b = a;
C* c = a;
}这段代码中,如A类多继承B类、C类,实例化A对象,赋值给B、C指针,A、B、C地址数值上有什么区别?
A* a = new A();创建了一个A类的对象,并将指针a指向该对象。B* b = a;创建了一个B*类型的指针b,并将a指向的A对象的地址赋值给了b。这实际上是将A对象的地址视为B类型的指针。C* c = a;创建了一个C*类型的指针c,并将a指向的A对象的地址赋值给了c。同样,这实际上是将A对象的地址视为C类型的指针。
这种情况下,虽然 A 类对象 a 的地址与 B 类指针 b 和 C 类指针 c 的地址是相同的,但在类型系统中,它们是不同的类型。这也就意味着:
b指针的静态类型是B*,因此你可以使用b指针访问B类中的成员函数和数据成员,但不能访问C类的成员。- 同样,
c指针的静态类型是C*,因此你可以使用c指针访问C类中的成员函数和数据成员,但不能访问B类的成员。 a指针的静态类型是A*,因此你可以使用a指针访问A类、B类和C类中的成员函数和数据成员,因为A类同时继承了B类和C类的成员。
18、场景题3:
事务隔离级别
SELECT a FROM tbl1 WHERE id = 1; a = 1000
事务1
BEGIN;
UPDATE tbl1 SET a = a + 200 WHERE id = 1;
COMMIT;
事务2
BEGIN;
SELECT a FROM tbl1 WHERE id = 1;
......
SELECT a FROM tbl1 WHERE id = 1;
COMMIT;事务隔离级别定义了多个事务之间的可见性和互相影响的程度。
常见的隔离级别有四种,分别是:读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
读未提交(Read Uncommitted):
- 事务1修改数据并提交,但未提交前,事务2可以读取到事务1的未提交的修改。这意味着事务2可以看到脏数据(未提交的数据)。
- 在上述示例中,事务2可以在事务1修改并提交之前读取到修改的结果。
读提交(Read Committed):
- 事务1修改数据并提交后,事务2才能读取到事务1的修改。这保证了已提交的数据对其他事务可见。
- 在上述示例中,事务2只能在事务1提交后才能读取到修改的结果。
可重复读(Repeatable Read):
- 事务1修改数据并提交后,事务2仍然看不到事务1的修改,直到事务2自身提交。这保证了事务2在整个事务过程中看到一致的数据状态,但在事务2提交之前,会出现不可重复读(同一数据在不同时间点读取结果不同)的情况。
- 在上述示例中,事务2只有在自身提交后才能读取到修改的结果,从而避免了脏读和不可重复读。
串行化(Serializable):
- 串行化是最高隔离级别,确保事务之间完全隔离,不会发生并发冲突。在串行化隔离级别下,事务1修改数据并提交后,事务2只能在事务1完成后才能执行。
- 在上述示例中,事务2只有在事务1完成(包括提交)后才能执行,从而避免了脏读、不可重复读和幻读(事务2查询到了事务1未提交的新增数据)。
但是需要注意的是,隔离级别越高,系统的并发性能越低,因为隔离级别高意味着更多的锁和资源占用,导致了更多的阻塞和等待。因此,在选择隔离级别时,需要权衡数据一致性和性能之间的权衡。
总结:
- 读未提交:允许脏读、不可重复读、幻读。
- 读提交:允许不可重复读和幻读,但不允许脏读。
- 可重复读:允许幻读,但不允许脏读和不可重复读。
- 串行化:最高级别,不允许任何并发问题,但性能最差。
应该还有一些其他的,想不起来了
问如果让你转语言,比如c++换到go,该怎么学习
问有什么比较倾向的技术发展方向
问项目里有遇到什么困难吗,怎么解决的
最后反问
