【面经总结】——C++篇

【面经总结】——C++篇

1.C++ 程序的编译过程(源码如何转化成二进制)

编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接

  • 编译预处理:处理以 # 开头的指令
  • 编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码
  • 汇编:将汇编代码 .s 翻译成机器指令 .o 文件
  • 链接:汇编程序生成的目标文件并不会立即执行,可能有源文件中的函数引用了另一个源文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些目标文件连接成一个整体,从而生成可执行的程序 .exe 文件。
    在这里插入图片描述
    链接分为两种:
  • 静态链接:代码从其所在的动态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中
  • 动态链接:代码被放到动态链接库共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间
    二者的优缺点:
  • 静态链接 浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容
  • 动态链接节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要进行链接,性能会有一定的损失。
2.指针和引用的区别(4条)
  • 是否可变------指针所指向的内存空间在程序运行过程中可以改变(指针的指向可以改变),而引用所绑定的对象一旦绑定就不能改变。(是否可变),因为引用的关系一旦建立就无法被修改,因此引用在定义时必须进行初始化

  • 是否占用内存------指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间(是否占内存)

  • 是否可以为空-------指针可以为空,但是引用必须绑定对象(是否可为空)

  • 是否可以为多级-------指针可以有多级,但是引用只能一级(是否能为多级)

  • 引用的本质及实现其实还是指针,传引用其实传入的就是变量的地址

3.堆和栈的区别(内存角度)
  • 申请方式:栈是系统自动分配,堆是程序员主动申请
  • 申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上
  • 内存空间是否连续:栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的
  • 申请效率:栈是由系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片(已申请的堆空间不再使用后未对空间进行释放会造成内存泄露)
  • 存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制
4.new与delete,malloc与free的异同(对于变量而言没有区别,对于类对象而言有区别)

首先在使用的时候 new,delete 搭配使用,malloc 和 free 搭配使用

  • 属性:malloc/free 是库函数,需要头文件的支持;new/delete 是关键字,需要编译器的支持
  • 参数:new 申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc 在申请空间时,需要确定所申请空间的大小
  • 返回值
    • new 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,符合类型安全的操作符;
    • malloc 申请空间时,返回的是 void 类型*,需要进行强制类型的转换,转换为对象类型的指针
  • 分配失败:new 分配失败时,会抛出 bad_alloc 异常,malloc 分配失败时返回空指针
  • 重载:new/delete 支持重载,malloc/free 不能进行重载
  • 自定义类型实现:new 首先调用 operator new 函数申请空间(底层通过 malloc 实现),然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete 释放空间(底层通过 free 实现)。malloc/free 无法进行自定义类型的对象的构造和析构
  • 内存区域:new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)自由存储区和堆的区别见下方
5.C 和 C++ 的区别
  • C 是面向过程的编程,特点是函数;C++ 是面向对象的编程,特点是。(特性)
  • C 主要用在嵌入式开发、驱动开发和硬件直接打交道的领域;C++ 可以用于应用层的开发、用户界面开发等和操作系统直接打交道的领域。(应用领域)
  • C++ 继承了C的底层操作特性,增加了面向对象的机制,增加了泛型编程、异常处理、运算符重载,还增加了命名空间,避免了命名冲突。(相较于 C 的升级)
6.Struct 和 class 的区别(只有默认访问权限和默认继承权限的不同)

struct 和 class 都可以自定义数据类型,也支持继承操作

  • struct 中默认的访问级别是 public,默认的继承级别也是 public
  • class 中默认的访问级别是 private,默认的继承级别也是 private
  • 当 class 继承 struct 或者 struct 继承 class 时,默认的继承级别取决于继承者class或struct本身, class(private 继承) ,struct(public 继承)
struct A{}class B : A{}; //private继承
struct C : B{}//public继承
#include<bits/stdc++.h>

using namespace std;

class A{
public:
    void printA(){
        cout << "class A" << endl;
    }
};

struct B: A{//由于 B 是 struct,A的继承级别为 public(取决于B的默认继承级别)
public:
    void printB(){
        cout << "class B" << endl;
    }
};

class C: B{//由于 C 是 class,B的继承级别为 private(取决于C的默认继承级别),所以无法访问基类B中的printB函数

};

