首页 > 其他分享 >继承

继承

时间:2025-01-19 20:09:59浏览次数:1  
标签:函数 继承 成员 基类 int 派生类 public

一、什么是继承

继承是面向对象编程的一种特性,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。子类可以扩展父类的功能,也可以重写父类的方法。

继承的语法:

class 子类 : public 父类
{
    // 子类的成员
};

例子:父类Animal,子类Cat

#include<iostream>
using namespace std;

class Animal
{
public:
    int _age;
    string _name;
    Animal(){}
    Animal(int age, string name):_age(age),_name(name){}
    void SetAge(int age)
    {
        _age = age;
    }

    void SetName(string name)
    {
        _name = name;
    }
};

class Cat:public Animal
{
public:
    void EatFish()
    {
        cout << "cat eat fish" << endl;
    }

    void Sleep()
    {
        cout << "cat sleep" << endl;
    }

    void Show()
    {
        cout << "age:" <<_age << " name:" << _name << endl;
    }
};

int main()
{
    Cat cat;
    cat.SetAge(5);
    cat.SetName("Tom");
    cat.Show();
    return 0;
}

二、继承方式

分为以下三种:

  • public继承:子类可以访问父类的public和protected成员,但不能访问父类的private成员。
  • protected继承:子类可以访问父类的public和protected成员,但不能访问父类的private成员。同时,子类的成员在父类外部是不可见的。
  • private继承:子类可以访问父类的public和protected成员,但不能访问父类的private成员。同时,子类的成员在父类外部是不可见的,并且子类的成员在子类外部也是不可见的。

如果不考虑继承,protected 和 private是一样的。但是当存在继承的时候,基类中的protected成员可以再派生类中访问,而基类中的private成员不能在派生类中访问。

如图理解:
alt text
派生类B中的继承方式是public,那么基类A中的public成员在派生类B中仍然是public的,protected成员在派生类B中仍然是protected的,private成员在派生类B中是不可见的。

派生类C中的继承方式是protected,那么基类A中的public成员在派生类C中仍然是protected的,protected成员在派生类C中仍然是protected的,private成员在派生类C中是不可见的。

派生类D中的继承方式是private,那么基类A中的public成员在派生类D中仍然是private的,protected成员在派生类D中仍然是private的,private成员在派生类D中是不可见的。

使用准则

  • 继承方式中的public、protected、private,是用来指明基类成员在派生类中最高的访问权限。

  • 不管如何继承,基类中的private成员始终不能使用

  • 如果需要基类成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为public或protected。不希望使用的声明为private。

  • 如果不希望基类成员在类外通过对象访问,还能在派生类中使用,只能声明为protected。

  • 实际开发中,一般使用public。

  • 在派生类中,可以通过基类的公有成员访问基类的私有成员。

    class A
    {
        public:
            A(){}
            A(int a, int b, int c):A_a(a), A_b(b), A_c(c){}
            int A_a = 10;
            void Show()
            {
                cout << "_b = " << A_b << endl;
                cout << "_c = " << A_c << endl;
            }
        protected:
            int A_b = 20;
        private:
            int A_c = 30;
    };
    
    class B:public A
    {
    public:
        B(){}
    };
    
    int main()
    {   
        B b;
        b.Show();       //_b = 20 _c = 30
        return 0;
    }
    
  • 使用using 关键字,可以改变基类成员在派生类中的访问权限。(只能改变基类中public 和 protected成员的访问权限,不能改变private的访问权限,因为基类中private成员在派生类中是不可见的,根本不能使用)。

    class B:public A
    {
    public:
        using A::A_b;       //将A中的被保护类型更改为公有类型
    protected:
        using A::A_a;       //将A中的公有类型更改为被保护类型
    };
    

三、继承的对象模型

  1. 创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。

  2. 销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。

  3. 创建派生类对象时只会申请一次内存,派生类对象包含了基类对象的内存空间,this指针也是同样。

  4. 创建派生类对象时,先初始化基类对象,再初始化派生类对象。

  5. 对派生类对象用sizeof得到的是基类所有成员(包括私有成员)+ 派生类所有成员的大小。

    #include <iostream>
    using namespace std;
    
    void* operator new(size_t size)
    {
        void *ptr = malloc(size);
        cout << "申请到的内存的地址为:" << ptr << " 大小为:" << size <<endl;
        return ptr;
    }
    
    void operator delete(void *ptr)
    {
        if(ptr == nullptr)  return;
        cout << "释放的内存的地址为:" << ptr <<endl;
        free(ptr);
    }
    
    class Base 
    {
    public:
        Base() 
        {
            cout << "A的this指针是:" << this << endl;
            cout << "A中的_a的地址为:" << &_a << endl;
            cout << "A中的_b的地址为:" << &_b << endl;
            cout << "A中的_c的地址为:" << &_c << endl;
        }
        ~Base()
        {
            cout << "~Base()" << endl;
        }
        int _a = 10;
    protected:
        int _b = 20;
    private:
        int _c = 30;
    };
    
    class Derived : public Base 
    {
    public:
        int _d = 40;
        Derived()
        {
            cout << "B的this指针是:" << this << endl;
            cout << "B中的_a的地址为:" << &_a << endl;
            cout << "B中的_b的地址为:" << &_b << endl;
            //cout << "B中的_c的地址为:" << &_c << endl;
            cout << "B中的_d的地址为:" << &_d << endl;
        }
        ~Derived()
        {
            cout << "~Derived()" << endl;
        }
    };
    
    int main() 
    {
        cout << "基类的大小:" << sizeof(Base) << endl;
        cout << "派生类的大小:" << sizeof(Derived) << endl;
    
        Derived *d = new Derived();
        delete d;
        return 0;
    }
    

    alt text

    从申请的内存大小可以得知,当创建派生类对象时,系统会预先分配一块 基类 + 派生类 大小的内存空间。然后先调用基类的构造函数初始化基类成员,再调用派生类的构造函数初始化派生类成员。

    所以this指针指向的是同一个地址,其他的成员变量也是一个地址。

  6. 在C++中,不同的继承方式的访问权限只是语法上的处理。使用指针可以绕过继承方式,直接访问基类成员。

    void* operator new(size_t size)
    {
        void *ptr = malloc(size);
        return ptr;
    }
    
    void operator delete(void *ptr)
    {
        if(ptr == nullptr)  return;
        free(ptr);
    }
    
    class Base 
    {
    public:
        Base() 
        {
        }
        ~Base()
        {
    
        }
        void fun()
        {
            cout << "_a = " << _a << " _b = " << _b << " _c = " << _c << endl;
        }
        int _a = 10;
    protected:
        int _b = 20;
    private:
        int _c = 30;
    };
    
    class Derived : public Base 
    {
    public:
        int _d = 40;
        Derived()
        {
        }
        ~Derived()
        {
            
        }
        void fun1()
        {
            cout << "_d = " << _d << endl;
        }
    };
    
    int main() 
    {
        Derived *d = new Derived();
        d->fun();           //_a = 10 _b = 20 _c = 30
        d->fun1();          //_d = 40
    
        memset(d, 0, sizeof(Derived));
        d->fun();           //_a = 0 _b = 0 _c = 0
        d->fun1();          //_d = 0
    
        return 0;
    }
    

    alt text

    此处将基类中private变量_c也被清0,我们直接操作内存,可以绕过继承方式,直接访问基类成员。memset()函数一般不用于类对象,因为类对象中可能存在虚函数,虚函数表指针等,如果直接使用memset()函数,可能会破坏这些内容,导致程序崩溃。

    还可以使用指针来修改基类中的私有成员:

        int main() 
    {
        Derived *d = new Derived();
        d->fun();
        d->fun1();
    
        *((int*)d + 2) = 31;        //相当于基类中的_c
        d->fun();
        d->fun1();
        
        return 0;
    }
    

