C++说爱你不容易-5

C++软件与嵌入式软件面经解析大全(蒋豆芽的秋招打怪之旅)


本章讲解点

  • 1.1 C++与C的区别——看看你的理解是否深刻
  • 1.2 从代码到可执行文件的过程
  • 1.3 extern "C"
  • 1.4 宏——到底是什么
  • 1.5 内联函数
  • 1.6 条件编译
  • 1.7 字节对齐详解
  • 1.8 Const——今天必须把它搞懂
  • 1.9 Static作用
  • 1.10 volatile和mutable
  • 1.11 volatile在嵌入式里的应用
  • 1.12 原子操作
  • 1.13 指针与引用的区别
  • 1.14 右值引用
  • 1.15 面向对象的编程思想
  • 1.16 类
  • 1.17 类的成员
  • 1.18 友元函数
  • 1.19 初始化列表
  • 1.20 this指针
  • 1.21 继承
  • 1.22 多态
  • 1.23 虚函数与重写
  • 1.24 虚构造函数与虚析构函数
  • 1.25 函数重载
  • 1.26 操作符重载
  • 1.27 迭代器与指针
  • 1.28 模板
  • 1.29 C++智能指针
  • 1.30 四种cast转换
  • 1.31 Lambda
  • 1.32 function和bind

受众:本教程适合于C/C++已经入门的学生或人士,有一定的编程基础。

本教程适合于互联网嵌入式软件求职的学生或人士。

img

故事背景

img

蒋 豆 芽:小名豆芽,芳龄十八,蜀中人氏。卑微小硕一枚,科研领域苟延残喘,研究的是如何炒好一盘豆芽。与大多数人一样,学习道路永无止境,间歇性踌躇满志,持续性混吃等死。会点编程,对了,是面对对象的那种。不知不觉研二到找工作的时候了,同时还在忙论文,豆芽都秃了,不过豆芽也没头发啊。

隔壁老李:大名老李,蒋豆芽的好朋友,技术高手,代码女神。给了蒋豆芽不少的人生指导意见。

导 师:蒋豆芽的老板,研究的课题是每天对豆芽嘘寒问暖。

img

故事引入

img

隔壁老李:(笑容邪魅)豆芽~~

蒋 豆 芽:(惊吓)怎么了,老李,你的眼神不怀好意啊。

隔壁老李:嘿嘿,豆芽,因为内容太多,其实我们上一章还没讲完。

蒋 豆 芽:(晕)害,学海无涯苦作舟啊。

隔壁老李:哈哈,不用担心,豆芽,好好学习,肯定能找到好工作的。

img

1.21 继承

img

隔壁老李:行吧我们就继续讲了,我们在前面介绍了C++三大特性之一的“封装”。这章我们介绍三大特性之一的“继承”。

蒋 豆 芽:(疑惑)那继承又是什么呢?

隔壁老李:别急,豆芽,听我慢慢讲。

面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。

当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类

蒋 豆 芽:很有道理,父亲的财产当然可以由儿子来继承了,但是老婆不行,因为老婆是私人财产。

隔壁老李:(嫌弃)怎么听起来这么俗啊!但是话糙理不糙。

我们给个例子:

// 基类
class Animal {
    // eat() 函数
    // sleep() 函数
};

//派生类
class Dog : public Animal {
    // bark() 函数
};

哺乳动物是动物,狗是哺乳动物,因此,狗是动物。

动物需要吃饭、睡觉,狗也需要吃饭、睡觉,因此狗可以继承动物同样的行为。而狗也有自己独特的行为,如狂吠。

蒋 豆 芽:懂了懂了。这就是C++的第二大特性——继承

再总结一下继承类型访问属性

当一个类派生自基类,该基类可以被继承为 public、protectedprivate 几种类型。

我们几乎不使用 protectedprivate 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

总结: 不管是哪种继承方式,派生类中新增成员可以访问基类的公有成员和保护成员,无法访问私有成员。但是只有公有继承中,派生类的对象能访问基类的公有成员。使用友元(friend)可以访问保护成员和私有成员。

隔壁老李:豆芽你总结得很好,我们紧接着就讲讲多重继承

我们直接看个例子:

