三类继承方式

  • 子类会将父类的除构造函数析构函数赋值运算重载符外的成员继承过来,只不过编译器隐藏了父类的私有属性,子类不可以访问。
 1 class Base {
 2 public:
 3     int a_;
 4 protected:
 5     int b_;
 6 private:
 7     int c_;
 8 };
 9 
10 class Son :public Base {
11 public:
12     int d_;
13 };
14 void test01() {
15     cout << sizeof(Son) << endl;
16 }

几点注意:

  • 如果显示创建了一个子类对象,又显示创建了一个父类对象,则该子类对象并不是该父类对象的子类,该子类对象的父类在创建子类的时候被隐式创建了,因此显示创建的子类对象在操作它继承下来的成员的时候并不会影响显示创建的父类对象的成员。
 1 class Base {
 2 public:
 3     int a_;
 4 protected:
 5     int b_;
 6 private:
 7     int c_;
 8 };
 9 
10 class Son :public Base {
11 public:
12     int d_;
13 };
14 void test01() {
15     Son son;
16     Base base;
17     son.a_ = 10;
18     base.a_ = 20;
19     cout <<"显示创建的子类对象的a_属性"<< son.a_ << endl;
20     cout << "显示创建的父类类对象的a_属性" << base.a_ << endl;
21 }

 

  • 创建子类对象的时候,即便不显示创建父类,编译器也会创建父类。而且先构造父类,后析构子类。

 

子类的类型可以转换为父类(但不建议),父类却不能转换为子类,子类之间同样不能相互转换。

 因此下面标黄处的语句是错误的:

 1 void dospeak1(Animal& animal) {
 2     animal.speak();
 3 }
 4 void dospeak2(Cat& cat) {
 5     cat.speak();
 6 }
 7 void test() {
 8     Animal animal;
 9     Cat cat;
10     Dog dog;
11 
12     dospeak1(animal);
13     dospeak1(cat);
14     dospeak2(animal);
15     dospeak2(cat);
16 }

  • 同时子类可以拷贝给父类(但不建议),父类不能拷贝给子类,子类之间也不能相互拷贝

 

子类成员与父类同名问题

当子类成员没有和父类同名时,子类直接用"."符号调用该成员即可,如果子类成员和父类成员有同名的,需要在"."符号后面加上父类作用域

多继承

 

菱形继承问题

一个父类A有多个子类BCD时,这些子类有一个共同的子类E,多个子类会将父类的属性继承下来,同样多个子类的子类也会将这个属性继承下来,这样做的问题是:

  • 多个子类的子类E访问这个属性时不知道访问BCD哪一个,造成二义性
  • E继承了BCD的这个属性,可它们都是同一个属性这样会导致资源浪费,即E只需要继承一个就可以
 1 class Animal {
 2 public:
 3     int age_;
 4 };
 5 class Sheep :public Animal {
 6 
 7 };
 8 class Tuo :public Animal {
 9 
10 };
11 class Sheeptuo :public Sheep, public Tuo {
12 
13 };
14 int main(){
15     Sheeptuo st;
16     //菱形继承,两个父类有相同的数据,加作用域后就能区分到底是哪个父类的属性
17     st.Sheep::age_ = 18;
18     st.Tuo::age_ = 28;
19     cout << "st.Sheep::age_ =" << st.Sheep::age_ << endl;
20     cout << "st.Tuo::age_ =" << st.Tuo::age_ << endl;
21     system("pause");
22     return 0;
23 }

运行结果:

