父类中访问子类成员

委屈的父类

一般情况下来说,父类的中的成员往往是最委屈的,子类要是有个同名(仅仅需要名字一样)成员,自己的成员就会被隐藏,子类还可以访问自己的非私有成员,但是自己想访问子类中的成员就不行,当子类与自己有同名的成员时,访问到的是自己的成员,没有同名成员时意图直接访问子类成员则直接报错.......这是何等悲惨,为此,我们应该做些什么来为父类谋得一些权利,让父类可以顺利的访问子类非费私有成员。

为什么委屈

名字查找一

为了让这一切顺利进行,我们先来看看为什么父类是如此的委屈。这要从C++编译器对名字的查找规则说起:

1、编译器首先在名字所在的代码块(一对大括号{})内查找名字(名字需要是在使用之前的名字声明)

2、如果没找到则继续查找外层的作用域,如果找到则立即停止查找

3、一直向外层查找且没找到则报错

 

:

#include <iostream>

int a = 0;

int main()
{
	int a = 1;
	if( 1 )
	{
		std::cout << "a = " << a << std::endl;
		int a = 2;
	}
	
	return 0;
}


输出结果是 a = 1;

编译器先在a使用的代码块内查找使用之前的名字声明,找到了if之上一行的名字声明,并立即停止对名字a的查找,并输出其初始值1

 

这一规则对应到类中:

1、首先在成员函数体范围内查找函数体中使用的名字声明(其实也就是在名字所在代码段)

2、如果没找到则在类中继续查找(类本身就是一个作用域,类成员属于类作用域之内,所以相当于在外层作用域中查找,由于类中声明部分先于定义被编译器处理,编译到函数体时,类中所有名字就已经可见了),找到则停止查找

3、继续向外层作用域查找,直到找不到并报错

:

#include <iostream>

class Test
{
public:
    void print()
    {
        std::cout << "a = " << a << std::endl;
        int a = 10;
    }

private:
    int a = 0;		//类内初始化
};

int main()
{
    Test t;
    t.print();

    return 0;
}

建议用C++11方式编译, 输出结果为a = 0

编译器先在a使用的代码块内查找使用之前的名字声明,由于该代码块内并没有关于名字a的声明,于是开始向外层的类作用域查找,又由于类内部成员的声明是先于定义被处理,即此时成员名字已经可见,于是找到了类末尾的名字声明。输出其类内初始值0

作用域嵌套

上面的名字查找规则是父类为什么这么委屈的原因之一,另外,在遇到继承时会发生更加奇妙的事情:

发生继承时,子类的作用域会被嵌套在父类的作用域中!

这导致了子类有机会使用父类的成员:当子类中出现了一个子类中不存在名字时,编译器向外层查找并在父类作用域中查找到,停止查找,之后编译器对名字进行访问属性检查(注意类的作用域是作用域,类的访问属性是访问属性,不是同一个概念,名字在其作用域之内都是可见、可访问的,访问属性是类对外界可见名字的成员访问权限说明),如果名字不是私有属性,子类使用名字成功,否则失败,编译器报错。

如:

#include <iostream>

class Base
{
    int a = 0;
};
class Child : public Base
{
public:
    void print()
    {
        std::cout << "a = " << a << std::endl;
    }
};

int main()
{
    Child child;
    child.print();

    return 0;
}

编译报错,错误信息如下:

 

编译器在使用a的代码块内未能找到名字声明,一直向外查找直到父类作用域,找到a的名字声明,但是在检查其访问属性时发现其为私有属性,报错

再看名字查找

静态类型决定名字应该去哪个作用域中寻找

也就是,如果是是以子类对象/引用/指针去访问一个成员,编译器会在子类的作用域范围内开始查找这个名字,如果找到且名字立即停止查找,再检查访问属性,如果子类作用域范围没有找到才会去父类作用域以同样的方式查找。这一过程说明了为何会发生同名隐藏。

 

名字查找先于类型检查

这一过程是这样的,如果名字找到了,编译器才会开始进行类型检查,如果类型完全一致,则编译通过,如果类型不完全一致则尝试隐式类型转换,如果转换成功,则编译通过,失败则编译报错,因此类型不同时亦可发生同名隐藏。

