目录
(3) 继承与组合
1.继承的概念和定义
1.1 继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是镭射机层次的复用;
以学生类和老师类为例,两类存储信息都有姓名、年龄等,就可以将姓名与年龄单独编写成Person类,这样就可以实现类设计定义层次的复用:
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age<<endl;
}
string _name = "peter";
int _age = 18;
};
class Student : public Person
{
protected:
int _stuid;
int _major;
};
class Teacher :public Person
{
protected:
int _jobid;
};
int main()
{
Student s;
s._name = "张三";
s._age = 18;
s.Print();
Teacher t;
t._name = "赵老师";
t._age = 40;
t.Print();
return 0;
}
1.2 继承的定义
1.2.1 继承格式
1.2.2 继承关系和访问限定符
1.2.3 继承基类成员访问方式的变化
总结:基类private成员在派生类中无论以什么方式继承都是不可见的。
基类的其他成员在子类的访问方式=Min(成员在基类的访问限定符,继承方式),其中:public>protected>private;
PS:
(1)当不显式书写继承方式时,struct默认继承方式为public,class默认继承方式为private;
(2)如果基类成员希望在类外不能被访问,而在派生类中可以进行访问,就可以将其成员设置为protected。可见,protected是因继承才出现的;
2.基类和派生类的赋值转换
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student :public Person
{
public:
int _No;
};
int main()
{
Student sobj;
//1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj; //1
Person* pp = &sobj; //2
Person& rp = sobj; //3
//以上代码虽是不同类型,但不是隐式类型转换,可视为语法特殊支持子类对象赋值给父类对象(仅公有继承支持)
//2.基类对象不能赋值给派生类对象
//sobj = pobj;
//3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj;
Student* ps1 = (Student*)pp;
ps1->_No = 10;
}
PS:(1)第一类中标注123的代码语句,并不是一种隐式类型转换。
此处对隐式类型转换作简要解释:
//隐式类型转换:
int i = 0;
//double& d = i;//本质要产生一个double类型的临时变量,而临时变量具有常性,会导致权限放大,错误
//需修改为:
const double& d = i;
//同理上文第3行代码,能说明这种赋值操作不是隐式类型转换;
(2)基类对象不能赋值给派生类对象,其中基类的指针可以通过强制类型转换赋值给派生类的指针,但这其实是非常危险的,并不建议采用;
3.继承中的作用域
(1)在继承体系中,基类和派生类都有独立的作用域;
(2)子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况较隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111; //身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 学号:" << _num << endl; //999
cout << " 身份证号:" << Person::_num << endl; //111
}
protected:
int _num = 999; // 学号
};
void main()
{
Student s1;
s1.Print();
}
当重名时,需要指定类域才可以进行对父类成员的访问;
(3)如果是成员函数的隐藏,只需要函数名相同就构成隐藏;
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
};
void main()
{
B b;
b.fun(10);
//b.fun(); 访问错误,默认访问子类中参数为i的函数,而参数不匹配就报错
b.A::fun();
}
在上文代码中,父类和子类中的fun不构成重载:重载要求两个同名函数必须在同一作用域。
但两个fun函数构成隐藏。
(4)在实际继承体系中,最好不要定义同名的成员;
4.派生类的默认成员函数
class Person
{
public:
Person(const char* name)
: _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;
}
protected:
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;
}
//由于多态的需要,析构函数的名字会统一处理为destructor()
//子类的析构函数与父类的析构函数构成隐藏
~Student()
{
//Person::~Person();
//不需要显示调用父类析构函数
//每个子类析构函数后面会自动调用父类析构函数以保证先析构子类后析构父类;
cout << "~Student()" << endl;
}
protected:
int _num;
};
int main()
{
Student s1("zhnangsan",1);
Student s2(s1);
Student s3("lisi", 2);
s1 = s3;
return 0;
}
(1)编译器默认生成的构造函数:
① 对自己类的成员,同类和对象一样,内置类型不处理,自定义类型调用它的默认构造函数;
② 继承的父类成员,必须调用父类的构造函数初始化;
(2)编译器生成默认拷贝构造:
① 对自己类的成员,同类和对象一样,内置类型完成值拷贝,对于自定义类型调用它的拷贝构造函数;
② 继承的父类成员,必须调用父类的拷贝构造函数;
(3)编译器默认生成的operator=
① 对自己类的成员,同类和对象一样,内置类型完成值拷贝,对于自定义类型调用它的拷贝构造函数;
② 继承的父类成员,必须调用父类的operator=
(4)编译器默认生成的析构函数:
① 对自己类的成员,同类和对象一样,内置类型不处理,对于自定义类型调用它的默认析构函数;
② 继承的父类成员,必须调用父类的析构函数;
5.继承和友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员;
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
//friend void Display(const Person& p, const Student& s);
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
//cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
Display函数时父类Person的友元函数,但不是子类Student的友元函数,如需利用Display函数访问子类成员变量,需在子类内声明Display函数为友元函数;
6.继承和静态成员
基类定义了static静态成员,则整个集成体系里面只有一个这样的成员。
无论派生出多少个子类,都只有一个static成员;
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
}
即普通成员继承后,如_name,p._name与s._name是不同的,而对于count,Person::count与Student::count是同一个count;
7.菱形继承及菱形虚拟继承
(1)单继承:一个子类只有一个直接父类;
(2)多继承:一个子类有两个及其以上的的直接父类;
(3)菱形继承:是多继承的一种特殊情况:
菱形继承的基本问题:数据冗余(2个Person类)和二义性(访问_name二义)
7.1 菱形继承的二义性
试运行以下代码:
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant at;
at._name="Jack";
}
运行报错如下:
为了解决菱形继承的二义性问题,我们可以通过指定作用域来避免二义:
Assistant at;
at.Student::_name = "Jack";
at.Teacher::_name = "Mike";
注意指定作用域是指定直接父类(Student或Teacher)而非初始定义该变量的类(Person);
我们通过指定类域可以避免二义性的问题,但是无法避免数据冗余的问题;
7.1.2 菱形继承的数据冗余
虚拟继承可以解决菱形继承的数据冗余问题,在上例中,Student和Teacher类采取虚拟继承Person就可以解决数据冗余的问题:
增加关键字virtual关键字即可,实现代码如下:
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant at;
at._name = "John";
at.Student::_name = "Jack";
at.Teacher::_name = "Mike";
}
采取虚拟继承后,编译器会增加一个存放虚基类的公共区域,同时在中间继承类中增加了两个指针保存距离其共同父类的偏移量,从而在切片时查询到共同父类的地址:
8.关于继承的思考
(1)如何设计一个不能被继承的类
父类构造函数私有化:
C++98:子类不可见,当子类实例化对象时,就会报错:
class A
{
private:
A() //将父类构造函数私有化——>子类不可见
{}
protected:
int _a;
};
class B:public A
{};
int main()
{
B bb;
//子类对象实例化时,无法调用构造函数
return 0;
}
C++11:在不能被继承的类后加关键字final修饰(即使不实例化,编译器也会报错):
class A final
{
private:
A() {}
protected:
int _a;
};
class B:public A
{};
int main()
{
return 0;
}
(2)多继承与切片
同理若class Derive : public Base2,public Base1 { public: int _d; }; 则此时p2与p3相等,与p1不相等;
打印地址可发现p2的值更大,说明在栈帧中,开辟空间的方式如下图所示:
(3) 继承与组合
1.public 继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象;
2.组合是一种has-a的关系,假设B组合了A,每个B对象都有一个A对象;
3.优先使用组合而不是类继承;