概念
概念
继承是面向对象程序使代码可以复用的手段,它允许在保持原有类特性的基础上进行扩展,增加功能,产生新的类,称为派生类或子类,被继承的类叫作基类或父类,继承是类设计层次的复用
格式
class Person
{
public:
void Print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
protected:
string _name = "Bob";
int _age = 20;
};
// 继承后Person类的成员函数和成员变量都会成为子类的一部分
class Student : public Person
{
protected:
int _stuid = 100000; // 学号
};
class Teacher : public Person
{
int _jobid = 100000; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
继承基类成员访问方式的变化
总结:
- 在实际运用中一般使用都是public继承,几乎很少使用protected/private继承,也不提倡使用protected/private继承,因为protected/private继承下来的成员都只能在派生类的类中使用,实际中扩展性不强
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
- 如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected,可以看出protected关键字是因继承才出现的
基类和派生类对象赋值转换
- 派生类对象可以赋值给基类的对象/基类的指针/基类的引用,这种做法叫作切片或切割
- 基类对象不能赋值给派生类对象
class Person
{
protected:
string _name = "Bob";
string _sex = "男";
int _age = 20;
};
class Student : public Person
{
protected:
int _stuid = 00000; // 学号
};
int main()
{
Student stuobj;
// 1. 派生类对象可以赋值给基类对象/指针/引用
Person perobj = stuobj;
Person* pp = &stuobj;
Person& rp = stuobj;
// 2. 基类对象不能赋值给派生类对象
//stuobj = perobj; // 会报错
return 0;
}
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域
- 当基类和派生类有同名成员(成员变量同名或成员函数函数名相同)时,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏或重定义(在子类成员中,可以使用基类::基类成员显示访问)
- 在实际的继承体系中最好不要定义同名成员
// Student的_num和Person的_num构成隐藏关系
class Person
{
protected:
string _name = "Bob";
string _sex = "男";
int _num = 200000; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << "姓名: " << _name << endl;
cout << "身份证号: " << Person::_num << endl;
cout << "学号: " << _num << endl;
}
protected:
int _num = 10000000; // 学号
};
int main()
{
Student s;
s.Print();
}
派生类的默认成员函数
构造函数:先调用基类的构造函数,再调用派生类的构造函数。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用(先父后子)
拷贝构造和赋值运算符:先调基类的再调派生类的
**析构函数:**先调用派生类的析构函数再调用基类的构造函数(先子后父)
class Person
{
protected:
Person(const char* name = "Bob")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person& operator=(const Person& p)" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
private:
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int num)
: Person(name)
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
: Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
private:
int _num; // 学号
};
int main()
{
Student s1("Bob", 20);
Student s2(s1);
Student s3("Becky", 19);
s1 = s3;
return 0;
}
友元函数和静态成员
- 友元函数不能继承,也就是说基类友元不能访问派生类私有和保护成员
- 基类定义了static静态成员,则整个继承体系中只有一个这样的成员。无论派生出多少个子类, 都只有一个static成员示例
菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
在上述菱形继承的例子中,D类继承了B类和C类,B类和C类分别继承自A类,这就会导致D类中存放了两份A类的成员,导致了数据冗余和二义性问题,二义性问题可以用指定B类或C类的作用域解决,但数据冗余无法解决
class A
{
public:
int _a = 1;
};
class B : public A
{
public:
int _b = 2;
};
class C : public A
{
public:
int _c = 3;
};
class D : public B, public C
{
public:
int _d = 4;
};
int main()
{
// 由于二义性无法明确访问的是哪一个
D d;
//d._a = 1; // 报错
// 通过指定作用域可以解决二义性问题,但无法解决数据冗余问题
d.B::_a = 2;
d.C::_a = 3;
return 0;
}
虚拟继承可以解决菱形继承的二义性和数据冗余问题。如上例所示,在B类和C类继承A类时使用虚拟继承,即可解决问题。但是虚拟继承不要再其他地方使用
class A
{
public:
int _a = 1;
};
class B : virtual public A
{
public:
int _b = 2;
};
class C : virtual public A
{
public:
int _c = 3;
};
class D : public B, public C
{
public:
int _d = 4;
};
int main()
{
D d;
d._a = 1; // 不会报错
return 0;
}
在实际编写代码的过程中不要使用菱形继承
总结
- 不建议设计多继承,不要设计菱形继承,否则复杂度和性能上都有问题
- 继承和组合
- public继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象
- 组合是一种has-a的关系,假设B组合了A,每个B对象都有一个A对象
- 优先使用对象组合,而不是类继承