泛型和抽象编程

多态

         面向对象的三大特性:封装、继承、多态中,可以说多态是核心了,没有多态机制的语言不能纯面向对象的语言这句话很有道理的。封装强调自定义数据类型来适用问题,继承强调类间的层次结构,有了前面的基础,多态就真的让面向对象语言构建出多姿多态的程序了。

要说多态的精华,自然是设计模式这门艺术了,多态让程序是真的产生了质变。

 

         多态分类:1)、编译时的多态,体现在函数重载。

                            2)、运行时的多态,体现在虚函数的重写(覆盖)。

 

         多态的实现:面向对象之所以比面向过程“高级“,其中有一点是因为,面向过程中的数据类型与函数是离散的,每次都需要程序员去组织;而面向对象的类将相关的变量成员和函数绑定在了一起,数据类型不仅可以自定义值集合,还可以绑定操作集合。而多态让类可以动态绑定函数,即虚函数。具体实现是为存在虚函数的类建立一维的虚函数表,让类绑定这个虚函数表(指针),具体怎么绑定,到程序运行时动态决定。

         C++注意事项虚函数:

                  1)、与java默认都是虚函数不同,c++需要自行加virtual指定哪些成员函数做虚函数。

                  2)、与java相同,属于类所有的静态成员不能加virtual,包括构造函数。

                  3)、析构函数通常声明为虚函数。否则无法通过父类指针释放子类的空间造成内存泄漏。

                  4)、区分普通函数重载与虚函数重写。重载其他相同看参数列表(注意有无const关键字的两函数算重载),重写其他相同返回值类型可以相同或不同。

抽象编程

客观物质世界的东西是不可能真的在计算机中表示出来的,计算机所能做的仅仅是经过人抽象后的逻辑表示。不得不说咋们人类这个存在还是很牛逼的,明明搞出的计算机就只能识别0和1,结果硬是让它展现出绚丽的世界。

没错的,编程就是在做抽象,不管咋们有没有认识到。

而且越是抽象的也就越是“可靠”,软件工程的“稳定依赖原则”?比如,天气预报说,明天中国南方a城b村在下午六点整有雨。这样的话这个雨就具体到b村这个确定位置的小面积而且还得六点整,这个天气预报“稳不稳”?如果抽象点这样说呢,明天中国南方a城有雨,这样条件只剩a城和明天了,如果再抽象点进行预报,明天中国有雨,是不是更“稳”?

抽象编程做的不仅仅是将客观世界的对象抽象并用计算机表示出来,而且还要处理

这些用计算机表示出来的对象之间的关系。其实我说的是宏观上程序模块间的关系。程序为什么分模块?用咋们大中国的话说“大事化小,小事话了”。只有把程序这个整体细分更小的单位才能谈维护、扩展、重用。那么怎么组织这些单位的关系呢?像上面天气预报一样自然越抽象越好、也才“稳”(别走极端)。一门好的程序设计语言因该在语言本身的机制上就提供“抽象“的机制保证,而不是全靠程序员亲历亲为。

          然后顺其自然地可以谈谈抽象类了。这点java和c++比较相同,对抽象类的定义是:含有(纯虚拟函数|抽象方法)的类就是抽象类。抽象类的作用一是减少重复代码,二是强制继承的子类必须实现纯虚函数。这比仅继承要严格多了,是本身语言机制上的强制,而不是靠程序员自觉重写父类的虚函数。所以抽象类通常都是用在软件设计的时候,用来确保软件质量的,故抽象类没理由实例化。

          然而有个问题是含有(纯虚拟函数|抽象方法)的类就是抽象类,那么抽象类中可以多加点其他什么或少点什么,只要最终有纯虚函数或抽象方法就可以了。那么这种“不太稳定“的抽象类做模块间设计也显得不稳。有问题自然会被解决,所以又发展出比抽象类更抽象,用作模块间设计更”稳“的面向接口编程。这样模块间以”接口“通信,各接口内的实现屏蔽掉,仅提供有限的、稳定的通信界面,少去很多依赖了。面向接口这种更抽象的编程好处是:其一对模块内来说,有维护变动、升级的空间。其二对模块的依赖者来说,只要模块提供的接口没变其他都不会对自己造成影响。