解决上述问题需要虚继承,这就会导致我们age_这个数据只有一份

 1 class Animal {
 2 public:
 3     int age_;
 4 };
 5 class Sheep :virtual public Animal {
 6 
 7 };
 8 class Tuo :virtual public Animal {
 9 
10 };
11 class Sheeptuo :public Sheep, public Tuo {
12 
13 };
14 int main(){
15     Sheeptuo st;
16 
17     
18     st.Sheep::age_ = 18;
19     st.Tuo::age_ = 28;
20     cout << "st.Sheep::age_ =" << st.Sheep::age_ << endl;
21     cout << "st.Tuo::age_ =" << st.Tuo::age_ << endl;
22     cout << "st.age_=" << st.age_ << endl;
23 
24     system("pause");
25     return 0;
26 }

 

 1 class Animal {
 2 public:
 3     int age_;
 4 };
 5 class Sheep :virtual public Animal {
 6 
 7 };
 8 class Tuo :virtual public Animal {
 9 
10 };
11 class Sheeptuo :public Sheep, public Tuo {
12 
13 };
14 int main(){
15     Sheeptuo st;
16 
17     
18     st.Sheep::age_ = 18;
19     st.Tuo::age_ = 28;
20     cout << "st.Sheep::age_ =" << st.Sheep::age_ << endl;
21     cout << "st.Tuo::age_ =" << st.Tuo::age_ << endl;
22     cout << "st.age_=" << st.age_ << endl;
23     Sheep sh;
24     sh.age_ = 10;
25     Tuo tuo;
26     tuo.age_ = 20;
27     cout << "sh.age_ =" << sh.age_ << endl;
28     cout << "tuo.age_ =" << tuo.age_ << endl;
29 
30     system("pause");
31     return 0;
32 }

 

 

底层原理:

virtual关键字使得Sheeptuo这个子类继承的不是/它两个父类(Sheep和Tuo)从基类(Animal)那里继承的成员属性(也就是age_),而是继承了两个父类的虚基类指针,这两个指针分别指向两个父类自己的虚基类表,这个虚基类表记录了一个偏移量,而虚基类指针加上这个偏移量就能找到唯一的成员属性,即基类的成员属性,因此现在只有一份。

st.Sheep::m_age;st.Tuo::m_age;的时候,会首先找到Sheep或Tuo的虚函数指针通过虚函数指针找到虚函数表,通过虚函数中的偏移量找到m_age

st.m_age的时候直接调用st继承下来的m_age

虚继承只有多继承的时候才起作用

此时B直接调用从A继承来的属性,虚继承不起作用

动态多态与静态多态

 静态多态:

