c++面试常见问题总结

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_26105397/article/details/80585613

近来在面试的过程,发现面试官在c++方面总是喜欢问及的一些相关问题总结,当时没怎么答出来,或者是答的不怎么全面,故而查询相关资料总结下。(后面实际工作会进行实时更新信息)

<一>c++虚函数方面

    虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

那么虚函数表的指针存放在哪里呢?

上述已经描述过了,存放在具体的实例对象中,通过虚函数指针来操控虚函数表,在进行多态的时候,需要用到,根据具体的实例对象就能确定所要调用的时哪一个具体类的具体方法。都是通过虚函数表来完成相应的操作的。在虚函数表中存放的时具体实例类重写的父类的虚函数方法地址。当调用时,根据具体的实例即可访问到具体方法。综上所述虚函数可以概括为以下几点:

a>虚函数表是全局共享元素,全局就只有一个。

b>虚函数表好比是一个类似数组的东西,在类对象中存储vptr(虚函数指针),并指向虚函数表,因为其不属于方法,也不属于代码,所以不能存放在代码段。

c>虚函数表中存储的是虚函数的方法的地址,其大小在编译节点就已经确定好了,根据的继承关系,就能确定好虚函数表的大小。所以不用动态的分配内存,不在堆中。

d>由于虚函数表是全局共享的,类似于static变量一样,存储在全局代码区域。

<二>有关c++类占用内存的多少计算

 在c++中一个空类所占内存的大小为1字节,例如:

  class A{};  计算其占用内存大小:sizeof(A) = 1;为什么一个空类的大小占用1字节呢?是因为类的实例化实质上就是在内存中分配一块独一无二地址,这样保证了实例是唯一存在的。所以,给空类分配一个字节,就相当于给实例分配了一个地址。如果不隐含包含一个字节的话,就不能进行实例化。当该类作为基类被继承的时候,系统会优化该类成为0字节,这个被称为空白基类最优化过程。

以下几种类型的类占用内存字节的大小,在32位系统下:

class A{};  sizeof(A) = 1;该大小上述已经具体化介绍。

//只包含普通成员函数的类,成员函数不占用内存空间

class B{

  public:

     B(){}

     ~B(){}

};

sizeof(B) = 1;  

//包含普通成员变量的类,根据变量实际大小计算占用内存大小

 

class C{

  public:

     C(){}

     ~C(){}

 private:

   int c;

 

};

sizeof(C) = 4 ;

//包含虚函数的类,包含虚函数指针,虚函数指针变量本身占用内存大小

class D{

  public:

     D(){}

     virtual ~D(){}

 private:

   int d;

};

 

sizeof(D) =  8;

 

//包含继承关系的类

 

class E:public D{

    public:

      E(){}

      ~E(){}

    private:

      int e;

};

sizeof(E) = 8;

从以上A到E几个实例来解释该类所占用内存大小的原因。

 

A类,是一个空类,因为空类也可以进行实例化,实例化就需要系统分配一块唯一地址的内存,所有系统隐含添加一个字节大小。

 

B类,虽然包含有构造函数和析构函数,但是在类中,成员函数是不占用内存的,另外该类并无成员变量,所以占用的内存大小和仍旧是1字节。同样是需要实例化所需要的。

 

C类,包含成员变量,系统给成员变量按照实际类型来分配具体内存大小的。例如,一个int型变量,占用4字节,所有sizeof(C) = 4.

 

D类,存在虚函数的类都有一个一维的虚函数表叫虚表,虚表里存放的就是虚函数的地址了,因此,虚表是属于类的。这样的类对象的前四个字节是一个指向虚表的指针,类内部必须得保存这个虚表的起始指针。在32位的系统分配给虚表指针的大小为4个字节,得到类D的大小为4,所以在算上成员变量d所占用的字节数,sizeof(D) = 8;

 

E类,继承了D类,E类和D类共享一个虚函数指针,故而算上E类自身的一个成员变量e,sizeof(E) = 8;

 

