目录
1.继承的概念和定义
1.1继承的概念
1.2继承的定义
1.2.1定义格式
1.2.1不同继承方式下父类成员访问方式的变化
2.父类和子类对象赋值转换
3.继承中的作用域
3.1成员变量
?3.2成员函数
4.子类的默认成员函数
?4.1子类的默认构造函数
?4.2子类的默认析构函数
4.3子类的默认拷贝构造函数
4.4子类默认operator=
5.继承与友元
5.1友元函数
5.2继承中的友元函数
6.继承与静态成员
7.菱形继承及菱形虚拟继承
7.1单继承、多继承
7.1.1单继承:一个类只有一个直接父类。
?7.1.2多继承:一个类有两个或两个以上的直接父类。
?7.1.3菱形继承(多继承的一种特殊情况)
7.2菱形继承存在的问题?
7.3菱形继承的二义性
7.4虚拟继承
8.继承和组合的对比
1.继承的概念和定义
1.1继承的概念
继承:代码复用的重要的手段,使得子类具有父类的属性、重新定义、追加属性和函数等;可以在保持原有类特性的基础上进行扩展,增加功能。
继承的概念并不是固定的,只要能够通过自己的语言组织起来,再结合一些实例能够解释就可以了。
1.2继承的定义
既然提到继承的定义,那么至少要有两个类才能够完成,我们可以先定义一个Person类:
class Person
{
void show()
{
cout << "name:" << name << " " << "age:" << age << endl;
}
string name;
int age;
};
1.2.1定义格式
以学生(Student)类为例,继承Person类:
class Student : public Person
{};
1.Person类是父类,也称为基类;Student类是子类,也称为派生类。
2.继承方式和访问限定符一样,也有三种(public、protected、private),但是和访问限定符表示的有所差别。
1.2.1不同继承方式下父类成员访问方式的变化
下面来演示一下,不同限定符的父类成员在子类中的变化:
1.父类中的public成员,公有继承
class Person
{
public:
void show()
{
cout << "name:" << name << " " << "age:" << age << endl;
}
string name;
int age;
};
class Student : public Person
{};
int main()
{
Student s1;
s1.name = "阿飞";
s1.age = 19;
s1.show();
return 0;
}
Student中没有任何成员,只有从Person类中继承下来的name和age。
2.父类的protected成员,公有继承
同样使用1中的Person类,只是把成员变量name和age改为了protected:
class Person
{
public:
void show()
{
cout << "name:" << name << " " << "age:" << age << endl;
}
protected:
string name;
int age;
};
子类继承之后成员属性为protected,不能在类外进行访问。
?protected属性的成员在类内是可以访问的:
class Student : public Person
{
public:
void Set(string m_name, int m_age)
{
name = m_name;
age = m_age;
}
};
int main()
{
Student s1;
s1.Set("阿飞", 19);
return 0;
}
类外无法访问类内的protected/private成员,但是可以设置公有的接口对类内的protected/private成员进行访问。?
3.父类的private成员,公有继承
上面提到,父类的private成员在子类中是不可见的,那么这个不可见是什么含义呢?
class Person
{
public:
void show()
{
cout << "name:" << name << " " << "age:" << age << endl;
}
private:
string name;
int age;
};
在子类Student中设置公有的属性去访问父类中的private是否可行?
不可见: 子类对象在类内和类外都无法进行访问。
一般的话,我们不会设置父类的成员为private,除非不行被子类继承的成员。
关于继承方式和访问限定符就演示这三种情况,剩下的几种情况大家感兴趣的话可以自己去演示一下。
下面进行一下总结:
- 父类中的private成员在派生类中无论以什么方式继承都是不可见的。(语法上限制子类对象不管是在类内还是类外都不能访问)
- 如果子类成员不想在类外被访问,但需要在类内访问的,就可以定义为protected。
- 父类的私有成员子类不可见,其他成员在子类中的访问方式 为继承方式和访问限定符中权限小的一个。(public > protected > private)
- 使用class定义类时默认的继承方式是private,使用struct默认继承方式为public,不过最好显示写出继承方式。
- 实际运用中一般使用public继承,很少用到protected和private继承。
? ? ?
2.父类和子类对象赋值转换
Student类公有继承Person类:
class Person
{
public:
string name;
int age;
};
class Student : public Person
{
public:
int id;//学号
};
定义一个子类的对象,那么能不能赋值给父类?如果能是否发生了类型的转换?
int main()
{
Student s;
Person p = s;
return 0;
}
通过编译,可以得出结论:子类对象可以赋值给父类对象。
在子类对象赋值给父类对象的时候,实际上发生了切片,把子类对象中继承父类的成员切割赋值给了父类对象。?
下面对以上结论进行扩展,既然子类对象可以赋值给父类,那么子类对象的地址能不能赋值给父类对象的指针?子类对象能不能赋值给父类对象的引用?
int main()
{
Student s;
Person p = s;
Person* pp = &s;
Person& ps = s;
return 0;
}
这两种情况同样也是正确的,通过画图来加深一下理解:
??
上面我们遗留了一个问题:赋值的时候发生了切割,?为什么不是类型转换?
int main()
{
int a = 10;
const double& d = a;//a赋值给d的时候产生一个临时变量,临时变量具有常性,不加上const会报错
return 0;
}
子类对象赋值给父类引用的时候,没有加const,而且没有报错,说明没有发生类型转换。
注意:子类对象可以赋值给父类对象,但是父类对象不能赋值给子类对象。
3.继承中的作用域
3.1成员变量
每一个变量都有其对应的作用域,类中也有属于自己的类域;而且不同的类有不同的类域。
父类和子类中的成员在不同的类域中。
class Person
{
public:
string name;
int age;
};
class Student : public Person
{
public:
string name;//父类中有同名的name
int id;//学号
};
int main()
{
Student s1;
s1.name = "阿飞";
s1.age = 19;
return 0;
}
通过s1访问name,首先访问的是Student类中的name,因为在这里存在一个就近原则;s1属于Student类,首先调用Student类域中的name。
?3.2成员函数
class Person
{
public:
void func(int n)
{
cout << "func(int n)" << endl;
}
string name;
int age;
};
class Student : public Person
{
public:
void func()
{
cout << "func()" << endl;
}
int id;//学号
};
int main()
{
Student s1;
s1.func();
return 0;
}
成员函数的调用同样满足就近原则:
那如果使用子类对象传参数调用func(),运行结果是什么?
答案是:编译报错。
这里来总结一下:
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫作重定义。
- 在子类成员函数中,可以使用? 父类::父类成员 显示访问。
- 成员函数的隐藏,只需要函数名相同。?
显示访问一下父类中的func():
注意:父类和子类中的同名函数参数不同,并不能构成函数重载;因为函数重载要求函数必须要在相同的作用域。
4.子类的默认成员函数
提供一个Person类,类中提供了构造函数(有缺省值)、拷贝构造函数、operator=、析构函数。
class Person
{
public:
Person(const char* name = "peter")
: _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; // 姓名
};
?4.1子类的默认构造函数
- 子类成员,跟类和对象一样(内置类型不处理,自定义类型调用它的构造函数)。
- 继承的父类成员,必须调用父类的构造函数。
class Student : public Person//子类Student公有继承Person类
{
protected:
int _id; //学号
};
Student(const char* name, int id)
: Person(name)//调用父类的构造函数
, _id(id)
{
cout << "Student()" << endl;
}
注意:子类构造函数的调用顺序是父类先于子类。
?4.2子类的默认析构函数
- 子类成员,跟类和对象一样(内置类型不处理,自定义类型调用它的析构函数)。
- 继承的父类成员,必须调用父类的析构函数。
~Student()
{
~Person();
cout << "~Student()" << endl;
}
析构函数可以这样写吗?
由于多态的需要,父类和子类析构函数的名字会统一处理为destructor();
这也就造成了子类的构造和父类的构造构成了隐藏。
指定调用父类的析构:
~Student()
{
Person::~Person();
cout << "~Student()" << endl;
}
如果显式的调用析构会存在一个问题,创建一个子类对象,清理的时候会调用两次父类的析构。
注意:子类析构函数不需要显式调用父类的析构函数。
每个子类析构函数后面,会自动调用父类的析构函数,这样才能保证先析构子类,再析构父类(栈中先进后出)。
4.3子类的默认拷贝构造函数
- 子类的成员跟类和对象一样(内置类型值拷贝,自定义类型调用它的拷贝构造)
- 继承的父类成员,必须调用父类的拷贝构造。
Student(const Student& s)
: Person(s)//子类传参给父类时发生切片
, _id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
?
?拷贝构造函数调用之后,s2中的_name和_id都是相等的,显式写的子类拷贝构造完成。
4.4子类默认operator=
- 子类的成员跟类和对象一样(内置类型值拷贝,自定义类型调用它的operator=)
- 继承的父类成员,必须调用父类的operator=。
一种错误示例写法:
Student& operator = (const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
operator =(s);
_id = s._id;
}
return *this;
}
?父类中的operator=和子类中的函数名相同,构成了隐藏,如果不显式调用父类中的operator=,会不断的进行子类operator,最后导致栈溢出。?
正确写法:
Student& operator = (const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
Person::operator =(s);
_id = s._id;
}
return *this;
}
5.继承与友元
5.1友元函数
友元函数:某些虽然不是类中的成员却能够访问类的所有成员的函数,类授予它的友元特别的访问权。
class Person
{
friend void Display(const Person& p);//Display是Person类的友元函数
public:
void SetName(const string& name)
{
_name = name;
}
protected:
string _name; // 姓名
};
void Display(const Person& p)
{
cout << p._name << endl;
}
int main()
{
Person p;
p.SetName("阿飞");
Display(p);
return 0;
}
?
友元函数解决了类外不能访问类中protected/private的问题。
5.2继承中的友元函数
友元关系不能继承,也就是说父类的友元函数不能访问子类中的protected和private成员。
class Student;//声明子类
class Person
{
public:
friend void Display(const Person& p, const Student& s);//父类的友元函数
protected:
string _name = "阿飞"; // 姓名
};
class Student : public Person
{
protected:
int _stuNum = 666; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
//cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
函数Display()是父类Person的友元函数,可以访问父类中的protected/private成员;
但是友元关系不能继承,所以无法访问子类中的_stuNum。
?如果同时想要访问子类中的protected/private,可以把函数声明为子类的友元。
class Student : public Person
{
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum; // 学号
};
6.继承与静态成员
一般成员在子类和父类中都是单独的一份,而静态成员在父类和子类中是同一份。
class Person
{
public:
static int _count;
};
int Person::_count = 0;//静态成员只能在类外进行初始化
class Student : public Person
{};
int main()
{
Person p;
Student s;
cout << s._count << endl;
Person::_count++;
cout << s._count << endl;
//打印一下父类和子类中静态成员的地址
cout << &Person::_count << endl;
cout << &Student::_count << endl;
return 0;
}
??
子类对象和父类对象中的静态成员_count是同一份,改变父类对象中的_count,子类对象中的_count也会随之改变。?
7.菱形继承及菱形虚拟继承
7.1单继承、多继承
7.1.1单继承:一个类只有一个直接父类。
?7.1.2多继承:一个类有两个或两个以上的直接父类。
?7.1.3菱形继承(多继承的一种特殊情况)
7.2菱形继承存在的问题?
?从上图的对象模型,可以看出菱形继承有数据冗余和二义性的问题。(在Assistant对象中Person成员有两份)
7.3菱形继承的二义性
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 a;
a._name = "peter";
return 0;
}
?菱形继承的二义性导致Assistant类中的_name访问不明确的问题,此问题可以显式指定类域来解决。
int main()
{
Assistant a;
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
虽然可以指定类域来进行访问,但是这样无法从根本上解决菱形继承存在的问题。
7.4虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余问题。
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
下面来借助VS2019中的调试内存窗口来观察一下类对象成员分配:
1.菱形继承(非虚拟继承)
B中和C中都有一份A,导致数据冗余。
2.菱形虚拟继承
下图是菱形虚拟继承的内存对象成员模型:A同时属于B和C,那么B和C如何去找公共的A呢?
这里通过了B和C的两个指针,指向的一张表;这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的是偏移量,通过偏移量可以找到A。
使用到虚基表指针和虚基表的两种情况举例:
int main()
{
//1.切片->需要找到A
D d;
B b = d;
B* pb = &d;
//2.通过父类B、C访问A中的成员->通过偏移量和地址找到_a
pb->_a = 10;
return 0;
}
8.继承和组合的对比
- public继承是一种is-a的关系;例如 Student is-a Person
- 组合是一种has-a的关系;例如 车has-a轮胎(a只是一个量词,实际不一定是一个)
- 如果两个物体之间的关系既可以是is-a,也可以使has-a,那么优先使用has-a的组合
在编程中,我们追求的是一种“高内聚,低耦合”:继承一定程度上破坏了基类的封装,父类中的protected属性的成员在子类内是可以使用的,父类和子类之间的依赖关系强,耦合度高。
但如果使用的是组合,那么protected属性脱离了类内,在类外不能够使用,依赖关系较弱,耦合较低。
也有些关系适合使用继承,例如要实现多态就必须要使用继承。
面向对象三大特性之——继承到这里就结束了,喜欢这部分内容的铁汁们可以给博主一个三连支持,你们的支持是博主最大的动力,后续会继续更新面向对象三大特性中的多态,喜欢的铁汁们记得三连哈。
|