C++面试知识点总结

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

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

  • 编译预处理:处理以 # 开头的指令
  • 编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码
  • 汇编:将汇编代码 .s 翻译成机器指令 .o 文件
  • 链接:汇编程序生成的目标文件并不会立即执行,可能有源文件中的函数引用了另一个源文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些目标文件连接成一个整体,从而生成可执行的程序 .exe 文件。

链接分为两种:

  • 静态链接:代码从其所在的动态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中
  • 动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间

二者的优缺点:

  • 静态链接 浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容
  • 动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要进行链接,性能会有一定的损失。

指针和引用的区别

  • 指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)
  • 指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间(是否占内存)
  • 指针可以为空,但是引用必须绑定对象(是否可为空)
  • 指针可以有多级,但是引用只能一级(是否能为多级)

堆和栈的区别

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

new 和 delete 是如何实现的,new 与 malloc 的异同处
在使用的时候 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 函数从堆上动态分配内存。(自由存储区不等于堆)

C 和 C++ 的区别

  • C 是面向过程的编程,特点是函数;C++ 是面向对象的编程,特点是类。(特性)
  • C 主要用在嵌入式开发、驱动开发和硬件直接打交道的领域;C++ 可以用于应用层的开发、用户界面开发等和操作系统直接打交道的领域。(应用领域)
  • C++ 继承了C的底层操作特性,增加了面向对象的机制,增加了泛型编程、异常处理、运算符重载,还增加了命名空间,避免了命名冲突。(相较于 C 的升级)

C++、Java 的联系与区别,包括语言特性、垃圾回收、应用场景等(java 的垃圾回收机制)
参考链接
二者在语言特性上有很大的区别:

  • 指针:C++ 可以直接操作指针,容易产生内存泄漏以及非法指针引用的问题;JAVA 并不是没有指针,虚拟机(JVM)内部还是使用了指针,只是编程人员不能直接使用指针,不能通过指针来直接访问内存,并且 JAVA 增加了内存管理机制

  • 多重继承:C++ 支持多重继承,允许多个父类派生一个类,虽然功能很强大,但是如果使用的不当会造成很多问题,例如:菱形继承;JAVA 不支持多重继承,但允许一个类可以继承多个接口,可以实现 C++ 多重继承的功能,但又避免了多重继承带来的许多不便

  • 数据类型和类:C++ 可以将变量或函数定义成全局,但是JAVA是完全面向对象的语言,除了基本的数据类型之外,其他的都作为类的对象,包括数组。
    垃圾回收:

  • JAVA 语言一个显著的特点就是垃圾回收机制,编程人员无需考虑内存管理的问题,可以有效的防止内存泄漏,有效的使用空闲的内存

  • JAVA 所有的对象都是用 new 操作符建立在内存堆栈上,类似于 C++ 中的 new 操作符,但是当要释放该申请的内存空间时,JAVA 自动进行内存回收操作,C++ 需要程序员自己释放内存空间,并且 JAVA 中的内存回收是以线程的方式在后台运行的,利用空闲时间。
    应用场景:

  • java 运行在虚拟机上,和开发平台无关,C++ 直接编译成可执行文件,是否跨平台在于用到的编译器的特性是否有多平台的支持,

  • C++ 可以直接编译成可执行文件,运行效率比 JAVA 高

  • JAVA 主要用来开发 web 应用

  • C++ 主要用在嵌入式开发、网络、并发编程的方面

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继承

C++

#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;
}
  • class 可以使用模板,struct 不能

define 和 const 的区别(编译阶段、安全性、内存占用等)

  • 编译阶段:define 是在编译预处理阶段起作用,const 是在编译阶段和程序运行阶段起作用

  • 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的只读变量是有类型的,是要进行判断的,可以避免一些低级的错误

  • 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份;const 定义的只读变量在程序运行过程中只有一份

  • 调试:define 定义的不能调试,因为在预编译阶段就已经进行替换了;const 定义的可以进行调试
    const 的优点:

  • 有数据类型,在定义式可进行安全性检查

  • 可调式

  • 占用较少的空间

