0
点赞
收藏
分享

微信扫一扫

重构响应对象

40dba2f2a596 2024-11-06 阅读 23

文章目录

继承

笔试面试题

  1. 什么是菱形继承?菱形继承的问题是什么?
  2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
  3. 继承和组合的区别?什么时候用继承?什么时候用组合?

1. 什么是菱形继承?菱形继承的问题是什么?

菱形继承指的是一个类通过两个子类继承了同一个基类,这两个子类再被另一个派生类继承,形成菱形结构。例如:

class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {};

在这个结构中,D 通过 BC 间接继承了两次 A,这就形成了菱形继承。

菱形继承的问题主要有两个:

  • 数据冗余:由于 D 通过 BC 继承了两份 A,导致存在两份相同的 A 成员。这在内存中会引起冗余,浪费空间。
  • 二义性问题:当在 D 中试图访问 A 的成员时,编译器无法确定该访问来自 B 继承的 A 还是 C 继承的 A,从而导致二义性。例如:
D d;
d._a;  // 编译器不确定是访问 B::_a 还是 C::_a

2. 什么是菱形虚拟继承?如何解决数据冗余和二义性?

菱形虚拟继承是为了解决菱形继承中的数据冗余和二义性问题的一种机制。通过在继承时使用 virtual 关键字,编译器确保只会有一个 A 的实例被共享,而不会有两个冗余的 A 实例。例如:

class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

解决方式

  • 数据冗余问题:通过虚拟继承,D 类中的 A 只会有一份实例,不管是通过 B 还是 C 继承,D 都只包含一份 A。这样消除了冗余。
  • 二义性问题:由于只有一个 A 实例,编译器在 D 中访问 A 的成员时,不会再出现二义性。例如:
D d;
d._a;  // 正常访问唯一的 A 实例

虚拟继承通过虚基表指针实现,BC 各自保存一个指针,指向一个共享的 A 实例,这样就避免了冗余的 A

3. 继承和组合的区别?什么时候用继承?什么时候用组合?

继承组合都是用来复用代码和实现类之间关系的两种手段,但它们的适用场景和概念有明显的区别:

  • 继承(Inheritance):
    • 描述的是“is-a”关系:如果类 B 继承类 A,意味着 BA 的一种特殊类型(比如 Cat 是一种 Animal)。
    • 特点
      • 子类自动继承父类的所有属性和行为。
      • 继承是一种强耦合,子类的行为和父类行为紧密关联。
      • 一旦父类发生变化,子类也会受到影响。
    • 适用场景
      • 当子类是父类的一种类型时,使用继承是合适的。例如:Car 继承 Vehicle
      • 适合复用父类的方法和属性,同时允许子类对父类的行为进行扩展或重写。
  • 组合(Composition):
    • 描述的是“has-a”关系:如果类 B 包含了类 A 的对象,意味着 B 拥有 A 的功能或行为,但 B 并不是 A 的一种类型。例如,Car 包含了一个 Engine,但 Car 不是一种 Engine
    • 特点
      • 组合是弱耦合,每个类保持独立性。
      • 可以动态地替换或改变组合类的行为,不需要修改组合类本身的代码。
    • 适用场景
      • 当一个类的功能可以通过另一类来实现,但它们之间不是类型的关系时,使用组合。
      • 当需要在运行时组合对象行为或功能,而不希望因为继承导致复杂的耦合关系。

总结

  • 继承用于表达“BA”的关系(is-a)。
  • 组合用于表达“B 拥有 A”的关系(has-a)。
  • 当两个类之间有明显的层次关系时,继承是适合的;当一个类需要复用另一个类的功能时,但没有“类型”关系时,使用组合。

选择题

多态

概念考察

  1. 下面哪种面向对象的方法可以让你变得富有( A )
    A: 继承 B: 封装 C: 多态 D: 抽象
  • 解释:这是一道幽默的选择题,实际上并没有哪种面向对象方法可以直接让你变得“富有”。不过这里的“富有”是双关语,隐喻了“继承”在实际编程中可以让你复用父类的属性和方法,就像生活中的“继承财富”一样。通过继承,你可以得到父类的功能,而不需要重新实现,因此继承“让你变得富有”是一个双关的比喻。
  1. (D)是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,
    而对方法的调用则可以关联于具体的对象。
    A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定

解释:动态绑定(Dynamic Binding)是一种多态机制,它允许在程序运行时根据实际对象的类型来调用相应的方法。在编译时,程序并不确定将调用哪个方法,而是在运行时决定。因此,这使得代码更加灵活和可扩展。动态绑定是实现多态的核心机制。

其他选项的解释:

  • A: 继承:继承允许子类从父类继承属性和方法,不能直接实现动态绑定。
  • B: 模板:模板是C++中的一种泛型编程机制,允许在编译时生成代码,并不涉及动态绑定。
  • C: 对象的自身引用:这是对象通过this指针访问自身成员的机制,与动态绑定无关。
  1. 面向对象设计中的继承和组合,下面说法错误的是?(C
    A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复
    用,也称为白盒复用
    B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动
    态复用,也称为黑盒复用
    C:优先使用继承,而不是组合,是面向对象设计的第二原则
    D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封
    装性的表现

解释:这道题考察的是继承与组合的区别,错误的选项是C。实际上,组合优先于继承 是面向对象设计中的一条重要原则,特别是在设计模式中被称为“组合优于继承原则”(Favor composition over inheritance)。过度使用继承会导致代码耦合性增强、灵活性降低。

其他选项的解释:

  • A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用:正确。继承中的复用被称为“白盒复用”,因为子类能够直接看到父类的实现细节。
  • B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用:正确。组合是一种“黑盒复用”,对象间的关系可以在运行时建立,而不需要了解彼此的内部实现。
  • D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现:正确。继承虽然提供了接口复用,但有时会导致父类的封装性被破坏,因为子类可以依赖父类的内部实现。
  1. 以下关于纯虚函数的说法,正确的是(A )
    A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
    C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数

解释:声明了纯虚函数的类被称为抽象类,不能直接实例化对象。抽象类的目的是提供一个接口,并由派生类实现该接口的纯虚函数。

其他选项的解释:

  • B:声明纯虚函数的类是虚基类:错误。纯虚函数与虚基类无关,虚基类用于解决多重继承问题。
  • C:子类必须实现基类的纯虚函数:不完全正确。子类可以选择不实现纯虚函数,但这样它本身也会变成抽象类。
  • D:纯虚函数必须是空函数:错误。纯虚函数只是声明没有实现,但可以有实现,只不过声明部分是= 0
  1. 关于虚函数的描述正确的是(D—>B )
    A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
    C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数

解释:虚函数的多态机制是在运行时实现的,而内联函数是在编译时展开的。这两者是相互冲突的,因此虚函数不能是内联函数。

其他选项的解释:

- **A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型**:错误。虚函数的重写要求派生类中的虚函数与基类的虚函数有相同的参数列表。
- **C:派生类必须重新定义基类的虚函数**:错误。派生类可以选择重写基类的虚函数,<u>但不一定必须重新定义。</u>
- **D:虚函数可以是一个static型的函数**:错误。<u>虚函数是与对象实例相关的,而静态函数与类相关,虚函数不能是静态函数。</u>
  1. 关于虚表说法正确的是( C—>D
    A:一个类只能有一张虚表
    B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
    C:虚表是在运行期间动态生成的
    D:一个类的不同对象共享该类的虚表

解释

  • D:一个类的不同对象共享该类的虚表:正确。虚表(vtable)是针对生成的,而不是针对每个对象生成的,因此同一个类的所有对象共享同一张虚表。对象中的虚表指针(vptr)指向这张虚表。

其他选项的解释:

  • A:一个类只能有一张虚表:错误。如果一个类继承了多个具有虚函数的类,或者自身定义了多个虚函数,可能会有多张虚表存在。
  • B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表:错误。即使子类没有重写虚函数,它依然有自己的虚表,虽然可能会指向相同的虚函数。
  • C:虚表是在运行期间动态生成的:错误。虚表是在编译期生成的,而不是在运行时生成。虚表指针会在运行时动态调整,以指向正确的虚表
  1. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则(B–>D
    A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
    B:A类对象和B类对象前4个字节存储的都是虚基表的地址
    C:A类对象和B类对象前4个字节存储的虚表地址相同
    D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚

解释

  • D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表:正确。A类和B类的虚表中的虚函数数量相同,但由于B类重写了A类的虚函数,B类的虚表会指向B类的实现,而A类的虚表指向A类的实现。因此,虽然虚表中函数的数量相同,虚表本身是不同的。

其他选项的解释:

  • A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址:错误。A类和B类对象的前4个字节都是存储虚表指针(vptr),用于指向各自的虚表。
  • B:A类对象和B类对象前4个字节存储的都是虚基表的地址:错误。这与虚基类无关,A类和B类存储的都是虚表指针。
  • C:A类对象和B类对象前4个字节存储的虚表地址相同:错误。B类重写了A类的虚函数,B类有自己独立的虚表,因此它们的虚表地址不同。
  1. 下面程序输出结果是什么? (A
class A {
public:
	A(char* s) { cout << s << endl; }
	~A() {}
};
class B :virtual public A
{
public:
	B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
	C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
	D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1)
	{
		cout << s4 << endl;
	}
};
int main() {
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

A:class A class B class C class D B:class D class B class C class A

C:class D class C class B class A D:class A class C class B class D

解释:

  • 虚继承会确保在派生类中共享一个唯一的基类实例。因此,当 BC 虚继承了 A 时,在派生类 D 中,A 类的构造函数只会被调用一次
  • 其他就是按顺序来就行了
  1. 多继承中指针偏移问题?下面说法正确的是( C )
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

解释:

d的内存布局类似:
Base1 部分:存储 Base1::_b1
Base2 部分:存储 Base2::_b2
Derive 部分:存储 Derive::_d


Base1* p1 = &d;  // 指向 d 中的 Base1 部分,p1 指向对象 d 的起始地址。
Base2* p2 = &d;  // 指向 d 中的 Base2 部分,这个地址与 p1 不同
Derive* p3 = &d; // 指向 d 整个 Derive 对象的起始地址。与 p1 相同,
                           //因为 Derive 对象的起始部分就是 Base1。

  1. 以下程序输出结果是什么(B
class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

解释:

1.virtual void test() { func(); }

  • testA 类的虚函数,并且 B 没有重写它。因此,当你调用 p->test() 时,它实际上执行的是 A::test()
  • **A::test()**** 中调用了 **func()**,而 **func()** 是虚函数**。虚函数的行为是动态绑定的,意味着根据对象的实际类型来调用合适的函数。在这种情况下,pB* 类型,所以 func() 会调用 B::func()
  1. virtual void func(int val = 1)
  • 虽然 B::func() 覆盖了 A::func(),但有一个非常重要的点:默认参数是静态绑定的,它在编译时绑定。具体来说,A::test() 中的 func() 调用会使用 <u>A</u> 类中定义的默认参数,而不是 B 类中的默认参数。
  • 因此,当 A::test() 调用 func() 时,它使用的是 A 类中给定的默认参数 val = 1,即使实际调用的是 B::func()

调用流程:

  1. p->test() 实际调用的是 A::test()
  2. A::test() 中调用了虚函数 func()。由于 p 指向 B 类对象,动态绑定会让 B::func() 被调用。
  3. 虽然调用的是 B::func(),但 A::test() 使用的是 A 的默认参数 val = 1,因为默认参数是在编译时绑定的。

输出结果:

  • 虽然 B::func(int val = 0) 具有默认参数 0,但由于 A::test() 调用了 func() 并使用了 A 类的默认参数 1,因此程序输出的是:B->1

问答题

作为面试者,我将依次回答上列面试题:

1. 什么是多态?

多态是面向对象编程中的一种特性,它允许同一个函数或方法在不同对象上具有不同的行为。在 C++ 中,多态主要有两种形式:

  • 编译时多态(静态多态):通过函数重载和运算符重载实现。
  • 运行时多态(动态多态):通过继承和虚函数机制实现。当基类的指针或引用指向派生类对象时,调用虚函数会根据实际对象的类型执行不同的实现。

多态的核心目的是提高代码的可扩展性和复用性。

2. 什么是重载、重写(覆盖)、重定义(隐藏)?

  • 重载(Overloading):是同一个作用域内,允许函数同名但参数类型或参数个数不同的现象。编译器通过参数列表的不同进行区分,这是静态多态的一种形式。
    • 例子:
void func(int a);
void func(double a);
  • 重写(Overriding):也称为覆盖,是派生类重新实现基类中的虚函数,必须保持函数签名完全相同。通过这种方式,基类的指针或引用在运行时调用派生类的实现,这是动态多态的实现方式。
    • 例子:
class Base {
public:
    virtual void func() { cout << "Base"; }
};
class Derived : public Base {
public:
    void func() override { cout << "Derived"; }
};
  • 重定义(Hiding):是派生类中的同名函数隐藏了基类中的非虚函数或静态成员函数。尽管函数签名不同,基类函数不再可见。可以通过作用域分辨符 Base::func() 访问基类函数。
    • 例子:
class Base {
public:
    void func(int a) { cout << "Base"; }
};
class Derived : public Base {
public:
    void func(double a) { cout << "Derived"; }
};

3. 多态的实现原理?

多态的实现基于 虚函数表(vtable)虚函数指针(vptr)

  • 虚函数表:对于包含虚函数的类,编译器会为该类生成一个虚函数表,表中存储该类的虚函数地址。
  • 虚函数指针:每个对象都会有一个虚函数指针(vptr),指向其所属类的虚函数表。当通过基类指针或引用调用虚函数时,编译器通过该指针找到对象对应的虚函数表,从而在运行时调用正确的派生类函数。

这一机制支持运行时根据对象的实际类型执行相应的虚函数。

4. inline 函数可以是虚函数吗?

可以,但是编译器通常会忽略虚函数的 inline 特性。当函数被声明为虚函数时,它必须通过虚函数表来调用,而不是内联替换。虚函数调用涉及动态绑定,无法直接替换为内联代码,因此虚函数即使被声明为 inline,在大多数情况下也不会被内联。

5. 静态成员可以是虚函数吗?

不能。原因如下:

  • 静态成员函数 不属于任何对象,它们不依赖于具体的实例,也没有 this 指针。
  • 虚函数 依赖于对象的 this 指针,通过 vtable(虚函数表)来实现动态绑定,而静态成员函数无法访问虚函数表。因此,静态成员函数不能是虚函数。
class Base {
public:
    virtual void func() { // 普通虚函数
        cout << "Base::func() called" << endl;
    }

    static void staticFunc() { // 静态成员函数
        cout << "Base::staticFunc() called" << endl;
    }
};

class Derived : public Base {
public:
    void func() override { // 重写虚函数
        cout << "Derived::func() called" << endl;
    }

    // 重定义静态成员函数
    static void staticFunc() {
        cout << "Derived::staticFunc() called" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    
    //通过虚函数表实现了动态绑定,ptr 实际指向 Derived 对象
    // 调用虚函数,运行时绑定,输出 Derived::func()
    ptr->func();

    // 静态成员函数是基于类名调用的,不能通过指针动态绑定
    // ptr->staticFunc(); // 错误!静态成员函数不能通过对象指针调用

    // 必须用类名调用静态成员函数,静态成员函数没有虚表,不能实现多态
    Base::staticFunc();    // 输出 Base::staticFunc() called
    Derived::staticFunc(); // 输出 Derived::staticFunc() called

    delete ptr;
    return 0;
}

6. 构造函数可以是虚函数吗?

答:不能,原因是对象中的虚函数表指针(vptr)是在构造函数初始化列表阶段才初始化的。在对象构造过程中,虚函数表还未完成设置,此时如果调用虚函数,会无法正确绑定到具体的函数实现。因此,构造函数无法是虚函数。

详细解释:

  • 虚函数的作用是在运行时实现动态绑定(多态),但在构造函数执行时,类的虚函数表指针尚未被正确设置。因为在对象创建时,基类部分的构造函数先执行,这时候派生类部分还没被初始化,如果基类构造函数是虚函数,就会产生不一致的行为。

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

答:可以,并且在多态场景下,基类的析构函数最好定义为虚函数。

  • 如果一个类可能作为基类被继承,并且会通过基类指针或引用指向派生类对象,在这种情况下,基类的析构函数需要是虚函数。否则,当通过基类指针删除派生类对象时,只有基类的析构函数会被调用,而派生类的析构函数不会被执行,从而导致资源泄漏

场景:

class Base {
public:
    virtual ~Base() { cout << "Base destructor called" << endl; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "Derived destructor called" << endl; }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 如果Base的析构函数不是虚函数,这里只会调用Base的析构函数,导致派生类的析构函数不执行。
    return 0;
}

输出:

Derived destructor called
Base destructor called

如果基类的析构函数不是虚函数,Derived 类的析构函数将不会被调用,造成内存泄漏或其他资源释放问题。

8. 对象访问普通函数快还是虚函数更快?

答:普通函数更快

  • 如果是通过普通对象调用函数,普通函数和虚函数的访问速度是一样的。
  • 但在通过指针或引用调用时,普通函数更快。调用虚函数需要动态绑定,这意味着编译时不能确定具体要调用哪个函数,必须通过查找虚函数表(vtable)来找到对应的函数指针,因此虚函数调用会略慢一些。普通函数则在编译时就能确定,不需要查表,直接调用。

例子:

class Base {
public:
    virtual void virtualFunc() { cout << "Base::virtualFunc" << endl; }
    void normalFunc() { cout << "Base::normalFunc" << endl; }
};

int main() {
    Base b;
    Base* ptr = &b;

    ptr->normalFunc();  // 普通函数,直接调用
    ptr->virtualFunc(); // 虚函数,查表调用

    return 0;
}

对于普通函数调用,编译器直接生成调用指令;而虚函数则需要通过虚函数表查找,稍微增加了开销。

9. 虚函数表是在什么阶段生成的,存在哪?

答:虚函数表(vtable)是在编译阶段生成的

虚函数表一般存储在代码段(常量区)。每个带有虚函数的类在编译时会生成一个虚函数表,虚函数表中存储了该类的所有虚函数的地址。每个对象有一个虚表指针(vptr),指向虚函数表的位置。编译器在构造对象时,会初始化虚表指针(vptr),指向类对应的虚函数表。

  • 在程序运行时,如果通过指针或引用调用虚函数,编译器会通过对象的 vptr 查找虚函数表中的函数地址,从而实现动态绑定。

总结:

  • 编译阶段生成虚函数表。
  • 虚函数表一般存储在代码段,属于程序的常量区域。

10. C++菱形继承的问题?虚继承的原理?

菱形继承问题:

C++ 中的菱形继承是一种特殊的多继承结构,它是指一个类从两个基类继承,而这两个基类又继承自同一个父类。菱形继承引发了两个主要问题:

  1. 数据冗余问题
    当派生类通过多条路径继承同一个基类时,基类的成员变量会在派生类中出现多份副本。这意味着,派生类对象中会有多份相同的基类成员,导致内存浪费和逻辑混乱。
  2. 二义性问题
    由于派生类通过多个路径继承基类,编译器可能无法确定调用哪个基类的成员函数,尤其是当基类有同名成员时。例如,编译器会遇到二义性:调用 A::func() 时是从 B 继承的 A 版本,还是从 C 继承的 A 版本。

例子:

class A {
public:
    int x;
    void func() { cout << "A's func" << endl; }
};

class B : public A {};
class C : public A {};
class D : public B, public C {};

int main() {
    D obj;
    // obj.x 会导致二义性,编译器不确定是B::A::x还是C::A::x
    // obj.func() 也会导致二义性
}

虚继承的原理:

虚继承是为了解决上述菱形继承中的问题。通过虚继承,基类的成员在继承路径上只保留一份拷贝,从而解决了数据冗余和二义性问题。

  • 虚继承声明:在继承时,基类前加上 virtual 关键字,表示对该基类进行虚继承。
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
  • 虚基表:虚继承的实现依赖于虚基表(Virtual Base Table,简称 VBT)。虚基表存储了虚基类成员在派生类中的偏移量。当派生类访问虚基类的成员时,会通过虚基表找到正确的偏移,从而解决了菱形继承中的二义性问题。虚基表的工作原理是:
    • 当类进行虚继承时,派生类会为虚基类保留一份特殊的偏移量表,即虚基表。每个虚继承的类都会有指向虚基表的指针。
    • 在访问虚基类的成员时,编译器会通过该指针找到虚基表,进而计算虚基类成员的实际地址,避免产生冗余的副本。

虚继承的好处:

  • 只保留一份基类成员,节省内存,避免数据冗余。
  • 消除了访问基类成员的二义性,解决了编译时的歧义。

11. 什么是抽象类?抽象类的作用?

抽象类定义:

抽象类是包含纯虚函数的类。纯虚函数是一个没有实现的虚函数,其定义如下:

virtual void func() = 0;

任何包含至少一个纯虚函数的类都称为抽象类。抽象类不能被实例化,必须通过派生类来实现其中的纯虚函数。

抽象类的作用:

  1. 强制子类实现特定功能
    抽象类通过定义纯虚函数,强制派生类必须提供这些函数的具体实现。这种设计确保了某些行为在派生类中一定会被实现。
  2. 提供接口继承
    抽象类体现了接口继承的概念,即抽象类定义了一组功能的接口,而派生类实现具体功能。通过这种方式,可以实现面向接口编程,减少对具体实现的依赖,从而提高代码的扩展性和可维护性。
  3. 实现多态
    抽象类是实现多态的重要手段。使用抽象类的指针或引用,可以在运行时通过动态绑定调用派生类的具体实现。

例子:

class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数
};

class Circle : public Shape {
public:
    void draw() override { cout << "Drawing a circle" << endl; }
};

class Rectangle : public Shape {
public:
    void draw() override { cout << "Drawing a rectangle" << endl; }
};

int main() {
    Shape* shape1 = new Circle();
    Shape* shape2 = new Rectangle();
    shape1->draw();  // 动态绑定,调用Circle的draw
    shape2->draw();  // 动态绑定,调用Rectangle的draw
    delete shape1;
    delete shape2;
}

抽象类通过纯虚函数为派生类提供了统一的接口,同时也使得派生类必须实现这些接口,从而保证了多态的实现。

选择题

class Base {
public:
    virtual void show();  // 声明时加 virtual
};

// 定义时无需加 virtual
void Base::show() {
    cout << "Base show" << endl;
}

class A{ 
public: 
    void test(float a) { cout << a; } 
}; 

class B : public A{ 
public: 
    void test(int b) { cout << b; } 
}; 

void main() { 
    A *a = new A; 
    B *b = new B; 
    a = b; 
    a->test(1.1); 
}

class Base {
public:
    virtual void pureVirtualFunction() = 0; // 纯虚函数
    virtual void implementedFunction() {
        // 有函数体的虚函数
    }
};

class Base {
public:
    virtual void func() { std::cout << "Base::func()" << std::endl; }
};

int main() {
    Base obj1, obj2;
    // 检查虚表地址
    std::cout << "obj1 virtual table address: " << *(void**)&obj1 << std::endl;
    std::cout << "obj2 virtual table address: " << *(void**)&obj2 << std::endl;
    // 确认共享同一虚表
    if (*(void**)&obj1 == *(void**)&obj2) {
        std::cout << "Both objects share the same virtual table." << std::endl;
    }
    else {
        std::cout << "Objects do not share the same virtual table." << std::endl;
    }
    return 0;
}
7.下面函数输出结果是( )
class A {
public: 
    virtual void f() {
        cout << "A::f()" << endl;
    }
};

class B : public A {
private://注意是 私有的
    virtual void f() {
        cout << "B::f()" << endl;
    }
};

int main() {
    A* pa = (A*)new B; // 强制类型转换,将 B 的对象指针转为 A 的指针
    pa->f(); // 调用虚函数 f
    return 0;
}

A.B::f()
B.A::f(),因为子类的f()函数是私有的
C.A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象
D.编译错误,私有的成员函数不能在类外调用

函数的输出结果是 B::f(),原因如下:

多态的工作原理

  1. 虚函数:类 A 中的 f() 是一个虚函数,意味着可以通过基类指针调用派生类的实现。即使派生类 Bf() 函数是私有的,编译器在处理虚函数调用时,仍然会查找对象的虚表,找到实际指向的 B 类的 f()
  2. 强制类型转换A* pa = (A*)new B; 这行代码将 B 类的对象强制转换为 A 类的指针。虽然 Bf() 是私有的,但由于 pa 实际上指向的是一个 B 类型的对象,因此在调用 pa->f() 时,程序会根据对象的实际类型(<font style="color:#DF2A3F;">B</font>)来决定调用哪个版本的 f()
  3. 访问权限与多态:虽然 Bf() 是私有的,但这并不妨碍通过 A 的指针调用它。C++ 的多态机制依赖于虚表(vtable)来查找方法,确保调用的是对象的真实类型的方法,而不是指针类型的方法。因此,尽管 <font style="color:#DF2A3F;">f()</font> 是私有的,<font style="color:#DF2A3F;">pa->f()</font> 调用的依然是 <font style="color:#DF2A3F;">B</font><font style="color:#DF2A3F;">f()</font>

选项分析

  • A. B::f():正确,因为实际调用的是 B 类的 f() 方法。
  • B. A::f(),因为子类的f()函数是私有的:错误,因为私有性只影响访问权限,而不影响多态调用。
  • C. A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象:错误,pa 指向的是 B 类型的对象。
  • D. 编译错误,私有的成员函数不能在类外调用:错误,因为访问权限不影响多态的运行时行为。

以下程序输出结果是( )
class A
{
public:
  A(): m_iVal(0) { test(); }  // 1
  virtual void func() { std::cout << m_iVal << ' '; } // 2
  void test() { func(); } // 3

public:
  int m_iVal;
};

class B : public A
{
public:
  B() { test(); } // 4
  virtual void func() { ++m_iVal; std::cout << m_iVal << ' '; } // 5
};

int main(int argc, char* argv[])
{
  A* p = new B; // 6
  p->test(); // 7
  return 0;
}
A.1 0
B.0 1
C.0 1 2
D.2 1 0
E.不可预期
F. 以上都不对

  1. 📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
  2. 本人也很想知道这些错误,恳望读者批评指正!
  3. 我是:勇敢滴勇~感谢大家的支持!

举报

相关推荐

0 条评论