面向对象三大特性是什么
这个问题几乎是所有程序员都会被问到的一个问题。首先,我们要记住这个问题的答案:面向对象程序设计的三大特性是:封装,继承,多态! 我们不仅要记住这三个特性是什么,更要知道这三大特性的具体有什么,更要理解这三大特性体现的程序设计的思想
为什么要有继承,怎么理解继承
今天我们就来看一看三大特性中的继承。那么首先我们要明白为什么会有继承这个问题。不妨来看这么一个案例:
//野猫
struct Cat
{
int _age;
int _legs;
};
//家养狗
struct Dog
{
const char* _name;
int _age;
int _leg;
const char* _owner;
};
//野猪类
struct Pig
{
int _age;
int _leg;
};
我们发现,这里的野猫,家养狗,还有野猪仅仅只有一小部分的成员是不一样的,而他们都有年龄,腿的数量这些公共的属性! 重复冗余的设计显然是不符合软件 设计的理念。为了提高代码的可复用性,c++引入了继承这个语法!也就是说继承实际上是一种代码复用的手段!
接下来,我们就来正式学习继承的语法:
继承的语法格式:
struct / class 派生类名 : 继承的方式:struct/class 基类
接下来,我们就来简单玩一玩继承:
#include
using namespace std;
//继承
class Animal
{
public:
int _legs=4;
int _age=3;
};
//猫类继承动物类
class Cat : public Animal
{
public:
void sound()
{
cout << "喵" << endl;
}
};
//狗类继承自动物类
class Dog : public Animal
{
public:
void sound()
{
cout << "汪" << endl;
}
};
int main()
{
Cat c;
Dog d;
return 0;
}
打开调试窗口查看c和d的情况:

可以看到这里的c和d确实把Animal的成员给继承下来了!也就是说c和d里面分别有一份_legs和_age,这就是继承实现的代码复
用。 而对于继承机制,C++设计的比较复杂,有如下的几种继承方式:

在继承体系中,又增加了新的权限访问限定符:protected:受保护的权限,这个访问权限限定符可以说就是为了继承而出现的。收到protected限定符修饰的成员,在派生类中依然可见,但是在基类和派生类外部不可访问!
而对于上述的继承体系,可以简单记住下面的口诀:
1.派生类继承基类的成员的访问权限的计算公式是 :min(基类中的访问权限,继承方式)。
2.private方式继承,派生类继承了基类的成员,但是语法限定了子类无法访问
3.class定义的类默认是private继承,struct默认是public继承,不过建议都声明继承方式.
而在实际的设计里面,大多是public继承!因为继承本身就是为了提高代码的复用性!使用protected/private继承本身就不利于代码的复用
继承的"天然特性"
接下来我们看一看继承其中的语法特性。
1.基类对象可以赋值给派生类对象
2.基类指针可以指向的派生类对象
3.基类引用可以引用派生类对象
这一切都是建立在public方式继承,protected/private继承都会改变原有的权限!所以这两种继承方式没有上面的3个特性!
这三个都是继承语法天然支持的!不会发生任何的类型转换
#include
using namespace std;
//汽车类
class Car
{
public:
Car(double speed=45)
{}
double _speed;
};
//宝马汽车继承汽车类
class BMW :public Car
{
public:
BMW(int price=100000)
:_price(price)
{}
int _price;
};
int main()
{
Car c;
BMW b;
//派生类对象可以赋值给基类对象
c = b;
//基类对象的指针可以指向派生类对象
Car* pc = &b;
//基类对象的引用可以引用派生类对象
Car& rc = b;
return 0;
}
那么我们应该怎么理解这种语法特性呢?有人形象地称之为切片或者是切割

而我们后面的多态还有菱形虚拟继承都是基于切片的!
继承里面的作用域
我们来看下面这么一段代码:
#include
using namespace std;
//
class A
{
public:
A()
{}
void fun()
{
cout << "A::fun()" << endl;
}
};
class B : public A
{
public:
B()
{}
void fun()
{
cout << "B::fun()" << endl;
}
};
int main()
{
B b;
//调用哪一个?
b.fun();
return 0;
}