// 基类 Douyafather
class Douyafather {
   public:
      void setWidth(int w){
         width = w;
   protected:
      int width;
};

// 基类 Douyamother
class Douyamother {
   public:
      int getCost(int area){
         return area * 70;
      }
};

// 派生类
class Douya: public Douyafather, public Douyamother{ // 多重继承
   public:
      int getArea(){ 
         return width; 
      }
};

蒋 豆 芽:确实,这个就很直观了。

隔壁老李:但是多重继承会出现一种非常特别的情况,那就是菱形继承

img

类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C。

这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突

假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B-->D 这条路径,还是来自 A-->C-->D 这条路径。比如你爸和你妈都有一份财产让豆芽你来继承,但是你一想,不对啊,你爸的工资卡不是在你妈妈那里吗?你不就很困惑了吗?下面是菱形继承的具体实现:

//间接基类A
class A{
protected:
    int m_a;
};

//直接基类B
class B: public A{
protected:
    int m_b;
};

//直接基类C
class C: public A{
protected:
    int m_c;
};

//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //命名冲突: error: reference to ‘m_a’ is ambiguous
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

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

怎么样,豆芽,你明白了吗?

蒋 豆 芽:(恍然大悟)哎呀,我想起来了呀,之前有面试官问我多重继承的二义性问题,我当时没回答出来,原来是这么回事啊。那老李,应该怎么解决呢?

隔壁老李:使用虚继承。为了解决多继承时的命名冲突和冗余数据问题,C++提出了虚继承,使得在派生类中只保留一份间接基类的成员。再回到你继承你爸妈财产的问题,现在你明白了,你爸的财产是“虚”的,你最终其实继承的是你妈妈的那一份。

蒋 豆 芽:害,男人不哭。

隔壁老李:哈哈,我们继续,在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:

//间接基类A
class A{
protected:
    int m_a;
};

//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};

//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};

//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

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

这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。如下图。

img

蒋 豆 芽:太棒了!

隔壁老李:但是豆芽,使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工作都会变得更加困难,因此我们不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承

也正是由于这个原因,C++ 之后的很多面向对象的编程语言,例如 Java、C#、PHP等,都不支持多继承。

蒋 豆 芽:懂了懂了!

img

1.22 多态

img

隔壁老李:接下来我们就要讲相当重要的概念了,C++第三大特性——多态

我们从一个例子引入:

#include <iostream>  
using namespace std;  
class Parent {  
public:  
    Parent() {}  
    void func() { cout << "Parent" << endl; }  
};  

class Child :public Parent {  
public:  
    Child() {}  
    void func() { cout << "Child" << endl; }  
};  

int main() {  
    Parent *Pparent;  
    Parent parent;  
    Pparent = &parent;  
    Pparent->func();  
    Child child;  
    Pparent = &child;  
    Pparent->func();  
    system("Pause");  
    return 0;  
}  

隔壁老李:豆芽你看这段代码,Child类继承了我们的Parent类,两个类里面有个同名函数func(),输出不同的内容。

而在我们的main函数中,我们定义了一个Parent指针,前后分别指向了Parent和Child对象,从这段代码的逻辑来看,豆芽你觉得作者是想输出什么内容?

蒋 豆 芽:按照代码逻辑来看,*Pparent指针先指向一个Parent对象,调用func函数想输出"Parent",后指向一个Child对象,调用func函数想输出" Child"。

隔壁老李:那我们来看看最后的结果,很遗憾,程序并没有按照我们想要的逻辑来实现。

Parent
Parent

正是因为如此,人们就希望达到这样的目的:指针指向基类则调用基类的方法,指针指向派生类则调用派生类的方法。这样程序变得十分灵活,增强了代码的可复用性。

所以C++为实现这个目的,引入了多态的机制,多态的实现机制是虚函数

虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类的同名函数

方法是在基类中为同名函数添加关键字virtual,如下:

#include <iostream>  
using namespace std;  
class Parent {  
public:  
    Parent() {}  
    virtual void func() { cout << "Parent" << endl; }  
};  

class Child :public Parent {  
public:  
    Child() {}  
    void func() { cout << "Child" << endl; }  
};  

int main() {  
    Parent *Pparent;  
    Parent parent;  
    Pparent = &parent;  
    Pparent->func();  
    Child child;  
    Pparent = &child;  
    Pparent->func();  
    system("Pause");  
    return 0;  
}  

结果如下:

Parent
Child

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。

换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)