在 C++ 中 const 和 static 的用法(定义,用途)
const 参考 这里

  • const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率

  • const 修饰函数参数,使得传递过来的函数参数的值不能改变

  • const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外,函数参数也除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量
    static 定义静态变量,静态函数

  • static 作用于局部变量,改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间

  • static 作用于全局变量和函数,改变了全局变量的作用域,使得全局变量只能在定义它的文件中使用,在源文件中不具有全局可见性

  • static 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数

const 和 static 在类中使用的注意事项(定义、初始化和使用)
参考 这里

static 静态成员变量:

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

static 静态成员函数:

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

const 成员变量

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

const 成员函数

  • 不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量
  • 不能调用非常量成员函数,以防修改成员变量的值

const static
如果要想成员变量在整个类中都是恒定的常量,应该用类的枚举常量或者 static const.
在类中进行声明,在类外进行初始化(类似于类的静态常量)

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

本篇重点总结:
new delete , free malloc 区别和联系
C++ 和 C 的区别
C++ 和 Java 的区别
struct class 区别
define const 区别
const static 用法和作用

(11)计算下面几个类的大小:
参考链接:这里

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

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

class A{
    //静态成员
    static int a;
    const static int b;    
    static int fun1(){}

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

//普通数据成员
class B{
    int a;    
};

//虚函数
class C{
    virtual int fun(){}
};

//结构体对齐原则
class D{
    int a;
    virtual int fun(){}
};
int main(){
    cout << sizeof(A) << endl;//1
    cout << sizeof(B) << endl;//4:表示一个整型变量的大小
    cout << sizeof(C) << endl;//8:虚函数表的指针的大小
    cout << sizeof(D) << endl;//16:整型变量的大小+虚函数指针的大小+对齐原则(4)
    return 0;
}

(12)给一个代码,求输出结果

class A
{
public:
	A(int x){}
};

问:A a = 1;是否正确, 如果正确, 那么它调用了哪些函数?,
这里会进行隐式转化 A a(1).implicit
正确

这类题目更常见的是在基类和子类有不同实现方法。(虚函数相关,例子很多,不多说了)

(13)C++的STL介绍
(这个系列也很重要,建议侯捷老师的这方面的书籍与视频),其中包括内存管理allocator,函数,实现机理,多线程实现等,笔者也整理过STL相关的面经,有兴趣的可以自行查阅

(14)STL 源码中的 hash 表的实现
hashtable 是采用开链法来完成的,(vector + list)

  • 底层键值序列采用 vector 实现,vector 的大小取的是质数,且相邻质数的大小约为 2 倍关系,当创建 hashtable 时,会自动选取一个接近所创建大小的质数作为当前 hashtable 的大小;
  • 对应键的值序列采用单向 list 实现;
  • 当 hashtable 的键 vector 的大小重新分配的时候,原键的值 list 也会重新分配,因为 vector 重建了相当于键增加了,那么原来的值对应的键可能就不同于原来分配的键,这样就需要重新确定值的键。

(15)STL 中 unordered_map 和 map 的区别

  • 底层实现不同:
    unordered_map 底层实现是一个哈希表,元素无序
    map 底层实现是红黑树,其内部所有的元素都是有序的,因此对 map 的所有操作,其实都是对红黑树的操作
  • 优缺点:
    unordered_map:查找效率高;但是建立哈希表比较耗费时间
    map:内部元素有序,查找和删除操作都是 logn 的时间复杂度;但是维护红黑树的存储结构需要占用一定的内存空间
  • 适用情况:
    对于要求内部元素有序的使用 map,对于要求查找效率的用 unordered_map

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