可以看到,这里调用的是B里面的fun!注意到A里面也有一个函数叫做fun,A里面的fun和B里面的fun构成的关系不是重载,而是隐藏! 这是很多笔试题里面很喜欢问的重载和隐藏的区别!后面到了多态的时候还会有一个易混的重写,一定要做好区别。
//使用B对象调用A的fun就要指明类域
#include
using namespace std;
//
class A
{
public:
A()
{}
void fun()
{
cout << "A::fun()" << endl;
}
};
class B: public A
{
public:
B()
{}
void fun()
{
cout << "B::fun()" << endl;
}
};
int main()
{
B b;
//继承了A类的B类对象调用A类的函数要加上类域
b.A::fun();
return 0;
}

而面试的时候考的更多的隐藏关系是这样的:
class A
{
public:
A()
{}
void fun()
{
cout << "A::fun()" << endl;
}
};
class B: public A
{
public:
B()
{}
void fun(int i)
{
cout << "B::fun(int i)" << endl;
}
};
int main()
{
B b;
//b.fun(); 无法通过编译,因为隐藏了A类的fun,
//所以找到的是B类的需要一个int的fun
return 0;
}
这里的A类的fun和B类的fun构成隐藏关系,这是相对来说比较难以发现的隐藏关系。
继承和友元
这个相对来讲就比较简单。基类的友元不一定是派生类的友元。友元关系是不会继承的。
class A
{
public:
friend class C;
A(int a=0)
: _a(a)
{}
protected:
int _a;
};
class B: public A
{
public:
B(int b=1)
:_b(b)
{}
protected:
int _b;
};
class C
{
public:
void fun()
{
A a;
B b;
cout << a._a << endl;
cout << b._b << endl;
}
};
int main()
{
C c;
c.fun();
return 0;
}

继承和静态成员
静态成员在整个继承体系里面只会有1份,你可以认为它是世代相传的传家宝一样。
class A
{
public:
A()
{
++_x;
}
static int _x;
};
class B :public A
{
public:
B()
{}
};
class C :public A
{
public:
C()
{}
};
int A::_x = 0;
int main()
{
A a;
B b;
C c;
cout << c._x <<endl ;
return 0;
}

派生类的默认成员函数
那么我们在类和对象的那个章节可以知道,任何一个类都有六大默认的成员函数。而其中最重要的成员函数就是默认构造,拷贝构造,赋值重载和析构函数。那么在引入了继承以后,这几个默认的成员函数生成和之前有没有什么区别呢?
class A
{
public:
A()
{
cout << "A::A()" << endl;
}
};
class B : public A
{
public:
B()
{
cout << "B::B()" << endl;
}
};
int main()
{
B b;
return 0;
}

可以看到,我们只构造了B类对象,但是调用到了A类的构造函数,说明在构造派生类对象的时候会优先调用父类的构造函数初始化父类的部分,然后才会初始化子类的部分!
而如果父类没有默认构造就会出错!那么如果想要显式初始化子类就要这么书写:
class A
{
public:
A(int a=0)
:_a(a)
{
cout << "A::A()" << endl;
}
int _a;
};
class B : public A
{
public:
//把父类当作一个成员来用
B(int a=3,int b=4)
:A(a)
,_b(b)
{
cout << "B::B()" << endl;
}
int _b;
};
int main()
{
B b;
return 0;
}
接下来,我们来看一看拷贝构造函数怎么写
class A
{
public:
A(int a=0)
:_a(a)
{
cout << "A::A()" << endl;
}
A(const A& a)
:_a(a._a)
{}
int _a;
};
class B : public A
{
public:
//把父类当作一个成员来用
B(int b=4)
:A()
,_b(b)
{
cout << "B::B()" << endl;
}
//也要先拷贝B类中父类的部分,然后在拷贝子类
B(const B& b)
//利用继承的语法天然特性切片
:A(b)
,_b(b._b)
{}
int _b;
};
int main()
{
B b1(3);
A a(3);
B b2(b1);
return 0;
}

对应的赋值运算符重载的书写
//也是调用父类的operator=(),再拷贝自己的
B& operator=(const B& b)
{
//必须指明类域,否则因为隐藏找不到就会死递归
A::operator=(b);
_b = b._b;
return *this;
}
继承关系中对于析构函数的处理是非常复杂的,我们来看析构函数
class A
{
public:
A(int a=0)
:_a(a)
{
cout << "A::A()" << endl;
}
A(const A& a)
:_a(a._a)
{}
A& operator=(const A& a)
{
_a = a._a;
return *this;
}
~A()
{
cout << "~A()" << endl;
}
int _a;
};
class B : public A
{
public:
//把父类当作一个成员来用
B(int b=4)
:A()
,_b(b)
{
cout << "B::B()" << endl;
}
//也要先拷贝B类中父类的部分,然后在拷贝子类
B(const B& b)
//利用继承的语法天然特性切片
:A(b)
,_b(b._b)
{}
//也是调用父类的operator=(),再拷贝自己的
B& operator=(const B& b)
{
//必须指明类域,否则因为隐藏找不到就会死递归
A::operator=(b);
_b = b._b;
return *this;
}
~B()
{
cout << "~B()" << endl;
}
int _b;
};
int main()
{
B b1(3);
return 0;
}
观察调用的结果:

