上机四 运行时的多态性和抽象类程序设计
上机目的
1.理解多态性的概念。
2.掌握运行时的多态性概念和设计方法。
3.掌握虚函数的使用方法。
实验目的
理解多态性的概念。
掌握运行时的多态性概念和设计方法。
掌握虚函数的使用方法。
实验内容
P187:6.10,6.11
上机内容(p187 6.11)
编写计算正方体、圆柱体、球体的表面积和体积的类。要求:
(1) 这三个类有一个公共的基类;
(2) 这三个类计算正方体、圆柱体、球体的表面积和体积的成员函数名字相同;
(3) 按照运行时的多态性方法设计一个测试主函数,并进行测试。
正确源码
#include<iostream> using namespace std; class basic { public: virtual double superficial_area() = 0; virtual double volume() = 0; ~basic() {} }; class cube :public basic { private: double length; public: cube(double a):length(a) {} double superficial_area() { return(6 * length * length); } double volume() { return(length * length * length); } ~cube() {} }; class cylinder :public basic { private: double radius; double high; public: cylinder(double a, double b):radius(a), high(b) {} double superficial_area() { return ((2 * 3.1415 * radius * radius) + (2 * 3.1415 * radius * high)); } double volume() { return (3.1415 * radius * radius * high); } ~cylinder() {} }; class globe :public basic { private: double radius; public: globe(double a):radius(a) {} double superficial_area() { return (4 * 3.1415 * radius * radius); } double volume() { return (((3.1415 * 4) / 3) * radius * radius * radius); } ~globe() {} }; int main() { int a; char c, e, f; double b, d, g, h; basic* p; cout << "请选择形状:" << endl; cout << "1.正方形" << endl << "2.圆柱体" << endl << "3.球体" << endl; cout << "-----------------------------------" << endl; while (scanf("%d", &a) != EOF) { switch (a) { case 1: { cout << "请输入边长:"; cin >> b; cube cube1(b); p = &cube1; cout << "-----------------------------------" << endl; cout << "请选择操作:" << endl << "a.表面积" << endl << "b.体积" << endl << "c.返回" << endl; while (cin >> c) { if (c == 'a') { cout << "正方体的表面积是:" << p->superficial_area() << endl; } if (c == 'b') { cout << "正方体的体积是:" << p->volume() << endl; } if (c == 'c') { cout << "-----------------------------------" << endl; break; } } break; } case 2: { cout << "请输入半径:"; cin >> d; cout << endl; cout << "请输入高:"; cin >> h; cylinder cylinder1(d, h); p = &cylinder1; cout << "-----------------------------------" << endl; cout << "请选择操作:" << endl << "a.表面积" << endl << "b.体积" << endl << "c.返回" << endl; while (cin >> e) { if (e == 'a') { cout << "圆柱体的表面积是:" << p->superficial_area() << endl; } if (e == 'b') { cout << "圆柱体的体积是:" << p->volume() << endl; } if (e == 'c') { cout << "-----------------------------------" << endl; break; } } break; } case 3: { cout << "请输入半径:"; cin >> g; globe globe1(g); p = &globe1; cout << "-----------------------------------" << endl; cout << endl; cout << "请选择操作:" << endl << "a.表面积" << endl << "b.体积" << endl << "c.返回" << endl; while (cin >> f) { if (f == 'a') { cout << "球体的表面积是:" << p->superficial_area() << endl; } if (f == 'b') { cout << "球体的体积是:" << p->volume() << endl; } if (f == 'c') { cout << "-----------------------------------" << endl; break; } } break; } } } return 0; }
以下是一个示例实现,满足上述所有要求: #include <iostream> #include <cmath> using namespace std; // 定义基类Shape class Shape { public: // 虚析构函数 virtual ~Shape() {} // 计算表面积的成员函数calcArea virtual double calcArea() = 0; // 计算体积的成员函数calcVolume virtual double calcVolume() = 0; }; // 定义派生类Cube,继承自Shape class Cube : public Shape { private: double sideLength; public: // 构造函数 Cube(double sideLength) { this->sideLength = sideLength; } // 计算表面积 double calcArea() override { return 6 * sideLength * sideLength; } // 计算体积 double calcVolume() override { return sideLength * sideLength * sideLength; } }; // 定义派生类Cylinder,继承自Shape class Cylinder : public Shape { private: double radius; double height; public: // 构造函数 Cylinder(double radius, double height) { this->radius = radius; this->height = height; } // 计算表面积 double calcArea() override { return 2 * M_PI * radius * (radius + height); } // 计算体积 double calcVolume() override { return M_PI * radius * radius * height; } }; // 定义派生类Sphere,继承自Shape class Sphere : public Shape { private: double radius; public: // 构造函数 Sphere(double radius) { this->radius = radius; } // 计算表面积 double calcArea() override { return 4 * M_PI * radius * radius; } // 计算体积 double calcVolume() override { return (4.0 / 3.0) * M_PI * pow(radius, 3); } }; // 测试主函数,使用指针数组和循环调用基类的成员函数 int main() { Shape* shapes[] = { new Cube(2.0), new Cylinder(1.0, 3.0), new Sphere(1.5) }; int numShapes = sizeof(shapes) / sizeof(shapes[0]); for (int i = 0; i < numShapes; ++i) { cout << "Shape " << i+1 << ", Area: " << shapes[i]->calcArea() << ", Volume: " << shapes[i]->calcVolume() << endl; delete shapes[i]; } return 0; } 在上述代码中,所有形状都是从基类Shape继承而来,并且具有相同名称的calcArea()和calcVolume()成员函数。为了支持运行时的多态性(即派生类对象可以赋值给基类指针),这些成员函数均被声明为虚函数。 而在测试主函数中,我们创建了一个指针数组,存放了三个不同形状的对象。使用循环遍历每个对象,并分别调用基类的成员函数来计算表面积和体积。最后,为了释放动态分配的内存,我们还需要使用delete来删除每个对象。
实验用例所写的代码
#include<string> #include<iostream> using namespace std; #define pi 3.1415926 class Shap { public: virtual void area()=0; virtual void volume()=0; 41 }; class Cube:public Shap//正方体 { private: float side_length; public: Cube(float Side_length):side_length(Side_length){} void area() { cout<<"正方体表面积是:"<<6*pow(side_length,2)<<endl; } void volume() { cout<<"正方体体积是:"<<pow(side_length,3)<<endl; } }; class Cylinder:public Shap//圆柱体 { private: float r; float h; public: Cylinder(float H,float R):r(R),h(H){} void area() { cout<<"圆柱体表面积是:"<<2*pi*pow(r,2)+4*pi*r*h<<endl; } void volume() { cout<<"圆柱体体积是:"<<pi*pow(r,2)*h<<endl; } }; class Sphere:public Shap//球体 { private: float r; public: Sphere(float R):r(R){} void area() { cout<<"球体表面积是:"<<4*pi*pow(r,2)<<endl; } void volume() { cout<<"球体体积是:"<<4*pi*pow(r,3)/3<<endl; } }; #include "stdafx.h" #include<iostream> #include<stdlib.h> #include <stdio.h> #include "class.h" using namespace std; void main(void) { Shap *p; int n;//形状 char op;//操作 float side_length; float r; float h; bool flag=1; cout<<"请选择形状:"<<endl; cout<<"1:正方体"<<endl; cout<<"2:圆柱体"<<endl; cout<<"3:球体"<<endl; // cin>>n; loop: cout<<"-------------------------------------"<<endl; while(cin>>n) { switch(n) { case 1://正方体 44 cout<<"请输入边长:"; cin>>side_length; p=new Cube(side_length); cout<<"-------------------------------------"<<endl; cout<<"请选择操作:"<<endl; cout<<"a:表面积"<<endl; cout<<"b:体积"<<endl; cout<<"c:返回"<<endl; while(cin>>op) { switch(op) { case 'a': p->area(); break; case 'b': p->volume(); break; case 'c': goto loop; default: cerr<<"输入错误!"<<endl; } } break; case 2://圆柱体 cout<<"请输入半径:"; cin>>r; cout<<endl; cout<<"请输入高:"; cin>>h; p=new Cylinder(r,h); cout<<"-------------------------------------"<<endl; cout<<"请选择操作:"<<endl; cout<<"a:表面积"<<endl; cout<<"b:体积"<<endl; cout<<"c:返回"<<endl; while(cin>>op) { switch(op) { case 'a': p->area(); break; case 'b': p->volume(); break; case 'c': goto loop; default: cerr<<"输入错误!"<<endl; } } break; case 3://球体 cout<<"请输入半径:"; cin>>r; p=new Sphere(r); cout<<"-------------------------------------"<<endl; cout<<endl; cout<<"请选择操作:"<<endl; cout<<"a:表面积"<<endl; cout<<"b:体积"<<endl; cout<<"c:返回"<<endl; while(cin>>op) { switch(op) { case 'a': p->area(); break; case 'b': p->volume(); break; case 'c': goto loop; default: cerr<<"输入错误!"<<endl; } } break; } } }
p1876.10
程序阅读题
#include <iostream> using namespace std; class Base {//定义了一个Base类 public: virtual void Set(int b) {//虚函数的使用 x = b; } virtual int Get() { return x; } private://定义私有类 int x; }; class Derived: public Base { public: void Set(int d) { y = d; } int Get() { return y; } private: int y; }; int main() { Base B_obj; Derived D_obj; Base *p = & B_obj; p->Set(100); cout << "B_obj x=" << p->Get() << endl; p = &D_obj; p->Set(200); cout << "D_obj x=" << p->Get() << endl; p->Base::Set(300); cout << "B_obj x=" << p->Base::Get() << endl; p->Set(p->Get() + 200); cout << "D_obj y=" << p->Get() << endl; }
对这段代码的理解
这段代码是一个简单的继承示例,其中`Base`类作为基类,具有一个虚函数`Set()`和一个虚函数`Get()`,分别用于设置和获取私有成员变量`x`的值。 `Derived`类从`Base`派生,并覆盖了`Set()`和`Get()`函数,引入了新的私有成员变量`y`。由于这两个函数在`Base`中被声明为虚函数,则它们在`Derived`中也是虚函数。 在主函数中,首先创建了一个`Base`类型的对象`B_obj`和一个`Derived`类型的对象`D_obj`,并使用指向`Base`的指针`p`依次指向这两个对象。接着调用前者的`Set()`函数,并输出其值;然后调用后者的`Set()`函数,并输出其值。随后,又调用了基类中的`Set()`和`Get()`函数,并输出其结果。最后,在调用`p->Set(p->Get() + 200)`时,将使用`Derived`对象的`Set()`函数,为私有成员变量`y`重新赋值,并输出其结果。 需要注意的是,在这个例子中,如果想直接修改`Derived`对象的私有成员变量`y`,可以通过`Derived`类型的指针或引用实现。因此: ``` Derived d; d.Set(100); // 修改 y 的值 ``` 与 ``` Base* p = &d; p->Set(100); // 修改 x 的值(Base 类中的私有成员变量) ``` 是不同的效果,即通过指向基类的指针或引用修改派生类对象的私有成员变量,只能访问到其从基类继承得来的成员变量,不能直接访问派生类独有的成员变量。如果要修改派生类独有成员变量的值,需要使用指向派生类类型的指针或引用。
此外,还需要注意以下几点:
Base中的x成员变量被设置为私有成员,因此只能通过Set()和Get()函数进行访问。在Derived类中并没有直接访问Base类的私有成员变量,而是引入了新的私有成员变量y,这样保证了数据的封装性。
在Derived类中Set()和Get()函数使用了相同的名称,但它们与基类中同名函数不属于重载,而是函数覆盖(override)。在派生类中重新定义同名函数时,使用关键字virtual可以使其变为虚函数,并实现多态特性。当使用指向派生类对象的指针或引用调用这些函数时,将自动根据对象类型选择正确的函数版本。
在基类和派生类的各个函数中,如果想避免出现隐藏的Bug,应该使用作用域解析运算符(::)来明确指明要使用哪个类的函数或成员变量。比如,在最后一行的代码中,使用了如下语句:p->Set(p->Get() + 200);。由于Derived类中的成员变量和函数都被隐藏了,所以无法直接访问到Derived对象的私有成员变量y。需要使用p->Get()来获取当前对象的值,并利用Set()函数来重新赋值。在这个过程中,使用了作用域解析运算符来明确指明调用的是基类中的Set()和Get()函数。
在这个例子中,将基类的指针或引用指向派生类对象时,使用了向上转型(upcasting)的方式。向上转型发生在从派生类类型到基类类型的隐式转换中,这是一种安全的转换方式。可以将指向派生类的指针或引用赋值给指向基类的指针或引用,从而可以通过基类接口操纵派生类对象。
调用虚函数时,会依据调用者的实际类型选择正确的函数版本,即使用后期绑定(late binding)技术。在这个例子中,当p指向基类对象B_obj时,调用的是基类的Set()和Get()函数,输出了其私有成员变量x的值;当p指向派生类对象D_obj时,调用的是派生类的Set()和Get()函数,输出了其私有成员变量y的值。
在最后一句代码中使用了链式操作语法:先调用p->Get()获取当前值,再将其加上200,并作为参数传递给p->Set()函数进行修改。需要注意的是,由于Set()函数没有返回值,因此不能在一个语句中同时对其进行两次调用。如果想避免出错,可以分别对其进行多条语句的连续调用,或者将操作和输出分开进行。
总之,这个例子展示了基类和派生类继承关系中的一些基本语法和概念。在实际编程中,应该合理运用继承、多态等特性,遵循良好的设计原则和编码规范,以提高代码质量和可维护性。
除了基类和派生类之间的继承关系,C++还支持多重继承、虚继承、抽象类等高级特性。下面简单介绍一下这些概念:
多重继承:一个派生类可以同时继承多个基类。该派生类将具有所有基类的成员和方法,并按照声明的顺序排列。如果多个基类具有相同的函数或成员变量,衍生类必须使用作用域解析运算符指定访问哪个基类的成员。
虚继承:当一个派生类通过多层继承直接或间接地继承自同一基类时,可能会出现菱形继承的问题。为了避免这种情况,C++提供了虚继承的机制,使得派生类中只有一个共享基类的实例。
抽象类:在C++中,抽象类通常是指包含纯虚函数(即没有实现的虚函数)的类。抽象类不能被实例化,只能被用作基类来派生新的类。
除此之外,C++还支持运算符重载、类型转换、模板等高级特性,这些功能可以充分发挥C++语言的优势,提高开发效率和代码质量。
在实际应用中,面向对象的编程思想可以帮助我们更好地组织程序结构,降低代码的复杂度和耦合度,提高代码的可扩展性和可重用性。下面列举一些使用面向对象编程思想的例子:
游戏开发:游戏中的各种角色、场景、物品等都可以抽象成对象,通过继承和多态来实现不同类别之间的关系。
GUI界面编程:GUI控件是很常见的面向对象应用,例如按钮、文本框、菜单栏等都可以设计为独立的类,然后利用继承和虚函数来实现其共性和特性。
数据库管理系统编程:数据库里的表、字段、记录等也可以用对象来表示,便于进行封装、抽象和组合。
设计模式应用:设计模式是固定模式化的解决问题的方案,在某些复杂场景中尤其有用。例如工厂模式、单例模式、观察者模式等都是以对象为基础思想的典型例子。
总之,在C++语言中,面向对象编程思想得到广泛应用,对软件设计与开发有着重要的贡献。但同时也需要注意遵循良好的设计、规范和命名等规则,才能使程序结构更清晰、简洁、易于理解和维护。
思考题
虚函数的运行机制是如何实现的,静态联编和动态联编有何区别?
虚函数是实现多态的关键机制,在C++中,当一个基类指针或引用调用虚函数时,将自动进行后期绑定(late binding)或动态绑定(dynamic binding)动作,即在运行时根据实际对象类型动态解析调用哪个版本的虚函数。
那么虚函数的运行机制是如何实现的呢?在编译阶段,编译器会为每个包含虚函数的类生成一张虚函数表(vtable),其中保存了该类所有虚函数的地址。在运行阶段,通过访问对象的虚函数表中对应的位置,确定调用哪一个具体的虚函数。
静态联编和动态联编有什么区别呢?静态联编也称为早期绑定(early binding),是在编译阶段进行的函数调用绑定,即根据函数名和参数类型来识别调用哪个函数。这种绑定方式在效率上比较高,但不支持多态。
动态联编也称为后期绑定(late binding),则是在运行时进行的函数调用绑定,即根据实际对象类型确定调用哪个函数。这种绑定方式可以支持多态,使得程序更加灵活。但由于需要在运行时动态解析调用,因此效率相对较低。
在C++中,默认情况下通过基类指针或引用进行函数调用时,是采用动态联编的方式。如果想要使用静态联编,需使用限定作用域运算符::(双冒号)来明确指明要调用的函数版本。
总之,虚函数实现了多态和动态绑定的特性,为C++语言提供了强大的面向对象能力。掌握虚函数的运行机制和区别可以更好地理解和应用这一特性。
除了虚函数,C++还提供了纯虚函数(pure virtual function)和抽象类(abstract class)来实现接口和多态。
纯虚函数是指没有函数体的虚函数,其声明形式为virtual type func() = 0;,其中0表示没有函数体。纯虚函数不能被直接调用,只能被子类重载实现。如果一个类包含纯虚函数,那么该类就成为抽象类,无法实例化。抽象类的作用是为了定义公共接口,用于派生新的类。
例如,下面的Animal类中定义了一个纯虚函数eat(),并且因此成为了一个抽象类:
class Animal{
public:
virtual void eat() = 0;
};
类似地,我们可以创建如下的Dog、Cat子类,并分别对eat()纯虚函数进行重载:
class Dog : public Animal{
public:
void eat(){
cout << "狗吃骨头" << endl;
}
};
class Cat : public Animal{
public:
void eat(){
cout << "猫吃鱼" << endl;
}
};
这样,我们就可以通过基类指针或引用来调用子类对象的eat()方法,并实现多态效果:
int main(){
Animal *animal1 = new Dog();
animal1->eat(); // 输出:“狗吃骨头”
Animal *animal2 = new Cat();
animal2->eat(); // 输出:“猫吃鱼”
delete animal1;
delete animal2;
return 0;
}
总之,C++中的纯虚函数和抽象类为接口和多态的实现提供了重要支持,使得程序设计更加灵活和可扩展。在实际应用中,我们可以根据需要定义自己的接口和抽象类,以达到规范化、复用性和扩展性的目的。
除了虚函数和纯虚函数之外,C++中还有虚析构函数和多重继承等特性。
虚析构函数的作用是帮助确保在通过基类指针或引用销毁对象时能够正确调用子类析构函数。如果基类析构函数不是虚函数,那么当通过基类指针或引用销毁对象时,只会调用该对象的基类部分的析构函数。而如果基类析构函数是虚函数,那么当销毁子类对象时,会先调用子类的析构函数,然后再调用父类的析构函数。因此,在设计需要通过基类指针或引用销毁对象的程序时,应该将基类的析构函数定义为虚函数。
例如,下面是一个包含虚析构函数的基类Shape,以及派生类Rectangle和Triangle的定义:
class Shape{
public:
virtual ~Shape(){}
};
class Rectangle : public Shape{
public:
~Rectangle(){
cout << "销毁矩形" << endl;
}
};
class Triangle : public Shape{
public:
~Triangle(){
cout << "销毁三角形" << endl;
}
};
在主函数中,我们可以使用基类指针来指向不同的子类对象,并尝试销毁这些对象:
int main(){
Shape *shape1 = new Rectangle();
Shape *shape2 = new Triangle();
delete shape1; // 输出:“销毁矩形”
delete shape2; // 输出:“销毁三角形”
return 0;
}
多重继承是指一个派生类可以同时继承自两个或以上的基类。由于多重继承可能导致命名冲突和二义性等问题,在使用时需要特别注意设计。
例如,假设我们有形状Shape、颜色Color和图案Pattern三个基类,现在需要定义一个具有形状、颜色和图案属性的新类Graphic。那么我们可以像下面这样使用多重继承:
class Color{
public:
void setColor(string c){
color = c;
}
string getColor() const{
return color;
}
private:
string color;
};
class Pattern{
public:
void setPattern(string p){
pattern = p;
}
string getPattern() const{
return pattern;
}
private:
string pattern;
};
class Shape{
public:
virtual double area() const = 0;
virtual void print() const = 0;
};
class Graphic : public Shape, public Color, public Pattern{
public:
void print() const{
cout << "形状:" << endl;
Shape::print();
cout << "颜色:" << getColor() << endl;
cout << "图案:" << getPattern() << endl;
}
};
在Graphic类中,我们既继承了Shape的area()和print()接口,又继承了Color和Pattern的属性。在print()方法中,我们还可以通过getColor()和getPattern()方法获取对象的颜色和图案等属性。
总之,C++中的虚析构函数和多重继承为程序设计提供了更多的灵活性和可扩展性。虚析构函数可以确保通过基类指针或引用销毁对象时能够正确调用子类析构函数;多重继承可以让派生类同时继承多个基类的接口和属性。在使用时,需要特别注意继承顺序、二义性和其他相关问题。