冲进银行测开,扛住了!
大家好,我是小林。
最近招商银行的 24 届秋招已经开始了,有很多同学问我 C++ 能投银行吗?
银行的技术大多数都是 Java,但是我看银行后端开发和测开岗位的要求:熟悉Java/C++中至少一门编程语言。
所以,C++同学也是可以投银行开发的。今天分享,一位c++同学面招商银行测开岗位的面经,共问了 20+ 个问题,测开问的问题其实跟后端开发差不多,还是编程语言+数据库+计算机基础+算法这些,只是难度不会太深。
当然,还会额外问几个测试相关的问题,比如针对某个场景,你会如何设计测试用例?
所以,投了测开岗位的同学,可以去补充学习下这类的测试相关内容。
C++
1. float/double所占字节分别是多少?
float
类型占用4个字节,double
类型占用8个字节。
其他变量大小如下图:
2. 深拷贝和浅拷贝的区别?
浅拷贝是指将一个对象的值复制到另一个对象,包括对象中的数据和指向动态分配内存的指针。这意味着原始对象和拷贝对象将共享同一块内存,当其中一个对象修改数据时,另一个对象也会受到影响。这可能导致潜在的问题,尤其是在释放内存时可能会发生错误。
深拷贝是指创建一个新的对象,并复制原始对象的所有数据和指针指向的数据。这意味着原始对象和拷贝对象将拥有彼此独立的内存空间,彼此之间的修改不会相互影响。深拷贝通常需要在拷贝过程中分配新的内存,并将原始对象的数据复制到新的内存中。
3. ++i和i++的区别?
++i
和i++
都是C++中的自增运算符,它们的区别在于它们的行为和返回值。
++i
是前置自增运算符,它会先将变量i
的值加1,然后返回加1后的值。也就是说,++i
会先执行自增操作,再使用自增后的值。i++
是后置自增运算符,它会先返回变量i
的当前值,然后再将i
的值加1。也就是说,i++
会先使用当前值,再执行自增操作。
下面是一个示例代码,展示了++i
和i++
的使用:
#include <iostream>
int main() {
int i = 0;
std::cout << "Before increment: " << i << std::endl;
int result1 = ++i;
std::cout << "After pre-increment: " << i << std::endl;
std::cout << "Result of pre-increment: " << result1 << std::endl;
int result2 = i++;
std::cout << "After post-increment: " << i << std::endl;
std::cout << "Result of post-increment: " << result2 << std::endl;
return 0;
}
输出结果将是:
Before increment: 0
After pre-increment: 1
Result of pre-increment: 1
After post-increment: 2
Result of post-increment: 1
可以看到,++i
先将i
的值加1,再返回加1后的值,而i++
先返回i
的当前值,再将i
的值加1。
4. 多态是什么?怎么实现的?
C++的多态是通过虚函数(virtual function)和指向基类的指针或引用来实现的。在基类中声明虚函数,派生类中重写该函数,通过基类指针或引用调用该函数,就可以实现运行时多态。
多态的实现原理主要涉及到两个概念:虚函数表(vtable)和虚函数指针(vptr)。每个含有虚函数的类,或者从这样的类派生的类,都有一个虚函数表。这个表中存储了虚函数的地址。类的对象中包含一个虚函数指针,指向这个虚函数表。当我们通过基类的指针或引用调用虚函数时,实际上是通过这个虚函数指针找到虚函数表,然后在表中查找并调用相应的函数。这个过程是在运行时完成的,所以可以实现运行时多态。
多态性的实现主要依靠两个机制:继承和虚函数。
继承:派生类可以继承基类的属性和方法。通过继承,派生类可以具有基类的行为和特征。 虚函数:在基类中声明一个虚函数,派生类可以对该虚函数进行重写。通过使用虚函数,可以在运行时根据实际对象的类型来调用相应的函数,而不是根据指针或引用的类型。
实现多态的步骤如下:
定义基类:定义一个基类,并在其中声明一个或多个虚函数。 派生类:从基类派生出一个或多个派生类,并在派生类中重写基类的虚函数。 使用基类指针或引用:使用基类类型的指针或引用来引用派生类对象。这样做可以根据实际对象的类型来调用相应的函数。
下面是一个简单的示例代码,展示了多态的实现:
#include <iostream>
class Animal {
public:
virtual void makeSound() {
std::cout << "Animal makes a sound." << std::endl;
}
};
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Dog barks." << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "Cat meows." << std::endl;
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->makeSound(); // Output: Dog barks.
animal2->makeSound(); // Output: Cat meows.
delete animal1;
delete animal2;
return 0;
}
在上述代码中,Animal
是基类,Dog
和Cat
是派生类。Animal
类中声明了一个虚函数makeSound
,派生类Dog
和Cat
分别重写了这个虚函数。
在main
函数中,通过基类指针animal1
和animal2
分别引用了Dog
和Cat
对象,并调用了makeSound
函数。由于makeSound
函数是虚函数,所以根据实际对象的类型,调用了相应的函数。
输出结果将是:
Dog barks.
Cat meows.
可以看到,通过多态性,我们可以根据实际对象的类型来调用相应的函数,而不需要显式地判断对象的类型。这样可以提高代码的灵活性和可维护性。
5. 重载和重写的区别?
重载(overload)即函数重载:根据函数的参数列表的不同,可以定义多个同名函数。重载函数可以有不同的参数类型、参数个数或参数顺序。编译器根据函数调用时提供的参数来确定调用哪个重载函数。重载函数的返回类型可以相同也可以不同。
重载有两个常见的问题:
第一个:一个类方法名和参数数量、类型和顺序都是一样的,但是返回值类型不一样,是否构成重载?答案是不构成,因为重载不以返回值类型不同作为函数重载的条件。 第二个问题,一个方法加了 const 和不加 const 是否构成重载?答案是构成重载的
重写(Override)是指在派生类中重新定义基类的虚函数。重写函数具有相同的函数名、参数列表和返回类型。通过重写,派生类可以改变基类虚函数的实现,以适应派生类的特定需求。重写函数必须与基类函数具有相同的签名(函数名、参数列表和返回类型),并且使用override
关键字进行显式标记。
6. 引用和指针的区别?
指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。
而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。
它们之间有几个主要的不同:
不存在空引用。引用必须连接到一块合法的内存。 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。 引用必须在创建时被初始化。指针可以在任何时间被初始化。 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
MYSQL
7. 数据库操作基本的语句
数据库操作基本的语句包括:
创建数据库:
CREATE DATABASE database_name;
删除数据库:
DROP DATABASE database_name;
创建表:
CREATE TABLE table_name (
column1 datatype,
column2 datatype,
...
);删除表:
DROP TABLE table_name;
插入数据:
INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...);更新数据:
UPDATE table_name
SET column1 = value1, column2 = value2, ...
WHERE condition;删除数据:
DELETE FROM table_name
WHERE condition;查询数据:
SELECT column1, column2, ...
FROM table_name
WHERE condition;排序数据:
SELECT column1, column2, ...
FROM table_name
ORDER BY column1 ASC/DESC;连接表:
SELECT column1, column2, ...
FROM table1
INNER JOIN table2
ON table1.column = table2.column;
8. 索引是什么?优点及缺点
在MySQL中,索引是一种用于提高查询效率的数据结构。它类似于书籍的目录,可以帮助数据库系统快速定位和访问数据。
索引的优点包括:
提高查询速度:索引可以加快数据库的查询速度,通过使用索引,数据库可以快速定位到满足查询条件的数据,而不需要逐行扫描整个表。 减少IO操作:索引可以减少磁盘IO操作,因为数据库可以直接通过索引定位到数据所在的磁盘位置,而不需要扫描整个表。 加速排序:如果查询需要对结果进行排序,索引可以提供有序的数据,从而加快排序操作的速度。
索引的缺点包括:
占用存储空间:索引需要占用额外的存储空间,特别是在大规模数据表中创建复合索引时,可能会占用较大的存储空间。 增加写操作的开销:当对表进行插入、更新或删除操作时,索引需要被更新,这会增加写操作的开销。 增加索引维护的成本:当表中的数据发生变化时,索引需要被维护,包括索引的创建、更新和删除操作,这会增加数据库的维护成本。
9. 内连接和外连接区别?
在MySQL中,内连接(Inner Join)和外连接(Outer Join)是用于联接(Join)多个表的操作。
内连接是通过匹配两个表之间的共同值,返回满足连接条件的行。只有在两个表中都存在匹配的行时,才会返回结果。内连接可以使用关键字JOIN
或INNER JOIN
来表示。
外连接是根据连接条件返回满足条件的行,并且包括未匹配的行。外连接分为左外连接(Left Outer Join)、右外连接(Right Outer Join)和全外连接(Full Outer Join)。
左外连接返回左表中所有的行,以及右表中与左表匹配的行。如果右表中没有匹配的行,则返回NULL值。 右外连接返回右表中所有的行,以及左表中与右表匹配的行。如果左表中没有匹配的行,则返回NULL值。 全外连接返回左表和右表中所有的行,如果没有匹配的行,则返回NULL值。
总结区别:
内连接只返回两个表中匹配的行,而外连接返回匹配的行以及未匹配的行。 内连接的结果集是两个表的交集,而外连接的结果集是两个表的并集。 内连接不包含NULL值,而外连接可能包含NULL值。
10. 什么是数据库存储过程?
数据库存储过程是一种在数据库中存储和执行的一组预定义的SQL语句。它可以看作是一段可重复使用的程序代码,用于封装和执行特定的数据库操作和业务逻辑。
存储过程通常由一系列SQL语句、流程控制语句(如条件判断和循环)、变量定义和参数等组成。它们可以接收输入参数、执行一系列的操作,并返回结果。
操作系统
11. 常用的linux指令有哪些
1.文件相关(mv mkdir cd ls) 2.进程相关( ps top netstate ) 3.权限相关(chmod chown useradd groupadd) 4.网络相关(netstat ip addr) 5.测试相关(测试连通性:ping 测试端口连通性:telnet)
12. 删除文件A/移动文件A到B命令是什么?
删除文件A的命令通常是在命令行中使用"rm"命令,例如:
rm A
这将删除当前目录下的文件A。
移动文件A到B的命令通常是使用"mv"命令,例如:
mv A B
这将把文件A移动到目标位置B,并且文件A在原始位置将被删除。如果目标位置B已经存在同名文件,则会覆盖该文件。如果目标位置B是一个目录,则文件A将被移动到该目录下。
13. 中断和异常的区别?
中断是由外部事件触发的,而异常是由程序内部错误触发的。
中断是指来自外部设备或其他程序的异步事件,它会打断当前正在执行的程序,引起操作系统的注意。中断可以是硬件中断(如定时器中断、键盘输入中断)或软件中断(如系统调用)。当中断事件发生时,操作系统会中断当前程序的执行,保存当前上下文,并转而处理中断事件。处理完中断事件后,操作系统会恢复被中断的程序的执行。
异常是指在程序的执行过程中发生的一些意外或非法的事件,如除零错误、访问非法内存等。异常通常是由程序内部的错误引起的,它会导致程序无法正常继续执行。当异常事件发生时,操作系统会中断当前程序的执行,保存当前上下文,并转而处理异常事件。处理完异常事件后,操作系统可能会终止异常程序的执行或采取其他措施进行处理。
网络
14. tcp和udp区别?
TCP提供可靠的、面向连接的数据传输,适用于对数据完整性和顺序性要求较高的场景;UDP提供快速、无连接的数据传输,适用于实时性要求较高、数据丢失可以容忍的场景。
连接性:TCP是面向连接的协议,而UDP是无连接的协议。TCP在通信之前需要建立连接,而UDP不需要建立连接,可以直接发送数据。 可靠性:TCP提供可靠的数据传输,它使用确认、重传和流量控制等机制来确保数据的完整性和顺序性。UDP不提供可靠性保证,它只是简单地将数据报发送出去,不关心是否到达目的地。 速度和效率:UDP比TCP更加轻量级,没有TCP的连接建立和断开的开销,以及可靠性机制的开销。因此,UDP通常比TCP更快速和高效,适用于实时性要求较高的应用,如音频、视频流传输等。 数据包顺序:TCP保证数据包按照发送的顺序进行传输,接收端按照顺序重新组装数据。UDP不保证数据包的顺序,接收端收到数据包后按照接收顺序处理。 适用场景:TCP适用于对数据传输可靠性要求较高的应用,如网页浏览、文件传输等。UDP适用于对实时性要求较高、数据丢失可以容忍的应用,如语音通话、视频直播等。
15. 为什么tcp是三次握手?
相信大家比较常回答的是:“因为三次握手才能保证双方具有接收和发送的能力。”
这回答是没问题,但这回答是片面的,并没有说出主要的原因。
在前面我们知道了什么是 TCP 连接:
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。
所以,重要的是为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接。
接下来,以三个方面分析三次握手的原因:
三次握手才可以阻止重复历史连接的初始化(主要原因) 三次握手才可以同步双方的初始序列号 三次握手才可以避免资源浪费
原因一:避免历史连接
我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
我们考虑一个场景,客户端先发送了 SYN(seq = 90)报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100)报文(注意!不是重传 SYN,重传的 SYN 的序列号是一样的)。
看看三次握手是如何阻止历史连接的:
客户端连续发送多次 SYN(都是同一个四元组)建立连接的报文,在网络拥堵情况下:
一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK
报文给客户端,此报文中的确认号是 91(90+1)。客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文。 服务端收到 RST 报文后,就会释放连接。 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。
上述中的「旧 SYN 报文」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。
如果是两次握手连接,就无法阻止历史连接,那为什么 TCP 两次握手为什么无法阻止历史连接呢?
我先直接说结论,主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
你想想,在两次握手的情况下,服务端在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED 状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回 RST 报文来断开连接,而服务端在第一次握手的时候就进入 ESTABLISHED 状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST 报文后,才会断开连接。
可以看到,如果采用两次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。
因此,要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。
所以,TCP 使用三次握手建立连接的最主要原因是防止「历史连接」初始化了连接。
原因二:同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
接收方可以去除重复的数据; 接收方可以根据数据包的序列号按序接收; 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN
报文的时候,需要服务端回一个 ACK
应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
原因三:避免资源浪费
如果只有「两次握手」,当客户端发生的 SYN
报文在网络中阻塞,客户端没有接收到 ACK
报文,就会重新发送 SYN
,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK
报文,所以服务端每收到一个 SYN
就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端发送的 SYN
报文在网络中阻塞了,重复发送多次 SYN
报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN
报文,而造成重复分配资源。
小结
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用「两次握手」和「四次握手」的原因:
「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号; 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
16. cookie session token区别?
Cookie,Session和Token都是用于识别用户身份的技术,但它们的工作方式和使用场景有所不同:
Cookie:是服务器发送到用户浏览器并保存在浏览器上的一块数据,主要用于记录用户的一些信息。每次浏览器向服务器发送请求时,都会自动带上这个Cookie数据。
Session:是在服务器端保存的一个数据结构,用来跟踪用户的状态。这个数据可以保存在集群、数据库、文件中等。用户浏览器的每一次请求,服务器都会根据这个Session来识别用户状态。
Token:是服务端生成的一串字符串,作为客户端进行请求的一个凭证。当用户第一次登录后,服务器生成一个Token返回给客户端,以后客户端只需带上这个Token来请求数据,无需再次登录验证。
主要区别在于:
Cookie和Session是服务器用来识别用户的,而Token是无状态的,它不需要在服务端保存用户状态。 Cookie数据存放在客户的浏览器上,Session数据放在服务器上。 Token设计目的是为了减轻服务器压力,不需要在服务器保存会话信息。Token更适用于移动应用和单页面应用(SPA)。
17. get 和 post的区别?
根据 RFC 规范,GET 的语义是从服务器获取指定的资源,这个资源可以是静态的文本、页面、图片视频等。GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。
比如,你打开我的文章,浏览器就会发送 GET 请求给服务器,服务器就会返回文章的所有文字及资源。
根据 RFC 规范,POST 的语义是根据请求负荷(报文body)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制。
比如,你在我文章底部,敲入了留言后点击「提交」(暗示你们留言),浏览器就会执行一次 POST 请求,把你的留言文字放进了报文 body 里,然后拼接好 POST 请求头,通过 TCP 协议发送给服务器。
如果从 RFC 规范定义的语义来看:
GET 方法就是安全且幂等的,因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。所以,可以对 GET 请求的数据做缓存,这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),而且在浏览器中 GET 请求可以保存为书签。 POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。所以,浏览器一般不会缓存 POST 请求,也不能把 POST 请求保存为书签。
测试
18. 黑盒测试有哪些方法?
黑盒测试是一种软件测试方法,它不考虑内部实现细节,只关注软件的输入和输出。以下是一些常见的黑盒测试方法:
等价类划分(Equivalence Partitioning):将输入数据划分为等价类,选择代表性的测试用例来覆盖每个等价类。这样可以有效地减少测试用例的数量,同时保证测试覆盖。 边界值分析(Boundary Value Analysis):关注输入的边界值,选择接近边界的测试用例。边界值往往是导致错误的关键点,因此测试边界值可以发现潜在的问题。 决策表测试(Decision Table Testing):根据不同的条件和规则,创建决策表,覆盖不同的组合情况。这种方法适用于有多个条件和规则的场景。 状态转换测试(State Transition Testing):针对有状态的系统,定义不同的状态和状态转换规则,设计测试用例来覆盖不同的状态转换路径。 错误推测测试(Error Guessing):基于测试人员的经验和直觉,猜测可能存在的错误,并设计测试用例来验证这些猜测。这种方法比较主观,依赖于测试人员的经验。 功能点测试(Function Point Testing):根据软件的功能点,设计测试用例来验证每个功能点的正确性和完整性。 用户界面测试(User Interface Testing):关注软件的用户界面,测试用户界面的交互和响应是否符合预期。
这些方法可以单独使用,也可以结合使用,根据具体的测试目标和需求选择适合的方法进行黑盒测试。
19. 一个输入月份的框,测试点有哪些?
以从以下几个方面考虑:
正常值测试:输入正常的月份值,例如1-12。 边界值测试:测试最小值和最大值,即1和12。 错误值测试:输入非法的月份值,例如0、13、-1、100等。 非数字测试:输入非数字字符,例如字母、符号等。 空值测试:不输入任何值,直接提交。 数据类型测试:输入浮点数、长整数等看是否可以接受。 输入长度测试:如果有长度限制,可以测试输入超过限制的长度。 用户界面测试:检查输入框的布局、字体、颜色等是否符合规范。 性能测试:快速连续输入看是否会出现卡顿或者崩溃。 兼容性测试:在不同的浏览器、操作系统上测试该输入框的表现。
20. 发朋友圈怎么测试?功能测试重点关注什么?
发朋友圈的功能测试可以关注以下几个方面:
文本输入:测试输入各种字符、符号、表情、链接等,检查是否可以正常显示和发布。 图片/视频上传:测试上传各种格式、大小、分辨率的图片和视频,检查是否可以正常显示和发布。 定位功能:测试是否可以正确获取和显示位置信息。 评论和点赞功能:测试用户是否可以对朋友圈进行评论和点赞,是否可以看到其他人的评论和点赞。 删除和编辑功能:测试用户是否可以删除和编辑自己的朋友圈。 隐私设置:测试用户是否可以设置谁可以看到自己的朋友圈,是否可以看到自己被屏蔽的朋友圈。 通知功能:测试发布朋友圈后,是否可以正确发送通知给好友。 性能测试:测试在网络环境差、设备性能低的情况下,是否可以正常使用发朋友圈的功能。 兼容性测试:测试在不同的设备、操作系统、浏览器上,发朋友圈的功能是否正常。 安全性测试:测试是否存在安全漏洞,如私密朋友圈被未授权的人查看等。
其他
为测开这个岗位做了什么准备? 你对自己的职业规划是什么?