目录
多态性的实现依赖于继承和虚函数。在面向对象编程中,通过基类的指针或引用来调用派生类的成员函数时,可以根据实际的对象类型决定调用哪个类的成员函数,从而实现多态性。
下面,我们围绕静态联编,给大家举一个简单例子:
- #include
-
- class Shape {
- public:
- void draw() {
- std::cout << "绘制一个形状" << std::endl;
- }
- };
-
- class Circle : public Shape {
- public:
- void draw() {
- std::cout << "绘制一个圆形" << std::endl;
- }
- };
-
- class Rectangle : public Shape {
- public:
- void draw() {
- std::cout << "绘制一个矩形" << std::endl;
- }
- };
-
- int main() {
- Shape shape;
- Circle circle;
- Rectangle rectangle;
-
- // 基类指针指向派生类对象
- Shape* shapePtr1 = &circle;
- Shape* shapePtr2 = &rectangle;
-
- // 调用基类的成员函数,由于静态联编,在编译时就确定了调用的函数
- shape.draw(); // 绘制一个形状
- shapePtr1->draw(); // 绘制一个形状
- shapePtr2->draw(); // 绘制一个形状
-
- return 0;
- }
在上述示例中,我们有一个基类 Shape 和两个派生类 Circle 和 Rectangle。基类和派生类都有一个名为 draw 的成员函数。
在 main 函数中,我们创建了一个基类对象 shape 和两个派生类对象 circle 和 rectangle。然后,我们将派生类对象的地址赋给基类指针 shapePtr1 和 shapePtr2。
在调用成员函数时,基类对象 shape 和派生类对象的指针 shapePtr1 和 shapePtr2 都调用了 draw 函数。由于静态联编,在编译时就确定了调用的函数,因此无论是通过基类对象还是派生类对象的指针调用,都会执行基类的 draw 函数,输出 "绘制一个形状"。
这个示例展示了静态联编的特点,即在编译时就确定了调用哪个函数,不会根据对象的实际类型来决定调用哪个函数。
通过多态性,我们可以以一种统一的方式处理不同类型的对象。这样做的好处是,我们可以将对象视为其基类类型,而不需要关注具体是哪个派生类的实例。这使得我们可以设计出更加灵活和可扩展的代码,同时提高了代码的可读性和可维护性。
总结来说,多态性是面向对象编程的重要特性,它通过继承和虚函数机制实现。它允许不同类型的对象对同一个消息做出不同的响应,提高了代码的灵活性和可扩展性。静态联编和动态联编是多态性的两种实现方式,静态联编由于编译时候就已经确定好怎么执行,因此执行起来效率高;而动态联编想必虽然慢一些,但优点是灵活。两者各有千秋,有各自不同的使用场景。
虚函数是用于实现动态联编和多态性的一种机制。通过将父类中的成员函数声明为虚函数,可以在子类中重写该函数并根据对象的实际类型选择调用哪个函数,从而实现多态性。
在 C++ 中,将函数声明为虚函数需要在函数名前加上关键字 virtual。当一个函数被声明为虚函数时,它的子类中的同名函数也会自动成为虚函数。当使用一个指向子类对象的基类指针或引用调用虚函数时,程序会在运行时动态地确定调用哪个函数,这就是动态联编。
一般形式如下:
- virtual 函数返回值 函数名(形参)
- {
- 函数体
- }
需要注意的是:
以下是一个简单的使用虚函数的示例:
- #include
- using namespace std;
-
- class Shape {
- public:
- virtual void draw() {
- cout << "绘制一个形状" << endl;
- }
- };
-
- class Circle : public Shape {
- public:
- void draw() {
- cout << "绘制一个圆形" << endl;
- }
- };
-
- class Rectangle : public Shape {
- public:
- void draw() {
- cout << "绘制一个矩形" << endl;
- }
- };
-
- int main() {
- Shape shape;
- Circle circle;
- Rectangle rectangle;
-
- Shape* shapePtr1 = &circle;
- Shape* shapePtr2 = &rectangle;
-
- // 通过基类指针调用虚函数,程序会在运行时动态地确定调用哪个函数
- shape.draw(); // 绘制一个形状
- shapePtr1->draw(); // 绘制一个圆形
- shapePtr2->draw(); // 绘制一个矩形
-
- return 0;
- }
在上述示例中,我们将 Shape 类的 draw 函数声明为虚函数,并在其派生类 Circle 和 Rectangle 中重写了该函数。在 main 函数中,我们创建了一个基类对象 shape 和两个派生类对象 circle 和 rectangle,并将派生类对象的地址赋给基类指针。
通过基类指针调用虚函数时,程序会在运行时动态地确定调用哪个函数。因此,通过基类对象 shape 调用 draw 函数时,输出 "绘制一个形状";通过派生类对象的指针 shapePtr1 和 shapePtr2 调用 draw 函数时,分别输出 "绘制一个圆形" 和 "绘制一个矩形",实现了多态性。
在C++中,我们不能将构造函数声明为虚函数,因为构造函数的调用是在实例化对象时发生的,而虚函数的实现是通过虚函数表来实现的。在实例化对象之前,对象还没有被创建,也没有内存空间可供调用虚函数,所以将构造函数声明为虚函数是没有意义的。
然而,析构函数可以被声明为虚函数,并且通常情况下我们将其声明为虚析构函数。通过将析构函数声明为虚函数,可以在删除指向派生类对象的基类指针时,根据实际所指向的对象类型动态绑定调用相应的析构函数,从而实现正确的对象内存释放。
当使用基类指针指向派生类对象时,如果基类的析构函数不是虚函数,那么在删除指针时只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类对象中动态分配的资源没有得到正确释放,从而造成内存泄漏。
通过声明析构函数为虚函数,我们可以保证在删除基类指针时调用正确的析构函数,确保对象的内存得到正确释放,避免内存泄漏问题。
下面是一个使用虚析构函数的示例:
- #include
- using namespace std;
-
- class Base {
- public:
- virtual ~Base() {
- cout << "调用了基类的析构函数" << endl;
- }
- };
-
- class Derived : public Base {
- public:
- ~Derived() {
- cout << "调用了派生类的析构函数" << endl;
- }
- };
-
- int main() {
- Base* ptr = new Derived();
- delete ptr;
- return 0;
- }
在上述示例中,我们定义了一个基类 Base 和一个派生类 Derived。基类 Base 的析构函数被声明为虚函数(使用 virtual 关键字),而派生类 Derived 的析构函数没有显式声明为虚函数。
在 main 函数中,我们创建了一个派生类对象的指针 ptr,并将其赋给基类指针。接着,我们使用 delete 关键字删除基类指针,这会触发析构函数的调用。
由于基类的析构函数是虚函数,因此在删除指针时,会调用正确的析构函数。输出结果为:
- 调用了派生类的析构函数
- 调用了基类的析构函数
可以看到,通过将基类的析构函数声明为虚函数,我们确保了派生类的析构函数也被正确地调用,从而释放了派生类对象中的资源。
在C++中,纯虚函数是一种在基类中声明但没有实现的虚函数。纯虚函数的声明形式是在函数原型的末尾使用 "= 0" 进行标记。纯虚函数用于定义一个接口,要求派生类必须实现该函数。
virtual 返回值 函数名(形参)=0;
纯虚函数的作用是定义一个基类的接口,这个接口可以被派生类继承并实现。通过在基类中声明纯虚函数,我们可以强制派生类提供相应的实现,以确保派生类具有某些特定的行为或功能。
下面是一个使用纯虚函数的示例:
- #include
- using namespace std;
-
- class Shape {
- public:
- virtual double getArea() const = 0; // 纯虚函数
- };
-
- class Rectangle : public Shape {
- private:
- double length;
- double width;
- public:
- Rectangle(double l, double w) : length(l), width(w) {}
- double getArea() const override { // 派生类必须实现纯虚函数
- return length * width;
- }
- };
-
- class Circle : public Shape {
- private:
- double radius;
- public:
- Circle(double r) : radius(r) {}
- double getArea() const override { // 派生类必须实现纯虚函数
- return 3.14159 * radius * radius;
- }
- };
-
- int main() {
- Shape* shape1 = new Rectangle(5, 6);
- Shape* shape2 = new Circle(3);
-
- cout << "矩形的面积: " << shape1->getArea() << endl;
- cout << "圆形的面积: " << shape2->getArea() << endl;
-
- delete shape1;
- delete shape2;
-
- return 0;
- }
在上述示例中,我们定义了一个基类 Shape,其中声明了一个纯虚函数 getArea()。这个函数没有提供实现,只是要求派生类必须实现它。派生类 Rectangle 和 Circle 都继承自基类 Shape,并实现了 getArea() 函数。
在 main 函数中,我们创建了基类指针 shape1 和 shape2,分别指向派生类对象 Rectangle 和 Circle。通过调用 getArea() 函数,我们可以获得矩形和圆形的面积。
注意到,基类 Shape 是抽象类,因为它包含一个纯虚函数。抽象类不能被实例化,只能用作其他类的基类。而派生类必须实现纯虚函数,否则它们也会成为抽象类。
在C++中,数据抽象是一种面向对象编程的概念,它允许将数据和对数据的操作分离开来,并隐藏了数据的内部细节。数据抽象通过定义类来实现,类将数据成员和成员函数封装在一起,外部只能访问类的公有接口,而不能直接访问类的私有成员。
数据抽象的目的是为了实现信息隐藏和封装,以提高程序的可维护性和安全性。通过隐藏数据的内部表示和实现细节,我们可以防止外部代码直接修改数据,从而确保数据的有效性和一致性。
访问标签通过定义成员变量和成员函数的访问级别,实现了数据抽象和封装。
在C++中,访问标签包括public、private和protected。它们用于控制类的成员的访问权限。
访问标签可以多次使用,并且每个标签的作用范围会一直有效,直到遇到下一个访问标签或者类主体的结束右括号。
- class MyClass {
- public: // 公有标签
- void publicMethod() {
- // 这个方法可以被任何地方的代码访问
- }
-
- private: // 私有标签
- int privateVariable; // 私有成员变量,无法被外部代码直接访问
-
- protected: // 保护标签
- void protectedMethod() {
- // 这个方法可以在类内部和派生类中访问
- }
- };
在这个示例中,publicMethod()是一个公共成员函数,可以被任何地方的代码访问。privateVariable是一个私有成员变量,只能在类内部访问。protectedMethod()是一个受保护的成员函数,可以在类内部和派生类中访问。
通过使用访问标签,我们可以控制类成员的可访问性,达到数据抽象和封装的目的。这样可以隐藏实现细节,提高程序的安全性和可维护性。
下面是一个简单的示例,展示了如何使用数据抽象和封装:
- #include
- using namespace std;
-
- class BankAccount {
- private:
- string accountNumber; // 账号
- double balance; // 余额
- public:
- BankAccount(string accNum) : accountNumber(accNum), balance(0) {}
-
- void deposit(double amount) { // 存款
- balance += amount;
- }
-
- void withdraw(double amount) { // 取款
- if (amount <= balance) {
- balance -= amount;
- } else {
- cout << "余额不足" << endl;
- }
- }
-
- double getBalance() const { // 获取余额
- return balance;
- }
- };
-
- int main() {
- BankAccount myAccount("123456789"); // 创建账户对象
-
- myAccount.deposit(1000); // 存款
- cout << "账户余额: " << myAccount.getBalance() << endl;
-
- myAccount.withdraw(500); // 取款
- cout << "账户余额: " << myAccount.getBalance() << endl;
-
- myAccount.withdraw(1000);
- cout << "账户余额: " << myAccount.getBalance() << endl;
-
- return 0;
- }
这个示例代码中,BankAccount 类代表银行账户,其中的私有成员有 accountNumber(账号)和 balance(余额)。这些私有成员被封装在类的私有部分,外部无法直接访问。
BankAccount 类提供了公共的成员函数 deposit()(存款)、withdraw()(取款)和 getBalance()(获取余额),用于对账户进行操作。这些函数是类的公有接口,外部代码通过调用它们来操作账户。
在 main 函数中,我们创建了一个 BankAccount 对象 myAccount,并通过公有接口进行存款、取款和获取余额操作。由于私有成员被封装起来,外部无法直接修改账户余额,只能通过调用类的方法来进行操作。
通过数据抽象和封装,我们可以隐藏内部数据细节,使得代码更加模块化和易于理解。
数据抽象和封装是面向对象编程中的两个重要设计策略。
数据抽象是指将数据和对数据的操作分离,只暴露必要的接口给外部使用,隐藏内部实现细节。它通过定义抽象数据类型(ADT)来实现,包括数据的定义和对数据进行操作的方法。数据抽象使得程序设计更加模块化,可以降低系统的复杂性,提高代码的可维护性和可复用性。
封装是指将数据和操作数据的方法封装在一个单元(类)中,形成一个独立的模块。封装通过访问修饰符(如public、private、protected)控制成员的可见性,只允许通过公共接口访问和操作数据,隐藏了内部实现的细节。封装可以防止外部直接访问和修改对象的状态,提供了数据的安全性和一致性。
数据抽象和封装的设计策略有以下几个优点:
综上所述,数据抽象和封装是面向对象编程中的重要设计策略,可以提高代码的可维护性、可复用性和安全性。
在面向对象编程中,接口(interface)是一种抽象数据类型的实现方式。接口定义了一个类或对象应该执行的操作,并指定了这些操作的输入和输出。它只包含函数原型和常量,没有任何实现代码。
接口通常用于定义程序的公共接口,使得不同的类或对象可以通过实现相同的接口达到一致的行为。这有助于提高代码的模块化和可重用性,同时也提供了更好的代码隔离和安全性。
在C++中,接口可以通过抽象类(abstract class)来实现。抽象类是一种不能被实例化的类,它只能作为其他类的基类使用。抽象类中至少有一个纯虚函数(pure virtual function),即没有实现的虚函数。
纯虚函数通过在函数声明末尾添加=0来声明,表示没有函数体,这就是一个纯虚函数。包含纯虚函数的类就是抽象类,一个抽象类至少有一个纯虚函数。例如:
- class MyInterface {
- public:
- virtual void myFunction() = 0; // 纯虚函数
- };
上面示例中,myFunction()是一个纯虚函数,没有实现代码。这个类成为抽象类,不能直接实例化。如果想要使用这个接口,需要创建一个子类,并实现纯虚函数。子类必须实现所有的纯虚函数,否则它自己也会成为抽象类。
下面是一个示例代码,展示了如何使用抽象类来实现接口:
- #include
- using namespace std;
-
- // 定义接口
- class MyInterface {
- public:
- virtual void myFunction() = 0; // 纯虚函数
- };
-
- // 子类实现接口
- class MyClass : public MyInterface {
- public:
- void myFunction() override { // 实现纯虚函数
- cout << "MyClass::myFunction() called" << endl;
- }
- };
-
- int main() {
- MyClass obj; // 实例化子类
- obj.myFunction(); // 调用接口函数
-
- return 0;
- }
在这个示例中,MyInterface是一个接口,它定义了一个纯虚函数myFunction()。MyClass是这个接口的一个实现,它通过继承MyInterface并实现myFunction()来实现接口。
在main()函数中,我们创建了一个MyClass对象,并调用了它的myFunction()函数,这个函数实际上是实现了MyInterface接口的函数。
抽象类的特点可以概括为以下几点:
通过使用抽象类和纯虚函数,我们可以定义类的接口,提高代码的模块化和可重用性,同时也避免了实现细节的暴露,提高了程序的安全性和可维护性。
这是一个常见的设计策略,称为抽象基类/接口设计模式。它通过在抽象基类中定义公共函数作为纯虚函数(即没有具体实现的函数),来提供一个通用的接口。派生类继承抽象基类,并实现这些纯虚函数,从而为外部应用程序提供具体的功能。
这种设计策略有以下优点:
标准化接口:通过抽象基类定义的接口,可以提供一个统一、标准化的接口给外部应用程序使用,使得不同的实现可以以一致的方式进行交互。
可扩展性:新的应用程序可以轻松地添加到系统中,只需要继承抽象基类并实现相应的函数即可,无需修改现有的代码。这样可以保持系统的稳定性和可扩展性。
代码复用:通过抽象基类定义公共的接口和功能,可以避免重复编写相同的代码,提高代码的复用性和开发效率。
松耦合:抽象基类将接口和实现分离,外部应用程序只需要依赖于抽象基类而不是具体的实现类。这样可以降低模块之间的耦合度,提高代码的灵活性和可维护性。
然而,这种设计策略也有一些注意事项:
合理定义抽象基类:需要合理地定义抽象基类,将相关的操作进行归类,并定义合适的接口。如果抽象基类过于庞大或不符合单一职责原则,可能会导致设计上的问题。
考虑接口的稳定性:一旦抽象基类被定义并在系统中使用,其接口就应该保持稳定,以避免对现有代码的破坏性修改。因此,在设计抽象基类时应该考虑未来的需求和变化,并预留扩展空间。
综上所述,抽象基类/接口设计模式是一种常见的设计策略,可以提供通用的、标准化的接口,同时保持代码的可扩展性和可维护性。通过合理定义抽象基类和接口,可以实现代码的模块化、复用和解耦。