《随笔二十七》—— “ 【Effective C++ 中文第三版】 提炼总结三”

 

目录

第六章

条款32: 确保你的 public 继承建模出 “ is-a ” 关系

条款33:避免掩盖继承而来的名称


第六章



条款32: 确保你的 public 继承建模出 “ is-a ” 关系


“ is-a ” 关系用于表示public 继承,意味着基类与派生类之间的 一般 / 特殊关系。 基类它是一般类, 它的行为可以在派生类中特殊化。基类 TPerson 中拥有所有人都具有的一般行为, 而派生类 TStudent 可以将这些一般行为特殊化, 以符合某个学生的需要。实现这样的特殊化,并未修改 TPerson的任何功能。

特殊化在这样的语境中并不止于此。为了满足派生类的要求, 派生类可以扩展(extension)或精化 (refinement)基类。即派生类可以在已提供的一个或多个基类方法中添加新的功能(扩展), 或者派生类还可以重新实现某些或全部的基类方法(精化)。

 

当派生类 public 继承 基类时,基类可以派上用场的地方,派生类一样可以派上用场。 意思就是说派生类对象可以当作基类对象来使用。反之不成立。 这个规则只对 public 继承才有效。

 

公共继承 和 is-a的 等价关系听起来很简单,但有时你的直觉会误导你。例如,企鹅是一只鸟这是一个事实,鸟类可以飞也是一个事实。如果我们天真地试图用C++来描述这一层关系, 例如:

class Bird
{
public:
	virtual void fly() { cout << "it can fly." << endl; }
};

class Penguin : public Bird
{
	// fly()被继承过来了,可以覆写一个Penguin 的fly()方法,也可以直接用 Bird 类的
};

int main()
{
	Penguin p;
	p.fly(); // 问题是企鹅并不会飞!
	system("pause");
	return 0;
}

通过该程序就知道“ is-a ” 关系不是表面看起来那么简单,大家可以学习其它的著作来了解。 我推荐一本书叫 《 C++ 面向对象高效编程 第二版 》, 该书中 有讲解 “ is-a ” 关系, “ has_a ”  关系的章节。

 


条款33:避免掩盖继承而来的名称


该条款重点讲的是名字查找的过程,以及在类内部中查找名称的查找规则。 我们首先看书上 的例子:

int x = 10;  //global
void someFunc()
{
	double x = .45f; //local
	cout << x << endl; // 输出0.45
	cout << ::x << endl; // 这时输出的就是外层的x,输出结果为10
}
int main()
{
	someFunc();
	system("pause");
	return 0;
}

当我们在主调函数中 调用 someFunc() 时, 第一条 输出语句是0.45, 因为局部作用域的 x 隐藏了 全局作用域的x。如图:

当如果我们想输出外层作用域的x的时候, 我们可以在x的前面加上 “ ::” 运算符来显式地访问。

像这样的程序的名字查找(需找与所用名字最匹配的过程) 是相对简单的(注明:但是对于类的成员函数以及继承中的类作用域查找规则也有所区别), 像这样的程序采用以下规则:

  • 首先, 在名字所在的块中寻找其声明语句, 只考虑在名字的使用之前出现的声明
  • 如果没找到, 继续查找外层作用域。
  • 如果最终没有找到匹配的声明, 则程序报错。

本人注( 关于这样的程序的作用域问题在 C++ Primer 第五版 第43页有介绍)

成员函数中使用的名字按照如下方式解析:

  • 首先, 在成员函数内查找该名字的声明。和前面一样, 只有在函数使用之前出现的声明才被考虑。
  • 如果在成员函数内没有找到, 则在类内继续查找, 这时类的所有成员都可以被考虑。
  • 如果类内也没找到该名字的声明, 在成员函数定义之前的作用域内继续查找。

本人注( 关于这样的程序的作用域问题在 C++ Primer 第五版 第 255 页有介绍)

 


现在来看看 名称冲突与继承 中的情况。 在C++ 中继承层次结构中,  派生类可以重用定义 或者 覆盖其直接(或者 间接)基类中的名字; 如果是重用定义 的情况,此时定义在内层作用域 (即派生类) 的名字将隐藏( 它只会隐藏基类中能访问的名称,比如public 和 protected 区域的名称,它不会隐藏基类中的 private 区域的名称,即使派生类的该名称与基类中的名称相同,因为派生类本身就不能访问基类中的私有成员的。)定义在外层作用域(即基类)的名字 。

覆盖只发生在 派生类的同名函数跟基类中那个virtual 函数的函数原型一致, 这样我们就说派生类中的版本覆盖了基类中的相应函数版本。

 

