c++类和继承面试点25连问

cpp加油站 等级 1726 2 0

本篇文章连问面试时经常会遇到的类和继承相关25个问题,看看你能回答出几道题呀。

还是先看一下思维导图,如下:

c++类和继承面试点25连问

1. c++的三大特性是什么

c++的三大特性,说白了其实就是面向对象的三大特性,是指:封装、继承、多态,简单说明如下:

  • 封装是一种技术,它使类的定义和实现分离,也就是隐藏了实现细节,只留下接口给他人调用,另外封装还有一层意义是它把某种事物具现出属性和方法并形成了一个整体,就像一个人,同时具有身高和身体等等这些,才是完整的人,如果不封装,那这个人就相当于四分五裂了;
  • 继承,所谓继承,其实就是真实意义上讲的继承了某些东西,放到c++的类里面,其实就是实现了代码的重用,即派生类要使用基类的属性和方法,就不用再重新编写代码,这种可以算是实现继承。还有一种就是继承了某样东西,但是派生类需要重新实现一下,也就是接口继承,下面第三点要讲的多态就是接口继承的典型代表;
  • 多态,多种形态,就是我们使用基类的指针或者引用调用基类的某个函数时,编译期并不知道到底是要调用哪个函数,因为我们不能确定这个指针或者引用到底指向基类对象还是派生类对象,直到运行时才能确定,这个就叫多态。

2. c++继承的优点和缺点

优点:根据第1点中讲的,其实继承优点就是实现了代码的重用和接口的重用;

缺点:子类会继承父类的部分行为,父类的任何改变都可能影响子类的行为,也就是说,如果继承下来的实现不适合子类的问题,那么父类必须重写或者被其他的类替换,这种依赖关系限制了灵活性。

从以上对比看,同一种属性既可以是优点,也可以是缺点,就看个人在编程过程中的灵活运用了。

3. 派生类调用构造函数和析构函数的顺序

看代码:

#include <iostream>
using namespace std;

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

class B:public A
{
public:
    B()
    {
        cout << "B()" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
};

int main()
{
    B b;
    return 0;
}

输出结果如下:

A()
B()
~B()
~A()

根据结果,可知顺序如下:

  • 派生类对象定义时,先调用基类的构造函数,再调用派生类的构造函数;
  • 派生类对象销毁时,先调用派生类的析构函数,再调用基类的析构函数。

4. c++中多态有什么作用

个人理解,其实就是实现了接口的重用,同样的接口,派生类与基类不同的实现。

5. 多态的实现原理

一般来讲多态分为编译时多态和运行时多态,编译时多态就是指的重载哪些,我们通常默认多态是运行时的多态。

运行时多态简单来讲就是:使用基类指针或者引用指向一个派生类对象,在非虚继承的情况下,派生类直接继承基类的虚表指针,然后使用派生类的虚函数去覆盖基类的虚函数,这样派生类对象通过虚表指针访问到的虚函数就是派生类的虚函数了。

更详细的说明请看之前写的这篇文章:c++头脑风暴-多态、虚继承、多重继承内存布局

6. 类成员函数的重载、覆盖和隐藏的区别

重载即为函数重载,重载的特征:

  • 相同的范围,也就是在同一个类中;
  • 函数名字相同;
  • 函数参数不同;
  • virtual关键字无影响。

覆盖是指派生类函数覆盖基类函数,覆盖的特征:

  • 不同的范围,即函数分别位于派生类和基类中;
  • 函数名字相同;
  • 函数参数相同;
  • 基类函数必须有virtual关键字。

隐藏是指派生类的函数屏蔽了与其同名的基类函数,特征如下:

