类的继承
1.继承的基本概念和派生类的定义
继承是面向对象编程中的一个重要特性,它允许我们创建一个新的类(称之为派生类),从一个已有的类(称之为基类或父类)中继承它的属性和方法(函数)。在继承中,派生类可以继承基类的公共成员以及这些成员的所有属性和方法。这样,通过继承,我们可以让派生类获得从基类中的一些特性,并且可以在这些特性的基础上进行扩展和定制。
2.案例
下面我们举一个通俗易懂的例子来说明派生类的定义。
假设我们现在要定义一个汽车类,其中包括汽车的品牌、颜色、价格等属性以及汽车的加油、启动、停止和驾驶等方法。汽车类的定义如下所示:
class Car {
public:
string brand_;
string color_;
double price_;
// 构造函数
Car(string brand, string color, double price) : brand_(brand), color_(color), price_(price) {}
void refuel() { cout << "加油" << endl; } // 加油
void start() { cout << "启动" << endl; } // 启动
void stop() { cout << "停止" << endl; } // 停止
void drive() { cout << "驾驶" << endl; } // 驾驶
};
接着,我们要定义一种新的汽车类,它是从 Car 类继承而来的,并定义它的一些新属性和方法。这个新的汽车类我们称之为 SUV 类。SUV 类的定义如下所示:
class SUV : public Car{ // 派生类定义
public:
// 构造函数,调用基类构造函数初始化(这里我后面会详细讲解)
SUV(string brand, string color, double price, bool offroad) : Car(brand, color, price), offroad_(offroad) {}
// 汽车在越野场地上驾驶
void offroad_drive() { cout << "越野驾驶" << endl; }
private:
bool offroad_;
};
3.讲解案例
在这个例子中,我们定义了一个 新的 SUV 类,它从 Car 类中继承了汽车的基本属性和方法,包括品牌、颜色、价格和加油、启动、停止和驾驶等方法。然后,SUV 类还定义了一个新增属性 offroad_ 来表示越野能力,并且新增了一个方法 offroad_drive() 用于在越野场地上驾驶。
在派生类中,我们使用关键字 public 来指出继承基类的方式,这里是公有继承(也是最常见的继承方式)。这意味着基类中的公有成员在派生类中保持不变,并且可以通过派生类的对象来访问这些成员。
在派生类的构造函数中,我们还调用了基类的构造函数来初始化基类的成员变量。这是因为基类的成员变量在派生类中并不会被自动初始化。因此,在派生类的构造函数中,可以调用基类的构造函数进行初始化。在这里,我们使用了初始值列表的方式来调用基类的构造函数,以初始化基类的属性。
这个例子比较通俗易懂,通过继承机制的实现,我们成功定义了一个新的汽车类 SUV,它继承了基类 Car 的基本属性和方法,并在此基础上增加了越野能力和相应的方法。这样,我们就可以将 Car 类型的对象用作 SUV 类型的对象,在 SUV 类型对象上增加新的方法和属性,并且可以使代码更加灵活和可复用。
三种继承方式
在C++中有三种继承方式,分别是公有继承、私有继承和保护继承,下面我们将分别进行详细解释。
1. 公有继承
公有继承指的是派生类继承基类的属性和方法时,基类中公有的成员和受保护的成员在派生类中仍然是公有的和受保护的,而基类中私有成员在派生类中不能被访问。公有继承是最常用的继承方式,因为它能够最大程度地利用基类的资源,并对派生类的扩展提供了最大的灵活性。
例如,我们定义一个形状类 Shape:
class Shape {
public:
string color;
protected:
string name;
private:
int size;
public:
void show() {
cout << "形状:" << name << ", 颜色:" << color << endl;
}
};
然后,我们定义一个派生类 Circle,它表示圆形,它从 Shape 类公有继承属性和方法,并且增加了自身的属性 radius 和方法 area() 以及方法print():
class Circle : public Shape { // 公有继承
public:
double radius;
Circle(string name,string color):name(name),color(color){}
double area() {
return 3.14 * radius * radius;
}
void print(){
// this->size; 无法访问
cout<<"颜色 "<<this->color<<"名字 "<<this->name<<endl;
}
};
int main()
{
Circle c("圆","红色");
c.print();
cout<<c.color<<endl;
//cout<<c.name<<c.size<<endl; 访问不到name和size
return 0;
}
在这个例子中,我们使用公有继承方式将派生类 Circle 与基类 Shape 之间建立了继承关系,派生类 Circle 可以直接访问颜色和名字等基础属性,并且可以自定义一个圆形所独有的属性半径(radius)和一个计算面积的函数 area()。这样,我们就完美实现了圆形这种形状的要求,并且与基类形状也实现了衔接,实现了形状的分类。
2. 私有继承
私有继承指的是派生类继承基类的属性和方法时,基类中公有的和受保护成员在派生类中转化为私有的,而基类中的私有成员在派生类中无法访问.通过私有继承,派生类只能在类内部访问基类中的公有成员和受保护成员,而在外部只能通过派生类内部的公有成员函数来进行访问.
举一个例子,我们定义一个基类 Person,包含一个 string 类型的私有成员变量 name 和一个公有的展示信息的成员函数 show()。
class Person {
private:
string name;
public:
int age;
protected:
int height;
public:
void show() {
name="kunkun";
age=2;
height=18;
cout << "姓名:" << this->name << endl;
cout << "年龄:" << this->age << endl;
cout << "体重:" << this->height << endl;
}
};
然后,我们定义一个派生类 Student,它从 Person 类私有继承属性和方法,并增加了自身的属性和方法 id 和 study():
class Student : private Person { // 私有继承
private:
int id;
public:
Student(int height,int age,int id)
{
this->height=20,this->age=3,this->id=15;
}
void study() {
//cout<<this->name<<endl; 这里会报错,也验证了name并非私有属性
cout<<this->height<<" "<<this->age<<endl;
cout << this->id << endl;
}
void PrintShow(){
this->show();
//注意,这里能打印出name,并非说明name同其他两者通过继承后变为private属性
//这里只能说明name实际上是有被派生类继承的,但在派生类中是隐性状态,无法访问,只能通过父类提供的方法间接访问
}
};
int main()
{
Student s;
//s.id 明显访问不到
//s.name s.age s.height s.show()
//在私有继承下,不管原先是什么访问权限,对象都是无法直接访问的
s.study(); //只有通过在自身类内部定义的公开方法,才能访问得到父类中的非私有成员和自身成员
s.Printshow(); //通过继承父类提供的方法是可以访问name的,但一般我们并不会这么做.
return 0;
}
在这个例子中,我们使用私有继承方式将派生类 Student 与基类 Person 之间建立了继承关系,派生类 Student 对象无法访问基类的所有成员,派生类的外部访问基类的成员只能通过派生类提供的公有函数接口,如 Printshow(),study()等来间接访问。通过私有继承方式,我们可以封装除了派生类之外的外部对象,控制对外部成员的访问,保障数据的安全性。
3. 保护继承
保护继承指的是派生类继承基类的属性和方法时,基类中公有和保护成员都会变成派生类中的保护成员,而基类中的私有成员在派生类中无法访问,无法被派生类或派生类的外部对象访问。
我们来举一个保护继承的例子。假设我们有一个基类 Animal,它有一个声音的成员函数 makeSound(),其中包含一些成员变量,用于记录该动物的食物、体重、年龄等属性。我们要定义一个派生类 Cat,它从 Animal 类保护继承属性和方法,并增加了自己的属性和方法 catchMouse():
class Animal {
public:
string food;
protected:
double weight;
private:
int age;
public:
void makeSound() {
cout << "这是一个动物" << endl;
}
};
class Cat : protected Animal { // 保护继承
public:
Cat(string name, double weight):name(name),weight(weight){}
void catchMouse() {
cout << "小猫会抓老鼠" << endl;
}
void show() {
//this->age 即使是在类内部,我们也无法直接访问age
//说明age并非私有继承
cout << "食物:" << this->food << ", 体重:" <<this->weight<< endl;
this->makeSound();
}
};
int main()
{
Cat c("鱼",14);
//在保护继承中,所有的数据都不能被对象直接访问到.
//我们只能通过类的公开函数间接访问
c.show();
return 0;
}
在这个例子中,我们使用保护继承方式将派生类 Cat 与基类 Animal 之间建立了继承关系,派生类 Cat 内部可以访问基类的保护成员变量,例如食物、体重等成员。同时,派生类 Cat 自定义了一个抓老鼠的方法 catchMouse(),这个方法属于猫类的特有行为,不能在基类 Animal 中定义。另外,派生类 Cat 新增了一个展示自己属性的公共函数 show(),这个函数用于输出父类中继承过来的属性(包括从基类继承下来的非私有属性和方法),对外提供了一个公共接口,方便外部访问。
通过保护继承方式,派生类可以继承基类的保护成员和公有成员,在派生类中,使其都为保护属性,但不能访问基类的私有成员,保证了基类的封装性,同时也保证了派生类的必要的访问权限,避免了可能存在的安全问题。
4. 总结
需要注意的是, C++父类的私有成员,不管是哪种继承方式,子类都是有继承的,但是子类不能直接访问,需要使用父类提供的方法才能间接访问该成员.但我们一般不做去做,因为这样破坏了程序的封装性和安全性.
三种继承方式各有特点,在面向对象编程中,我们应该根据具体的需求来选择不同的继承方式,以达到最好的编程效果和代码复用。
公有继承是最常用的继承方式,可以最大程度地利用基类的资源,保证了派生类的可扩展性;私有继承和保护继承可以保障数据的安全性,让外部对象无法直接访问核心属性和方法,同时提供了必要的访问接口,有利于程序的封装性和安全性。
继承中的构造和析构
前言
在C++中,使用继承时,子类可以显性的继承所有父类的非private成员(函数和变量),但子类不能继承父类的构造函数和析构函数。
继承中的构造函数
为什么不能继承?即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。
虽然,构造函数不会被继承过来,但是在做子类初始化时,很多场景都需要使用到父类的构造函数,此时,还是可以在子类中调用父类的构造函数的。
//子类中调用父类的构造函数
//语法
ChildClassName (string name,int age,float score):SuperClassName(name,age),score(score)
{}
代码案例
#include<iostream>
class Animal{
public:
string name;
int age;
//构造函数使用参数列表方式初始化
Animal(string name,int age):name(name),age(age){}
//打印变量的值
void Print()
{
cout<<this->name<<" "<<this->age<<endl;
}
}
class Dog:public Animal{
public:
int weight;
// using Animal::Animal; 通过using来继承基类构造函数
//调用父类构造函数初始化
Dog(string name,int age,int weight):Animal(name,age),weight(weight){}
void DogPrint()
{
this->Print(); //调用父类继承的方法
cout<<this->weight<<endl;
}
}
int main()
{
Dog d("哈士奇",14,20);
d.DogPrint();
return 0;
}
通过上面的案例,我们轻松的掌握了如何在子类中调用父类构造函数的方法.但其中应该注意的一点是,C++11标准中,可通过using Base::Base把基类构造函数继承到派生类中,不再需要书写多个派生类构造函数来完成基类的初始化。写法就是在上面的案例中添加上using Animal:Animal;这句代码。
当然,子类并不会继承父类的析构函数。
更为巧妙的是,C++11标准规定,继承构造函数与类的一些默认函数(默认构造、析构、拷贝构造函数等)一样,是隐式声明,如果一个继承构造函数不被相关代码使用,编译器不会为其产生真正的函数代码。
构造函数和析构函数的调用顺序
子类继承父类时,当实例化子类时, 一定会优先调用父类的构造函数,事实上,通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。
换句话说,定义派生类构造函数时最好指明要的调用基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数) ; 如果没有默认构造函数,那么编译失败。
在C++中,使用子类继承父类时,当析构子类时, 一定会优先调子类的析构函数,接着,才会调用父类的析构函数,正好跟构造函数的调用顺序相反。(类似栈进和栈出)
多继承
在C++中,当派生类只有一个基类时,称为单继承。同时,在C++中,当派生类有多个基类时,称为多继承。
多继承容易让的代码逻辑复杂,思路混乱,备受争议,在中小型项目较少使用。
继承的构造函数
在C++中,多继承的构造函数和单继承基本相同,只是要在派生类的构造函数中调用多个基类构造函数。
基类构造函数的调用顺序跟声明派生类时基类出现的顺序相同。
class Person{
public:
Person(string name):name(name){}
protected:
string name;
};
class Worker{
public:
Worker(double salary):salary(salary){}
protected:
double salary;
};
//子类调父类构造函数的顺序跟子类的定义时基类的出现顺序相同
class Ai:public Person,public Worker{
public:
Ai(string name,double salary,stirng nick):Person(name)
Worker(salary),nick(nick){}
void Printinfo(){
cout<<this->nick<<endl;
cout<<this->name<<endl;
cout<<this->salary<<endl;
}
private:
string nick;
};
int main()
{
Ai a1("zx",1000,"jqr");
a1.PrintInfo();
return 0;
}
多继承命名冲突
在C++中,如果多个子类有相同的成员变量或函数,那么此时就会存在冲突问题,如果存在冲突,我们调用时变量或函数时就必须显示声明钓调用。
class Person()
{
public:
Person(){}
void PrintInfo(){}
};
class Worker{
public:
Worker(){}
void PrintInfo(){}
};
class Ai:public Worker,public Peron{
public:
Ai():Worker(),Person(){}
//需显示调用
void func(){
Person::PrintInfo();
Worker::Worker();
};
int main()
{
Ai a;
a.func();
return 0;
}
菱形继承
使用多继承时,如果发生A派生B和C,D继承B和C,就会发生菱形继承。
在C++,在使用多继承时,如果发生了菱形继承,那么就会出现数据冗余的问题。这时,我们需要声明好变量或函数所继承的基类。
虚继承
为解决菱形继承出现的数据冗余问题,C++提出了虚继承,虚继承使得子类只保留一份间接基类的成员。
class B:virtual public A{
}:
//虚继承语法
虚继承的构造函数
在c++中,普通继承是子类直接显示调用父类的构造函数,而在虚继承中,虚基类是由最终派生类初始化的。
也就是说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终派生类来说,虚基类是间接基类,而不是直接基类。这更普通继承不同,普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。
//虚基类
class A{
public:
A(int a):m_a(a){}
protectecd:
int m_a;
};
//直接派生B
class B:virtual public A
{
public:
B(int a,int b):A(a),m_b(b){}
public:
void display()
{
cout<<"Call B m_a"<<m_a<<",m_b="<<m_b<<endl;
}
protected:
int m_b;
};
//直接派生C
class C:virtual public A
{
public:
B(int a,int c):A(a),m_c(c){}
public:
void display()
{
cout<<"Call C m_a"<<m_a<<",m_c="<<m_c<<endl;
}
protected:
int m_c;
};
}
//间接派生类D
class D:public B,public C{
public:
D(int a,int b,int c,int d):A(a),B(69,b),C(100,c),m_d()
{
}
public:
void display()
{
cout<<"m_a<"<<m_a<<endl;
cout<<"m_b<"<<m_b<<endl;
cout<<"m_c<"<<m_c<<endl;
cout<<"m_d<"<<m_d<<endl;
}
private:
int m_d;
}
int main()
{
B b(10,20);
b.display();
C c(30,40);
c.display();
D d(50,60,70,80);
d.display();
return 0;
}
多态
C++面向对象的三大特征为:封装,继承和多态 。这三种机制能够有效提高程序的可读性,可扩展性和可重用性。多态指的是同一个名字的事物可以完成不同的功能。
多态可以分为编译时的多态和运行时的多态。
前者主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定调用哪个函数,因此叫编译时的多态。而后者则和继承,虚继承等概念有关。
有虚函数
有虚函数,基类指针指向基类对象时,使用基类的成员,指向子类对象时,使用子类的成员。即基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种形态,这种现象称为多态。
C++提供多态的目的是为了基类指针可以对所有子类(包括直接派生和间接派生)的成员变量和函数进行"全方位”的访问,尤其是成员函数。
如果没有多态,只能访问成员变量。
构成多态的条件
- 必须存在继承关系
- 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
- 存在基类的指针,通过该指针调用虚函数。
多态案例
//Person类
class Person
{
public:
Person(string name,int age):name(name),age(age){}
void info()
{
cout<<"Person.info"<<endl;
cout<<"name "<<this->name<<endl;
cout<<"age "<<this->age<<endl;
}
protected:
string name;
int age;
};
class Student:public Person{
public:
Student(string name,int age,float score):Person(name,age),score(score)
{
}
void info()
{
cout<<"Student info"<<endl;
cout<<"name "<<this->name<<endl;
cout<<"age "<<this->age<<endl;
cout<<"score "<<this->score<<endl;
protected:
float score;
};
int main()
{
//正常调用
Person*person=new Person("zs",20);
person->info();
Student*student =new Student("ww",21,90);
student->info();
//错误案例,没有使用virtual
Person*person=new Person("zs",20);
person->info();
person=new Student("ww",21,90);
person->info();
return 0;
}
上例:
定义了一个Person和一个Student类,Student类继承Person,是Person的子类,然后再main里分别实例一个Person和Student对象。
最后,分别调用Person类和Student类的方法,结果是两者各自调用自己的info函数。
现在,用Student来实例Person类。最终调用了info函数,但此时的info函数还是调用Person类的,这并不是想要的结果,若为多态的话,应该是调用Student类的info函数。
为了能够访问派生类的成员函数,C++增加了虚函数。
在C++中,使用virtual关键字修饰的函数被称为虚函数,虚函数对于多态具有决定性的作用,有虚函数才能构成多态。
一般只有当成员函数所在的类作为基类,且成员函数在类的继承后希望更改其功能的,我们会将其声明为虚函数。
virtual void info()
//虚函数的语法
//正确案例
//Person类
class Person
{
public:
Person(string name,int age):name(name),age(age){}
virtual void info()
{
cout<<"Person.info"<<endl;
cout<<"name "<<this->name<<endl;
cout<<"age "<<this->age<<endl;
}
protected:
string name;
int age;
};
class Student:public Person{
public:
Student(string name,int age,float score):Person(name,age),score(score)
{
}
virtual void info()
{
cout<<"Student info"<<endl;
cout<<"name "<<this->name<<endl;
cout<<"age "<<this->age<<endl;
cout<<"score "<<this->score<<endl;
protected:
float score;
};
int main()
{
//正常调用
Person*person=new Person("zs",20);
person->info();
Student*student =new Student("ww",21,90);
student->info();
//指针实现多态
Person*person=new Person("zs",20);
person->info();
person=new Student("ww",21,90);
person->info();
return 0;
}
当然,除了使用指针实现多态,我们也可以使用引用实现多态。但引用不像指针那么灵活,指针可以随时改变指向,而引用只能指代固定的对象,再多态性缺乏表现力。
//引用实现多态
int main()
{
//引用类似于常量,只能在定义时初始化,之后不能再引用
//其他数据,所以本例必须定义两个变量。
Person& person=new Person("xiao",100);
person.info();
Person& student= new Student("zhang",120,88);
student.info();
}
虚析构函数
在C++中,使用virtual关键字修饰的函数称为虚函数。C++的构造函数不可以被声明为虚函数,但析构函数可以,且有时候必须将析构函数声明为析构函数。(在C++开发中,用来做基类的析构函数一般都是虚函数)
虚析构函数的作用
虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。
当父类的析构函数不声明成虚析构函数,且子类继承父类,父类的指针指向子类时,若此时delete掉父类指针,只会调动父类的析构函数,而不会调动子类的析构函数。
虚函数的实现(虚函数表)
在C++中,多态是由虚函数实现的,而虚函数主要是通过虚函数表来实现的。在多态中,对象不包括虚函数表,只有虚指针,类才包括虚函数表,派生类会生成一个兼容基类的函数表。
如果一个类中包含虚函数(virtual修饰的函数),则该类就会包括一张虚函数表,虚函数表存储的都是虚函数的地址。
虚函数表是一个指针数组,其元素是虚函数的指针,每一个元素都是函数是指针。虚函数表在编译器的编译阶段就形成了。
虚函数表是属于类的,一个类只需一个虚函数表,同一个类的对象共用一张虚函数表。
class A{
public:
virtual void func1();
virtual void func2();
private:
int data1,data2;
};
如图:
动态绑定
1. 什么是动态绑定
动态绑定是指在程序运行时根据对象的实际类型来确定调用哪个方法或属性。
在面向对象编程中,如果一个类继承自其他类或实现了某个接口,那么它可以被看作是父类或接口的一个实例。动态绑定允许在调用方法或访问属性时,根据实际对象的类型来确定要执行的代码。这样可以实现多态性,提高代码的灵活性和可扩展性。
2. C++如何实现动态绑定
在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?在对象构建也就是在对象初始化调用构造函数的时候。
编译器会首先默认在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象所属类的虚函数表。
起初,子类继承基类,继承基类的vptr指针,这个vptr指针是指向基类的虚函数表,当子类调用构造函数,使得子类的vptr指针指向子类在虚函数表。
3. 多态的原理:
链接:link
抽象基类和纯虚函数
在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际地创建一个基类的对象。
要做到这点,可以在基类中加入至少一个纯虚函数,来使基类成为抽象类。纯虚函数使用关键字virtual,并且在其后面加上=0。如果某人试着生成一个抽象类的对象,编译器会制止他,这个工具允许生成特定的设计。
当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出的类也将是一个抽象类。创建一个纯虚函数允许在接口中放置成员函数,而不一定要提供一段可能对这个函数毫无意义的代码。同时,纯虚函数要求出的类对它提供一个定义。纯虚函数总是变成“哑”函数。
建立公共接口,也就是纯虚函数抽象类。它能对于每个不同的子类有不同的表示,它建立一个基本的格式。
抽象基类至少要有一个纯虚函数。
//虚函数
virtual type funName(plist){}
//纯虚函数
virtual type fuName(plist)=0;
案例:
class AbstractDrinking{
public:
virtual void Boil()=0;
virtual void Brow()=0;
virtual void PourInCup()=0;
virtual void PutSomething()=0;
void MakeDrink()
{
Boil();
Brow();
PourInCup();
PutSomething();
}
virtual ~AbstractDrinking()
{
}
};
class Coffee:public AbstractDrinking
{
public:
virtual void Boil()
{
cout<<"煮山泉"<<endl;
}
virtual void Brow()
{
cout<<"泡咖啡"<<endl;
}
virtual void PourInCup()
{
cout<<"咖啡倒入杯子"<<endl;
}
virtual void PutSomething()
{
cout<<"加牛奶"<<endl;
}
};
class Tea:public AbstractDrinking{
public:
virtual void Boil()
{
cout<<"煮白开水"<<endl;
}
virtual void Brow()
{
cout<<"泡茶"<<endl;
}
virtual void PourInCup()
{
cout<<"茶倒入杯子"<<endl;
}
virtual void PutSomething()
{
cout<<"加盐"endl;
}
};
//业务函数
void DoBussiness(AbstractDrinking* drink)
{
drink->MakeDrink();
delete drink;
}
int main()
{
DoBussiness(new Coffee);
cout<<endl;
DoBussiness(new Tea);
return 0;
}
结尾
文章到这里就结束了,希望读者可以支持一波,给个三连。