第九天2017/04/18(2、类的继承、面试题:继承访问修饰符、组合、static、构造、多态)

本文探讨了面向对象编程中的继承和多态概念,强调它们在代码复用中的作用。同时,针对面试题,解释了继承访问修饰符和虚函数的效率问题,指出虚函数在运行时动态调用可能导致效率降低,并提醒不应过度使用虚函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继承:可以使用原来的代码,代码复用
多态:代码复用、接口复用,用基类的指针“根据对象”调用“指定对象的函数”。

1、继承、访问修饰符
    //C++类成员的3种访问级别
    //C++继承的3种继承方式
2、C++不是类型安全的语言
3、赋值兼容问题:
    类型转换
4static与继承
    在基类中定义的静态成员,将被所有派生类共享;
    根据静态成员自身的访问特性和派生类的继承方式,在类层次体系中具有不同的访问属性;
    派生类中访问静态成员,用下面形式:类名::成员 、 对象名.成员 。

/*在类中声明了static静态变量,如果不在类外进行初始化,就想访问
该static变量,此时会发生编译错误,错误信息见下:
error LNK2001: 无法解析的外部符号 "public: static int B::b" 
因此,在类中声明了static变量后,在使用该静态变量之前,一定要
先进行初始化。
*/

【讨论:在父类和子类中声明、定义同名的静态变量b,子类中的b会不会覆盖父类中的b】
#include <iostream>
using namespace std;
/*
在子类和父类中有同名的static成员变量static int b;
子类中有两个static变量b,只不过在子类中不加::作用域修饰符直接对b进行访问时,
    与普通变量一样,父类中的static变量b被子类中的static变量b覆盖掉。
*/
class B
{
public:
    B()
    {
        cout<<"&B::b = "<<&/*B::*/b<<endl; //默认访问的是本类中的b
    }
    static int b;
};
//int B::b = 0; //初始化static成员变量时,相当于告诉C++编译器给b变量开辟内存

class D:public B
{
public:
    D()
    {
        cout<<"&D::b = "<<&/*D::*/b<<endl; //默认访问的是本类中的b,即子类的b把父类的b覆盖
        cout<<"&B::b = "<<&B::b<<endl;//用::作用域访问符可以访问父类中的b
    }
    static int b;//初始化static成员变量时,相当于告诉C++编译器给b变量开辟内存
};
int D::b = 0; //初始化static成员变量时,相当于告诉C++编译器给b变量开辟内存

int main()
{
    D d;
}

5、继承中的构造
①子类中的父类如何进行初始化?
    答:用成员初始化列表调用父类的构造函数进行初始化子类中的父类。
②继承与组合混搭的情况下,如何进行构造?
    答:构造顺序:基类、组合类、派生类
③继承中同名成员变量的处理方法?

#include <iostream>
using namespace std;
class A  //A类用作组合类
{
public:
    A(int aa):a(aa){cout<<"A"<<endl;}
    int get_A()
    {
        return a;
    }
    void show_A()
    {
        cout<<"a="<<a<<endl;
    }
protected:
private:
    int a;
};
class B //B类用作基类
{  
public:
    B(int bb):b(bb){cout<<"B"<<endl;}
    int get_B()
    {
        return b;
    }
    void show_B()
    {
        cout<<"b="<<b<<endl;
    }
protected:
private:
    char b;
};

class D:public B //D类用作派生类
{//子类中的父类如何进行初始化?答:用成员初始化列表进行初始化子类中的父类
public:
    D(int bb,int dd,int aa):a1(aa),B(bb),d(dd)//调用构造函数的顺序:父类、组合类、子类
    {
        cout<<"D"<<endl;
    }
    void show_D()
    {
        a1.show_A();
//D类public继承于B类,因此D中有B中的所有成员,包括get_B()函数,可以在D中调用从B中继承来的get_B()函数。
        cout<<"b="<<get_B()<<endl<<"d="<<d<<endl; //get_B()
    }
protected:
private:
    int d;
    A a1;  //组合
};

int main()
{
    D d(1,2,3);  //B A D 
    d.show_D();  
}
6、多继承
//问题:多继承中会发生“二义性”!
//结论:
//C多继承于A和B,A、B中有同名的成员变量a。
//在C对象c访问a时,要用A::a或者B::a明确指出访问的是A中的a还是B中的a。

#include <iostream>
using namespace std;
class A  
{
public:
    A(int i=1):a(i){} //虽然默认值是1,但是在构造C的对象时,传入了默认参数0给i,因此i=0
    int a;
private:

};
class B
{
public:
    B(int i=1):a(i){} //虽然默认值是1,但是在构造C的对象时,传入了默认参数0给i,因此i=0
    int a;
private:

};
class C:public A,public B 
{
public:
    C(int i=0):A(i),B(i)
    {

    }
};
int main()
{
    C c1;
    //cout<<c1.a<<endl;  //编译失败:error: 对“a”的访问不明确   
    cout<<c1.A::a<<endl; //编译运行成功,结果:0
    cout<<c1.B::a<<endl; //编译运行成功,结果:0
}   

虚继承图示见下:

这里写图片描述

7、多态(发生的条件)
    父类的指针或引用指向子类的对象,并调用子类重写的父类的虚函数。
