面向对象的三大特性
- 封装
- 继承
- 多态
继承的概念和定义
继承的本质就是类层次的复用。
-
继承的概念
-
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段.
它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类, 称派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
继承是类设计层次的复用
基类(父类) 派生类(子类)
父类就是被继承的类。
子类就是继承的类。
子类复用了父类
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "tongtong"; // 姓名
int _age = 15; // 年龄
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
int main()
{
Student s;
//如果共有子类可以用父类的函数
//私有就不可以
s.Print();
return 0;
}
用上面的定义来说,
person就是父类,student就是子类,子类会复用父类
看打印的结果,我们发现,虽然studen没有名字和年龄,但是却可以打印,原因就是继承了
继承关系和访问限定符
class Student : public Person
,
在定义student
的时候其中pubilc
就是继承方式。
继承关系和访问限定符都是有三种,public,protected,private
所以继承关系和访问限定符有9种组合方式,具体如下图。
类成员/继承方式 | pubilc 继承 | protected 继承 | private 继承 |
---|---|---|---|
父类的pubilc 成员 | 子类的pubilc 成员 | 子类的protected 成员 | 子类的private 成员 |
父类的protected 成员 | 子类的protected 成员 | 子类的protected 成员 | 子类的private 成员 |
父类的private 成员 | 在子类中不可见 | 在子类中不可见 | 子类中不可见 |
当在继承的时候,可以不写继承关系。
但是class默认的继承方式是private,struct的继承方式是public。
-
总结
- 访问方式==父类访问限定符和限定方式小的。pubic>protected>private.
-
在父类中,这两个访问限定符没有区别,都是在类中可以访问,在类外不可访问。
但是在子类中,他们两个有区别。
protected
在子类中可见,不可用。
private
在子类中不可见,不可用。
protected
和
private
的区别
父类和子类的赋值转换
将子类转换为父类,不需要发生类型转换,不会产生临时变量,天然支持的。
可以这样理解,父类是特殊的子类。
int main()
{
//发生隐式类型类型转换
double d = 1.5;
int i = d;
//必须用const,临时变量具有常性
const int& i2 = d;
//天然支持的,不存在类型转换发生
Student s;
Person p = s;
//没有产生临时对象,所以不用加const
Preson p2& = s;
Preson* ptr = &s;
return 0;
}
如果不理解,可以形象的看这个图。
-
那么问一个问题?
-
父能不能给给子?
不可以,父类的东西比子类的东西少。
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用
基类::基类成员
显示访问) - 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
-
问题:既然子类和父类有自己的作用域,那么能不能有同名变量或同名函数?(隐藏/重定义)
-
可以,独立的作用域当然可以拥有同名的变量或函数。
就近原则,优先访问自己作用域,
但是如果想访问同名的其他作用域,指定作用域就可以。
做下面这个练习题
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;
}
};
//A:两个fun构成函数重载
//B:两个fun构成隐藏
//C:编译报错
//D:以上说法都不对
B,
在不同的作用域,不可能构成函数重载,这两个类也没有问题,他们构成隐藏。
有人会想,他们同名,参数不同还构成隐藏吗?
如果是成员函数的隐藏,只需要保证函数名相同就可以。不用考虑参数,和返回值。
如果想用A的fun怎么用?
B().A::fun();
加上域作用限定符就可以,我用的是匿名对象,大家也可以不用。
子类的默认成员
先考虑默认成员函数
先给大家一个父类,写一个
student
子类
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; // 姓名
};
1、子类的构造函数
父类的成员必须调用父类的构造进行初始化。如果在子类中不写构造和析构函数,调用父类的构造函数和析构函数。
父类的成员调用父类的构造函数初始化,子类的自己的成员自己初始化,
用到方法就是在初始化列表中,按照这个方式初始化person(name)
,调用父类的构造,用我们自己传的参数构造。
如果不写person(name)
,也会调用父类的构造函数,用缺省参数构造。
我们可以这样理解,将父类当成子类的成员。
class Student : public Person
{
Student(const char* name,int num)
:Person(name)//规定这么写,调用父类的构造函数初始化
//如果不写person(name)也可以调用父类的构造函数。
,_num(num)
{}
protected:
int _num;
};
int main()
{
Student s("tongtong",20);
return 0;
}
2、子类的拷贝构造
如果不写子类的拷贝构造,会调用父类的默认的拷贝构造。
class Student : public Person
{
public:
Student(const Student& s)
{}
protected:
int _num;
};
int main()
{
Student s("tongtong",20);
Student s2(s);
return 0;
}
对于上面这种写了拷贝构造但不做处理,编译器不会调用父类的拷贝构造。只进行了一次构造,还是因为传参的时候调用。
初始化父类,就传父类的拷贝构造
class Student : public Person
{
public:
Student(const Student& s)
//直接将子类切片。调用父类的拷贝构造
:Person(s)
,_num(s._num)
{}
protected:
int _num;
};
3、赋值 重载
Student& operator =(const Student& s)
{
if (this != &s)
{
Person::operator =(s);
_num = s._num;
}
return *this;
}
4、析构函数
//析构函数会被处理为Destructor,构成隐藏所以用作用域
~Student()
{
Person::~Person();
}
int main()
{
Student s1("tongtong", 20);
}
如果这样我们的析构会调用三回,本来只需要调用两回,多调用了一会的原因是
Person::~Person();
这个多调用了一会。
析构函数不需要我们自己显示调用,他自己调用父类的析构和子类的析构。
所以为了保证析构的顺序,先析构子再析构父。
如果我们自己操作不能保证这个顺序,
所以编译器帮我们做,子类析构函数完成时,会自动调用父类析构函数,保证先析构子再析构父。
因为要保证顺序,
构造的顺序肯定是先构造父类在构造子类。
四个子类的默认函数调用,除了析构函数,顺序都是先父后子。
5、继承和友元
友元关系不能继承。
下面代码有错误,因为友元函数不能继承,所以我们再有友元函数中不能访问子类的成员,没有继承了父类的友元。
class Student;
class Person
{
public:
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;
//错误的代码下一句,不可访问
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
但是如果想要访问,就在子类加上友元函数的声明就可以了
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);
}
6、继承与静态成员
父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例
.7、复杂的菱形继承及菱形虚拟继承
一、单继承
一个子类只有一个直接父类时称这个继承关系为单继承
二、多继承
一个子类有两个或以上直接父类时称这个继承关系为多继承
三、多继承会引起菱形继承
菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
使用虚继承解决数据冗余和二义性
8、虚继承继承
现在有一个菱形继承(A B C D)
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
在主函数中测试,不能再监视窗口观察,它已经经过处理,我们要在内存窗口观察。
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
上面没有虚继承,就是单纯的多继承,会有数据冗余和二义性,先观察一下
使用虚继承
内存窗口
将公共继承的A放到一块空间,
B通过第一个地址找到偏移量20,向下走5个地址,从而找到A,就可以改变A。
C通过第一个地址找到偏移量12,向下走3个地址,从而找到A,就可以改变A。
这样就只有一块A,解决数据冗余和二义性
9、继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。