综上所述:

 

 空类:占用一个字节大小,因为每一个类都需要实例化时分配一块独一无二的内存空间。

 类内部:普通成员变量根据各自类型占用相应的内存大小,但是static变量不占用累内存大小,其存放在全局区域。子类继承父类,则将父类的成员变量计算进入子类占用的大小。非虚的成员函数不占用内存大小,但是虚函数,需要维护一张虚函数表存放相应的虚函数地址,所以虚函数指针在类内部,指针占用相应的内存大小,并且继承之后,父子类共享此虚指针。

 

<三>c++中的虚继承的作用?

 在c++中需要一个继承是一个特性,通常使用的都是一些继承虚函数,为了实现多态的过程。但是往往存在一种情况,为了提高代码的服用性,有一个基类,其自身有很多方法,是很多子类都能使用,所以往往就让子类直接将该类继承过来使用,避免了子类自己在实现一边,这样会造成大量的代码冗余,维护也不方便。但是,如果大量的子类都继承同一个基类,同样也是有问题的,比如从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,占用内存大小。所以,使用虚继承实现将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。

虚继承的原理?

虚继承的原理过程是通过虚基类指针和虚基类表来实现,一个虚基类指针占用四个字节的大小,虚基类表不占用类存储空间大小,在虚基类表中存储的时虚基类相对于派生类的偏移量,这样就根据偏移量找到虚基类成员。如果虚继承的类被继承,该派生类同样有一份虚积类指针的拷贝。这样就能保证虚基类中在子类中存在一份拷贝。避免有多分拷贝造成二义性。

语法:

 

class 派生类: virtual 基类1,virtual 基类2,...,virtual 基类n

{

...//派生类成员声明

 

};

如图所示:

 

构造函数的和析构函数的执行顺序

首先执行虚基类的构造函数,多个虚基类的构造函数按照被继承的顺序构造;

执行基类的构造函数,多个基类的构造函数按照被继承的顺序构造;

执行成员对象的构造函数,多个成员对象的构造函数按照申明的顺序构造;

执行派生类自己的构造函数;

析构以与构造相反的顺序执行;

注:

从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。

 

在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。

例如:如下代码所示:

一个派生类继承多个基类,多个基类中包含重复名称的方法。(普通继承)

class A{

    public:

        A(){cout<<"A is been called"<<endl;}

        void fun(){cout<<"A fun been called"<<endl;}

};

class B{

    public:

        B(){cout<<"B is been called"<<endl;}

        void fun(){cout<<"B fun been called"<<endl;}

};

 

class C:public  A,public B{

    public:

        C(){cout<<"C is been called"<<endl;}

};

此种方法会导致同名方法存在多分拷贝,导致调用时会产生二义性,只能使用该种方法调用,如下:

int main(void){

    C c;

  //c.fun()错误,会产生二义性

    c.A::fun();

    c.B::fun();

    return 0;

}

2,继承一个多层的类

class F{

    public:

        F(){cout<<"F been called"<<end;}

        void gun(){cout<<"F gun been called"<<endl;}

};

class S1:public F{

    public:

        S1(){cout<<"S1 been called"<<endl;}

};

class S2:public F{

    public:

           S2(){cout<<"S2 been called"<<endl;}

};

class Son:public S1,public S2{

    public:

        Son(){cout<<"Son been called"<<endl;}

};

int main(){

    Son son;

    //son.gun();有二义性,因为该方法在S1和S2中各有一份备份

    son.S1::gun();

    son.S2::gun();

    return 0;

}

虚继承就避免了这样的二义性,也节省了空间

(1)

 

 

 

class F{

    public:

        F(){cout<<"F been called"<<end;}

        void gun(){cout<<"F gun been called"<<endl;}

};

class S1:public virtual F{

    public:

        S1(){cout<<"S1 been called"<<endl;}

};

class S2:public virtual F{

    public:

           S2(){cout<<"S2 been called"<<endl;}

};

