c++访问修饰符_继承_赋值兼容_多态初探

本文详细解析C++中的继承概念,包括不同访问控制符的影响、赋值兼容原则及构造与析构顺序。同时介绍了多态的基础知识及其在实际工程中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


站在编译器和C的角度剖析c++原理, 用代码说话


访问修饰符

在我们引入继承之前,先来了解一些c++的访问修饰符:
private: 修饰的成员变量成员函数只能在类的内部使用.
public: 修饰的成员变量成员函数能在类的内部和外部随便使用.
protected:修饰的成员变量和成员函数只能在类的内部使用.
在进入继承之前上面的铁律一定要记住, 或许会有疑问,说protected不是能继承吗?但是在不继承的情况下就是只能在类中使用,所以这样记忆就混乱了,所以记住这三个铁律,我一步一步举例分析.

继承

面向对象中的继承指的是类之间的父子关系:
1. 子类拥有父类所有的成员变量和方法,只不过是私有的不能用而已,但是是拥有下来的.
2. 子类就是一种特殊的父类.
3. 子类对象可以当做父类的对象去使用
4. 子类可以拥有父类没有的属性和方法
对上面这几点或许会有疑问,不知道在说什么,别急,我们一个一个举例分析. 首先我们先探讨一个大块儿就是: 用不同的访问控制继承父类中不同的访问控制修饰的成员变量或方法在子类和子类外的访问情况.
这里写图片描述
那么这个表格怎么用呢?我们执行三看原则:
1. 看调用语句,这句话是写在子类的内部还是子类的外部.
2. 看子类如何从父类继承(public, private, protected)
3. 看父类中的访问级别(public, private, protected)

class A
{
private:
    int a;
protected:
    int b;
public:
    int c;
    A()
    {
        a = 0;
        b = 0;
        c = 0;
    }
    void set(int a, int b, int c)
    {
        this->a = a;
        this->b = b;
        this->c = c;
    }
};
class B : public A
{
public:
    void print()
    {
        //cout<<"a = "<<a; //err
        cout<<"b = "<<b; //ok
        cout<<"c = "<<c<<endl; //ok
    }
};
class C : protected A
{
public:
    void print()
    {
        //cout<<"a = "<<a; //err
        cout<<"b = "<<b; // ok
        cout<<"c = "<<c<<endl; //ok
    }
};
class D : private A
{
public:
    void print()
    {
        //cout<<"a = "<<a; //err
        cout<<"b = "<<b<<endl;  
        cout<<"c = "<<c<<endl;
    }
};
int main(void)
{
    A aa;
    B bb;
    C cc;
    D dd;

    aa.c = 100;  //ok

    bb.c = 100; //ok
    //cc.c = 100; //err
    //dd.c = 100;  //err

    aa.set(1, 2, 3); //ok
    bb.set(10, 20, 30); //ok
    //cc.set(40, 50, 60); //err
    //dd.set(70, 80, 90);
    bb.print();
    cc.print();
    dd.print();
    return 0;
}

来我们开始分析:
首先a在父类A中是私有属性,子类B是公有继承, 那么通过上表分析,私有属性是出了出身类都不能被使用的,所以在B中是不能使用a的,但是是继承下来的,这样的话B类之外(main内)也是肯定不能使用的.
b是保护属性,保护属性是可以被儿子使用的,父类的私有属性就像是父亲的情人,儿子能继承但是肯定不能用,父类的保护属性就像是父亲的银行卡,它是可以让儿子使用的. 这样的话,B是公有继承, 那么通过上表分析, 公有继承父类protected的属性,那么这个属性相当于是protected属性了,这样的话,出了子类就不能用了. C和D是保护和私有继承,与父类私有的出来访问控制权限是protected和private,都是出了子类不能用的. 是不是有点乱,简单点记住就是子父类之间,出了private子类不能用,剩下的父类的属性是什么都能在子类中用,不管子类是怎样继承,子类怎样的继承这样就用到那个表,只是为了得出在类的外部能不能使用.
c是父类中的公有属性, B公有继承,这样的话首先是子类肯定能用,并且公有继承后还是公有,这样类外也能用了. C是protected继承,这样的话首先是子类肯定能用,并且protected继承公有的后变为了protected, 这样的在类的外部就不能使用了. D是私有继承,首先是子类肯定能用,但是私有继承公有的后变为了私有的,这样类的外部就不能用了.

赋值兼容原则