int main(){
    A ex1;
    ex1.printA(); // class A

    B ex2;
    ex2.printA(); // class A
    ex2.printB(); // class B

    C ex3;
    ex3.printB(); // error: 'B' is not an accessible base of 'C'
    return 0;
}
7.define 和 const 的区别(编译阶段、安全性、内存占用等)
  • 编译阶段:define 是在编译预处理阶段起作用,const 是在编译阶段和程序运行阶段起作用
  • 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的只读变量是有类型的,是要进行判断的,可以避免一些低级的错误
  • 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份;const 定义的只读变量在程序运行过程中只有一份
  • 调试:define 定义的不能调试,因为在预编译阶段就已经进行替换了;const 定义的可以进行调试

const 的优点:

  • 有数据类型,在定义时可进行安全性检查
  • 可调试
  • 占用较少的空间
8.在 C++ 中 const 和 static 的用法(定义,用途)

const

  • const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率
  • const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外,函数参数也除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量
  • const 修饰函数参数,使得传递过来的函数参数的值不能改变

static 定义静态变量,静态函数

  • static 作用于局部变量改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间
  • static 作用于全局变量和函数改变了全局变量的作用域,使得全局变量只能在定义它的文件中使用,在源文件中不具有全局可见性(未加static关键字的全局变量在整个源文件中都具有可见性)
  • static 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数(普通成员函数和成员变量不能进行访问,因为普通成员函数和成员变量一般是在在类的实例化之后才有的,而静态成员函数和成员变量在类的实例化之前就已经可以访问了),不能将静态成员函数定义成虚函数
9.const 和 static 在类中使用的注意事项(定义、初始化和使用)

static 静态成员变量:

  • 静态成员变量是在类内进行声明在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static 关键字和 private/public/protected 访问规则。
  • 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。
  • 静态成员变量可以作为成员函数的参数可选参数,而普通成员变量不可以
class base{ 
public : 
    static int _staticVar; 
    int _var; 
    void foo1(int i=_staticVar);//正确,_staticVar为静态数据成员 
    void foo2(int i=_var);//错误,_var为普通数据成员 
};
class base{ 
public : 
    static base _object1;//正确,静态数据成员 
    base _object2;//错误 
    base *pObject;//正确,指针 
    base &mObject;//正确,引用 
    
};

static 静态成员函数:

  1. 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数作为类作用域的全局函数
  2. 静态成员函数不能声明成虚函数(virtual)、const、volatile

const 成员变量

  1. const 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化
  2. const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的,所以不能在类的声明中初始化 const 成员变量,因为类的对象还没有创建,编译器不知道他的值

const 成员函数

  1. 不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量
  2. 不能调用非常量成员函数,以防修改成员变量的值
    const static
    如果要想成员变量在整个类中都是恒定的常量,应该用类的枚举常量或者 static const.
    在类中进行声明,在类外进行初始化(类似于类的静态常量)

C++ 中的 const 类成员函数(用法和意义)
用法:如上
意义:防止修改成员变量

10.计算下面几个类的大小:

类本身没有大小,这里类的大小指的是:类对象所占的大小

  • 类的大小遵循结构体的对齐规则
  • 类的大小与普通成员函数静态成员(注意是静态成员,包含的可是很多的哦!)无关(包括:普通成员函数,静态成员函数,静态数据成员,静态常量数据成员),与普通数据成员有关
  • 虚函数对类的大小有影响,是因为虚函数指针的影响(虚函数指针占8个字节)
  • 虚继承对类的大小有影响,是因为虚基表指针带来的影响
  • 空类的大小是1
#include <bits/stdc++.h>
using namespace std;

class A{
    //静态数据成员,静态常量数据成员,静态成员函数均不影响类的大小
    static int a;
    const static int b;    
    static int fun1(){}

    //普通成员函数
    void fun(){}
};

//普通数据成员,int占用4个字节
class B{
    int a;    
};

//虚函数,虚表指针占用8个字节
class C{
    virtual int fun(){}
};

//结构体对齐原则,int型数据成员占用4个字节,虚表指针占用8个字节
class D{
    int a;
    virtual int fun(){}
};
int main(){
    cout << sizeof(A) << endl;//1:空类的大小为1
    cout << sizeof(B) << endl;//4:表示一个整型变量的大小
    cout << sizeof(C) << endl;//8:虚函数表的指针的大小
    cout << sizeof(D) << endl;//16:整型变量的大小+虚函数指针的大小+对齐原则(4)
    return 0;
}
11.STL源码中的hash表的实现

