继承的概念
class Member
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "XiaoMing";//姓名
int _age = 18;//年龄
};
子类/派生类
class Teacher : public Member
{
protected:
int _jobid;//工号
};
子类/派生类
class Student : public Member
{
protected:
int _stuid; // 学号
};
继承的定义
定义格式
基类与普通类的格式一致。
派生类与普通类稍有差别,主要是类名后面加上了继承方式和父类,具体定义格式如下。
继承关系与访问限定符
对于派生类能访问基类的哪些成员,实际上需要上面的继承方式与访问限定符的组合,3×3=9种可能的情况。
这里给出一个顺序:public > protected > private
对于派生类中能否访问基类成员,我们给出这样一条规律:取子类继承方式和父类访问限定符较小的那一个作为访问方式。
注意:所有的成员都可以被继承,但是继承后的访问关系是有规则的。
例子:
理解:
- 对于基类的private成员,我们在派生类中无论以什么方式继承都是不可见的(可以被继承但是不能被访问)。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出protected成员限定符是因继承才出现的。
总结:
在实际应用中,用的最多的是基类访问限定符是public,protected,派生类的继承方式是protected方式。
基类和派生类对象赋值转换
Student s;
Member m1 = s;
Member* m2 = &s;
Member& m3 = s;
它的原理是这样的。
int i=10;
double d = i;//产生了临时拷贝,进行类型转换
Student s;
Member m=s;//没有产生临时拷贝
//通过切片的方式把s对象中含有的基类成员变量直接赋值给基类对象m
继承中的作用域
class Member
{
protected:
string _name = "XiaoMing";//姓名
int _age = 18;//年龄
int _id = 123;
};
class Student : public Member
{
public:
void Print()
{
cout << "_id:" << _id << endl;
cout << "Member::_id:" << Member::_id << endl;//通过指定类域访问基类变量
}
protected:
int _id = 1111; // 学号
};
int main()
{
Student s;
s.Print();
return 0;
}
![](https://i-blog.csdnimg.cn/blog_migrate/417d1ff4301e11e529866a78395383b0.png)
派生类的默认成员函数
构造函数
默认的构造函数
派生类的成员:1.内置类型不做处理 2.自定义类型去调他的构造函数(和普通的类一样!)。
派生类中的基类成员:去调基类的构造函数(与普通类的区别!)。
例子:
class Member
{
protected:
string _name;//姓名
int _age;//年龄
};
class Student : public Member
{
public:
private:
int _id;
};
int main()
{
Student s;
return 0;
}
构造Student对象,对于他自己(派生类)的成员,调用其默认的构造函数,即1.内置类型不做处理(int类型的_id初始化为随机值) 2.自定义类型去调他的构造函数(此处没有)
对于基类的成员(string类型的_name和int类型的_age),去调其基类的构造函数(此处父类仍是默认构造函数)。
显示的构造函数
在初始化基类成员时不能在派生类中单独初始化,需要在父类构造函数中初始化。
显示调用初始化函数
class Member
{
public:
Member(const char* name = "XiaoMing", int age = 10)
:_name(name)
,_age(age)
{
cout << "Member()" << endl;
}
protected:
string _name;//姓名
int _age;//年龄
};
class Student : public Member
{
public:
Student(const char* name, int age ,int id = 10)
:Member(name, age)
,_id(id)
{
cout << "Student()" << endl;
}
private:
int _id;
};
int main()
{
Student s("XiaoFang",10,100);
return 0;
}
进行对象实例化,结果为
无论初始化列表顺序如何改变,构造函数构造顺序都是先父后子。
理解:如果子类初始化时需要父类继承的成员变量时,此时若父类成员还未初始化,则子类初始化出现错误,所以构造顺序一定是先父类后子类。
构造函数构造的顺序:先父后子。
析构函数
class Member
{
public:
Member(const char* name = "XiaoMing", int age = 10)
:_name(name)
,_age(age)
{
cout << "Member()" << endl;
}
~Member()
{
cout << "~Member()" << endl;
};
protected:
string _name;//姓名
int _age;//年龄
};
class Student : public Member
{
public:
Student(const char* name, int age ,int id = 10)
:Member(name, age)
,_id(id)
{
cout << "Student()" << endl;
}
~Student()
{
//~Member();错误写法,原因如下
//子类的析构函数和父类的析构函数构成隐藏关系
//由于多态原因,析构函数被特殊处理,函数名都被处理成destructor(),同名函数形成隐藏
Member::~Member();//要指定类域;
cout << "~Student()" << endl;
};
private:
int _id;
};
int main()
{
Student s("XiaoFang",10,100);
return 0;
}
我们创建一个对象,观察一下
此处我们发现Member被析构两次,原因是父类析构函数会在子类析构后自动调用。
我们在~Student()中显示调用~Member(),在子类自己析构后又自动调用一次父类的析构函数,从而导致析构两次,所以这种析构函数的写法是错误的。
总结:对于父类的析构函数不用再子类的析构函数中显示调用,子类析构函数结束后父类析构函数自动调用!
继承体系中正确析构函数写法:
class Member
{
public:
Member(const char* name = "XiaoMing", int age = 10)
:_name(name)
,_age(age)
{
cout << "Member()" << endl;
}
~Member()
{
cout << "~Member()" << endl;
};
protected:
string _name;//姓名
int _age;//年龄
};
class Student : public Member
{
public:
Student(const char* name, int age ,int id = 10)
:Member(name, age)
,_id(id)
{
cout << "Student()" << endl;
}
~Student()
{
cout << "~Student()" << endl;
};
private:
int _id;
};
int main()
{
Student s("XiaoFang",10,100);
return 0;
}
观察结果我们发现析构顺序与构造顺序相反,析构顺序是先子类后父类。
理解:假设析构先父后子,会存在安全隐患,可能父类成员的资源已经清理,派生类再去访问可能会找不到,或者出现野指针等问题。
析构函数析构的顺序:先子后父
拷贝构造函数
利用切片原则,先将子类赋值给父类,然后再单独赋值子类剩下的函数。
例子:
class Member
{
public:
Member(const char* name = "XiaoMing", int age = 10)
:_name(name)
,_age(age)
{}
~Member()
{
cout << "~Member()" << endl;
};
protected:
string _name;
int _age;
};
class Student : public Member
{
public:
Student(const char* name, int age ,int id = 10)
:Member(name, age)
,_id(id)
{
}
//----------------------------------------------------------------------------
Student(const Student& s)
:Member(s)//利用赋值转换的原则(切片原则)将派生类成员切片给基类进行赋值
,_id(s._id)
{}
//----------------------------------------------------------------------------
~Student()
{
cout << "~Student()" << endl;
};
private:
int _id;
};
int main()
{
Student s("XiaoFang",10,100);
Student s1(s);
return 0;
}
赋值重载函数
class Member
{
public:
Member(const char* name = "XiaoMing", int age = 10)
:_name(name)
,_age(age)
{}
//-----------------------------------------------------------
Member& operator=(const Member& p)
{
if (this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}
//-----------------------------------------------------------
~Member()
{
cout << "~Member()" << endl;
};
protected:
string _name;
int _age;
};
class Student : public Member
{
public:
Student(const char* name, int age ,int id)
:Member(name, age)
,_id(id)
{
}
Student(const Student& s)
:Member(s)//利用赋值转换的原则(切片原则)将派生类成员切片给基类进行赋值
,_id(s._id)
{}
//----------------------------------------------------------
Student& operator=(const Student& s)
{
if (this != &s)
{
Member::operator=(s);
_id = s._id;
}
return *this;
}
//----------------------------------------------------------
~Student()
{
cout << "~Student()" << endl;
};
private:
int _id = 10;
};
int main()
{
Student s("XiaoFang",10,100);
Student s1 = s;
return 0;
}
总结:派生类的默认成员函数规则跟普通类的规则一致,唯一不同的是,不管是构造/析构/拷贝,多的是基类那一部分,基类部分调用基类那一部分对应函数去完成
继承与友元
友元关系不能继承,基类友元不能访问子类私有和保护成员。(爸爸的朋友不是孩子的朋友)
class Student;
class Member
{
public:
//friend void print(const Member& m, const Student& s);
//友元不能继承,无法获取子类的成员
protected:
string _name;
int _age;
};
class Student:public Member
{
public:
friend void print(const Member& m, const Student& s);
private:
int _id;
};
void print(const Member& m,const Student& s)
{
cout << m._name << endl;
cout << s._id << endl;
}
int main()
{
print();
return 0;
}
继承与静态成员
基类定义了static静态成员,则整个继承体系中只有一个这样的成员,子类只能继承访问权。(子类中不会再产生一份拷贝,原因是static静态成员不是在对象中的,是在静态区的)
菱形继承
单继承:一个子类只有一个直接父类。
多继承:一个子类有两个或两个以上的直接父类。
菱形继承:在多继承的基础上,被同一个类继承的两个类,他们又继承自同一对象。
有了多继承就可能出现菱形继承,菱形继承会产生二义性,空间浪费。
产生的问题:Student类和Teacher类继承Member类的成员,具有相同的成员(作用域不同),Assistant类又继承Student类和Teacher类,这样Assistant类就有两个相同的成员(但是作用域不同,所以可以通过不同的作用域访问这另个成员),造成了变量二义性,数据浪费。
为了解决这个问题,引入关键字virtual,加在产生同一个成员变量的类中,这个时候B对象实例化时没有保存A类成员,而是通过一个机制指向A类成员
例子:
深入探究一下virtual的底层原理
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 dd;
dd.B::_a = 1;
dd.C::_a = 2;
dd._b = 3;
dd._c = 3;
dd._d = 3;
return 0;
}
该代码描述的是这样的场景:
我们观察其内存地址:
观察发现其确实多储存了两份_a(B::_a与C::_a)。
在B类与C类中加入virtual关键字。
我们观察其内存地址:
在两个蓝色框中我们发现下一行分别存储B类_b和C类_c的值,而上一行储存的是地址,由于我的是小端机,则_b存储的地址是0x001c7bdc,_c存储的地址是0x001c7be4 ,找到这两个地址,我们发现它们下一个地址存储了一个十六进制的值,这个值是偏移量。
再将这个偏移量加回原来存储地址的地址,我们发现它们的结果都指向了A类的地址。
B类和C类都通过偏移量表去找到公共的A类。这个时候B或C对象实例化时没有保存A类成员,而是通过一个机制指向A类成员。
都指向A有什么用?
//例
B* ph = ⅆ
ph->_a++;
//例
B bb;
bb._a=10;
这样在B对象通过偏移量表就可以访问A的成员。