C++:继承及其相关问题
继承的定义
继承机制是⾯向对象程序设计实现代码复⽤的重要⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法 (成员函数) 和属性 (成员变量),从而产⽣的类,这样的类称为派⽣类,也称为子类。而这样的类就成为 基类 / 派生类 或 父类 / 子类 。
class Person
{
public:
void Print()
{
cout << "void Print()" << endl;
}
private:
string name;
string tel;
int age;
};
class Student : public Person
{
public:
private:
string id;
};
继承的格式
继承父类成员的访问方式
继承了父类的子类中可以使用父类的 public 、protected 的变量和函数,但值得注意的是,虽然子类不可以使用父类的 private 的成员变量和函数,但子类中包含父类的 private 的成员变量和函数,只是不能使用。
此外,如果未指定继承方式,则使用关键字 class 时默认为 private,使用关键字 struct 时默认为 public。
类模板的继承
之前在模拟实现 stack 时,我们使用适配器模式,来实现顺序栈或链栈,而继承也可以达到类似的效果。但要注意基类是类模板时,需要指定⼀下类域,否则会报错。
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
vector<T>::push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
};
因为模版是按需实例化,在 stack 实例化时,vector 也会实例化,如果不指定类域,push_back等成员函数不会被实例化,从而导致出错。
基类与派生类的赋值兼容转换:切片操作
在C++中,当派生类对象被赋值给基类的指针或引用时,会发生派生类对象向基类对象的赋值兼容转换。不同于类型转换,其会直接指向的是派⽣类中切出来的基类那部分(因此称之为切片)。这意味着可以将派生类的地址赋给基类指针,或者将派生类对象赋给基类对象。
Student stu;
Person* p = &stu;
Person& p1 = stu;
继承的隐藏机制
如果基类和派生类中有同名的成员时,派⽣类成员将会屏蔽基类对同名成员的直接访问,这种现象称为隐藏。如果要访问基类的同名成员,就要指定类域。还要注意的一点是,派生类和基类如果出现同名函数,则只要函数名相同就构成隐藏。
class Person
{
public:
void Print()
{
cout << "void Print()" << endl;
}
private:
string name;
string tel;
int age;
};
class Student : public Person
{
public:
void Print(int i = 1)
{
cout << "void Print(int i = 1)" << endl;
}
private:
string id;
};
int main()
{
Student stu;
stu.Print();
stu.Person::Print();
return 0;
}
派生类的默认成员函数
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{}
Person(const Person& p)
: _name(p._name)
{}
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{}
private:
string _name;
};
构造函数
派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果有默认构造,会自动调用。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显示调⽤。
其显示调用时需要使用类名来访问。
class Student : public Person
{
public:
Student(const char* name, int id)
: Person(name)
, _id(id)
{}
private:
int _id;
};
拷贝构造
派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
class Student : public Person
{
public:
Student(const char* name, int id)
: Person(name)
, _id(id)
{}
Student(const Student& s)
: Person(s)
, _id(s._id)
{}
private:
int _id;
};
赋值重载
派⽣类的 operator= 必须要调⽤基类的 operator= 才能完成基类的复制。需要注意的是派⽣类的 operator= 与基类的 operator= 构成了隐藏,所以调⽤基类的 operator= 时,需要指定基类作用域。
class Student : public Person
{
public:
Student(const char* name, int id)
: Person(name)
, _id(id)
{}
Student(const Student& s)
: Person(s)
, _id(s._id)
{}
Student& operator = (const Student& s)
{
if (this != &s)
{
// 会构成隐藏,所以需要显⽰调⽤
Person::operator =(s);
_id = s._id;
}
return *this;
}
private:
int _id;
};
析构函数
派⽣类的析构函数不需要显示调用基类的析构函数,其会在析构派生类后,自动调⽤基类的析构函数清理基类成员。这样才能保证先清理派生类对象,再清理基类成员的顺序(因为基类可能还会被其他派生类继承,不能先清理基类)。
class Student : public Person
{
public:
Student(const char* name, int id)
: Person(name)
, _id(id)
{}
Student(const Student& s)
: Person(s)
, _id(s._id)
{}
Student& operator = (const Student& s)
{
if (this != &s)
{
// 构成隐藏,所以需要显⽰调⽤
Person::operator =(s);
_id = s._id;
}
return *this;
}
~Student()
{}
private:
int _id;
};
final 关键字
添加了 final 的类不能被继承,否则会报错。
// 不能被继承
class Person final
{
public:
private:
string _name;
};
静态成员与继承的关系
基类中定义的 static 成员,无论被继承多少次,其都只有一份,存储在静态区中。一般直接通过类名访问,也可以通过实例的对象访问。
class Person
{
public:
static int i;
private:
string _name;
};
int Person::i = 1;
class Student : public Person
{
public:
private:
int _id;
};
int main()
{
Person::i++;
Student::i++;
Person per;
Student stu;
per.i++;
stu.i++;
return 0;
}
多继承
⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
C++支持多继承,被继承的多个类之间用逗号进行分隔。
class Fruit
{
};
class Vegetables
{
};
// 多继承
class Tomato : public Fruit, public Vegetables
{
};
C++ 多继承出现的问题及解决方法
出现的问题:菱形继承
菱形继承是多继承的⼀种特殊情况。⽀持多继承就⼀定会有菱形继承的出现。
class Food
{
public:
string name;
};
class Fruit : public Food
{
};
class Vegetables : public Food
{
};
class Tomato : public Fruit, public Vegetables
{
};
菱形继承会出现数据冗余和⼆义性的问题,在 Tomato 的对象中 Food 成员会有两份,在调用其成员 name 时,会有指向不明确的错误,必须指定类域才能解决。
解决方法:虚继承
virtual 是定义C++中虚函数的关键字,在会出现菱形继承的地方添加 virtual 就可以解决菱形继承出现的的问题。
class Food
{
public:
string name;
};
class Fruit : virtual public Food
{
};
class Vegetables : virtual public Food
{
};
class Tomato : public Fruit, public Vegetables
{
};