class Son:public S1,public S2{

    public:

        Son(){cout<<"Son been called"<<endl;}

};

 

int main(){

    Son son;

    son.gun();//也可使用先前的方法调用

    return 0;

}

<四>为什么构造函数不能写成虚函数,析构函数需要写成虚函数?以及什么情况下,子类的析构不会被调用?

   因为虚函数的是通过虚函数指针操控虚函数表来实现的,且虚函数指针存放在实例对象的头部位置,在创建一个实例对象时,需要调用对应的构造函数,此刻对象还未生成,是不能调用虚函数的,故而构造函数不能为虚函数。

   对于析构函数,是实例对象将要释放资源,需要调用调用析构函数,在继承关系中,为了防止带释放对象的时候调用析构函数,只调用了基类的析构而没有对派生类的构造进行调动,所以,将基类的析构函数记性虚化,从而保证基类和派生类的析构函数都被调用,确保释放其所占用的资源。

当基类指针值想子类对象,但是子类析构函数不是虚函数,当调用析构函数的时候,子类的析构函数是不会被调用的。

<五>volatile是作用

访问寄存器要比访问内存要块,因此CPU会优先访问该数据在寄存器中的存储结果,但是内存中的数据可能已经发生了改变,而寄存器中还保留着原来的结果。为了避免这种情况的发生将该变量声明为volatile,告诉CPU每次都从内存去读取数据。

注:一个参数可以即是const又是volatile的吗?可以,一个例子是只读状态寄存器,是volatile是因为它可能被意想不到的被改变,是const告诉程序不应该试图去修改他。

<六>析构函数能抛出异常吗
答案肯定是不能。
C++标准指明析构函数不能、也不应该抛出异常。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分

<七>避免在基类的构造函数和析构函数中调用虚函数?

  例如:

class A{

    public:
        A(){
            fun();
        }
        virtual void fun(){
            cout<<"Afun"<<endl;
        }
};

class B:public A{

    public:
        virtual void fun(){
            cout<<"Bfun"<<endl;
        }
};

int main(){

    B b;
    
    return 0;
}


如上所示,的调用结果是输出Afun,这是因为在创建对象b的时候,因为B继承A,在进行构造函数的调用的时候,优先调用基类的构造函数,此时的派生类对象尚未完成初始化,此刻虚函数指针还未完成初始化,不能够去检索对应的虚函数表,所以此时进行构造调用的时候为基类的方法。

构造函数的调用顺序是从基类到派生类,逐层构造。在构造的过程中,vptr被指向本层的vtable。而虚函数的行为依赖于vptr。因此,在本层构造函数中,编译器无法获知派生类的任何信息,因此无法形成正确的vtable访问。

<.八>c++中const的用法介绍

        在c++模型中,const关键字通常使用的放变量对象意外的改变的功能,在c++中const可以用来修饰的对象有变量,函数返回值,函数参数,以及函数本身,下面介逐一介绍其方式;

const修饰变量:通常是用来定义个常量的属性,表示在白变量在使用范围内是一个常量,不能进行修改的,如果强制修改的话,会出现错误。例如:const int a = 10;

const修饰函数参数:在函数的参数传递中有三种类型的传递方式,值传递,指针传递,引用传递,只有值传递的时候会出现临时变量的产生,其余两种的传递都是相当于调用所传的对象的本身。在这三种传递方式中,需要注意的是,在和const结合使用的时候的一些注意事项:

        函数参数为传入参数,不管是指针传递和引用传递,为了防止意外更改该参数,加上const修饰,可以起到保护作用。例如:void fun(const int *p) or  void fun(const int & x).

        函数参数为传出参数,此时的参数为输出参数,不能使用const进行修饰,否则,此刻该参数将失去输出参数的功能。例如:void fun( char* in,char * out ) or  void fun(char* in,char* & out).

 

 const修饰函数的返回值:

           当函数的返回值为指针,使用const修饰,表的是函数的返回的指针指向的内容是不可变,但是指针本身的指向是可以改变,并且,返回的指针必须使用一个对应的const修饰的指针进行接收,

           例如:const char* GetString(), const char* ptr = GetString()

           当函数的返回值为值传递的方式,使用const没有价值的,因为返回的是一个临时对象。

           如果函数返回值是引用,需要格外注意的是,根据实际情况来区分是要获取该对象的一份拷贝,还是该对象的一个别名使用。要根据实际情况做判断。通常参数返回引用使用使用在类赋值函数中使用,用于链式表达式的调用,a=b=c;具体可参考string类的赋值函数。

 