四、如何构造基类和派生类

  1. 如果没有指定基类的构造函数,将使用基类默认构造函数。

  2. 可以使用初始化列表指明要使用的基类构造函数。

  3. 基类构造函数负责初始化被继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。

    class A
    {
    public:
        int _a;
    
    private:
        int _b;
    
    public:
        A(): _a(0), _b(0)
        {
            cout << "调用了基类的无参构造函数" << endl;
        }
    
        A(int a, int b): _a(a), _b(b)
        {
            cout << "调用了基类的带参构造函数" << endl;
        }
    
        A(const A &a): _a(a._a), _b(a._b)
        {
            cout << "调用了基类的拷贝构造函数" << endl;
        }
        void showA()
        {
            cout << "_a = " << _a << "_b = " << _b << endl;
        }
    };
    
    class B : public A
    {
    public:
        int _c;
        B(): _c(0) ,A()     //派生类的默认构造函数,指明使用基类的构造函数
        {
            cout << "调用了派生类的无参构造函数" << endl;
        }
        B(int a, int b, int c): A(a, b), _c(c)
        {
            cout << "调用了派生类的带参构造函数" << endl;
        }
        B(const A &a, int c): A(a), _c(c)
        {
            cout << "调用了派生类的拷贝构造函数" << endl;
        }
        void ShowB()
        {
            cout << "_c = " << _c << endl;
        }
    };
    
    int main()
    {
        //基类的成员变量由基类的构造函数初始化
        //派生类新增的成员变量由派生类的构造函数初始化
    
        //为什么不能由派生类初始化全部的成员变量?
        //因为派生类无法看见基类私有的成员变量
    
        //如果将初始化基类成员的代码写在派生类
        //当基类被多个派生类继承的时候,每个派生类都需要初始化基类的成员。
    
    
        B b1;               //创建对象b1,调用基类的构造函数
        b1.showA();
        b1.ShowB();
    
        B b2(1, 2, 3);      //创建对象b2,调用基类的带参构造函数
        b2.showA();
    
        A a(10,20);
        B b3(a, 30);         //创建对象b3,调用基类的拷贝构造函数
        b3.showA();
        b3.ShowB();
        
        return 0;
    }
    
  4. 基类

五、名字遮蔽和类作用域

1、名字遮蔽

  1. 在派生类中,如果基类和派生类中有同名的成员(包括成员变量和成员函数),那么在派生类中访问该成员时,默认访问的是派生类中的成员。

    #include<iostream>
    using namespace std;
    
    class A
    {
    public:
        int _a = 10;
        void func()
        {
            cout << "funcA: " << _a << endl;
        }
    };
    
    class B : public A
    {
    public:
        int _a = 88;
        void func()
        {
            cout << "funcB: " << _a << endl;
        }
    };
    
    int main()
    {
        B b;
        b.func();   //调用类B的成员函数,输出:funcB: 88
        return 0;
    }
    

    alt text

    此处派生类B的大小为8,里面既有基类A中的成员变量_a,也有派生类B中的成员变量_a,但是派生类B中的成员变量_a(仅从语法上)覆盖了A中的_a。

    2)注意:基类的成员函数和派生类的成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数。

    class A
    {
    public:
        int _a = 10;
        void func()
        {
            cout << "funcA: " << _a << endl;
        }
        void func(int a)
        {
            cout << "funcA(int a): " << _a << endl;
        }
    };
    
    class B: public A
    {
    public:
        int _a = 88;
        //void func()
        //{
        //    cout << "funcB: " << _a << endl;
        //}
    };
    
    int main()
    {
        B b;
        b.func();   //调用类B的成员函数,输出:funcB: 88
        b.func(99); //调用失败,函数func()不接受一个参数,当将B类中的func()函数注释掉,通过
        return 0;
    }
    

    此处派生类中的func(),将基类中同名的所有func()全部都隐藏。