下面首先说说派生类的名称隐藏基类中的名称:

struct Base
{
public:
	Base() : mem(11)
	{
		cout << "调用的是基类中的构造函数!" << endl;
	}
	int get_mem() 
	{ 
		return mem; 
	}
protected:
	int mem;
};
struct Derived : public Base
{
public:
	Derived(int i) : mem(i)  // i 初始化Derived 中的 mem 
	{
		cout << "调用的是派生类中的构造函数!" << endl;
	}
	
	int get_mem() //  隐藏了基类中的同名成员 
	{ 
		return mem;  // returns Derived::mem
	} 
	
protected:
	int mem; // 隐藏了基类中的同名成员 
};

int main()
{
	Derived d(42);
	cout << "输出派生类的mem的值:" << d.get_mem() << endl;
	cout << "输出基类的mem的值:" << d.Base::get_mem() << endl;//可以显式使用作用域运算符来访问Base 中的 get_mem 成员函数

	Derived *myDerived = &d; 
	cout << "\n输出派生类的mem的值:" << myDerived->get_mem() << endl;
	cout << "输出基类的mem的值:" << myDerived->Base::get_mem() << endl;

	Base *myBase = &d;
	// 下面这个调用的都是基类中的函数版本,为什么呢? 因为调用的该函数不是虚函数,所以调用是在编译期就进行绑定
	cout << "\n输出基类的mem的值:" << myBase->get_mem() << endl;
	// cout << myBase->myDerived::get_mem() << endl; // 这样写是错误的,因为派生类的成员在其基类中可能有也可能没有
	system("pause");
	return 0;
}

输出结果为:

调用的是基类中的构造函数!
调用的是派生类中的构造函数!
输出派生类的mem的值:42
输出基类的mem的值:11

输出派生类的mem的值:42
输出基类的mem的值:11

输出基类的mem的值:11

 

 

 

下面这个程序演示这一点 —— 派生类中的成员隐藏基类中的成员,参数列表和返回类型不一致的情况 :

struct Base 
{
	void memfcn() { cout << "Base::memfcn" << endl; } // 该函数的参数列表跟派生类中的同名函数不一样
	int memfcn1()  该函数跟派生类中的同名函数返回类型不一样
	{
		cout << "Base::memfcn1" << endl;
		return 0;
	}
};
struct Derived : Base 
{
	void memfcn(int i) { { cout << "Derived::memfcn" << endl; } } // 隐藏基类中的memfcn
	void memfcn1()
	{
		cout << "Derived::memfcn1" << endl;
		
	}
};
 
int main()
{
	Derived d; Base b;
	b.memfcn(); // calls Base::memfcn
	d.memfcn(10); // calls Base::memfcn
	// d.memfcn(); // 错误, 参数列表为空的 memfcn 被隐藏了
	d.Base::memfcn(); // ok: calls Base::memfcn
 
	cout << endl;
	
 
	b.memfcn1();  // calls Base::memfcn1
	d.memfcn1(); // calls Base::memfcn1
	d.Base::memfcn1(); // ok: calls Base::memfcn1
	cout << endl;
	system("pause");
	return 0;
}
 
输出结果为:
 
Base::memfcn
Derived::memfcn
Base::memfcn
 
Base::memfcn1
Derived::memfcn1
Base::memfcn1

“ d.memfcn() ”  该调用是错误的,为什么?

  • 为了解析这条调用语句 , 编译器首先在 Derived 中查找名字 memfcn ; 因为Derived确实定义了一个名为memfcn的成员,所以查找过程终止。一旦名字找到, 编译器就不再继续查找了。Derived中的memfcn版本需要一个int实参, 而当前的调用语句无法提供任何实参, 所以该调用语句是错误的。

 

下面在说派生类的名称覆盖基类中的名称:

class Base
{
private:
	int x;
public:
	virtual void mf1() = 0;
	virtual void mf1(int i)
	{
		cout << "调用的是Base的virtual mf1(int i) " << endl;
	}
	virtual void mf2()
	{
		cout << "调用的是Base的 virtual mf2() " << endl;
	}
	void mf3()
	{
		cout << "调用的是Base的  mf3() " << endl;
	}
	void mf3(double i)
	{
		cout << "调用的是Base的  mf3(double i) " << endl;
	}

};
void Base::mf1() // 纯虚函数是可以有实现的, 但是必须在类外定义
{
	cout << "调用的是Base的virtual mf1() = 0 " << endl;
}


class Derived : public Base
{
public:
	virtual void mf1()
	{
		cout << "调用的是Derived 的virtual mf1() " << endl;
	}
	void mf3()
	{
		cout << "调用的是Derived 的virtual mf3() " << endl;
	}
	void mf4()
	{
		cout << "调用的是Derived 的virtual mf3() " << endl;
	}

};