hashtable是采用开链法来完成的(vector+list)

  • 底层键序列(key)采用vector实现,vector的大小取的是质数,且相邻质数的大小约为2倍关系,当创建hashtable时,会自动选取一个接近所创建大小的质数作为当前hashtable的大小;
  • 对应键的值序列(value)采用单向list实现;
  • 当hashtable的键的vector的大小重新分配的时候,原键的值的list也会重新分配,因为当vector重建时相当于键的数量增加了,那么原来的值对应的键可能就不同于原来分配的键,这样就需要重新确定值的键
12.STL中unordered_map和map的区别
  • 底层实现不同
    • unordered_map底层实现是一个哈希表,元素无序
    • map底层实现是红黑树,其内部所有的元素都是有序的,因此对map的所有操作,其实都是对红黑树的操作
  • unordered_map和map的优缺点
    • unordered_map:查找效率高,但是建立哈希表比较耗费时间
    • map:内部元素有序,查找和删除操作都是log(n)的时间复杂度,但是红黑树的存储结构需要占用一定的内存空间
  • unordered_map和map的适用情况
    • 对于要求内部元素有序的可以使用map
    • 对于要求查找效率的用unordered_map
13.STL中vector的实现

vector是一个动态数组,底层实现是一段连续的线性内存空间
扩容的本质:当vector实际所占用的内存空间和容量相等时,如果再往其中添加元素需要进行扩容,其步骤如下:

  • 首先,申请一块更大的存储空间,一般是增加当前容量的50%或者100%,具体增加多少和编译器有关
  • 然后,将旧内存空间的内容,按照原来的顺序放到新的空间中
  • 最后,将旧内存空间的内容释放掉,本质上其存储空间不会释放,只是删除了里面的内容
  • 从vector扩容的原理也可以看出:vector容器释放后,与其相关的指针,引用以及迭代器也会失效
14.vector 使用的注意点及其原因,频繁对 vector 调用 push_back() 对性能的影响和原因
  • 主要是在插入元素方面:插入元素需要考虑元素的移动问题和是否需要扩容的问题
  • 频繁的调用 push_back() 也是扩容的问题对性能的影响
