一、多态
1、通过指针创建对象(动态分配)
#include <iostream>
using namespace std;class Base {
public:virtual void show() {cout << "Base class show" << endl;}
};class Derived : public Base {
public:void show() override {cout << "Derived class show" << endl;}
};int main() {Base* b = new Derived();b->show(); // 输出:Derived class showdelete b;return 0;
}
在这个程序中,Base
类指针 b
被分配为指向一个 Derived
类对象。因为 show
是一个虚函数,并且 b
实际上指向的是一个 Derived
类对象,所以当调用 b->show()
时,会调用 Derived
类中的 show
方法,输出 "Derived class show"。
2.直接创建对象(静态分配)
#include <iostream>
using namespace std;class Base {
public:virtual void show() {cout << "Base class show" << endl;}
};class Derived : public Base {
public:void show() override {cout << "Derived class show" << endl;}
};int main() {Base b;b.show(); // 输出:Base class showreturn 0;
}
在这个程序中,Base
类对象 b
是静态分配的。虽然 Base
类有一个虚函数 show
,但是 b
是 Base
类的对象,而不是 Derived
类的对象。因此,当调用 b.show()
时,会调用 Base
类中的 show
方法,输出 "Base class show"。
2.1静态分配具体怎么实现的
在C++中,静态分配意味着对象的内存分配在编译时就已经确定了。这种分配方式通常在栈上进行,当函数执行结束时,这些对象的内存会自动释放。以下是静态分配的详细解释和实现方式:
静态分配的实现
-
声明对象:在函数内部或类的成员变量中直接声明对象。这些对象在声明时就分配了内存,并在作用域结束时自动释放。
-
作用域管理:静态分配的对象在它们声明的作用域内是有效的。一旦超出作用域,对象的析构函数(如果有)将自动调用,并释放内存。
-
示例代码
以下是一个使用静态分配的示例代码:
#include <iostream>
using namespace std;class Base {
public:Base() {cout << "Base constructor" << endl;}~Base() {cout << "Base destructor" << endl;}virtual void show() {cout << "Base class show" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived constructor" << endl;}~Derived() {cout << "Derived destructor" << endl;}void show() override {cout << "Derived class show" << endl;}
};int main() {Base b;b.show(); // 输出:Base class showreturn 0;
}
对象声明:
-
Base b;
-
这行代码在栈上分配一个
Base
类对象b
。 -
构造函数调用:
- 当对象
b
被声明时,Base
类的构造函数被调用,输出 "Base constructor"。
- 当对象
-
虚函数调用:
- 当调用
b.show()
时,因为b
是Base
类的对象,而不是Derived
类的对象,所以调用的是Base
类的show
方法,输出 "Base class show"。
- 当调用
-
析构函数调用:
- 当
main
函数结束时,b
超出其作用域,Base
类的析构函数被调用,输出 "Base destructor"。
- 当
静态分配的特点
- 内存管理简单:静态分配的对象在作用域结束时自动释放,不需要显式删除。
- 生命周期受作用域控制:对象的生命周期由它们声明的作用域决定。
- 栈上分配:静态分配通常在栈上进行,这意味着对象的分配和释放速度很快,但受限于栈的大小。
与动态分配的对比
动态分配则在堆上进行,需要显式的内存管理(例如 new
和 delete
)。它允许在运行时决定对象的生存周期,但也增加了内存泄漏的风险。如果没有正确释放动态分配的内存,程序会导致内存泄漏。
int main() { Base* b = new Derived(); b->show(); // 输出:Derived class show delete b; // 手动释放内存,调用 Derived 和 Base 的析构函数 return 0; }
在上面的动态分配示例中,需要手动释放通过 new
分配的内存(delete b
),否则会导致内存泄漏。
2.2调用构造函数是静态分配吗?
调用构造函数本身不是静态分配,但构造函数的调用与对象的创建方式有关。静态分配和动态分配分别决定了构造函数在何时以及如何被调用。让我们更详细地探讨静态分配的过程。
静态分配的详细过程
静态分配指的是在编译时确定内存分配,并在栈上分配内存。当在函数内部声明对象时,会自动调用其构造函数进行初始化。静态分配的对象在作用域结束时会自动调用析构函数释放内存。
- 静态分配:在栈上分配,编译时确定,作用域结束时自动释放。
- 动态分配:在堆上分配,运行时确定,需要手动释放。
构造函数的调用与对象的创建方式(静态或动态分配)相关,在对象创建时自动调用,无论是静态分配还是动态分配。
2.3对象的创建时,构造函数就自动执行吗?
是的,在C++中,当对象被创建时,构造函数会自动执行以初始化该对象。无论是静态分配还是动态分配,构造函数都会在对象创建时被调用。
#include <iostream>
using namespace std;class Base {
public:Base() {cout << "Base constructor" << endl;}~Base() {cout << "Base destructor" << endl;}virtual void show() {cout << "Base class show" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived constructor" << endl;}~Derived() {cout << "Derived destructor" << endl;}void show() override {cout << "Derived class show" << endl;}
};int main() {Base* b = new Derived(); // 动态分配,b 指向 Derived 对象b->show(); // 输出:Derived class showdelete b; // 释放内存,调用 Derived 和 Base 的析构函数return 0;
}
运行结果:
Base constructor
Derived constructor
Derived class show
Derived destructor
Base destructor
解释
动态分配对象:
Base* b = new Derived();
这行代码在堆上分配了一个
Derived
类对象,并将b
指针指向该对象。在new Derived()
执行时,首先调用Base
类的构造函数,然后调用Derived
类的构造函数,依次输出 "Base constructor" 和 "Derived constructor"。调用成员函数:
调用b->show()
时,因为b
实际上指向一个Derived
类对象,并且show
是一个虚函数,所以调用Derived
类的show
方法,输出 "Derived class show"。释放内存:
delete b;
这行代码释放
b
指向的内存,首先调用Derived
类的析构函数,然后调用Base
类的析构函数,依次输出 "Derived destructor" 和 "Base destructor"。
总结
- 静态分配:对象在栈上分配,声明时自动调用构造函数,作用域结束时自动调用析构函数。
- 动态分配:对象在堆上分配,
new
操作符执行时自动调用构造函数,delete
操作符执行时自动调用析构函数。
在对象创建时,构造函数会自动执行,无论是静态分配还是动态分配。这是对象初始化过程的一个重要部分,确保对象在使用前被正确初始化。
2.4构造函数在项目中的具体运用
1. 初始化成员变量
在对象创建时通过构造函数初始化成员变量,以确保对象在使用前处于有效状态。
2. 资源分配和管理
构造函数可以用于分配资源,例如打开文件、分配动态内存、建立数据库连接等。析构函数用于释放这些资源,防止内存泄漏和资源泄漏。
3. 依赖注入
构造函数可以接受参数,用于注入对象依赖的其他对象。这样可以提高代码的灵活性和可测试性。
4. 单例模式
构造函数可以用于实现设计模式,例如单例模式,确保一个类只有一个实例,并提供一个全局访问点。
2.5纯虚函数和虚函数的区别
(1)虚函数的使用
#include <iostream>
using namespace std;// 抽象基类
class Shape {
public:virtual void draw() {};virtual double area() { return 0; };virtual ~Shape() {} // 虚析构函数
};class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) {}void draw() {cout << "Drawing Circle" << endl;}double area() {return 3.14159 * radius * radius;}
};class Rectangle : public Shape {
private:double width, height;
public:Rectangle(double w, double h) : width(w), height(h) {cout << "构造函数的实现" << endl;}void draw() {cout << "Drawing Rectangle" << endl;}double area() {return width * height;}
};int main() {Shape* shapes[2];shapes[0] = new Circle(5.0);shapes[1] = new Rectangle(4.0, 6.0);for (int i = 0; i < 2; ++i) {shapes[i]->draw();cout << "Area: " << shapes[i]->area() << endl;delete shapes[i]; // 记得删除动态分配的对象}return 0;
}
运行结果:
(2)纯虚函数的使用
#include <iostream>
using namespace std;// 抽象基类
class Shape {
public:// 纯虚函数,必须在派生类中实现virtual void draw() const = 0;virtual double area() const = 0;virtual ~Shape() { cout << "基类析构函数的实现" << endl; } // 虚析构函数
};class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) { cout << "Circle构造函数的实现" << endl; }void draw() const override {cout << "Drawing Circle" << endl;}double area() const override {return 3.14159 * radius * radius;}
};class Rectangle : public Shape {
private:double width, height;
public:Rectangle(double w, double h) : width(w), height(h) {}void draw() const override {cout << "Drawing Rectangle" << endl;}double area() const override {return width * height;}
};int main() {Shape* shapes[2];shapes[0] = new Circle(5.0);shapes[1] = new Rectangle(4.0, 6.0);for (int i = 0; i < 2; ++i) {shapes[i]->draw();cout << "Area: " << shapes[i]->area() << endl;delete shapes[i]; // 记得删除动态分配的对象}return 0;
}
运行结果:
区别
-
定义方式:
- 虚函数:
virtual void show() { ... }
- 纯虚函数:
virtual void show() = 0;
- 虚函数:
-
实现:
- 虚函数:可以在基类中提供实现,也可以在派生类中重写。
- 纯虚函数:不能在基类中提供实现,必须在派生类中实现。
-
实例化:
- 虚函数:基类可以实例化。
- 纯虚函数:包含纯虚函数的类是抽象类,不能实例化。
-
用途:
- 虚函数:用于实现多态性。
- 纯虚函数:用于定义接口,强制派生类实现特定的函数。
什么时候使用虚函数?
- 多态行为:当你希望基类提供一个默认的行为,但允许派生类重写时,使用虚函数。
- 可选重写:当派生类不一定需要提供自己的实现时,可以在基类中提供一个虚函数的默认实现。
什么时候使用纯虚函数?
- 抽象接口:当你希望基类只定义接口,而不提供任何实现时,使用纯虚函数。
- 强制实现:当你希望所有派生类都必须提供自己的实现时,使用纯虚函数。
2.6虚函数映射表
#include <iostream>
using namespace std;class Base {
public:virtual void show() {cout << "Base show" << endl;}virtual void display() {cout << "Base display" << endl;}
};class Derived : public Base {
public:void show() override {cout << "Derived show" << endl;}void display() override {cout << "Derived display" << endl;}
};int main() {Base* b = new Derived();b->show(); // 输出:Derived showb->display(); // 输出:Derived displaydelete b;return 0;
}
Base* b = new Derived();
当创建 Derived
对象时,编译器会为 Derived
类生成一个虚函数表,并在对象中存储一个指向该表的指针。虚函数表包含 Derived
类中虚函数 show
和 display
的地址。
多态调用:
b->show()
和b->display()
是通过基类指针b
调用的。- 程序会通过
b
的 V-Table Pointer 查找Derived
类的虚函数表,然后从表中找到show
和display
函数的地址并调用它们。
注意事项
- V-Table 的生成:虚函数表是在编译时生成的,每个类只有一张虚函数表。
- 对象的 V-Table Pointer:每个对象包含一个指向相应虚函数表的指针(隐藏在对象中),该指针在对象构造时被初始化。
- 内存开销:使用虚函数表会增加一些内存开销(存储虚函数表指针和虚函数表本身),并稍微增加函数调用的时间开销(间接调用)。
2.7为什么要使用虚析构函数
(1)通常情况的析构函数
#include <iostream>
using namespace std;class Base {
public:Base() {cout << "Base constructor" << endl;}~Base() { // 虚析构函数cout << "Base destructor" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived constructor" << endl;}~Derived() {cout << "Derived destructor" << endl;}
};int main() {Base* b = new Derived();delete b; // 正确调用派生类的析构函数return 0;
}
可以看到释放资源的时候,delete b只是调用了基类的析构函数,并没有对派生类进行内存释放。
(2)使用虚函数的析构函数
#include <iostream>
using namespace std;class Base {
public:Base() {cout << "Base constructor" << endl;}virtual ~Base() { // 虚析构函数cout << "Base destructor" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived constructor" << endl;}~Derived() {cout << "Derived destructor" << endl;}
};int main() {Base* b = new Derived();delete b; // 正确调用派生类的析构函数return 0;
}