int main()
{

	Derived d;
	d.mf1(); // 正确,静态调用,调用的是Derived:: mf1()
	//d.mf1(10); // 错误! 静态调用, 因为Derived :: mf1 隐藏了Base::mf1
	
	d.mf2(); // fine, calls Base::mf2
	d.mf3(); // fine, calls Derived::mf3
	// d.mf3(10); // error! Derived::mf3 隐藏 Base::mf3

	cout << endl;
	Base *myBase = &d;
	myBase->mf1(); // 虚调用,调用Derived::mf1()
	myBase->Base::mf1(); // 静态调用,显式使用 :: 运算符调用 Base:: mf1()
	myBase->mf1(10); // 虚调用,调用的是 Base:: mf1( int i)
	myBase->mf3(); // 静态调用,调用的是 Base:: mf3(),因为调用的不是虚函数,静态绑定
	myBase->mf3(20); // 静态调用,调用的是 Base:: mf3(double i),因为调用的不是虚函数,静态绑定
	system("pause");
	return 0;
}

输出结果为:

调用的是Derived 的virtual mf1()
调用的是Base的 virtual mf2()
调用的是Derived 的virtual mf3()

调用的是Derived 的virtual mf1()
调用的是Base的virtual mf1() = 0
调用的是Base的virtual mf1(int i)
调用的是Base的  mf3()
调用的是Base的  mf3(double i)

 在C++语言中, 当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。根据引用或指针所绑定的对象类型不同, 该调用可能执行基类的版本, 也可能执行某个派生类的版本。

 

那么在主函数中的 d 它是一个静态对象, 它调用虚函数,调用的时候不会根据所绑定的对象类型来调用的。然后,d 它的静态类型是 Derived, 所以说,当调用某一个函数的时候,首先查找的就是 Derived 类的作用域。 当在Derived 作用域查找到 mf1 和 mf3 函数时,将发生报错,因为Derived 中没有带参数的相应版本。

 

当我们用基类的指针指向派生类时,即该代码 “ Base *myBase = &d; ”,当调用 “ myBase->mf1(10); ”时, 它首先在 Derived 类查找,因为没有,所以调用的是 Base 类中的相应版本, 从输出结果可以看出。

 

如果现在你非要访问父类里面的方法,第一种方法是派生类中使用 using 声明:

class Base
{
private:
	int x;
public:
	virtual void mf1() = 0;
	virtual void mf1(int i)
	{
		cout << "调用的是Base的virtual mf1(int i) " << endl;
	}
	virtual void mf2()
	{
		cout << "调用的是Base的 virtual mf2() " << endl;
	}
	void mf3()
	{
		cout << "调用的是Base的  mf3() " << endl;
	}
	void mf3(double i)
	{
		cout << "调用的是Base的  mf3(double i) " << endl;
	}

};
void Base::mf1() // 纯虚函数是可以有实现的, 但是必须在类外定义
{
	cout << "调用的是Base的virtual mf1() = 0 " << endl;
}


class Derived : public Base
{
	
public:
	// 使用 using 声明把 mf1 和 mf3 在基类中的所有重载实例都添加到Derived作用域中,并且是public的
	// 注意:派生类只能为基类中能访问的名称提供using 声明, 即不能为基类中私有成员提供using声明
	// 如果下面这两条语句放在 protected 和 private 区域中, 结果还是跟原来一样,都是报错。
	using Base::mf1;
	using Base::mf3;
	virtual void mf1()
	{
		cout << "调用的是Derived 的virtual mf1() " << endl;
	}
	void mf3()
	{
		cout << "调用的是Derived 的virtual mf3() " << endl;
	}
	void mf4()
	{
		cout << "调用的是Derived 的virtual mf3() " << endl;
	}

};



int main()
{

	Derived d;
	d.mf1(); // 正确,静态调用,调用的是Derived:: mf1()
	d.mf1(10); // 正确,调用 Base::mf1(int i)
	
	d.mf2(); // fine, calls Base::mf2
	d.mf3(); // fine, calls Derived::mf3
	d.mf3(10); // 正确,调用 Base::mf3(double i)

	cout << endl;
	Base *myBase = &d;
	myBase->mf1(); // 虚调用,调用Derived::mf1()
	myBase->Base::mf1(); // 静态调用,显式使用 :: 运算符调用 Base:: mf1()
	myBase->mf1(10); // 虚调用,调用的是 Base:: mf1( int i)
	myBase->mf3(); // 静态调用,调用的是 Base:: mf3(),因为调用的不是虚函数,静态绑定
	myBase->mf3(20); // 静态调用,调用的是 Base:: mf3(double i),因为调用的不是虚函数,静态绑定
	system("pause");
	return 0;
}


