继承(Inheritance): 子类接收父类的产业.
派生(Derive) : 父类把产业传给子类.
继承语法:
class 派生类名: [继承方式] 基类名{
派生类新增成员;
};
#include <iostream> #include <string> using namespace std; class People{ public: void setname(string name); void setage(int age); string getname(); int getage(); private: string name; int age; }; void People::setname(string name){ this->name = name; } void People::setage(int age) { this->age = age ; } string People::getname(){ return this->name; } int People::getage() { return this->age ; } class Student: public People{ //公有继承 public: void setscore(float score); float getscore(); void show(); private: float score; }; void Student::setscore(float score){ this->score = score; } float Student::getscore(){ return this->score; } void Student::show(){ cout << "name=" << this->getname() << ", age=" << this->getage() << ", score=" << this->getscore() << endl; } int main(){ Student *p_stu0 = new Student; //通过new关键字返回的是指针 p_stu0->setname("Xiaoming"); //指针通过->调用成员 p_stu0->setage(17); p_stu0->setscore(95.2); p_stu0->show(); Student stu1; //没有new, 返回的是对象. stu1.setname("XiaoHong"); //对象通过"."调用成员 stu1.setage(18); stu1.setscore(59.2); stu1.show(); }
继承方式有三种: public, protect, private.
不同继承方式, 基类成员在派生类中的属性
继承方式 | public成员 | protected成员 | private成员 |
---|---|---|---|
public 继承 | public | protected | 不可见 |
protect继承 | protected | protected | 不可见 |
private继承 | private | private | 不可见 |
继承方式中的public、protected、private是用来指明基类成员在派生类中的最高访问权限的.
不管继承方式如何,基类中的private成员在派生类中始终不能使用
如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private
如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。
由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public.
使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。
注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
比如父类People和派生类Student中都定义了函数show(), 则
stu.show() 调用的是派生类的成员.
stu.People::show()调用的是父类的成员.
注意: 父类与派生类名称一样的函数不构成重载, 只要名字相同就会遮蔽, 不管参数是否相同.
构造函数不能继承, 所有一般需要在派生类的构造函数中调用基类的构造函数.
因为基类构造函数不会被继承,不能当做普通的成员函数来调用, 只能在初始化列表中使用.
#include <iostream> using namespace std; class People{ public: People(string name, int age); void show(); protected: string m_name; int m_age; }; People::People(string name, int age): m_name(name), m_age(age) {} void People::show(){ cout << "name=" << m_name << ", age=" << m_age << endl; } class Student: public People{ public: Student(string name, int age, float score); void show(); private: float m_score; }; //定义派生类的构造函数 //Student::Student(string name, int age, float score): People(name, age), m_score(score){} //此句没问题 Student::Student(string name, int age, float score): People(name, age){ //在初始化列表中调用基类构造函数 //People(name, age); //此句是错的, 因为基类构造函数不会被继承,不能当做普通的成员函数来调用, 只能在初始化列表中使用 m_score = score; } void Student::show(){ cout << "name=" << m_name << ", age=" << m_age << ", score=" << m_score << endl; } int main(){ Student stu0("XiaoMing", 13, 19.8); stu0.show(); }
构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。
还有一点要注意,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。
事实上,通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。换句话说,定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。
和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
一个派生类可以有多个基类.
多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java、C#、PHP 等干脆取消了多继承。
多继承语法:
class 派生类名称: [继承类型] 基类A, [继承类型] 基类B[, ...]{
派生类成员;
};
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
数据类型转换的前提是,编译器知道如何对数据进行取舍.
类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括
1)将派生类对象赋值给基类对象、
2)将派生类指针赋值给基类指针、
3)将派生类引用赋值给基类引用,
这在 C++ 中称为向上转型(Upcasting)。
相应地,将基类赋值给派生类称为向下转型(Downcasting)。
赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。
运行结果也有力地证明了这一点,虽然有a=b;这样的赋值过程,但是 a.display() 始终调用的都是 A 类的 display() 函数。
换句话说,对象之间的赋值不会影响成员函数,也不会影响 this 指针。
将派生类对象赋值给基类对象时,会舍弃派生类新增的成员,也就是“大材小用”
与对象变量之间的赋值不同的是,对象指针之间的赋值并没有拷贝对象的成员,也没有修改对象本身的数据,仅仅是改变了指针的指向。
将派生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数.
编译器虽然通过指针的指向来访问成员变量,但是却不通过指针的指向来访问成员函数:编译器通过指针的类型来访问成员函数。
对于 pa,它的类型是 A,不管它指向哪个对象,使用的都是 A 类的成员函数
总结:
“多态(polymorphism)”指的是同一名字的事物可以完成不同的功能。
多态可以分为编译时的多态和运行时的多态。
编译时的多态: 主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;
运行时的多态: 和继承、虚函数等概念有关,是本章要讲述的内容。本教程后面提及的多态都是指运行时的多态。
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。
多态是面向对象编程的主要特征之一,C++中虚函数的唯一用处就是构成多态。
C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。
//虚函数的注意事项:
//构成多态的条件
#include <iostream> class Base{ public: virtual void func(); virtual void func(int n); }; void Base::func(){ std::cout << "In Base::func()" << std::endl; } void Base::func(int n){ std::cout << "In Base::func(int n)" << std::endl; } class Derived: public Base{ public: void func(); void func(std::string str); }; void Derived::func(){ std::cout << "In Derived::func()" << std::endl; } void Derived::func(std::string str){ std::cout << "In Derived::func(std::string str)" << std::endl; } int main(){ Base *p = new Derived; //指针p是基类类型, 但指向派生类. p->func(); //调用派生类的虚函数, 构成了多态; p->func(6);//调用基类的虚函数, 因为派生类中没有覆盖它; //p->func("name"); //编译错误, 因为基类的指针只能访问从基类继承过去的成员, 不能访问新增的成员. }
声明方法: virtual 返回值类型 函数名(函数参数) = 0;
特点: 只有函数声明, 没有函数体, 函数声明时在结尾加了=0;
概念: 包含纯虚函数的类称为抽象类.
特点: 抽象类无法实例化(无法创建对象), 因为纯虚函数没有函数体, 无法调用, 无法分配内存空间;
作用: 抽象类通常是做为基类, 让派生类去实现纯虚函数. 派生类必须实现纯虚函数才能被实例化;
#include <iostream> class Line{ public: Line(float len); virtual float area() = 0; virtual float volume() = 0; protected: float m_len; }; Line::Line(float len): m_len(len){} class Rec: public Line{ public: Rec(float len, float width); float area(); protected: float m_width; }; Rec::Rec(float len, float width): Line(len), m_width(width){} float Rec::area(){ return m_len * m_width; } class Cuboid: public Rec{ public: Cuboid(float len, float width, float height); float area(); float volume(); protected: float m_height; }; Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){} float Cuboid::area(){ return 2*(m_len*m_width + m_len*m_height + m_width*m_height); } float Cuboid::volume(){ return m_len*m_width*m_height; } class Cube: public Cuboid{ public: Cube(float len); float area(); float volume(); }; Cube::Cube(float len): Cuboid(len, len, len){} float Cube::area(){ return 6*m_len*m_len; } float Cube::volume(){ return m_len*m_len*m_len; } int main(){ Line *p_cuboid = new Cuboid(10, 20, 30); Line *p_cube = new Cube(15); std::cout << "Cuboid.area() =" << p_cuboid->area() << std::endl; std::cout << "Cuboid.volume()=" << p_cuboid->volume() << std::endl; std::cout << "Cube.area() =" << p_cube->area() << std::endl; std::cout << "Cube.volume()=" << p_cube->volume() << std::endl; }
在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。
抽象基类除了约束派生类的功能,还可以实现多态。请注意第 51 行代码,指针 p 的类型是 Line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 Line 类中将这两个函数定义为纯虚函数;如果不这样做,51 行后面的代码都是错误的。我想,这或许才是C++提供纯虚函数的主要目的。
注意: