继承
#include <iostream>
using namespace std;
class Base_Person
{
private:
string m_name;
int m_age;
public:
Base_Person(const string &name = "none", const int &age = 0)
{
m_age = age;
m_name = name;
}
void show()
{
cout<<"基类show函数的调用"<<endl;
cout << "age: " << m_age << " name: " << m_name << endl;
}
};
class Teacher : public Base_Person
{
private:
string m_duty;
public:
Teacher(string duty, const string &name = "none", const int &age = 0);
Teacher(string duty, const Base_Person &p);
void show()
{
cout << "子类show函数的调用" << endl;
}
};
Teacher::Teacher(string duty, const string &name, const int &age) : Base_Person(name, age)
{
m_duty = duty;
}
int main()
{
Teacher t("teach", "jack", 40);
t.show();
system("pause");
return 0;
}
如上代码,我们定义了一个基类Base_Person,其定义了私有的属性以及公有的构造函数以及show函数。我们也定义了一个子类Teacher,其继承了Base_Person类,在基类的基础上新增了自己的私有属性duty和自己的构造函数。因为创建派生类对象时,程序先创建基类对象,所以我们的构造函数以初始化列表的方式先初始化了Base_Person基类(否则将使用默认的基类构造函数),注意到我们使用初始化列表的方法在派生类的构造函数初始化了基类的私有成员!并且注意到这里的name和age不能像Teacher构造函数一样使用默认参数默认初始化(在其他情况下,派生类不能直接访问基类的私有属性。并且只有构造函数能使用成员初始化语法)。并且注意到我们的还定义了一个Teacher的拷贝构造函数。最后我们在派生类中调用了基类的方法。在此提一下,如果需要使用拷贝构造函数但是没有定义,编译器将自动生成一个。
- 派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
- 基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示类型转换的情况下引用派生类对象。
例如我们在上述代码的基础上用下面这段代码来验证:
int main()
{
Teacher t("teach", "jack", 40);
//基类指向派生类
Base_Person &bp = t;
bp.show();
Base_Person *bp1 = &t;
bp1->show();
//派生类指向基类
Base_Person ba("xiaoming", 18);
// Teacher &ta = ba; // 错误!
// Teacher *ta = &ba; // 错误!
system("pause");
return 0;
}
运行结果:
可能会觉得奇怪,不是说 基类引用可以在不进行显示类型转换的情况下引用派生类对象吗?为什么我调用的show函数还是基类的show函数,不是子类的呢?
别急,这是因为,我们要加上virtual关键字,这样才能动态联编,这样编译器就不会根据我们的数据类型Base_Person而误以为我们使用的是Base_Person的show函数了。
我们只需要将基类的show函数前加上virtual关键字即可:
再次运行查看结果:
因为派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。因此,我们这里没有对m_name和m_age进行任何处理。
多态
两个重要知识点:
- 在派生类中重新定义基类的方法
- 使用虚方法
下面我们定义了一个计算器基类Base_cal,我们将要完成的多态主要是对于add方法的实现,我们将用不同子类完成不同的add方法。因为我们在父类的add方法不需要完成任何事情(主要是实现子类的add方法),因此我们先用virtual关键字将add方法变成虚函数以便子类能够方便的重写(不用virtual也能重写,但是在某些情况调用起来会比较麻烦),因为我们的add方法并没有改变类的属性只是单纯的返回一个值,因此我们在后面加上const修饰,最后 = 0让其变成纯虚函数!析构函数我们加上virtual关键字让其变成虚析构,但是由于我们这里的例子比较简单,并没有在堆区开辟内存,因此并没有在析构函数中写任何代码。
再看看我们的子类cal_1,继承了Base_cal,并且定义了自己的成员变量m_weight,并且构造函数使用初始化列表的方式初始化了父类,最后重写了add方法。可以看出cal_1的add方法很简单就是一个带权重的加法器!
我们定义的子类cal_2就比较简单了,就是纯两数之和。
#include <iostream>
class Base_cal
{
private:
int m_num1;
public:
Base_cal(const int num)
{
m_num1 = num;
}
int get_num() const
{
return m_num1;
}
virtual int add(const int num2) const = 0;
virtual ~Base_cal() {}
};
class cal_1 : public Base_cal
{
private:
int m_weight;
public:
cal_1(const int weight, const int num) : Base_cal(num)
{
m_weight = weight;
}
int add(const int num_add) const
{
return (get_num() + m_weight * num_add);
}
};
class cal_2 : public Base_cal
{
public:
cal_2(const int num) : Base_cal(num)
{
}
int add(const int num_add) const
{
return (get_num() + num_add);
}
};
int main()
{
cal_1 cal1(2, 4);
std::cout << cal1.add(3) << std::endl;
cal_2 cal2(4);
std::cout << cal2.add(3) << std::endl;
system("pause");
return 0;
}
运行结果:
再看一个多态的特性:
假设我们要同时管理cal_1和cal_2两个类,如果能使用一个数组来保存两个类就好了,但是这两个类的数据类型不一样,因此这是不可能的。但是在上一篇文章说过,可以定义基类的引用指向子类,因此同样我们可以创建指向Base_cal的数组指向cal_1或cal_2对象:
int main()
{
cal_1 cal1(2, 4);
cal_2 cal2(4);
Base_cal *arr[2];
arr[0] = &cal1;
arr[1] = &cal2;
std::cout << arr[0]->add(3) << std::endl;
std::cout << arr[1]->add(3) << std::endl;
system("pause");
return 0;
}
运行结果是一样的!这就是多态性!
为何需要虚析构函数:
如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。对于上述代码,则只有Base_cal被析构,有了父类的虚析构函数,即使子类没有定义析构函数,也能够完成对应类型的析构。因此使用虚析构能够保证正确的析构函数序列被调用!
参考资料:
C++ Prime Plus第六版