可以看到,这里自动调用了父类的析构函数,而且是先调用了子类的析构函数,然后才调用了父类的析构函数。 那么假设我们像显式调用析构函数可以吗?
//是否可以?
~B()
{
~A();
cout << "~B()" << endl;
}
先看结果:

可以看到,这里是不允许调用的。这就涉及更深层次的方面的问题,这里构成隐藏!因为多态的需要,析构函数统一被处理成destructor()了,所以构成了隐藏!
~B()
{ //显式调用A类的析构
A::~A();
cout << "~B()" << endl;
}

但是调用的结果又出了很大的问题。这里的A类被析构了4次,但是实际上只有2个B类对象,造成了多次析构!事实上,子类对象再完成自身的析构工作后会自动调用父类的析构函数进行析构保证析构顺序的合法性!我们显式取调用析构反而是错误的!
菱形继承
首先要说明一点,C++是允许多继承的!即一个子类可以有多个父类,那么多继承就会带来一定的问题。其中最经典的就是菱形继承。
class A
{
public:
int _a;
};
//BC继承A
class B : public A
{
public:
int _b;
};
class C :public A
{
public:
int _c;
};
//D继承B和C
class D :public B, public C
{
public:
int _d;
};
int main()
{
D d;
return 0;
}
整个的继承体系就是这样:

因为形状是一个菱形,所以得名菱形继承。菱形继承最大的问题就是数据冗余和二义性!

因为存在两份_a,所以就会存在二义性的问题:
int main()
{
D d;
//能否这样?
d._a = 4;
return 0;
}

可以看到,这里对于_a的访问不明确,这就是菱形继承带来的数据二义性问题。而指明所属的类域就可以解决二义性的问题
int main()
{
D d;
//显式指定类域可以解决类域访问冲突问题
d.B::_a = 1;
d.C::_a = 2;
return 0;
}
但是虽然指明类域解决了访问的二义性问题,但是解决不了数据冗余的问题!我们可以内存窗口来观察对象的模型。
int main()
{
D d;
//显式指定类域可以解决类域访问冲突问题
d.B::_a = 10;
d._b = 1;
d.C::_a = 11;
d._c = 2;
d._d = 3;
return 0;
}

可以看到这里还是存了两份_a,指明类域并没有解决数据冗余的问题!
那么怎么解决数据冗余的问题呢?—>菱形虚拟继承!
菱形虚拟继承
为了解决菱形继承带来的数据冗余问题,C++引入了菱形虚拟继承来解决这个问题! 我们先来看看菱形虚拟继承的语法格式
//菱形虚拟继承研究
class A
{
public:
int _a;
};
//菱形虚拟继承,再继承权限前加vitrual
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
D()
{}
int _d;
};
int main()
{
D d;
d._a = 1;
d._b = 2;
d._c = 3;
d._d = 4;
return 0;
}
菱形虚拟继承很好地解决了数据冗余和二义性问题,那么到底是怎么处理的呢?同样打开内存窗口进行分析:

可以看到,菱形虚拟继承把_a独立存在了一个位置,而不是和菱形继承一样在B的部分和C的部分都放一份,解决了数据冗余问题和二义性的问题。可能细心的读者还关注到了这个对象模型里面还存放了两个"随机值"。接下来我们再去看看这两个"随机值"存放了什么?

可以看到这两个指针本身指向的是空,但是下面两个的值一个是20(16进制的14),一个是12。联系前面的内存存储,我们可以看出这两个指针的值是B,C类起始部分到_a的偏移地址量!
为什么需要存放偏移量,这是和切片的原因相关!假设是B类的指针pb指向对象d,站在pb的角度,它只能看到一个B类大小的字节内容,但是_a并没有存在这里。所以为了能够找到_a,就要存储一个到_a的偏移量,方便查找。 这就是菱形虚拟继承解决菱形继承的问题。
总结:避免设计菱形继承。
以上就是继承的内容,如有不足之处还望指正。希望大家共同进步!