一、继承的概念
面向对象的三大特性:封装,继承,多态
继承是面向对象程序设计使得代码可以服用的最重要的手段,允许程序员在保持原有类特性的基础上进行扩展,增加功能,产生派生类
被继承的对象叫做父类(基类),继承的对象叫做子类(派生类)
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
void show()
{
cout << "name:" <<_name<< endl;
cout << "age:" << _age << endl;
}
private:
string _name="rxy";
int _age=20;
};
class Student :public Person//公有方式继承
{
public:
int _stud_NUM;
};
class Teacher :public Person
{
public:
int _teach_NUM;
};
int main()
{
Student s1;
s1.show();
Teacher t1;
t1.show();
return 0;
}
继承定义
继承方式分为:公共继承,保护继承,私有继承。与类内的访问权限对应
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的proteched成员 | 派生类的private成员 |
基类的private成员 | 派生类不可见 | 派生类不可见 | 派生类不可见 |
不同继承方式会导致子类对于父类属性的访问权限变化如图所示
注意:
①继承中父类所有的属性都会被子类继承下去,只是父类的私有属性被编译器隐藏,无法访问,但是事实存在
②如果基类成员不想在类外被直接访问,并且允许在派生类中访问,那么就定义为protected
③使用关键字class时默认的访问限定符为private,使用关键字struct时默认的访问限定符为public
④实际上一般都是用public继承,几乎很少使用protect与private继承。也不提倡使用protect与private继承,因为继承下来的成员都只能在派生类类内使用,实际中的维护扩展性不强
二、基类和派生类对象赋值转换
派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。也就是切除子类不同的属性,保留与父类相同的属性赋值
也就是父类中的_name和_age属性与子类中_name和_age属性相同,但是没有_stu_NUM属性
基类对象不能赋值给派生类对象
void test_1()
{
int a = 1;
double b = a;//会有隐式类型转换 int->double
Student s1;
Person p1 = s1;//并没有隐式类型转换,而是切割
//将子类s1与父类属性的相同部分赋值给p1
Person& p2 = s1;//p2为s1中与父类相同属性的引用
Person* p3 = &s1;//p3保存s1中与父类相同属性的地址
}
三、继承中的作用域
基类和派生类的作用域是相互独立的
①如果基类和派生类有相同的属性名称,那么派生类会屏蔽父类同名属性,这种情况叫隐藏,也叫重定义。但是仍然可以通过指定作用域的方式调用
②如果是成员函数的隐藏,只需要函数名相同就构成隐藏(函数重载必须在同一个作用域)
当子类与父类出现同名成员时
访问子类同名成员,直接访问即可 访问父类中的同名成员,加作用域
四、派生类中的默认成员函数
派生类的默认成员函数包括构造函数,析构函数,拷贝构造函数,赋值重载,取地址重载
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
3. 派生类的operator=必须要调用基类的operator=完成基类的复制
4.派生类对象的初始化先调用基类的构造函数,然后调用自己的构造函数
5.派生类对象先调用自己的析构函数再调用基类的析构函数
对于先后生成的两个对象,一定是后定义的先析构。所以对于一个子类对象来讲,先进行的是基类的构造函数,因此后执行基类的析构函数。但不允许显示调用析构函数,因为需要保证释放顺序
#include<iostream>
using namespace std;
class Person
{
public:
Person(const char* name = "rxy-p")
: _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)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
//operator=(s);//会产生隐藏,调用Student的operator
_num = s._num;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
~Student()
{
//~Person();
//不能手动直接调用基类析构函数是因为,为了适应多态,析构函数会被处理成destructor
//基类与派生类的析构同时被处理成destructor,就会产生隐藏
//Person::~Person();允许这样调用,但不允许显式调用,因为需要保证释放顺序
cout << "~Student()" << endl;
}
// 子类析构函数完成时,会自定调用父类析构函数,保证先析构子再析构父
protected:
int _num; //学号
};
int main()
{
Student s1("rxy",18);
Student s2(s1);
Person p1 = s1;
s1 = s2;
return 0;
}
五、继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
#include<iostream>
#include<string>
using namespace std;
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
protected:
int _num; //学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;//是友元则可以访问
//cout << s._num << endl;//不是友元则不可以访问
}
六、继承与静态成员
静态成员只有一个。如果父类定义了一个static对象,那么不论有多少个派生类都只有一个static属性,也就是整个继承体系里只有一个static,并且它及属于父类也属于子类
#include<iostream>
using namespace std;
class Person
{
public:
Person() { ++_count; }
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;//静态成员类外初始化
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Teacher : public Student
{
protected:
string _class; // 研究科目
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Teacher s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
}
int main()
{
TestPerson();
Person p;
Student s;
cout << &(p._name) << endl;
cout << &(s._name) << endl;
cout << &(p._count) << endl;
cout << &(s._count) << endl;
return 0;
}
可以看出对于不同对象的static属性地址相同,也就是共享静态成员
七、菱形继承
继承分为单继承和多继承
单继承:一个子类只具备一个父类的特征
多继承:一个子类具备多个父类的特征
菱形继承,也叫做钻石继承:①两个派生类(子类)继承同一个基类②某个类同时继承两个派生类
骡是马与驴的杂交种,骡马驴都属于动物,但是骡又同时继承了属于马与骡的特性。马继承了动物的特性,驴继承了动物的特性,当骡使用数据时就会产生二义性
当使用骡的动物属性时,同时具有马的动物属性与驴的动物属性,然而实际上只需要一份骡的动物属性即可
#include<iostream>
using namespace std;
class Animal
{
protected:
string _name;
};
class horse:public Animal
{
protected:
int _num_h = 64;
};
class donkey:public Animal
{
protected:
int _num_d = 62;
};
class mule :public horse, public donkey
{
private:
int _num_m = 63;
};
void test_1()
{
mule m1;
//m1._name = "mule";
//报错“不明确”,也就是同时有horse的_name和donkey的_name
//也就是数据冗余与二义性
}
int main()
{
test_1();
return 0;
}
对于多继承产生的二义性问题,可以采用虚继承的方式解决
二义性也就是对于同一个对象继承了重复的属性,采用虚继承就是令继承的属性指向同一个
#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::_a与d.C::_a实际上是通过指针实现的,编译器将其转换为d.ptrB->_a和d.ptrC->_a来读取数据,ptrB与ptrC中存放的都是地址,这两个地址指向的空间存储一个数据。数据被称为偏移量值,这两个指针被称为偏移量地址。编译器会单独存储_a的数值,并计算出_a地址与BC地址的差值,这个插值就是偏移量值
一般情况下不要写菱形继承,实际应用场景中还有多态等,十分复杂。并且有效率损失