:

#include <iostream>

class Base
{
protected:
    int a = 0;
public:
    void print(double)
    {
        std::cout << "Base::a = " << a << std::endl;
    }
};
class Child : public Base
{
public:
    int a = 1;

    void print(int)
    {
        std::cout << "Child::a = " << a << std::endl;
    }
};

int main()
{
    Child child;
    child.print(0.0);

    return 0;
}

输出Child::a = 1

编译器发现child的类型为Child,于是去类Child的作用域中查找print,查找到了print后立即停止查找并开始进行类型检查,发现实参类型为double,形参类型为int,但可以隐式转换,于是没有报错(如果对同名隐藏规则不熟悉,咋一看确实以为会输出Base::a = 0)

在函数体Child::print内部时,向外查找到child::a时,停止查找,而不会查找到子类可访问的属性为protectedBase::a,因此运行时会输出child::a的类内初始值1

函数Base::print会查找到Base::a而不会查找的publicChild::a

 

为父类掰回一局

是时候为父类做点事情了,让父类也有访问子类成员的机会!名字是以什么样的方式查找这个是无法改变的,那怎样才能有机会在父类中访问到子类中的成员呢?是时候搬出面向对象三大特征之一的”多态君”了。

应该这样定义父类:

class Base
{
protected:
    int a = 0;
public:
    void want_to_visit_child()
    {
        std::cout << "typeid(this).name() = " << typeid(this).name() << std::endl;
        std::cout << "typeid(*this).name() = " << typeid(*this).name() << std::endl;

        this->visit_child();
    }
    virtual void visit_child() = 0;
    virtual ~Base() noexcept
    {
    }
};

想在父类的成员函数want_to_visit_child访问子类成员,我们是这样做的,设计一个访问子类的”接口”visit_child,并将其声明为纯虚函数,迫使子类重写并实现visit_child函数体,并在实现的函数体中访问子类的成员。

子类应该这样写:

class Child : public Base
{
private:
    int a = 1;
public:
    void visit_child() override
    {
        name();
        std::cout << "I have a variable named \"a\" which has value : " << a << std::endl;
    }
    void name()
    {
        std::cout << "I am child,It's my honor to meet you" << std::endl;
    }
};

在子类中,我们重写了visit_child并在其函数体中访问了子类的成员变量。

使用是酱婶的:

int main()
{
    Child child;
    child.want_to_visit_child();

    return 0;
}

 

为什么?

嗯.....结果看上去很好,但是你发现什么问题了吗?

按照刚才的名字查找规则,want_to_visit_child只能访问父类中的visit_child才对啊,为什么输出却显示他调用了子类中的visit_child呢?难道是多态?那么多态是如何发生的呢?不是要通过基类的指针或者引用调用虚函数才会发生多态吗?这到底是怎么一回事?

隐藏的答案

把父类的成员函数want_to_visit_child这样写,也许你就会发现答案:

void want_to_visit_child()
{
    std::cout << "typeid(this).name() = " << typeid(this).name() << std::endl;
    std::cout << "typeid(*this).name() = " << typeid(*this).name() << std::endl;

    this->visit_child();
}

 

别忘了非静态成员函数都有一个隐藏的this指针参数,通过RTTI机制,我们发现了this指针的静态类型是Base类型,指向了子类对象Child,并调用了虚函数visit_child。也就是说,多态了!多态了!多态了!

成员函数want_to_visit_child拥有一个隐藏参数Base类型的this指针,当在want_to_visit_child内部访问成员时,相当于通过此指针去访问成员,然而,此时通过this访问的visit_child又恰好是个虚函数,更巧的是this此时正好又指向了子类对象且虚函数确实发生了重写,于是编译器通过子类对象中隐藏的虚函数指针访问虚函数表以查找应该调用的函数地址,这个地址正是子类中重写的visit_child的函数地址,于是多态了!多态了!多态了!

 

注意包含头文件

#include <iostream>
#include <typeinfo>


 

参考:C++ Primer 第五版


  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值