15.C++ 中的重载和重写(覆盖)的区别:
  • 对于类中函数的重载或者重写而言,重载发生在同一个类的内部(作用域相同),重写发生在不同的类之间,子类和父类之间(作用域不同
  • 重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;
  • 重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰(函数重写是多态的体现
  • virtual 关键字:重写的函数基类中必须有 virtual 关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有
16.C++ 内存管理(热门问题,问过)

C++ 内存分区:栈、堆、自由存储区、全局/静态存储区、常量区(一共5部分

  • :存放函数的局部变量,由编译器自动分配和释放
  • :动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收
  • 自由存储区:和堆十分相似,存放由 new 分配的内存块,由 delete 释放内存
  • 全局区/静态区:存放全局变量和静态变量
  • 常量存储区:存放的是常量,不允许修改
堆和自由存储区的区别:(new所申请的区域仍然在堆上,只是new所申请的堆上的存储区域称为自由存储区)
  • 自由存储是 C++ 中通过 new 与 delete 动态分配和释放对象的抽象概念,而堆是 C 语言和操作系统的术语,是操作系统维护的一块动态分配内存
  • new 所申请的内存区域在 C++ 中称为自由存储区。即由堆实现的自由存储,可以说 new 所申请的内存区域在堆上
  • 堆和自由存储区有区别,并非等价。使用 new 来分配内存,程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。(自由存储有区别与堆的情况,感觉比较难理解)
指针和数组的对比:
  1. 字符数组的内容可以改变,字符指针的内容不可以改变
char a[] = "hello";
a[0] = 'l';
cout << a << endl;
char *p = "world";//p指向常量字符串
p[0] = 'h'; //运行出错
  1. 字符数组之间不能赋值不能直接用比较运算符直接比较,若进行赋值调用 strcpy() 函数,若进行比较调用 strcmp() 函数
char a[] = "hello";
char b[10];
strcpy(b, a);//不能用b = a;
if(strcmp(a, b) == 0)//不能用a==b
        cout << "endl";
  1. 运算符 sizeof 可以计算出字符数组的容量,但是计算字符指针时,得到的是指针变量所占用的空间,而不是指针所指向空间的大小
char a[] = "hello";
char *p = a;
cout << sizeof(a) << endl;//6字节
cout << sizeof(p) << endl;//8字节(64位)
  1. 但是当数组作为参数传递时,数组会自动退化为指针
void fun(char a[100]){
        cout << sizeof a << endl;//结果是8(64位)
}
调用函数申请空间
  1. 指针作为函数的参数传递:
void GetMemory(char *p, int num)
{
 p = (char *)malloc(sizeof(char) * num);
}

void Test(void)
{
 char *str = NULL;
 GetMemory(str, 100); // str 仍然为 NULL
 strcpy(str, "hello"); // 运行错误
}

上述程序出错的原因在于:调用函数时,传递的是变量的值“值传递”,在 GetMemory 函数内部改变的是参数 p 的副本,并不会影响参数 p 本身,也就是说 str 并没有获得内存。这样程序最终会导致内存泄漏,调用 GetMemory 函数多少次,就会出现多少次内存泄漏,因为在函数内部只动态申请了内存,并没有用 free 释放掉动态内存空间。

如果想要通过这种方式申请空间,将 GetMemory 函数的形参改为“指向指针的指针”

void GetMemory2(char **p, int num)
{
 *p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
 char *str = NULL;
 GetMemory2(&str, 100); // 注意参数是 &str,而不是str
 strcpy(str, "hello");
 cout<< str << endl;
 free(str);//特别注意:这里是将动态申请的内存空间释放掉
}
  1. 用函数的返回值来传递动态申请的内存空间
har *GetString(void)
{
 char p[] = "hello world";//局部变量,函数调用结束后就会被释放掉
 return p; // 编译器将提出警告:将局部变量返回
}
void Test4(void)加粗样式
{
 char *str = NULL;
 str = GetString(); // str 的内容是垃圾,野指针
 cout<< str << endl;
}

GetString()函数调用结束后,其生存期已经结束,此时返回char* 指针指向的内存空间,其实是已经被编译器释放掉的内存空间,存在越界访问

char *GetString2(void)
{
 char *p = "hello world";//p实际上为const string类型的指针
 return p;//编译器给出警告:将string const转化成 char*
}
void Test5(void)
{
 char *str = NULL;
 str = GetString2();//野指针
 cout<< str << endl;
}
在一个函数中如何返回一个变量的指针?

对于普通变量而言,是存储在栈内存中,当函数调用完毕后,栈内存的空间会被释放,如果返回局部变量的指针,回到主调函数后,该指针是悬挂指针指向垃圾内存,如何来处理这种情况呢?
可以返回存储在堆内存或者全局区的变量的指针:

  • 返回静态类型对象的指针,会导致占用大量的内存空间
  • 返回存放在堆上的对象的指针,该对象所在的内存空间由用户控制什么时候释放,那如何知道什么时候释放合适呢?可以用智能指针来处理,智能指针内部有计数器,当计数器为 0 时,会自动释放该内存。
#include <iostream>
using namespace std;
 
int func1(int param)
{
 int local_param = param;
 
 return local_param;
}

//返回静态变量的地址 
int* func2(int param)
{
 static int local_param = param;
 
 return &local_param;
}
 
//返回存放在堆上变量的地址* 
int *func3(int param)
{
 int *new_param = new int(param);
 
 return new_param;
}
 
 
int main()
{
 int *pNewVar = func3(3);
 int *pVar = func2(2);
 
 cout << *pVar << endl;//2
 cout << *pNewVar << endl;//3
 cout << func1(1) << endl;//局部变量,函数调用结束就会释放
 
 return 0;
}
出现野指针的情形:
  1. 指针定义的时候未初始化
  2. 指针指向动态分配的内存空间在释放(delete 或 free)后,未置为 NULL,让人误以为是合法指针
  3. 指针操作超过了变量的作用范围。例如:在函数中将一个局部变量的地址作为函数的返回值,这里编译器会给出警告,因为离开该函数后,局部变量的空间就会释放掉,返回的地址(指针)相当于是野指针。
深拷贝和浅拷贝带来的问题:

类中默认的拷贝构造函数赋值构造函数都是浅拷贝,当类的成员变量中出现指针变量时,最好使用深拷贝,避免内存空间多次释放的问题出现。(使用一个对象给另一个对象赋值的时候属于浅拷贝,浅拷贝并没有为新创建的对象分配新的内存空间,因此新创建的对象与原先创建的对象使用同一块内存空间

浅拷贝存在的问题:
class CTest
{
        CTest()
        {
                //构造函数中有内存空间的开辟
                m_size = (char*)malloc(255);
        }
        ~CTest()
        {        
                //析构函数中有内存空间的释放
                free(m_size);
        }
}
int main()
{
        CTest t1(10,"nike");
        CTest t2 = t1;
}
  • 在上面这个例子中,调试程序机会发现,创建t1对象时调用了一次构造函数,创建t2对象时调用了默认的拷贝构造函数,在出main函数之前编译器会默认调用两次析构函数释放t1和t2的内存空间,但是由于默认拷贝构造函数是浅拷贝,即t1和t2实际是使用同一块内存空间,因此两次释放内存空间会报错
  • 进一步思考可以知道,之所以会造成内存空间重复释放,是因为上面类中在构造和析构时进行了内存的动态分配,如果我们在类的析构和构造中并未进行内存的动态分配,则不会出现重复释放的情况
  • 如何解决上面的问题呢?——自定义拷贝构造函数,在拷贝构造函数中使用深拷贝进行实现
class CTest
{
        CTest(){};
        CTest(CTest& obj)
        {
                //浅拷贝的实现方式
                this->m_size = obj.m_size;        
                //深拷贝的实现方式
                m_size = (char*)malloc(255);
                if(m_size == nullptr)
                {
                        return;
                }
                memcpy(this->m_size,obj.m_size,255);
        }
}
内存泄漏:

内存泄漏常指的是堆内存泄漏,当然还包括系统资源的泄漏

野指针:

指针指向的内存空间已经释放掉

悬挂指针:

指针指向的内存空间由于中间改变了其指向,之前的内存空间已无法释放,个人认为是和内存泄漏。
感觉二者并没有什么区别!!!!
野指针和悬挂指针都是指向垃圾内存的

17.面向对象和面向过程的区别(问过)
  • 面向过程: 以事件为中心的编程思想,编程时把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数
  • 面向对象: 世界上有很多的人和事物,每一个都可以看作一个对象,每个对象都有自己的属性和行为,对象和对象之间可以通过方法来交互,对象的抽象称为类
18.什么是面向对象,介绍面向对象的三大特性,并且举例说明每一个

面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。

  • 封装:将具体的实现过程和数据封装成一个类,只能通过接口进行访问,降低耦合性。
  • 继承:子类继承父类的特征和行为,子类有父类的非private方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被final关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
  • 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式
19.多态的实现和应用场景
  • 多态通过虚函数实现
  • 应用场景:在实际开发中,一个功能有多种实现方式,流程相同,但是具体的细节有区别,例如:支付功能,可以进行支付宝支付、微信支付等
20.C++虚函数相关(虚函数表,虚函数指针),虚函数的实现原理(热门,重要)
  • 虚函数是通过虚函数表来实现的,虚函数表里面保存了虚函数的地址这张表保存在含有虚函数的类的实例对象的内存空间中,虚函数表的指针存放在对象实例的最前面的位置.
  • 虚函数表是在编译阶段建立的,也就是说在程序的编译过程中会将虚函数的地址放在虚函数表中。(问过)
21.虚继承(和虚函数混合在一起问)

提出:
虚继承是为了解决多重继承命名冲突数据冗余问题而提出的。
例如:类D继承类B1,B2,而类B1,B2都继承自类A,因此在类D中出现两次类A中的变量和函数,那为了节省内存空间可以将B1,B2对A的继承定义为虚继承,而A就成了虚基类。
在这里插入图片描述

class A{};
class B1:public virtual A{};
class B2:public virtual A{};
class D:public B1,public B2{};

这样使用虚继承就能够确保在派生类D中只保存一份A的成员变量和成员函数。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类,这个被共享的基类就称为虚基类

虚基类的成员可见性问题:
  • 如果B1,B2中都没有定义x,那么x将被解析为A的成员,不存在二义性;
  • 如果B1或B2其中的一个类定义了x,那么也不存在二义性的问题,派生类的x比虚基类的x的优先级更高;
  • 如果B1和B2中都定义了x,那么直接访问x会产生二义性问题
22.实现编译器处理虚函数表应该如何处理
  • 编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。
  • 如果派生类没有重新定义基类的虚函数A,则派生类的虚函数表中保存的是基类的虚函数A的地址,也就是说基类和派生类的虚函数A的地址是一样的
  • 如果派生类重写了基类的某个虚函数B,则派生的虚函数表中保存的是重写后的虚函数B的地址,也就是说虚函数B有两个版本,分别存放在基类和派生类的虚函数表中
  • 如果派生类重新定义了新的虚函数C,派生类的虚函数表保存新的虚函数C的地址
23.析构函数一般写成虚函数的原因
  • 析构函数定义成虚函数是为了防止内存泄漏,因为当父类的指针或者引用指向或绑定到子类的对象时,如果未将基类的析构函数定义成虚函数,会调用基类的析构函数,那么只能将基类的成员所占的空间释放掉,派生类中特有的就会无法释放内存空间导致内存泄漏。
24.构造函数为什么一般不定义为虚函数
  • 从存储空间的角度考虑,构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。
  • 从使用的角度来看,虚函数是基类的指针指向派生类的对象时,通过该指针实现对派生类的虚函数的调用,构造函数是在创建对象时自动调用的
  • 从实现上来看,虚函数表是在创建对象之后才有的,因此不能定义成虚函数
  • 从类型上来看,在创建对象时需要明确其类型
25.构造函数或者析构函数中调用虚函数会怎样
  • 程序可以正常运行
  • 但是无法达到虚函数调用的效果,当用基类的指针指向派生类的对象时,在调用基类的构造函数时,若出现虚函数的调用,程序的本意是调用派生类中的虚函数,但是当虚函数出现在构造函数或者析构函数中时,调用的是其所在类(基类)的虚函数。派生类对象构造期间进入基类的构造函数时,对象的类型变成了基类类型,而不是派生类类型。同样进入基类析构函数时,对象也是基类类型。
26.纯虚函数
  • 纯虚函数在类中声明时,加上=0
  • 含有纯虚函数的类称为抽象基类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法
  • 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
  1. 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
  2. 抽象类可以声明为抽象类指针,可以声明抽象类的引用;
  3. 子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。
27.静态绑定和动态绑定的介绍

静态类型和动态类型

  • 静态类型:变量在声明时的类型,是在编译期确定的。静态类型不能更改
  • 动态类型:目前所指对象的类型,是在运行期确定的。动态类型可以更改
    静态绑定和动态绑定
  • 静态绑定是指程序在编译的过程中确定对象的类型(静态类型)
  • 动态绑定是指程序在运行期间确定对象的类型(动态类型)
    静态绑定和动态绑定的区别
    • 发生的时期不同:如上
    • 对象的静态类型不能更改,动态类型可以更改
    • 要想实现多态,必须进行动态绑定
    • 在继承体系中,只有虚函数是动态绑定,其他都是静态绑定

编译时多态和运行时多态(面试时问过)

  • 编译时多态:在程序编译过程中出现, 发生在模板和函数重载中(泛型编程)
  • 运行时多态:在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数
    编译时多态和运行时多态的区别:
  • 编译时多态发生在程序编译过程中,运用泛型编程来实现,在编译时完成,提升程序的运行效率,但是对于无法实现模板的分离编译对于大程序编译时十分耗时
  • 编译时多态无法处理异质对象的集合(异质对象是通过异质类定义的,异质类是指存储类型不一致的数据对象)
  • 运行时多态体现了面向对象的特征,但是虚函数会占用一定的存储空间
  • 运行时多态发生在程序的运行过程中,编译器无法进行优化处理
    引申出:显示接口和隐式接口
  • 显示接口:能够明确来源的接口,例如在运行时多态中,能够明确的知道所调用的函数是来源于哪个类
  • 隐式接口:无法确定来源的接口,例如对于函数重载和模板,不知道是调用哪个实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值