参考
- 公众号:程序员贺先生——C++八股
- (44条消息) 重载,重写与重定义的区别_重写和重定义_sunjiyuana的博客-CSDN博客
- C++ 动态联编和静态联编 - scyq - 博客园 (cnblogs.com)
- (46条消息) [面试题]虚函数和纯虚函数_@SmartSi的博客-CSDN博客
构造函数
构造函数的作⽤:初始化对象的数据成员。 类的对象被创建时,编译系统为对象分配内存空间,并⾃动调⽤构造函数,由构造函数完成成员的初始化⼯作。
-
⽆参数构造函数:即默认构造函数
自动生成:如果没有明确写出⽆参数构造函数,编译器会⾃动⽣成默认的⽆参数构造函数,函数为空,什么也不做,如果不想使⽤⾃动⽣成的⽆参构造函数,必需要⾃⼰显示写出⼀个⽆参构造函数。 -
⼀般构造函数:也称重载构造函数
⼀般构造函数可以有各种参数形式,⼀个类可以有多个⼀般构造函数,前提是参数的个数或者类型不同,创建对象时根据传⼊参数不同调⽤不同的构造函数。 -
拷⻉构造函数:
拷⻉构造函数的函数参数为对象本身的引⽤,⽤于根据⼀个已存在的对象复制出⼀个新的该类的对象,⼀般在函数中会将已存在的对象的数据成员的值⼀⼀复制到新创建的对象中。
自动生成:如果没有显示的写拷⻉构造函数,则系统会默认创建⼀个拷⻉构造函数,但当类中有指针成员时,最好不要使⽤编译器提供的默认的拷⻉构造函数,最好⾃⼰定义并且在函数中执⾏深拷⻉。(why)- 赋值运算符的重载: 注意,这个类似拷⻉构造函数,将=右边的本类对象的值复制给=左边的对象,它不属于构造函数,=左右两边的对象必需已经被创建。如果没有显示的写赋值运算符的重载,系统也会⽣成默认的赋值运算符,做⼀些基本的拷⻉⼯作。
-
类型转换构造函数:
阻止隐式转换:根据⼀个指定类型的对象创建⼀个本类的对象,也可以算是⼀般构造函数的⼀种,这不允许默认转换的话,要将其声明为 explict 的,来阻⽌⼀些隐式转换的发⽣。
重载重写重定义
-
重载: (发生在类内部,不能跨作用域) 是指同⼀可访问区内被声明的⼏个具有不同参数列表的同名函数,参数类型、个数、顺序不同可以出现重载。根据参数列表决定调⽤哪个函数,重载不关⼼函数的返回类型。
函数名相同,函数的参数个数、参数类型或参数顺序三者中必须至少有一种不同。函数返回值的类型可以相同,也可以不相同。发生在一个类内部,不能跨作用域。- const成员函数可以和非const成员函数形成重载;
- virtual关键字、返回类型对是否够成重载无任何影响。
实现原理:编译器内部用不同的参数类型来修饰不同的函数名以实现函数重载(内部改名了)
作用:提高代码可读性和可维护性,需要修改和拓展函数的功能时可以在不改变函数名称的情况下添加新的重载版本,也增加了代码的灵活性减少了命名冲突
-
重写:覆盖(不同类中,虚函数重写,发送多态) 也叫做覆盖,一般发生在子类和父类继承关系之间。子类重新定义父类中有相同名称和参数的虚函数。(override)。
- 虚函数:被重写的函数不能是static。只能是virtual。
- 完全相同:重写函数必须是相同类型、名称和参数列表。
- 访问修饰符可变:重写函数的访问修饰符是可以不同的,尽管 virtual 中是 private 的,派⽣类中重写可以改为 public。
-
重定义:隐藏(非虚函数重写)
也叫做隐藏,子类重新定义父类中有相同名称的非虚函数 ( 参数列表和返回类型可以不同 ) ,指派生类的函数屏蔽了与其同名的基类函数。可以理解成发生在继承中的重载。即⽗类中除了定义成 virtual 且完全相同的同名函数才不会被派⽣类中的同名函数所隐藏(重定义)。隐藏规则的底层原因其实是C++的名字解析过程。
在继承机制下,派生类的类域被嵌套在基类的类域中。派生类的名字解析过程如下:
1、首先在派生类类域中查找该名字。
2、如果第一步中没有成功查找到该名字,即在派生类的类域中无法对该名字进行解析,则编译器在外围基类类域对查找该名字的定义。特征:
1、如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
2、如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。与重写:
也就是说同样作为应用于在派生类中对继承自基类的某个函数进行修改,使派生类对象能够在不同的场景下做出不同的行为这么一种思路,能够满足条件的就是重写(覆盖),不能满足条件的就被称之为重定义(隐藏)
计算类大小
面向对象三大特性
C++ ⾯向对象的三⼤特征是:封装、继承、多态。
封装: 是把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让信任的类或者对象操作,对不可信的进⾏信息隐藏。⼀个类就是⼀个封装了数据以及操作这些数据的代码的逻辑实体。在⼀个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种⽅式,对象对内部数据提供了不同级别的保护,以防⽌程序中⽆关的部分意外的改变或错误的使⽤了对象的私有部分。
继承: 可以让某个类型的对象获得另⼀个类型的对象的属性的⽅法。它⽀持按级分类的概念。继承是指这样⼀种能⼒:它可以使⽤现有类的所有功能,并在⽆需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“⼦类”或者“派⽣类”,被继承的类称为“基类”、“⽗类”或“超类”。继承的过程,就是从⼀般到特殊的过程。要实现继承,可以通过“继承”和“组合”来实现。
- 概念:所谓的继承就是⼀个类继承了另⼀个类的属性和⽅法,这个新的类包含了上⼀个类的属性和⽅法,被称为⼦类或者派⽣类,被继承的类称为⽗类或者基类;
- 特点: ⼦类拥有⽗类的所有属性和⽅法,⼦类可以拥有⽗类没有的属性和⽅法,⼦类对象可以当做⽗类对象使⽤;
- 访问控制: public、protected、private 多态: 向不同的对象发送同⼀个消息,不同对象在接收时会产⽣不同的⾏为(即⽅法)。即⼀个接⼝,可以实现多种⽅法。
多态与⾮多态的实质区别就是函数地址是早绑定还是晚绑定的。
如果函数的调⽤,在编译器编译期间就可以确定函数的调⽤地址,并产⽣代码,则是静态的,即地址早绑定。
⽽如果函数调⽤的地址不能在编译器期间确定,需要在运⾏时才确定,这就属于晚绑定。
析构函数的作用,如何起作用?
构造函数只是起初始化值的作⽤,但实例化⼀个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数⾥⾯,这样就使其他的函数⾥⾯有值了。规则,只要你⼀实例化对象, 系统⾃动回调⽤⼀个构造函数,就是你不写,编译器也⾃动调⽤⼀次。
析构函数与构造函数的作⽤相反,⽤于撤销对象的⼀些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前⾯加~。
析构函数没有参数,也没有返回值,⽽且不能重载,在⼀个类中只能有⼀个析构函数。 当撤销对象时,编译器也会⾃动调⽤析构函数。 每⼀个类必须有⼀个析构函数,⽤户可以⾃定义析构函数,也可以是编译器⾃动⽣成默认的析构函数。⼀般析构函数定义为类的公有成员。
构造函数的执行顺序?析构函数的执行顺序?
构造函数执行顺序:
- 基类构造函数。如果有多个基类,则按基类被列出的顺序调用。
- 成员类对象构造函数。如果有多个成员类对象则构造函数的调⽤顺序是对象在类中被声明的顺序,⽽不是它们出现在成员初始化表中的顺序。
- 派⽣类构造函数。
析构函数执行顺序:
- 调用派生类的析构函数
- 调用成员类对象的析构函数
- 调用基类的析构函数
什么情况下会调用拷贝构造函数(三种情况)
类的对象需要拷⻉时,拷⻉构造函数将会被调⽤,以下的情况都会调⽤拷⻉构造函数:
- ⼀个对象以值传递的⽅式传⼊函数体,需要拷⻉构造函数创建⼀个临时对象压⼊到栈空间中
- ⼀个对象以值传递的⽅式从函数返回,需要执⾏拷⻉构造函数创建⼀个临时对象作为返回值。
- ⼀个对象需要通过另外⼀个对象进行初始化。
为什么拷贝构造函数必须是引用传递,不能是值传递?
为了防⽌递归调⽤。当⼀个对象需要以值⽅式进⾏传递时,编译器会⽣成代码调⽤它的拷⻉构造函数⽣成⼀个副本,如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造函数,这就是⼀个⽆限递归。
怎么去访问类的私有成员变量,假定为int类型
- 公共接口
- 取实例首地址 // 定义类 class A { public: A(int aa) {a = aa;} private: int a; }; // main中cls的首地址等于p // 若类中无虚函数表:cls的首地址处为变量a // 若类中有虚函数表:cls的首地址处为虚函数表,虚函数表后面才是变量 // 存储顺序为:虚函数表-->公共变量-->私有变量 int main() { A cls(2); int* p = reinterpret_cast<int*>(&cls); cout << *p << endl; return 0; }
- 友元
- 全局函数作友元访问A中的私有变量
- 类做友元访问提供公共接口访问A中的私有变量
- 类的成员函数做友元访问A中的私有变量
private、protected、public
public:类外可访问
private:类外和子类都不可访问
protected:类外不可访问,但派生类可以访问
发生继承时,基类成员在派生类中权限变化如下:
public继承:在子类中权限不变
private继承:全变为private
protected继承:private不变,其他全变为protected
C++ 里的 static 关键字了解吗,有哪些作用?
-
全局静态变量
在全局变量前加上关键字 static,全局变量就定义成一个全局静态变量静态存储区,在整个程序运行期间一直初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化) 作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。 -
局部静态变量
- 在局部变量之前加上关键字 static,局部变量就成为一个局部静态变量。
- 内存中的位置:静态存储区初始化:末经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化)
- 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
-
静态函数
- 在函数返回类型前加 static,函数就定义为静态函数。函数的定义和声明在默认情况下都是 extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
- 函数的实现使用 static修饰,那么这个函数只可在本pp内使用,不会同其他Cpp中的同名函数引起冲突Warning:不要再头文件中声明 astatic的全局函数,不要在Cpp内声明非 Estatic的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则pp内部声明需加上 - static修饰
-
类的静态成员
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用。 -
类的静态函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>∷<静态成员函数名>(<参数表>)
为什么静态成员函数无法访问非静态成员,为什么静态成员不可以被类的对象调用,不能访问类的非静态成员变量:
-
产生顺序:静态static成员函数不同于非静态函数,它只属于类本身,而不属于每一个对象实例。静态函数随着类的加载而独立存在。与之相反的是非静态成员,他们当且仅当实例化对象之后才存在。
静态成员函数产生在前,非静态成员函数产生在后,不可能让静态函数去访问一个不存在的东西。 -
this指针:在访问非静态变量的时候,用的是this指针;而static静态函数没有this指针,所以静态函数也确实没有办法访问非静态成员。
静态数据成员是与类本身相关联的,而不是与类的每个实例(对象)相关联的。因此,它们不依赖于类的任何特定实例,而是属于整个类。
静态方法在没有创建对象时就已经存在,所有静态方法直接可以互相调用,非静态方法和变量是属于对象的,不能访问。
静态成员函数属于类本身,而不作用于对象,因此它不具有this指针。 正因为它没有指向某一个对象,所以它无法访问属于类对象的非静态成员变量和非静态成员函数,它只能调用其余的静态成员函数和静态成员变量。
静态数据成员通常用关键字 static 声明,它们在类的内部定义,但它们不属于类的对象的一部分。相反,它们属于类本身,可以通过类名来访问,而不需要创建类的实例。
继承
菱形继承
是什么:
一个类同时继承两个类,而这两个类又共同继承自一个基类
class Base{
public:
int data;
};
class DerivedA :public Base{
public:
// 其他成员
}
class DerivedB :public Base{
public:
// 其他成员
}
class DerivedC :public DerivedA, public DerivedB{
public:
// 其他成员
}
问题:
- 二义性问题:DerivedC访问data时,编译器无法确定应该使用哪个路径上的Base类
- 资源浪费:存在两份Base子对象导致资源浪费
解决:虚继承
为了菱形继承带来的问题,可以使用虚继承(virtual inheritance)。通过在 DerivedA 和 DerivedB 继承 Base 时使用 virtual 关键字,可以确保在 DerivedC 中只包含一份 Base 类的子对象。
虚继承原理:
通过虚基类指针和虚基类表,可以确保再Derived对象中只包含一份Base类的子对象。
具体实现流程:当一个类使用虚继承时,编译器会在该类的对象中插入一个指向虚基类的指针(虚基指针),在访问虚基类的成员时,编译器会使用虚基类指针和虚基类表进行正确的定位。当创建DerivedC 对象时,编译器会在 DerivedC 的对象中插入一个指向 Base 类的虚基指针。同时,DerivedC 的虚基类表中记录了 Base 类的偏移量。
class Base{
public:
int data;
};
class DerivedA :public virtual Base{
public:
// 其他成员
}
class DerivedB :public virtual Base{
public:
// 其他成员
}
class DerivedC :public DerivedA, public DerivedB{
public:
// 其他成员
}
多态
多态实现
虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。 多态其实⼀般就是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器为函数⽣成符号表时的不同规则,᯿载只是⼀种语⾔特性,与多态⽆关,与⾯向对象也⽆关,但这⼜是 C++中增加的新规则,所以也算属于 C++,所以如果⾮要说᯿载算是多态的⼀种,那就可以说:多态可以分为静态多态和动态多态。
静态多态(编译时多态)其实就是重载,因为静态多态是指在编译时期就决定了调⽤哪个函数,根据参数列表来决定;
动态多态是指通过⼦类重写⽗类的虚函数来实现的,因为是在运⾏期间决定调⽤的函数,所以称为动态多态,⼀般情况下我们不区分这两个时所说的多态就是指动态多态。动态多态(运行时多态)的实现与虚函数表,虚函数指针相关。
静态多态和动态多态区别:早绑定(静态联编)还是晚绑定(动态联编)。
联编:同一个名称的函数有多种,联编就是把调用和具体的实现进行链接映射的操作。
静态联编:
静态联编工作是在程序编译连接阶段进行的,这种联编又称为早期联编,因为这种联编实在程序开始运行之前完成的。在程序编译阶段进行的这种联编在编译时就解决了程序的操作调用与执行该操作代码间的关系。但是重载、重写、虚函数使得这项工作变得困难。因为编译器不知道用户将选择哪种类型的对象,执行具体哪一块代码。所以,编译器必须生成能够在程序运行时选择正确的虚函数的代码,这个过程被称为动态联编,又称晚期联编。**
动态联编: 编译程序在编译阶段并不能确切地指导将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切地指导将要调用的函数,要求联编工作在程序运行时进行,这种在程序运行时进行的联编工作被称为动态联编,或称动态束定,又叫晚期联编。
C++不允许将一种类型的地址赋给另一种类型的指针。也不允许一种类型的引用指向另一种类型。 指向基类的引用或指针可以引用派生类对象,而不需要进行显式转换。
将派生类引用或指针转换为基类引用或指针称为向上强制转换,这使得公有继承不需要进行显式类型转换就可以通过基类指针或引用来引用派生类对象。
相反,将基类指针或引用转换为派生类指针或引用,称为向下强制转换。如果不使用显式类型转换,则向下强制转换是不允许的。 隐式向上强制转换的存在,使得基类指针和引用可以指向派生类对象,因此需要动态联编。即程序运行时,我才知道究竟要执行哪一个。C++通过虚函数来满足这样的需求。
编译器对非虚方法使用静态联编。,编译器对虚方法使用动态联编。
扩展:⼦类是否要重写⽗类的虚函数?⼦类继承⽗类时, ⽗类的纯虚函数必须重写,否则⼦类也是⼀个虚类不可实例化。 定义纯虚函数是为了实现⼀个接⼝,起到⼀个规范的作⽤,规范继承这个类的程序员必须实现这个函数。
虚函数表: ——动态多态的底层原理
C++中多态的表象,在基类的函数前加上 virtual 关键字,在派⽣类中重写该函数,运⾏时将会根据对象的实际类型来调⽤相应的函数。如果对象类型是派⽣类,就调⽤派⽣类的函数,如果是基类,就调⽤基类的函数。
实际上,当⼀个类中包含虚函数时,编译器会为该类⽣成⼀个虚函数表,保存该类中虚函数的地址,同样,派⽣类继承基类,派⽣类中⾃然⼀定有虚函数,所以编译器也会为派⽣类⽣成⾃⼰的虚函数表。当我们定义⼀个派⽣类对象时,编译器检测该类型有虚函数,所以为这个派⽣类对象⽣成⼀个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。
后续如果有⼀个基类类型的指针,指向派⽣类,那么当调⽤虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调⽤派⽣类的虚函数表中的虚函数以此实现多态。
补充:如果基类中没有定义成 virtual,那么进⾏ Base B; Derived D; Base *p = D; p->function(); 这种情况下调⽤的则是 Base 中的 function()。因为基类和派⽣类中都没有虚函数的定义,那么编译器就会认为不⽤留给动态多态的机会,就事先进⾏函数地址的绑定(早绑定),详述过程就是,定义了⼀个派⽣类对象,⾸先要构造基类的空间,然后构造派⽣类的⾃身内容,形成⼀个派⽣类对象,那么在进⾏类型转换时,直接截取基类的部分的内存,编译器认为类型就是基类,那么(函数符号表[不同于虚函数表的另⼀个表]中)绑定的函数地址也就是基类中函数的地址,所以执⾏的是基类的函数。
编译器处理虚函数表应该如何处理:
对于派⽣类来说,编译器建⽴虚函数表的过程其实⼀共是三个步骤:
- 拷⻉基类的虚函数表,如果是多继承,就拷⻉每个有虚函数基类的虚函数表
- 当然还有⼀个基类的虚函数表和派⽣类⾃身的虚函数表共⽤了⼀个虚函数表,也称为某个基类为派⽣类的主基类
- 查看派⽣类中是否有重写基类中的虚函数,如果有,就替换成已经重写的虚函数地址;查看派⽣类是否有⾃身的虚函数,如果有,就追加⾃身的虚函数到⾃身的虚函数表中
虚函数、纯虚函数:
-
虚函数
虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。非虚函数总是在编译时根据调用该函数的对象,引用或指针的类型而确定。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定或指针所指向的对象所属类型定义的版本。
虚函数必须是基类的非静态成员函数。虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。
一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。 函数只能借助于指针或者引用来达到多态的效果。
-
纯虚函数:纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” virtualvoid GetName() =0。定义纯虚函数就是为了让基类不可实例化,定义纯虚函数就是为了让基类不可实例化。
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决上述问题,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的函数绝不会调用。
声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。必须在继承类中重新声明函数(不要后面的=0)否则该派生类也不能实例化,而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
-
区别:
- 抽象类:虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。
- 函数定义:虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。
- 都可多态:虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用
- 定义不可static:被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。
动态多态的损耗,主要空间损耗和寻址时间损耗
C++有哪几类构造函数
空类中有哪些:
Empty(); // 缺省构造函数
Empty( const Empty& ); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const;// 取址运算符 const
抽象类:
-
抽象类: 称带有纯虚函数的类为抽象类。
抽象类的作用:抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。
使用抽象类时注意:
- 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。
- 如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。
- 如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。抽象类是不能定义对象的。
析构函数一般写成虚函数的原因:
直观的讲:是为了降低内存泄漏的可能性。举例来说就是,⼀个基类的指针指向⼀个派⽣类的对象,在使⽤完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那 么编译器根据指针类型就会认为当前对象的类型是基类,调⽤基类的析构函数 (该对象的析构函数的函数地址早就被绑定为基类的析构函数),仅执⾏基类的析构,派⽣类的⾃身内容将⽆法被析构,造成内存泄漏。如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执⾏派⽣类的析构函数,再执⾏基类的析构函数,成功释放内存。
构造函数为什么⼀般不定义为虚函数:
- 虚函数调⽤只需要知道“部分的”信息,即只需要知道函数接⼝,⽽不需要知道对象的具体类型。但是,我们要创建⼀个对象的话,是需要知道对象的完整信息的。特别是,需要知道要创建对象的确切类型,因此,构造函数不应该被定义成虚函数;
- ⽽且从⽬前编译器实现虚函数进⾏多态的⽅式来看,虚函数的调⽤是通过实例化之后对象的虚函数表指针来找到虚函数的地址进⾏调⽤的,如果说构造函数是虚的,那么虚函数表指针则是不存在的,⽆法找到对应的虚函数表来调⽤虚函数,那么这个调⽤实际上也是违反了先实例化后调⽤的准则。
构造函数或析构函数中调⽤虚函数会怎样:
举例来说就是,有⼀个动物的基类,基类中定义了⼀个动物本身⾏为的虚函数 action_type(),在基类的构造函数中调⽤了这个虚函数。语法上没问题,所以构造函数跟虚构函数里面都是可以调用虚函数的,并且编译器不会报错。但是在基类中声明纯虚函数并且在基类的析构函数中调用,编译器会报错。
派⽣类中᯿写了这个虚函数,我们期望着根据对象的真实类型不同,⽽调⽤各⾃实现的虚函数,但实际上当我们创建⼀个派⽣类对象时,⾸先会创建派⽣类的基类部分,执⾏基类的构造函数,此时,派⽣类的⾃身部分还没有被初始化,对于这种还没有初始化的东⻄,C++选择当它们还不存在作为⼀种安全的⽅法。
由于类的构造顺序先构造基类然后再派生类,所以在构造函数中调用虚函数(绑定的是基类的虚函数),虚函数是不会呈现出多态的。 也就是说构造派⽣类的基类部分是,编译器会认为这就是⼀个基类类型的对象,然后调⽤基类类型中的虚函数实现,并没有按照我们想要的⽅式进⾏。即对象在派⽣类构造函数执⾏前并不 会成为⼀个派⽣类对象。
在析构函数中也是同理,派⽣类执⾏了析构函数后,派⽣类的⾃身成员呈现未定义的状态,那么在执⾏基类的析构函数中是不可能调⽤到派⽣类᯿写的⽅法的。所以说,我们不应该在构在函数或析构函数中调⽤虚函数,就算调⽤⼀般也不会达到我们想要的结果。
类的析构顺序是先析构派生类然后再析构基类,所以当调用继承层次中某一层次的类的析构函数时,这代表其派生类已经进行了析构,所以也并不会呈现多态。 构造/析构函数中对this的虚函数调用基本上可以理解为静态绑定。而对并非指向当前对象的指针/引用仍然使用动态绑定。gcc对此的实现是:对于构造函数,先调用基类构造函数,然后将this的虚表指针指向本类虚表,再执行初始化列表的成员部分以及构造函数体;对于析构函数,将虚表指针指向本类虚表,然后执行析构函数体。
如果在基类构造函数中调用虚函数被解析成调用派生类的虚函数,而派生类的虚函数中又访问到未初始化的派生类数据,这是危险的,将会导致程序出现未知行为及bug。
纯虚函数(应用于接口继承和实现继承):
实际上,纯虚函数的出现就是为了让继承可以出现多种情况:
- 有时我们希望派⽣类只继承成员函数的接⼝
- 有时我们⼜希望派⽣类既继承成员函数的接⼝,⼜继承成员函数的实现,⽽且可以在派⽣类中可以重写成员函数以实现多态
- 有的时候我们⼜希望派⽣类在继承成员函数接⼝和实现的情况下,不能重写缺省的实现。
其实,声明⼀个纯虚函数的⽬的就是为了让派⽣类只继承函数的接⼝,⽽且派⽣类中必需提供⼀个这个纯虚函数的实现,否则含有纯虚函数的类将是抽象类,不能进⾏实例化。
对于纯虚函数来说,我们其实是可以给它提供实现代码的,但是由于抽象类不能实例化,调⽤这个实现的唯⼀⽅式是在派⽣类对象中指出其 class 名称来调⽤。
虚析构和纯虚析构的区别
首先虚析构和纯虚析构都是为了解决多态中子类有堆区数据,父类释放时无法释放子类的堆区数据而导致内存泄露的问题。
-
语法不同
虚析构语法: virtual~ 类名(){}; 类内声明和实现
纯虚析构语法: virtual~类名() = 0; 类内声明类外实现
———————实现方式 作用域::~类名(){}; -
抽象类:纯虚析构函数写了之后,这个类也属于抽象类,而无法实例化对象。
静态绑定和动态绑定的介绍
说起静态绑定和动态绑定,我们首先要知道静态类型和动态类型,静态类型就是它在程序中被声明时所采用的类型,在编译期间确定。动态类型则是指“⽬前所指对象的实际类型”,在运⾏期间确定。
静态绑定,⼜名早绑定,绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发⽣在编译期间。
动态绑定,⼜名晚绑定,绑定的是动态类型,所对应的函数或属性依赖于动态类型,发⽣在运⾏期间。
比如说,virtual 函数是动态绑定的,⾮虚函数是静态绑定的,缺省参数值也是静态绑定的。这⾥呢,就需要注意,我们不应该᯿新定义继承⽽来的缺省参数,因为即使我们重定义了,也不会起到效果。因为⼀个基类的指针指向⼀个派⽣类对象,在派⽣类的对象中针对虚函数的参数缺省值进行了重定义, 但是缺省参数值是静态绑定的,静态绑定绑定的是静态类型相关的内 容,所以会出现⼀种派⽣类的虚函数实现⽅式结合了基类的缺省参数值的调⽤效果,这个与所期望的效果不同。
C++空类中有哪些成员函数
class Empty{ public: Empty(); // 缺省构造函数 Empty( const Empty& ); // 拷贝构造函数 ~Empty(); // 析构函数 Empty& operator=( const Empty& ); // 赋值运算符 Empty* operator&(); // 取址运算符 const Empty* operator&() const; // 取址运算符 const };