/*******************************类的继承*******************************/
class Animal
{
public:
void eat()
{
std::cout << "Animal eat" << std::endl;
}
protected:
void sleep()
{
std::cout << "Animal sleep" << std::endl;
}
private:
void fly()
{
std::cout << "Animal fly" << std::endl;
}
};
class Fish : public Animal
{
public:
void swim()
{
std::cout << "Fish swim" << std::endl;
}
};
Animal为父类(基类) Fish为子类(派生类);
这是最简单的公有继承
对于外界 只有public的内容可以被访问
对于自己 自己的所有内容可被访问
对于子类 子类可以访问自己的所有内容 而且可以访问父类的public:内容 protected:内容
/*******************************类的封装*******************************/
对于继承的方式和权限:
类的继承特性: 父类的访问特性: 子类的访问特性:
public: public:
public: protected: protected:
private: No access:
------------------------------------------------------------
public: protected:
protected: protected: protected:
private: No access:
------------------------------------------------------------
public: private:
private: protected: private:
private: No access:
------------------------------------------------------------
/*******************************类的多态*******************************/
class Animal
{
public:
Animal()
{
std::cout << "Animal Construct" << std::endl;
}
~Animal()
{
std::cout << "Animal Destruct" << std::endl;
}
};
class Fish : public Animal
{
public:
Fish()
{
std::cout << "Fish Construct" << std::endl;
}
~Fish()
{
std::cout << "Fish Destruct" << std::endl;
}
};
构造函数与析构函数:
首先 我们实例化一个Fish fi;
会首先调用父类Animal的构造函数; 然后调用子类Fish的构造函数;
然后析构的时候 会先调用子类的析构函数, 在调用父类的析构函数
运行结果:
Animal Construct
Fish Construct
Fish Destruct
Animal Destruct
/------------------------------------------------------------------------/
如果此时 我们把Animal的构造函数改为Animal( int a, int b );
那么Fish类在构造的时候 就无法调用到父类Animal的构造函数 会报一个错 无合适的构造函数
因为Animal类已经没有默认构造函数了, 所以Fish类无法先构造Animal;
现在解决办法 Fish类的构造函数 改为 Fish() : Animal( 200, 300 )
现在是传递了常量初始化 可以改为 Fish( int a, int b ) : Animal( a, b )
这就是在构造函数之后初始化; 冒号:后 可以初始化 用逗号连接;
/------------------------------------------------------------------------/
如果此时 在Animal类中 写了一个 void Breath();函数
在Fish类中 也写了一个 void Breath();函数 他们返回值 函数名 参数列都一样;
这样就会产生函数的"覆盖"; 当我们继承了一个类的时候 我们可以覆盖一些行为;
此时 对于父类 他还是会调用"自己"的 Breath();函数, 对于Fish子类 他会调用属于"自己"的 Breath();函数
此时 也可以使用作用域解析符号 让子类又使用自己的方法 又使用父类的方法
void Breath()
{
Animal::Breath();
std::cout << "Fish" << std::endl;
}
/------------------------------------------------------------------------/
假设有如下两个类:
class Animal
{
public:
void Func1();
};
class Fish : public Animal
{
public:
void Func2();
void Func3();
};
int main( int argc, char *argv[] )
{
Animal an;
Fish fi;
return 0;
}
--> 第一类:
最简单的 an可以调用Func1(); fi可以调用Func2(); Fun3();
--> 第二类:
如果设置 an = fi; 结论是: an可以调用Func1(); fi可以调用Func2(); Func3();
先讨论一下内存模型: 对于一个animal类成员an 他的内存模型就是一片连续的内存
而对于一个Fish类成员fi; 他先构造了Animal类对象在构造了自己; 所以他的内存模型是 Animal类内存模型+Fish类内存模型
然后 an = fi; 相当于发生了一个截断, 最后an还是an; 所以并没有实际意义;
-->第三类:
如果设置 fi = an; 这种类型即为小进大 但是也不允许, 因为这是一个类对象 类对象的生成需要进行构造
但是等号并无法进行Fish类内存模型构造 所以这种方式不允许;
/------------------------------------------------------------------------/
假设有如下两个类:
class Animal
{
public:
void Func1();
};
class Fish : public Animal
{
public:
void Func1();
void Func2();
};
int main( int argc, char *argv[] )
{
Animal *an;
Fish *fi;
return 0;
}
--> 第一类:
最简单的 an可以调用Func1(); fi可以调用Func1(); Fun1(); (用的都是自己的)
--> 第二类:
如果设置 an = fi; 结论是: an可以调用Func1(); fi可以调用Func1(); Func2();
给一个解释: 首先 对于父类和子类的内存结构 在上面 对于指针类型的赋值
只是把指针4字节的内容强制改变了数据, 至于他指向了什么 这不一定( 或者说这是需要我们来保证的 )
之后 编译器会"按照接收地址的内存模型来解读这个地址"
( 类似如上就是: 把fi的地址给了an; 然后编译器按照an的内存模型来解读了fi )
在这里就会发现: 其实还是一个截断 an的内存模型只有父类部分 所以对an的解读也只有父类部分
所以an可以调用Func1();
-->第三类:
如果设置 fi = an; 这个是不行的 原因同上 an是小 fi是大 小进大 缺的部分无法构造
/------------------------------------------------------------------------/
class Animal
{
public:
virtual void Func1();
};
class Fish : public Animal
{
public:
virtual void Func1();
void Func2();
};
int main( int argc, char *argv[] )
{
Animal *an;
Fish *fi;
return 0;
}
--> 第一类:
最简单的 an可以调用Func1(); fi可以调用Func1(); Fun1(); (用的都是自己的)
--> 第二类:
如果设置 an = fi; 结论是: an可以调用Func1(); fi可以调用Func1(); Func2();
如上两种情况所说 其实给进行 an = fi; 其实没啥大用, 因为他们都调用了自己的函数;
但是有一种情况我们是需要解决的 我们希望使用父类的指针来模拟子类的行为
或者说 我们希望使用一根父类的指针 来调用我们在子类里"覆盖"了的函数
那么我们需要使用 "虚函数";
这样我们就可以使用一根父类的an指针 然后来模拟在子类中被 "override" 的函数 Func1();
这就是多态, 在未引入"虚函数"之前 父类总会根据内存模型来调用父类的函数;
引入了"虚函数"之后 父类就会(在运行时采用 "迟绑定")去检查 是否有子类override了父类的virtual函数;
然后确定 调用的是哪一个函数 ( 子类的? 父类的? "子类优先" );
"多态的实现":
当编译器发现一个类中有虚函数时;就会为这个类自动生成一个"虚表" ; 该表是一个一维数组,在这个数组中存放每个虚函数的地址
而且会生成一个"指向虚表指针"; 那么对于父类Animal有一个"虚表", 子类Fish由于一定有虚函数所以也有一个"虚表"
现在在来结合上面的信息; "编译器按照an的内存结构来解释fi"; 这里就发生了"两个动作": ( an小fi大 才会截断 大给小 ok 小给大 GG )
第一个是"内存的截断": 这就解释了为什么fi自己的函数 这里用不了了; 因为内存截断了,( 对于an, fi自己的子类部分成垃圾了 )
第二个是"虚表指针改变": 一开始an的虚表指针应该指向着自己的虚函数, fi的虚表指针也指向了自己的虚函数;
而此时an指向了fi子类对象 当按照an的内存结构解释fi时 解释到的是fi的"虚表"; 所以调用到了fi的虚函数;
( 在这里在说一下 int 和 short 之间为什么可以互相等, 但是父类子类不可以互相等;
在这里 无非就是两种情况 "大进小" "小进大";
对于基本类型来说 "小进大" 是 "赋值" ; "大进小" 是 "截断";
对于类来说 "小进大" 没法解释, 报错 ; "大进小" 是 "截断"; ( 这个截断和上面的不太一样, 他截断之后 自己还是自己 );
对于类的指针来说 "小进大" 同样没法解释; "大进小" 按照 "小的方式解释"; )
-->第三类:
如果设置 fi = an; 这个是不行的 原因同上 an是小 fi是大 小进大 缺的部分无法构造
/-----------------------------------------------------------------------------/
"虚析构函数":
与上同理 如果使用了父类指针来使用子类override掉的函数,那么会存在一个问题 谁来析构
正常情况下 在"迟绑定"之后 这一根父类的指针会优先指向了子类override后的函数 这没有问题;
那么析构函数呢 对于一个类来说他必然有析构函数 而且无参数,
-->第一种情况:
{
Fish fi;
Animal *an = &fi;
}
当主函数中出现这种情况的时候, 不会出现任何问题
an是一根指针 出栈即完蛋, fi是个类对象 出栈会自动调用Fish的析构函数
-->第二种情况:
Fish *fi = new Fish();
Animal *an = fi;
delete an;
当我们希望通过使用一根父类的指针来delete一个子类的对象的时候, 那么就会出现问题;
这根an指针会按照父类an的内存规则来匹配 最后匹配到了父类的析构函数, 所以他会调用到父类的析构函数;
但这是不对的 因为他其实并没有把这个子类对象完全析构 子类对象剩下了一部分继承出来的 会出现意外情况;
所以我们需要"把父类的析构函数也声明为虚析构函数", 当我们检测到需要调用父类的析构函数时 他是一个"虚析构函数"
那么类就会去"迟绑定"他"真正需要的析构函数" 对于一个子类 他真正需要的析构函数是 "子类的析构 + 父类的析构"
在这里就是子类Fish的析构函数 之后在调用自己的析构函数, 这个子类对象就被完全析构了. 这就是虚析构函数存在的意义
/-----------------------------------------------------------------------------/
总结: "虚函数用于想使用父类指针来对子类对象进行某种操作的时候"
/-----------------------------------------------------------------------------/
对于一个类中 有一个函数:
virtual Func1() = 0;
他是一个纯虚函数, 含有这个函数的类 是一个"抽象类" 他不能实例化对象;
( 对于子类,如果override了这个函数并且实现了他,才可以实例化,否则子类也不能实例化 )