const修饰函数:

      再类中,任何不会修改成员的函数都应该白定义成const成员函数,如果在const成员函数中修改了成员,会出现错误。通过此种类型提高程序的健壮性。有关const成员函数的几个特性:

      const对象只能访问const成员函数,非const对象两者都可访问;

      const对象的成员是不可修改,但是通过指针维护的对象是可以修改的

      const成员函数不可以修改对象成员,不管对象是否具有const属性,在编译是以是否修改成员为依据,进行检查

      如果一个成员被mutable修饰,那个任何方式都可以修改该成员,即便是const成员函数,也可以修改他

例如:

#include <iostream>
#include <string.h>
using namespace std;
//const修饰函数的返回值
const char* getString(char* str){

    char *p = str;
    return p;

}

void GetInfo(const char* src, char* dst){

  memcpy(dst,src,strlen(src));

}

class A{

public:
  A(int a,int b,int c):a(a),b(b),c(c){
  }
  ~A(){}


  //非const方法,都可以修改
  void fun(int a,int b,int c){

      cout<<"修改之前"<<endl;
      cout<<"this->a "<<this->a<<endl;
      cout<<"this->b "<<this->b<<endl;
      cout<<"this->c "<<this->c<<endl;
      //this->a = a;   //const成员不可更改
      this->b = b;
      this->c = c;
      cout<<"修改之后"<<endl;
      cout<<"this->a "<<this->a<<endl;
      cout<<"this->b "<<this->b<<endl;
      cout<<"this->c "<<this->c<<endl;
  }

  //const成员
  void gun(int a,int b,int c) const{
    cout<<"修改之前"<<endl;
    cout<<"this->a "<<this->a<<endl;
    cout<<"this->b "<<this->b<<endl;
    cout<<"this->c "<<this->c<<endl;
    //cosnt成员不可以修改成员
    //this->a = a;  //本身是cosnt成员,不能修改
    //this->b = b; //普通成员,但是在const函数中不可修改
    this->c = c;   //使用了mutable修饰,任何地方丢可修改
    cout<<"修改之后"<<endl;
    cout<<"this->a "<<this->a<<endl;
    cout<<"this->b "<<this->b<<endl;
    cout<<"this->c "<<this->c<<endl;

  }
private:
  const int a;
  int b;
  mutable int c;

};


 int main(int argc, char const *argv[]) {

   //此刻返回的指针的内容是不可更改的
  const char* p = getString("liux");
  cout<<"p = "<<p<<endl;
  char c[12] = {"0"};
  char *dst = c;
  GetInfo(p,dst);
  cout<<"dst = "<<dst<<endl;

  A* a = new A(1,2,3);
  a->fun(7,8,9);
  a->gun(10,11,12);

  const A *a1= new A(11,22,33);
  //a1->fun(0,0,0);//const对象只能访问const成员
  a1->gun(21,31,41);

  delete a;

  return 0;
}