class Animal {
public:
    void speak() {
        cout << "动物在说话" << endl;
    }
    int age_;
};
class Cat :public Animal {
public:
    void speak() {
        cout << "小猫在说话" << endl;
    }
};
//执行的speak函数属于地址早绑定,在编译阶段确定所调用函数(animal.speak())的地址,
//注意此时还没有发生传参,因此编译器只能在"animal.speak()"这条语句中看到animal下的speak()
void dospeak(Animal& animal) { animal.speak(); } void test() { Cat cat; dospeak(cat);//相当于用父类的引用接收了一个子类的对象 }

  • 静态多态到底早绑定哪个函数的地址,也就是到底调用谁的函数,这个要看谁在调用speak(cat.speak()就是cat在调用,animal.speak()就是animal调用)
 1 class Animal {
 2 public:
 3     void speak() {
 4         cout << "动物在说话" << endl;
 5     }
 6     int age_;
 7 };
 8 class Cat :public Animal {
 9 public:
10     void speak() {
11         cout << "小猫在说话" << endl;
12     }
13 };
14 
15 
16 
17 void dospeak1(Animal& animal) {
18     animal.speak();
19 }
20 void dospeak2(Cat& cat) {
21     cat.speak();
22 }
23 void test() {
24     Animal animal;
25     Cat cat;
26     dospeak1(animal);
27     dospeak1(cat);
28     dospeak2(cat);
29 }

运行结果:

 动态多态:

动态多态的原理是:指通过虚函数和继承实现的多态,即同一函数可以在不同的子类对象上产生不同的行为。在动态多态中,父类指针或引用可以指向子类对象,通过父类的指针和引用调用虚函数实现对子类对象的多态访问。

c++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此,在子类从新声明该虚函数时,可以加,也可以不加,但习惯上每一层声明函数时都加virtual,使程序更加清晰。

这里面正是因为有子类的虚函数,所以子类中的speak虚函数的地址替换了继承下来的虚函数表中虚函数的地址。

class Animal {
public:
    virtual void speak() {
        cout << "动物在说话" << endl;
    }
    int age_;
};
class Cat :public Animal {
public:
void speak() { cout << "小猫在说话" << endl; } };
void dospeak1(Animal& animal) { animal.speak(); }void test() { Animal animal; Cat cat; dospeak1(animal); dospeak1(cat);//当对象(这个对象指的是cat,dospeak1的animal是cat的别名)调用虚函数时,会根据对象的虚函数指针找到该对象所属的虚函数表,并从表中查找对应的虚函数地址,然后调用该虚函数。 }

我们常说的多态是动态多态,下面举个例子来表明多态带来的好处。

不使用多态来实现加减:

 1 class AbstractCalculator {
 2 public:
 3     //该成员函数的缺点是:
 4     //如果想扩展新的功能,需要修改源码
 5     //我们在开发中尽量要去扩展代码,而不是修改代码
 6     //如果此时我想加入除法功能,我需要修改成员函数getResult
 7 
 8     //如果我们这个功能出现问题,我们还需要浏览整个getResult函数,不便于我们的维护
 9     int getResult(string oper) {
10         if (oper == "+") {
11             return Num1_ + Num2_;
12         }
13         else if (oper == "-") {
14             return Num1_ - Num2_;
15         }
16     }
17     int Num1_;
18     int Num2_;
19 };
20 void test01() {
21     AbstractCalculator c;
22     c.Num1_ = 10;
23     c.Num2_ = 10;
24     cout << c.Num1_ << "+" << c.Num2_ << "=" << c.getResult("+") << endl;
25 }

使用多态来完成加减:

class AbstractCalculator {
public:
    virtual int getResult() {
        return 0;
    }
    int Num1_;
    int Num2_;
};
class AddCalculator:public AbstractCalculator {
    int getResult() {
        return Num1_ + Num2_;
    }
};
class SubCalculator :public AbstractCalculator {
    int getResult() {
        return Num1_ - Num2_;
    }
};
void test02() {
    AbstractCalculator* abc = new AddCalculator;
    abc->Num1_ = 10;
    abc->Num2_ = 10;
    cout << abc->Num1_ << "+" << abc->Num2_ << "=" << abc->getResult() << endl;
    delete abc;
    abc = new SubCalculator;
    abc->Num1_ = 10;
    abc->Num2_ = 10;
    cout << abc->Num1_ << "-" << abc->Num2_ << "=" << abc->getResult() << endl;
    delete abc;

}
int main() {
    test02();
    system("pause");
    return 0;
}

多态好处:
1、组织结构清晰
2、可读性强
3.对于前期和后期扩展以及维护性高。假如我们想加入除法的时候,我们另外写一个除法的子类就可以了;如果我们加法有问题,那么我们找到加法的子类修改一下就可以

纯虚函数与抽象类

虚析构和纯虚析构

  • 正是因为有多态的存在,才导致父类的指针可以指向子类堆区对象,但是delete父类指针时,无法调用子类的析构函数,这就导致如果子类中有成员属性在堆区开辟了一块内存,那么它理应在子类中的析构函数释放,但是此时子类的析构函数不会被调用,这样子类中堆区内存不会被释放造成内存泄漏
  • 而由于虚析构的存在,在delete父类指针时会调用子类的析构函数,然后子类的析构函数会调用父类的析构函数,这样整个子类被完全释放
 1 class AbstractDrinking {
 2 public:
 3     AbstractDrinking() {
 4         cout << "AbstractDrinking的默认构造的调用" << endl;
 5     }
 6     ~AbstractDrinking() {
 7         cout << "AbstractDrinking的析构函数的调用" << endl;
 8     }
 9     virtual void Boil() = 0;
10 };
11 class Coffee :public AbstractDrinking {
12 public:
13     Coffee() = default;
14     Coffee(string name) {
15         cout << "Coffee的有参构造的调用" << endl;
16         Name_ = new string(name);
17     }
18     ~Coffee() {
19         cout << "Coffee的析构函数调用" << endl;
20         if (Name_) {
21             delete Name_;
22             Name_ = nullptr;
23         }
24     }
25     virtual void Boil() {
26         cout << *Name_<<"煮农夫山泉" << endl;
27     }
28     string* Name_;
29 };
30 void test01() {
31     //这个堆区内存可以被释放,但是上面的new string(name)堆区内存无法被释放
32     //此处的Coffee是堆区对象,那么父类指针释放这个堆区子对象时,不会调用子对象的析构函数
33     AbstractDrinking* abs = new Coffee("雀巢");
34     abs->Boil();
35     delete abs;
36 }

 

 1 class AbstractDrinking {
 2 public:
 3     AbstractDrinking() {
 4         cout << "AbstractDrinking的默认构造的调用" << endl;
 5     }
 6     virtual ~AbstractDrinking() {
 7         cout << "AbstractDrinking的虚析构函数的调用" << endl;
 8     }
 9     virtual void Boil() = 0;
10 };
11 class Coffee :public AbstractDrinking {
12 public:
13     Coffee() = default;
14     Coffee(string name) {
15         cout << "Coffee的有参构造的调用" << endl;
16         Name_ = new string(name);
17     }
18     ~Coffee() {
19         cout << "Coffee的析构函数调用" << endl;
20         if (Name_) {
21             delete Name_;
22             Name_ = nullptr;
23         }
24     }
25     virtual void Boil() {
26         cout << *Name_<<"煮农夫山泉" << endl;
27     }
28     string* Name_;
29 };
30 void test01() {
31     //这个堆区内存可以被释放,但是上面的new string(name)堆区内存无法被释放
32     //此处的Coffee是堆区对象,那么父类指针释放这个堆区子对象时,不会调用子对象的析构函数
33     AbstractDrinking* abs = new Coffee("雀巢");
34     abs->Boil();
35     delete abs;
36 }

 纯虚析构函数:

 1 class AbstractDrinking {
 2 public:
 3     AbstractDrinking() {
 4         cout << "AbstractDrinking的默认构造的调用" << endl;
 5     }
 6 
 7     virtual ~AbstractDrinking() = 0;
 8     virtual void Boil() = 0;
 9 };
10 //纯虚析构函数必须类内声明,类外定义(有函数体实现)
11 //防止父类中也会开辟堆区内存,导致无法释放
12 AbstractDrinking::~AbstractDrinking() {
13     cout << "AbstractDrinking的纯虚析构函数的调用" << endl;
14 }
15 class Coffee :public AbstractDrinking {
16 public:
17     Coffee() = default;
18     Coffee(string name) {
19         cout << "Coffee的有参构造的调用" << endl;
20         Name_ = new string(name);
21     }
22     ~Coffee() {
23         cout << "Coffee的析构函数调用" << endl;
24         if (Name_) {
25             delete Name_;
26             Name_ = nullptr;
27         }
28     }
29     virtual void Boil() {
30         cout << *Name_ << "煮农夫山泉" << endl;
31     }
32     string* Name_;
33 };
34 void test01() {
35     //这个堆区内存可以被释放,但是上面的new string(name)堆区内存无法被释放
36     //此处的Coffee是堆区对象,那么父类指针释放这个堆区子对象时,不会调用子对象的析构函数
37     AbstractDrinking* abs = new Coffee("雀巢");
38     abs->Boil();
39     delete abs;
40 }

 

posted on 2023-06-04 23:21  小凉拖  阅读(12)  评论(0编辑  收藏  举报