什么是赋值兼容呢?赋值兼容有以下几个特点:
1. 子类对象可以当作父类对象使用
2. 子类对象可以直接赋值给父类对象
3. 子类对象可以直接初始化父类对象
4. 父类指针可以直接指向子类对象
5. 父类引用可以直接引用子类对象

class Parent
{
public:
    void printP()
    {
        printf("我是父亲...\n");
    }
protected:
    int a;
    int b;
};
class Child : public Parent
{
public:
    Child()
    {
        a = 0; b = 0; c= 0;
    }
    void printC()
    {
        printf("我是儿子...\n");
    }
protected:
private:
    int c;
};

我们通过上面定义了父类与继承类并且进行了公有继承.
然后我们先写一种Main函数:

int main(void)
{
    Parent p1;
    Child c1;
    Parent *base = NULL;
    base = &c1;
    base->printP();
    Parent &myp = c1;
    myp.printP();
    return 0;
}

从这里我们能够看出打印的都是”我是父亲…”, 这就是说可以把子类对象赋给基类指针或引用,这也就是赋值兼容. 我们继续拓展.
我们加两个全局方法:

void howToPrint(Parent *p)
{
    p->printP();
}
void howToPrint2(Parent &p)
{
    p.printP();
}

然后再一种main:

int main(void)
{
    Parent p1;
    Child c1;
    howToPrint(&p1);
    howToPrint(&c1);
    howToPrint2(p1);
    howToPrint2(c1);
    return 0;
}

我们也能发现打印的全是”我是父亲…”, 所以我们能够用子类对象来初始化父类对象. 这就是复制兼容性, 这是编译器规定的,我们只能照办. 还有一点就是当我们初始化子类的时候,是得先执行父类的构造函数,对其继承得来的成员进行初始化。 在子类对象析构时,需要调用父类析构函数需要对其继承得来的成员进行清理, 但是会先析构派生类. 所以先调用父类的构造函数,然后再调用子类的构造函数,这样就完成了子类的初始化. 当父类的构造函数有参数时,需要在子类的初始化列表中显示调用, 只要继承那么孩子都会从爹那里都得到,只不过是调用父类的函数初始化而已,不管属性是私有的共有的,这就是代码复用的思想.
既然谈到了继承中的构造和析构顺序, 那么我们就顺便探讨一下.

继承和组合混搭的构造和析构

继承是什么样子,肯定也了解了,但是组合是什么样子呢?一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员.创建组合类的对象:首先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对基本类型的成员进行初始化,又要对内嵌对象初始化。创建组合类的对象,构造函数的执行顺序:先构造父类,再构造成员变量、最后构造自己, 先析构自己,在析构成员变量、最后析构父类.

class Object
{
public:
    Object(const char* s)
    {
        cout<<"Object()"<<" "<<s<<endl;
    }
    ~Object()
    {
        cout<<"~Object()"<<endl;
    }
};
class Parent : public Object
{
public:
    Parent(const char* s) : Object(s)
    {
        cout<<"Parent()"<<" "<<s<<endl;
    }
    ~Parent()
    {
        cout<<"~Parent()"<<endl;
    }
};
class Child : public Parent
{
protected:
    Object o1;
    Object o2;
public:
    Child() : o2("o2"), o1("o1"), Parent("Parameter from Child!")
    {
        cout<<"Child()"<<endl;
    }
    ~Child()
    {
        cout<<"~Child()"<<endl;
    }
};
int main(int argc, char *argv[])
{
    Child child;
    return 0;
}

所以这个代码,先跳到child的构造函数,发现有继承,并且发现父亲的构造函数是带参数构造函数,所有必须在初始化列表中写入, 然后先构造父亲,(注意与构造函数初始化列表的顺序无关的,只与成员变量定义的时候的顺序有关.), 进了parent发现还有父类,就又上去,并且是带参的所以也要初始化列表显示. 指向玩object中的构造方法后,紧接着退回来执行parent自身的构造方法, 然后回到child开始执行成员变量的构造方法, 限制性o1的然后执行o2的,最后执行自身child的,然后析构的话就相反了.

重名对象

派生类定义了与基类同名的成员,在派生类中访问同名的成员时,屏蔽了基类的同名成员,如果想要在派生类中使用基类的同名成员,就得使用类名::成员

