C++面对对象编程进阶(2)

2024-01-08 03:10:40


这节是上届的延续,所以我就接着上节编号了~

6.多态与虚函数

还记得之前我们讲过的C++函数的重载吗?C++在处理同名函数有三种方法,除了重载,在面对对象编程中还特有另外两种,即隐藏和覆盖。

隐藏

如果父类和子类拥有一个相同名称的方法,那么不管参数列表是否相同,子类的实例都无法调用父类的同名方法。这便是因为父类的该方法在子类中被隐藏了。但是如果我们用父类指针指向子类实例再调用这个方法,就只能调用到父类的这个方法了。看个例子:

class Student
{
    public:
        void test(string a)
        {
            cout<<"This is Student"<<endl;
        }
};
class undergraduate:public Student
{
    public:
        void test(char a)
        {
            cout<<"This is undergraduate"<<endl;
        }
};
int main() // 为了展示隐藏和重载的区别,两个方法的参数列表故意设置成不同
           // 其实可以完全一样
{
    Student ZhangSan,*p1;
    undergraduate LiSi;
    ZhangSan.test("me");
    LiSi.test('m');
    // LiSi.test("me"); // 这里会报错,原因是父类的test方法会在派生中被隐藏,LiSi是子类的实例,无法找到被隐藏的方法
                        // 如果子类的test是父类的重载,这个语句执行后会打印This is Student
                        // 实际上,父类和子类里的同名方法可以使用类型相同的参数而不影响隐藏。
    p1=&ZhangSan;
    // 使用父类指针指向子类实例也只能调用父类的test方法
    p1->test("me");
    p1=&LiSi;
    p1->test("me");
// 输出为:This is Student
//        This is undergraduate
//        This is Student
//        This is Student
}

这里插入一个冷知识,C++中的char类型需要用单引号加字符的方式赋值,双引号加字符会被C++编译器默认为const char类型从而无法赋值给char类型变量哦。

覆盖

覆盖的实现需要声明虚函数作为基类和派生类中同名的方法。如果我们在基类中定义了一个虚函数,在派生类中用同名函数覆写了该虚函数,大家猜一猜,如果此时我们用一个基类指针指向派生类实例,再去调用虚函数,调用的会只是基类中的虚函数吗:

class Student // 这个大括号里的叫类定义
{
    public:
        virtual void study (); // virtual用于在基类中声明一个虚函数
};
void Student:: study()
{
    cout<<"basic subject"<<endl;
}
class undergraduate:public Student
{
    public:
        void study()override // override用于告知编译器子类中该函数为虚函数,且意图重写基类中的虚函数
        {
            cout<<"common required course"<<endl;
        }
};
class graduate:public Student
{
    public:
       void study() override
       {
            cout<<"specialized courses"<<endl;
       }
};
int main()
{
    Student ZhangSan,*p1,*p2;
    undergraduate LiSi;
    graduate WangWu;
    ZhangSan.study();
    LiSi.study(); // 通过实例调用方法可以发现原有方法被新的方法覆盖
    WangWu.study();
    // 通过基类指针指向不同类的实例可以发现
    // 基类指针能够调用的study函数取决于基类指针
    // 指向的实例属于什么类
    p1=&ZhangSan;
    p1->study();
    p1=&LiSi;
    p2=&WangWu;
    p1->study();
    p2->study();
}
// 输出为:basic subject
//        common required course
//        specialized courses
//
//        basic subject
//        common required course
//        specialized courses

很显然,这回计算机就聪明多了,他会根据所指向的实例属于哪一类调用对应的同名属性,这就实现了鼎鼎大名的多态。

7.纯虚函数与抽象类

纯虚函数指在基类中声明,在派生类中必须实现的虚函数。含有纯虚函数的类为抽象类,抽象类有现实意义,但是不能够实例化。抽象类允许拥有除纯虚函数外的其他成员,包括构造函数。
抽象类的意义是什么呢?比如我们之前的例子中,学生这个宽泛的概念就可以被设置成有实际意义但不允许实例化的存在,只有它的子类如研究生,大学生等才可以实例化:

