鱼C论坛

 找回密码
 立即注册
查看: 2665|回复: 3

[技术交流] 000系列1:C++三大特性

[复制链接]
发表于 2018-2-5 14:16:08 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 Messj 于 2018-2-5 18:18 编辑

C++具有三大特性,即封装、继承与多态


封装可使代码模块化,继承可以扩展已存在的代码,二者都有实现代码重用的目的。而多态则是为了实现接口重用。


目录:

2L:封装性分析
3L:继承性分析
4L:多态性分析


本文大量借鉴http://huqunxing.site/博客中的文章以及百度百科等其他文章,在此表示感谢。本文不做讨论(用作记录),如果有问题,可直接在空间中留言。

本帖被以下淘专辑推荐:

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 2018-2-5 14:26:13 | 显示全部楼层
本帖最后由 Messj 于 2018-2-5 16:44 编辑

一 &何谓封装

所谓封装,即将某些事物包装隐藏起来,让外界无法直接使用,只能通过已定义的特有方式进行访问。封装可以隐藏代码实现的细节,使代码模块化。在面向对象编程上可理解为:把客观事物封装成抽象的类,并且类可以有选择性的把自己的数据和方法提供或是拒绝给其他类或者对象操作。

二 &数据抽象

数据抽象是指只向外界提供关键信息并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。数据抽象是一种依赖于接口和实现分离的编程(设计)技术。
数据抽象有两个重要的优势:1.类的内部受到保护,不会因无意的用户级错误导致对象状态受损。 2.类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。(若只在类的私有部分定义数据成员,编写该类的作者就可以随意更改数据,不必担心影响到其他类。如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响。若数据是公有的,则任何直接访问旧表示形式的数据成员的函数都可能受到影响。)

三 &数据封装

所有的 C++ 程序都有以下两个基本要素:1.程序语句(代码):这是程序中执行动作的部分,它们被称为函数。2.程序数据:数据是程序的信息,会受到程序函数的影响。
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。

数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。

四 &类

C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。我们已经知道,类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。默认情况下,在类中定义的所有项目都是私有的。 <类的成员函数,可以访问本类内的任何成员变量和成员函数>

关于public、protect、private的访问权限控制的了解,可以查看 http://blog.csdn.net/ycf74514/article/details/49053041

PS:关于class和struct的区别
1.class不写修饰符,成员默认是private的,而struct 默认是public的
2.class的继承默认是private的,而struct默认是public的

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2018-2-5 16:44:48 | 显示全部楼层
本帖最后由 Messj 于 2018-2-5 17:48 编辑

一 &何谓继承

C++中的继承,主要指类的继承,即新的类从已有类那里得到已有的特性。原有的类称为基类或父类,产生的新类称为派生类或子类。

派生类的声明:
class 派生类名:继承方式 基类名1, 继承方式 基类名2,...,继承方式 基类名n
{
    派生类成员声明;
};
在 c++ 中,一个派生类可以同时有多个基类,这种情况称为多重继承。如果派生类只有一个基类,称为单继承。派生类继承基类中除构造和析构函数以外的所有成员。

二 &类的继承方式


                               
登录/注册后可看大图

继承方式规定了如何访问基类继承的成员。继承方式有public, private, protected。如果不显示给出继承方式,默认为private继承。继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限。(在2L的四中有推荐的文章地址)

三 &派生类的构造函数

派生类中由基类继承而来的成员的初始化工作还是由基类的构造函数完成,派生类中新增的成员在派生类的构造函数中初始化。
派生类构造函数的语法:
派生类名::派生类名(参数总表):基类名1(参数表1),基类名(参数名2)....基类名n(参数名n),内嵌子对象1(参数表1),内嵌子对象2(参数表2)....内嵌子对象n(参数表n)
{
    派生类新增成员的初始化语句;
}
注:构造函数的初始化顺序并不以上面的顺序进行,而是根据声明的顺序初始化。
如果基类中没有不带参数的构造函数,那么在派生类的构造函数中必须调用基类构造函数,以初始化基类成员。派生类构造函数执行的次序:
1.调用基类构造函数,调用顺序按照它们 被继承时声明的顺序 (从左到右);
2.调用内嵌成员对象的构造函数,调用顺序按照它们在类中声明的顺序;
3.派生类的构造函数体中的内容。

四 &派生类的析构函数