2、类作用域

  1. 在类作用域外,普通成员只能通过对象(对象本身,对象指针,对象引用)来访问,
    静态成员可以通过对象访问,也可以通过类访问。
    class A
    {
    public:
        int _a = 10;
        void func()
        {
            cout << "funcA: " << _a << endl;
        }
        void func(int a)
        {
            cout << "funcA(int a): " << _a << endl;
        }
        static void func2()
        {
            cout << "func2A: " << endl;
        }
    };
    
    int main()
    {
        //A::func();        //报错,普通成员函数只能通过对象来访问
        A::func2();         
        //通过,静态成员函数可以通过对象也可以通过类来访问
    }
    
  2. 当存在继承关系时,如果成员在派生类的作用域中找到了,就不会去基类的作用域中查找;
    如果没有找到,再去基类的作用域中寻找。
    #include<iostream>
    using namespace std;
    
    class A
    {
    public:
        int _a = 0;
        void func()
        {
            cout << "A::func()" << endl;
        }
    };
    
    class B : public A
    {
    public:
        int _a = 10;
        void func()
        {
            cout << "B::func()" << endl;
        }
    };
    
    class C : public B
    {
    public:
        int _a = 20;
        void func()
        {
            cout << "C::func()" << endl;
        }
    };
    
    int main()
    {
        C c;
        cout << c._a << endl;           //0
        cout << c.B::_a << endl;        //10
        cout << c.B::A::_a << endl;     //20
        c.func();                      //C::func()  
        c.B::func();                   //B::func()
        c.B::A::func();                //A::func()
    }
    

六、继承的特殊关系

派生类和基类之间有一些特殊的关系

  1. 如果继承方式是公有的,派生类对象可以使用基类成员。
  2. 可以把派生类对象赋值给基类对象(包括私有成员),但是,会舍弃非基类成员。
    #include<iostream>
    using namespace std;
    
    class A
    {
    public:
        int _a = 0;
        void fun()
        {
            cout << "A::fun():" << "_a = "<< _a << " _b = " << _b <<endl;
        }
        void set(int b)
        {
            _b = b;
        }
    private:
        int _b = 0;
    };
    
    class B : public A
    {
    public:
        int _c = 3;
        void fun()
        {
            cout << "B::fun():" << "_a = " << _a << " _c = " << _c << endl;
        }
    };
    
    int main()
    {
        A a;
        B b;
    
        b._a = 10;
        b.set(20);  //设置_b的值
        b._c = 30;
    
        a.fun();    //A::fun():_a = 0 _b = 0
        a = b;      //将派生类对象赋值给基类对象
        a.fun();    //A::fun():_a = 10 _b = 20
        return 0;
    }  
    
  3. 基类指针可以在不进行显式转换的情况下指向派生类对象。
    //基类指针指向派生类对象
    //基类指针 只能调用基类的成员函数,访问基类的成员变量
  4. 基类引用可以在不进行显式转换的情况下引用派生类对象。

七、多继承和虚继承

不提倡使用多继承,只有比较简单和不出现二义性的时候才使用,能用单一继承切记不要使用多继承。

1、多继承的语法:

class 派生类名 :[继承方式] 基类名1, [继承方式] 基类名2, ...
{
    派生类新增的成员
};
class A
{
public:
    int _a = 10;
};

class B
{
public:
    int _b = 20;
};

class C : public A, public B
{
public:
    int _c = 30;
};

int main()
{
    C c;
    cout <<"_a = " << c._a << " _b = " << c._b << " _c = " << c._c << endl;
    return 0;
}

2、菱形继承

alt text

派生类D的内存:

alt text

可以发现有两个A类成员,两个A类成员的内存是重叠的,所以菱形继承会导致二义性(可以加类名和作用域解析符解决)和数据冗余

int main()
{
    D d;
    cout << d._a  << endl;  //二义性,编译器不知道访问的是哪个A类的_a
    //cout << d.A::_a << endl;   //访问A类的_a
    //cout << d.B::_a << endl;  //访问B类的_a
	return 0;
}

3、虚继承

虚继承的作用:解决菱形继承问题,使得在派生类中只保留一份基类的成员,解决二义性。
虚继承的语法:

class 派生类名 : virtual 继承方式 基类名
{
    派生类新增的成员
};

D类通过虚继承从B类和C类中继承了A类,确保了A类在D类中只有一个实例。

int main()
{
    D d;
    cout << "B中的_a地址:" << &(d.B::_a) << endl;
    cout << "C中的_a地址:" << &(d.C::_a) << endl;
	return 0;
}

他们的地址也是同一个地址:

alt text

由于消除了二义性,可以直接写成这样

D d;
cout << d._a << endl;

类D的内存分布,发现只分配了一次类A,B和C中都有vbptr。

alt text

A在被虚继承后,就变成了虚基类,虚基类在派生类中只会存在一份,且在派生类中只存在一份。vbptr指向虚基类表vbtable。