输出结果为:

调用的是Derived 的virtual mf1()
调用的是Base的virtual mf1(int i)
调用的是Base的 virtual mf2()
调用的是Derived 的virtual mf3()
调用的是Base的  mf3(double i)

调用的是Derived 的virtual mf1()
调用的是Base的virtual mf1() = 0
调用的是Base的virtual mf1(int i)
调用的是Base的  mf3()
调用的是Base的  mf3(double i)

现在原来那些 用 d 对象调用的函数,都正确了。

 

可以想象,有时您不并想继承基类中的所有重载函数。在公共继承下,不应该出现这种情况,因为它再次违反了基类和派生类之间的is-a关系。(这就是为什么上面的using声明在派生类的public部分中: 在基类中是公共的名称在公共派生类中也应该是公共的)。然而,在私有继承下,可以不需要承基类中的所有重载函数。

例如,假设 Derived 私有地从Base继承,并且Derived想要继承的唯一版本的mf1是不带参数的版本。 using声明在这里不会起作用,因为using声明使所有具有给定名称的继承函数在派生类中可见。可以在 派生类中写一个转交函数,然后在该函数调用基类中的相应版本。

class Base
{
private:
	int x;
public:
	virtual void mf1() = 0;
	virtual void mf1(int i)
	{
		cout << "调用的是Base的virtual mf1(int i) " << endl;
	}
	virtual void mf2()
	{
		cout << "调用的是Base的 virtual mf2() " << endl;
	}
	void mf3()
	{
		cout << "调用的是Base的  mf3() " << endl;
	}
	void mf3(double i)
	{
		cout << "调用的是Base的  mf3(double i) " << endl;
	}

};
void Base::mf1() // 纯虚函数是可以有实现的, 但是必须在类外定义
{
	cout << "调用的是Base的virtual mf1() = 0 " << endl;
}


class Derived : private  Base // 私有继承 Base
{

public:
	virtual void mf1()
	{
		Base::mf1(); // 一个转交函数
	}
	void mf3()
	{
		cout << "调用的是Derived 的virtual mf3() " << endl;
	}
	void mf4()
	{
		cout << "调用的是Derived 的virtual mf3() " << endl;
	}

};



int main()
{

	Derived d;
	d.mf1(); // 正确,静态调用,调用的是Derived:: mf1()

	system("pause");
	return 0;
}

 

建议: 除了覆盖继承而来的虚函数的名称之外, 派生类最好不要重定义在基类中的名字(不管是 成员函数还是数据成员)。

 


条款34:区分接口继承和实现继承


 

 


条款35:考虑虚函数以外的其他选择


 


条款36:绝不重新定义继承而来的non-virtual函数


 

class Base
{
public:
	void func() { cout << "base function" << endl; }
};

class Drived : public Base
{
public:
	void func() { cout << "drived function" << endl; }
};

int main()
{
	Drived d;
	Base* pb = &d;
	pb->func();

	Drived* pd = &d;
	pd->func();

	system("pause");
	return 0;
}

当我们通过 pb 和 pd 调用 func 函数时,显示结果却不相同。当 func 被pd 调用时,它又可能调用的是自定义的版本,也可能调用的是基类中的函数版本。

现在这个现象的原因是在于 Base:: func与 Derived :: func都是静态绑定,所以调用的non-virtual函数都是各自定义的版本。 引用也是一样。

 

回顾下之前的条款,如果是public继承的话,那么:

  • 适用于Base Class的行为一定适用于Derived Class,因为每一个Derived Class对象都是一个BaseClass对象;
  •  如果Base Class里面有非虚函数,那么 Derived Class一定是既继承了接口,也继承了实现;
  • 子类里面的同名函数会隐藏( 如果派生类中的同名函数与基类中的相应版本不是虚函数)父类的同名函数,这是由于名称查找规则导致的。

如果Derived Class重定义一个non-virtual函数,那么会违反上面列出的法则。以第一条为例,如果子类真的要重定义这个函数,那么说明父类的这个函数不能满足子类的要求,这就与每一个子类都是父类的原则矛盾了。 既然如此,派生类就不应该以public 的方式继承基类。

如果Derived 真的需要实现出于 Base 中不同版本的 func, 那么该函数应该声明为 virtual 函数。

可以总结一下了,无论哪一个观点,结论都相同:

  • 任何情况下都不该重新定义一个继承而来的non-virtual函数。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值