文章目录
1.继承的概念及定义
1.1继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特 性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
class Person
{
public:
int _a;
void f()
{
cout << "Person" << endl;
}
};
class Student:public Person
{
public:
int _b;
};
int main()
{
Person a;
Student b;
a.f();
b.f();
}
这里看到对象a和对象b打印的结果都是Person。
1.2访问限定符
我们可以看到这里的基类的访问限定符和继承方式都是有3种的,那么就会造成9种情况。
class Person
{
//在父类中protect和private是没有区别的
//但在子类中protect是可以给子类用的,但是private就不可以给子类用
//但protect在类外就不可以用了
protected:
string _name; // 姓名
private:
int _age; // 年龄
public:
int _no;
void Print()
{
cout << _name << endl;
cout << _age << endl;
cout << _no << endl;
}
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
public:
void func9()
{
cout << _name << endl;
cout << _age<< endl;//这句是不行的
cout << _no << endl;
}
protected:
int _stunum; // 学号
};
int main()
{
Person p;
p._name;//不行的
Student s;
s._no;
return 0;
}
那么我们可以整理一下,如果在父类中,访问限定符protect和private是一样的,
1.在子类就不一样了,protect是可以用的,private是不可以用的。
2.在类外,protect和private是一样的不可以用。
如果继承方式改变了,那么还是一样的按照public>protect>private这个规律来看就行了。
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是 被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能 访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类 的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的 写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用 protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中 扩展维护性不强
注意:如果派生类是私有继承,但它的成员变量是公有的,那么在类外还是可以访问派生类的成员变量。
2.基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切 割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象 。
基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才 是安全的。
class Person
{
public:
int _a;
int _c;
void f()
{
cout << "Person" << endl;
}
};
class Student:public Person
{
public:
int _b;
};
int main()
{
Person a;
Student b;
// 1.子类对象可以赋值给父类对象/指针/引用
//父类=子类
a = b;
Person *pstr = &b;
pstr->f();
Person &ref = b;
//2.基类对象不能赋值给派生类对象
b = a;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
//子类=父类
//子类=父类
Student *ppstr = (Student*)&a;
ppstr->f();
ppstr->_b=1;//这里就会越界
}
父类=子类这个是属于是切片和切割,这里天然的行为。
3.继承中的作用域
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
class A
{
public:
void f()
{
cout << "f()" << endl;
}
};
class B:public A
{
public:
void f(int i=0)
{
cout << "f(int i)" << endl;
}
};
int main()
{
B b;
b.f();
b.A::f();
}
4.派生类的默认成员函数
4.1问题引入
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个.
1、那么在派生类的重点的四个默认成员函数,我们不写,编译器会默认生成的会干些什么事情呢?
我们不写默认生成的派生的构造和析构?
a、父类继承下来得 (调用父类默认构造和析构处理)
b、自己的(内置类型和自定义类型成员)(跟普通类一样)
我们不写默认生成的拷贝构造和operator=?
a、父类继承下来得 (调用父类拷贝构造和operator=)
b、自己的(内置类型和自定义类型成员)(跟普通类一样)
总结:原则,继承下来调用父类处理,自己的按普通类基本规则。
2、那么什么情况需要自己写呢?
a、父类没有默认构造,需要我们自己显示写构造
b、如果子类有资源需要释放,就需要自己显示写析构
c、如果子类存在浅拷贝问题,就需要自己实现拷贝构造和赋值解决浅拷贝问题
3、如果我们要写,要做些什么事情呢?
a、父类成员调用父类的对应构造、拷贝构造、operator = 和析构处理
b、自己成员按普通类处理。
4.2构造函数
当我们写Student s,这个对象的时候,它里面是包含父类的数据的,那么我们写构造函数的时候,应该怎么写呢?我们知道如果我们对于父类的部分可以调用父类的构造函数,子类就可以自己进行处理。
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
protected:
string _name; // 姓名
//int* _ptr = new int[10];
};
class Student :public Person
{
public:
Student(const char* name="张三",int nums=0)
: Person(name)
, _nums(nums)
{
}
protected:
int _nums;
//int *_ptr = new int[10];
};
这里很简单,只需要在初始化列表的时候调用父类的构造函数就行了。
注意:构造函数是必须要写的,其他默认的成员函数可以用编译器自带的。
4.3析构函数和运算符重载
class Person
{
public:
Person(const char* name = "peter")
: _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;
}
protected:
string _name; // 姓名
//int* _ptr = new int[10];
};
class Student :public Person
{
public:
Student(const char* name="张三",int nums=0)
: Person(name)
, _nums(nums)
{
}
//拷贝构造
Student(const Student& p)
:Person(p)
, _nums(p._nums)
{
}
//赋值运算符
Student& operator =(const Student& q)
{
if (this != &q)
{
Person::operator=(q);
_nums = q._nums;
}
}
protected:
int _nums;
//int *_ptr = new int[10];
};
我们可以发现这里子类写的这两个函数,调用父类的时候是出现前面我们学到的切片和切割的
4.4析构函数
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
//int* _ptr = new int[10];
};
class Student :public Person
{
public:
Student(const char* name="张三",int nums=0)
: Person(name)
, _nums(nums)
{
}
~Student()
{
//~Person();这样是错的
//Person::~Person();
}
// 子类析构函数结束时,会自动调用父类的析构函数
// 所以我们自己实现子类析构函数时,不需要显示调用父类析构函数
// 这样才能保证先析构子类成员,再析构父类成员
protected:
int _nums;
//int *_ptr = new int[10];
};
我们可以看到子类写的析构函数什么都没有写,这里没有调用.需要说明的一点是我们如果调用父类的析构函数,而且没有指定的去调用,就会出错。因为子类和父类的析构函数最后都会统一的处理成destructor(),就会构成隐藏,如果没有指定类域,那么就会出现无限调用的场景,就会出现栈溢出。
~Student()
{
Person::~Person();
}
如果析构函数这样写,那就会出现调用两次析构函数的情况。那是因为子类析构函数结束时,会自动调用父类的析构函数, 所以我们自己实现子类析构函数时,不需要显示调用父类析构函数,这样才能保证先析构子类成员,再析构父类成员。
5.友元与静态成员
5.1友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
//friend void Display(const Person& p, const Student& s);加了这句才能访问子类
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
}
5.2静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一 个static成员实例 。
6.复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
6.1复杂的菱形继承
菱形继承:菱形继承是多继承的一种特殊情况。
6.1.1菱形继承的问题
那么这样就会出现一些问题:在这里定义的类province会出现数据冗余和二义性的问题。
数据冗余:是指的类province会出现两份village成员。
二义性:是指的每次调用不知道调用哪一个,需要指定类域。
class village
{
public:
string _name1;//村
};
class town :public village
{
public:
string _name2;//乡镇
};
class county :public village
{
public:
string _name3;//县
};
class province :public town, public county
{
public:
string _name4;//省
};
province s;
我们通过调试我们发现,这里的s存在两份_name1.
s._name1 = "huaxi";
如果这样写就是会报错的,因为编译器不知道该调用哪一个_name1 。
s.town::_name1 = "xxxx";
s.county::_name1 = "yyyyy";
如果存在菱形继承,那么就要指定作用域才能使用。
6.1.2虚继承解决菱形继承
虚继承可以解决菱形继承的 二义性和数据冗余的问题。如上的继承关系就可以用虚继承解决。
class village
{
public:
string _name1;//村
};
class town :virtual public village//在多继承的腰部进行虚继承
{
public:
string _name2;//乡镇
};
class county :virtual public village//在多继承的腰部进行虚继承
{
public:
string _name3;//县
};
class province :public town, public county
{
public:
string _name4;//省
};
province s;
s._name1 = "huaxi";
注意这里的虚继承是在多继承的腰部
那么很多人就会问这里的存的huaxi在那个类里的呢?那么这就是我们要重点学习的内容。虚继承的原理
6.1.3虚继承的原理
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;
};
我们通过来看,通过内存来看一看对象成员的模型。
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
我们发现这里先继承就在内存的前面,这里的_a是在两个类域的。而且我们发现这两个类的内存是在一片连续空间中的。
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
这里我们能看到如果用了虚函数,就会出现类B和类C继承的_a都想存在了一片空间。
我们已经知道了这些继承的类存放的数据是放在一片连续的空间的,那么另外两行是怎么回事呢?这个就牵扯到一个更深层次的东西了,虚基表。
虚基表
我们可以看到这一串显然就是地址了。那么我们就可以用编译器看看这一片地址存的是什么东西。
我们看到是有两串数字。00 00 00 00这个的意义我们以后会在多态学习的,那么我们就注意下一个14 00 00 00这里是十六进制的,所以就表示20,这里的20就是类B与类A地址差20。
所以我们知道,在虚继承中,B类对象和C类对象的内存中加入的是一个地址,分别用于寻找两者与A类对象的偏移量。所以这样称为虚基表。
比如,当我们创建B类和C类建立两个变量:
B b=d;
C c=d;
此时会出现切片处理,需要将d中的类A对象赋值到b和c中,此时就需要使用虚基表来寻找。
7.继承与组合
7.1两者的区别
继承就是子类继承父类。
组合表示的是在一个类中定义了另一个类的成员变量。
//继承
class A
{
public:
int _a;
};
class B:public A
{
public:
int _b;
};
//组合
class C
{
public:
int _c;
};
class D
{
public:
int _d;
C _ans;
};
7.2继承和组合的区别
(2)继承与组合的区别
我们需要明确一点:类之间,模块之间最好是低耦合,高内聚的,因为方便维护。
低耦合:类之间依赖关系越弱越好。
高内聚:内部成员关系紧密。
1.继承对应于白盒:B可以直接使用A中的公有和保护成员,破坏了封装性。
2.组合对应于黑盒:D只能使用C的公有,不能直接使用保护成员。
举一个例子:
如果A中有5个public,5个protected
对于组合来说,非基类只能使用这5个public,基类中的其他成员随便修改都不会影响该非基类。
对于继承来说,基类中一切的改变都会影响子类。
那可以抛弃继承的语法吗?当然是不行的。
多态是建立在继承的基础上的。
7.3使用场景
1.如果B就是一个A,比如Student是一个Person,我们称这种关系为is-a关系,此时适合使用继承。
2.如果D被包含于C,比如head包含eyes,我们称这种关系为has-a关系,此时适合使用组合。
3.当遇到特殊情况,is-a和has-a都可以讲通时,优先使用组合。