C++后端面试知识
本文多数总结来自于CSDN博主「Tracker-for-1995」的文章【C++后端面试知识精选系列】,链接为:https://me.csdn.net/trackxiaoxin321。若有错误,望指正,笔者的邮箱为:wuxiaofang555555@163.com 。
1. 师兄建议
- 国企看重简历中的硬性指标,如:论文情况、比赛、专利、软著等
- Qt
- C++基础知识(较琐碎)
- 类
- 面向对象编程的特点:抽象、封装、继承、多态
- 引用的用法
- 指针的用法
- 内存泄漏
- 内存溢出
2. 内存泄漏
指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
当程序员使用new或是malloc分配内存,而忘记使用delete或是free释放内存时,容易造成内存泄漏。
怎么避免
- 使用智能指针,而不是手动地去管理内存
- 程序进程字符串处理时,避免使用string代替char*
- 使用RAII(Resource Acquisition Is Initialization,资源获取就是初始化),RAII 的做法是使用一个对象,在其构造时获取资源,最后在对象析构的时候释放资源。即把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。
- 尽可能少的使用new,若一定要使用,new/malloc分配内存,要及时delete/free释放
怎么检测
-
new和delete都是内置的操作符
void * operator new(size_t size); //new 操作符的声明 //返回一个未初始化的地址,其中size来确定分配多少内存
-
自定义跟踪new和delete的类
-
重载new和delete操作符,通过计数new的次数和delete次数,即可判断是否存在内存泄漏
-
track_new.h
#ifndef TRACK_NEW_H #define TRACK_NEW_H #include <map> void* operator new(size_t size, const char *file, long line); void* operator new(size_t size); void operator delete(void *p); class TrackerNew { class TrackerNewIn { const char *m_file; long m_line; public: TrackerNewIn(const char *file = nullptr, long line = 0) :m_file(file), m_line(line){ } ~TrackerNewIn(){ } const char *file() const { return m_file; } long line() const { return m_line; } }; class Lock { //省去m_lock_cout的自减,避免出错 TrackerNew &_track; public: Lock(TrackerNew &track) :_track(track) { _track.m_lock_count++; } ~Lock() { _track.m_lock_count--; } }; public: TrackerNew(); ~TrackerNew(); void add(void *p, const char *file, long line); void remove(void *p); void dump(); public: static bool flag; private: std::map<void*, TrackerNewIn> m_track; long m_lock_count; //为了避免无限调用 }; extern TrackerNew tracker_new; #endif TRACK_NEW_H //!TRACK_NEW_H
-
track_new.cpp
#include"track_new.h" #include <cstdlib> #include <iostream> TrackerNew tracker_new;//定义全局的类 bool TrackerNew::flag = false; void* operator new(size_t size, const char *file, long line) { void *p = malloc(size); if (TrackerNew::flag) tracker_new.add(p, file, line); return p; } void* operator new(size_t size) { void *p = malloc(size); if (TrackerNew::flag) tracker_new.add(p, "unknow", -1); return p; } void operator delete(void *p) { if (TrackerNew::flag) tracker_new.remove(p); free(p); } TrackerNew::TrackerNew() :m_lock_count(0) { TrackerNew::flag = true; } TrackerNew::~TrackerNew() { TrackerNew::flag = false; dump(); } void TrackerNew::add(void *p, const char *file, long line) { if (m_lock_count > 0) return; Lock lock(*this); m_track[p] = TrackerNewIn(file, line); /*m_lock_count++; m_track[p] = TrackerNewIn(file, line); m_lock_count--;*/ } void TrackerNew::remove(void *p) { if (m_lock_count > 0) return; Lock lock(*this); auto it = m_track.find(p); if (it != m_track.end()) { m_track.erase(it); } } void TrackerNew::dump() { for (auto it : m_track) { std::cout << "ox: " << it.first << "\t" << "file: " << it.second.file() << "\t" << "line: " << it.second.line() << std::endl; } }
-
main.cpp
#include "track_new.h" #include "debug_new.h" int main() { int *p1 = new int; delete p1; int *p2 = new int[10]; //delete[] p2; return 0; }
-
内存溢出
3. 堆和栈
-
数据结构层面
- 栈:后进先出
- 堆:二叉堆,树型结构、可任意访问
-
内存分配层面(以地址的增长方向为上)
- 栈区:处于相对较高地址,栈地址是向下增长的;栈中分配局部变量空间。
- 堆区:堆区是向上增长的用于分配程序员申请的内存空间,例如使用malloc、new申请的区域。
-
区别总结一
-
堆和栈申请方式不同
- 栈是系统自动分配空间,如定义一个
char a;
系统会自动在栈上为其开辟空间。栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉了,不能再访问了。 - 堆则是程序员根据需要自己申请的空间,如
malloc(10);
开辟十个字节的空间。堆上的数据只要程序员不释放空间,就一直可以访问,一旦顽疾释放会造成内存泄漏。
- 栈是系统自动分配空间,如定义一个
-
-
申请后系统的响应
-
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
- 堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆。结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。也就是说堆会在申请后还要做一些后续的工作这就会引出申请效率的问题。
-
申请效率的比较
- 由系统自动分配,速度较快。但程序员是无法控制的。
-
是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便
-
申请大小的限制
-
栈是向低地址扩展的数据结构,是一块连续的内存的区域。意思是栈顶的地址和栈的最大容量是系统预先规定好的。因此,能从栈获得的空间较小。
- 堆是向高地址扩展的数据结构,是不连续的内存区域。堆获得的空间比较灵活,也比较大。
-
-
堆和栈中的存储内容
- 栈: 在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
- 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
- 存取效率的比较:存取中,栈上的数据比堆上的数据快。
-
区别总结二
- 存储内容:栈存放的是局部变量和函数参数等;堆存放的是new/malloc申请的变量;
- 管理方式:栈由系统自动分配,自动释放;堆由程序员手动申请,手动释放;
- 空间大小:32位系统一般堆能达到4G,可以看做堆没有什么限制的,但栈一般是有特定大小的,windows下一般2M;
- 生长方向:栈是向下(高地址——>低地址)生长的,堆是向上生长(低地址——>高地址)的;
- 申请效率:栈由系统自动申请分配,速度较快;堆由new/malloc分配,速度较慢。
4. static
- 修饰全局变量
- 修饰全局变量,不初始化默认初始为0,存储在静态存储区,从声明到程序结束一直存在
- 修饰局部变量
- 也存储在静态存储区,作用域是局部作用域,当定义他的函数或者语句块结束时,作用域结束,但该变量并没有被销毁,只不过我们不能访问,直到该函数再次被调用
- 修饰类的数据成员
- 不属于类的对象,类内声明,类外初始化,类的静态成员变量被所有对象共享,保证了共享性,又具备安全性
- 修饰类的成员函数
- 静态成员函数只能访问静态成员变量,不能访问非静态成员函数,原因是静态成员函数不与任何对象绑定,因此不包含this指针,在静态成员函数内部不能直接使用this指针,因此不能访问类的非静态成员变量
- 修饰函数
- 即静态函数,仅在声明它的cpp文件中使用,其他文件不可使用
- 面试例子
- static关键字的作用,修饰函数有什么用?
- static修饰的函数叫静态函数,包括两种
- 静态函数出现在类里,则它是一个静态成员函数。静态成员函数不与任何对象绑定,因此不包含this指针,在静态成员函数内部不能直接使用this指针,即不能直接访问普通成员函数;静态成员函数属于类本身,在类加载时就会分配内存,可以通过类名直接去访问;虽然静态成员函数不属于类对象,但是可以通过类对象访问静态成员函数。
- 普通的全局的静态函数。限定在本源码文件中,不能在其他文件调用(普通函数的定义和声明默认情况下都是extern的,即可被其他文件调用)。故好处在于:1)其他文件中可以定义相同名字的函
- static修饰的函数叫静态函数,包括两种
- static关键字的作用,修饰函数有什么用?
5. const
-
修饰变量,使其不能被修改
-
修饰函数参数,表明输入的参数在参数内不能被修改
-
修饰指针
- 常量指针,指向的内容不能被修改,即指向“常量”的指针 , const int *p
- 指针常量,指针的指向不能改变,即指针类型的常量,int * const p
-
修饰类的成员函数,表明其是常函数,即不能修改类的成员变量,const成员函数斌调用非const成员函数,因为非const成员函数可能会修改成员变量
class A { int m_a; public: void get() const { } //函数体里不能修改成员变量 }
6. URL
- 浏览器输入www.baidu.com发生了什么?(百度面试题)
- 首先浏览器向域名系统DNS请求将域名地址转换为IP地址,然后与百度服务器建立TCP连接,发送http请求(默认端口80)请求百度首页,服务器处理请求并将首页文件发给浏览器,TCP连接释放,浏览器解析首页文件展示web界面。
7. C和C++区别
- C是面向过程的编程语言,C++是面向对象的结构化编程语言,C++是C的扩展,但C和C++确是两种不同的语言;
- C++有抽象、封装、继承、多态几大特性,C+相比于C,增加了许多类型安全的功能,同时还有功能强大STL标准库;
- 作用域,C中只有局部和全局,C++有局部、类作用域和名称作用域三种;
- 开辟/释放空间,C用malloc和free(函数),C++使用new和delete(关键字),new开辟空间在堆区或者自由存储区(C plus plus原话:if the object is created by using new,it resides in heap memory, or the free store and its destructor is called automatically when you use delete to free the memory),malloc开辟在堆区;
- C++有引用,C中没有;
- const不同,C中的const叫只读变量,只是无法做左值的变量;C++中的const是真正的常量,但也有可能退化成c语言的常量,默认生成local符号;
- C++灵活、可扩展、易于维护,耦合性较低,C语言耦合性较高。
8. malloc和new区别
-
malloc和new都是在堆上开辟内存的。malloc只负责开辟内存,没有初始化功能,需要用户自己初始化;new不但开辟内存,还可以进行初始化,如new int(10);表示在堆上开辟了一个4字节的int整形内存,初始值是10,再如new int[10] ();表示在堆上开辟了一个包含10个整形元素的数组,初始值都为0。
-
malloc是函数,开辟内存需要传入开辟空间的大小(字节数),返回类型为void*,表示分配的堆内存的起始地址,因此malloc的返回值需要转换成指定类型的地址;new是运算符,开辟内存需要指定类型,返回指定类型的地址,因此不需要类型转换。
int *p1 = (int*)malloc(sizeof(int)); //根据传入字节数开辟内存,没有初始化 int *p2 = new int(0); //根据指定类型int开辟一个整形内存,初始化为0 int *p3 = (int*)malloc(sizeof(int)*100); //开辟400个字节的内存,相当于包含100个整形元素的数组,没有初始化 int *p4 = new int[100](); //开辟400个字节的内存,100个元素的整形数组,元素都初始化为0
-
malloc开辟内存失败返回nullptr(空指针),new开辟内存失败抛出bad_alloc类型的异常,需要捕获异常才能判断内存开辟成功或失败,new运算符其实是operator new函数的调用,它底层实现也是调用的malloc函数,new它比malloc多的就是初始化功能,对于类类型来说,所谓初始化,就是调用相应的构造函数。
-
malloc开辟的内存是通过free来释放的;而new单个元素内存,用的是delete,如果new[]数组,用的是**delete[]**来释放内存的。
-
malloc开辟内存只有一种方式,而new有四种分别是普通的new(内存开辟失败抛出bad_alloc异常), nothrow版本的new,const new以及定位new。
-
new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构,而malloc不会
-
new/delete可以被重载,但malloc/free不能
-
new/delete 和 new[]/delete[] 在自定义的类类型对象有析构函数时,不能混用
9. 指针和引用区别
- 指针本质是地址,有自己的一块空间,引用本质是某一个变量的别名;
- 指针初始化可以为空,引用初始化必须有确定的对象(取别名的本体要存在,否则不存在别名一说);
- 指针大小为指针本身的大小,为4字节(32位机器为4,64位机器为8),引用的大小是变量的大小;
- 指针可以改变指向,但引用不行(一旦为A的别名,不能再成为B的别名);
- 引用比指针安全(指针可能存在野指针的情况);
10.C++中内存分区
-
五大分区:栈、自由存储区、堆、全局/静态存储区、常量存储区
-
栈区:编译器自行开辟自行释放的内存区,一半存放局部变量和函数参数等。
-
自由存储区:一般是malloc开辟的空间,与堆区类似,但不等价于堆区,几乎所有的编译器都用堆来实现自由存储(堆是操作系统维护的一块内存,而自由存储是C++通过new/delete实现动态内存分配和释放对象的抽象概念)。
-
堆区:由程序员手动开辟手动释放,一般是new开辟的空间(new开辟的也可能在自由存储区),如果程序员不释放,OS可能回收,但也可能造成内存泄露。
-
全局/静态存储区:存放全局变量,静态变量(static关键字修饰的),一般是被共享的。
-
常量存储区:保存常量,这部分的数据不允许被修改。
11. 野指针
-
野指针是指向不可用内存的指针(并不是指向nullptr,不能用if判断一个指针是否是野指针,良好的编程习惯是杜绝野指针的唯一方法)。
-
野指针形成原因:
-
指针变量没有被初始化。任何指针变量刚被创建时不会自动生成nullptr指针,它的指向是随机的,在创建指针时应当被初始化,要么将指针指向nullptr,要么让它指向合法的地址。
-
指针被free和delete(只是将内存释放,并没有把指针干掉)后没有置为nullptr。
-
指针操作超出了变量的作用范围,比如某个函数结束后,其局部变量会销毁,此时如果指针指向该变量,在其他地方是不能使用的。
int *p; { int a; p=&a; cout<<*p<<endl; } cout<<*p<<endl; //此时p成为野指针,因为其所指向的局部变量a过了生命期
-
12. 类的默认函数
一个空类会生成以下默认函数:默认构造函数、默认析构函数、默认拷贝构造函数、默认拷贝赋值函数
13. class和struct区别
-
class默认继承访问权限为private继承,而struct是public继承
-
class默认访问属性是private,而struct是public
-
class具有继承、多态等性质,而struct不具备
-
class能够定义模板函数,而struct不行
14. 构造函数中初始值列表的必要性
在执行构造函数时,当执行到函数体内部时,所有的数据成员就已经在内存中创建,并利用类内默认值初始化(如果类内提供了初始值)。
对于引用类型的成员或是有const修饰符的成员,必须要利用初始值列表进行初始化,否则将会错过初始化的时机,无法完成初始化。
- 必须在初始值列表中定义的数据成员
- 引用类型的成员
- 有const修饰符的成员
- 没有默认构造函数的类类型
15. 虚函数
-
虚函数是C++实现多态的重要保障,当类中有虚函数时,就存在一个虚函数指针指向虚函数表,虚函数表存放着虚函数的地址
-
派生类继承基类时,虚函数指针和虚函数表都继承了,当派生类重写基类的虚函数后,派生类的虚函数表存的地址就变成了派生类重写的虚函数地址
-
当基类指针或引用指向派生类时,就产生了多态
-
含有纯虚函数,则为抽象类,不能被实例化
virtual void get() = 0; //纯虚函数
16. 继承中的访问权限
-
派生类继承基类
class Student :public Person{ //若省略public,则默认为private继承 ... }
-
若派生类public继承基类,派生类能否访问基类中的成员,取决于其在基类中的访问限定声明;若为private,则派生类不能访问;一般我们采用protected来修饰基类中的成员,以实现在派生类中的访问权限
class Base { private: int m_pri; //private 成员, 在基类以外的地方,均不能被访问 protected: int m_pro; //protected 成员, 在派生类中可被访问,在其他地方不能被访问 public: int m_pub; //public 成员, 在类内类外均可被访问 }; class PubDerv :public Base { void foo() { m_pri = 10;//错误:不能访问 Base 类私有成员 m_pro = 1; //正确:可以访问 Base 类受保护成员 } }; void test() { Base b; b.m_pro = 10;//错误:不能访问 Base 受保护成员 }
-
protected修饰符可以看作是private和public的混合物
- 和公有成员类似,基类中受保护(protected)的成员在其派生类里是可以访问的
- 和私有成员类似,基类中受保护的成员在类外是无法访问的
17. C++防止头文件重复包含
两种方式防止头文件重复包含
-
ifndef语句,如下为myHeader.h
#ifndef MYHEADER_H #define MYHEADER_H // 头文件主题 #endif // !MYHEADER_H
-
#pragma once语句
#pragma once // 头文件主题
由于第二种是windows下的,建议使用第一种
18.TCP连接和断开
-
三次握手
-
四次挥手
19. 长连接和短连接
长连接和短连接指的是TCP的长连接和TCP的短连接。
- TCP长连接:TCP长连接指建立连接后保持连续而不断,如一段时间没有数据交互,保活定时器超时后,服务端会发送探测报文给客户端检测客户端是否在线。连接—传输数据—保活—传输数据—保活—…。
- TCP短连接:指建立并传输数据完成后立即断开。连接—传输数据—断开。
- 应用场景:长连接适用单对单通信且连接数不太多的情况,短连接适用于连接数多且经常需要更换连接对象的情况。
- 说明:HTTP/1.0中,默认使用短连接,HTTP/1.1起,默认使用长连接。HTTP的长连接也不是永久保持连接,保活计时器超时后,探测报文没回应,那么也会断开连接。
20. 进程和线程区别
- 进程是操作系统进行资源分配和调度的基本单位,而线程是操作系统进行运行调度的最小单位,包含在进程中,是进程中实际工作的单位;
- 线程存在于进程之中,与进程是多对一的关系,一个进程可以存在多个线程,这些线程共享进程资源;
- 进程有自己的独立地址空间,每启动一个进程,系统就会为他分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵;线程则共享进程的空间及数据。
- 线程之间的通信更方便(共享全局变量、静态变量等数据),而进程通信需要以通信方式(管道、消息队列、信号量、共享存储、socket、streams)进行。
- 多进程程序更健壮,多线程程序只有有一个线程死掉,整个进程也死掉了。而一个进程死掉,不影响另外一个进程,因为进程有独立空间。
21. 进程通信和线程通信
- 进程间通信方式(InterProcess Communication, IPC)
- (无名)管道(pipe):是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道(namedpipe):可以在无关进程之间交换数据。
- 消息队列(messagequeue):是消息的链接表,存放在内核中,一个消息队列由一个标识符(队列ID)来标识。消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;独立于发送与接收进程,进程终止时,消息队列及其内容不会被删除;可以实现消息的随机查询。
- 信号量(semophore):它是一个计数器,用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。用于进程间同步,若要在进程间传递数据需要结合共享内存。
- 共享内存(shared memory):指两个或多个进程共享一个给定的存储区。共享内存是最快的IPC,因为进程是直接对内存进行存取;因为多个进程可以同时操作,所以需要同步;信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
- 套接字(socket):是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
- 总结
- 管道:速度慢,容量有限,只有父子进程能通讯
- 命名管道:任何进程间都能通讯,但速度慢
- 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
- 信号量:不能传递复杂消息,只能用来同步
- 共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全
- 线程间通信方式
- 锁机制:互斥锁、条件变量、读写锁
- 信号量机制:无名线程信号量和命名线程信号量
22. 线程池
由于线程的创建和销毁需要消耗资源,当线程频繁创建和销毁时或者线程创建和销毁的时间大于线程执行的时间,那么单纯的采用线程创建/销毁策略明显不合适,所以有了线程池的概念,线程池中存放一定数量创建好的线程,需要用的时候拿出来用,用完之后再放到池子里。
-
线程池的好处就是
- 降低了频繁创建销毁线程的开销;
- 提高了响应速度;
- 提高了线程的可管理性;
-
线程池组成部分
- 线程管理池:用于创建和管理线程池,有创建、销毁、添加新任务
- 工作线程:线程池中的线程,其在没有任务的时候处于等待状态,可以循环的执行任务
- 任务队列:用于存放没有处理的任务,提供一种缓存机制
- 任务接口:每个任务必须实现接口,以供工作线程调度任务的执行,规定了任务的入口及执行结束的收尾工作和任务的执行状态等
-
线程池工作流程
- 判断顺序:核心线程池、队列、线程池
23. 实践中优化MySQL
- SQL语句及索引的优化;
- 数据表结构的优化;
- 系统配置的优化;
- 硬件的优化。
上述四条从效果上来说,第一条影响最大,后面越来越小。
24. 设计高并发的系统
- 数据库的优化,包括合理的事务隔离级别、SQL语句优化、索引优化
- 使用缓存,尽量减少数据库IO操作;
- 分布式数据库、分布式缓存;
- 服务器的负载均衡。
25. 小端和大端机器
- 小端/大端的区别是指低位数据存储在内存低位还是内存高位。
- 小端机器:数据低位存储在内存低位,数据高位则存储在内存高位;
- 大端机器:数据低位存储在内存高位,数据高位存储在内存低位。
- 目前绝大多数都是小端机器,符合人们的逻辑思维的数据存储方式。
26. strlen和sizeof区别
- strlen就算字符串长度
- sizeof会将结束符算进去
strlen("12345678");//8
sizeof("12345678");//9
char str1[] = { 'a','b', 'c', 'd', 'e', '\0', 'a', 'b' };
int s1 = strlen(str1); //5
int s1_ = sizeof(str1); //8
char str2[] = "abcde";
int s2 = strlen(str2); //5
int s2_ = sizeof(str2); //6
char *str3 = "abcde";
int s3 = strlen(str3); //5
int s3_ = sizeof(str3); //4
int str4[10] = "abc";
int s4 = strlen(str4); //3
int s4_ = sizeof(str4); //10