<九>static关键字修饰总结

   static关键字修饰变量,表示该变量为一个静态变量,并且全局共享(在全局作用于中定义,并且变量作用范围仅限于定义改变量的文件中)

    static 关键字在函数体中修饰变量,表示该变量只进行一次初始化,之后每次调用进来该函数,该变量的值仍旧是上一次调用时的值,如果更能该了个变量的值,就保持了该值,拥有一种持久化保存的性质。而函数体中普通的变量生命周期在函数返回时会被销毁,之后从新调用的时候才会重新定义。

   static在类中修饰数据成员,表示该变量不属类中的某个实例,所有的实例共享此变量,改变量需要在类外进行初始化,实际使用中,尽量避免在.h文件初始化该变量,容易造成重复定义。除此之外,类的静态变量可以被类的const成员方法合法更改,见如下代码演示:

   static 修饰的类成员方法的地址,可以直接使用普通的函数指针来存储,而普通的成员方法的地址必须使用类成员函数指针来存储,详情将如下代码所示:

注:static修饰的函数在内存中只有一份,而普通函数在每个调用者中都维持一份拷贝。

以上几种总结方式示例代码如下:

#include <iostream>
using namespace std;


class A{

public:
  A(){}
  ~A(){}

  void gun(int m){
    cout<<"gun修改前: "<<val<<endl;
    val = m;
    cout<<"gun修改前: "<<val<<endl;
  }
  //const成员函数可以更改静态成员变量的值
  void fun(int m) const{
    cout<<"fun修改前: "<<val<<endl;
    val = m;
    cout<<"fun修改前: "<<val<<endl;
  }

  //静态成员可以作为静态成员方法的默认参数,普通方法不允许
  static void hun(int m = val){
      cout<<"hun() = "<<m<<endl;
      /*
      静态方法只能访问静态成员,不能访问非静态成员,否则会报错,如下
      cout<<" hun() nal =  "<<nal<<endl;
      */
  }

  //静态方法和普通方法的函数指针
 static void s_fun(){ cout<<"++++++++++++++"<<endl;}
 void f_fun(){cout<<"******************"<<endl;}

 //函数内使用static修饰变量让其持久化
 void last(){
    static int lt = 888;
    cout<<"lt value is "<<lt--<<endl;

 }

private:
  static int val;
  int nal;

};

//在类外初始化静态成员变量,注尽量在.h文件中初始化静态成员变量,不然很容易引起冲定义的错误
//该出是为了实现方便,都在.cpp文件中
int A::val = 100;

int main(){
  A* a = new A();
  //通过const成员变量修改静态成员变量
  //a->fun(222);

  //使用静态成员变量作为静态方法的参数
  a->hun();

  /**
  静态成员函数的地址可以使用普通的函数指针来存储,而普通成员函数的地址
  必须使用成员函数指针来存储,如下所示:
  **/
  void (*s_ptr)() = &A::s_fun;
  void (A::*f_ptr)() = &A::f_fun;
  f_ptr = &A::f_fun;
  s_ptr();    //通过普通的函数指针可以直接调用类中的静态方法
  (a->*f_ptr)(); //通过类成员函数指针调用类中的成员方法
  //============================================
  /*
  以下写法错误,涉及到运算符的优先级,如下几种写法,因括号优先级最高,编译是报错,正确写法附上所示
  a->*f_ptr(); or (a->*f_ptr()());报错:must use ‘.*’ or ‘->*’ to call pointer-to-member function in ‘f_ptr (...)’, e.g. ‘(... ->* f_ptr) (...)’
  a->(*f_ptr)();报错:invalid use of unary ‘*’ on pointer to member
  */
  //============================================


  //函数内使用static修饰变量持久化
  int i;
  for(i = 0;i < 5;++i){
      a->last();
  }
  return 0;
}

 

<十>explcit关键字

  该关键字用来修饰单参或者除了第一个参数其余都是取胜参数的构造函数,避免构造函数进行隐式转化。

例如:

class A{

    public:

          explcit A(int i){n = i;}

         void print(){cout<<"n = "<<n<<endl;}

    private:

        int n;

};

int main(){

    A a(1);

    a.print();

  //此时因为构造函数被explcit修饰,不能隐士转化,如下不能使用此方法调用,否则会报错。

   //A b = 2;

    // b.print();

    return 0;

}

 

 

 

     

 

    

   

 

       

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

展开阅读全文

没有更多推荐了,返回首页