C++面试问题收集
牛客网参考:【C++工程师面试宝典】学习说明_互联网校招面试真题面经汇总_牛客网 (nowcoder.com)
C++
Q:C++面向对象思想
概述:面向对象技术中的对象就是现实世界中,某个具体的物理实体在计算机中的映射和体现,是模拟现实世界中的实体。我们可以通过设计类,然后再实例化产生一个对象。
基本特征:抽象,封装,继承,多态
Q:多态以及实现
就是向不同的对象发送同⼀个消息,不同对象在接收时会产⽣不同的⾏为(即⽅法)。即⼀个接⼝,可以实现多种⽅法。 实现:多态其实⼀般就是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器为函数⽣成符号表时的不同规则,重载只是⼀种语⾔特性,与多态⽆关,与⾯向对象也⽆关,但这⼜是 C++中增加的新规则,所以也算属于 C++,所以如果⾮要说重载算是多态的⼀种,那就可以说: 多态可以分为静态多态和动态多态。 静态多态其实就是重载,因为静态多态是指在编译时期就决定了调⽤哪个函数,根据参数列表来决定; 动态多态是指通过⼦类重写⽗类的虚函数来实现的,因为是在运⾏期间决定调⽤的函数,所以称为动态多态,⼀般情况下我们不区分这两个时所说的多态就是指动态多态。动态多态的实现与虚函数表,虚函数指针相关。
Q:虚函数以及虚函数的实现原理
C++中多态的表象,在基类的函数前加上 virtual 关键字,在派⽣类中重写该函数,运⾏时将会根据对象的实际类型来调⽤相应的函数。如果对象类型是派⽣类,就调⽤派⽣类的函数,如果是基类,就调⽤基类的函数。 实际上,当⼀个类中包含虚函数时,编译器会为该类⽣成⼀个虚函数表,保存该类中虚函数的地址,同样,派⽣类继承基类,派⽣类中⾃然⼀定有虚函数,所以编译器也会为派⽣类⽣成⾃⼰的虚函数表。当我们定义⼀个派⽣类对象时,编译器检测该类型有虚函数,所以为这个派⽣类对象⽣成⼀个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。 后续如果有⼀个基类类型的指针,指向派⽣类,那么当调⽤虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调⽤派⽣类的虚函数表中的虚函数以此实现多态。
Q:new/delete 和 malloc/free
使用new操作符来分配对象内存时会经历三个步骤: 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。 第三部:对象构造完成后,返回一个指向该对象的指针。 使用delete操作符来释放对象内存时会经历两个步骤: 第一步:调用对象的析构函数。 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。 细节 自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。 C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。 那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。 new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。 在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler。new_handler是一个指针类型,为了指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数,set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。 区别 1、new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配; 2、new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。 3、new不仅分配一段内存,而且会调用构造函数,malloc不会。 4、new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。 5、new是一个操作符可以重载,malloc是一个库函数。 6、malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。 malloc、calloc函数的实质体现在将一块可用的内存连接为一个链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块,然后将该内存块一分为二(一块与用户申请的大小一样,另一块就是剩下的字节)。接下来,将分配给用户的那块内存地址传给用户,调用free函数时,它将用户释放的内存块连接到空链上,最后空闲链表会被切成很多的小内存片段。 realloc是从堆空间上分配内存,当扩大一块内存空间时,realloc试图直接从现存的数据后面的哪些字节中获得附加的字节,如果能够满足需求,自然天下太平,如果后面的字节不够,那么就使用堆上第一个足够满足要求的自由空间块,现存的数据然后就被拷贝到新的位置上,而老块则放回堆空间,这句话传递的一个很重要的信息就是数据可能被移动。 clear allocation memory allocation real allocation 7、new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。 8、申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
Q:C++的继承方式
protected 成员和 private 成员类似,也不能通过对象访问。但是当存在继承关系时,protected 和 private 就不一样了:基类中的 protected 成员可以在派生类中使用,而基类中的 private 成员不能在派生类中使用。 不同的继承方式会影响基类成员在派生类中的访问权限。 1) public继承方式 基类中所有 public 成员在派生类中为 public 属性; 基类中所有 protected 成员在派生类中为 protected 属性; 基类中所有 private 成员在派生类中不能使用。 2) protected继承方式 基类中的所有 public 成员在派生类中为 protected 属性; 基类中的所有 protected 成员在派生类中为 protected 属性; 基类中的所有 private 成员在派生类中不能使用。 3) private继承方式 基类中的所有 public 成员在派生类中均为 private 属性; 基类中的所有 protected 成员在派生类中均为 private 属性; 基类中的所有 private 成员在派生类中不能使用。
Q:析构函数是否定义为虚函数的区别
(1)析构函数定义为虚函数时:基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。 (2)析构函数不定义为虚函数时:编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。
Q:智能指针shared_ptr的实现
(构造函数)构造shared_ptr(公共成员函数) (析构函数)销毁shared_ptr(公共成员函数) operator= shared_ptr赋值(公共成员函数) swap 交换内容(公共成员函数) reset 重置指针(公共成员函数) get 获取指针(公共成员函数) operator* 取消引用对象(公共成员函数) operator-> 取消引用对象成员(公共成员函数) operator[] (C++17)提供到被存储数组的带下标访问(公开成员函数) use_count 返回计数(公共成员函数) unique(C++20前) 检查是否唯一(公共成员函数) operator bool 检查是否不为空(公共成员函数) owner_before 基于所有者的共享指针排序(公共成员函数模板) 非成员函数 swap 交换shared_ptr对象的内容(函数模板) relational operators 关系运算符 ==, !=, <, <=, >, >= (函数模板 ) ostream operator<< 将存储的指针的值输出到输出流(函数模板) 具体功能: make_shared,make_shared_for_overwrite(C++20) 创建管理一个新对象的共享指针(函数模板) allocate_shared,allocate_shared_for_overwrite(C++20) 创建管理一个用分配器分配的新对象的共享指针(函数模板) static_pointer_cast,dynamic_pointer_cast,const_pointer_cast,reinterpret_pointer_cast (C++17)应用 static_cast、dynamic_cast、const_cast 或 reinterpret_cast 到被存储指针(函数模板) get_deleter 返回指定类型中的删除器,若其拥有(函数模板)
1 #include <utility> 2 #include <cstddef> 3 4 class ref_count 5 { 6 public: 7 int use_count() const noexcept { return count_; } 8 void inc_ref() noexcept { ++count_; } 9 int dec_ref() noexcept { return --count_; } 10 11 private: 12 int count_{1}; 13 }; 14 15 template <typename T> 16 class Shared_ptr 17 { 18 public: 19 constexpr Shared_ptr() noexcept = default; 20 constexpr Shared_ptr(nullptr_t) noexcept : Shared_ptr() {} 21 explicit Shared_ptr(T *ptr) : ptr_{ptr} 22 { 23 if (ptr_ != nullptr) 24 { 25 rep_ = new ref_count{}; 26 } 27 } 28 Shared_ptr(const Shared_ptr &rhs) noexcept : ptr_{rhs.ptr_}, rep_{rhs.rep_} 29 { 30 if (ptr_ != nullptr) 31 { 32 rep_->inc_ref(); 33 } 34 } 35 Shared_ptr(Shared_ptr &&rhs) noexcept : ptr_{rhs.ptr_}, rep_{rhs.rep_} 36 { 37 rhs.ptr_ = nullptr; 38 rhs.rep_ = nullptr; 39 } 40 ~Shared_ptr() noexcept 41 { 42 if (rep_ != nullptr && rep_->dec_ref() == 0) 43 { 44 delete ptr_; 45 delete rep_; 46 } 47 } 48 49 Shared_ptr &operator=(const Shared_ptr &rhs) 50 { 51 Shared_ptr{rhs}.swap(*this); 52 return *this; 53 } 54 Shared_ptr &operator=(Shared_ptr &&rhs) 55 { 56 Shared_ptr{std::move(rhs)}.swap(*this); 57 return *this; 58 } 59 void reset() noexcept 60 { 61 Shared_ptr{}.swap(*this); 62 } 63 void reset(nullptr_t) noexcept 64 { 65 reset(); 66 } 67 void reset(T *ptr) 68 { 69 Shared_ptr{ptr}.swap(*this); 70 } 71 72 void swap(Shared_ptr &rhs) noexcept 73 { 74 std::swap(ptr_, rhs.ptr_); 75 std::swap(rep_, rhs.rep_); 76 } 77 T *get() const noexcept 78 { 79 return ptr_; 80 } 81 82 long use_count() const noexcept 83 { 84 return rep_ == nullptr ? 0 : rep_->use_count(); 85 } 86 bool unique() const noexcept 87 { 88 return rep_->use_count() == 1; 89 } 90 91 T &operator*() const noexcept 92 { 93 return *ptr_; 94 } 95 T &operator->() const noexcept 96 { 97 return ptr_; 98 } 99 100 explicit operator bool() const noexcept 101 { 102 return static_cast<bool>(ptr_); 103 } 104 105 private: 106 T *ptr_{nullptr}; 107 ref_count *rep_{nullptr}; 108 }; 109 110 template <typename T, typename... Args> 111 auto make_Shared(Args &&...args) 112 { 113 return Shared_ptr<T>{new T(std::forward(args)...)}; 114 }
Q:智能指针shared_ptr的计数原理
指向相同资源的所有 shared_ptr 共享“引用计数管理区域”,并采用原子操作保证该区域中的引用计数值被互斥地访问。 “引用计数管理区域”是指通过 new 创建的 sp_counted_impl_p<D> 或 sp_counted_impl_pd<D> 对象,在创建成功后立即由其基类指针指向它,而该基类指针被 shared_ptr 间接持有。
Q:简述一下 C++11 中四种类型转换
C++中四种类型转换分别为const_cast、static_cast、dynamic_cast、reinterpret_cast,四种转换功能分别如下: 1. const_cast 将const变量转为非const 2. static_cast 最常用,可以用于各种隐式转换,比如非const转const,static_cast可以用于类向上转换,但向下转换能成功但是不安全。 3. dynamic_cast 只能用于含有虚函数的类转换,用于类向上和向下转换 向上转换:指子类向基类转换。 向下转换:指基类向子类转换。 这两种转换,子类包含父类,当父类转换成子类时可能出现非法内存访问的问题。 dynamic_cast通过判断变量运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。dynamic_cast可以做类之间上下转换,转换的时候会进行类型检查,类型相等成功转换,类型不等转换失败。 4. reinterpret_cast reinterpret_cast可以做任何类型的转换,不过不对转换结果保证,容易出问题。
Q:static 修饰全局变量和局部变量的区别
静态局部变量使用static修饰符定义,即使在声明时未赋初值,编译器也会把它初始化为0。 且静态局部变量存储于进程的全局数据区,即使函数返回,它的值也会保持不变。 静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。 函数的使用方式与全局变量类似,在函数的返回类型前加上static,就是静态函数。 其特性如下: 静态函数只能在声明它的文件中可见,其他文件不能引用该函数 不同的文件可以使用相同名字的静态函数,互不影响 详细: https://blog.csdn.net/guotianqing/article/details/79828100
Q:register关键字作用
请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。
Q:volatile关键字的作用
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。 而且读取的数据立刻被保存。
操作系统
Q:软链接和硬链接的区别
1. 定义不同 软链接又叫符号链接,这个文件包含了另一个文件的路径名。 可以是任意文件或目录,可以链接不同文件系统的文件。 硬链接就是一个文件的一个或多个文件名。 把文件名和计算机文件系统使用的节点号链接起来。 因此我们可以用多个文件名与同一个文件进行链接,这些文件名可以在同一目录或不同目录。 2. 限制不同 硬链接只能对已存在的文件进行创建,不能交叉文件系统进行硬链接的创建。 软链接可对不存在的文件或目录创建软链接;可交叉文件系统。 3. 创建方式不同 硬链接不能对目录进行创建,只可对文件创建。 软链接可对文件或目录创建。 4. 影响不同 删除一个硬链接文件并不影响其他有相同 inode 号的文件。 删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。
Q:什么是大端小端,如何判断大端小端?
小端模式:低的有效字节存储在低的存储器地址。
小端一般为主机字节序;常用的X86结构是小端模式。
很多的ARM,DSP都为小端模式。
大端模式:高的有效字节存储在低的存储器地址。
大端为网络字节序;KEIL C51则为大端模式。
有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
//我们可以根据联合体来判断系统是大端还是小端。 //因为联合体变量总是从低地址存储。 int fun1(){ union test{ char c; int i; }; test t; t.i = 1; //如果是大端,则t.c为0x00,则t.c != 1,反之是小端 return (t.c == 1); }
Q:进程调度算法有哪些
先来先服务调度算法:
每次调度都是从后备作业(进程)队列中选择一个或多个最先进入该队列的作业(进程),将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。
短作业(进程)优先调度算法:
短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业(进程),将它们调入内存运行。
高优先级优先调度算法:
当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。
当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程。
时间片轮转法:
每次调度时,把CPU 分配给队首进程,并令其执行一个时间片。
时间片的大小从几ms 到几百ms。
当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾。
然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。
多级反馈队列调度算法:
综合前面多种调度算法。
Q:简述Linux系统态与用户态,什么时候会进入系统态
内核态与用户态:
内核态(系统态)与用户态是操作系统的两种运行级别。
内核态拥有最高权限,可以访问所有系统指令。
用户态则只能访问一部分指令。
什么时候进入内核态:
共有三种方式:a、系统调用。b、异常。c、设备中断。其中,系统调用是主动的,另外两种是被动的。
为什么区分内核态与用户态:
在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。
比如:清内存、设置时钟等。所以区分内核态与用户态主要是出于安全的考虑。
Q:有哪些页面置换算法
最佳置换算法(OPT)
选择那些以后永不使用的,或在最长(未来)时间内不再被访问的页面作为淘汰的页面。
先进先出置换算法(FIFO)
总是淘汰最先进入内存的页面,即选择在内存中驻留时间最长的页面予以淘汰。
最近最久未使用置换算法(LRU)
赋予每个页面一个访问字段,用来记录相应页面自上次被访问以来所经历的时间t。
当淘汰一个页面时,应选择所有页面中其t值最大的页面,即内存中最近一段时间内最长时间未被使用的页面予以淘汰。
最少使用置换算法(LFU)
为每个页面配置一个计数器,一旦某页被访问,则将其计数器的值加1。
在需要选择一页置换时,则将选择其计数器值最小的页面,即内存中访问次数最少的页面进行淘汰。
Q:Linux中进程和线程的区别,以及协程
进程是系统进行资源分配和调度的一个独立单位。
每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。
由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。
协程拥有自己的寄存器上下文和栈。
协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
Q:用户态地址空间和内核态地址空间区别
https://zhuanlan.zhihu.com/p/343597285 用户态的程序不能随意操作内核地址空间,这样对操作系统具有一定的安全保护作用。
Q:copy-on-write
父子进程的内存共享的数据仅仅是fork那一时间点的数据,fork 后的数据不会有任何共享;
所谓 lazy copy,就是在需要修改的时候拷贝一个副本出来,如果没有任何改动,则不会占用额外的物理内存。