  • 如果派生类的函数与基类的函数同名,但是参数不同,此时不论有没有virtual关键字,基类的函数都将被隐藏;
  • 如果派生类的函数与基类的函数同名,参数也相同,但是基类函数没有virtual关键字,此时,基类的函数将被隐藏;

总结:函数名相同,参数也相同的情况下,如果基类函数有virtual关键字,则是多态,否则就是隐藏;函数名相同,参数不同的情况下,如果函数位于同一个类中,则是重载,否则就是隐藏。

7. 析构函数是否可以为虚函数?如果可以,有什么作用?

析构函数可以是虚函数,因为它是对象结束时才调用,不影响虚表构建。

那么析构函数作为虚函数有什么作用呢,看这样一段代码:

#include <iostream>
using namespace std;

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

class B:public A
{
public:
    B()
    {
        cout << "B()" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
};

int main()
{
    A* a = new B;
    if ( a != nullptr )
    {
        delete a;
    }
    return 0;
}

这段代码执行后输出如下:

A()
B()
~A()

构造的时候是正常的,但是析构的时候只调用了基类的析构函数,此时我们把类A的析构函数修改为virtual,看看结果:

A()
B()
~B()
~A()

一般情况下,只有当一个类被用作基类时才需要使用虚析构函数,这样做的作用是当一个基类的指针删除派生类的对象时,能确保派生类的析构函数会被调用。因为销毁的时候直接销毁的基类指针,此时编译器只知道调用基类析构,并不会主动去调用派生类的析构函数,所以基类析构函数需为虚析构函数,这样运行时程序才会去调用派生类的析构函数,其实这就相当于析构函数的多态,基于多态的作用,这个指向派生类的基类指针会先调用派生类的析构函数,然后再调用基类的析构函数。

所以当类有派生类时,析构函数一定要是虚函数。

8. 构造函数里面”初始化列表”和”赋值”的区别

初始化列表和赋值的区别如下:

  • 初始化列表只会调用一次构造函数,其实就是变量声明时初始化;
  • 赋值会先调用构造函数,再调用一次赋值函数,它相当于在声明后,又进行了赋值。

9. 构造函数什么情况下必须使用初始化列表

实际上,根据上面第8点,赋值是先声明以后再赋值的,我们初次接触c++的时候就应该知道有些类型是必须要声明的时候就有初值的,这里我想到的有以下类型:

  • const声明的变量,必须要有初值;
  • reference引用声明的变量,必须要有初值;
  • 没有默认构造函数但存在有参构造函数的类,它必须初始化的时候给一个入参。

以上三种情况都必须使用初始化列表而不能在构造函数中进行赋值。

10. 什么情况下要使用虚继承?

多重继承时需要使用虚继承,一般的我们在多重继承时使用虚继承来防止二义性问题。

看下面这段代码:

#include <iostream>
using namespace std;

class A
{
public:
    A()
    {
        cout << "A()" << endl;
    }
    virtual ~A()
    {
        cout << "~A()" << endl;
    }
};

class B: public A
{
public:
    B()
    {
        cout << "B()" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
};

class C: public A
{
public:
    C()
    {
        cout << "C()" << endl;
    }
    ~C()
    {
        cout << "~C()" << endl;
    }
};

class D:public B, public C
{
};

int main()
{
    D d;
    return 0;
}

执行后输出结果如下:

A()
B()
A()
C()
~C()
~A()
~B()
~A()

看到没有类A的构造函数和析构函数都执行了两次,这很显然是不正确的,因为执行类B构造函数时要执行一次类A的构造函数,执行类C的时候也要执行一次类A的构造函数,析构函数同理,到这里问题还不大,毕竟可以编译和运行。

把代码改一下,如下:

#include <iostream>
using namespace std;

class A
{
public:
    A()
    {
        cout << "A()" << endl;
    }
    void print()
    {
        cout << "print()" << endl;
    }
    virtual ~A()
    {
        cout << "~A()" << endl;
    }
};

class B: public A
{
public:
    B()
    {
        cout << "B()" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
};

class C: public A
{
public:
    C()
    {
        cout << "C()" << endl;
    }
    ~C()
    {
        cout << "~C()" << endl;
    }
};

class D:public B, public C
{
};

int main()
{
    D d;
    d.print();
    return 0;
}

编译直接就报错了:

test.cpp:54:4: 错误:对成员‘print’的请求有歧义

这就是二义性了,解决办法是使用形如class C: virtual public A这样的虚继承形式,B虚继承A,C也虚继承A,那样就可以编译通过,且运行也都是没有问题的。

11. 怎么防止类对象被拷贝和赋值?

防止类对象被拷贝和赋值,无非是禁止类对象调用拷贝构造函数和赋值函数,在c++11以后有三种方法:

