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++已经入门的学生或人士,有一定的编程基础。
本教程适合于互联网、嵌入式软件求职的学生或人士。
故事背景
蒋 豆 芽:小名豆芽,芳龄十八,蜀中人氏。卑微小硕一枚,科研领域苟延残喘,研究的是如何炒好一盘豆芽。与大多数人一样,学习道路永无止境,间歇性踌躇满志,持续性混吃等死。会点编程,对了,是面对对象的那种。不知不觉研二到找工作的时候了,同时还在忙论文,豆芽都秃了,不过豆芽也没头发啊。
隔壁老李:大名老李,蒋豆芽的好朋友,技术高手,代码女神。给了蒋豆芽不少的人生指导意见。
导 师:蒋豆芽的老板,研究的课题是每天对豆芽嘘寒问暖。
故事引入
隔壁老李:(笑容邪魅)豆芽~~
蒋 豆 芽:(惊吓)怎么了,老李,你的眼神不怀好意啊。
隔壁老李:嘿嘿,豆芽,因为内容太多,其实我们上一章还没讲完。
蒋 豆 芽:(晕)害,学海无涯苦作舟啊。
隔壁老李:哈哈,不用担心,豆芽,好好学习,肯定能找到好工作的。
1.21 继承
隔壁老李:行吧我们就继续讲了,我们在前面介绍了C++三大特性之一的“封装”。这章我们介绍三大特性之一的“继承”。
蒋 豆 芽:(疑惑)那继承又是什么呢?
隔壁老李:别急,豆芽,听我慢慢讲。
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
蒋 豆 芽:很有道理,父亲的财产当然可以由儿子来继承了,但是老婆不行,因为老婆是私人财产。
隔壁老李:(嫌弃)怎么听起来这么俗啊!但是话糙理不糙。
我们给个例子:
// 基类 class Animal { // eat() 函数 // sleep() 函数 }; //派生类 class Dog : public Animal { // bark() 函数 };
哺乳动物是动物,狗是哺乳动物,因此,狗是动物。
动物需要吃饭、睡觉,狗也需要吃饭、睡觉,因此狗可以继承动物同样的行为。而狗也有自己独特的行为,如狂吠。
蒋 豆 芽:懂了懂了。这就是C++的第二大特性——继承。
再总结一下继承类型和访问属性。
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。
我们几乎不使用 protected 或 private 继承,通常使用 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; } };
蒋 豆 芽:确实,这个就很直观了。
隔壁老李:但是多重继承会出现一种非常特别的情况,那就是菱形继承。
类 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,直接访问就不会再有歧义了。如下图。
蒋 豆 芽:太棒了!
隔壁老李:但是豆芽,使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工作都会变得更加困难,因此我们不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。
也正是由于这个原因,C++ 之后的很多面向对象的编程语言,例如 Java、C#、PHP等,都不支持多继承。
蒋 豆 芽:懂了懂了!
1.22 多态
隔壁老李:接下来我们就要讲相当重要的概念了,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关键字就解决了这个问题,也太神奇了吧?这到底是什么样的实现机制呢?
1.23 虚函数与重写
隔壁老李:C++实现虚函数的原理是虚函数表+虚表指针。
当一个类里存在虚函数时,编译器会为类创建一
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> - 本专刊适合于C/C++已经入门的学生或人士,有一定的编程基础。 - 本专刊适合于互联网C++软件开发、嵌入式软件求职的学生或人士。 - 本专刊囊括了C语言、C++、操作系统、计算机网络、嵌入式、算法与数据结构等一系列知识点的讲解,并且最后总结出了高频面试考点(附有答案)共近400道,知识点讲解全面。不仅如此,教程还讲解了简历制作、笔试面试准备、面试技巧等内容。 </p> <p> <br /> </p>