c++常见面试问题-继续更新

c++常见面试问题

问题 :为什么子类的初始化列表不能初始化父类的成员

# 写法一:编译不过,提示
class Operation {
public:
    Operation() {}
    virtual ~Operation(){}
    
    int m_nFirst;
    int m_nSecond;
    virtual double getResult() = 0;
};

class SubOperation : public Operation {
public:
    SubOperation(int x, int y) : m_nFirst(x), m_nSecond(y){
    }
    virtual double getResult() {
        return m_nFirst - m_nSecond;
    }
};
/*
原因:
子类:来来来,Operator,你先构造
父类:好,我的成员m_nFirst和m_nSecond还没定义,那就用初始化列表m_nFirst,m_nSecond = 0来初始化,构造完毕。
子类:轮到我构造了,我的成员里没有m_nFirst,m_nSecond,我也用的是初始化列表(int m_nFirst = x, int m_nSecond=y),所以我也来int m_nFirst=x,int m_nSecond=y,好像不对,我的成员里没有m_nFirst,m_nSecond啊?(可是如果父类里继承的话,为什么主人要用初始化列表int m_nFirst = x, int m_nSecond=y再来定义一次呢?)
*/
# 写法二: 可以编译通过
class Operation {
public:
    Operation() {}
    virtual ~Operation(){}
    
    int m_nFirst;
    int m_nSecond;
    virtual double getResult() = 0;
};

class SubOperation : public Operation {
public:
    SubOperation(int x, int y) {
        m_nFirst = x;
        m_nSecond = y;
    }
    virtual double getResult() {
        return m_nFirst - m_nSecond;
    }
};

子类:来来来,父类,你先构造
父类:好,我的成员m_nFirst 和m_nSecond还没定义,那就用初始化列表int m_nFirst = x, int m_nSecond=y来初始化,构造完毕。
子类:轮到我构造了,我用的是赋值初始化m_nFirst = x, m_nSecond=y,当然也要先看下有没有m_nFirst, m_nSecond,正好有(从父类继承的),执行m_nFirst = x, m_nSecond=y,初始化好了。

问题: new和malloc的区别

new从自由存储区上分配内存,malloc从堆上分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配。那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。
new、delete 返回的是某种数据类型指针;malloc、free 返回的是 void 指针。
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算;使用malloc则需要显式地指出所需内存的尺寸。
new 可以调用对象的构造函数,对应的 delete 调用相应的析构函数;malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数。在new一个对象的时候,首先会调用malloc为对象分配内存空间,然后调用对象的构造函数。delete会调用对象的析构函数,然后调用free回收内存。
new、delete 是操作符,可以重载;malloc、free 是函数,可以重写(覆盖)。

问题:内联函数

内联函数和普通函数的区别在于编译阶段编译器需要知道内联函数的内部具体实现

问题:c和c++编译器编译后的函数符号不一样,所以c++可以重载

问题:对于封装、继承、多态的理解

封装就是将程序模块化,对象化,把具体事物的特性属性和通过这些属性来实现一些动作的具体方法放在一个类中。对象是封装的最基本单位。

继承是子类自动共享父类数据和方法的机制。父类的相关属性,可以被子类重复使用,而对于子类中需要用到的新的属性和方法子类可以自己扩展。

多态包含了重载和重写。说白了就是使用一个接口来实现不同的功能,C++中多态实现是基于虚函数表实现的,每个具备多态性对象的内部都会有一个隐藏的虚函数表,虚函数表里面的函数指针指向具体的函数实现,可能是父类中的实现,或是子类重写了的方法。
C语言实现多态,可以使用结构体封装指针函数形式,初始化的成员的时候可以赋值不同的地址,看实际使用进行调用。
封装可以隐藏实现细节包括包含私有成员,使得代码模块增加安全指数;继承可以扩展已存在的模块,为了增加代码的复用性;多态则是为了保证类在继承和派生的时候,类的实例被正确调用,实现了接口的重用

问题:迭代器 ++it,it++的源码

1) 前置返回一个引用,后置返回一个对象
前置
不会产生临时对象,后置必须产生临时对象

问题:左值和右值

看能不能对表达式取地址,如果能,则为左值,否则为右值

问题:友元类作用

类的函数或者其他类能够访问该类的内部成员变量、函数()

问题:重载、重写和覆盖

重载:同名函数参数(包括参数类型,个数与顺序)或返回值不同,注意返回值不能作为重载的标志。
覆盖:派生类覆盖基类函数(虚函数)
重写:派生类重写基类函数,但不覆盖(基类函数是虚函数的话就变成覆盖了)

什么是虚函数

定义实例时会在构造函数中进行虚表的创建和虚表指针的初始化,每个对象调用的虚函数都是通过虚表指针来索引的

虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数**,那么虚表中的地址就会改变,指向自身的虚函数实现**。如果派生类有自己的虚函数,那么虚表中就会添加该项

问题:虚函数表是在运行/还是编译时创建的?