  • 拷贝构造函数和赋值函数定义为私有的;
  • 私有继承基类;
  • 构造函数后面加=delete,这是c++11新增的用法;

12. 构造函数里面是否可以为虚函数?

答案是不可以,构造函数是不能声明为virtual的,这与虚函数的机制有关,虚函数是存放在虚表的,而虚表是在构造函数执行过程中才建立的,构造函数声明为virtual就会陷入到是先有鸡还是先有蛋的尴尬境地,所以编译器做了限制。

13. 构造函数里面是否可以抛出异常?

构造函数可以抛出异常,若有动态分配内存,则要在抛异常之前手动释放。

有关构造函数最全面的说明请看这篇文章:最全面的c++中类的构造函数高级使用方法及禁忌

14. struct和class区别

区别如下:

struct的成员默认是公有的,class的成员默认是私有的。

一个原则:当类中有很少的方法并且有公有数据时,应该使用struct关键字,否则使用class关键字。

15. 析构函数是否可以抛异常

可以,但是最好不要抛出,如果一定要抛出,那要在析构函数内部处理,保证析构函数能执行完成。

16. 构造函数里面是否可以调用虚函数

可以调用,因为虚函数表是在编译期建立的,当调用构造函数时,首先就会初始化虚函数指针,那我们就知道了虚函数的地址,当然可以调用虚函数了。

17. 什么是友元函数

在函数前面加上friend,这个函数就变成了友元函数,它代表这个函数与某个类成为朋友了,此时访问类的私有成员也是不受限制的。

18. 友元类是什么

与友元函数类似,在一个类A中声明另外一个类B为friend类型,那么这个类B就是友元类,它访问类A的私有成员和保护成员都不受限制。

有关友元详细说明,请看这篇文章:c++类访问权限及友元

19. 友元是否违反了封装的原则

违反了,友元函数可以不受访问权限的限制而访问类的任何成员,也就是它可以直接接触类的实现,当然违反了封装的原则,只是有时基于我们自身的某些使用场景,不得不使用友元。

20. 多重继承时类对象内存布局

非虚继承时,按照继承顺序存储,虚继承时,虚基类的内容放在一块内存的最后面存储。

详细的看之前这篇文章:c++头脑风暴-多态、虚继承、多重继承内存布局

21. 类的大小由哪些因素决定?空类是多大?

由成员变量和是否有虚函数决定,如果类中有虚函数,那就在所有成员变量的基础上加上一个虚函数指针的大小,在64位机器中,虚函数指针为8个字节,注意计算类大小的时候要考虑字节对齐的问题。

空类大小为1个字节。

22. new一个类的时候发生了什么

new其实就是申请动态内存,而一个类只有虚指针和成员变量才需要内存,所以new一个类就是给虚指针和成员变量申请内存空间。

23. 类的成员函数有地址吗?

有呀,编译器编译的时候就给了成员函数地址,且一个类的成员函数是唯一的,所有对象共用。

24. 类指针被赋值成NULL还能调用成员函数吗

可以的,看以下代码:

#include <iostream>
using namespace std;

class CPeople
{
public:
    double height;
    int age;
    char sex;

public:
    CPeople(){}
    ~CPeople(){}
    void print()
    {
        cout << "print()" << endl;
    }
};

int main()
{
    CPeople *people = nullptr;
    people->print();
    return 0;
}

粗粗一看,代码使用了空指针调用,结合我们知道的,如果使用了空指针,就会发生段错误,那这里肯定也会发生段错误,但实际上编译执行后并没有产生错误,print函数被正确执行了,这就很尴尬了,这是为什么呢?

这是因为类的成员函数的实现机制,上题说了,类的成员函数跟某个对象无关,实际上它被编译后,我们可以把它理解为一个全局性的函数,从汇编的角度看,print函数被编译后真正的函数名是_ZN7CPeople5printEv这个,并且此时因为print函数没有使用类CPeople的任何成员,它当然可以正常的执行。

但是,假设在print里面调用了某个成员变量呢,如下:

#include <iostream>
using namespace std;

class CPeople
{
public:
    double height;
    int age;
    char sex;

public:
    CPeople(){age = 100;}
    ~CPeople(){}
    void print()
    {
        cout << "age=" << this->age << endl;
    }
};

int main()
{
    CPeople *people = nullptr;
    people->print();
    return 0;
}

这次再执行就会报段错误了,为什么呢,因为成员函数是公用的,但是成员变量却是每个对象独有的,没有为people分配空间,就是没给成员变量分配空间,且此时people为空指针,那给成员函数传入的隐形this指针也是空指针,它怎么可能访问到某个成员变量呢。

25. 什么是纯虚函数?什么是抽象类?

看一下这段代码:

class CPeople
{
public:
    CPeople(){}
    ~CPeople(){}
    virtual void print() = 0;
};

这段代码里面print就是纯虚函数,所谓纯虚函数其实就是虚函数后面加= 0,此时print函数是不需要实现的,它只是定义了一个抽象接口而已。

同样的,这段代码里面的CPeople就是抽象类了,某个类不论是自己定义了纯虚函数,还是从其他基类继承了纯虚函数但却并没有实现的,都可以称为抽象类,所谓抽象,其实就是具体的反义词,比方说这里只给了一个接口,但是接口到底是怎么实现的,不知道,这就叫做抽象了。

好了,本篇文章就为大家介绍到这里,觉得内容对你有用的话,记得顺手点个赞哦~

收藏
评论区

相关推荐

c++头脑风暴-多态、虚继承、多重继承内存布局
本篇文章深入分析多态、虚继承、多重继承的内存布局以及实现原理。首先还是看一下思维导图:下面根据这个大纲一步一步的进行深入解析。 一、没有虚函数时内存布局是怎样的 1. 没有虚函数时类的内存布局一个类没有虚函数的时候,其实就是结构体,它的内存布局就是按照成员变量的顺序来的。看如下代码:cppinclude <iostreamusing namespace s
c++类和继承面试点25连问
本篇文章连问面试时经常会遇到的类和继承相关25个问题,看看你能回答出几道题呀。还是先看一下思维导图,如下: 1. c++的三大特性是什么c++的三大特性,说白了其实就是面向对象的三大特性,是指:封装、继承、多态,简单说明如下: 封装是一种技术,它使类的定义和实现分离,也就是隐藏了实现细节,只留下接口给他人调用,另外封装还有一层意义是它把某种事物具现出属性和方
C++中初始化的顺序问题
C++的初始化顺序非常重要,牢记才能不出常识性的错误。 其初始化顺序为: 1 类中的static成员是最先初始化的,这个是先于main函数的执行的,但是必须注意,如果这个成员只是在类中声明,而没有在类外边进行定义的话,那么这个是不会开辟内存的,是不会初始化的。 2 调用基类的构造函数。但是基类分为两种顺序,特别注意。一种是虚继承的基类;另一种是普通继承
C++中基类虚析构函数的作用及其原理分析
虚析构函数的理论前提是 执行完子类的析构函数,那么父类的虚构函数必然会被执行。 那么当用delete释放一个父类指针所实例化的子类对象时,如果没有定义虚析构函数,那么将只会调用父类的析构函数,而不会调用子类的虚构函数,导致内存的泄漏。 故: 继承时,要养成的一个好习惯就是,基类析构函数中,加上virtual。 知识背景 ====          
C++基类的析构函数定义为虚函数的原因
1:每个析构函数只会清理自己的成员(成员函数前没有virtual)。 2:可能是基类的指针指向派生类的对象,当析构一个指向派生类的成员的基类指针,这时程序不知道这么办,可能会造成内存的泄露,因此此时基类的析构函数要定义为虚函数; 基类指针可以指向派生类的对象(多态),如果删除该指针delete\[\]p,就会调用该指针指向的派生类的析构函数,而派生类
C++构造函数调用虚函数的后果
#include <iostream> class cx { public: virtual void func() { std::cout << "func" << std::endl; } cx() { func(); //构
C++类有继承时,析构函数必须为虚函数
C++类有继承时,析构函数必须为虚函数。如果不是虚函数,则使用时可能存在内在泄漏的问题。 假设我们有这样一种继承关系: ![](https://oscimg.oschina.net/oscnet/5f0452c79b70794f2e4689cffa37f5a99f1.png) ### 如果我们以这种方式创建对象: SubClass* pObj
Java对象的引用类型
![](https://oscimg.oschina.net/oscnet/24d3ddce8c92eb32f8e3a68063234324da7.jpg)      Java对象的引用类型有强引用,软引用,弱引用,虚引用和FinalReference,提供这几种引用类型的主要目的: 1.程序员可以通过不同的引用方式决定某些对象的生命周期;2.
Java虚拟机类加载机制
### 概述   虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。   与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载、连接和初始化过程都在程序运行期间完成的,这种策略虽然会稍微增加一些系统性能开销,但是会为Java应用程序
Java高级篇——深入浅出Java类加载机制
> **类加载器** 简单讲,类加载器ClassLoader的功能就是负责将class文件加载到jvm内存。 > **类加载器分类** 从虚拟机层面讲分为两大类型的类加载器,一是Bootstrap Classloader即启动类加载器(C++实现),它是虚拟机的一部分,二是其他类型类加载器(JAVA实现),在虚拟机外部,并全部继
Boost Python官方样例(三)
### 导出C++类(纯虚函数和虚函数) 大致做法就是为**class**写一个**warp**,通过**get\_override**方法检测虚函数是否被重载了,如果被重载了调用重载函数,否则调用自身实现,最后导出的时候直接导出**warp**类,但是类名使用**class**,析构函数不需要导出,因为它会被自动调用 #### 纯虚函数 编写C++函
C++primer学习笔记(六)
1. virtual函数是基类希望派生类重新定义的函数,希望派生类继承的函数不能为虚函数。根类一般要定义虚析构函数。 2. 派生类只能通过派生类对象访问protected成员,不能用基类对象访问。基类定义为virtual就一直为虚函数,派生类写不写virtual都是虚函数。用做基类的类必须是已定义的。 3. 存在虚函数+指针或引用=
Javascript 构造函数和类
###1.构造函数 构造函数的名称一般都是首字母大写 挂载在this上面的属性为实例属性,实例属性再每个实例间都是独立的 原型链属性通过prototype添加,他是所有实例共享的 类方法/静态属性只能由构造函数本身访问 当实例属性和原型链上的属性重名时,优先访问实例属性,没有实例属性再访问原型属性 大多数浏览器的 ES5 实现之中,每一个对象都有\_\_pr
Jvm类的加载机制
1.概述 ---- 虚拟机加载Class文件(二进制字节流)到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的Java类型,这一系列过程就是类的加载机制。 2.类的加载时机 -------- 类从被虚拟机加载到内存开始,直到卸载出内存为止,整个生命周期包括:加载——验证——准备——解析——初始化——使用——卸载 这7个阶段。其中验
System类 和 Runtime 类
java程序在不同操作系统上运行时,可能需要取得平台相关属性,或者调用平台本地命令(如windows下sys32和system64下的可执行文件、本地其他语言写的函数等) 来完成特定功能.java提供了System和Runtime两个类来与程序的运行平台交互。 1.System类 --------- 首先,看构造器(constructor),是一个私有的