  • 首先,申请一块更大的存储空间,一般是增加当前容量的 50% 或者 100%,和编译器有关;
  • 然后,将旧内存空间的内容,按照原来的顺序放到新的空间中
  • 最后,将旧内存空间的内容释放掉,本质上其存储空间不会释放,只是删除了里面的内容。
    从 vector 扩容的原理也可以看出:vector 容器释放后,与其相关的指针、引用以及迭代器会失效的原因。

(17)vector 使用的注意点及其原因,频繁对 vector 调用 push_back() 对性能的影响和原因
主要是在插入元素方面:插入元素需要考虑元素的移动问题和是否需要扩容的问题
频繁的调用 push_back() 也是扩容的问题对性能的影响

(18)C++ 中的重载和重写(覆盖)的区别:

  • 对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间,子类和父类之间
  • 重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰
  • virtual 关键字:重写的函数基类中必须有 virtual 关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有

(19)C++ 内存管理(热门问题,问过)
参考zheli
C++ 内存分区:栈、堆、自由存储区、全局/静态存储区、常量区

  • 栈:存放函数的局部变量,由编译器自动分配和释放

  • 堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收

  • 自由存储区:和堆十分相似,存放由 new 分配的内存块,由 delete 释放内存

  • 全局区/静态区:存放全局变量和静态变量

  • 常量存储区:存放的是常量,不允许修改
    堆和自由存储区的区别:
    参考链接:zheli

  • 自由存储是 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位)

但是当数组作为参数传递时,数组会自动退化为指针

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 函数的形参改为“指向指针的指针”

v

oid 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. 用函数的返回值来传递动态申请的内存空间
char *GetString(void)
{
 char p[] = "hello world";
 return p; // 编译器将提出警告:将局部变量返回
}
void Test4(void)
{
 char *str = NULL;
 str = GetString(); // str 的内容是垃圾
 cout<< str << endl;
}