C++中要面向接口编程,只能自己将类的成员约束,通常只定义纯虚函数。而java

因为有足够的借鉴就在一开始就提供接口类型这一特殊类型来保障面向接口编程了,而且接口类型也是对java单继承缺点的弥补。

          特别的是,c++中的抽象类中,一定要把析构函数显示定义成虚函数,且不能是纯虚函数,因为抽象类就是用来继承的。

         

 

 

泛型编程

在谈泛型编程前,先谈下下面的东西:

1)、什么叫泛型:又叫“参数化类型”,即将函数、类等中的具体数据类型抽象化。本质是从一组相关的具体类型中抽象出与类型无关的通用算法或数据结构。具体到c++,比如,要实现两个数据的交换,可以有下面的声明:

void swap(int& a,int& b){

         int c = a;

         a = b;

         b = c;

}

上面是针对int型的。如果要交换double型呢,函数重载:

void swap(int& a,int& b){

         int c = a;

         a = b;

         b = c;

}

其他类型的交换呢?继续重载、、、

问题是面向对象的类机制可以自定义出无数种类类型出来,怎么办?开始抽象,上面函数不同点:参数列表和中间变量的数据类型在本函数是一致的,不同函数中不同。相同点:操作目的相同;算法相同;形式上除了数据类型都同。要是我们可以这样声明:

void swap(T& a,T&b){

         T c = a;

         a = b;

         b = c;

}

          然后在函数调用时,每个调用自己加上相应类型进行处理就好了。这样不就可以用这一个代表所有做这种swap的函数了吗?可以吗?可以的,不过具体语言的实现不是这样简单表示的。我们不提具体类型,但只要满足这种操作的类型都可以使用,甚至包括将来会出现的类型。

          上面过程体现了“抽象”,其实我们的编程就是在抽象,具体的物质世界不可能在计算机中表示出来,只能是经过抽象后的逻辑表示。我们通常意义上的数据结构和算法也是抽象的表示,具体到一种编程语言就是实现的数据类型和程序。泛型也是一种抽象,抽象的源头是各种数据类型,结果是通用的算法或数据结构。泛型机制在具体编程语言的实现会不同。

 

 

 

              2)、泛型机制的实现

Java中因为类就是程序基本单位,故java泛型主要就是泛型类了,类中的变量和方法自然也是泛型化的了。还有针对引用变量,称为泛型引用,在方法中的局部引用变量可能带上下限。

特点:[1]、泛型的类型参数只能是引用类型不能是基本类型;

  [2]、可自定泛型标记符,但通常用推荐的,如下面:

T ——泛型Type。

K ——键Key,比如映射的键。

V ——值Value,比如映射的值。

E ——元素Element。

                    [3]、支持通配符:“?”,用在具体类型不确定的时候。例如声明一个引用,List<?> lis;这个lis可以指向所有List集合,但其中元素都会丢失具体类型,其实质是object类。

                    [4]、支持泛型限定(上限和下限):

         如限定类上限class Info<T extends Number>{ }//此处泛型只能是继承自Number的数字类型

         如限定引用下限 Man<? super Chinese> ma;//此处stu只能指向Chinese的父类,上限是Object。

 

Java泛型实质是用默认超类Object类型,因为java都是类,而所有类都是Object子类,这样做是安全的。

 

 

C++的泛型:两个大类,函数模板、类模板。

定义方式,函数或类前加模板前缀,如:

template<typename T>//模板前缀

void swap(T& a,T&b){

         T c = a;a=b;b=c;

}

 

如一个简单的链表的类模板:

Template<typename T>//模板前缀

Struct Node{

         T c;

         Node *next,*pref;

         Node(T& d):c(d),next(0),pref(0){}

}

Template<typename T>//模板前缀

class List{

         Node<T> *first,*last;

Public:

         List();

         void add(T& c);//定义与实现分离,实现时要加与类相同的模板前缀

         void remove(T& c);

