一、多态基本概念
在C++中,多态性是面向对象编程的一个重要概念,它允许我们通过基类指针或引用来调用派生类的方法,从而实现在运行时选择不同的实现。这种特性大大增强了代码的灵活性和可维护性。
如果不使用虚函数,那么基类指针只能调用基类成员函数,不能调用派生类成员函数。多态性的核心概念是虚函数(Virtual Function)。当我们在基类中声明一个成员函数为虚函数时,它可以在派生类中被重写(覆盖),并且通过基类的指针或引用调用时,会根据实际指向的对象类型来调用相应的派生类方法,从而访问派生类的成员变量。
- 基类指针指向基类对象时就使用基类的成员函数和数据
- 基类指针指向派生类对象时就使用派生类的成员函数和数据(这里的数据需要填通过派生类成员函数来使用)
基类指针表现出了多种形式,这种现象称为多态。
1.1 示例代码
首先,我们定义一个基类 Animal
,其中包含一个虚函数 speak()
。接着,我们有派生类 Dog
,它重写了基类的虚函数。现在我们可以使用多态性来创建基类指针,并在运行时调用派生类的方法:
-
指针调用:
class Animal { public: virtual void speak() { std::cout << "叫叫叫" << std::endl; } }; class Dog : public Animal { public: void speak() { std::cout << "汪汪汪" << std::endl; } }; int main() { Animal* animal = new Animal; Animal* dog = new Dog; animal->speak();//叫叫叫 dog->speak();//汪汪汪 }
-
引用调用:
class Animal { public: virtual void speak() { std::cout << "叫叫叫" << std::endl; } }; class Dog : public Animal { public: void speak() { std::cout << "汪汪汪" << std::endl; } }; int main() { Animal animal; Dog dog; Animal& animal1 = animal; Animal& dog1 = dog; animal1.speak();//叫叫叫 dog1.speak();//汪汪汪 }
1.2 注意事项
-
如果函数定义在类外,只需要在基类的函数声明中加上
virtual
关键字,函数定义时不能加。class Animal { public: virtual void speak();//函数声明时需要加上virtual }; void Animal::speak() {//函数定义时不加 std::cout << "叫叫叫" << std::endl; } class Dog : public Animal { public: void speak() { std::cout << "汪汪汪" << std::endl; } }; int main() { Animal *dog = new Dog; dog->speak();//汪汪汪 }
-
派生类中重写虚函数的函数签名必须与基类中的虚函数一致。
函数签名包括函数的名称、参数列表和参数类型 -
如果派生类没有重写虚函数,基类的虚函数会被继续使用。
class Animal { public: virtual void speak() { std::cout << "叫叫叫" << std::endl; } }; class Dog : public Animal { public: }; int main() { Animal *dog = new Dog; dog->speak();//叫叫叫 }
-
如果想在派生类中调用基类的虚函数,可以使用类名和作用域解析符。
class Animal { public: virtual void speak() { std::cout << "叫叫叫" << std::endl; } }; class Dog : public Animal { public: void speak() { //Animal::speak();也可以在派生类里调用基类函数 std::cout << "汪汪汪" << std::endl; } }; int main() { Animal *dog = new Dog; dog->Animal::speak();//叫叫叫 dog->speak();//汪汪汪 }
-
如果要在派生类中重新定义基类的函数,则将它设置为虚函数;否则,不要设置为虚函数。
原因:- 效率更高:虚函数会带来开销。
- 指出不要重新定义该函数:让程序员知道,这个函数没有virtual关键字,就不需要重新定义该函数。
二、多态使用场景
2.1 示例
- 使用多态:
class Animal { public: virtual void speak() { std::cout << "叫叫叫" << std::endl; } }; class Dog : public Animal { public: void speak() { std::cout << "汪汪汪" << std::endl; } }; class Cat : public Animal { public: void speak() { std::cout << "喵喵喵" << std::endl; } }; int main() { std::vector<Animal*> arr; arr.push_back(new Dog); arr.push_back(new Cat); for(Animal* animal:arr){ animal->speak();//汪汪汪,喵喵喵 } }
- 不使用多态:
class Dog { public: void speak() { std::cout << "汪汪汪" << std::endl; } }; class Cat { public: void speak() { std::cout << "喵喵喵" << std::endl; } }; int main() { std::vector<Dog> arr_dog; std::vector<Cat> arr_cat; arr_dog.push_back(Dog()); arr_cat.push_back(Cat()); for(Dog dog:arr_dog){ dog.speak();//汪汪汪 } for(Cat cat:arr_cat){ cat.speak();//喵喵喵 } }
2.2 优点
-
灵活性和可扩展性: 使用多态的代码中,我们只需在基类中添加新的派生类,而不必修改现有的代码。这使得代码更具灵活性和可扩展性。在不使用多态的情况下,每当添加新的类时,都需要修改主代码来处理新类的实例,增加了维护成本。
-
减少重复代码: 使用多态,我们只需在基类中定义一次函数,然后让派生类继承并重写,从而减少了重复代码。在不使用多态的情况下,每个类都需要单独实现相似的函数,导致代码冗余。
-
简化代码维护: 当需要修改公共操作时,只需更改基类中的代码,而不必在每个派生类中进行修改。这样可以减少维护工作并降低出错的风险。
-
运行时决定调用: 多态允许在运行时根据实际对象类型来决定调用哪个函数。这种灵活性可以根据具体情况来选择不同的实现,从而更好地适应程序的需求。
-
适用于抽象和通用操作: 多态特别适用于处理抽象的操作,如在这个例子中的"叫"。基类可以定义抽象的操作,而派生类可以根据自己的特点进行实现。
三、多态的对象模型
多态的对象模型可以理解为在面向对象编程中,对象和函数之间的关系,特别是在涉及继承和多态性的情境下。多态性允许同一方法名在不同的对象类型上具有不同的实现,以提供更灵活的代码结构和更具扩展性的设计。
在C++中,多态性通过虚函数和继承来实现。下面是对你提供的内容进行进一步解释:
-
类的普通成员函数: 在类中定义的普通成员函数的地址是静态的,这意味着在编译阶段就已经确定了函数的调用地址。这种情况下,无论对象的实际类型如何,调用的都是静态绑定的函数。
-
虚函数和虚函数表: 当基类中声明虚函数时,C++为每个对象创建一个虚函数表(VTable),其中存储了虚函数的名称和地址。这样,即使通过基类的指针或引用访问派生类对象,也可以在运行时根据实际对象的类型来调用正确的函数。(注意:派生类会继承基类的虚函数和虚函数表)
-
派生类的虚函数重写: 如果派生类重写了基类中的虚函数,派生类的函数将会取代虚函数表中基类函数的位置。这意味着通过基类指针或引用调用虚函数时,会根据实际对象的类型来动态选择正确的函数实现。
-
多态的种类:
- 静态多态(编译时多态): 这种多态在编译时期就已经确定了要调用的函数地址,主要有函数重载和函数模板。函数重载允许同一个函数名拥有不同的参数列表,编译器会根据调用时的参数类型选择合适的函数。函数模板允许在编译时根据参数类型生成不同的函数实例。
- 动态多态(运行时多态): 也称为动态绑定,它在运行时根据实际对象的类型来选择要调用的函数。一般用于基类指针或引用指向派生类对象时,通过虚函数的动态绑定来调用派生类中的实现。
多态性的核心思想在于将函数调用的具体实现推迟到运行时,从而更好地适应不同的对象类型和扩展需求。这种灵活性使得代码更易于维护和扩展,同时也更贴近实际问题的建模。
四、如何析构派生类
在C++中,析构函数的调用顺序与构造函数相反。析构函数在销毁对象时被调用。派生类的析构函数会在执行完派生类的清理工作后自动调用基类的析构函数,无需显式调用。
4.1 虚构派生类
class Base {
public:
Base() { std::cout << "基类构造函数" << std::endl; }
~Base() { std::cout << "基类析构函数" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "派生类构造函数" << std::endl; }
~Derived() { std::cout << "派生类析构函数" << std::endl; }
};
int main() {
Derived d; // 创建派生类对象
return 0;
}
基类构造函数
派生构造构函数
派生类析构函数
基类析构函数
如果手动调用派生类的析构函数,也会自动调用基类的析构函数:
class Base {
public:
Base() { std::cout << "基类构造函数" << std::endl; }
~Base() { std::cout << "基类析构函数" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "派生类构造函数" << std::endl; }
~Derived() { std::cout << "派生类析构函数" << std::endl; }
};
int main() {
Derived d; // 创建派生类对象
d.~Derived();
}
基类构造函数
派生类构造函数
派生类析构函数
基类析构函数
派生类析构函数
基类析构函数
4.2 虚析构函数
如果使用基类指针指向派生类对象并尝试删除基类指针,只会调用基类的析构函数。如果希望正确地调用派生类的析构函数,必须将基类的析构函数声明为虚析构函数。
class Base {
public:
Base() { std::cout << "基类构造函数" << std::endl; }
~Base() { std::cout << "基类析构函数" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "派生类构造函数" << std::endl; }
~Derived() { std::cout << "派生类析构函数" << std::endl; }
};
int main() {
Base* ptr = new Derived(); // 使用基类指针指向派生类对象
delete ptr; // 此处会调用派生类的析构函数,然后调用基类的析构函数
return 0;
}
基类构造函数
派生类构造函数
基类析构函数
class Base {
public:
Base() { std::cout << "基类构造函数" << std::endl; }
virtual ~Base() { std::cout << "基类析构函数" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "派生类构造函数" << std::endl; }
~Derived() { std::cout << "派生类析构函数" << std::endl; }
};
int main() {
Base* ptr = new Derived(); // 使用基类指针指向派生类对象
delete ptr; // 此处会调用派生类的析构函数,然后调用基类的析构函数
return 0;
}
基类构造函数
派生类构造函数
派生类析构函数
基类析构函数
原因解释:
- 静态绑定与动态绑定: C++是一种静态类型语言,这意味着在编译时需要确定每个变量的类型。当你使用基类指针指向派生类对象时,编译器只知道这是一个基类指针,因此在编译时,它将使用基类的析构函数。这种行为被称为静态绑定,因为在编译时绑定(决定调用哪个函数)已经确定了。
- 虚函数和动态绑定: 为了实现多态性,C++引入了虚函数和动态绑定机制。当你将基类的析构函数声明为虚函数时,编译器会在运行时检查对象的实际类型,然后根据实际类型调用正确的析构函数。这种行为被称为动态绑定,因为在运行时绑定(决定调用哪个函数)发生。
所以,如果你不将基类的析构函数声明为虚析构函数,当你使用基类指针删除派生类对象时,编译器只会使用基类的析构函数,无法正确调用派生类的析构函数。这可能会导致资源泄漏和不正确的对象清理。通过声明基类的析构函数为虚函数,你告诉编译器在运行时进行动态绑定,以便在删除派生类对象时正确调用派生类的析构函数。
4.3 析构派生类要点
- 析构派生类对象时,会自动调用基类的析构函数。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
- 析构函数可以手工调用,如果对象中有堆内存,析构函数中以下代码是必要的:
delete ptr; ptr = nullptr;//如果不写这一句话,就会变成指针悬挂,如果再delete ptr就会报错,而delete空指针不会吧报错
- 用基类指针指向派生类对象时,delete 基类指针调用的是基类的析构函数,不是派生类的,如果希望调用派生类的析构函数,就要把基类的析构函数设置为虚函数。
- 对于基类,即使它不需要析构函数,也应该提供一个空虚析构函数。
这是因为当你的基类在继承关系中作为派生类的基类时,可能会发生资源泄漏或未定义行为,除非基类具有虚析构函数。
这是为了确保以下情况下的正确行为:- 如果派生类在析构函数中分配了资源(如堆内存或打开的文件),而基类没有虚析构函数,当你通过基类指针删除派生类对象时,只会调用基类的析构函数。这将导致派生类对象中的资源没有被正确释放,可能导致资源泄漏。
- 赋值运算符函数不能继承赋值运算符函数不继承: 赋值运算符函数是基于对象类型的操作,因此它们不会被隐式继承。这意味着如果你在基类中定义了一个赋值运算符函数,它不会被派生类隐式继承。如果你希望在派生类中使用相同的赋值操作,你需要在派生类中重新定义该赋值运算符函数。
- 友元函数不是类成员,不能继承。
五、纯虚函数和抽象类
纯虚函数是一种特殊的虚函数,它在基类中声明但没有实际的函数体。它的目的是为了让派生类去实现这个函数,以便实现多态性。纯虚函数常常用于描述基类中的通用行为,而具体的实现则由派生类提供。
5.1 纯虚函数的声明和使用
纯虚函数的声明采用以下语法:
virtual 返回值类型 函数名(参数列表) = 0;
纯虚函数只有函数名、参数和返回值类型,没有函数体。它在基类中为派生类保留了一个函数名,以便派生类可以重定义这个函数,实现自己的功能。如果一个类中包含了至少一个纯虚函数,那么这个类就被称为抽象类。
5.2 抽象类的特点和用途
抽象类是包含纯虚函数的类,无法实例化对象,但可以创建指向它的指针和引用。抽象类的主要作用是为了定义一组接口,规定了派生类必须实现的函数,从而实现多态性。
class Animal{
public:
virtual void speak()=0;
};
class Dog:public Animal{
public:
virtual void speak(){
cout<<"汪汪汪"<<endl;
}
};
int main() {
//Animal* animal = new Animal;错误,无法实例化抽象类对象
Animal* dog = new Dog;
dog->speak();
}
在这个例子中,Animal 是一个抽象类,它包含一个纯虚函数 speak,而 Dog 是一个派生类,实现了 speak 函数。我们通过使用指向基类的指针来实现多态性,这样可以在运行时确定调用的是派生类的函数。
- 注意:派生类必须重定义抽象类中的所有纯虚函数,否则派生类也会成为抽象类,无法实例化对象。
5.3 纯虚析构函数
基类中的纯虚析构函数必须要实现
在C++中,如果一个类中有纯虚函数,那么这个类就是抽象类,不能实例化生成对象,只能派生。由它派生的类的纯虚函数如果没有被实现,那么,该派生类还是个抽象类。只有全部实现了纯虚函数的派生类才可以被实例化。定义一个函数为纯虚函数,一般表示该函数没有被实现。但是,这不代表纯虚函数不能被实现。纯虚函数也是可以定义的。
在C++中,如果一个类中有纯虚析构函数,那么这个类必须要有一个虚析构函数的定义。因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对纯虚析构函数的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上。
所以基类中的纯虚析构函数必须要有代码实现。
- 注意:C++中,纯虚析构函数的定义必须在类之外,而不能在类的内部,因为纯虚析构函数必须有定义体。如果在类内部定义纯虚析构函数,那么编译器就会认为这个析构函数是一个普通的虚析构函数,而不是一个纯虚析构函数。这样就会导致编译错误
class Animal{
public:
virtual ~Animal()=0;
};
Animal::~Animal() {
cout<<"基类析构函数"<<endl;
};
class Dog:public Animal{
public:
~Dog(){
cout<<"派生类析构函数"<<endl;
}
};
int main() {
Animal* dog = new Dog;
delete dog;
}
派生类析构函数
基类析构函数