多态成立的3个条件:
        有继承
        子类重写父类的虚函数
        父类的指针或引用指向子类对象

这里写图片描述

【总结】
动态联编:当有virtual修饰时,调用子类?父类?的函数由“____对象”决定!
静态编译:当无virtual修饰时,调用子类?父类?的函数由“____指针”决定!

8、类与类之间的关系

1.组合:包含关系
2.关联:一个类部分的使用另外一个类。通过类之间成员函数的相互联系,定义友元或对象参数传递实现。(了解)
3.继承
4.友元类

9、多态面试题
这里写图片描述

①多态的实现效果:
    多态:同样的调用语句有多种不同的形态
②多态的三个条件:
    继承、子类重写父类的虚函数、父类指针或引用指向子类对象
③多态的C++实现:虚函数表、虚函数指针、动态绑定
    virtual关键字,告诉C++编译器要进行“迟绑定”的动态联编;不要根据指针类型判断如何调用,而是要根据指针所指向的实际对象类型来判断如何调用。
④多态的理论基础:
    动态联编PK静态联编,根据实际的对象类型来判断重写函数的调用。
⑤多态的重要意义:
    设计模式的基础
⑥实现多态的理论基础:
    函数指针作函数参数



#include <iostream>
using namespace std;
class B
{
public:
    virtual void f(int i) {  }//第一个动手脚的地方:virtual void f(int i)中的virtual,动了什么手脚?
};
class D:public B
{
public:
    void f(int i) {  }
};

void g(B& b)//第二个动手脚的地方:void g(B& b)中的b,我们怎么知道传入的是父类的对象?还是子类的对象?
{
    b.f(1);
}
void g(B* b)//第二个动手脚的地方:void g(B* b)中的b,我们怎么知道传入的是父类的对象?还是子类的对象?
{
    b->f(1);
}
void main()
{
    D dd;//定义子类对象
    g(dd); //多态
    g(&dd);//多态
}
以上面代码为例,分析一下要实现多态,C++编译器应该动什么手脚?
①第一个动手脚的地方:virtual void f(int i)中的virtual,动了什么手脚?
    虚函数表、虚函数指针
②第二个动手脚的地方:void g(B& b)中的b,我们怎么知道传入的是父类的对象?还是子类的对象?
    传入的对象中有一个虚函数表指针*Vptr,通过该虚函数表指针*Vptr找到对应的虚函数表,之后再找到要调用的函数的函数指针,通过函数指针对函数进行调用。
    因此,加上virtual关键字声明后的函数的调用结果是由“对象类型”决定的!


C++中多态实现原理
    当类中声明虚函数时,C++编译器会在类中生成一个虚函数表
    虚函数表是一个存储类成员函数指针的数据结构
    虚函数表是由编译器自动生成和维护的
    virtual成员函数会被编译器放入虚函数表中
    存在虚函数表时,每个对象中有一个指向虚函数表的指针(vptr指针)

这里写图片描述

【答案:面试题1、2】

这里写图片描述

这里写图片描述
【说明1】
通过虚函数表指针Vptr调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数;而普通成员函数是在编译时就确定了调用的函数,因此:在效率上,虚函数的效率要低很多。
【说明2】
出于效率问题,没有必要把所有的成员函数都声明为虚函数。

这里写图片描述

【答案:面试题4】
    可以把所有的函数声明为虚函数,但是会降低代码的执行效率。因此,不使用多态时,不要把函数声明为虚函数。
父类中的虚函数表指针能否被子类继承?

这里写图片描述

【答案:面试题5】构造函数中调用虚函数不能实现多态!
为什么不能?
    Vptr指针的初始化过程是分步完成的
    1.构造子类对象过程中,先执行父类的构造函数,此时C++编译会初始化子类的Vptr指针,让Vptr指针暂时先指向父类的函数表。
    因此,在父类的构造函数中调用虚函数virtual void f(),此时调用的是父类中的f().
    2.当父类的构造函数执行完成后,再执行子类的构造函数,此时C++编译会重新修改子类的Vptr指针,让Vptr指针真正的指向子类的函数表。

同样:析构函数中调用虚函数不能实现多态!    
#include <iostream>
using namespace std;
class B
{
public:
    B()
    {
        f();
    }
    virtual void f() { cout<<"父类"<<endl; }

    virtual ~B()
    {
        f();
    }
};
class D:public B
{
public:
    D()
    {

    }
    virtual void f() { cout<<"子类"<<endl;  }
    ~D()
    {

    }
};

int main()
{
    D d;  //试图想在构造函数中实现多态,能实现么?答案:不能!
    return 0;//试图想在析构函数中实现多态,能实现么?答案:不能!
}
//执行结果:父类    父类

这里写图片描述

【答案:面试题8#include <iostream>
using namespace std;
class A
{
public:
    virtual ~A()
    {
        cout<<"析构父类A"<<endl;
    }
};
class B:public A
{
public:
    ~B()
    {
        cout<<"析构子类B"<<endl;
    }
};
int main()
{
    A *p = new B; //给父类的指针new一个子类的对象
    delete p; //在进行delete父类的指针的时候,如果父类的虚函数中不加virtual,则只会调用父类的析构函数,子类的析构函数不会被调用,造成内存泄漏。
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值