         Node<T>* find(T& c);

         void print();

         ~List();

};

 

模板前缀中的参数问题:

         [1]、模板参数可以带多个且可带默认值。这个默认值与函数中的默认值不是一

个东西,这个默认值的数据类型。如我们熟悉的向量容器:

                  tempate<class T,class Allocator=allocator<T> >

                  class vector;

第二个参数是STL的一个默认的内存分配器,负责内存分配与释放。

         [2]、参数可以是数据类型,也可以是数值。

                  Template<unsigned int N>

                  class bitset;

                  使用,bitset<100> bi;//大小为100的位集。

         模板参数为数值时多传递常量,描述空间大小或集合大小。

 

 

模板实例化问题

首先,模板只是一个产生实例的“模板”,函数模板不是函数,它产生的实例才是

函数;类模板也不是类,它产生的实例才是类。

模板这种泛型是在编译期决定了的,模板本身不会被编译成任何代码,而是编

译器根据模板激活语句产生特定的函数或类定义。也就是说模板可以产生多个函数定义或类定义,故函数模板是一个有相同操作的函数簇(算法的抽象),类模板是一个有相同结构的类族(数据结构的抽象)。

实例化:指根据模板创建函数定义或类定义的过程。这一过程包括两个大的过程,

其一确定模板参数的具体数据类型;其二由这个具体的数据类型产生函数或类定义的代码。

那么怎样确定模板参数的类型呢?也就是说怎么匹配模板?

(1)、对于函数,有两种:其一参数演绎;其二显示模板类型指定。

*参数演绎,如swap(2,3);编译器根据实参类型按规则一步步匹配函数模板

的形参,最后确定模板的具体数据类型。

演绎的问题:

         [1]、与普通函数调用混淆。用参数演绎方式匹配函数模板跟普通函数

调用在形式上是一样的,故模板机制规定普通函数优先。

         [2]、多个参数时,参数类型必须相同。模板参数匹配比普通函数匹配

严格得多,不会隐式类型转换。

                  如:template<typename T>

                          void swap(T& a, T&b){

         T c=a;a=b;b=c;

}

                          int main(){

                                   int a=3;double b=6;

                                   swap(a,b);//错,只能匹配(int,double)

                                   //下面这两种,在有的书上说是可以,但我用vs亲测不行。

                                   swap<double>(a,b);//ok?

                                   swap(static_cast<double>(a),b);//ok?

}

                                   [3]、特殊得const修饰符,const修饰的变量,在模板参数匹配时把const也作为参数类型的一部分。如,上面,把变量定义为const int a =1;就匹配出错了。

 

                          *显示模板类型指定:swap<double>(a,b);//不会存在调用普通函数。而且这样很直观,明明白白就是用的函数模板,建议用这种。

 

                  (2)、对于类,

         对类模板的使用必须显示指明数据类型,如List<double> li;

         模板类中的模板函数的特别处:它是单独实例化的,也就是说List<double> li;这句只是在编译时产生一个没有函数定义的类。然后像这样li.add(3.6);每使用一个才编译一个函数定义到类中(遵循一次定义)。有的书上称为“模板类的惰性实例化”。为了什么?提高编译效率,还是减少编译产生的代码。

         不过可以显示实例化,这样实例化类时把类的成员函数模板一次性都实例化了。

         Template List<double> li;//感觉没啥意义

 

         (1)、模板特例(template specializatin):通常用模板类来描述一个适应大部分数据类型的通用数据结构,也就是说模板不是万能的,有时会出现一个特别的类型,在结构或函数处理上有点不同。这时将这个通用的模板类特殊化处理,让它只适用于这个特别的类型。

         显然这样T就被这个特殊的类型取代了,特例化后的就不叫模板了,而是普通类,因为它只适用于这个特别的类型。故编译器也就能确定它的类型,直接编译成代码了。

         如,上面链表模板类特例化一个适用Student的专门链表;

                  Template<>

                  Class List<Student>{

                          //需要有的、要加入、修改等的操作。

}

         感觉模板特例化的实际意义并不大啊,与另写一个针对特定类型的链表也没啥区别了。唯一的好处是带来使用方式上的一致,让使用者看起来好像是一个模板解决了所有问题?

 

         (2)、还有一种“局部特例化”:模板特例化时不用非得特例化成一个普通的类,还可以局部特例化,结果任然是一个类模板,这点有些不同。局部特例化通常是带多个参数的类模板,特例化个别参数。

         如,类模板

         Template<typename T>

         Class A<T,T>{}//两个参数

 

         局部特例化:

         Template<typename T>

         Class A<T,Student>{}//将一个参数特例化成确定的类型

 

 

