C++和Python类继承中的成员隐藏和覆盖特点比较

之前在看一篇有关C++ static关键字的总结的文章的时候,这篇文章谈到在派生类会继承基类的静态成员变量,并与之共享。可以在派生类中定义重名的成员变量来屏蔽派生类对基类静态成员变量的影响。 对这句话蛮疑惑的,然后敲了一段代码试了一下。

using namespace std;
#include <iostream>
class A {
public:
    static int x;
    int y = 1;  //给成员变量设置默认值,会在接下来的代码中用到
    void g() {
        ++x;
    }
};
class B :public A {
public:
    static int x;
    int y = 1;
    void f() {
        ++x;
    }
};

int A::x = 1;
int B::x = 1;

int main() {
    A a;B b;
    a.g(),b.f(),b.f();
    cout << A::x << " " << B::x << endl;
    
    return 0;
}

输出结果是 2 3,从表面上看起来,这样做确实把派生类对子类静态成员变量的影响屏蔽掉了。但是问题是,如果一方面在派生类中重写子类的成员,另一方面又希望调用子类的成员,那应该怎么做呢?
如果是静态成员变量的话,那很简单,以上面的代码为例,只需要加上作用域说明符就可以了。

void k() {
        (*this).A::x+=10;  //这是新增加的B类成员函数
    }
A a;B b;
    a.g(),b.f(),b.f(),b.k(); //在main函数中增加对 k这个成员函数的调用
    cout << A::x << " " << B::x << endl;

输出结果是 12 3。当然这个例子可能不足以解决问题,因为静态成员变量本来就可以不通过对象直接访问(看起来上面的this指针使用也很多余)
在上面的代码中,我们在A类和B类中都定义了整型变量y,接下来增加对y变量修改的函数。

void u() { y += 10; }  //这是A类新增加的成员函数
void i_A() { A::y += 20; }    //这是B类新增加的成员函数
void i_B() { ++y; }
int main() {
    A a;B b;   //新的main函数
    a.u(), b.i_A(), b.i_B();
    cout << a.y <<" "<< b.y <<" " << b.A::y << endl;
    return 0;
}

输出的结果是 11 2 21,a.y = 11是自然的,b.y和b.A::y的输出结果就能说明一些问题了:在继承中对于派生类和基类重名的成员变量,在派生类中都会保留,只是在访问时默认访问派生类的成员变量,如果需要访问基类的成员变量,只需要通过作用域说明符显式地指出就可以了。
成员函数也是类似的,子类和派生类一旦有重名的成员函数(不论参数列表是否相同),派生类对这个名字的成员函数的调用就默认是调用派生类的成员函数,除非通过作用域说明符显式指出。
下面这段代码是从别人那copy过来的,自己懒得敲了…

#include <iostream>
using namespace std;
class CBase
{
public:
    void  my(int a,int b){
        cout << "父类" << endl;
    }
};
class CDerivedA : public CBase
{
public:
    void my(int a ){
        cout << "子类" << endl;//参数个数不同
    }
};
int main()
{
    CDerivedA ptr;
    ptr.my(5);
    ptr.my(5,5);  // 试图调用基类的成员函数
    system("pause");
    return 0;
}

虽然两个成员函数的参数列表不同,也许编译器会认为是函数重载,但实际上这段代码编译过不了,不能直接调用基类的重名成员函数(顺便一提,用的编译器是 VS 2019)
但是在调用时加上作用域说明符,编译就能够通过了

ptr.CBase::my(5,5);

再看下面的代码

using namespace std;
#include <iostream>
class A {
public:
    void uu() {
        this->kk();
    }
    void kk() {
        cout << "我是基类" << endl;
    }
};
class B :public A {
public:
    void kk() {
        cout << "我是子类" << endl;
    }
};

int main() {
    A a;B b;
    b.uu();
    return 0;
}

输出结果是: 我是基类
这个结果蛮令我惊讶的(也许是因为之前一直写的python的缘故),我又在pycharm上敲了一段类似的python程序

class A:
    def ff(self):
        self.gg()

    def gg(self):
        print("hhh")

class B(A):
    def gg(self):
        print("nnn")

b = B()
b.ff()

有意思的事情是,在pycharm上的输出结果是 nnn
这说明C++的继承机制和python的继承机制不大一样(然而我也并不懂这些底层的机制)
目前可以做一个粗浅的理解是:C++的派生类对象的内存地址中会先将基类的所有成员变量都拷贝过来,然后再接下来的内存地址中存放自己独有的成员变量。对于重名的成员函数和成员变量,编译器会有相应的机制区分它们,默认调用派生类的成员变量和成员函数,但也可以通过作用域的说明符显式地调用基类的成员函数和成员变量。
再看一个python继承的例子:

class A:
    a = 1

    def ff(self):
        A.a += 1

    def gg(self):
        A.a += 1


class B(A):

    def gg(self):
        B.a += 10