虚基类表存放了数据成员的相对位置。

在D中,B的虚基类指针相对位置是0,所以下面的8+0 = 8,就找到了_a。 C的虚指针相对位置是4,虚基类表中的数据成员位置是4,所以也找到了_a。

八、类的多态

1、什么是多态

基类指针只能调用基类的成员函数,不能调用派生类的成员函数。
如果将基类 成员函数 声明为虚函数virtual,基类指针就可以调用派生类中同名的成员函数,通过派生类的同名成员函数,
可以访问派生对象的成员变量。

有了虚函数,基类指针指向基类对象就使用基类的成员函数和数据,指向派生类对象的时候就使用派生类的成员和数据,
基类指针表现出了多种形式,这种情况被称为多态。

基类引用也可以使用多态。

#include<iostream>
using namespace std;

class Animal
{
public:
    int _age;
    Animal(int age):_age(age){}
    virtual void show()
    {
        cout << "Animal age:" << _age << endl;
    }
};

class Dog : public Animal
{
public:
    int _weight;
    Dog(int age, int weight):Animal(age),_weight(weight){}
    void show()
    {
        cout << "Dog age:" << _age << endl;
        cout << "Dog weight:" << _weight << endl;
    }
};

int main()
{
    Dog dog(3, 10);
    dog.show();
    //基类指针指向派生类对象
    //基类指针 只能调用基类的成员函数,访问基类的成员变量
    Animal* animal = &dog;
    animal->show();


    //再将基类成员show函数改为虚函数,当用基类指针指向派生类对象,会调用派生类中的同名成员函数
    return 0;
}
int main()
{
    Animal a; a._age = 100;                //创建基类对象并对成员赋值
    Dog d; d._age = 10; d._weight = 20;    //创建派生类对象并对成员赋值

    Animal* p;          //声明基类指针
    p = &a; p->show();  //让基类指针指向基类对象,并调用虚函数
    p = &d; p->show();  //让基类指针指向派生类对象,并调用虚函数

    //证明了:有了虚函数,
    //基类指针指向基类对象的时候就使用基类的成员函数和数据
    //基类指针指向派生类的时候就使用派生类的成员函数和数据
}
//将基类中的成员函数show函数改为虚函数
int main()
{
   Dog dog(3, 10);
   Animal a(100);
   cout << "基类引用指向基类对象" << endl;
   Animal &ra = a;
   ra.show();
   cout << "基类引用指向派生类对象" << endl;
   Animal &ra2 = dog;
   ra2.show();
}

alt text

2、注意:

  1. 只需在基类的函数声明中加上virtual关键字,函数定义时不能加。
  2. 在派生类中重定义虚函数时,函数特征要相同。
  3. 在基类中定义了虚函数时,如果派生类没有重定义该函数,那么将使用基类的虚函数。

仅给派生类重载了一个show函数,基类没有重载。

//2
int main()
{
   Dog dog(3, 10);
   Animal a;
   Animal *pa = &dog;       //基类指针指向派生类
   pa->show();              //调用派生类重写的函数

   //当将派生类的show函数在类中加上参数
   //void show(int)...
   //pa->show();  //调用基类的show函数
}

给派生类重载一个带参数的show函数,将基类的show函数改为虚函数。

class Animal
{
public:
    int _age;
    Animal(){}
    Animal(int age):_age(age){}
    virtual void show()
    {
        cout << "Animal age:" << _age << endl;
    }
};

class Dog : public Animal
{
public:
    int _weight;
    Dog(){}
    Dog(int age, int weight):Animal(age),_weight(weight){}
    void show()
    {
        cout << "Dog age:" << _age << endl;
        cout << "Dog weight:" << _weight << endl;
    }
    void show(int a)
    {
        cout << "带参数的show函数" << endl;
    }
};

int main()
{
   Dog dog(3, 10);
   Animal a;
   Animal *pa = &dog;       //基类指针指向派生类
   pa->show();              //调用派生类重写的函数

   //pa->show(1);             //报错
   //对于基类指针,不认识派生类中的重载函数。
}

再给基类也重载一个show,并修改为虚函数。

