前面我们讲过面向对象的第一大特性——封装,接着我们要面对的就是第二大特性——继承,那继承是啥呢?从功能的角度来说,就是复用。
比如:我们都有人的特性(性别,外貌,身份证号)
,那在社会上,我们可能还是学生、老师、工人
等具有身份意义的信息,那如果在描述学生时,我们还需要把人的特性描述一遍,那未免有点太繁琐了,因此,学生,老师,工人就可以复用
人的特性信息,再此基础上再添加对应身份特有的信息即可。
前面我们通过举例,能够简单理解继承,下面我们来说一下,具体的定义:
接下来我们从空间的角度来谈一下继承,一个对象具体划分,可以分为:
如何验证空间上的继承呢?我们先讲继承的用法,之后会证明。
下面我们从权限的角度来理解,如何定义一个继承类:
那继承方式有什么用呢?一张表即可说明:
protected成员与private成员区别:基类的protect成员在继承之后还能通过派生类在类里进行使用,而private则无法访问。
此时我们再来完成上面的证明:
代码:
class A
{
public:
void func()
{
cout << "A::func()"<<endl;
}
int _a = 1;
static int _c;
};
int A::_c = -1;
class B : public A
{
public:
int _b = 0;
};
int main()
{
A a;
B b;
//成员函数
a.func();
b.func();
cout << endl;
//非静态的成员变量
cout << &a._a << endl;
cout << &b._a << endl << endl;
//静态的成员变量
cout << &a._c << endl;
cout << &b._c << endl;
return 0;
}
再来运行代码:
总结:
成员函数和静态成员变量继承的是使用权。非静态成员变量继承的是一份成员变量的拷贝。
在讲赋值转换之前,我们得先来搞懂,继承的引用和指针的用法。
class A
{
public:
void func()
{
cout << "A::func()"<<endl;
}
int _a = 1;
static int _c;
};
int A::_c = -1;
class B : public A
{
public:
int _b = 0;
};
int main()
{
B b;
int i = 0;
int& ri = i;
const double& di = i;
A& rb = b;
A* rptr = &b;
return 0;
}
我们之前讲过,不同类型的引用中间会生成临时变量,而临时变量具有常属性,因此这里的i转换为double中间会生成临时变量,因此需要加上const。
但是继承之后的类转换为基类,不会生成临时变量,因此没有加上const。这种现象被称之为向上转换,也就是能子类向父类进行转换。
拓展:
一个指向派生类的基类指针可以通过安全转换,来转换为子类的指针,从而达成向上转换。
class A
{
public:
void func()
{
cout << "A::func()"<<endl;
}
int _a = 1;
static int _c;
};
int A::_c = -1;
class B : public A
{
public:
int _b = 0;
};
int main()
{
B b;
A* rptr = &b;
B* rrptr = static_cast <B*>(rptr);
//B* rrptr = dynamic_cast (rptr);
//这个是虚函数才能使用的(多态会将)。
return 0;
}
明白了这些,赋值转换就不难理解了。
class A
{
public:
void func()
{
cout << "A::func()"<<endl;
}
int _a = 1;
static int _c;
};
int A::_c = -1;
class B : public A
{
public:
int _b = 0;
};
int main()
{
B b;
A a;
a = b;
//编译器生成的为 A & A::operator = (const A &);
//b = a;报错,因此引用支持向下转换,不支持向上转换。
//且无法进行强制类型转换。
return 0;
}
为了理解作用域的性质,我列出一段代码便于理解:
class Person
{
public:
int func(int i)
{
cout << "Person::func" << endl;
}
int _num = 0;
};
class Student : public Person
{
public:
void func()
{
cout << "Student::func" << endl;
cout << _num << endl;
}
int _num = 1;
};
int main()
{
Student stu;
stu._num = -1;
stu.func();
//stu.func(1);
return 0;
}
我们首先要明白作用域是编译器查找的范围,而作用域包括局部域,全局域,命名空间域,类域,作用域限定符的作用是指定在某个域里面查找,否则就报错。
然后我们再来说明这里为啥不会报错,因为继承下来的东西,并不在一个类域里面,又因为不同类域是独立存在的,因此互相会产生屏蔽左右,那么我们一般称由命名相同产生屏蔽的现象,称为隐藏或者重定义。
明白这两个概念,我们再来分析第二个问题,首先对_num赋值-1,先在Studen的类域进行查找,如果找到就停止,很显然这里是对Student的_num进行赋值,然后调用func函数,同理,先对Student进行查找,如果找到就停止,这里找到了,调用的是Student里的func函数,最后查找_num,首先再当前局部域查找,如果没有就在当前类域进行查找,如果还没有就在基类的类域进行查找,如果还没有就在全局域进行查找,如果还找不到就报错。
很显然这里是到当前类域就找到了,因此是Student的类域里的_num。
有了隐藏/重定义的概念,这里的func显然是不构成重载的,因为重载要求在同一个类域!
最后将注释的代码放开,会产生编译报错的结果,因为编译器很懒,它找到就不再找了
,所以这里查找的还是Student的func。
class A
{
public:
A()
:_a(1)
{}
int _a;
};
class B : public A
{
public:
B()
:_b(0)
{}
int _b;
};
int main()
{
B a;
return 0;
}
调试 f11逐语句运行:
为啥要这样设计呢?
个人理解:总不能一个人干两份活吧?你干你的,我干我的,这样分工比较明确
,至于先后顺序,可能是因为子类的成员可能会用父类成员的一些值初始化
。
说明一点:如果子类的初始化列表,没有显示调用父类的构造函数,则调用默认构造函数,如果没有,则报错,这也说明了如果父类没有默认构造,要在子类显示的调用父类的构造函数。
举例:
class A
{
public:
A(int val)
:_a(val)
{}
int _a;
};
class B : public A
{
public:
B()
:_b(0)
,A(1)
{}
int _b;
};
int main()
{
B a;
return 0;
}
class Person
{
public:
Person(const char* name = "张三", int age = 18)
{
_name = name;
_age = age;
}
Person(const Person& per)
{
_name = per._name;
_age = per._age;
}
private:
string _name;
int _age;
};
class Student :public Person
{
public:
Student(const char* name = "张三", int age = 18,int id = 12345)
:Person(name,age)
,_id(id)
{
_id = id;
}
Student(const Student& stu)
:Person(stu)
{
_id = stu._id;
}
private:
int _id;
};
int main()
{
Student stu2("李四",19,8888);
Student stu1 = stu2;
return 0;
}
拷贝构造跟构造函数的区别不大,这里在实现过程中,尤其是子类的拷贝构造在显示地调用父类的构造函数时,会发生向下转换(引用),这里很关键!
另外强调一点,如果不显示调用拷贝构造,会调用默认构造函数,但这样可能不会完成拷贝的效果,如果没有默认构造会报错!
最后总结:构造函数的调用顺序先父后子。
class A
{
public:
~A()
{}
};
class B : public A
{
public:
~B()
{}
};
int main()
{
B b;
return 0;
}
思考一下为啥会这样?
这就跟构造函数有点关系了,我们构造的时候,提过先构造父类的成员变量,再构造子类的成员变量,这样是为了增加子类的信息灵活度,可以让子类的成员跟父类沾上边,如果沾上边的话,析构如果先析构父类,那子类跟父类沾上边的成员的数据就失效无法使用,如果在子类的析构中再进行使用,那么可能就会产生越界等危险行为,因此先析构子类的成员,再析构父类的成员就显得必要了。
class A
{
public:
A(int a = 0, int b = 0)
: _a(a)
, _b(b)
{}
void swap(const A& a)
{
_a = a._a;
_b = a._b;
}
A& operator = (A a)
{
swap(a);
return *this;
}
private:
int _a;
int _b;
};
class B : public A
{
public:
B(int a = 0, int b = 0, int c = 0, int d = 0)
:A(a,b)
,_c(c)
,_d(d)
{}
void swap(const B& b)
{
A::swap(b);
_c = b._c;
_d = b._d;
}
B& operator =(B b)
{
swap(b);
return *this;
}
int _c;
int _d;
};
int main()
{
B b1(1,2,3,4);
B b2(4,3,2,1);
b1 = b2;
return 0;
}
class A
{
public:
A(int a = 0, int b = 0)
:_a(a)
, _b(b)
{}
A& operator = (const A& a)
{
if (this != &a)
{
_a = a._a;
_a = a._b;
}
return *this;
}
private:
int _a;
int _b;
};
class B : public A
{
public:
B(int a = 0, int b = 0, int c = 0, int d = 0)
:A(a,b)
,_c(c)
,_d(d)
{}
B& operator =(const B& b)
{
if (this != &b)
{
//因为基类的私有成员,在派生类中不可见,所以我们需要调用基类的赋值进行拷贝。
A::operator =(b);
_c = b._c;
_d = b._d;
}
return *this;
}
int _c;
int _d;
};
C++11 标准不允许友元函数的声明有默认参数,除非友元声明是一个定义
个人理解:为了防止不合适参数,如还未被定义的类的匿名对象,可能直接报错。
class B;
class A
{
friend void func(const A& a, const B& b);
public:
private:
int _a = 1;
};
class B :public A
{
public:
private:
int _b = 2;
};
void func(const A& a, const B& b = B())
{
cout << a._a << endl;
cout << b._a << endl;
//cout << b._b << endl;
}
int main()
{
B b;
func(b,b);
return 0;
}
总结:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,但是子类中的基类的私有和保护成员可以通过基类的友元进行访问。
class Person
{
protected:
string _sex;
};
class Student : public Person
{
protected:
int _id;
};
class Teacher : public Person
{
protected:
string _profession;
};
class Assistant : public Student,public Teacher
{
protected:
int _age;
};
int main()
{
Assistant a;
a._sex = 1;
return 0;
}
说明:多继承从左往右进行继承。单继承先继承父类。也就是多继承从左往右开始,从上往下进行画对象模型,单继承从上往下,先画父类再画子类。
那如何解决这个问题呢?
如何实现呢?
class Person
{
public:
int _number = 10;
};
class Student : virtual public Person
{
public:
int _id = 6;
};
class Teacher :virtual public Person
{
protected:
int _telephone= 1;
};
class Assistant : public Student, public Teacher
{
public:
int _age = 18;
};
int main()
{
Assistant a;
return 0;
}
那其解决数据冗余和二义性的原理是什么呢?
第一步画出对象模型:
我们看出来,原来存Person的位置变了,取而代之的是类似与地址的数据。
有人就要问了,为啥要用一张表存偏移量,而不是直接在原来的位置放上Person 的地址,答案其实很简单,想要知道Person的地址,不也得进行计算吗? 况且如果每个实例化的类都进行计算,那必然是损耗效率的,但是实例化的类都有一个特点那就是 相对位置不会发生改变!因此我们只需计算一次,然后直接根据相对位置进行计算,实例化的类共用一份即可,这样也提升了效率。
还有一点,这样在向上转换时就不容易出错。
int main()
{
Assistant a;
Student stu;
Student& stu1 = a;
stu1._number = 1;
//基类的引用,访问父类的虚基表,得到偏移量1,从而访问Person。
stu._number = 2;
//基类的对象,访问基类的虚基表,得到偏移量2,从而访问Person。(偏移量1 != 偏移量2)
return 0;
}
说明:OOP——Object Oriented Programming(面向对象编程)
继承,更像是一种白盒测试
,我们从基类中继承的protect成员还能够进行使用,一旦基类的成员名一改,就会导致派生类的成员无法使用,这也就是耦合度提高的原因。组合,更像是一种黑盒测试
,我只用功能,你的底层细节我不关心,这样即使你的细节改了,对我没有影响,这降低了耦合度。组合更符合高内聚,低耦合的概念
。因此我们更提倡使用组合。至于继承应该具体场景下分析再进行使用,尤其是多继承和菱形虚拟继承!说明:
高内聚 ——一心只干一件事
。
低耦合 ——不同功能的关联程度很小
。
补充——组合与继承:
class A
{
protected:
int _a;
};
class B :public A
{
//继承
};
class C
{
class A;//组合
};
is_a关系,即花是植物
,而组合更像是has_a的关系,即车里面有轮胎
。 今天的分享就到这里了,如果觉得文章不错,点个赞鼓励一下吧!我们下篇文章再见
!