C++知识点----继承
更多C++知识:c++目录索引
1. 继承
1.1 继承概念
继承是面向对象复用的重要手段。通过继承定义一个类,继承是类型之间的关系建模,共享共有的东西,实现自己本质的东西。
1. 2 访问限定符 && 继承关系
private(私有):从字面上来看,私有属于个人的,不会让其他人使用;那么在父类里,成员为私有限定,意味着不管是什么继承,这个成员就是不让你看,即使你是从我这里继承过去的
protect(保护):父类成员受保护,说明访问还是可以让你访问的,但只能是子类在自己的内部访问,不可以在类外面通过类对象进行访问,不管是基类的对象还是子类的对象
三种继承关系
继承方式 | 基类public成员 | 基类protect成员 | 基类private成员 | 继承引起的访问限制关系 |
---|---|---|---|---|
public继承 | 仍为public成员 | 仍为protect成员 | 不可见 | 基类的非私有成员在子类的访问属性都不变 |
protect继承 | 变为protect成员 | 仍为protect成员 | 不可见 | 基类的非私有成员都成为子类的保护成员 |
private继承 | 变为private成员 | 变为private成员 | 不可见 | 基类的非私有成员都成为子类的私有成员 |
例子:
class father
{
public:
char* _fname;//名字都可以知道
protected:
char* _fIDCard;//银行卡号要受保护的,只有我和我的继承者可以知道
private:
char* _fpassward;//银行卡密码只有自己知道
};
class son : public father
{
public:
char* _sname;
protected:
char* _sIDCard;
private:
char* _spassward;
public:
void func()
{
_sIDCard = _fIDCard;//在子类的内部可以访问父类的保护成员
}
};
void Test()
{
son s;
s._fIDCard = "123456";//不可通过子类对象访问父类的保护成员
}
结果:
几点总结:
基类中的private成员,不管通过何种方式继承,在其子类中均不能被访问。
某个成员不想被基类对象直接访问,但要在子类中能被访问,就定义成protected成员。
public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
protetced/private继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。
不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,但是基类的私有成员存在但是在子类中不可见(不能访问)。
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承。
注:
is-a原则: 全称为is-a-kind-of,显然这里is-a原则指的是子类是父类的一种;例如:长方形是基类,正方形是子类,子类和基类构成继承关系;满足is-a 原则;正方形是长方形的一种
has-a 原则:代表的是对象和它的成员的从属关系。同一种类的对象,通过它们的属性的不同值来区别,是有一个的原则;
例如:下面ID,Name,telephon 和student 是has-a的关系;学生有一个学号,有一个名字,有一个电话
class ID{.....};
class Name{......};
class telephon{.....};
class student{//学生类里面包含了ID,Name,tele
puvlic:
private:
ID _ID;
Name _name;
telephon _tel;
};
注:关于is-a更多解释可阅读Effective C++ 条款32;关于has-a更多解释可阅读Effective C++ 条款38
2. 继承与转换—-赋值兼容规则(public继承)
子类对象可以赋值给父类对象
父类对象不能复制给子类对象
父类的指针/引用可以指向子类对象
子类的指针/引用不能指向父类对象(可以通过强制类型转换完成,但存在隐患)
class Father
{
public:
int _fid;
int _ftel;
};
class Son:public Father
{
public:
int _sid;
};
void Test()
{
Son s;
Father f;
f = s;//子类可以赋给父类(通过切片)
s = f;//父类不可赋给子类
//父类的引用可以指向子类
Father& f1 = f;
f1 = s;
//父类的指针可以指向子类
Father* f2 = &f;
f2 = &s;
//子类的引用不能指向父类(可以通过强转完成)
Son& s1 = s;
s = (Son&)f;
//子类的指针不能指向父类(可以通过强转完成)
Son* s2 = &s;
s2 = (Son*)&f;
}
个人理解:
关于子类可以赋给父类:
子类是从父类继承的,故而父类必定包含子类的部分成员,在赋值时,父类只取自己的那部分成员,并不接受子类的成员,从而发生切片,丢弃了子类的成员;
父类不可赋值给子类:
父类给子类赋值时,子类并不知道父类有多少成员,部分成员继承过来,但私有成员不可见,故而赋值时子类没有更多的空间来存放父类的私有成员;如果说要进行切片,那么要哪些成员,不要那些成员,并不知道;而且你既然是从父类继承的,有什么权利说丢弃父类的成员
3. 成员函数的重载、 覆盖和隐藏
3.1 重载
在同一个类的作用域里,成员函数的名字相同,参数不同,两个函数构成重载
参数不同可以是顺序不同,数目不同(包括const参数和非const参数)
virtual 关键字可有可无
3.2 覆盖
覆盖指派生类重新实现(或改写)了基类的成员函数
发生覆盖的 两个成员函数分别在两个不同的类域,分别为基类和派生类
发生覆盖的两个成员函数的函数名相同,参数完全相同(参数顺序,参数数目)
基类的成员函数必须为虚函数(virtual 关键字修饰)
3.3 隐藏
隐藏指派生类的成员函数遮蔽了与其函数名相同的基类成员函数,具体规则如下:
派生类的函数名和基类的函数名相同,参数不同;基类的成员函数被隐藏(注意不要和重载混淆)
派生类的函数名和基类的函数名相同,参数完全相同,但基类成员函数没有virtual关键字;基类的成员函数被隐藏(注意不要和覆盖混淆)
实例1:
class Father
{
public:
void func(int x)
{
cout << "Father:func" << endl;
}
void func1(int x, int y)
{
cout << "Father:func1" << endl;
}
virtual void func2(int x)
{
cout << "Father:func2" << endl;
}
};
class Son :public Father
{
public:
void func(float)
{
cout << "Son: func1" << endl;
}
void func1(int x, int y)
{
cout << "Son: func1" << endl;
}
virtual void func2(int x)
{
cout << "Son: func2" << endl;
}
};
//Father::func 和 Father::func1 构成重载
//Son::func 隐藏了Fther::func,不是重载,作用域不同
//Son::func1 隐藏了Father::func1,不是覆盖,基类func1不是虚函数
//Son::func2 覆盖了Father::func2
实例2:
class AA
{
public:
void f()
{
cout<<"AA::f()"<<endl;
}
};
class BB : public AA
{
public:
void f(int a)
{
cout<<"BB::f()"<<endl;
}
};
int main()
{
AA aa;
BB bb;
aa.f();
bb.f();//想调基类的f函数,但基类AA的f函数被隐藏,故而bb调f时调子类的f,此时子类f带参,报错
//bb.AA::f(); 可注明作用域调f函数
system("pause");
return 0;
}
总结:
在继承体系中基类和派生类都有独立的作用域。
子类和父类中有同名成员,子类成员将屏蔽父类对成员的直接访问(在子类成员函数中,可以使用 基类::基类成员 访问)
注意在实际中在继承体系里面最好不要定义同名的成员。
关于重载、覆盖和隐藏的更多解释阅读《高质量程序设计指南 C/C++》第14章
4. 派生类的默认成员函数
派生类继承父类时,并不会继承基类的成员函数(构造函数、析构函数、赋值函数)
在继承关系里面,在派生类中如果没有显示定义六个成员函数,编译系统则会默认合成这六个默认的成员函数。(1.构造函数、2.拷贝构造函数、3.析构函数、4.赋值操作符重载、5.取地址操作符重载、6.const修饰的取地址操作符重载)
派生类的构造函数(包括拷贝构造函数)应在其初始化列表也只能在其初始化列表里(不能在派生类构造函数内调用基类的构造函数)显示地调用基类的构造函数(除非基类的构造函数不可访问)。
如果基类是多态类,那么必须把基类的析构函数定义为虚函数
实现派生类的赋值函数时,派生类中继承自父类的成员变量可以直接调用基类的赋值函数实现。
代码:
class Base
{
public:
Base(const char* name = "")
:_name(name)
{
cout << "Base()构造" << endl;
}
Base(const Base& b)
:_name(b._name)
{
cout << "Base()拷贝构造" << endl;
}
Base& operator=(const Base& b)
{
cout << "Base()赋值运算符重载" << endl;
if (this != &b)
{
_name = b._name;
}
return *this;
}
~Base()
{
cout << "Base() 析构" << endl;
}
protected:
string _name;
};
class Dervied : public Base
{
public:
Dervied(const char* name,int num)
:Base(name)//显示调基类的构造
, _num(num)
{
cout << "Dervied()构造" << endl;
}
Dervied(const Dervied& der)
:Base(der)//显示调基类的拷贝构造
, _num(der._num)
{
cout << "Dervied()拷贝构造" << endl;
}
Dervied& operator=(const Dervied& der)
{
cout << "Dervied()赋值运算符重载" << endl;
if (this != &der)
{
Base::operator=(der);//调基类的赋值运算符重载
_num = der._num;
}
return *this;
}
~Dervied()
{
cout << "Dervied()析构" << endl;
}
protected:
int _num;
};
void Test()
{
Dervied d("xiaozhang", 20);
cout << endl;
Dervied d1(d);
cout << endl;
Dervied d3("xiaoming", 21);
cout << endl;
d1 = d3;
cout << endl;
}
int main()
{
Test();
system("pause");
return 0;
}
分析:在实现默认成员函数时,从基类继承的成员就调用基类的成员函数进行构造,拷贝构造等;在析构时,顺序则相反,后构造的先析构
注:派生类的析构函数隐藏了基类的析构函数,在调用完自己的析构函数时,自动调用父类的析构函数,(编译器会把所有析构函数名换成destructor,这样就构成了隐藏)
5. 单继承和多继承
5.1 单继承
单继承指单一继承,一个子类只 有一个直接父类时称这个继承关系为单继承。这种关系比较简单是一对一的关系:
5.2 多继承
多继承是指 一个子类有两个或以上直接父类时称这个继承关系为多继承。这种继承方式使一个子类可以继承多个父类的特性。
多继承可以看作是单继承的扩展。派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承。
多继承下派生类的构造函数与单继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。同时,派生类的参数个数必须包含完成所有基类初始化所需的参数个数。在子类的内存中它们是按照声明定义的顺序存放的,下面的截图将清晰看到。
6. 菱形继承和虚继承
6.1 菱形继承
两个子类继承同一个父类,而又有子类同时继承这两个子类,也被称为钻石继承。
6.2 菱形继承的问题
数据冗余
产生二义性
模型:
实例:
class Farther
{
public:
void func()
{
cout << "Farther::func()" << endl;
}
protected:
int _father;
};
class Son1 :public Farther
{
protected:
int _son1;
};
class Son2 :public Farther
{
protected:
int _son2;
};
class GrandSon :public Son1, public Son2
{
protected:
int _gson;
};
void Test()
{
GrandSon gson;
gson.func();//产生二义性,此时son1和son2都继承了Farther,有两份,找func时目标不明确
}
int main()
{
Test();
system("pause");
return 0;
}
分析: son1和son2都继承了父类Farther,此时func存了两份,GrandSon又继承了son1和son2,必定去它的父类里面找,但现在有两份,不知道选哪一个,因此产生了二义性。
解决办法:
1. 指明类域
void Test()
{
GrandSon gson;
gson.Son1::func();
gson.Son2::func();
}
2.采用虚继承
class Farther
{
public:
void func()
{
cout << "Farther::func()" << endl;
}
protected:
int _father;
};
class Son1 :virtual public Farther//son1虚继承Farther
{
protected:
int _son1;
};
class Son2 :virtual public Farther//son2虚继承Farther
{
protected:
int _son2;
};
class GrandSon :public Son1, public Son2
{
protected:
int _gson;
};
void Test()
{
GrandSon gson;
cout << sizeof(gson) << endl;
gson._father = 1;
gson._son1 = 2;
gson._son2 = 3;
}
int main()
{
Test();
system("pause");
return 0;
}
虚继承:
虚继承即让son1和son2在继承Farther时加上virtural关键字,不可写成GrandSon:virtual public son1,public son2
虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。
虚继承的内存分布:
先来看下gson在虚继承和普通继承下的大小
普通继承:
虚继承:
从两个图上看出,虚继承下的Son2比普通继承的大小还要大,下来看看虚继承下的内存分配
gson的内存
从内存中可以看到数据确实存进去了,但2(Son1的成员)上面还有一个指针,3(Son2的成员)上面同样存在一个指针,可查看到底存的是什么?
Son1的成员上指针指向的内容,保存了数字20
Son2的成员上指针指向的内容,保存了数字12
两个数字的用途:
一个是数值20,一个是12。这时候看看内存1这张图片,我们发现gson. _ farther(1)的地址和 gson. _ son1(2)地址之差是20,gson. _ son2(3) 的地址和 gson. _ farther(1)地址之差是12。
可以推算出,每一个继承自父类对象的实例中都存有一个指针,这个指向指向虚基表的其中一项,里面存的是一个偏移量。对象的实例可以通过自身的地址加上这个偏移量找到存放继承自父类对象的地址。
虚继承下的内存模型