class Animal
{
public:
    int _age;
    Animal(){}
    Animal(int age):_age(age){}
    virtual void show()
    {
        cout << "Animal age:" << _age << endl;
    }
    virtual void show(int a)
    {
        cout << "基类带参数的show函数" << endl;
    }
};

class Dog : public Animal
{
public:
    int _weight;
    Dog(){}
    Dog(int age, int weight):Animal(age),_weight(weight){}
    void show()
    {
        cout << "Dog age:" << _age << endl;
        cout << "Dog weight:" << _weight << endl;
    }
    void show(int a)
    {
        cout << "派生类带参数的show函数" << endl;
    }
};

int main()
{
   Dog dog(3, 10);
   Animal a;
   Animal *pa = &dog;       //基类指针指向派生类
   pa->show();              //调用派生类重写的函数

   //给基类也重载一个show的虚函数
   pa->show(1);             
   //调用派生类的重载show函数。
}
  1. 在派生类中重定义了虚函数的情况下,如果想使用基类的虚函数,可以加类名和作用域解析符。
//和上面的类不变
int main()
{
   Dog dog(3, 10);
   Animal a;
   Animal *pa = &dog;       //基类指针指向派生类
   pa->show();              //调用派生类重写的函数

   //给基类也重载一个show的虚函数
   pa->show(1);             
   //调用派生类的重载show函数。

   pa->Animal::show(1);      //调用基类的show函数
}
  1. 如果要在派生类中重新定义基类的函数,则将它设置为虚函数;
    否则不要再设置虚函数。

九、多态的应用场景

现实:

网盘的基础功能免费,高级功能需要收费

程序员:

基类的虚函数实现基本功能
派生类重定义虚函数,扩展功能、提升性能
实现个性化功能

一个简单的例子,当不适用多态和虚函数时候:

#include<iostream>
using namespace std;

class Hero
{
public:
    int viability;
    int attack;

    void skill1()
    {
        cout << "Hero skill1" << endl;
    }

    void skill2()
    {
        cout << "Hero skill2" << endl;
    }

    void useSkill()
    {
        cout << "Hero useSkill" << endl;
    }
};

class JS: public Hero       //剑圣英雄
{
public:
    void skill1()
    {
        cout << "JS skill1" << endl;
    }

    void skill2()
    {
        cout << "JS skill2" << endl;
    }

    void useSkill()
    {
        cout << "JS useSkill" << endl;
    }
};

class DM : public Hero       
{
public:
    void skill1()
    {
        cout << "DM skill1" << endl;
    }

    void skill2()
    {
        cout << "DM skill2" << endl;
    }

    void useSkill()
    {
        cout << "DM useSkill" << endl;
    }
};

int main()
{
    int id = 0;
    cout << "请输入选择英雄:1、剑圣;2、德玛" << endl;
    cin >> id;

    if(id == 1)
    {
        JS js;
        js.skill1();
        js.skill2();
        js.useSkill();
    }
    else if(id == 2)
    {
        DM dm;
        dm.skill1();
        dm.skill2();
        dm.useSkill();
    }

    return 0;
}

引入多态和虚函数,将基类三个函数设置为虚函数,创建基类指针指向派生类即可

class Hero
{
public:
    int viability;
    int attack;

    virtual void skill1()
    {
        cout << "Hero skill1" << endl;
    }

    virtual void skill2()
    {
        cout << "Hero skill2" << endl;
    }

    virtual void useSkill()
    {
        cout << "Hero useSkill" << endl;
    }
};
...
int main()
{
    int id = 0;
    cout << "请输入选择英雄:1、剑圣;2、德玛" << endl;
    cin >> id;

    Hero *hero = nullptr;   //创建一个基类指针

    if(id == 1)
    {
        hero = new JS();   //需要谁就new谁
    }
    else if(id == 2)
    {
        hero = new DM();    //需要谁就new谁
    }

    if(hero != nullptr)     //如果创建成功,则调用函数
    {
        hero->skill1();     //基类指针指向派生类,并且派生类成员函数是虚函数,会调用派生类函数
        hero->skill2();
        hero->useSkill();
    }

    return 0;

}

十、多态的对象模型

cl main.cpp /d1 reportSingleClassLayoutHero

基类内存空间:

alt text

