一.抽象类和抽象函数
1.定义
抽象类和抽象函数是面向对象编程(OOP)中的重要概念。
抽象类:抽象类是一种特殊的类,它不能被实例化,只能被作为其他类的基类。抽象类通常包含至少一个抽象函数,用于定义该类的行为。它为子类提供了可以共享的数据和方法的框架,但不提供具体的实现。子类必须实现所有父类中的抽象函数,以提供具体的实现。
抽象函数:抽象函数是定义在抽象类中的函数,它没有具体的实现。它的作用是声明一个接口,告诉子类必须提供哪些行为。子类必须提供这些抽象函数的实现。抽象函数可以是纯虚函数或非纯虚函数。纯虚函数需要在基类中进行声明,并在派生类中提供具体的实现。
通过使用抽象类和抽象函数,可以构建一个具有一致接口的类层次结构,使代码更加模块化、可维护和可扩展。这种设计模式有助于减少代码重复,提高代码的可重用性和可维护性。
2.举例说明
// 引入标准输入输出库,这样我们就可以使用cout和cin了
#include <iostream>
// 使用std命名空间,这样我们就可以直接使用标准库中的名称,而不必写出std::
using namespace std;
// 定义一个名为Figure的抽象类,该类包含两个受保护的成员变量x和y,以及一个公共成员函数set,用于设置这两个变量的值
class Figure {
protected:
double x, y;
public:
// 设置x和y的值
void set(double i, double j) {
x = i;
y = j;
}
// 声明一个抽象函数area,该函数没有提供实现,因此它是一个纯虚函数
virtual void area() = 0;
};
// 定义一个名为Trianle的类,该类从Figure类公有派生,并实现了Figure中的纯虚函数area
class Trianle : public Figure {
public:
// 计算并打印三角形的面积(假设是直角三角形)
void area() { cout << "三角形面积:" << x * y * 0.5 << endl; }
};
// 定义一个名为Rectangle的类,该类从Figure类公有派生,并实现了Figure中的纯虚函数area
class Rectangle : public Figure {
public:
// 计算并打印矩形的面积
void area() { cout << "这是矩形,它的面积是:" << x * y << endl; }
};
// 主函数,程序的入口
int main(int argc, char const *argv[]) {
// 定义一个指向Figure类的指针pF,初始化为NULL
Figure *pF = NULL;
// 定义一个Rectangle类的对象r
Rectangle r;
// 定义一个Trianle类的对象t,并设置其坐标为(10,20)
Trianle t;
t.set(10, 20);
// 将pF指向t,这样我们就可以通过pF来调用t的area方法
pF = &t;
pF->area(); // 调用t的area方法,打印出"三角形面积:100.0"
// 将r的坐标设置为(10,20),注意此时rF还是指向t的,所以rF.area()将仍然打印出"三角形面积:100.0"
r.set(10, 20);
pF = &r; // 将pF指向r,这样我们就可以通过pF来调用r的area方法
pF->area(); // 调用r的area方法,打印出"这是矩形,它的面积是:200.0"
// 定义一个Figure类的引用rF,并将其初始化为t的引用,这样rF和t实际上指向的是同样的内存地址
Figure &rF = t;
rF.set(20, 20); // 修改t的坐标,rF也随之修改,所以此时t和rF的坐标都是(20,20)
rF.area(); // 调用t(也就是rF)的area方法,打印出"这是矩形,它的面积是:400.0",因为此时t和rF的坐标都是(20,20)
// 返回0表示程序正常退出
return 0;
}
二.虚函数
//基类指针或引用指向派生类对象时,虚函数与非虚函数区别:
//声明Employee的print为虚函数,则可访问到Manager的print函数,非虚函数,则只能访问到Employee的print
#include <iostream>
#include <string>
using namespace std;
class Employee {
public:
Employee(string name, string id);
string getName();
string getId();
float getSalary();
virtual void print();
private:
string Name;
string Id;
};
Employee::Employee(string name, string id) {//初始化构造函数
Name = name;
Id = id;
}
string Employee::getName() { return Name; }
string Employee::getId() { return Id; }
float Employee::getSalary() { return 0.0; }
void Employee::print() {
cout << "姓名:" << Name << "\t"
<< "编号:" << Id << endl;
}
class Manager : public Employee {
public:
Manager(string name, string id, float s = 0.0) : Employee(name, id) {
weeklySalary = s;
}
void setSalary(float s) { weeklySalary = s; } //设置经理的周薪
float getSalary() { return weeklySalary; } //获取经理的周薪
void print() { //打印经理姓名、身份证、周薪
cout << "经理:" << getName() << "\t\t 编号: " << getId()
<< "\t\t 周工资: " << getSalary() << endl;
}
private:
float weeklySalary; //周薪
};
/*
不论哪种赋值方式,都只能通过基类对象(或基类对象的指针或引用)访问到派生类对象从基类中继承到的成员,
不能借此访问派生类定义的成员。而虚函数使得可以通过基类对象的指针或引用访问派生类定义的成员。
*/
int main() {
Employee e("小米", "NO0001"), * pM;
Manager m("小汪", "NO0002", 128);
m.print();
pM = &m;
pM->print();
Employee& rM = m;
rM.print();
return 0;
}
// Virtual关键字其实质是告知编译系统,被指定为virtual的函数采用动态联编的形式编译。
基类指针或引用指向派生类对象时,虚函数与非虚函数区别:
声明Employee的print为虚函数,则可访问到Manager的print函数,非虚函数,则只能访问到Employee的print
三.虚析构函数
1.定义
虚析构函数是在基类中声明的虚函数,用于在删除派生类对象时正确地调用析构函数。
当使用基类的指针删除派生类对象时,如果基类没有包含虚析构函数,则只会调用基类的析构函数,这可能导致派生类资源没有被正确释放,从而产生资源泄漏。
因此,为了解决这个问题,通常在基类中声明虚析构函数。当删除派生类对象时,虚析构函数会确保先调用派生类的析构函数,然后再调用基类的析构函数。这样就能保证资源的正确释放。
总结一下,虚析构函数主要用于解决基类的指针指向派生类对象,并用基类的指针删除派生类对象时可能出现的资源泄漏问题。
2.举例
/* 虚析构函数.cpp */
#include <iostream>
using namespace std;
class A {
public:
virtual ~A() { cout << "call A::~A()" << endl; }
};
class B : public A {
char* buf;
public:
B(int i) { buf = new char[i]; }
~B() {
delete[] buf;
cout << "call B::~()" << endl;
}
};
int main() {
A* a = new B(10);
delete a;
return 0;
}
四.虚函数特性
一旦将某个成员函数声明为虚函数后,它在继承体系中就永远为虚函数了
// 引入输入输出流库,用于进行输出
#include <iostream>
// 引入字符串库,用于进行字符串操作
#include <string>
// 使用命名空间 std,这样就可以直接使用 std 库中的元素,如 cout
using namespace std;
// 定义了一个名为 A 的类
class A {
public:
// 定义了一个名为 f 的函数,该函数接受一个整数参数 i
void f(int i) { cout << "A::f()" << endl; };
};
// 定义了一个名为 B 的类,该类从 A 类公有地派生出来
class B : public A {
public:
// 重写了从 A 类继承的 f 函数,该函数接受一个整数参数 i
virtual void f(int i) { cout << "B::f()" << endl; }
};
// 定义了一个名为 C 的类,该类从 B 类公有地派生出来
class C : public B {
public:
// 重写了从 B 类继承的 f 函数,该函数接受一个整数参数 i
void f(int i) { cout << "C::f()" << endl; }
};
// 定义了一个名为 D 的类,该类从 C 类公有地派生出来
class D : public C {
public:
// 重写了从 C 类继承的 f 函数,该函数接受一个整数参数 i
void f(int) { cout << "D::f()" << endl; }
};
// 主函数,程序从这里开始执行
int main() {
// 定义了一个指向 A 类型对象的指针 pA 和一个 A 类型的对象 a
A *pA, a;
// 定义了一个指向 B 类型对象的指针 pB 和一个 B 类型的对象 b
B *pB, b;
// 定义了一个 C 类型的对象 c
C c;
// 定义了一个 D 类型的对象 d
D d;
// 将 pA 指向 a
pA = &a;
// 通过 pA 调用 f 函数,参数为 1,输出 "A::f()"
pA->f(1); // 调用A::f
// 将 pB 指向 b
pB = &b;
// 通过 pB 调用 f 函数,参数为 1,输出 "B::f()"
pB->f(1); // 调用B::f
// 将 pB 指向 c
pB = &c;
// 通过 pB 调用 f 函数,参数为 1,输出 "C::f()"
pB->f(1); // 调用C::f
// 将 pB 指向 d
pB = &d;
// 通过 pB 调用 f 函数,参数为 1,输出 "D::f()"
pB->f(1); // 调用D::f
// 主函数结束,返回0,表示程序正常结束
return 0;
}
在B类中使用override覆盖原成员函数也可
五.从基类继承的成员将访问到派生类版本
/* 从基类继承的成员将访问到派生类版本.cpp */
#include <iostream>
using namespace std;
class B {
public:
void f() { g(); }
virtual void g() { cout << "B::g"; }
};
class D : public B {
public:
void g() { cout << "D::g\n"; }
};
int main() {
D d;
d.f();
return 0;
}