学习C++的人都知道面向对象程序设计有4个主要特点:抽象、封装、继承和多态性。可见继承和多态在C++中占据的分量不一般。
本篇文章分别用阐述概念和举栗说明两种方式研究继承与多态,其中会穿插图片说明,望达到轻松理解C++中继承与多态机制的效果。
继承的概念
所谓继承就是在一个已有类的基础上建立一个新的类。以存在的类称为“基类”或“父类”,新建的类称为“派生类“或”子类”。继承机制可提高程序的可重用性,减少重复的工作量。
声明派生类的一般形式:
class 派生类名:[继承方式] 基类名
{
派生类新增成员
};
三种继承方式
(1)公有继承(public)
基类的公有成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类私有。
class Person
{
public:
int _age = 23;
protected:
string _name = "xiaoxu";
private:
int _id = 11;
};
class Student : public Person
{
public:
void Display()
{
cout << _age << endl; //引用基类的公有成员,可以
cout << _name << endl; //引用基类的保护成员,可以
cout << _id << endl; //引用基类的私有成员,出错
cout << _num << endl; //引用自己的私有成员,可以
}
private:
int _num;
};
(2)私有继承(private)
基类的公有成员和保护成员在派生类中的访问属性变为私有,其私有成员仍为基类私有。
class Student : private Person
{
private:
int _num;
};
int main()
{
Student s;
//基类所有成员访问属性在派生类中全部变为私有,外界不能引用
s._age = 26;
s._name = jiyong;
s._id = 22;
system("pause");
return 0;
}
(3)保护继承(protected)
基类的公有成员和保护成员在派生类中的访问属性变为保护,其私有成员仍为基类私有。保护的意思是:不能被外界引用,但可以被派生类的成员引用。
class Student : protected Person
{
public:
void Display()
{
cout << _age << endl;
cout << _name << endl;
}
private:
int _num;
};
int main()
{
Student s;
s.Display(); //通过派生类对象的公有成员函数可以访问基类的公有、保护成员
//不能通过派生类对象直接去访问基类中的成员
s._age = 26;
s._name = jiyong;
s._id = 22;
system("pause");
return 0;
}
继承与转换——赋值兼容规则
1.子类对象可以赋值给父类对象(切割/切片)
2.父类对象不能赋值给子类对象
3.父类的指针/引用可以指向子类对象
4.子类的指针/引用不能指向父类对象(除非通过强制类型转换完成)
class Person
{
public:
void Display()
{
cout << _name << endl;
}
protected:
string _name = "xiaoxu";
};
class Student : public Person
{
public:
int _num;
};
void Test()
{
Person p;
Student s;
p = s; //可以
s = p; //出错
Person* p1 = &s; //可以
Person& r1 = s; //可以
Student* p2 = &p; //出错
Student& r2 = p; //出错
Student* p2 = (Student*)&p; //可以
Student& r2 = (Student&)p; //可以
}
派生类的默认成员函数
1.在继承关系中,如果派生类没有显示定义默认的六个成员函数,编译系统会默认合成这六个默认的成员函数。
2.子类中的构造函数是由父类和子类合成的,初始化时只需初始化子类中的变量,父类的变量调用父类的构造函数即可,无需在子类中初始化。
3.子类的拷贝构造会发生切割/切片行为,因为是把子类对象传给父类拷贝构造的引用参数。
4.不需在子类中显式调用父类的析构函数,编译器会自动调用。
5.析构时先析构子类,再析构父类,因为遵循栈中有高地址向低地址生长的原则。
class Person
{
public:
Person() //无参构造
{
cout << "Person()" << endl;
_name = "xiaoxu";
}
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person& operator=(const Person& p)" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student()
{
cout << "Student()" << endl;
_num = 1;
}
Student(const char* name, int num)
:Person(name)
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
private:
int _num;
};
void Test()
{
Student s1("weiwei", 9);
Student s2(s1);
Student s3;
s3 = s1;
}
int main()
{
Test();
system("pause");
return 0;
}
输出结果:
继承的层次机构
1.单继承一个派生类只有一个直接父类
2.多继承
一个子类有两个或两个以上个直接父类
3.菱形继承
class Person
{
public:
string _name;
};
class Student : public Person
{
protected:
int _credit;
};
class Teacher : public Person
{
protected:
int _id;
};
class Assistant : public Student, public Teacher
{
protected:
int _num;
};
void Test()
{
Person p;
Student s;
Teacher t;
Assistant a;
a._name = "xiaoxu"; //_name的指向不明确,出现二义性
}
菱形继承的对象模型
4.菱形虚拟继承
为解决菱形继承的二义性和数据冗余,引入虚继承,在继承方式前加关键字virtual即可。
虚继承是将Assistant间接继承的基类Person声明为虚基类,使得在Assistant类中只保留一份Person类成员。
注意:虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。
class Student : virtual public Person
{
protected:
int _credit;
};
class Teacher : virtual public Person
{
protected:
int _id;
};
菱形虚拟继承的对象模型
虚基类的初始化
如果在虚基类中定义了带参数的构造函数,而没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化列表对虚基类进行初始化。也就是说,在最后的派生类中不仅要负责 对其直接基类进行初始化,还要负责对虚基类初始化。因为虚基类在派生类中只有一份数据成员,如果由虚基类的直接派生类(如类B和类C)对虚基类初始化,就有可能由于初始化参数不同而产生矛盾。
class A
{
A(int i) //基类构造函数,有一个参数
{}
};
class B : virtual public A
{
B(int n)
:A(n)
{}
};
class C : virtual public A
{
C(int n)
:A(n)
{}
};
class D : public B, public C
{
D(int n) //D类构造函数,在初始化表中对所有基类初始化
:A(n)
, B(n)
, C(n)
{}
};
虚函数
虚函数:类的成员函数前加virtual关键字,则这个成员函数称为虚函数。
在同一个类中不能定义两个名字相同、参数个数和类型都相同的函数。但在类的继承中允许在派生类中定义与基类完全相同的虚函数,称这个派生类中的函数重写(覆盖)了基类的虚函数。可以通过基类指针或引用来访问基类和派生类中的同名函数。
class Person
{
public:
virtual void Display()
{
cout << _name << endl;
}
protected:
string _name = "xiaoxu";
};
class Student : public Person
{
public:
void Display() //重写
{
cout << _num << endl;
}
protected:
int _num = 11;
};
void Test()
{
Person p;
Student s;
Person* pt = &p;
pt->Display(); //输出基类对象中的数据"xiaoxu"
pt = &s;
pt->Display(); //输出派生类对象中的数据"11"
}
纯虚函数:在成员函数的形参后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义后,派生类才能实例化出对象。
class Person
{
public:
virtual void Display() = 0; //纯虚函数
protected:
string _name;
};
class Student : public Person
{
public:
void Display() //重新定义
{
cout << _num << endl;
}
protected:
int _num;
};
多态
所谓多态,就是多种“形态”,其具体表述为:向不同的对象发送同一个调用函数,不同的对象会有不同的实现。
从系统实现的角度来看,多态性分为两类:静态动态性和动态多态性。
静态多态性是通过函数重载实现的。函数重载、运算符重载都属于静态多态性,要求在程序编译时就知道调用函数的全部信息,因此,在程序编译时系统就能决定要调用的是哪个函数。静态多态性又称编译时的多态性。静态多态性的函数调用速度快、效率高,但缺乏灵活性,在程序运行前就已决定了执行的函数和方法。
动态多态性是通过虚函数实现的。又称为运行时的多态性。动态多态性不在编译时确定调用的是哪个函数,而是在程序运行过程中才动态地确定操作所针对的对象。
class Base
{
public:
virtual void Fun1()
{
cout << "Base::Fun1" << endl;
}
virtual void Fun2()
{
cout << "Base::Fun2" << endl;
}
void Display()
{
cout << "Display()" << endl;
}
void Display(int i)
{
cout << "Display(int i)->" << i << endl;
}
private:
int a;
};
class Derive : public Base
{
public:
virtual void Fun1()
{
cout << "Derive::Fun1" << endl;
}
private:
int b;
};
void Fun(Base& b)
{
b.Fun1();
b.Fun2();
b.Display();
b.Display(10);
}
void Test()
{
Base b1;
Derive d1;
Fun(b1);
Fun(d1);
}
int main()
{
Test();
system("pause");
return 0;
}
输出结果:
继承体系同名成员函数的关系
1.重载
a.在同一作用域
b.函数名相同,参数不同
c.返回值可以不同
2.重定义(隐藏)
a.在不同作用域(基类和派生类中)
b.函数名相同
c.在基类和派生类中只要不构成重写就是重定义
3.重写(覆盖)
a.在不同作用域(基类和派生类中)
b.函数名相同,参数相同,返回值相同(协变例外)
c.基类函数必须有virtual关键字
d.访问修饰符可以不同