凡是面向对象的语言,都有三大特性,继承,封装和多态,但并不是只有这三个特性,是因为者三个特性是最重要的特性,那今天我们一起来看继承!
目录
1.继承的概念及定义
1.概念
2.继承的定义
2.基类和派生类对象赋值转换
3.继承中的作用域
4.派生类的默认成员函数
1.构造和拷贝构造,赋值
2.析构函数的两怪!
5.继承与友元
6. 继承与静态成员
7.多继承
7.1继承分类
7.2 菱形继承 &&菱形虚拟继承
1.解决二义性的过程(指定作用域)
2.解决数据冗余(需要虚继承)
8.继承和组合(都是一种复用)
总结:
1.继承的概念及定义
1.概念
class Person { protected: char* _name; int _old; }; class student :public Person { private: char* _id; }; int main() { student s; }
当好多类都需要写Person类中的成员时 ,为了避免数据冗余,就可以使用类的继承,使代码复用,继承是让每一个派生类中,都有一份基类(父类)的成员。
2.继承的定义
继承的方式:(当然继承的目的就是为了让子类可以拥有父类的成员,并访问,所以一般情况下,我们只会进行公有继承:public:)
那么我们来看一下,继承方式和访问之间的关系:
首先必须知道的一点是:基类中有私有成员时,子类中继承的父类的私有成员不可见。
(不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。)
其次就是,基类成员的权限和继承方式的权限,谁的权限更小,在子类中继承的成员就是更小的那个权限。public > protected> private。
如上图所示:
2.基类和派生类对象赋值转换
首先得回想起赋值转换这个过程:
不同的类型相互赋值时,中间会产生临时变量,通过临时变量进行赋值转换。
但是若子类和父类进行赋值交换时,并不产生中间的临时变量,而是天然的一个赋值。
(只能向上转换,即子类赋值给父类,字可以给父,父不可以给子)
看下面代码:
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
public:
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
int i = 1;
double d = 2.2;
//中间会产生一个临时变量,临时变量具有常性,不可以改变。
i = d;
//所以此时ri是中间的临时变量的引用,而不是d的引用,如果不加const,就会放大权限
const int& ri = d;
//但父类和子类之间的赋值就不会产生中间的临时变量
Person p;
Student s;
// 中间不存在类型转换,天然的一个赋值
p = s;
Person& rp = s;//对s的引用,可以访问和修改成员变量
rp._age = 1;
Person* ptrp = &s;
ptrp->_age++;
return 0;
}
只能向上转换,即子类赋值给父类,字可以给父,父不可以给子
3.继承中的作用域
我们知道,一个类他就是一个域(作用域)。同一作用域不能定义同名的两个变量,但不同作用域它可以定义两个同名变量。所以父类,子类中都有同名的成员变量时,默认会自动访问子类的成员,因为就近原则,若想访问父类的成员,那就可以指定作用域!
同一作用域,定义两个同名函数,且参数不同叫做函数重载;
不同作用域,定义两个同名函数,叫做重定义;
举例说明:
class A
{
public:
void fun()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "B::func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
};
这中情况,函数构成什么???函数重载?? 重定义(隐藏)??编译错误???
首先我们知道两个类中的同名函数,在不同作用域,这就构成了重定义(隐藏)!
若访问父类成员函数即:b.A::fun();
4.派生类的默认成员函数
1.构造和拷贝构造,赋值
先回顾一下,默认成员函数(无参,全缺省,编译器自己生成)
具体分析:
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)
{}
Student(const Student& s)
//子类显示拷贝构造时,父类不可以直接访问进行初始化,必须调用父类自己的显示拷贝构造函数
:Person(s)
, _num(s._num)
{}
Student& operator=(const Student& s)
{
if (this != &s)
{
//赋值的运算符重载,两个同名函数构成了隐藏,需要指定作用域
Person::operator=(s);
_num = s._num;
}
return *this;
}
protected:
int _num; //学号
};
int main()
{
Student s1("张三", 18);
Student s2(s1);
Student s3("李四", 20);
s1 = s3;
return 0;
}
最重要的一句话:父类成员必须调用父类自己的构造函数,拷贝构造完成初始化或拷贝。
或者说:子类中的父类那部分成员由父类自己的构造或者拷贝构造实现初始化或者拷贝。
2.析构函数的两怪!
直接看代码:
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;
delete[] p;
}
protected:
string _name; // 姓名
int* p = new int[10];
};
class Student : public Person
{
public:
Student(const char* name)
:Person(name)
, _num(1)
{}
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;
}
// 第一怪:1、子类析构函数和父类析构函数构成隐藏关系。(由于多态关系需求,所有析构函数都会特殊处理成destructor函数名)
// 第二怪:子类先析构,父类再析构。子类析构函数不需要显示调用父类析构,子类析构后会自动调用父类析构
~Student()
{
//Person::~Person();
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
int main()
{
Student s("张三");
return 0;
}
1、子类析构函数和父类析构函数构成隐藏关系。(由于多态关系需求,所有析构函数都会特殊处理成destructor函数名)
2.子类先析构,父类再析构。子类析构函数不需要显示调用父类析构,子类析构后会自动调用父类析构
构造顺序:先父类,再子类;析构顺序:先子类,再父类。
5.继承与友元
若子类对象也想访问友元函数,那只能在子类中也加上友元!(但不建议使用友元,会破坏继承关系)
6. 继承与静态成员
子类继承父类,不是继承父类这个对象,而是会有一份父类的模型。父类有的成员变量,子类也会有一份,互不干扰。
但静态成员就不一样了,他们是同一份;静态成员属于整个类和类的所有对象。同时也属于所有派生类及派生类的对象。
class Person { public: //friend void Display(const Person& p, const Student& s); protected: string _name; // 姓名 public: static int _num; }; int Person::_num = 0; class Student : public Person { protected: int _stuNum; // 学号 }; void test() { Student s; Person p; cout << p._num << endl; p._num++; cout << p._num << endl; s._num++; cout << s._num << endl; cout << &s._num << endl; cout << &p._num << endl; }
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例重点:Person* ptr = nullptr; ptr->Print(); cout << ptr->_num << endl; cout << ptr->_name << endl; cout << (*ptr)._num << endl; (*ptr).Print();
对象里面只存成员变量,成员函数在代码段中,所以以上代码哪个不对呢?
我们知道空指针不能解引用,解引用意思是,这里是去访问指针指向对象的内部成员,那看一看哪个访问了内部的成员呢?
函数不在内部,在代码段,可以!
_num为对象内部成员变量,不能解引用访问,不可以!
(*ptr)是解引用了吗?我们不能凭借解引用符号来判断是否解引用,我们需要看内部的访问情况,(*ptr)->Print();并没有访问内部成员,可以!
(*ptr)->_num;也可以,_num是静态成员,不在成员里面。
7.多继承
7.1继承分类
单继承:一个子类只有一个直接父类
多继承:一个子类有两个或两个以上的父类
菱形继承:是多继承的一种特殊情况,会产生数据冗余和二义性!
(person类的中的成员,会在student和teacher中都有一份,assistant继承student和teacher时,assistant中会有两份person,造成了数据冗余和二义性)
解决方法:
解决二义性:
可以通过访问限定符来指定访问哪一个成员。
那如何解决二义性的问题呢?
此时虚继承就上线了!
虚继承在腰部继承,谁引发的数据冗余,谁就进行虚继承(解决冗余)
由此可见,加上virtual,变为虚继承以后,确实解决了数据的冗余
那么到底如何解决的呢??具体下面分析!
7.2 菱形继承 &&菱形虚拟继承
1.解决二义性的过程(指定作用域)
(菱形继承)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 = 1; d._c = 2; d._d = 3; d.B::_a = 4; d.C::_a = 5; }
2.解决数据冗余(需要虚继承)
(菱形虚拟继承)
class A { public: int _a; }; class B:virtual public A { public: int _b; }; class C:virtual public A { public: int _c; }; class D:public B,public C { public: int _d; }; int main() { D d; d._b = 1; d._c = 2; d._d = 3; d.B::_a = 4; d.C::_a = 5; }
那如果遇到这种情况呢???父子类的赋值转换(切片)
class A { public: int _a; }; class B:virtual public A { public: int _b; }; class C:virtual public A { public: int _c; }; class D:public B,public C { public: int _d; }; int main() { D d; d._b = 1; d._c = 2; d._d = 3; d._a = 4; d._a = 5; B b; b._a = 1; b._b = 3; B* ptr = &b; ptr->_a = 2; ptr = &d; ptr->_a = 6; }
从b对象可以看的出来,只要是虚继承以后,就会把虚基类放到最下面;
就像切片这种情况,ptr指向不同,那么距离虚基类的距离就不同,所以就必须要有虚基表的地址,来访问虚基表继而找到偏移量,然后访问到虚基类!
我们通常使用下,很忌讳出现菱形继承,但可以多继承。
可以看得出,虚继承在时间上确实有损耗,过程比较复杂,但是如果虚基类比较大时,就可以很大程度上节省内存。
8.继承和组合(都是一种复用)
总结:
一口气说了这么多,你学会了吗?细节还是比较多的,我们应该下去多多自己琢磨,反复调试,去感受过程,从而理解的更深刻!下期再见!