蒋 豆 芽:只需要添加一个virtual关键字就解决了这个问题,也太神奇了吧?这到底是什么样的实现机制呢?

img

1.23 虚函数与重写

img

隔壁老李:C++实现虚函数的原理是虚函数表+虚表指针

当一个类里存在虚函数时,编译器会为类创建一

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

<p> - 本专刊适合于C/C++已经入门的学生或人士,有一定的编程基础。 - 本专刊适合于互联网C++软件开发、嵌入式软件求职的学生或人士。 - 本专刊囊括了C语言、C++、操作系统、计算机网络、嵌入式、算法与数据结构等一系列知识点的讲解,并且最后总结出了高频面试考点(附有答案)共近400道,知识点讲解全面。不仅如此,教程还讲解了简历制作、笔试面试准备、面试技巧等内容。 </p> <p> <br /> </p>

全部评论
1.24最后“但是没构造对象,哪里来的虚函数表?”   请问虚函数表是类实例化的时候才有的吗?还是说应该是“没有构造对象,就没有虚表指针,也就无法调用虚函数”?
点赞 回复 分享
发布于 2021-07-22 08:46
1.23最后“引用也能实现多态”,那么出现两个引用变量时,这两个变量同时维护一个虚函数表吗?即哪个使用的时候就对虚函数表进行更新覆盖
点赞 回复 分享
发布于 2021-08-03 09:47
如果虚函数表中子类虚函数地址覆盖了父类的,那么父类指针重新调用父类虚函数时如何找到父类的虚函数地址呢?
点赞 回复 分享
发布于 2021-08-03 14:44
1.23 Child类中func函数的地址将覆盖掉虚函数表里对应的地址 这里面Child类的func函数只是普通的 成员函数地址也可以覆盖掉虚函数表里对应的地址吗 这里的Child类中的func函数是不是不能是普通函数啊,应该是虚函数吧,应该只有子类中与父类同名的虚函数才能覆盖父类的虚函数吧
点赞 回复 分享
发布于 2021-10-15 00:38

相关推荐

断电再接线:1. 简历排版方面,你这内容比较少,一页放完。各模块之间建议用明显的分隔线分开,现在一眼看上去非常乱。教育经历留白太多。项目经历格式不统一。 2. 第一个项目,硬件设计太笼统,硬件架构规划是指板级电路设计还是FPGA逻辑设计?FPGA时序逻辑设计具体指的什么?实现的三个低速协议以及使用协议进行控制时序,是指什么? 3. 第二个项目,我觉得你可以和第一个项目整合一下,合并为一个项目。第二个项目说实话随便买个zynq开发板都有一直petalinux的教程,作为一个独立的项目不合适的,更像是一个小作业。 4. 第三个项目,项目内容这里,其实和环境搭建之类的东西提一嘴就好了,环境准备和编译安装工具链这种东西没多大必要写,实在要写的话可以 说 使用docker 独立sdk环境之类的。你说的这个工具我没用过,我用的比较多的是busybox和buildroot,是基于menuconfig进行配置的,如果scratch也是类似的模式的话,那我觉得这个项目也经不起细推。你可以往内核裁剪那方向靠,我说的这两个工具你也可以看看。 5. 你熟悉这些接口时序的话,你可以进一步去看一下驱动开发,然后面试的时候突出这个作为重点。第三个项目也可以将驱动开发给补充进去。因为单编内核和构建文件系统,其实很多时候是体力劳动。 6. 特长这里,独立成一个荣誉奖项的模块,把你获得的奖学金和竞赛奖项放一起。数模的话,写了国赛,美赛就不用写了。 7. 总的来说可以了,你简历上写的东西你只要都熟悉,找个实习还是没问题的。 8. 嵌入式分为硬件,底层软件和应用软件,看你的经历我建议你往底层靠,多去熟悉常用的通信接口,去看内核和驱动,网络编程这块也可以去了解一下。然后去**刷刷hot100
点赞 评论 收藏
分享
评论
点赞
1
分享
牛客网
牛客企业服务