继承
继承是面向对象编程的一个重要概念,他允许一个类(称为子类或者派生类)从另一个类(称为分类或者基类)继承属性和行为,这使得代码重用更为容易。 继是类设计层次的复用。
C++简单的继承示例
cpp
//基类
class Animal
{
public:
//构造函数
Animal(const string & name = "大黄") :_name(name) {}
void sleep()
{
cout << _name << "is sleeping" << endl;
}
protected:
string _name;
};
//派生类
class Dog :public Animal
{
public:
Dog()
{
_breed = "土狗";
}
void print()
{
cout << _name << endl;//派生类继承基类的_name属性
cout << _breed << endl;
}
private:
string _breed;
};
int main()
{
Dog d1;//创建派生类对象。
d1.sleep();//继承基类的成员函数
d1.print();//调用自己的成员函数
return 0;
}
继承的定义格式
继承方式和访问限定符
继承基类成员访问方式的变化
public继承
public
继承是最常见的形式,它使得基类的公共成员在派生类中保持为公共成员,基类的受保护成员在派生类中保持为受保护成员。- 派生类对象可以访问基类的公共成员。
cpp
class Base {
public:
int _publicMember;
protected:
int _protectedMember;
private:
int _privateMember;
};
class Derived : public Base {
// _publicMember 在 Derived 中仍然是公共的
// _protectedMember 在 Derived 中仍然是受保护的
// _privateMember 对于 Derived 是不可见的不可访的
};
- 对于最上面的例子基类成员变量访问限定符是protected,在派生类中仍然是protected,所以在派生类print函数中可以正常访问。如果定义成private,在派生类中就不能访问了。
- private和protect访问限定符最主要的区别就在于protected可以被派生类访问,而private不可以被派生类访问。
protected继承
protected
继承使得基类的公共和受保护成员在派生类中变成受保护的。- 派生类对象不能直接访问基类的公共和受保护成员。
cpp
class Base {
public:
int _publicMember;
protected:
int _protectedMember;
private:
int _privateMember;
};
class Derived : protected Base {
// _publicMember 在 Derived 中变成受保护的
// _protectedMember 在 Derived 中仍热是受保护的
// _privateMember 对于 Derived 是不可见的
};
private继承
private
继承使得基类的公共和受保护成员在派生类中变成私有的。- 派生类对象不能直接访问基类的公共和受保护成员。
cpp
class Base {
public:
int _publicMember;
protected:
int ------protectedMember;
private:
int _privateMember;
};
class Derived : private Base {
// _publicMember 在 Derived 中变成私有的
// _protectedMember 在 Derived 中变成私有的
// _privateMember 对于 Derived 是不可见的
};
strcut 和 class继承的区别
cpp
class Base {
//...
};
//class默认是private继承
class Derived : Base {
//...
};
//struct默认是public继承
struct Derived : Base {
//...
};
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
继承中的作用域
- 在继承体系中,基类和派生类都有独立的作用域
- 当基类和派生类中有同成员,派生类可以屏蔽基类对同名成员直接访问,这种情况叫做隐藏 或者叫重定义。
- 在派生类中访问基类的成员需要指定作用域
基类::基类成员 显示访问
cpp
class Person
{
protected:
string _name = "张三";//姓名
string _sex = "男";//性别
int _age = 18;//年龄
int _num = 13456789;//身份证号
};
class Student : public Person
{
public:
void print()
{
cout << _name << endl;
cout << _sex << endl;
cout << _age << endl;
cout << _num << endl;//屏蔽基类直接访问自己的成员变量
}
private:
int _num = 666;//学号
};
- 上面例子中,Person 和 Student中的_num构成隐藏关系。
- 访问基类的成员需要指定作用域(前提是基类的成员访问限定符是public 或者 protected)
cpp
void print()
{
cout << _name << endl;
cout << _sex << endl;
cout << _age << endl;
cout << _num << endl;//屏蔽基类直接访问自己的成员变量
cout << Person::_num << endl;//指定作用域访问基类的成员
}
- 派生类和基类的成员函数也可以构成隐藏关系
- 派生类和基类中只要函数名相同就构成隐藏,而不是函数重载,参数不同也不会构成函数重载。
- 函数重载的前提是同一个作用域下,而基类和派生类的作用域是相互独立的。
cpp
class Person
{
public:
void print()
{
cout << _name << endl;
cout << _sex << endl;
cout << _age << endl;
cout << _num << endl;
}
protected:
string _name = "张三";//姓名
string _sex = "男";//性别
int _age = 18;//年龄
int _num = 13456789;//身份证号
};
class Student : public Person
{
public:
void print()
{
cout << _name << endl;
cout << _sex << endl;
cout << _age << endl;
cout << _num << endl;
cout << Person::_num << endl;
}
private:
int _num = 666;//学号
};
int main()
{
Student s1;//派生类对象
s1.print();//调用自己的成员
s1.Person::print();//指定作用域,调用基类的成员函数
return 0;
}
基类和派生类对象的赋值转换
- 派生类对象可以赋值给基类对象的/基类的引用/基类的指针。派生类对象赋值给基类对象称之为切片或者切割,意思是把派生类中基类的那一部分切过来赋值
- 基类对象不能赋值给派生类对象
cpp
//基类
class Person
{
public:
Person(string name = "张三", string sex = "男", int age = 18)
:_name(name)
,_sex(sex)
,_age(age)
{}
void print()
{
cout << "name = " << _name << endl;
cout << "sex = " << _sex << endl;
cout << "age = " << _age << endl;
}
protected:
string _name;//姓名
string _sex;//性别
int _age;//年龄
};
//派生类
class Student : public Person
{
public:
Student()
:Person()//调用基类的默认构造
,_id(1111)//初始化自己的成员变量
{}
void print()
{
Person::print();调用基类的成员函数
cout << "id = " << _id << endl;
}
private:
int _id;//学号
};
int main()
{
Student s1;//派生类对象
s1.print();
Person p1 = s1;//派生类对象赋值给基类对象
p1.print(); //基类对象访问基类成员函数
Person& pp = s1;//派生类对象赋值给基类引用对象
pp.print();//基类引用对象访问基类成员函数
Person* ptrp = &s1;//派生类对象赋值给基类指针对象
ptrp->print();//基类指针对象访问基类成员函数
//基类对象不能赋值给派生类对象
//s1 = p1;//error
return 0;
}
将派生类中属于基类的一部分切割出来赋值给基类对象
派生类的默认成员函数
6个默认成员函数,意思就是指我们不写,编译器会默认生成,在派生类中,他们的默认成员函数和之前的会有差别
- 构造函数
派生类的构造函数必须调用基类的构造函数来初始化基类的一部分成员,如果基类没有默认的构造函数,则必须在派生类的构造函数的初始化列表中显示调用。 - 派生类对象初始化先调用基类构造再调派生类构造。
基类有默认构造函数:
cpp
//基类
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
protected:
string _name;//姓名
};
//派生类 派生类中没有实现构造函数
class Student : public Person
{
public:
private:
int _id;//学号
};
int main()
{
Student s1;//s1调用基类的构造函数
return 0;
}
运行结果:
_id会为随机值,编译器默认生成的构造函数对内置类型不作处理(有些编译器会初始化为0)
基类没有默认构造函数
cpp
//基类
class Person
{
public:
Person(const char* name)
:_name(name)
{
cout << "Person(const char* name)" << endl;
}
protected:
string _name;//姓名
};
//派生类
class Student : public Person
{
public:
Student(const char* name, int id = 1111)
:Person(name)//必须在派生类的构造函数的初始化列表中显示调用 并传递必要的参数
, _id(id)
{
cout << "Student(const char* name, int id = 1111)" << endl;
}
private:
int _id;//学号
};
int main()
{
Student s1("张三");
return 0;
}
运行结果:
- 拷贝构造函数
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
cpp
//基类
class Person
{
public:
//基类构造函数
Person(const char* name)
:_name(name)
{
cout << "Person(const char* name)" << endl;
}
//基类拷贝构造函数
Person(const Person& person)
:_name(person._name)
{
cout << "Person(const Person& person)" << endl;
}
protected:
string _name;//姓名
};
//派生类
class Student : public Person
{
public:
//派生类构造函数
Student(const char* name,int id = 1111)
:Person(name)
,_id(id)
{
cout << "Student(const char* name,int id = 1111)" << endl;
}
//派生类拷贝构造函数
Student(const Student& student)
:Person(student)//派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
,_id(student._id)//在赋值派生类自己的一部分
{
cout << "Student(const Student& student)" << endl;
}
private:
int _id;//学号
};
int main()
{
Student s1("张三");
Student s2(s1);
return 0;
}
运行结果:
- 赋值运算符重载
派生类的operator=必须要调用基类的operator=完成赋值,和拷贝构造规则一样 - 析构函数
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。 - 派生类对象析构清理先调用派生类析构再调基类的析构
cpp
//基类
class Person
{
public:
//基类构造函数
Person(const char* name)
:_name(name)
{
cout << "Person(const char* name)" << endl;
}
//基类析构函数
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;//姓名
};
//派生类
class Student : public Person
{
public:
//派生类构造函数
Student(const char* name,int id = 1111)
:Person(name)
,_id(id)
{
cout << "Student(const char* name,int id = 1111)" << endl;
}
//派生类析构函数
~Student()
{
cout << "~Student()" << endl;
}
private:
int _id;//学号
};
int main()
{
Student s1("张三");
return 0;
}
运行结果:
派生类调用析构完成后会自动调用基类析构函数
继承和友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
cpp
//声明Student类
class Student;
class Person
{
public:
//友元函数声明 是Person的友元
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl; //可以访问Person的保护成员
cout << s._stuNum << endl; //不可以访问
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
继承和静态成员
- 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。
- 也就是说,派生类和基类中的静态成员是一个,基类中的静态成员是所有派生类的共享
cpp
class Person
{
public:
Person()
{
++_count;
}
static int _count;//统计人数个数
protected:
string _name;//姓名
};
int Person::_count = 0;
class Student : public Person
{
public:
Student(int id = 1)
:Person()
,_id(id)
{}
protected:
int _id;//学号
};
class Teacher : public Person
{
public:
Teacher(int no = 1)
:Person()
, _no(no)
{}
protected:
int _no;//编号
};
int main()
{
Student s1;
Student s2;
Teacher t1;
Teacher t2;
cout << Person::_count << endl;//4
return 0;
}
单继承 && 多继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
复杂的菱形继承及菱形虚拟继承
菱形继承:菱形继承是多继承的一种特殊情况 比如
Assistant类继承了Student和Teacher类,Teacher类和Student类继承了Person类,造成了菱形继承
菱形继承问题
菱形继承会造成数据冗余和二义性,
像在Assistant的对象中Person成员会有两份。
cpp
//菱形继续
class Person
{
public:
string _name;
};
class Student : public Person
{
protected:
int _num;
};
class Teacher : public Person
{
protected:
int _num;
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse;//主修课程
};
int main()
{
Assistant a;
//菱形继承造成数据二义性
//a._name = "tom"; //error
//需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
a.Person::_name = "zzz";
}
Assistant对象中_name成员存在两份,造成数据冗余,且访问时需要指定父类 虚继承可以解决数据冗余和二义性的问题 如上面的继承关系,在Student和 Teacher的继承Person时使用虚拟继承,即可解决问题。
cpp
//虚继承
class Person
{
public:
string _name;
};
class Student : virtual public Person
{
protected:
int _num;
};
class Teacher :virtual public Person
{
protected:
int _num;
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse;//主修课程
};
int main()
{
Assistant a;
//虚继承解决数据冗余和二义性
a._name = "tom";
}
Assistant对象中_name只有一份,虚继承解决了数据冗余和二义性。
注意: 多继承是C++的一个大坑,不建议设计多继承,一定不要设计出菱形继承的关系。
继承和组合
组合:通过组合,一个类可以包含其他类的对象作为成员,从而达到复用功能的目的。 比如:
cpp
class Engine
{
public:
void start()
{
// 具体实现
}
};
class Car
{
public:
void drive()
{
_engine.start();
// 具体驾驶实现
}
private:
Engine _engine;
};
Engine类作为Car的一个成员,达到了复用的目的
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
选择继承还是组合:
- 优先使用组合: 组合通常被认为是一种更灵活、低耦合的设计方式。如果一个类只需要使用另一个类的部分功能,或者在运行时需要动态地改变关系,组合是一个更好的选择。
- 优先使用继承: 当类之间有明确的"is-a"关系时,或者需要使用多态性时,继承是一个更自然的选择。例如,派生类是基类的一种特殊类型。
- 混合使用: 在实际设计中,通常会使用继承和组合的混合方式,以便充分利用它们各自的优势。这被称为"组合优于继承"原则,强调了在设计中应该更多地使用组合而非继承,以提高代码的灵活性和可维护性。