【C++基础】第八十八课:[面向对象程序设计]访问控制与继承

访问控制与继承

Posted by x-jeff on November 9, 2023

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.访问控制与继承

每个类分别控制自己的成员初始化过程,与之类似,每个类还分别控制着其成员对于派生类来说是否可访问(accessible)。

1.1.受保护的成员

如前所述,一个类使用protected关键字来声明那些它希望与派生类分享但是不想被其他公共访问使用的成员。protected说明符可以看做是public和private中和后的产物:

  • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
  • 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。

此外,protected还有另外一条重要的性质。

  • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。

为了理解最后一条规则,请考虑如下的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
protected:
	int prot_mem; //protected成员
};
class Sneaky : public Base {
	friend void clobber(Sneaky&); //能访问Sneaky::prot_mem
	friend void clobber(Base&); //不能访问Base::prot_mem
	int j; //j默认是private
};
//正确:clobber能访问Sneaky对象的private和protected成员
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
//错误:clobber不能访问Base的protected成员
void clobber(Base &b) { b.prot_mem = 0; }

private、public、protected的访问范围:

  • private:只能由该类的成员函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问。
  • protected:可以被该类的成员函数、派生类的成员函数,以及其友元函数访问,但不能被该类的对象访问。
  • public:可以被该类的成员函数、派生类的成员函数,其友元函数访问,也可以由该类的对象访问。

1.2.公有、私有和受保护继承

某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符。举个例子,考虑如下的继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {
public:
	void pub_mem(); //public成员
protected:
	int prot_mem; //protected成员
private:
	char priv_mem; //private成员
};
struct Pub_Derv : public Base {
	//正确:派生类能访问protected成员
	int f() { return prot_mem; }
	//错误:private成员对于派生类来说是不可访问的
	char g() { return priv_mem; }
};
struct Priv_Derv : private Base {
	//private不影响派生类的访问权限
	int f1() const { return prot_mem; }
};

派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。Pub_Derv和Priv_Derv都能访问受保护的成员prot_mem,同时它们都不能访问私有成员priv_mem。

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

1
2
3
4
Pub_Derv d1; //继承自Base的成员是public的
Priv_Derv d2; //继承自Base的成员是private的
d1.pub_mem(); //正确:pub_mem在派生类中是public的
d2.pub_mem(); //错误:pub_mem在派生类中是private的

派生访问说明符还可以控制继承自派生类的新类的访问权限:

1
2
3
4
5
6
7
8
struct Derived_from_Public : public Pub_Derv {
	//正确:Base::prot_mem在Pub_Derv中仍然是protected的
	int use_base() { return prot_mem; }
};
struct Derived_from_Private : public Priv_Derv {
	//错误:Base::prot_mem在Priv_Derv中是private的
	int use_base() { return prot_mem; }
};

Pub_Derv的派生类之所以能访问Base的prot_mem成员是因为该成员在Pub_Derv中仍然是受保护的。相反,Priv_Derv的派生类无法执行类的访问,对于它们来说,Priv_Derv继承自Base的所有成员都是私有的。

假设我们之前还定义了一个名为Prot_Derv的类,它采用受保护继承,则Base的所有公有成员在新定义的类中都是受保护的。Prot_Derv的用户(即类的对象)不能访问pub_mem,但是Prot_Derv的成员和友元可以访问那些继承而来的成员。

总结如下表:

继承方式 基类的public成员 基类的protected成员 基类的private成员 继承引起的访问控制关系变化概括
public继承 仍为public成员 仍为protected成员 不可见 基类的非私有成员在子类的访问属性都不变
protected继承 变为protected成员 变为protected成员 不可见 基类的非私有成员都成为子类的保护成员
private继承 变为private成员 变为private成员 不可见 基类中的非私有成员都成为子类的私有成员

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

派生类向基类的转换(参见:派生类对象及派生类向基类的类型转换)是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。假定D继承自B:

  • 只有当D公有地继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换。
  • 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
  • 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果D继承B的方式是私有的,则不能使用。

对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行。

1.4.友元与继承

就像友元关系不能传递一样(参见:友元再探),友元关系同样也不能继承。基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员:

1
2
3
4
5
6
7
8
9
10
11
class Base {
	//添加friend声明,其他成员与之前的版本一致
	friend class Pal; //Pal在访问Base的派生类时不具有特殊性
};
class Pal {
public:
	int f(Base b) { return b.prot_mem; } //正确:Pal是Base的友元
	int f2(Sneaky s) { return s.j; } //错误:Pal不是Sneaky的友元
	//对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此
	int f3(Sneaky s) { return s.prot_mem; } //正确:Pal是Base的友元
};

当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来那个类来说,其友元的基类或者派生类不具有特殊的访问能力:

1
2
3
4
5
//D2对Base的protected和private成员不具有特殊的访问能力
class D2 : public Pal {
public:
	int mem(Base b) { return b.prot_mem; } //错误:友元关系不能继承
};

不能继承友元关系;每个类负责控制各自成员的访问权限。

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

有时我们需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
	std::size_t size() const { return n; }
protected:
	std::size_t n;
};
class Derived : private Base { //注意:private继承
public:
	//保持对象尺寸相关的成员的访问级别
	using Base::size;
protected:
	using Base::n;
};

因为Derived使用了私有继承,所以继承而来的成员size和n(在默认情况下)是Derived的私有成员。然而,我们使用using声明语句改变了这些成员的可访问性。改变之后,Derived的用户将可以使用size成员,而Derived的派生类将能使用n。

通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员(例如,非私有成员)标记出来。using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。也就是说,如果一条using声明语句出现在类的private部分,则该名字只能被类的成员和友元访问;如果using声明语句位于public部分,则类的所有用户都能访问它;如果using声明语句位于protected部分,则该名字对于成员、友元和派生类是可访问的。

派生类只能为那些它可以访问的名字提供using声明。

1.6.默认的继承保护级别

我们曾经介绍过使用struct和class关键字定义的类具有不同的默认访问说明符(参见:使用class或struct关键字)。类似的,默认派生运算符也由定义派生类所用的关键字来决定。默认情况下,使用class关键字定义的派生类是私有继承的;而使用struct关键字定义的派生类是公有继承的:

1
2
3
class Base { /*...*/ };
struct D1 : Base { /*...*/ }; //默认public继承
class D2 : Base { /*...*/ }; //默认private继承

人们常常有一种错觉,认为在使用struct关键字和class关键字定义的类之间还有更深层次的差别。事实上,唯一的差别就是默认成员访问说明符及默认派生访问说明符;除此之外,再无其他不同之处。

2.参考资料

  1. C++中public、protected、private的区别
  2. C++的三种继承方式:public,protected,private