目录
一、多态的定义及实现
1、多态的构成条件
- 比如Student继承了 Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
- 虚函数的重写:三同(函数名、参数、返回值类型)
- 例外(协变):返回值可以不同,必须是父子关系指针或者引用。
- 例外:子类虚函数可以不加virtual。
- 父类指针或者引用去调用。
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
// 重写(覆盖)
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
2、虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数
class Person {
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
3、虚函数的重写
- 在C++中,通过在基类中使用关键字"virtual"来声明一个函数为虚函数。当一个函数被声明为虚函数后,它可以在派生类中被重写。
- 这意味着如果派生类中定义了一个与基类中虚函数的返回值类型、函数名字、参数列表完全相同的函数,那么在通过基类指针或引用调用该函数时,实际上会根据对象的实际类型来决定调用哪个版本的函数。
接下来我们看看满足和不满足多态的两种情况:
(1)满足多态
看指向的对象的类型,调用这个类型的成员函数
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
(2)不满足多态
看调用者的类型,调用这个类型的成员函数
class Person {
public:
void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student :public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
(3)不加virtual构成重写的特殊情况
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了,在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。 (重写=接口继承+重写继承父类这个函数的实现)
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
父类的析构函数加virtual,子类不加也够成多态。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
void BuyTicket() { cout << "买票-半价" << endl; }
~Student()
{
cout << "~Student()" << endl;
}
};
void Func(Person* p)
{
p->BuyTicket();
delete p;
}
int main()
{
Func(new Person);
Func(new Student);
return 0;
}
(4)两个例外:协变和析构函数
- 协变返回值可以不同,必须是父子关系指针或者引用
- 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A {};
class B : public A {};
class Person {
public:
virtual A* f() { return new A; }
};
class Student : public Person {
public:
virtual B* f() { return new B; }
};
2. 析构函数的重写(基类与派生类析构函数的名字不同)
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
![](https://img-blog.csdnimg.cn/direct/b483e04879e94df8a64fe918acfc89fa.png)
4、例题
下面程序的输出结果是什么呢
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 定义了一个带默认参数
val = 1
的虚函数func
,以及一个test
函数,后者调用func()
(没有指定参数,所以使用默认参数)。 - 类B 继承自类A,并重写了
func
函数,新的默认参数为val = 0
。
虚函数调用:
- 在面向对象编程中,当函数被声明为
virtual
,它就可以在派生类中被重写(覆盖),并且当通过基类指针或引用调用时,实际调用的是派生类中重写的版本,这称为“多态”。
多态行为:
- 在
main
函数中,创建了一个指向B类对象的指针p
,并调用了p->test()
。 test
函数在基类A中定义,且没有在B中被重写,因此调用的是A中的test
版本。- 在
A::test
中,调用了func()
。由于func
是虚函数,并且此时的对象实际上是B类的对象,所以调用的是B::func
。
默认参数的选择:
- 关键点在于,虚函数的多态行为不影响默认参数的使用。默认参数的值是在编译时确定的,依据的是函数调用的静态类型,而不是运行时的类型。
- 尽管
B::func
被调用,但是因为调用func()
是在A的成员函数test
中,编译器选择的默认参数值是func
在A中声明时使用的值(即val = 1
)。
输出解释:
- 结果
B->1
的原因是:调用B::func
时使用的是A::func
定义时指定的默认参数值1
,而不是B::func
中指定的默认参数值0
。
如果test()函数在类B中调用,输出结果是什么呢?
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
virtual void test() { func(); }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
这里没有通过基类的指针或引用调用test()函数,是通过类B的指针p
的this指针来调用test
函数。
5、C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否写。
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
![](https://img-blog.csdnimg.cn/direct/e3780918ac4e41ecb8f17e5b86ab33a7.png)
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
void Drive() { cout << "Benz-速度快" << endl; }
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
Car p;
p.Drive();
Benz q;
q.Drive();
return 0;
}
6、重载、覆盖(重写)、隐藏(重定义)的对比
重载:
定义:在同一作用域内,函数名称相同,但参数列表不同(参数个数、类型或顺序不同),这样的函数称为重载函数。重载发生在同一个类中,针对的是具有相同名称但参数不同的函数。
示例:
class MyClass {
public:
void func(int a) { cout << "func with int: " << a << endl; }
void func(double b) { cout << "func with double: " << b << endl; }
};
重写(覆盖):
定义:在继承关系中,子类重新定义了父类中已经被声明为虚函数(virtual)的方法。子类中的方法与父类中同名、同参数列表的方法具有相同的签名,且子类方法的访问修饰符不得低于父类方法。通过基类指针或引用调用虚函数时,会根据实际指向的对象动态决定调用哪个方法。
示例:
class Base {
public:
virtual void print() { cout << "Base print" << endl; }
};
class Derived : public Base {
public:
void print() override { cout << "Derived print" << endl; }
};
隐藏(重定义):
定义:在继承关系中,子类定义了一个与父类成员(函数或变量)同名的成员,如果没有明确指出是重写父类的虚函数,那么子类中的同名函数会隐藏父类中的同名函数。如果调用时通过子类对象直接访问,就会调用子类的函数,而不是父类的。
-
成员函数隐藏:
class Base { public: void method() { cout << "Base method" << endl; } }; class Derived : public Base { public: void method() { cout << "Derived method (hiding)" << endl; } // 隐藏了父类的method };
-
数据成员隐藏:
class Base { public: int value = 10; }; class Derived : public Base { public: int value = 20; // 隐藏了父类的value变量 };
总结:
- 重载:同名不同参,发生在同一个类中,编译时确定调用哪一个函数。
- 覆盖(重写):发生在继承关系中,子类重新定义父类虚函数,运行时动态绑定,根据对象的实际类型决定调用哪一个函数。
- 隐藏(重定义):也发生在继承关系中,子类定义与父类同名成员,若非虚函数,则隐藏父类同名函数;如果是数据成员,则子类拥有独立的同名数据成员。
二. 抽象类(纯虚函数)
1、概念
class Car
{
public:
// 纯虚函数 -- 抽象类 -- 不能实例化出对象
virtual void Drive() = 0;
};
int main()
{
Car car;
return 0;
}
2、应用
纯虚函数
-
接口定义: 纯虚函数允许你在基类中定义一组接口方法,而不需要提供具体实现。这使得程序员能够专注于设计类的结构和职责,而不必立即关心其实现细节。
-
强制多态: 强制任何继承此类的子类必须实现这些纯虚函数。这有助于确保所有派生类遵循某种共同的行为规范,增加了代码的一致性和可维护性。
-
设计灵活性: 设计初期,某些功能的具体实现可能尚不明确,但你希望先定义出接口。纯虚函数允许你先定义接口框架,推迟具体实现到后来或子类中。
抽象类
-
作为接口: 抽象类本身不能实例化,但可以作为接口的实现方式。它提供了一种组织和封装相关操作的方式,使得代码更加模块化和易于理解。
-
行为共享: 虽然抽象类中包含纯虚函数,但它也可以包含非纯虚函数和数据成员。这样,派生类不仅可以继承接口规范,还可以继承和复用一些基础的实现逻辑。
-
层次结构构建: 抽象类有助于构建类的层次结构。在复杂的系统中,通过定义一系列的抽象类作为不同层级的接口,可以构建出清晰的类继承关系,便于管理和扩展。
-
促进设计思考: 使用抽象类和纯虚函数迫使开发者在设计阶段深入思考类之间的关系和责任分配,促进了良好的面向对象设计实践,如单一职责原则和里氏替换原则。
举例讲解
让我们通过一个具体的例子来讲解纯虚函数和抽象类在C++中的应用。设想我们正在设计一个图形编辑器,其中有很多不同类型的形状(Shape),如圆形(Circle)、矩形(Rectangle)等。我们希望所有的形状都能提供一个共同的操作——计算自身的面积(Area),但具体的计算方式依赖于形状的类型。这就非常适合使用抽象类和纯虚函数来实现。
定义抽象类 Shape
首先,我们定义一个抽象类 Shape
,里面包含一个纯虚函数 getArea()
,用于计算面积。
class Shape {
public:
// 纯虚函数,要求所有派生类必须实现
virtual double getArea() const = 0;
// 虚析构函数,确保通过基类指针删除派生类对象时能正确调用派生类的析构函数
virtual ~Shape() {}
protected:
// 可以包含一些辅助数据成员或保护成员函数,但在这个例子中未展示
};
派生类实现
接下来,我们定义两个派生类 Circle
和 Rectangle
,每个类都实现 getArea()
函数。
class Circle : public Shape {
public:
Circle(double radius) : radius_(radius) {}
// 实现 Shape 中的纯虚函数
double getArea() const override {
return 3.14159 * radius_ * radius_;
}
private:
double radius_;
};
class Rectangle : public Shape {
public:
Rectangle(double width, double height) : width_(width), height_(height) {}
// 实现 Shape 中的纯虚函数
double getArea() const override {
return width_ * height_;
}
private:
double width_, height_;
};
使用抽象类
现在,我们可以使用这些类来创建形状对象,并通过基类指针来调用它们的共同接口,而无需知道具体类型。
int main() {
Shape* shapes[] = {
new Circle(5), // 创建圆形,半径为5
new Rectangle(4, 6) // 创建矩形,宽4高6
};
for (Shape* shape : shapes) {
cout << "Area: " << shape->getArea() << endl; // 多态性:调用各自实现的getArea()
delete shape; // 记得释放内存
}
return 0;
}
输出结果
这段代码会输出每个形状的面积:
Area: 78.53975
Area: 24
通过这个例子,可以看到纯虚函数和抽象类是如何帮助我们设计出既灵活又易于扩展的代码结构。我们可以轻松添加更多形状类型(如三角形、正方形等),只要它们继承自 Shape
并实现 getArea()
即可,而无需修改现有代码。这就是面向对象设计中抽象类和纯虚函数的强大之处。
3、接口继承和实现继承
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
- 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
三、多态的原理
1、虚函数表(虚表)
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
接下来观察下面代码:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚 表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外,基类中的
Func2
被继承下来后仍然是虚函数,所以它也被放入了派生类的虚表中。Func3也继承下来了,但是不是虚函数,所以不会放进虚表。 - 虚函数表本质是一个存虚函数指针的指针数组,通常在数组的最后放置一个
nullptr
指针(vs系列编译器有,g++没有)。 - 总结一下派生类的虚表生成过程: a. 首先将基类中的虚表内容拷贝一份到派生类的虚表中。 b. 如果派生类重写了基类中的某个虚函数,就用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址。 c. 派生类自己新增加的虚函数按照在派生类中的声明顺序增加到派生类虚表的最后。
- 需要注意的是,虚函数本身是存在代码段中的,而虚表存储的是虚函数指针。对象中存储的是虚表指针,而不是虚表本身。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。
同类型对象共用一个虚表 :
int main()
{
Base b1;
Base b2;
Base b3;
Derive d;
return 0;
}
2、多态的原理
![](https://img-blog.csdnimg.cn/direct/406ee0077fa14d759de3332cfb46eec0.png)
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
![](https://img-blog.csdnimg.cn/direct/b55622eb71fd47e8b443b8c7b85f5b8c.png)
3、多态是面向对象编程中的一个重要概念,它允许不同类型的对象通过相同的接口来实现不同的行为。在C++中,多态性通过虚函数和对象的指针或引用来实现。
- 在上述代码中,有一个基类
Person
和一个派生类Student
。基类中定义了一个虚函数BuyTicket
,派生类中重写了该虚函数。 - 在
Func
函数中,参数是基类Person
的引用。当我们通过基类的引用调用Func
函数时,可以传递基类对象或派生类对象。在函数内部,通过引用调用对象的虚函数BuyTicket
。 - 在
main
函数中,创建了一个基类对象Mike
和一个派生类对象Johnson
。然后分别将它们作为参数传递给Func
函数。 - 当
Func
函数中的参数是基类引用时,根据对象的实际类型,会调用相应的虚函数。对于Mike
对象,调用的是基类Person
中的BuyTicket
函数;对于Johnson
对象,调用的是派生类Student
中重写的BuyTicket
函数。
4、这就是多态的实现原理。通过虚函数的覆盖和对象的指针或引用调用虚函数,实现了在运行时根据对象的实际类型来确定调用的函数。
5、在汇编代码中,可以看到函数调用并不是在编译时确定的,而是在运行时通过对象的虚表来查找相应的虚函数。这就是多态的特性,使得不同对象在执行相同行为时展现出不同的形态。
6、总结一下,多态的实现需要满足两个条件:虚函数的覆盖和对象的指针或引用调用虚函数。通过这种方式,可以实现不同对象在执行相同行为时表现出不同的形态。
2、打印虚表
class Base
{
public:
Base()
:_b(10)
{
++_b;
}
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
typedef void(*VF_PTR)();
void PrintVFTable(VF_PTR table[])
{
for (int i = 0; table[i] != nullptr; i++) {
printf("[%d]:%p->", i, table[i]);
VF_PTR f = table[i];
f();
}
}
int main()
{
Base b;
Derive d;
//32位
//PrintVFTable((VF_PTR*)*(int*)&b);
//PrintVFTable((VF_PTR*)*(int*)&d);
//64位
//PrintVFTable((VF_PTR*)*(long long*)&b);
//PrintVFTable((VF_PTR*)*(long long*)&d);
//32位/64位均可
PrintVFTable(*(VF_PTR**)&b);
PrintVFTable(*(VF_PTR**)&d);
return 0;
}
PrintVFTable(*(VF_PTR**)&b)
这行代码是在调用PrintVFTable
函数,并将b
对象的地址转换为指向虚函数表的指针,然后将该指针作为参数传递给PrintVFTable
函数。- 在这段代码中,
PrintVFTable
函数用于打印虚函数表的内容。它接受一个指向虚函数表的指针数组作为参数,并遍历数组中的每个元素,打印出函数指针的地址,并调用对应的函数。 *(VF_PTR**)&b
这个表达式的含义是将b
对象的地址转换为指向虚函数表的指针。首先,&b
获取b
对象的地址,然后通过(VF_PTR*)
将其转换为指向VF_PTR
类型的指针,最后再通过*
解引用操作符获取指针所指向的虚函数表的地址。- 通过这样的转换,我们可以将
b
对象的地址作为参数传递给PrintVFTable
函数,以便打印出Base
类的虚函数表的内容。 - 注释部分是根据不同的编译环境选择不同的转换方式,因为在32位和64位系统中,指针的大小不同。
64位环境下输出:
对象中虚表指针什么时候初始化的? -- 构造函数的初始化列表
接下来跳转到构造函数
构造函数结束,虚表指针初始化完成,
虚表存储在哪?—常量区(数据段)
int main()
{
Base b;
Derive d;
int x = 0;
static int y = 0;
int* z = new int;
const char* p = "xxxxxxxxxxxxxxxxxx";
printf("栈对象:%p\n", &x);
printf("堆对象:%p\n", z);
printf("静态区对象:%p\n", &y);
printf("常量区对象:%p\n", p);
printf("b对象虚表:%p\n", *((int*)&b));
printf("d对象虚表:%p\n", *((int*)&d));
return 0;
}
3、多继承中的虚函数表
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VF_PTR)();
//void PrintVFTable(VF_PTR table[])
void PrintVFTable(VF_PTR* table)
{
for (int i = 0; table[i] != nullptr; i++) {
printf("[%d]:%p->", i, table[i]);
VF_PTR f = table[i];
f();
}
}
int main()
{
Derive d;
PrintVFTable((VF_PTR*)(*(int*)&d));
//PrintVFTable((VF_PTR*)(*(int*)((char*)&d+sizeof(Base1))));
Base2* ptr2 = &d;
PrintVFTable((VF_PTR*)(*(int*)(ptr2)));
return 0;
}
在C++中,每个具有虚函数的类都有一个虚函数表(vtable),但是这个表是类级别的,而不是对象级别的。也就是说,所有同一类型的对象共享同一个虚函数表,而不是每个对象都有自己的虚函数表。
- 在上面的代码中,
Derive
类继承自Base1
和Base2
,并且覆盖了Base1
和Base2
的func1
函数。因此,Derive
类的虚函数表将包含指向Derive::func1
、Base1::func2
、Base2::func2
和Derive::func3
的指针。 - 当创建一个
Derive
对象d
时,d
的内存布局中将包含两个虚函数表指针(vptr),一个指Base1
的虚函数表,一个指向Base2
的虚函数表。这是因为Derive
类从两个基类继承,每个基类都有自己的虚函数表。 - 在上面的代码中,通过将
d
的地址转换为int
指针,然后解引用它来获取虚函数表的地址。这实际上获取的是d
的第一个虚函数表指针,也就是指向Base1
的虚函数表的指针。如果想获取d
的第二个虚函数表指针,也就是指向Base2
的虚函数表的指针,需要将d
的地址加上Base1
的大小,或者使用Base2指针指向d的地址进行切片,切出d中Base2的部分,然后再进行转换和解引用。
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
- 如果
Derive
类只从Base1
或Base2
单继承,那么Derive
类会有自己的虚函数表。这个虚函数表会包含Derive
类自己定义或覆盖的虚函数,以及从基类继承的所有虚函数。 - 例如,如果
Derive
类只从Base1
继承,那么Derive
类的虚函数表会包含func1()
和func2()
,因为这两个函数在Base1
类中被声明为虚函数。如果Derive
类覆盖了这些函数,那么虚函数表中对应的条目会被更新为指向Derive
类的函数版本。
4、动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。
四、例题
下面说法正确的是()
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;
}
-
p1 == p3
:这是因为p1
是指向Base1
类型的指针,而p3
是指向Derive
类型的指针。由于Derive
继承了Base1
,所以它们都指向同一个地址,即_b1
成员的位置。因此,p1
和p3
相等。 -
p3 != p2
:同样是因为p2
是指向Base2
类型的指针,它指向_b2
成员的位置。然而,p3
是指向Derive
类型的指针,它指向整个对象d
的起始位置。由于_b2
在_b1
之后,所以p3
和p2
不相等。
下面程序的输出结果()
#include <iostream>
class A {
public:
A() : m_iVal(0) { test(); }
virtual void func() { std::cout << m_iVal << ' '; }
void test() { func(); }
public:
int m_iVal;
};
class B : public A {
public:
B() { test(); }
virtual void func() {
++m_iVal;
std::cout << m_iVal << ' ';
}
};
int main(int argc, char* argv[]) {
A* p = new B;
p->test();
delete p;
return 0;
}
这是一个关于C++中的继承和虚函数的例子。在这个例子中,我们有两个类,A和B,其中B是A的子类。这两个类都有一个名为func
的函数,但在B中,这个函数被重写了。同时,这两个类都有一个名为test
的函数,它调用了func
函数。在类A的构造函数中,调用了test
函数。
现在,让我们逐步解析这个程序的执行过程:
-
在
main
函数中,我们创建了一个指向B类对象的A类指针p
。这是通过new B
实现的。 -
创建B类对象时,首先会调用其基类A的构造函数。在A的构造函数中,
m_iVal
被初始化为0,然后调用test
函数。 -
test
函数调用func
函数。这里需要注意的是,虽然此时正在创建B类对象,但是因为在A的构造函数中调用的func
,所以此时的func
是A类的func
,而不是B类的func
。因此,此时输出的是0。 -
A类的构造函数执行完毕后,开始执行B类的构造函数。在B类的构造函数中,也调用了
test
函数。 -
但是此时,因为B类对象已经创建完毕,所以调用的
func
是B类的func
。在B类的func
中,m_iVal
被增加1,然后输出。因此,此时输出的是1。 -
B类的构造函数执行完毕后,回到
main
函数。在main
函数中,通过A类指针p
调用test
函数。 -
因为
p
实际指向的是B类对象,所以此时调用的func
是B类的func
。因此,m_iVal
再次被增加1,然后输出。此时输出的是2。 -
最后,删除
p
指向的对象,并返回0。
所以,这个程序的输出应该是 "0 1 2 "。
下面程序的输出结果()
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;
pa->f();
}
这是一个关于C++中的继承、虚函数和访问控制的例子。在这个例子中,我们有两个类,A和B,其中B是A的子类。这两个类都有一个名为f
的函数,但在B中,这个函数被重写了。同时,B类中的f
函数被声明为私有的。
现在,让我们逐步解析这个程序的执行过程:
-
基类A的定义:类A中有一个公共的虚函数
f()
。由于它是虚函数,所以在派生类中重写它时,通过基类指针调用该函数会表现出多态性,即根据指针实际所指对象的类型来决定调用哪个版本的函数。 -
派生类B的定义:类B公开继承自类A,并重写了函数
f()
。关键在于这里将f()
声明为了private
(私有的)。在C++中,访问权限控制非常严格,private
成员只能被其所在类的成员函数和友元访问,即使是派生类或基类也无法直接访问。 -
在main函数中的操作:通过类型转换
(A*)new B
创建了一个B类的对象,并将其地址存储在一个A类型的指针pa
中。这一操作是合法的,因为B是A的公开派生类。然后尝试通过这个基类指针调用函数f()
。 -
问题所在:按照多态的规则,理论上应该调用B类中重写的
f()
函数。但是,由于B类中的f()
是私有的,它不能被A类(或任何非友元、非成员的外部代码)通过基类指针或引用来访问。因此,尽管动态类型为B,但由于访问权限的限制,这段代码在尝试编译时会遇到错误,指出无法访问B::f()
。
所以,这个程序在编译时会报错,因为试图访问一个私有成员函数。如果想让这个程序能够正确运行,需要将B类的f
函数声明为公有的或保护的。