1.封装和继承
1)单继承
class 派生类名:访问方式 基类名{
派生类中的新成员
};
例子:
#include <iostream>
using namespace std;
class A{
public :
int a;
A(){
a = 5;
}
};
class B:public A{
public:
int b;
B(){
b = 20;
}
void hello(){
cout << a << '\n' << b << endl;
}
};
int main(){
B().hello();
}
2)多继承
class 派生类名:访问方式 基类名1,访问方式 基类名2……{
派生类中的新成员
};
例子:
#include <iostream>
using namespace std;
class A{
public :
int a;
A(){
a = 5;
}
};
class B{
public:
int b;
B(){
b = 20;
}
};
class C:A,B{
public:
void hello(){
cout << a << '\n' << b << endl;
}
};
int main(){
C().hello();
}
不管在单继承还是在多重继承的定义格式中,访问方式,即继承方式,可以为public、private或protected,如果省略,则默认为private方式。
A. 访问方式为public方式时,这种继承称为公有继承;
当类的继承方式为公有继承时,基类中public和protected成员的访问属性在派生类中不变,而基类private成员不可访问。也就是说,基类的public和protected成员在公有继承方式下分别继承为派生类的public和protected成员,派生类中的其他成员可以直接访问它们,在派生类的外部只能通过派生类的对象访问从基类继承来的public成员。而无论是派生类的成员还是派生类的对象都无法访问从基类继承来的private成员。
B. 访问方式为private方式时,这种继承称为私有继承;
当类的继承方式为私有继承时,基类中的public和protected成员都以private成员出现在派生类中,而基类private成员不可访问。也就是说,基类的public和protected成员在私有继承方式下被继承为派生类的private成员,派生类中的其他成员可以直接访问它们,但在派生类的外部无法通过派生类的对象访问它们。而无论是派生类的成员还是派生类的对象都无法访问从基类继承来的private成员。
可以看出,经过私有继承后,所有基类的成员都成为派生类的私有成员,如果进一步派生的话,基类的成员就无法在新的派生类中被访问。因此,经过私有继承后,基类的成员再也无法在以后的派生类中发挥作用,实际是相当于中止了基类功能的继续派生。
C. 访问方式为protected方式时,这种继承称为保护继承。
当类的继承方式为保护继承时,基类中的public和protected成员都以protected成员出现在派生类中,而基类private成员不可访问。也就是说,基类的public和protected成员在保护继承方式下被继承为派生类的protected成员,派生类中的其他成员可以直接访问它们,但在派生类的外部无法通过派生类的对象访问它们。而无论是派生类的成员还是派生类的对象都无法访问从基类继承来的private成员。
3)构造函数和析构函数
基类的构造函数和析构函数不能继承,如果对派生类新增的成员进行初始化,就必须在派生类中根据需要加入新的构造函数,如果对从基类继承下来的成员进行初始化,还必须由基类的构造函数来完成派生类构造函数执行的一般次序为:
1)调用基类构造函数,调用顺序按照它们被继承时说明的顺序(从左到右),而不是按派生类构造函数在初始化表中的次序;
2)调用子对象的构造函数(如果在派生类中存在子对象的话),调用顺序按照它们在类中说明的顺序;
3)执行派生类构造函数的函数题。
当派生类的对象被删除时,派生类的析构函数被执行。由于基类的析构函数不能被继承,因此在执行派生类的析构函数时,基类的析构函数也将被调用。而执行顺序是先执行派生类的析构函数,再执行基类的析构函数,其顺序与执行构造函数是的顺序正好相反。
4)虚基类
在多重继承的情况下,派生类具有两个以上的直接基类,而这些直接基类的一部分或全部又是从另一个共同基类派生而来的,这些直接基类中从上一级基类继承来的成员拥有相同的名称,在派生类的对象中,这些同名成员在内存中同时拥有多个拷贝,如何进行分辨呢?有两种方法,一是使用作用域运算符唯一标帜并分别访问它们;二是将直接基类的共同你基类设置为虚基类。(1)使用作用域运算符方法
这种方法就是在需要访问的成员名前加上直接基类名和作用域运算符“::“。其格式是:
直接基类名::数据成员名
直接基类名:成员函数名(参数表)
(2)虚基类的方法
该方法就是将直接基类的共同基类设置为虚基类,即在基类的访问方式前加上关键字“virtual“,声明虚基类的格式如下:
class 派生类名:virtual 访问方式 基类名
{ //声明派生类成员};
虚基类虽然被一个派生类间接地多次继承,但派生类却只继承一份该基类的成员,这样就避免了在派生类中访问这些成员时的二义性
5)虚基类机制下的构造函数的执行顺序
虚基类机制下的构造函数的执行顺序与一般多重继承下的构造函数的执行顺序是不同的,其执行顺序如下:(1)一个类的所有直接基类中,虚基类的构造函数在非虚基类之前调用;
(2)如果一个类的所有直接基类中有多个虚基类,则这些虚基类的构造函数的执行顺序与在派生类中的说明的次序相同;
(3)若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再按照派生类中构造函数的执行顺序调用。
=======================
虚函数和多态
多态性与前面提到的数据封装和继承性共同构成了面向对象程序设计的三个重要机制。
1.静态联编与动态联编
由于函数重载的存在,当程序中出现调用同名函数时,编译器会根据函数的参数类型、个数决定调用执行哪一个同名函数的代码,这种把一个函数的调用与适当的函数实现代码联系在一起的过程,叫做联编。根据联编的实现阶段的不同,可将其分为静态联编和动态联编两种。静态联编是在程序编译阶段确定一个函数调用与函数实现代码间的对应关系,这种对应关系确定下来后,在程序运行过程中就根据这个对应关系去调用执行相应的函数代码,并且这种对应关系在程序运行过程中始终保持不变。
而动态联编是在编译阶段不能决定执行哪个同名的被调函数,只在程序运行过程中根据需要处理的对象类型来决定执行哪个类的成员函数。
2.多态性
所谓多态性就是指同样的消息被类的不同对象接收时导致的完全不同的行为的一种现象。这里所说的消息即对类的成员函数的调用。函数的重载可以实现多态性,但这里要讲的多态性是通过虚函数来实现的,而虚函数又必须存在于继承的环境下。
C++语言支持两种类型的多态:一种是编译时的多态(静态多态),另一种是运行时的多态(动态多态)。在编译时的多态是通过静态联编实现的,而在运行时的多态则是通过动态联编实现的。
3.虚函数
声明虚函数的方法是在基类中的成员函数原型前加上关键字virtual。格式如下:class 类名{
……
virtual 类型 函数名(参数表);
……
};
当一个类的成员函数声明为虚函数后,这就意味着该成员函数在派生类中可能有不同的实现,也就是说,该函数在派生类中可能需要定义与其基类虚函数原型相同的函数。
虚函数是动态联编的基础,当用基类类型的指针或引用的方法指向不同派生类对象时,系统会在程序运行中根据所指向对象的不同自动选择适当的函数,从而实现了运行时的多态性。
当通过基类指针或引用标识对象并调用成员函数时,由于基类指针可以指向该基类的不同派生类对象,因此存在需要动态联编的可能性,但具体是否使用动态联编,还要看所调用的是否是虚函数。
虚函数可以在一个或多个派生类中被重新定义,但它要求在派生类中重新定义时必须与基类中的函数原型完全相同,包括函数名、返回值类型、参数个数和参数类型的顺序。
注意:只有类的成员函数才能声明为虚函数,但类的构造函数以及全局函数和静态成员函数不能声明为虚函数。
4.用基类指针指向公有派生类对象
指向基类的指针自然可以指向其公有派生类的对象。但是,由于基类指针本身的类型并没有改变,因此,基类指针仅能访问派生类中的基类部分。5.纯虚函数与抽象类
在定义一个表达抽象概念的基类时,有时可能会无法给出某些成员函数的具体实现。这时,就可以将这些函数声明为纯虚函数。纯需函数的声明格式如下:
virtual 类型 函数名(参数表)=0;
声明了纯虚函数的基类只是用于继承,仅作为一个接口,具体功能在其派生类中实现。
声明了纯虚函数的类,称为抽象类。抽象类只能用作基类来派生新类,而不能用来创建对象
6.多态性的条件:
1):基类的虚函数。2):派生类的虚函数必须和基类的虚函数声明一致(包括参数类型,返回值类)
3):类的成员函数才可以说明成虚函数(一般函数不行)。静态成员函数不受制于某个对象,不能说明成虚函数。内联函数不能在运行中动态确定。构造函数因为负责构造对象,所以也不能是虚函数。而析构函数一般是虚函数。
对于析构函数一般都是虚函数的解释
4):指针,或者引用才能实现多态
例子:
#include <iostream>
using namespace std;
class A {
public:
void foo() {
cout << "A::function foo" << endl;
}
virtual void fuu() {
cout << "A::function fuu" << endl;
}
};
class B: public A {
public:
void foo() {
cout << "B::function foo" << endl;
}
void fuu() {
cout << "B::function fuu" << endl;
}
};
int main() {
A a;
B b;
A *p = &a;
p->foo();
p->fuu();
p = &b;
p->foo();
p->fuu();
return 0;
}
第一个p->foo()和p->fuu()都很好理解,本身是基类指针,指向的又是基类对象,调用的都是基类本身的函数。
第二个p->foo()和p->fuu()则是基类指针指向子类对象,正式体现多态的用法,p->foo()由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的foo()函数的代码了。而p->fuu()指针是基类指针,指向的fuu是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用fuu()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的fuu()函数的地址。