派生类的析构函数的功能是在该对象消亡之前进行一些必要的清理工作,析构函数没有类型,也没有参数。析构函数的执行顺序与构造函数相反
事例:
#include <iostream>
#include <time.h>
using namespace std;
// 基类 B1
class B1
{
public:
    B1(int i)
    {
        cout<<"constructing B1 "<<i<<endl;
    }
    ~B1()
    {
        cout<<"destructing B1"<<endl;
    }
};
//基类 B2
class B2
{
public:
    B2(int j)
    {
        cout<<"constructing B2 "<<j<<endl;
    }
     ~B2()
    {
        cout<<"destructing B2"<<endl;
    }
};
//基类 B3
class B3
{
public:
    B3()
    {
        cout<<"constructing B3"<<endl;
    }
    ~B3()
    {
        cout<<"destructing B3"<<endl;
    }
};
//派生类 C, 继承B2, B1,B3(声明顺序从左至右。 B2->B1->B3)
class C: public B2, public B1, public B3
{
public:
    C(int a, int b, int c, int d):B1(a), memberB2(d), memberB1(c),B2(b)
    {
                //B1,B2的构造函数有参数,B3的构造函数无参数
              //memberB2(d), memberB1(c)是派生类对自己的数据成员进行初始化的过程、
        //构造函数执行顺序, 基类(声明顺序)-> 内嵌成员对象的构造函数(声明顺序) -> 派生类构造函数中的内容
    }
private:
    B1 memberB1;
    B2 memberB2;
    B3 memberB3;
};
int main() 
{ 
    C obj(1,2,3,4);
    return 0; 
}
/* 输出结果 */
/*
constructing B2 2
constructing B1 1
constructing B3
constructing B1 3
constructing B2 4
constructing B3
destructing B3
destructing B2
destructing B1
destructing B3
destructing B1
destructing B2
*/

五 &二义性问题

1.在单继承下,基类的public 和protected 成员可以直接被访问,就像它们是派生类的成员一样,对多继承这也是正确的。但是在多继承下,派生类可以从两个或者更多个基类中继承同名的成员。然而在这种情况下,直接访问是二义的,将导致编译时刻错误。
示例:
#include <iostream>
using namespace std;
class A
{
public:
void f();
};
class B
{
public:
void f();
void g();
};
class C : public A, public B
{
public:
void g();
void h();
};
int main(){
        C c1;
    // c1.f();    产生二义性问题,访问A中的 f()? or B的 f() ?
    //通过指定成员名,限定消除二义性
    c1.A::f();
    c1.B::f();
}
使用成员名限定法可以消除二义性,但是更好的解决办法是在类C中定义一个同名函数 f(), 类C中的 f() 再根据需要来决定调用 A::f() or B::f(), 这样 c1.f() 将调用 C::f()

2.当一个派生类从多个基类派生类,而这些基类又有一个共同的基类,则对该基类中说明的成员进行访问时,也可能会出现二义性。
举例:
//  派生类 B1,B2 继承相同的基类 A, 派生类 C 继承 B1, B2
class A
{
public:
int a;
};
class B1 : public A
{
private:
int b1;
};
class B2 : public A
{
private:
int b2;
};
class C : public B1, public B2
{
public:
int f();
private:
int c;
};
int main(){
        C c1;
    c1.a();
    c1.A::a();
    c1.B1::a();
    c1.B2::a();
        return 0;
}
c1.a; c1.A::a; 这两个访问都有二义性,c1.B1::a; c1.B2::a;是正确的:
类C的成员函数 f() 用如下定义可以消除二义性:
int C::f()
{ 
        retrun B1::a + B2::a; 
}
由于二义性的原因,一个类不可以从同一个类中直接继承一次以上。

六 &虚基类

多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如非常经典的菱形继承层次。
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突,而且很少有这样的需求。使用虚基类,可以使得在派生类中只保留间接基类的一份成员。