再谈多态:

         先说面向对象中的多态,咋们都知道面向对象三大概念:封装、继承、多态。没有书在介绍时是按多态、继承、封装的顺序来的吧?也就是说面向对象中的多态是以继承为前提的,这个时候表现多态的对象是具有类的层次结构的。也就是说面向对象的多态具体点是,具有类继承层次结构的对象对于继承自父类的行为可以有不同的表现。

class Man

{

public:

         virtual void eat() { cout << "a man can eat!" << endl; }

};

 

class Boy :public Man

{

         void eat() { cout << "a boy can eat!" << endl; }

};

class Girl :public Man

{

         void eat() { cout << "a girl can eat!" << endl; }

};

void doEat(Man& m)      //面向对象的多态处理

{

         m.eat();

}

 

int main()

{

         doEat(Man());

         doEat(Boy());

         doEat(Girl());

        

         getchar();

}

 

 

         再说广义的多态,指不同对象对同一行为有不同的表现(没继承层次结构这一约束)。比如,人、蚂蚁、草等都能“eat”(抽象),但在“eat”时的表现方式、过程、结果等可能不同。

class Man                 //人

{

public:

         void eat() { cout << "a man can eat!" << endl; }

};

class Ant          //蚂蚁

{

public:

         void eat() { cout << "a Ant also can eat!" << endl; }

};

class Grass               //植物

{

public:

         void eat() { cout << "I grow up,so I alse can eat!" << endl;

         }

};

 

template<typename T>

void doEat(T& a)     //用泛型,不特定具体类型,抽象描述“吃”

{

         a.eat();

}

 

int main()

{

         doEat(Man());

         doEat(Ant());

         doEat(Grass());

        

         getchar();

}

 

         看到了吗?可以用泛型(c++函数模板)来进行“多态”处理。前面人有继承层次的多态用的面向对象处理;后面广义的多态用的函数模板处理,他们可以在一定程度上达到“多态”的效果。C++中,有的书上把面向对象的多态叫“动多态”,把模板表现出来的叫“静多态”。

区别是:”动“强调对象与函数的绑定是运行时动态绑定的,“静“自然是编译时就绑定好了的。

利弊:这种“静多态“因为是编译是确定的函数绑定关系,故带来的好处其一自然会有编译时对绑定函数体的安全检查;其二没有运行时的额外开销就让时、空效率更高。不过,与面向对象的”动多态”的优点相比,那么点开销也就无足挂齿了,比如面向对象的继承层次结构让多态的表现有迹可循,而不是“静多态”靠对象自己具有“eat”的行为(函数同名才能在函数模板中成功调用)。还有维护、扩展更是不能比的。现实世界的事物大都具有层次结构的,“动多态”自然是首选,然而还有一些对象是不能归为同一层次的,要描述他们可以用“静多态”。然后使用模板来简化一部分“静多态”编程可以作为c++面向对象编程的补充;java呢,自然是一贯的用接口类型作为补充啊。

 

总结:这个时候对泛型的认识是不是该提升下了?泛型不仅是一种抽象出与具体类型无关的通用算法或数据结构的手段,更是一种泛型编程思想的体现,它拓宽了抽象编程的方法。然而不论是java、c#、还是c++中,泛型一般都是使用在集合上或工具函数上。而这些绝大部分可以用语言内置提供的就能解决问题了。所以平时编程感觉用处有限、、、

 

posted @ 2017-11-17 13:56  衿沫青冥  阅读(142)  评论(0)    收藏  举报