答:虚函数表在编译的时候就确定了,而类对象的虚函数指针vptr是在运行阶段确定的这是实现多态的关键。(虚函数表示类所共有的,有点类似于static变量,存在全局数据区/常量区)

问题:为什么构造函数不能声明为虚函数

虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的
假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

问题:虚函数不能声明为static

因为静态成员函数/变量没有this指针。

问题:指针和引用区别

  1. 指针有自己的一块空间,而引用只是一个别名;
  2. 指针可以被初始化为NULL,而引用必须被初始化;
  3. 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
  4. 指针可以有多级指针(**p),而引用只有一级;

C++中的四个智能指针: shared_ptr、unique_ptr、weak_ptr、auto_ptr

shared_ptr实现共享式拥有概念。多个智能指针指向相同对象,该对象和其相关资源会在最后一个引用被销毁时被释放。
unique_ptr实现独占式拥有概念,保证同一时间内只有一个智能指针可以指向该对象。
weak_ptr 是一种共享但不拥有对象的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段,它的构造和析构不会引起引用计数的增加或减少。weak_ptr 设计的目的是为协助 shared_ptr工作的,用来解决shared_ptr相互引用时的死锁问题。注意的是我们不能通过weak_ptr直接访问对象的方法,可以通过调用lock函数来获得shared_ptr,再通过shared_ptr去调用对象的方法。
auto_ptr采用所有权模式,C++11中已经抛弃。

select、poll和epoll的区别、原理、性能、限制

select,poll,epoll都是I/O多路复用技术的具体实现。I/O多路复用就是在单个线程中,通过记录并跟踪每个I/O流的状态,来同时管理多个I/O流,一旦某个I/O流已经就绪,就能够通知程序进行相应的读写操作,以此提高服务器的吞吐能力。这种机制的优势不是在于对单个连接能处理得更快,而是在于能处理更多的连接,也就是多路网络连接复用一个I/O线程。

  • select
    select是第一个实现I/O复用概念的函数。它用一个结构体fd_set让内核监听多个文件描述符。fd_set(文件描述符集合)本质上就是一个数组,当调用select函数后,就会去里面轮询查找看是否有描述符被置位,也就是有需要被处理的I/O事件。

select函数主要存在三个问题:
内置数组的形式使得select支持的最大文件描述符受限于FD_SIZE;
每次调用select前都要重新初始化描述符集,将fd从用户态拷贝到内核态,每次调用select后,都需要将fd从内核态拷贝到用户态;
每次调用select后都要去轮询排查所有文件描述符,这在文件描述符个数很多的时候,效率很低。

  • poll
    poll可以理解为一个加强版的select。它通过一个可变长度的数组解决了select文件描述符受限的问题。数组中元素是结构体pollfd,这个结构体保存了描述符的信息,每增加一个文件描述符就向数组中加入一个结构体。同时,结构体只需要拷贝一次到内核态,解决了select重复初始化的问题。但是,它仍然存在轮询排查效率低的问题。
  • epoll
    轮询排查所有文件描述符的效率不高,使服务器并发能力受限。因此,epoll采用只返回状态发生变化的文件描述符,便解决了轮寻的瓶颈

问题:如何定义一个只能在堆上(栈上)生成对象的类?

只能在堆上
    方法: 将析构函数设置为私有
    原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
只能在栈上
    方法:将 new 和 delete 重载为私有
    原因: 在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。

delete this

类的成员函数中可以调用delete this,但是在释放后,对象后续调用的方法不能再用到this指针;
delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,此时其中的值是不确定的;
delete的本质是为将被释放的内存调用一个或多个析构函数,如果在类的析构函数中调用delete this,会陷入无限递归,造成栈溢出。

C++计算一个类的sizeof

一个空的类sizeof返回1,因为一个空类也要实例化,所谓类的实例化就是在内存中分配一块地址;
类内的普通成员函数不参与sizeof的统计,因为sizeof是针对实例的,而普通成员函数,是针对类体的;
一个类如果含有虚函数,则这个类中有一个指向虚函数表的指针,占4个字节;
静态成员不影响类的大小,被编译器放在程序的数据段中;
普通继承的类sizeof,会得到基类的大小加上派生类自身成员的大小;
当存在虚拟继承时,派生类中会有一个指向虚基类表的指针。所以其大小应为普通继承的大小,再加上虚基类表的指针大小。

指针转换类型

派生类-->基类(static_cast)
基类-->派生类(dynamic_cast) 可能会有不存在的一些成员变量,如果没有会返回一个NULL指针
以上这两个指针转换可能会带来的使指针的变化

项目开发模式

瀑布流:严格遵循预先计划的需求分析、设计、编码、集成、测试、维护的步骤顺序进行。
迭代开发:迭代是一次完整地经过所有工作流程的过程:需求、分析设计、实施和测试工作流程。整个项目所有的阶段都可以细分为迭代。每一次的迭代都会产生一个可以发布的产品,这个产品是最终产品的一个子集。
敏捷开发:以用户的需求进化为核心,采用迭代、循序渐进的方法进行软件开发

猜你喜欢

转载自blog.csdn.net/weixin_50005386/article/details/124992842