声明虚基类只需要在继承方式前面加上 virtual 关键字,如下面示例:
#include <iostream>
using namespace std;
class A{
protected:
    int a;
public:
    A(int a):a(a){}
};
class B: virtual public A{  //声明虚基类
protected:
    int b;
public:
    B(int a, int b):A(a),b(b){}
};
class C: virtual public A{  //声明虚基类
protected:
    int c;
public:
    C(int a, int c):A(a),c(c){}
};
class D: virtual public B, virtual public C{  //声明虚基类
private:
    int d;
public:
    D(int a, int b, int c, int d):A(a),B(a,b),C(a,c),d(d){}
    void display();
};
void D::display(){
    cout<<"a="<<a<<endl;
    cout<<"b="<<b<<endl;
    cout<<"c="<<c<<endl;
    cout<<"d="<<d<<endl;
}
int main(){
    (new D(1, 2, 3, 4)) -> display();
    return 0;
}
/* 
运行结果:
a=1
b=2
c=3
d=4
*/
本例中我们使用了虚基类,在派生类D中只有一份成员变量 a 的拷贝,所以在 display() 函数中可以直接访问 a,而不用加类名和域解析符。
虚基类的初始化, 请注意派生类D的构造函数,与以往的用法有所不同。 以往,在派生类的构造函数中只需负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。现在,由于虚基类在派生类中只有一份成员变量,所以对这份成员变量的初始化必须由派生类直接给出。如果不由最后的派生类直接对虚基类初始化,而由虚基类的直接派生类(如类B和类C)对虚基类初始化,就有可能由于在类B和类C的构造函数中对虚基类给出不同的初始化参数而产生矛盾。所以规定:在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。

在上述代码中,类D的构造函数通过初始化表调了虚基类的构造函数A,而类B和类C的构造函数也通过初始化表调用了虚基类的构造函数A,这样虚基类的构造函数岂非被调用了3次?大家不必过虑,C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

最后请注意: 为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。

六 &赋值兼容原则

赋值兼容: 赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。

赋值兼容规则中所指的替代包括:
1.派生类的对象可以赋值给基类对象;
2.派生类的对象可以初始化基类的引用;
3.派生类对象的地址可以赋给指向基类的指针。
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员



想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2018-2-5 16:58:39 | 显示全部楼层
本帖最后由 Messj 于 2018-2-5 18:14 编辑

一 &何谓多态

多态性是指对不同类的对象发出相同的消息将会有不同的实现。多态性也可以理解为,在一般类中定义的属性或服务被特殊类继承后,可以具有不同的数据类型或不同的实现。可见,多态性与继承性相关联。

C++语言支持多态性表现在:
1.C++语言允许函数重载和运算符重载。
2.C++语言通过定义虚函数来支持动态联编,动态联编是多态性的一个重要的特征。

二 &虚函数

虚函数: 就是允许被其子类重新定义的成员函数,子类重新定义父类虚函数的做法,可实现成员函数的动态覆盖(Override)。
纯虚函数: 是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
class <类名>
{
virtual <类型><函数名>(<参数表>)=0;       //纯虚函数的声明
…
};
抽象类: 包含虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能进行实例化
#include<iostream>
using namespace std;
//基类对象
class Base
{
public:
        //有virtual关键字,运行时多态
        virtual void f(float x)
        {
                cout<<"Base::f(float)"<< x <<endl;
        }
    //无viratul关键字,不会发生运行时多态
        void g(float x)
        {
                cout<<"Base::g(float)"<< x <<endl;
        }
        void h(float x)
        {
                cout<<"Base::h(float)"<< x <<endl;
        }
};
class Derived : public Base
{
public:
        virtual void f(float x)
        {
                cout<<"Derived::f(float)"<< x <<endl;   //多态、覆盖
        }
    //子类与父类的函数同名,无virtual关键字,则为隐藏
        void g(int x)
        {
                cout<<"Derived::g(int)"<< x <<endl;     //隐藏
        }
        void h(float x)
        {
                cout<<"Derived::h(float)"<< x <<endl;   //隐藏
        }
};
int main(void)
{
        Derived d;        //子类
        Base *pb = &d;        //基类指针指向子类
        Derived *pd = &d;        //子类指针指向自己
        // Good : behavior depends solely on type of the object
        pb->f(3.14f);   // Derived::f(float) 3.14        调用子类,多态
        pd->f(3.14f);   // Derived::f(float) 3.14        调用子类
        // Bad : behavior depends on type of the pointer
        pb->g(3.14f);   // Base::g(float)  3.14        无多态,调用自己的
        pd->g(3.14f);   // Derived::g(int) 3         无多态,调用自己的
        // Bad : behavior depends on type of the pointer
        pb->h(3.14f);   // Base::h(float) 3.14        无多态,调用自己的
        pd->h(3.14f);   // Derived::h(float) 3.14        无多态,调用自己的
        return 0;
}

三 &隐藏

隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
1.如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆,重载是在同一个类中,而隐藏涉及派生类与基类)。
2.如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆,覆盖有virtual关键字)。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-10-1 12:21

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表