个人主页:Lei宝啊
愿所有美好如期而遇
目录
1,继承概念及定义
通俗点说,继承就是子类从父类继承下来成员变量和成员方法,但是不是拷贝这个变量和方法给子类一份,而是给了子类使用权,子类的继承方式决定了是否可以使用父类的成员变量的成员方法。
举个例子:
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{
cout << "Person()" << endl;
};
void print() {};
protected:
int _a;
public:
int _b;
};
class Student : public Person
{
public:
private:
int _b;
};
int main()
{
Student s;
return 0;
}
我们也就可以很清晰的看到成员变量和方法还是属于父类,只是我们通过公有继承可以使用罢了。
定义方式:
继承方式:public,protected,private
我们这里有个问题:继承方式的不同会导致成员变量和成员方法访问方式不同,是怎样的不同呢?
教材上都会给出像这样的一个表格:
我们可以这样理解 ,基类private成员不管以什么方式进行继承,都是不可见的,什么叫不可见?首先,private成员和方法我们还是继承了下来,只是不可以在子类显式使用,这样继承下来的成员,我们通过继承父类的公有方法,通过父类的构造函数,还是可以访问的,举个例子
#include <iostream>
using namespace std;
class Person
{
public:
Person(int d)
:_d(d)
{}
Person()
{
cout << "Person()" << endl;
};
void print()
{
cout << _d << endl;
};
private:
int _d;
};
class Student : public Person
{
public:
Student(int b, int d)
:_b(b)
,Person(d)
{}
private:
int _b;
};
int main()
{
Student s(2,3);
s.print();
return 0;
}
我们运行后得到结果是3。
而protected成员,public成员被子类继承下来后,访问权限就取决于继承方式和访问限定符,我们可以从表中看到,继承方式和访问限定符,哪个权限小,子类继承下来的成员和方法就是什么权限,举个例子:
- 父类public成员,子类通过protected继承,那么继承下来的成员访问权限就是protected。
- 父类protected成员,子类通过public继承,那么继承下来的成员访问权限就是protected。
这里我们补充一点:private成员子类继承下来,在子类中不能直接访问,那么我们想要在子类中直接访问,但是又不想被类外访问到,那么父类就定义protected成员,子类继承下来就是protected权限。
继承方式可以不写,但是class默认继承方式为private,这个我们基本上不用,所以还是得写出来继承方式,struct默认继承方式为public。
2,基类和派生类赋值兼容转换
赋值兼容转换,我们也叫做向上转型,什么意思呢?
就是说派生类对象/指针/引用可以赋值给基类的对象/指针/引用,我们这里不研究基类对象赋值给派生类对象,后面再说。
对于赋值兼容转换,我们也形象的叫做切片,也就是说,把子类对象中父类的那部分切割出来赋值给子类对象,举个例子,画个图:
int main()
{
Person p;
Student s;
//子类对象赋值给父类对象
p = s;
Person* p1;
Student* s1;
//子类对象指针赋值给父类对象指针
p1 = s1;
//引用
Person &p2 = s;
return 0;
}
子类对象赋值给父类对象,就是将继承下来的那部分赋值给父类对象,而子类对象指针赋值给父类对象指针,就是让父类对象指针指向子类中继承自父类的那一部分,引用也是同理。
这里我们就有一个值得思考的问题:类型转换,会产生临时变量,临时变量具有常性,而非const变量引用临时变量,权限放大,直接就报错了;不同类型指针,赋值需要强制类型转换,而我们上面没有。举个例子:
int main()
{
int a = 4;
double b = 3.14;
//double类型变量赋值给int类型变量
//中间产生临时变量,临时变量值为3
a = b;
cout << b;
//这里是强制类型转换
int c = 4;
char* d = (char*)&c;
//double类型转int类型,中间产生int型临时变量,临时变量具有常性
//所以我们引用也需要常引用,否则权限放大就会报错。
const int& c = a;
return 0;
}
那么凭什么 向上转型不用遵循这个规律呢?这就是比较特殊的地方,编译器对其做了特殊处理,在向上转型时,不会产生临时变量,直接就可以赋值。
3,继承中的作用域
继承中,子类和父类都是独立的作用域,当他们有同名函数存在时,父类的函数会被隐藏(重定义,只要函数名相同,不管参数是否相同,就构成隐藏),也就是说当我们调用这个同名函数时,调用的是子类的,父类的需要显式调用,举个例子:
#include <iostream>
using namespace std;
class Person
{
public:
int add(int a = 1, int b = 2)
{
return a + b;
}
};
class Student : public Person
{
public:
int add(int a = 4, int b = 3)
{
return a + b;
}
};
int main()
{
Person p;
Student s;
cout << p.add() << endl;
cout << s.add() << endl;
return 0;
}
那么我们怎么调用父类的add函数呢?
4,派生类的默认成员函数
派生类的六个默认成员函数在我们不写的时候会默认生成,但是同之前我们单个类的默认生成有些许不同的地方。
1.子类初始化子类的成员变量,父类初始化父类的成员变量,举个例子:
class Person
{
public:
Person(){}
Person(int a)
:_a(a)
{}
private:
int _a;
};
class Student : public Person
{
public:
Student() {}
Student(int b, int a)
:_b(b)
,Person(a)
{}
private:
int _b;
};
2.类的构造顺序,析构顺序
- 构造时,先父后子
- 析构时,先子后父
构造时,因为子类要继承父类,如果父类不先构造,子类无法继承父类的成员变量,因为成员变量的定义在初始化列表,而初始化列表在构造之前。
析构时,因为此时可能还会用到父类的成员变量,但是先把父类析构了,子类再想用就扯淡了,所以要先析构子类。
子类中不需要写父类的析构函数,编译器在子类析构后自动调用父类的析构函数。
注意:子类和父类的析构函数不管人为起的名字是否相同,最后编译器都会将他特殊处理成叫做destructor()的析构函数,也就是说,他们的析构函数构成隐藏,这点在后面的多态中会用到。
5,继承与友元
友元关系不可被继承。
class Student;
class Person
{
public:
friend void use(Person& p, Student& s);
Person(){}
Person(int a)
:_a(a)
{}
private:
int _a;
};
class Student : public Person
{
public:
Student() {}
Student(int b, int a)
:_b(b)
,Person(a)
{}
private:
int _b;
};
void use(Person &p, Student& s)
{
cout << p._a << endl;
}
上面的代码现在没有问题,我们再加上一句。
也就是说,友元不可被继承,如果仍然想要访问子类的成员变量,那么就成为子类的朋友吧:
6,继承与静态成员
基类定义了static成员,则整个继承体系里只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例,举个例子:
#include <iostream>
using namespace std;
class A
{
public:
A()
{
count++;
}
void print()
{
cout << "A: count = " << count << endl;
}
protected:
static int count;
};
int A::count = 0;
class B : public A
{
public:
B()
{
count++;
}
};
class C : public B
{
public:
C()
{
count++;
}
};
int main()
{
A a;
B b;
C c;
a.print();
return 0;
}
结果为6
7,复杂的菱形继承及菱形虚拟继承
我们就可以发现一个问题,assistant对象中,Person成员会有两份,这样会有数据冗余和二义性的问题,举个例子:
我们先故意写一个菱形继承:
#include <iostream>
using namespace std;
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; // 主修课程
};
虚拟继承可以解决上面的数据冗余和二义性问题。我们在继承Person的派生类的继承方式前加上virtual,谁是导致菱形继承的根源,那么他的派生类继承时就虚继承。
这一次没有报错。这次我们再举个例子:
我们发现修改了Teacher类里的_name, Student类里的_name也修改了,那么接下来我们来研究一下虚继承的原理。
从最开始具有冗余数据和二义性问题的菱形继承来看:
#include <iostream>
using namespace std;
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//class C : virtual 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;
}
我们查看一下内存中d是如何存储的:
我们就可以看到数据冗余了,我们来看菱形虚拟继承。
我们现在可以看到,B和C虚继承后,之前存A的地方现在不再存A,而是存储了一个地址,这个地址叫做虚基表指针,这个指针指向的空间,还存储了一个值,这个值就是偏移量,B,C相对于A地址的偏移量,他们将根据这个偏移量找到A;这两个类共享A,所以_a = 1被覆盖成2。