class Student
{
    public:
        string name; 
        virtual void study (int a) const =0;// 虚函数写法为需要加=0,const代表不能够修改类成员
        virtual void work () = 0;
        Student():name("ZhangSan"){}
        Student(string x):name(x){}
};
class undergraduate:public Student
{
    public:
        void study(int age)const override // override用于告知编译器子类中该函数为虚函数,重写基类中的虚函数
                                          // 声明的study方法是const,重写时也必须带有const
        {cout<<"common required course"<<endl;}
        void work()override
        {cout<<"family education";}
};
class graduate:public Student
{
    public:
       graduate(string x):Student(x){} // 由于name不是在派生类中声明的,因此不能用初始化列表进行赋值
       void study(int age)const override
       {cout<<"specialized courses"<<endl;}
       void work() override
       {cout<<"business internship"<<endl;}
};
int main()
{
    // Student aa; // 报错,抽象类无法实例化
    Student* p;  // 但是可以申请抽象类类型的指针
    graduate aa("LiSi"),*p1;
    undergraduate bb;
    aa.study(0); // 派生类直接实例化是可以调用虚函数的
    aa.work();
    cout<<aa.name<<endl;
    cout<<bb.name<<endl;
    p1=&aa;   // 也可以通过类指针进行访问
    cout<<p1->name<<endl;
    p=&bb;   // 基类指针也可以使用,限制与普通基类指针相同
    cout<<p->name<<endl;
    p->study(1);
    p->work();
}
// 输出为:specialized courses
       // business internship
       // LiSi
       // ZhangSan
       // LiSi
       // ZhangSan
       // common required course
       // family education

这里就辛苦大家自己分析或debug一下吧,有了多态的基础再看抽象类就并不难哦~

8.子类的析构函数与虚析构函数

这节只做了解即可,不需要重点掌握。

普通析构函数

我们先来谈谈子类的析构函数,其实它的定义方法和基类一样,区别则在于析构函数的作用以及调用顺序。派生类实例调用析构函数时会优先调用派生类的析构函数,而后调用基类的析构函数,确保所有该释放的空间都得到释放,但是如果我们使用基类指针指向派生类,会有怎样的情况:

class Base 
{
public:
    ~Base() 
    {std::cout << "Base destructor" << std::endl;}
};

class Derived : public Base 
{
public:
    ~Derived() 
    {std::cout << "Derived destructor" << std::endl;}
};

int main() 
{
    Base *ptr1 = new Derived(); // 创建一个指向派生类的基类指针
    // ptr2->~Base(); // 析构函数不是类的成员,不可以用这样的方式调用,
                      // 它只能在适当的时候自行调用或使用点号调用
    delete ptr1; // 只会调用基类的析构函数
    Derived *ptr2=new Derived(); // 创建派生类指针指向派生类
    delete ptr2; // 先调用派生类析构函数再调用基类析构函数
    ptr1=new Base(); 
    delete ptr1; // 只会调用基类的析构函数
// 输出为:Base destructor

//        Derived destructor
//        Base destructor

//        Base destructor
}

从结果中可以看到,基类指针指向派生类的情况是不能自动调用派生类的析构函数的,这是我们不想看到的。那么有没有解决办法呢?
附:这个例子只用来展示析构函数如何调用,因为我们写的析构函数并没有释放空间的实际功能。

虚析构函数

解决上述问题需要用到多态的知识了,如前文所述,基类指针指向的不同可以调动不同的虚函数,因此如果我们设置析构函数为虚函数,那么当基类指针指向不同类时理论上就可以根据需求调用不同的虚构函数了:

class Base
{
public:
    virtual ~Base(){std::cout << "Base destructor" << std::endl;}
};
class Derived : public Base 
{
public:
    virtual ~Derived() {std::cout << "Derived destructor" << std::endl;}
};

int main() 
{
    Base* ptr1 = new Derived();
    delete ptr1;  // 这里会先调用Derived的析构函数,然后调用Base的析构函数
    ptr1=new Base();
    delete ptr1;
}
// 结果为:Derived destructor
//        Base destructor
//
//        Base destructor

这里子类和父类的析构函数都设置成虚函数是为了方便多层继承,在这里C++关于多层继承和多重继承没有需要额外补充的内容所以不做赘述。如果不考虑多层继承,可以只把基类的析构函数设置成虚函数。

总结

这两节我们盘了一下C++中面对对象编程的高级知识点,其中类指针、继承、多态希望大家可以重点掌握。说实话这些东西确实很让人头疼,但是也不用一次性全明白,熟悉一下,当我们在实际的编程任务中遇到时也许就派上用场了~

文章来源:https://blog.csdn.net/weixin_54929649/article/details/135433061
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。