C/C++
面向对象
面向对象和面向过程有什么区别?
面向对象与面向过程有以下四个方面的不同:
1) 出发点不同
面向对象使用符合常规思维的方式来处理客观世界的问题,强调把解决问题领域的“动作”直接映射到对象之间的接口上。而面向过程则强调的是过程的抽象化与模块化,是以过程为中心构造或处理客观世界问题。
2) 层次逻辑关系不同
面向对象使用计算机逻辑来模拟客观世界中的物理存在,以对象的集合类作为处理问题的单位,尽可能地使计算机世界向客观世界靠拢,以使处理问题的方式更清晰直接,面向对象使用类的层次结构来体现类之间的继承与发展。面向过程处理问题的基本单位是能清晰准确地表达过程的模块,用模块的层次结构概括模块或模块间的关系与功能,把客观世界的问题抽象成计算机可以处理的过程。
3) 数据处理方式与控制程序方式不同
面向对象将数据与对应的代码封装成一个整体,原则上其他对象不能直接修改其数据,即对象的修改只能由自身的成员函数完成,控制程序方式上是通过“事件驱动”来激活和运行程序的。而面向过程是直接通过程序来处理数据,处理完毕后即可显示处理的结果,在控制方式上是按照设计调用或返回程序,不能自由导航,各模块之间存在着控制与被控制,调动与被调用的关系。
4) 分析设计与编码转换方式不同
面向对象贯穿于软件生命周期的分析、设计及编码中,是一种平滑的过程,从分析到设计再到编码是采用一致性的模型表示,实现的是一种无缝连接。而面向过程强调分析、设计及编码之间按规则进行转换贯穿于软件生命周期的分析、设计及编码中,实现的是一种有缝的连接。
面向对象的基本特征有哪些?
面向对象的编程方法有四个基本特性:
1) 抽象:就是忽略一个主题中与当前目标无关的方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。
过程抽象是指任何一个明确定义功能的操作都可被使用者看作单个的实体看待,尽管这个操作实际上可能由一系列更低级的操作来完成。数据抽象定义了数据类型和施加于该类型对象上的操作,并限定了对象的值,只能通过使用这些操作修改和观察。
2) 继承:这是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。
派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。这也体现了大自然中一般与特殊的关系。继承性很好地解决了软件的可重用性问题。
3) 封装:就是把过程和数据包围起来,对数据的访问只能通过已定义的接口。面向对象的计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。一旦定义了一个对象的特性,则有必要决定这些特性的可见性,即哪些特性对外部世界是可见的,哪些特性用于表示内部状态。
在这个阶段定义对象的接口。通常,应禁止直接访问一个对象的实际表示,而应通过操作接口访问对象,这称为信息隐藏。封装保证了模块具有较好的独立性,使得程序维护修改较为容易。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低限度。
4) 多态:是指允许不同类的对象对同一消息做出响应。比如同样的复制-粘贴操作,在字处理程序和绘图程序中有不同的效果。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好地解决了应用程序函数同名问题。
什么是深拷贝?什么是浅拷贝?
深拷贝是彻底的拷贝,两对象中所有的成员都是独立的一份,而且,成员对象中的成员对象也是独立一份。
浅拷贝中的某些成员变量可能是共享的,深拷贝如果不够彻底,就是浅拷贝。
什么是友元?
有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行。这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦。
C++是从结构化的C语言发展而来的,需要照顾结构化设计程序员的习惯,所以在对私有成员可访问范围的问题上不可限制太死。
C++ 设计者认为, 如果有的程序员真的非常怕麻烦,就是想在类的成员函数外部直接访问对象的私有成员,那还是做一点妥协以满足他们的愿望为好,这也算是眼前利益和长远利益的折中。因此,C++ 就有了友元(friend)的概念。打个比方,这相当于是说:朋友是值得信任的,所以可以对他们公开一些自己的隐私。
友元提供了一种 普通函数或者类成员函数 访问另一个类中的私有或保护成员 的机制。也就是说有两种形式的友元:
(1)友元函数:普通函数对一个访问某个类中的私有或保护成员。
(2)友元类:类A中的成员函数访问类B中的私有或保护成员。
基类的构造函数/析构函数是否能被派生类继承?
基类的构造函数析构函数不能被派生类继承。
基类的构造函数不能被派生类继承,派生类中需要声明自己的构造函数。设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员初始化,也要考虑基类的数据成员的初始化。声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化,需要调用基类构造函数完成。
基类的析构函数也不能被派生类继承,派生类需要自行声明析构函数。声明方法与一般(无继承关系时)类的析构函数相同,不需要显式地调用基类的析构函数,系统会自动隐式调用。需要注意的是,析构函数的调用次序与构造函数相反。
初始化列表和构造函数初始化的区别?
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:
Example::Example() : ival(0), dval(0.0) {} //ival 和dval是类的两个数据成员
上面的例子和下面不用初始化列表的构造函数看似没什么区别:
Example::Example() { ival = 0; dval = 0.0; }
的确,这两个构造函数的结果是一样的。但区别在于:上面的构造函数(使用初始化列表的构造函数)显示的初始化类的成员;而没使用初始化列表的构造函数是对类的成员赋值,并没有进行显示的初始化。
初始化和赋值对内置类型的成员没有什么大的区别,像上面的任一个构造函数都可以。但有的时候必须用带有初始化列表的构造函数:
成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
const成员或引用类型的员。因为const对象或引用类型只能初始化,不能对他们赋值。
C++中有那些情况只能用初始化列表,而不能用赋值?
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面都跟一个放在括号中的初始化式。例如, Example:Example ival(o,dva(0.0){},其中ival与dva是类的两个数据成员。
在C++语言中,赋值与初始化列表的原理不一样,赋值是删除原值,赋予新值,初始化列表开辟空间和初始化是同时完成的,直接给予一个值
所以,在C++中,赋值与初始化列表的使用情况也不一样,只能用初始化列表,而不能用赋值的情况一般有以下3种:
- 当类中含有 const(常量)、 reference(引用)成员变量时,只能初始化,不能对它们进行赋值。常量不能被赋值,只能被初始化,所以必须在初始化列表中完成,C++的引用也一定要初始化,所以必须在初始化列表中完成。
- 派生类在构造函数中要对自身成员初始化,也要对继承过来的基类成员进行初始化当基类没有默认构造函数的时候,通过在派生类的构造函数初始化列表中调用基类的构造函数实现。
- 如果成员类型是没有默认构造函数的类,也只能使用初始化列表。若没有提供显式初始化时,则编译器隐式使用成员类型的默认构造函数,此时编译器尝试使用默认构造函数将会失败
类的成员变量的初始化顺序是什么?
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。这点在EffectiveC++中有详细介绍。
如果不使用初始化列表初始化,在构造函数内初始化时,此时与成员变量在构造函数中的位置有关。
注意:类成员在定义时,是不能初始化的
注意:类中const成员常量必须在构造函数初始化列表中初始化。
注意:类中static成员变量,必须在类外初始化。
静态变量进行初始化顺序是基类的静态变量先初始化,然后是它的派生类。直到所有的静态变量都被初始化。这里需要注意全局变量和静态变量的初始化是不分次序的。这也不难理解,其实静态变量和全局变量都被放在公共内存区。可以把静态变量理解为带有“作用域”的全局变量。在一切初始化工作结束后,main函数会被调用,如果某个类的构造函数被执行,那么首先基类的成员变量会被初始化。
当一个类为另一个类的成员变量时,如何对其进行初始化?
示例程序如下:
class ABC { public: ABC(int x, int y, int z); private : int a; int b; int c; }; class MyClass { public: MyClass():abc(1,2,3) { } private: ABC abc; };
上例中,因为ABC有了显式的带参数的构造函数,那么它是无法依靠编译器生成无参构造函数的,所以必须使用初始化列表:abc(1,2,3),才能构造ABC的对象。
C++能设计实现一个不能被继承的类吗?
在Java 中定义了关键字final ,被final 修饰的类不能被继承。但在C++ 中没有final 这个关键字,要实现这个要求还是需要花费一些精力。
首先想到的是在C++ 中,子类的构造函数会自动调用父类的构造函数。同样,子类的析构函数也会自动调用父类的析构函数。要想一个类不能被继承,我们只要把它的构造函数和析构函 数都定义为私有函数。那么当一个类试图从它那继承的时候,必然会由于试图调用构造函数、析构函数而导致编译错误。
可是这个类的构造函数和析构函数都是私有函数了,我们怎样才能得到该类的实例呢?这难不倒我们,我们可以通过定义静态来创建和释放类的实例。
基于这个思路,我们可以写出如下的代码:
/// // Define a class which can't be derived from /// class FinalClass1 { public : static FinalClass1* GetInstance() { return new FinalClass1; } static void DeleteInstance( FinalClass1* pInstance) { delete pInstance; pInstance = 0; } private : FinalClass1() {} ~FinalClass1() {} };
这个类是不能被继承,但在总觉得它和一般的类有些不一样,使用起来也有点不方便。比如,我们只能得到位于堆上的实例,而得不到位于栈上实例。能不能实现一个和一般类除了不能被继承之外其他用法都一样的类呢?办法总是有的,不过需要一些技巧。请看如下代码:
/// // Define a class which can't be derived from /// template <typename T> class MakeFinal { friend T; private : MakeFinal() {} ~MakeFinal() {} }; class FinalClass2 : virtual public MakeFinal<FinalClass2> { public : FinalClass2() {} ~FinalClass2() {} };
这个类使用起来和一般的类没有区别,可以在栈上、也可以在堆上创建实例。尽管类 MakeFinal <FinalClass2>
的构造函数和析构函数都是私有的,但由于类 FinalClass2 是它的友元函数,因此在 FinalClass2 中调用 MakeFinal <FinalClass2>
的构造函数和析构函数都不会造成编译错误。但当我们试图从 FinalClass2 继承一个类并创建它的实例时,却不同通过编译。
class Try : public FinalClass2 { public : Try() {} ~Try() {} }; Try temp;
由于类 FinalClass2 是从类 MakeFinal <FinalClass2>
虚继承过来的,在调用 Try 的构造函数的时候,会直接跳过 FinalClass2 而直接调用 MakeFinal <FinalClass2>
的构造函数。非常遗憾的是Try 不是 MakeFinal <FinalClass2>
的友元,因此不能调用其私有的构造函数。
基于上面的分析,试图从 FinalClass2 继承的类,一旦实例化,都会导致编译错误,因此是 FinalClass2 不能被继承。这就满足了我们设计要求。
构造函数没有返回值,那么如何得知对象是否构造成功?
这里的“构造”不单指分配对象本身的内存,而是指在建立对象时做的初始化操作(如打开文件、连接数据库等)。
因为构造函数没有返回值,所以通知对象的构造失败的唯一方法就是在构造函数中抛出异常。构造函数中抛出异常将导致对象的析构函数不被执行,当对象发生部分构造时,已经构造完毕的子对象将会逆序地被析构。
Public继承、protected继承、private继承的区别?
public(公有)继承、 protected(保护)继承和 private(私有)继承是常见的3种继承方式。
- 公有继承
对于子类的对象而言,采用公有继承时,基类成员对子类对象的可见性与一般类成员对对象的可见性相同,公有成员可见,其他成员不可见。
对于子类而言,基类的公有成员和保护成员可见;基类的公有成员和保护成员作为派生类的成员时,它们都维持原有的可见性(基类 public成员在子类中还是public,基类 protected成员在子类中还是 protected);基类的私有成员不可见,基类的私有成员依然是私有的,子类不可访问。
- 保护继承
保护继承的特点是:基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问。基类的私有成员仍然是私有的。由此可以看出,基类的所有成员对子类的对象都是不可见的。
- 私有继承
私有继承的特点是,基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
C++提供默认参数的函数吗?
C++可以给函数定义默认参数值。在函数调用时没有指定与形参相对应的实参时,就自动使用默认参数。
默认参数的语法与使用:
(1) 在函数声明或定义时,直接对参数赋值,这就是默认参数。
(2) 在函数调用时,省略部分或全部参数。这时可以用默认参数来代替。
通常调用函数时,要为函数的每个参数给定对应的实参。例如:
void delay(int loops=1000);//函数声明 void delay(int loops) //函数定义 { if(loops==0) { return; } for(int i=0;i<loops;i++) ; }
在上例中,如果将delay()函数中的loops定义成默认值1000,这样,以后无论何时调用delay()函数,都不用给loops赋值,程序都会自动将它当做值 1000进行处理。例如,当执行delay(2500)调用时,loops的参数值为显性化的,被设置为 2500;当执行delay()时,loops将采用默认值1000。
默认参数在函数声明中提供,当有声明又有定义时,定义中不允许默认参数。如果函数只有定义,则默认参数才可出现在函数定义中。例如:
oid point(int=3,int=4);//声明中给出默认值 void point(int x,int y) //定义中不允许再给出默认值 { cout<<x<<endl; cout<<y<<endl; }
如果一组重载函数(可能带有默认参数)都允许相同实参个数的调用,将会引起调用的二义性。例如:
void func(int);//重载函数之一 void func(int,int=4);//重载函数之二,带有默认参数 void func(int=3,int=4);//重载函数三,带有默认参数 func(7);//错误:到底调用3个重载函数中的哪个? func(20,30);//错误:到底调用后面两个重载函数的哪个?
虚函数
什么是虚函数?
指向基类的指针在操作它的多态类对象时,可以根据指向的不同类对象调用其相应的函数,这个函数就是虚函数。
虚函数的作用:在基类定义了虚函数后,可以在派生类中对虚函数进行重新定义,并且可以通过基类指针或引用,在程序的运行阶段动态地选择调用基类和不同派生类中的同名函数。(如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。)
下面是一个虚函数的实例程序:
#include "stdafx.h" #include<iostream> using namespace std; class Base { public: virtual void Print()//父类虚函数 { printf("This is Class Base!\n"); } }; class Derived1 :public Base { public: void Print()//子类1虚函数 { printf("This is Class Derived1!\n"); } }; class Derived2 :public Base { public: void Print()//子类2虚函数 { printf("This is Class Derived2!\n"); } }; int main() { Base Cbase; Derived1 Cderived1; Derived2 Cderived2; Cbase.Print(); Cderived1.Print(); Cderived2.Print(); cout << "---------------" << endl; Base *p1 = &Cbase; Base *p2 = &Cderived1; Base *p3 = &Cderived2; p1->Print(); p2->Print(); p3->Print(); } /* 输出结果: This is Class Base! This is Class Derived1! This is Class Derived2! --------------- This is Class Base! This is Class Derived1! This is Class Derived2! */
需要注意的是,虚函数虽然非常好用,但是在使用虚函数时,并非所有的函数都需要定义成虚函数,因为实现虚函数是有代价的。在使用虚函数时,需要注意以下几个方面的内容:
(1) 只需要在声明函数的类体中使用关键字virtual将函数声明为虚函数,而定义函数时不需要使用关键字virtual。
(2) 当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。
(3) 非类的成员函数不能定义为虚函数,全局函数以及类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。
(4) 基类的析构函数应该定义为虚函数,否则会造成内存泄漏。基类析构函数未声明virtual,基类指针指向派生类时,delete指针不调用派生类析构函数。有 virtual,则先调用派生类析构再调用基类析构。
C++如何实现多态?
C++中通过虚函数实现多态。虚函数的本质就是通过基类指针访问派生类定义的函数。每个含有虚函数的类,其实例对象内部都有一个虚函数表指针。该虚函数表指针被初始化为本类的虚函数表的内存地址。所以,在程序中,不管对象类型如何转换,该对象内部的虚函数表指针都是固定的,这样才能实现动态地对对象函数进行调用,这就是C++多态性的原理。
纯虚函数指的是什么?
纯虚函数是一种特殊的虚函数,格式一般如下
class <类名> { virtual()函数返回值类型 虚函数名(形参表)=0; ... }; class <类名>
由于在很多情况下,基类中不能对虚函数给出有意义的实现,只能把函数的实现留给派生类。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但是动物本身生成对象不合情理,此时就可以将动物类中的函数定义为纯虚函数,如果基类中有纯虚函数,那么在子类中必须实现这个纯虚函数,否则子类将无法被实例化,也无法实现多态。
含有纯虚函数的类称为抽象类,抽象类不能生成对象。纯虚函数永远不会被调用,它们主要用来统一管理子类对象。
什么函数不能声明为虚函数?
常见的不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。
1.为什么C++不支持普通函数为虚函数?
普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。
2.为什么C++不支持构造函数为虚函数?
这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)
3.为什么C++不支持内联成员函数为虚函数?
其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)
4.为什么C++不支持静态成员函数为虚函数?
这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态邦定的必要性。
5.为什么C++不支持友元函数为虚函数?
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
C++中如何阻止一个类被实例化?
C++中可以通过使用抽象类,或者将构造函数声明为private阻止一个类被实例化。抽象类之所以不能被实例化,是因为抽象类不能代表一类具体的事物,它是对多种具有相似性的具体事物的共同特征的一种抽象。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但是动物本身生成对象不合情理。
联系作者
关于作者
作者在准备秋招的过程中,凭借这份资料,最后顺利拿到了oppo,小米,兆易创新,全志科技,海康威视等十余家家公司的offer。现将这部分资料分享出来,希望能对大家有帮助!
关于嵌入式软件工程师笔试面试指南
嵌入式软件工程师笔试面试指南,详细分成了简历书写,面试技巧,面经总结,笔试面试八股文总结等四个部分。
其中,八股文又分成了C/C++,数据结构与算法分析,Arm体系与架构,Linux驱动开发,操作系统,网络编程,名企笔试真题等七个部分。
无论是做嵌入式应用层还是嵌入式底层,一定会对你有帮助的(没有帮助来找我领红包)。这些内容均会同步更新到github仓库中(内含PDF版本的获取方式)。
因个人能力有限,可能会有一些错误。大家发现错误,可以在github提交issues,勘误我也会整理出来,第一时间通知大家。
这些内容都是我熬夜整理的,创作不易,大家不要忘了点击「赞」支持下,也算没有白白熬夜,对得起我掉的一根根头发。
github:https://github.com/ZhongYi-LinuxDriverDev/EmbeddedSoftwareEngineerInterview