b = B()
d = A()
b.gg()
d.gg()
d.ff()
b.ff()
print(f"{d.a},{b.a}")

输出结果是 4,11
这表明B类在继承A类的属性a时,实际上把A,B的属性a给区分开了,而不是像C++那样派生类和基类共享一个静态成员变量(这里这样比较是因为我觉得python的类属性和C++的静态成员变量很相似,比如都是可以通过实例访问,也都可以直接通过类名进行访问)
再看一个例子:

class A:
    def __init__(self):
        self.a = 1
        self.b = 2

    def ff(self):
        self.a += 1
        self.b += 10

    def gg(self):
        print("hhh")


class B(A):
    def __init__(self):
        A.__init__(self)  # 调用A类的构造函数
        self.c = 3
        self.a = 40

    def gg(self):
        print(f"{self.a},{self.b},{self.c}")


b = B()
b.ff()
b.ff()
b.gg()
A.gg(b)

输出结果是 42,22,3 \n hhh
于是乎,关于python继承机制的理解(虽然也很粗浅):子类把父类的属性(强调这里是类属性)和方法拿过去之后,就直接当成自己的东西了,不再区分某个方法是否是继承而来的或者说是自己额外增加的
如果希望调用父类的方法而子类中又有重名函数的话,就需要像上面的代码中那样手动传入self参数(上面代码中调用父类的构造函数,父类的gg函数就是这样做的,所以前面继承的时候强调是继承类的属性是因为python中不会默认调用父类的构造函数(虽然C++是这么处理的),需要自己手动调用,如果没有调用父类构造函数的话,上面代码的B类的gg函数输出self.b就会报错了)

总结一下,可以形象地形容这两种类型的继承:就好像一只狼(基类)想要变成一只羊(派生类),C++的做法是让这只狼披上一层羊皮,虽然外观看起来这确实是一只羊,但实际上能够分辨出来狼的部分和羊的部分。而python的做法则是让这只狼去转基因,把羊的特点都注入进来,然后就区分不开狼的部分和羊的部分。

再回头看这两段程序

using namespace std;
#include <iostream>
class A {
public:
    void uu() {
        this->kk();
    }
    void kk() {
        cout << "我是基类" << endl;
    }
};
class B :public A {
public:
    void kk() {
        cout << "我是子类" << endl;
    }
};

int main() {
    A a;B b;
    b.uu();
    return 0;
}

当b对象调用uu函数的时候,uu函数清楚地知道自己是基类函数(是一只狼),因此这个this指针也可以理解为一个指向派生类的基类指针,既然是一个基类指针,那么调用kk函数时自然就会选择调用基类的kk函数,因此输出的是
我是基类

class A:
    def ff(self):
        self.gg()

    def gg(self):
        print("hhh")

class B(A):
    def gg(self):
        print("nnn")

b = B()
b.ff()

到了python的情况,调用ff的时候这个ff就不知道自己是基类继承而来的函数了,传入的self是B类对象,按着指示就调用了self.gg(),因此最后调用的是B类的gg函数,输出为 nnn

补充说一下虚函数的情况,如果上面C++程序的this指针是指向派生类的基类指针这一理解正确的话,如果gg函数是虚函数,那么这个语句实际上是多态。看这个程序

using namespace std;
#include <iostream>
class A {
public:
    void uu() {
        this->kk();
    }
    virtual void kk() {
        cout << "我是基类" << endl;
    }
};
class B :public A {
public:
    virtual void kk() {
        cout << "我是子类" << endl;
    }
};

int main() {
    A a;B b;
    b.uu();
    return 0;
}

在kk函数前面加上了virtual关键字,编译后运行输出: 我是子类

OK,这篇笔记就写到这了。

Update

在三年半后的某一天晚上看 gem5 教程时回顾python类继承的一些东西时,回想起来这篇文章,感慨之余,做一些纠错和补正。事实上,python的所有方法都类似于C++中的虚函数,在运行时才选择具体调用哪个方法。在类变量的继承上(或者说C++的静态变量)也和 C++ 是一致的。即子类不会继承父类的类变量。那下面这段代码为什么输出是 4, 11

class A:
    a = 1

    def ff(self):
        A.a += 1

    def gg(self):
        A.a += 1


class B(A):

    def gg(self):
        B.a += 10


b = B()
d = A()
b.gg()
d.gg()
d.ff()
b.ff()
print(f"{d.a},{b.a}")

其原因在于 B.a += 10 这一行代码,对 B.a 进行的赋值不会更新到 A.a 上,而是对 B 这个类定义了新的类变量。类似地,如果d.a = d.a + 1 这样的代码,会给类 A 的实例定义出新的属性字段,而不是对 A.a 进行更新。

这一点在我看来稍微有点奇怪,因为 python 不允许下面的写法

a = 10
def t():
  a += 10
t()
# UnboundLocalError: local variable 'a' referenced before assignment
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值