一面,面了一个钟,问了很多问题,大部分是计算机的基础知识,我也只能记录下一部分。
-
C++的继承问题,protected成员被public、protected和private继承的情况。
下面是关于protected成员在不同类型继承中的访问权限:-
public继承
如果使用public继承,基类中的protected成员将在派生类中变为protected或public成员,取决于在派生类中的声明方式。如果在派生类中声明为protected,则仍然是protected成员,如果声明为public,则将变为public成员。 -
protected继承
如果使用protected继承,基类中的protected成员将在派生类中变为protected成员。 -
private继承
如果使用private继承,基类中的protected成员将在派生类中变为private成员。
-
-
静态函数以及访问类中静态函数的方式
静态函数与普通的成员函数不同,静态函数不属于任何特定的对象实例,而是与类本身相关联。静态函数可以直接通过类名调用,而无需先创建类的实例。在函数内部,不能直接使用this指针来引用对象成员,因为静态函数没有对应的对象实例。但可以通过类名和作用域解析符来访问类的静态成员变量和静态函数。 -
多态的安全性
多态在C++中是一种非常有用的特性,可以提高代码的可维护性和可扩展性。然而,多态也会带来一些安全性问题,主要有以下两个方面:-
内存安全:多态中,如果基类的指针指向了一个派生类的对象,并且调用了派生类中未定义的虚函数,就会导致内存访问越界或者访问非法内存的风险。为了避免这种情况,可以使用纯虚函数或者默认实现虚函数,以确保所有派生类都实现了这些虚函数。
-
类型安全:多态中,基类指针可以指向任何派生类对象,因此需要确保基类和派生类之间的类型转换是安全的。如果类型转换不安全,就会导致程序崩溃或者产生未定义行为。为了确保类型转换的安全性,可以使用动态类型转换(dynamic_cast)或者在派生类中重载基类的类型转换运算符。
总的来说,多态在C++中是一种非常有用的特性,但是也需要开发者在使用时注意内存安全和类型安全的问题,以确保程序的正确性和稳定性。
拓展:“基类的指针指向了一个派生类的对象,并且调用了派生类中未定义的虚函数,为什么会使内存访问越界?”
这是因为在C++中,虚函数是通过虚函数表来实现的。每个对象都有一个指向虚函数表的指针,虚函数表中存储着每个虚函数的地址。当派生类重写了基类中的虚函数时,派生类会在其虚函数表中替换掉基类虚函数的地址。这样,在调用虚函数时,会根据对象指针所指向的虚函数表来调用相应的函数。
如果基类的指针指向了一个派生类的对象,并且调用了派生类中未定义的虚函数,那么在虚函数表中就找不到相应的函数地址,会导致程序访问了不存在的内存地址,从而引起内存访问越界的问题。这种情况下,由于调用的虚函数地址未定义,程序行为是未定义的,可能会导致程序崩溃或产生其他不可预测的行为。 -
-
NULL和nullptr的区别
在C++中,NULL和nullptr都用于表示一个指针类型的空值,但它们在实现和语义上有所不同。NULL是C++早期版本中使用的指针空值表示方式,通常定义为0或(void*)0。然而,这种定义在一些情况下可能会导致歧义,因为0还可以用作整数类型的值。例如,下面的代码片段可能会编译通过,但实际上它存在潜在的问题:
int *p = NULL; if (p == 0) { // ... }
在这个例子中,p指针被赋值为0,然后在if语句中与0进行比较。尽管这段代码的意图是比较指针是否为空,但实际上它比较的是指针是否等于整数0,这可能导致错误。
为了解决这个问题,C++11引入了一个新的关键字nullptr,用于表示一个空指针值。nullptr被定义为一个特殊的字面量,可以转换为任何指针类型。与NULL不同,nullptr不能隐式转换为整数类型。
因此,使用nullptr可以帮助我们避免指针和整数之间的混淆,提高代码的可读性和安全性。以下是使用nullptr的示例代码:
int *p = nullptr; if (p == nullptr) { // ... }
在这个例子中,p指针被赋值为nullptr,然后在if语句中与nullptr进行比较。由于nullptr不能隐式转换为整数类型,因此这个比较操作只能比较指针是否为空,而不会出现与整数0混淆的问题。
-
override和final
在面向对象编程中,override
和final
都是关键字,用于指示派生类重载基类函数的意图。override
表示派生类中重写了基类中同名函数,而final
表示该函数不能被再次重载。在C++11中,通过将一个函数声明为
override
,可以使编译器检查派生类中是否有对应的基类函数被重载。而
final
则用于防止进一步重载一个函数。例如,如果使用final
关键字在基类函数中声明一个函数,则派生类无法覆盖该函数,这意味着它将具有最终决定权。 -
进程之间怎么共用一把锁
进程之间可以通过共享内存或者网络通信的方式来实现锁的共享。下面分别介绍这两种方式:-
共享内存
进程可以将锁对象存储在共享内存中,多个进程可以同时访问这个共享内存中的锁对象。进程需要通过特定的机制来访问共享内存,例如System V共享内存或者POSIX共享内存。 -
网络通信
进程可以通过网络通信来实现锁的共享。例如,可以创建一个服务进程来维护锁,其他进程需要获取锁时,可以向服务进程发送请求。服务进程可以维护一个队列来管理进程的请求,并将锁的状态发送给请求者,以确保只有一个进程能够获得锁。
无论哪种方式,都需要确保在多个进程之间同步访问共享的锁对象,以避免竞态条件和死锁等问题。因此,需要使用一些同步机制来保护共享的锁对象,例如信号量、互斥量、条件变量。
-
-
C++的符号表
C++中的符号表(Symbol Table)是编译器在编译过程中用来管理程序中标识符(如变量、函数、类、枚举等)的数据结构。符号表通常用于存储和检索程序中定义的标识符的相关信息,例如名称、类型、作用域、地址等。
符号表是编译器在多个编译阶段中使用的重要数据结构,包括词法分析、语法分析、语义分析、类型检查、代码生成等阶段。它用于记录源代码中定义的标识符的属性,以便在后续的编译阶段中进行引用和检查。符号表的主要功能包括:
-
标识符的声明和定义管理:记录标识符的名称、类型、作用域等信息,以便在后续的编译阶段中进行引用和检查。符号表可以帮助编译器检测标识符的重复定义、类型不匹配等错误。
-
标识符的作用域管理:记录标识符的作用域(如全局作用域、局部作用域等),以便在编译过程中解析标识符的作用域和可见性。符号表可以帮助编译器正确处理标识符的作用域和解析规则,如识别局部变量和全局变量、处理命名空间等。
-
标识符的类型管理:记录标识符的数据类型信息,包括基本类型(如整型、浮点型等)和用户定义的类型(如类、枚举等),以便在编译过程中进行类型检查和类型推断。符号表可以帮助编译器检测类型不匹配、未声明的标识符等错误。
-
标识符的属性管理:记录标识符的其他属性,如存储类别(如自动变量、静态变量等)、存储位置(如栈、堆等)、地址信息等,以便在生成目标代码时进行优化和地址分配。
符号表通常以哈希表、树等数据结构来实现,支持快速的插入、查找和删除操作。在编译器的各个编译阶段中,符号表会不断地被更新和扩充,以便管理程序中的标识符,并为后续的编译处理提供必要的信息。
-
-
socket的bind(0)
在socket编程中,bind()函数被用来将一个socket与一个特定的IP地址和端口号绑定。如果在bind()函数中将端口号设置为0,那么操作系统会自动选择一个未被使用的端口号来绑定这个socket。这个特性通常被称为“动态绑定”或者“通配绑定”。在某些情况下,使用动态绑定可以使代码更加灵活和可移植。例如,在一个分布式系统中,一个节点可能需要动态绑定一个端口号以接收其他节点的请求。如果节点在启动时指定了一个固定的端口号,那么这个节点的可移植性就会受到限制,因为如果要将它部署到另一个环境中,可能需要修改它的配置文件或者重新编译它的代码。使用动态绑定可以避免这个问题。
需要注意的是,使用动态绑定可能会给网络安全带来一些隐患,因为攻击者可以通过扫描端口来发现哪些端口是被动态绑定的,从而进行攻击。因此,在某些情况下,使用固定的端口号可能更加安全。
-
C++的单元测试
单元测试是一种测试方法,用于对代码的单个模块或函数进行测试,以确保其功能正确、性能优良、稳定可靠。要对自己写的代码进行单元测试,可以按照以下步骤进行:
-
确定测试目标:选择需要测试的函数或模块,并确定测试的目标,例如测试函数的返回值、异常处理、性能等。
-
编写测试用例:根据测试目标编写测试用例,包括输入数据、预期输出结果、异常处理等。
-
编写测试代码:编写测试代码,调用被测试函数或模块,并验证其输出结果是否符合预期。
-
运行测试代码:运行测试代码,并查看测试结果,如果测试失败,则需要进行调试和修复。
-
重复测试:对于每个测试用例,都需要进行多次测试,以确保测试结果的稳定性和可靠性。
-
整合测试:将所有单元测试整合到一个测试套件中,并定期运行测试套件,以确保代码的质量和稳定性。
在编写单元测试时,需要注意以下几点:
-
测试用例应该尽可能地覆盖代码的各个分支和边界情况,以确保代码的正确性和鲁棒性。
-
测试代码应该与被测试代码分离,以确保测试的独立性和可重复性。
-
测试代码应该易于编写和维护,以便快速迭代和更新测试用例。
-
测试代码应该具有清晰的输出和日志,以便快速定位和修复问题。
总之,单元测试是一种重要的测试方法,可以提高代码的质量和稳定性,减少错误和漏洞的出现。
-
-
http的请求头有哪些内容
常见的HTTP请求头包括以下内容:
1. Host:指定请求的目标主机名和端口号。
2. User-Agent:浏览器或其他客户端应用程序的名称和版本号。
3. Accept:指定客户端能够接收的MIME类型。
4. Accept-Language:指定客户端语言偏好,比如zh-CN代表简体中文。
5. Accept-Encoding:指定客户端能够接受的内容编码方式,比如gzip或deflate。
6. Connection:指定是否保持连接以及如何保持连接,比如keep-alive或close。
7. Authorization:指定客户端的授权信息,例如用户名和密码。
8. Referer:指定页面请求的来源,对于哪些页面基于外部链接访问的、或者是一个跳转流程中的请求,此字段总是携带。
9. Content-Type:指定请求发送的实体数据的MIME类型。
10. Content-Length:指定请求发送的实体数据的大小(字节)。
11. Cookie:指定客户端的Cookie信息。
12. Cache-Control:指定请求和响应的缓存机制。 -
weak_ptr是不是线程安全的
weak_ptr 本身是线程安全的,因为它是只读的,多个线程可以同时读取它的值而不会产生竞争条件。
然而,使用 weak_ptr 可能需要谨慎地处理线程安全问题。例如,如果一个 weak_ptr 指向的对象可能被多个线程同时访问,那么在使用该 weak_ptr 时需要确保该对象已经被正确地保护起来了,否则可能会导致访问到已经被销毁的对象,从而引发未定义行为。这可以通过使用适当的同步机制(如互斥量或原子操作)来实现。 -
TCP三次握手,为什么挥手要四次
TCP连接的关闭过程是需要四次握手的。这是因为在TCP连接中,两端都可能存在未发送完的数据或者确认信息,所以TCP的关闭过程需要通过四次握手来确保每一方都已经完全关闭了连接,避免出现数据丢失或者丢失确认导致连接一直保持着的情况。具体来说,挥手过程如下:
-
主动关闭方A向被动关闭方B发送一个FIN(FIN=1,ACK=0),表示A不再发送数据给B了。
-
被动关闭方B收到FIN后,发送一个ACK(ACK=1,FIN=0)给A,表示B已经收到A的关闭请求了。
-
B再向A发送一个FIN(FIN=1,ACK=1),表示B也不会再向A发送数据了,但是B可能还需要接收A的一些数据或者确认信息。
-
A收到B的FIN后,发送一个ACK(ACK=1,FIN=0)给B,表示A已经收到B的关闭请求,并且连接已经完全关闭了。
总结起来,四次握手的目的是确保双方都已经完成数据的传输并关闭了连接,以避免可能的数据丢失或错乱。
-
-
什么会造成死锁
要同时满足死锁的四个必要条件:互斥、不剥夺、请求并保持和循环等待。 -
网络拥塞会发生什么?
网络拥塞是指在网络中传输的数据量超出了网络的容量或者处理能力,导致网络性能下降、延迟增加、数据丢失等问题。当网络拥塞时,网络传输的速率将变得非常慢,因为大量的数据包需要在网络中等待处理。同时,网络中的路由器和交换机可能会因为无法处理过多的数据包而崩溃或重启。数据包可能会丢失或出现乱序,导致数据传输错误或失败。
在企业环境中,网络拥塞可能会导致业务停滞,影响生产效率;在互联网中,网络拥塞可能会导致网站访问缓慢或无法访问,影响用户体验和收入。
为了避免网络拥塞,网络管理员通常会实施流量控制、拥塞控制等策略,以确保网络能够有效地传输数据。
-
程序到可执行文件经历的阶段
程序到可执行文件经历以下阶段:预处理、编译、汇编、链接。- 预处理:对源文件进行预处理(就是将一些宏定义或者内联函数进行替换等),其中包括宏定义、头文件包含、条件编译等操作,生成预处理后的源文件。
- 编译:将预处理后的源文件翻译成汇编代码,生成汇编代码文件。
- 汇编:将汇编代码翻译成机器码(二进制目标文件),生成目标文件。
- 链接:将目标文件与系统库和其他目标文件链接,生成可执行文件。
在链接阶段,将目标文件与系统库和其他目标文件链接在一起,生成可执行文件。在这个过程中,还会进行符号解析、重定位等操作。最终生成的可执行文件包含了程序的所有代码和数据,可以被操作系统加载运行。
拓展: gcc无法进行库文件的连接,即链接(程序到可执行文件经历的阶段的第四阶段)这一步无法完成;而g++则能完整执行四个阶段,编译出可执行文件。(实质上,g++从步骤1-步骤3均是调用gcc完成,步骤4连接则由自己完成)
-
gcc和g++的区别
gcc 和 g++ 都是 GNU 编译器集合中的一部分。gcc 是 GNU C 语言编译器,而 g++ 是 GNU C++ 语言编译器。两者的主要区别在于:- 对于 C++ 代码,g++ 会将代码传递给 C++ 解析器,而gcc 传递给 C 解析器。
- g++ 默认开启 C++ 标准库,而 gcc 不需要,需要手动添加即可。
- g++ 会自动链接 C++ 标准库,而 gcc 不会。
- g++ 触发C++ 异常处理时要快一些。
- 除了以上区别,gcc 和 g++ 在编译器特性、编译选项和命令行语法等方面也有一些微小的差异。
总的来说,gcc 主要用于编译 C 代码,而 g++ 主要用于编译 C++ 代码。如果你需要编译 C++ 代码,使用 g++ 编译器是更好的选择。有一篇不错的博文
-
vector的扩容机制
在 C++ 语言中,vector 是一种动态数组,会自动扩容,以适应动态添加和删除元素的需要。vector 的扩容是基于以下原理:- 当 vector 的元素个数等于其容量时,vector 会重新分配一段更大的内存,一般是原内存的两倍。
- 将原来的元素复制到新的内存中,并释放原来的内存。
- 在新的内存中添加新的元素。
扩容过程中,如果系统内存有限导致无法分配更大的内存,则扩容失败。因此,在设计 vector 容量时应考虑使用最合适的初始容量和扩容因子,以降低扩容的次数和代价,并提高 vector 的性能。
二面,面了40分钟,也是没记全。
-
谈谈对Lambda表达式的理解,它可以捕获哪些类型
Lambda的基础知识,可以看我这个链接。
Lambda 表达式可以捕获以下几种类型的变量:- 值捕获:以值的形式拷贝捕获变量。
- 引用捕获:以引用的形式捕获变量。
- 隐式捕获:通过外部变量的上下文进行捕获。
- 显式捕获:使用方括号 “[]” 显式指定捕获的变量列表。
在捕获变量时,需要注意避免捕获过多的临时变量和自由变量,并合理选择捕获方式,以提高 Lambda 表达式的性能和可读性。
-
乐观锁和悲观锁
乐观锁和悲观锁都是数据库并发控制的技术,用于解决多个并发事务同时访问同一数据时可能出现的数据冲突问题。悲观锁是一种悲观的思想,即认为并发访问会导致数据冲突,因此在访问数据时会对数据加锁,保证只有当前事务可以对数据进行修改。悲观锁适用于并发访问比较频繁、数据冲突可能性较大的场景,如高并发的交易系统。
乐观锁则是一种乐观的思想,即认为并发访问不会导致数据冲突,因此不会对数据加锁,而是在更新数据时通过版本号等方式判断数据是否被其他事务修改过。如果数据未被修改,则允许当前事务更新数据,否则会回滚当前事务。乐观锁适用于并发访问比较少、数据冲突可能性较小的场景,如数据采集系统。
悲观锁的优点是能够保证数据的正确性和一致性,缺点是会影响系统的并发性能。乐观锁的优点是能够提高系统的并发性能,缺点是可能会出现更新冲突,需要进行重试或回滚操作。因此,在使用乐观锁或悲观锁时,需要根据实际业务场景进行选择和调整。
-
线程调度方法
- 抢占式调度:操作系统会根据线程的优先级、状态、等待时间等因素进行调度,优先级高的线程会被先执行,但是也有可能被其他优先级更高的线程抢占执行。
- 时间片轮转调度:操作系统分配给每个线程一个时间片,当时间片用完后,操作系统会中断当前线程的执行,并将其放到队列的末端,等待下次调度。
- 优先级反转调度:当一个高优先级的线程需要等待低优先级的线程时,操作系统会将低优先级的线程提升至高优先级的级别,以避免高优先级线程一直等待。
- 随机调度:操作系统会随机选择一个可以执行的线程来运行,这种方式用于避免线程优先级的不平衡,保证每个线程有平等的机会被执行。
- 实时调度:针对实时系统中的任务设计的调度方式,会根据任务的截止时间进行调度,以保证任务能够在规定的时间内完成。
这些调度方式可以单独使用,也可以结合使用,根据具体应用场景选择适当的方式。
-
有了解自旋锁吗?
自旋锁是一种轻量级的同步机制,通常用于多线程环境中保护临界区,其工作原理是在竞争锁的情况下,线程不会阻塞等待锁的释放,而是一直循环测试,直到获得锁为止。由于自旋锁不涉及线程的切换和上下文切换,因此适用于对响应时间要求较高的场景。但是,在竞争锁的情况下,自旋锁会导致CPU大量消耗,因此在高并发场景下需要考虑使用其他同步机制。 -
在TCP完成三次握手的情况下,如果把网线拔了,对端知不知道?
如果在TCP完成三次握手之后,网线被拔掉,那么对端是会知道的。这是因为在三次握手之后,TCP连接已经建立,对端会不断发送心跳包来维持连接。当网线被拔掉后,对端发送的心跳包会一直未被回复,因此对端会认为连接已经断开,并立即关闭连接。 -
TCP四次握手中为什么要有TIME-WAIT
在TCP四次握手的过程中,主动关闭方发送完最后一个ACK报文后会进入TIME-WAIT状态,等待2MSL(最长报文段寿命)时间后才会关闭连接。这个等待时间的目的是为了保证网络中所有报文都被对端接收和处理完毕,从而避免后续的报文出现在已经关闭的连接中,导致出现问题。具体来说,主动关闭方进入TIME-WAIT状态后,它会等待2MSL时间。在这个时间内,主动关闭方可以收到对端可能发送的延迟报文,如果对端没有发送任何报文,那么2MSL时间到后,主动关闭方就可以安全地关闭连接了。
同时,TIME-WAIT状态还可以避免在网络中出现“失序”的报文。如果主动关闭方不进入TIME-WAIT状态,而是直接关闭连接,那么在这个时间段内,网络中可能还有一些旧的报文没有被处理完毕,这些报文可能会被错误地发送到新的连接中,导致出现问题。因此,TIME-WAIT状态可以保证连接的可靠关闭,避免出现网络问题。
-
谈谈设计模式(单例和工厂)
-
构造函数和析构函数可以是虚函数吗,为什么
构造函数不可以,析构函数可以且常常是。构造函数不能是虚函数的原因是因为虚函数的调用依赖于对象的创建,而构造函数在对象创建过程中执行,因此构造函数无法成为虚函数。在调用虚函数时,程序需要先创建一个对象并将其指针传递给函数,而构造函数在对象创建的过程中就已经被调用,因此构造函数不能是虚函数。
另一方面,析构函数可以是虚函数的原因是因为它在对象被销毁时执行,此时对象的类型已经确定,因此可以在对象销毁时调用正确的析构函数。如果析构函数不是虚函数,那么当使用基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致内存泄漏和程序行为不正常。因此,将析构函数声明为虚函数可以确保在对象被销毁时调用正确的析构函数。
-
GET和POST的区别
- get主要用来获取数据,post主要用来提交数据。
- get的参数有长度限制,最长2048字节,而post没有限制。
- get的参数会附加在url之后 ,以 " ? "分割url和传输数据,多个参数用 "&"连接,而post会把参数放在http请求体中。
- get是明文传输,可以直接通过url看到参数信息,post是放在请求体中,除非用工具才能看到。
- get请求会保存在浏览器历史记录中,也可以保存在web服务器日志中。
- get在浏览器回退时是无害的,而post会再次提交请求。
- get请求会被浏览器主动缓存,而post不会,除非手动设置。
- get请求只能进行url编码,而post支持多种编码方式。
- get请求的参数数据类型只接受ASCII字符,而post没有限制。
- get请求通常没有请求体,而post请求可能包含一些表单数据或JSON等数据。
-
怎么判断大端小端
- 可以使用联合体
- 使用位运算判断
- 使用宏
群面就是根据公司产品,分小组讨论产品所用到的技术和改进方案,这个过程中要积极参与讨论,并要抓住机会表达。
HR面大部分是牛客上常看到的哪些问题,问性格、家庭等。
一张照片证明自己曾经来过,哈哈哈哈
终面是过了的,本来以为会有体验实习,但最后是岗位匹配面试,问的是项目。面的部门只招一个人,有可能我流程比较久吧,从三月初我就投递,全部流程走完已经五月初了,没啥岗位了。面试没过之后,hr说会反馈给校招组,看看有没有其他部门要招,我过些天再问,就说已经没hc了。