程序在运行的时候,如果创建了对象,除了给对象的成员分配空间,还会创建一个虚函数表。用虚函数指针指向虚函数表。在程序中,如果调用的是普通成员函数,则直接调用;如果调用的是虚函数,要先通过虚函数指针找到虚函数表,查找虚函数表得到函数的地址,再调用函数。

派生类内存空间:

cl main.cpp /d1 reportSingleClassLayoutJS

alt text

派生类也有虚函数指针,是从基类继承过来的。如果基类有虚函数,派生类会从基类
多继承一个虚函数指针和虚函数表。这样设计师让基类和派生类保持相同的内存模型。

在运行时,如果创建了派生类对象,在虚函数表中,会用派生类成员函数取代基类成员函数的地址。

当基类指针指向派生类对象时,用基类指针调用虚函数的时候,
会调用的基类函数,但是函数地址已经被派生类替换了,所以会调用派生类函数。

十一、如何析构派生类

当用基类指针指向派生类对象时,delete基类指针只会调用基类的析构函数,不会调用派生类的析构函数。如果希望调用派生类的析构函数,就要把基类的析构函数设置为虚函数,这时派生类的析构函数被调用之后,会自动调用基类的析构函数。

如果手工调用派生类的析构函数,也会自动调用基类的析构函数。

构造函数、析构函数、赋值运算符函数、友元函数都不能继承。

析构函数可以手工调用,delete空指针安全,但是delete野指针可能会程序崩溃,如果对象有对空间需要加上ptr = nullptr;

对于基类来说,即使不需要析构函数,也应该提供一个空虚析构函数。

delete ptr;
ptr= nullptr

1、

#include<iostream>
using namespace std;

class Base
{
public:
    Base(){cout << "Base()" << endl;}
    virtual void show(){cout << "Base::show()" << endl;}
    ~Base(){cout << "~Base()" << endl;}
};

class Derived:public Base
{
public:
    Derived(){cout << "Derived()" << endl;}
    void show(){cout << "Derived::show()" << endl;}
    ~Derived(){cout << "~Derived()" << endl;}
};

int main()
{
    Derived d;
    return 0;
}

创建派生类对象,先调用Base的构造,在调用Derived的构造。析构的时候,先调用Derived的析构,再调用Base的析构。

2、创建一个派生类指针,new一个派生类对象。

int main()
{
    Derived *d = new Derived();
    return 0;
}

这样的代码会导致内存泄漏,因为您使用了 new 关键字动态分配了内存,但没有使用 delete 关键字来释放这块内存。

alt text

int main()
{
    Derived *d = new Derived();
    delete d;
    return 0;
}

要将new 和 delete 结合起来使用,以确保在不再需要对象时正确释放内存。

alt text

3、手动析构

int main()
{
    Derived *d = new Derived();
    d->~Derived();
    delete d;
    return 0;
}

通过派生类指针调用派生类的析构函数。

C++规定派生类的析构函数调用完毕,会自动执行基类的析构函数。

alt text

3.1 基类指针指向派生类对象:

int main()
{
    Base *d = new Derived();        
    delete d;
}

发现只调用了基类的析构函数,没有调用派生类的构造函数。

alt text

基类内存模型: 只有一个虚指针

alt text

派生类内存模型:有一个虚指针,虚函数表中有一个show函数

alt text

解决方式:将基类的析构函数设置为虚函数。

class Base
{
public:
    Base(){cout << "Base()" << endl;}
    virtual void show(){cout << "Base::show()" << endl;}
    virtual ~Base(){cout << "~Base()" << endl;}
};

class Derived:public Base
{
public:
    Derived(){cout << "Derived()" << endl;}
    void show(){cout << "Derived::show()" << endl;}
    ~Derived(){cout << "~Derived()" << endl;}
};

int main()
{
    Base *d = new Derived();        
    delete d;
}

alt text

基类内存模型:
虚函数表中多了一个函数,这个是析构函数(destructor)的缩写

alt text

派生类内存模型:
派生类中重定义基类的虚构函数,它们的函数名和参数列表要相同。但是基类的析构函数和派生类的析构函数不可能名字相同。这时C++编译器会做特殊的处理。

alt text