char *GetString2(void)
{
 char *p = "hello world";
 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. 指针操作超过了变量的作用范围。例如:在函数中将一个局部变量的地址作为函数的返回值,这里编译器会给出警告,因为离开该函数后,局部变量的空间就会释放掉,返回的地址(指针)相当于是野指针。

深拷贝和浅拷贝带来的问题:
类中默认的拷贝构造函数和赋值构造函数都是浅拷贝,当类的成员变量中出现指针变量时,最好使用深拷贝,避免内存空间多次释放的问题出现。

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

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

(20)面向对象和面向过程的区别(问过)

  • 面向过程: 以事件为中心的编程思想,编程时把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数
  • 面向对象: 世界上有很多的人和事物,每一个都可以看作一个对象,每个对象都有自己的属性和行为,对象和对象之间可以通过方法来交互,对象的抽象称为类

(21) 什么是面向对象,介绍面向对象的三大特性,并且举例说明每一个。
面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。

  • 封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。
  • 继承:子类继承父类的特征和行为,子类有父类的非private方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被final关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
  • 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式

(22) 多态的实现和应用场景
多态通过虚函数实现
应用场景:在实际开发中,一个功能有多种实现方式,流程相同,但是具体的细节有区别,例如:支付功能,可以进行支付宝支付、微信支付等

(23) C++虚函数相关(虚函数表,虚函数指针),虚函数的实现原理(热门,重要)
虚函数是通过虚函数表来实现的,虚函数表里面保存了虚函数的地址,这张表保存在含有虚函数的类的实例对象的内存空间中,虚函数表的指针存放在对象实例的最前面的位置.

虚函数表是在编译阶段建立的,也就是说在程序的编译过程中会将虚函数的地址放在虚函数表中。(问过)

(24)虚继承(和虚函数混合在一起问)
提出:
虚继承是为了解决多重继承中命名冲突和数据冗余问题而提出的。例如:类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的成员变量和成员函数。

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类,这个被共享的基类就称为虚基类。

虚基类的成员可见性问题:
假设A中定义了一个名为x的成员变量,当我们直接访问x时,会有三种可能:

  • 如果B1,B2中都没有定义x,那么x将被解析为A的成员,不存在二义性;
  • 如果B1或B2其中的一个类定义了x,那么也不存在二义性的问题,派生类的x比虚基类的x的优先级更高;
  • 如果B1和B2中都定义了x,那么直接访问x会产生二义性问题

(25) 实现编译器处理虚函数表应该如何处理

  • 编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。
  • 如果派生类没有重新定义基类的虚函数A,则派生类的虚函数表中保存的是基类的虚函数A的地址,也就是说基类和派生类的虚函数A的地址是一样的
  • 如果派生类重写了基类的某个虚函数B,则派生的虚函数表中保存的是重写后的虚函数B的地址,也就是说虚函数B有两个版本,分别存放在基类和派生类的虚函数表中
  • 如果派生类重新定义了新的虚函数C,派生类的虚函数表保存新的虚函数C的地址

(26) 析构函数一般写成虚函数的原因
析构函数定义成虚函数是为了防止内存泄漏,因为当父类的指针或者引用指向或绑定到子类的对象时,如果未将基类的析构函数定义成虚函数,会调用基类的析构函数,那么只能将基类的成员所占的空间释放掉,派生类中特有的就会无法释放内存空间导致内存泄漏。

(27) 构造函数为什么一般不定义为虚函数

  • 从存储空间的角度考虑,构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。
  • 从使用的角度来看,虚函数是基类的指针指向派生类的对象时,通过该指针实现对派生类的虚函数的调用,构造函数是在创建对象时自动调用的
  • 从实现上来看,虚函数表是在创建对象之后才有的,因此不能定义成虚函数
  • 从类型上来看,在创建对象时需要明确其类型

(28) 构造函数或者析构函数中调用虚函数会怎样
参考:这里

  • 程序可以正常运行
  • 但是无法达到虚函数调用的效果,当用基类的指针指向派生类的对象时,在调用基类的构造函数时,若出现虚函数的调用,程序的本意是调用派生类中的虚函数,但是当虚函数出现在构造函数或者析构函数中时,调用的是其所在类(基类)的虚函数。派生类对象构造期间进入基类的构造函数时,对象的类型变成了基类类型,而不是派生类类型。同样进入基类析构函数时,对象也是基类类型。

(29) 纯虚函数

  • 纯虚函数在类中声明时,加上=0,
  • 含有纯虚函数的类称为抽象基类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法
  • 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
    抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
    抽象类可以声明为抽象类指针,可以声明抽象类的引用;
    子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。

(30) 静态绑定和动态绑定的介绍
静态类型和动态类型:

  • 静态类型:变量在声明时的类型,是在编译期确定的。静态类型不能更改

  • 动态类型:目前所指对象的类型,是在运行期确定的。动态类型可以更改
    静态绑定和动态绑定:

  • 静态绑定是指程序在编译的过程中确定对象的类型(静态类型)

  • 动态绑定是指程序在运行期间确定对象的类型(动态类型)
    静态绑定和动态绑定的区别:

  • 发生的时期不同:如上

  • 对象的静态类型不能更改,动态类型可以更改

  • 要想实现多态,必须进行动态绑定

  • 在继承体系中,只有虚函数是动态绑定,其他都是静态绑定

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

  • 编译时多态:在程序编译过程中出现, 发生在模板和函数重载中(泛型编程)
  • 运行时多态:在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数

编译时多态和运行时多态的区别:

  • 编译时多态发生在程序编译过程中,运用泛型编程来实现,在编译时完成,提升程序的运行效率,但是对于无法实现模板的分离编译对于大程序编译时十分耗时

  • 编译时多态无法处理异质对象的集合(异质对象是通过异质类定义的,异质类是指存储类型不一致的数据对象)

  • 运行时多态体现了面向对象的特征,但是虚函数会占用一定的存储空间

  • 运行时多态发生在程序的运行过程中,编译器无法进行优化处理
    引申出:显示接口和隐式接口

  • 显示接口:能够明确来源的接口,例如在运行时多态中,能够明确的知道所调用的函数是来源于哪个类

  • 隐式接口:无法确定来源的接口,例如对于函数重载和模板,不知道是调用哪个实现

(31) 引用是否能实现动态绑定,为什么引用可以实现
参考链接:这里
能够实现动态绑定,因为对象的类型是确定的,引用时在运行阶段根据他们绑定的具体对象的类型确定的。
最根本的原因在于:多态是通过虚函数实现的,而虚函数是通过存储在对象的内存空间中的指针调用的;

  • 当基类的指针指向派生类的对象时,向上进行类型转化的是地址,此时派生类的对象中指向虚函数的指针并不会发生变化,只是改变了基类指针的地址,因此可以实现多态
  • 当派生类的对象赋值给基类的对象,此时派生类对象的指向虚函数的指针会发生变化(隐式的向上转化),变为基类的对象的虚函数的指针,因此无法实现多态

(32) 深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)
参考:这里

