1. 三种继承关系public、protected和private
在学习类的时候我们便接触了这三个关键字,当时它们是作为类的成员访问限定符,在继承当中也要用到这三个关键字,此时它们表示的是三种继承方式。对于刚学习继承的人来说,肯定都会有一个疑惑——为什么有了private还有用proected的?它俩有什么区别?
含有private成员的类:其无论通过哪种继承方式,其子类对象在类的外部和内部都不能访问继承来 private成员。
含有protected成员的的类:其子类虽然在类外也不能通过子类对象访问父类protected成员,但在子类的类内,通过public和protected这两种继承方式,可以在内部访问到继承而来的父类protected成员;但如果是private继承,那么在子类内部无法访问继承自父类的protected的成员。
继承方式 | 基类的public成员 | 基类的protected成员 | 基类的private成员 | 继承引起的访问控制关系变化 |
---|---|---|---|---|
public继承 | 仍为public成员 | 仍为protected成员 | 不可见 | 基类的非私有成员在子类的访问属性都不变 |
protected继承 | 变为protected成员 | 变为protected成员 | 不可见 | 基类的非私有成员都成为子类的保护成员 |
private继承 | 变为private成员 | 变为private成员 | 不可见 | 基类的非私有成员都成为子类的私有成员 |
< 示例 >
class father {
public:
char* MyName; //名字是可以公开的
protected:
int MyAsset; //财产是受保护的,只有我和我的继承者才能使用
private:
char* MyPrivacy; //隐私是保密的,只有我自己能知道
};
class son :public father{ //继承方式:public
public:
char* MyName;
protected:
int Asset;
private:
char* MyPrivacy;
public:
void Inherit()
{
Asset = MyAsset; //可以在子类中访问父类的protected成员
}
};
总结:
- 基类中的private成员,不管通过何种方式继承,在其子类中均不能被访问。
- 某个成员不想被基类对象直接访问,但要在子类中能被访问,就定义成protected成员。
- public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
- protetced/private继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。
- 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,但是基类的私有成员存在但是在子类中不可见(不能访问)。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承。
2. 继承与转换–赋值兼容规则(public继承)
- 子类对象可以赋值给父类对象(切割/切片,不是强制类型转换)。
- 父类对象不能赋值给子类对象。
- 父类的指针/引用可以指向子类对象。
- 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成,但有隐患)。
class Human{
public:
string Name;
protected:
int Asset;
};
class Student :public Human{
public:
int num;
};
int main()
{
Human h;
Student s;
//子类对象可以赋值给父类对象(切割/切片,不是强制类型转换)
h = s;
//父类对象不能赋值给子类对象
//s = h;
//父类的指针/引用可以指向子类对象
Human* h1 = &s;
Human& r1 = s;
//子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
Student* p2 = (Student*)& h;
Student& r2 = (Student&)h;
}
关于切割:
3. 隐藏
隐藏是什么?
答:子类和父类中有同名成员,子类成员将屏蔽对父类成员的直接访问。
关于隐藏:
如果子类的函数与父类的函数同名,但是参数列表不同。此时,不论有无virtual关键字,父类的函数在子类中都将被隐藏(注意别与重载混淆);
如果子类的函数与父类的函数同名,并且参数列表也相同,但是父类函数没有virtual关键字。此时,父类的函数在子类中将被隐藏(注意别与覆盖混淆)。
总结来说:非override(重写)的情况下,派生类对象将屏蔽基类同名函数。
class Base{
public:
void func(int x);
};
class Derived : public Base{
public:
void func(char* str);
//void func(int x){ Base::func(x); } 修改2
};
void Test(void)
{
Derived* p = new Derived;
//p->func(10); //报错
p->Base::func(10); //修改1
}
上述代码中,原意是想调用函数Base::func(int)
,但Base::func(int)
已经被Derived::func(char*)
隐藏了。可以通过指定类作用域或者修改Derived类来改正。
4. 子类的默认成员函数
在继承关系里面,在派生类中如果没有显示定义六个成员函数,编译系统则会默认合成这六个默认的成员函数。(1.构造函数、2.拷贝构造函数、3.析构函数、4.赋值操作符重载、5.取地址操作符重载、6.const修饰的取地址操作符重载)
子类的构造函数(包括拷贝构造函数)应在其初始化列表也只能在其初始化列表里(不能在子类构造函数内调用父类的构造函数)显示地调用父类的构造函数(除非父类的构造函数不可访问)。
如果父类是多态类,那么必须把父类的析构函数定义为虚函数(后面解释)。
实现子类的赋值函数时,子类中继承自父类的数据成员可以直接调用父类的赋值函数实现。
子类的析构函数会隐藏父类的析构函数。(编译器会把所有析构函数名换成destructor,这样就构成了隐藏)参考下下图。
< code >
class Person
{
public :
Person(const char* name = "")
: _name(name)
{
cout<<"Person()构造" <<endl;
}
Person(const Person& p)
: _name(p ._name)
{
cout<<"Person拷贝构造" <<endl;
}
Person& operator=(const Person& p )
{
cout<<"Person赋值运算符重载"<< 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()拷贝构造" << endl;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
cout << "Student()赋值运算符重载" << endl;
return *this;
}
~Student()
{
cout<<"~Student()析构" <<endl;
}
protected:
int _num;
};
int main()
{
Student s1("Tianzez", 20);
cout << endl;
Student s2(s1);
cout << endl;
Student s3("Wangye", 21);
cout << endl;
s1 = s3;
cout << endl;
system("pause");
return 0;
}
关于子类的析构函数隐藏父类的析构函数:
5. 单继承与多继承
单继承是一般的单一继承,一个子类只 有一个直接父类时称这个继承关系为单继承。这种关系比较简单是一对一的关系:
多继承是指 一个子类有两个或以上直接父类时称这个继承关系为多继承。这种继承方式使一个子类可以继承多个父类的特性。多继承可以看作是单继承的扩展。派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承。
多继承下派生类的构造函数与单继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。同时,派生类的参数个数必须包含完成所有基类初始化所需的参数个数。在子类的内存中它们是按照声明定义的顺序存放的,下面的截图将清晰看到。
6. 菱形继承 & 虚继承
菱形继承是多继承的特殊情况,这种多继承会带来两个问题:
1. 数据冗余
2. 二义性
class Base
{
protected:
int _base;
public:
void fun()
{
cout << "Base::fun" << endl;
}
};
class A : public Base
{
protected:
int _a;
};
class B : public Base
{
protected:
int _b;
};
class D : public A, public B
{
private:
int _d;
};
int main()
{
D d;
d.fun();//编译器报错:调用不明确
system("pause");
return 0;
}
D的对象模型里面保存了两份Base,当我们想要调用从Base里继承的fun函数时就会出现调用不明确问题,并且会造成数据冗余的问题,明明可以只要一份就好,而我们却保存了两份。
此时可以域作用限定符调用我们所需的函数:
int main()
{
D d;
d.A::fun();
d.B::fun();
return 0;
}
但这种写法也是很麻烦的,这时候就需要一个新的解决方案——虚继承
关于虚继承:
虚继承即让A和B在继承Base时加上
virtural
关键字,记住不是D继承A、B使用虚继承。虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。
虚继承和虚函数虽然用的是同一个关键字,但是!但是!但是!它俩之间没半毛钱关系。
class A : virtual public Base
class B : virtual public Base
使用了虚继承后,调用这句代码d.fun();
就能编译通过。但是虚继承是如何实现的呢?它的底层机制又是如何?
前面说到虚继承是能解决数据冗余问题的,现在我们就来测测非虚继承和虚继承下,D对象的大小。
//虚继承:
cout << sizeof(D) << endl;
//结果是:24
//非虚继承:
cout << sizeof(D) << endl;
//结果是:20
???不是说虚继承可以节省空间吗,这咋空间开销还变大了?那就来看看虚继承情况下,一个D对象的实例内存里面到底存了些什么。
//虚继承情况下:
int main()
{
D d;
d._base = 1;
d._a = 2;
d._b = 3;
d._d = 4;
system("pause");
return 0;
}
d的内存
我们看到地面确实存的是d._a(2), d._b(3), d._d(4), d._base(1)
,但是d._a(2)
上一行的90 dc af 00
以及d._b(3)
上一行的b8 dd af 00
又是什么呢?可以看出这是两个地址,那就再来看看这两个地址里面存的是啥。
一个是数值20,一个是12。这时候看看内存1这张图片,我们发现d._a(2)
的地址和 d._base(1)
地址之差是20,d._b(3)
的地址和 d._base(1)
地址之差是12。扯到这,就不继续推了,放大招吧。
每一个继承自父类对象的实例中都存有一个指针(虚基表指针),这个指针指向指向虚基表的其中一项,里面存的是一个偏移量。对象的实例可以通过自身的地址加上这个偏移量找到存放继承自父类对象的地址。
写到这也就能理解上面关于虚继承的第三点,为什么说虚继承会带来性能上的损耗,所以一般情况下,尽可能不要定义多继承这种结构。