class base
{
    public:
        int a, b;
};
class drive: public base
{
    int b, c;
};
int main(void)
{
    drive d;
    d.a = 1;//访问的是从基类继承下的成员a
    d.b = 2;//访问的是子类中的成员b
    d.base::b = 3;//访问的是从基类继承下的成员b
    return 0;
}

派生类中的静态成员

基类定义的所有静态成员将被所有派生类共享

class B
{
public:
    static void Add()
    {
        i++ ;
    }
    void out() {
        cout<<"static i="<<i<<endl;
    }
     static int i;
};
int B::i=0;
class D : private B
{
public:
    void f()
    {
        i=5;
        Add();
        B::i++;
        B::Add();
    }
};
int main()
{
    B x;
    D y;
    x.Add();
    x.out();
    y.f();
    cout<<"static i="<<B::i<<endl;
    cout<<"static i="<<x.i<<endl;
    //cout<<"static i="<<y.i<<endl;
    return 0;
}

这里没什么好讲的,因为这里其实和正常的静态变量一样,cout<<"static i="<<y.i<<endl;这句话是不能执行的,因为私有继承下来后的静态成员i在B中是私有属性了. 即使是静态成员但是也要符合继承的规则. 这里需要注意的一点是int B::i=0;这句话一定要有,如果没有这句话是报错的,这是因为这句话不仅是为静态成员初始化,而且是告诉编译器为这个静态属性分配空间. 你可以试试当把父亲中的i变为私有属性的时候,底下代码会报错,但是这个代码不会.

虚基类

如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,那么对个基类中声明的名字进行访问时,就会出现二义性.
如果在多条继承路径上有一个公共的基类,那么在继承路径的某汇合处,这个公共基类就会在派生类的对象中产生多个基类子对象.
要使得这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚基类,使这个基类成为虚基类.
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

多态引入

我们回想一下上面的赋值兼容的案例,当我们的赋值兼容性原则遇上函数重写,我们用子类初始化父类还是将子类的地址或引用传入函数参数中都是执行的父类的方法,这肯定不是我们想要的效果, 我想要:如果我传一个父类对象,执行父类的print函数, 如果我传一个子类对象,执行子类的printf函数. 但是为什么会出现上面的现象呢:
1. 赋值兼容性原则遇上函数重写 出现的一个现象
2. 在编译器编译期间,我就确定了,这个函数的参数是p,是Parent类型的. 这也就是静态联编的原则. C/C++是静态编译型语言,在编译的时候,编译器主动根据指针的类型判断指向的是一个什么样的对象, 如果期望传来什么对象就执行什么对象的方法,那么就得提前告诉编译器这是个虚函数(virtual). 那么我该怎么做呢?我们看个飞机的案例:

class HeroFighter
{
public:
    virtual int Power()
    {
        return 10;
    }
};
class AdvHeroFighter : public HeroFighter
{
public:
    int Power()
    {
        return 20;
    }
protected:
private:
};
class EnemyFighter
{
public:
    int attack()
    {
        return 15;
    }
protected:
private:
};
void ObjPlay(HeroFighter *pHf, EnemyFighter *peEf)
{
    //多态的存在
    if (pHf->Power() < peEf->attack())
    {
        cout<<"英雄gg。。。"<<endl;
    }
    else
    {
        cout<<"英雄win。。。"<<endl;
    }
}
int main()
{
    HeroFighter hf;
    EnemyFighter  ef;
    AdvHeroFighter advHf;

    ObjPlay(&hf, &ef);
    ObjPlay(&advHf, &ef);
    return 0;
}

如果父类中函数加上virtual关键字,并且在子类中这个函数被重写,那儿编译器就会进行特殊处理,也就是动态链编,会根据传来的类型来判断这个对象是子类对象还是父类对象,这样就是一句话有多种表现形式,这就是多态. 一句话,有多种效果,有多种表现形态, 这个功能的就是多态.
在这里我们就引入了面向对象的三大特征: 封装,继承,多态.
封装:
突破了C语言函数的概念
继承:
代码复用…我复用原来写好的代码
多态:
多态可以使用未来, 多态是我们软件行业追寻的一个目标.
多态成立的三个条件:
1. 要有继承
2. 要有函数重写…C 虚函数
3. 要有父类指针(父类引用)指向子类对象
那么多态在工程中有什么用呢:
1. 工程中大量应用,可以做框架
2. 可以架构上,解耦合.
但是多态是则么实现的呢?它的内部细节是什么样的呢?我们在下一节进行深入探讨.


联系方式: reyren179@gmail.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值