如果一个类拥有资源,该类的对象发生了复制,如果资源发生了重新分配,就是深拷贝,否则就是浅拷贝

  • 深拷贝:该对象和原对象占用不同的存储空间,即拷贝位于stack域(栈)中的内容,又拷贝类中位于heap域(堆)中的内容
  • 浅拷贝:该对象和原对象占用同一块内存区域,仅拷贝类中位于stack域(栈)中的内容
    当类的成员变量中有指针时,使用深拷贝安全,如果两个对象指向同一块内存空间,当其中一个对象的删除后,该块内存空间就会被释放,另外一个对象指向的就是垃圾内存。

(33) 对象复用的了解,零拷贝的了解(没问到过)

  • 零拷贝:不需要cpu参与在内存之间复制数据的操作
  • 对象的复用:享元模式,通过创建一个对象池,以避免对象的重复创建

(34) 介绍C++所有的构造函数
构造函数的作用:当创建对象时,系统分配了内存空间后,会自动调用相应的构造函数

  • 默认构造函数:没有参数,如果创建了一个类,没有定义任何构造函数,系统会自动生成默认的构造函数
  • 一般构造函数:带有参数,一个类可以有若干个一般构造函数,前提是参数的数量或者类型不同(C++重载函数原理)
  • 拷贝构造函数:参数为该类的常量引用对象,如果类中没有定义拷贝构造函数,系统会默认生成一个默认的拷贝构造函数,默认生成的拷贝构造函数都是浅拷贝的
  • 赋值构造函数:区别于以上构造函数,以上构造函数都没有函数的返回类型,这里虽然称为“赋值构造函数”,其实是“重载了赋值运算符的函数”,该函数的返回类型是该类的引用类型,参数是该类的常量引用对象。

拷贝构造函数(复制)和赋值构造函数(赋值)的区别:参考链接:这里

  • 二者的参数类型都是该类的常量引用对象,但是,拷贝构造函数没有返回类型,赋值构造函数的返回类型是该类的引用类型
  • 在实现过程中,调用拷贝构造函数是在创建一个新的对象时,所以在函数内直接申请空间(heap)然后给相应的成员变量赋值即可;调用赋值构造函数时,源对象和目标对象都已声明,所以在函数内需要将目标对象的内存空间(heap)释放掉,重新申请空间,再进行拷贝操作
  • 二者的使用场景不同,拷贝构造函数的使用场景见如下三种情况

(35) 什么情况下会调用拷贝构造函数(三种情况)
参考:这里

  • 创建类的对象时,有两种情形:代入法 classA obj(1,2);赋值法 classB obj1 = obj
  • 当类的对象作为函数的参数时,由实参到形参的复制过程会调用拷贝构造函数
  • 当类的对象作为函数的返回值时,会将返回值通过调用拷贝构造函数复制给一个临时对象,并传到函数的调用处
  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值