标签:函数,继承,成员,基类,int,派生类,public
From: https://www.cnblogs.com/baobaobashi/p/18679882

相关文章

  • 第10篇:从入门到精通:深入理解Python继承与多态的概念及应用
    第10篇:继承与多态内容简介本篇文章将深入探讨Python中的继承与多态概念。您将学习如何通过类的继承实现代码的重用,掌握方法重写的技巧,了解如何使用super()函数调用父类的方法,并探索多态的实现与应用。通过丰富的代码示例,您将能够熟练运用继承与多态,提升您的面向对象编程(OO......
  • 在C语言中实现封装、继承和多态
    在C语言中,没有直接的支持封装、继承和多态等面向对象特性。C语言是结构化编程语言,通常通过函数和数据结构(结构体)来实现类似的功能。我们可以通过结构体、函数指针、和手动管理的对象生命周期来模拟C++中的面向对象的特性。下面我们将逐一讨论如何在C语言中实现封装、继承和多......
  • CSS 选择器优先级与继承规则详解
    CSS选择器优先级与继承规则详解在编写CSS时,理解选择器的优先级和继承规则是至关重要的。它们决定了样式如何应用到HTML元素上,尤其是在多个样式规则冲突时。本文将详细介绍CSS选择器的优先级和继承规则,帮助你更好地掌握样式的应用机制。CSS选择器优先级CSS选择器的优......
  • 继承
    继承继承的概述:父类怎么形成的:我们的定义了多个类,发现这些类中有很多重复性的代码,我们就定义了一个父类,将相同的代码抽取出来放到父类中,其他的类直接继承这个父类,就可以直接使用父类中的内容了怎么去继承:extends子类extends父类继承的特点:1.继承只支持单继......
  • 陪玩系统源码,继承和混入的区别
    混入@mixinblock{.a{width:96%;margin-left:2%;border-radius:10px;border:1pxsolid#333;}}.container{@includeblock;} 转化为:.container.a{width:96%;margin-left:2%;border-r......
  • 【c++继承篇】--继承之道:在C++的世界中编织血脉与传承
    目录引言一、定义二、继承定义格式2.1定义格式2.2继承关系和访问限定符2.3继承后子类访问权限三、基类和派生类赋值转换四、继承的作用域4.1同名变量4.2同名函数五、派生类的默认成员构造函数5.1**构造函数调用顺序:**5.2**析构函数调用顺序:**5.3调用关系引言......
  • Java初学者笔记-01、封装继承多态
    封装:封装是指隐藏对象的属性和实现细节,仅对外提供公共访问方式。通过封装,可以将类的信息隐藏在类内部,只暴露对外的接口(如setter和getter方法),从而提高代码的安全性和可维护性。继承:继承是从已有的类中派生出新的类的过程。新的类(子类)能够吸收已有类(父类)的数据属性和行为,并且可以......
  • GaussDB云原生数据库SQL引擎继承原来openGauss的词法解析,语法解析,查询重写,查询优化和
    云原生数据库SQL引擎继承原来openGauss的词法解析,语法解析,查询重写,查询优化和执行引擎的能力。由于云原生数据库是shareddisk架构,一个事务在一个节点上执行,所以不需要原来分布式根据分布式key进行数据分布,分布式执行和分布式2PC提交的能力。为了支持数据库粒度的异地多活,云原生......
  • java面向对象继承
    1Java中的继承概念继承是面向对象编程(OOP)中的一个核心概念。在Java中,继承指的是一个类(子类)通过扩展(extends)另一个类(父类)来获得父类的属性和方法。继承有助于实现代码重用和扩展,也为多态性提供基础。继承使得子类能够拥有父类的所有非私有成员(字段、方法),同时子类还可以......
  • GaussDB云原生数据库SQL引擎继承原来openGauss的词法解析,语法解析,查询重写,查询优化和
    云原生数据库SQL引擎继承原来openGauss的词法解析,语法解析,查询重写,查询优化和执行引擎的能力。由于云原生数据库是shareddisk架构,一个事务在一个节点上执行,所以不需要原来分布式根据分布式key进行数据分布,分布式执行和分布式2PC提交的能力。为了支持数据库粒度的异地多活,云原生......