头图

1. 背景

Java中有四种访问控制:public、protected、default、private,它们的使用范围可以用下面一张表概括:

类内部本包子类外部包
public
protected
default
private

整个结构还是比较简单的,从类内部到本包到子类到外部包权限越来越小,比较好理解也比较好记忆。但是在C++中访问控制要复杂很多,因为不仅有属性和方法的访问控制,还有继承时的派生列表访问说明符。今天我们着重了解访问控制。

2. 受保护的成员protected

在Java中我们一般默认子类可以访问父类的protected,但是C++中要更复杂些。它有三个特征:

  1. 受保护的成员对于类的用户来说是不可访问的;
  2. 受保护的成员对于派生类的成员和友元来说是可访问的;
  3. 派生类的成员或友元只能通过派生类对象来访问积累的受保护成员,派生类对于一个基类对象中的受保护成员没有任何访问特权。

怎么理解第三点呢?举个例子:

class Base{
protected:
    int num;
}
class Sub:public Base{
    friend void set(Sub&);
    friend void set(Base&);
    int i;
}
void set(Sub& sub){
    sub.i = sub.num = 0;//正确,可以访问基类和派生类的成员
}
void set(Base &base){
    base.num = 0;//错误,不能访问基类的成员
}

为什么要这样设计呢?如果派生类的友元可以直接访问基类的受保护成员,那么我们对任何类,只要设计它的子类,在子类的友元函数中就可以访问基类受保护的成员了,破坏了设计访问控制的目的,这一点比Java要严谨一些,任何访问控制都会被反射虐的体无完肤。

所以这里的结论是:派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员,对于普通的基类对象中的成员不具有特殊的访问权限。

3. 派生访问说明符

派生访问说明符的目的是控制抱愧派生类的派生类在内的派生类用户对于基类成员的访问权限:

  1. 如果继承是公有的,则成员将遵循其原有的访问说明符;
  2. 如果继承是私有的,派生类中基类的公有成员也会变成私有。

4. 派生类向基类转换的可访问性

派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响:

  1. 只有Son公有的继承Base时,用户代码才能使用派生类向接力的转换;如果是私有或者受保护,则用户代码不能使用该转换;
  2. 不论子类以什么方式继承父类,子类的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元是永远可访问的;
  3. 如果是工友或者受保护的继承,子类的派生类的成员和友元可以使用子类向基类的转换;反之不行。

5. 改变个别成员的可访问性

我们经常在代码中看到using Base::size这样的语句,很蒙圈不知道是干什么。这里是为了改变派生类继承的某个名字的访问级别。比如我们大部分内容想要私有继承,那么派生类列表访问类型使用私有就行。但是如果私有继承时某个或者某几个字段想要公开就可以使用using语句。举个例子:

class Base{
public:
    int size() const{
        return n;
    }
protected:
    int n;
};
class Sub:private Base{
public:
    //保持size函数的可访问性
    using Base::size;
protected:
    using Base::n;
} 

6. 默认的继承保护级别

有时候我们很困惑strcut和class关键字的区别,其实它们唯一的区别就是具有不同的默认访问说明符和默认派生运算符。但是我们规范书写代码的话都尽量不适用默认访问权限,所以很多时候struct和class区别不大。

7. 名字冲突和继承

派生类的成员将隐藏同名的基类成员,因为编译器查找类属性时,会先从自己的作用域内找,如果找不到再查找直接基类,如果还是查不到再查找直接基类的基类...。

class Base{
public:
    Base():num(0){}
protected:
    int num;
}
class Sub:public Base{
public:
    Sub(int i):num(i){}
    int getNum(){
        return num;
    }
protected:
    int num;
}

void main(){
    Sub s(20);
    cout << s.getNum <<end;//打印结果为20,而不是基类0
}

我们可以通过作用域运算符来使用一个被隐藏的基类成员。

int Sub::getNum(){
    return Base::num;
}
void main(){
    Sub s(20);
    cout << s.getNum <<end;//打印结果就变成基类的0了
}

最佳实践:除了覆盖基类继承而来的虚函数之外,子类最好不要重用其他定义在基类中的名字。

注意:如果子类的成员和基类的成员同名,则子类将在它作用域内隐藏这个基类的成员。即使子类成员和基类成员的形参列表不一致也会被隐藏。因为类中的名字查找是先找子类再找基类,当编译器在子类找到了该名字的成员就不会再向父类去找。

8. 总结

文本介绍了Java和C++访问控制权限的区别,以及C++派生访问说明符、派生类向基类转换的可访问性、改变个别成员的可访问性、默认的继承保护级别的内容。


轻口味
16.9k 声望3.9k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei