Qu'est-ce que le polymorphisme ? Quel est son mécanisme de mise en œuvre ? Quelle est la différence entre la surcharge et la réécriture ? Ce sont les quatre concepts très importants que nous allons revoir cette fois-ci : l'héritage, le polymorphisme, la surcharge et l'écrasement.
Héritage
En termes simples, l'héritage est basé sur un type existant, en ajoutant de nouvelles méthodes ou en redéfinissant des méthodes existantes (sera discuté ci-dessous, cette méthode est appelée écrasement) pour générer un nouveau type . L'héritage est l'une des trois caractéristiques de base de l'orientation objet - encapsulation, héritage et polymorphisme. Chaque classe que nous écrivons lors de l'utilisation de JAVA hérite, car dans le langage JAVA, la classe java.lang.Object est la classe de base la plus fondamentale de toutes les classes (ou classe parent ou super classe). Si une classe nouvellement définie que nous définissons ne spécifie pas explicitement de quelle classe de base elle hérite, alors JAVA héritera par défaut de la classe Object.
Nous pouvons diviser les classes en JAVA selon les trois types suivants :
Classe : une classe définie à l'aide de class et ne contient pas de méthodes abstraites.
Classe abstraite : une classe définie à l'aide d'une classe abstraite, qui peut ou non contenir des méthodes abstraites.
Interface : Classe définie à l'aide de l'interface.
Les règles d'héritage suivantes existent entre ces trois types :
Les classes peuvent hériter (étendre) des classes, peuvent hériter (étendre) des classes abstraites et peuvent hériter (implémenter) des interfaces.
Les classes abstraites peuvent hériter (étendre) des classes, peuvent hériter (étendre) des classes abstraites et peuvent hériter (implémenter) des interfaces.
Les interfaces ne peuvent étendre que les interfaces.
Veuillez noter que les différents mots-clés extends et Implements utilisés dans chaque cas d'héritage dans les trois règles ci-dessus ne peuvent pas être remplacés à volonté. Comme nous le savons tous, une fois qu'une classe ordinaire hérite d'une interface, elle doit implémenter toutes les méthodes définies dans cette interface, sinon elle ne peut être définie que comme une classe abstraite. La raison pour laquelle je n'utilise pas ici le terme « implémentation » pour le mot-clé Implements est que, conceptuellement, il représente également une relation d'héritage, et dans le cas de la classe abstraite Implements interface, il n'est pas nécessaire d'implémenter cette définition d'interface Any. méthode, il est donc plus raisonnable d’utiliser l’héritage.
Les trois règles ci-dessus respectent également les contraintes suivantes :
Les classes et les classes abstraites ne peuvent hériter que d'au plus une classe, ou hériter d'au plus une classe abstraite, et ces deux situations sont mutuellement exclusifs. C’est-à-dire qu’ils héritent soit d’une classe, soit d’une classe abstraite.
Les classes, classes abstraites et interfaces ne sont pas limitées par le nombre d'interfaces dont elles héritent. En théorie, elles peuvent hériter d'un nombre illimité d'interfaces. Bien entendu, pour une classe, elle doit implémenter toutes les méthodes définies dans toutes les interfaces dont elle hérite.
Lorsqu'une classe abstraite hérite d'une classe abstraite ou implémente une interface, elle peut partiellement, totalement ou totalement ne pas implémenter la méthode abstraite de la classe abstraite parent ou l'interface définie dans l'interface de la classe parent.
Lorsqu'une classe hérite d'une classe abstraite ou implémente une interface, elle doit implémenter toutes les méthodes abstraites de la classe abstraite parent ou toutes les interfaces définies dans l'interface de la classe parent.
L'avantage que l'héritage apporte à notre programmation est la réutilisation (réutilisation) des classes originales. Tout comme la réutilisation des modules, la réutilisation des classes peut améliorer notre efficacité de développement. En fait, la réutilisation des modules est l'effet superposé de la réutilisation d'un grand nombre de classes. En plus de l'héritage, nous pouvons également utiliser la composition pour réutiliser les classes. La soi-disant combinaison consiste à définir la classe d'origine comme attribut de la nouvelle classe et à réaliser la réutilisation en appelant les méthodes de la classe d'origine dans la nouvelle classe. S'il n'y a pas de relation incluse entre le type nouvellement défini et le type original, c'est-à-dire à partir d'un concept abstrait, les choses représentées par le type nouvellement défini ne font pas partie des choses représentées par le type original, comme les personnes jaunes. C'est une sorte d'être humain, et il existe une relation entre eux, inclure et être inclus, donc à l'heure actuelle, la combinaison est un meilleur choix pour parvenir à la réutilisation. L'exemple suivant est un exemple simple de la méthode de combinaison :
public class Sub { private Parent p = new Parent(); public void doSomething() { // 复用Parent类的方法 p.method(); // other code } } class Parent { public void method() { // do something here } }
Bien entendu, afin de rendre le code plus efficace, nous pouvons également initialiser le type d'origine (comme Parent p) lorsque nous en avons besoin utilisez-le.
Utiliser l'héritage et la combinaison pour réutiliser les classes originales est un modèle de développement incrémental. L'avantage de cette méthode est qu'il n'est pas nécessaire de modifier le code original, donc cela n'affectera pas le code original. et il n'est pas nécessaire de re-tester en raison des modifications apportées au code original, ce qui est évidemment bénéfique pour notre développement. Par conséquent, si nous maintenons ou transformons un système ou un module original, surtout lorsque nous n'en avons pas une compréhension approfondie, nous pouvons choisir le modèle de développement incrémental, qui peut non seulement améliorer considérablement notre efficacité de développement, mais également éviter les risques causés par modifications au code original.
Polymorphisme
Le polymorphisme est un autre concept de base important. Comme mentionné ci-dessus, c'est l'une des trois caractéristiques fondamentales de l'orientation objet. Qu’est-ce que le polymorphisme exactement ? Jetons un coup d'œil à l'exemple suivant pour nous aider à comprendre :
//汽车接口 interface Car { // 汽车名称 String getName(); // 获得汽车售价 int getPrice(); } // 宝马 class BMW implements Car { public String getName() { return "BMW"; } public int getPrice() { return 300000; } } // 奇瑞QQ class CheryQQ implements Car { public String getName() { return "CheryQQ"; } public int getPrice() { return 20000; } } // 汽车出售店 public class CarShop { // 售车收入 private int money = 0; // 卖出一部车 public void sellCar(Car car) { System.out.println("车型:" + car.getName() + " 单价:" + car.getPrice()); // 增加卖出车售价的收入 money += car.getPrice(); } // 售车总收入 public int getMoney() { return money; } public static void main(String[] args) { CarShop aShop = new CarShop(); // 卖出一辆宝马 aShop.sellCar(new BMW()); // 卖出一辆奇瑞QQ aShop.sellCar(new CheryQQ()); System.out.println("总收入:" + aShop.getMoney()); } }
Résultats d'exécution :
Modèle : BMW Prix unitaire : 300000
Modèle : CheryQQ Prix unitaire : 20000
Revenu total :320000
继承是多态得以实现的基础。从字面上理解,多态就是一种类型(都是Car类型)表现出多种状态(宝马汽车的名称是BMW,售价是300000;奇瑞汽车的名称是CheryQQ,售价是2000)。将一个方法调用同这个方法所属的主体(也就是对象或类)关联起来叫做绑定,分前期绑定和后期绑定两种。下面解释一下它们的定义:
前期绑定:在程序运行之前进行绑定,由编译器和连接程序实现,又叫做静态绑定。比如static方法和final方法,注意,这里也包括private方法,因为它是隐式final的。
后期绑定:在运行时根据对象的类型进行绑定,由方法调用机制实现,因此又叫做动态绑定,或者运行时绑定。除了前期绑定外的所有方法都属于后期绑定。
多态就是在后期绑定这种机制上实现的。多态给我们带来的好处是消除了类之间的耦合关系,使程序更容易扩展。比如在上例中,新增加一种类型汽车的销售,只需要让新定义的类继承Car类并实现它的所有方法,而无需对原有代码做任何修改,CarShop类的sellCar(Car car)方法就可以处理新的车型了。新增代码如下:
// 桑塔纳汽车 class Santana implements Car { public String getName() { return "Santana"; } public int getPrice() { return 80000; } }
重载(overloading)和重写(overriding)
重载和重写都是针对方法的概念,在弄清楚这两个概念之前,我们先来了解一下什么叫方法的型构(英文名是signature,有的译作“签名”,虽然它被使用的较为广泛,但是这个翻译不准确的)。型构就是指方法的组成结构,具体包括方法的名称和参数,涵盖参数的数量、类型以及出现的顺序,但是不包括方法的返回值类型,访问权限修饰符,以及abstract、static、final等修饰符。比如下面两个就是具有相同型构的方法:
public void method(int i, String s) { // do something } public String method(int i, String s) { // do something }
而这两个就是具有不同型构的方法:
public void method(int i, String s) { // do something } public void method(String s, int i) { // do something }
了解完型构的概念后我们再来看看重载和重写,请看它们的定义:
重写,英文名是overriding,是指在继承情况下,子类中定义了与其基类中方法具有相同型构的新方法,就叫做子类把基类的方法重写了。这是实现多态必须的步骤。
重载,英文名是overloading,是指在同一个类中定义了一个以上具有相同名称,但是型构不同的方法。在同一个类中,是不允许定义多于一个的具有相同型构的方法的。
我们来考虑一个有趣的问题:构造器可以被重载吗?答案当然是可以的,我们在实际的编程中也经常这么做。实际上构造器也是一个方法,构造器名就是方法名,构造器参数就是方法参数,而它的返回值就是新创建的类的实例。但是构造器却不可以被子类重写,因为子类无法定义与基类具有相同型构的构造器。
重载、覆盖、多态与函数隐藏
经常看到C++的一些初学者对于重载、覆盖、多态与函数隐藏的模糊理解。在这里写一点自己的见解,希望能够C++初学者解惑。
要弄清楚重载、覆盖、多态与函数隐藏之间的复杂且微妙关系之前,我们首先要来回顾一下重载覆盖等基本概念。
首先,我们来看一个非常简单的例子,理解一下什么叫函数隐藏hide。
#include <iostream> using namespace std; class Base{ public: void fun() { cout << "Base::fun()" << endl; } }; class Derive : public Base{ public: void fun(int i) { cout << "Derive::fun()" << endl; } }; int main() { Derive d; //下面一句错误,故屏蔽掉 //d.fun();error C2660: 'fun' : function does not take 0 parameters d.fun(1); Derive *pd =new Derive(); //下面一句错误,故屏蔽掉 //pd->fun();error C2660: 'fun' : function does not take 0 parameters pd->fun(1); delete pd; return 0; }
/*在不同的非命名空间作用域里的函数不构成重载,子类和父类是不同的两个作用域。
在本例中,两个函数在不同作用域中,故不够成重载,除非这个作用域是命名空间作用域。*/
在这个例子中,函数不是重载overload,也不是覆盖override,而是隐藏hide。
接下来的5个例子具体说明一下什么叫隐藏
例1
#include <iostream> using namespace std; class Basic{ public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base::fun(int i)" << endl;}//overload }; class Derive :public Basic{ public: void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun();//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。 d.fun(1);//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。 return 0; }
例2
#include <iostream> using namespace std; class Basic{ public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base::fun(int i)" << endl;}//overload }; class Derive :public Basic{ public: //新的函数版本,基类所有的重载版本都被屏蔽,在这里,我们称之为函数隐藏hide //派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。 void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;} void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun(1,2); //下面一句错误,故屏蔽掉 //d.fun();error C2660: 'fun' : function does not take 0 parameters return 0; }
例3
#include <iostream> using namespace std; class Basic{ public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base::fun(int i)" << endl;}//overload }; class Derive :public Basic{ public: //覆盖override基类的其中一个函数版本,同样基类所有的重载版本都被隐藏hide //派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。 void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun(); //下面一句错误,故屏蔽掉 //d.fun(1);error C2660: 'fun' : function does not take 1 parameters return 0; }
例4
#include <iostream> using namespace std; class Basic{ public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base::fun(int i)" << endl;}//overload }; class Derive :public Basic{ public: using Basic::fun; void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun();//正确 d.fun(1);//正确 return 0; } /* 输出结果 Derive::fun() Base::fun(int i) Press any key to continue */
例5
#include <iostream> using namespace std; class Basic{ public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base::fun(int i)" << endl;}//overload }; class Derive :public Basic{ public: using Basic::fun; void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;} void fun2(){cout << "Derive::fun2()" << endl;} }; int main() { Derive d; d.fun();//正确 d.fun(1);//正确 d.fun(1,2);//正确 return 0; } /* 输出结果 Base::fun() Base::fun(int i) Derive::fun(int i,int j) Press any key to continue */
好了,我们先来一个小小的总结重载与覆盖两者之间的特征
重载overload的特征:
n 相同的范围(在同一个类中);
n 函数名相同参数不同;
n virtual 关键字可有可无。
覆盖override是指派生类函数覆盖基类函数,覆盖的特征是:
n 不同的范围(分别位于派生类与基类);
n 函数名和参数都相同;
n 基类函数必须有virtual 关键字。(若没有virtual 关键字则称之为隐藏hide)
如果基类有某个函数的多个重载(overload)版本,而你在派生类中重写(override)了基类中的一个或多个函数版本,或是在派生类中重新添加了新的函数版本(函数名相同,参数不同),则所有基类的重载版本都被屏蔽,在这里我们称之为隐藏hide。所以,在一般情况下,你想在派生类中使用新的函数版本又想使用基类的函数版本时,你应该在派生类中重写基类中的所有重载版本。你若是不想重写基类的重载的函数版本,则你应该使用例4或例5方式,显式声明基类名字空间作用域。
事实上,C++编译器认为,相同函数名不同参数的函数之间根本没有什么关系,它们根本就是两个毫不相关的函数。只是C++语言为了模拟现实世界,为了让程序员更直观的思维处理现实世界中的问题,才引入了重载和覆盖的概念。重载是在相同名字空间作用域下,而覆盖则是在不同的名字空间作用域下,比如基类和派生类即为两个不同的名字空间作用域。在继承过程中,若发生派生类与基类函数同名问题时,便会发生基类函数的隐藏。当然,这里讨论的情况是基类函数前面没有virtual 关键字。在有virtual 关键字关键字时的情形我们另做讨论。
继承类重写了基类的某一函数版本,以产生自己功能的接口。此时C++编绎器认为,你现在既然要使用派生类的自己重新改写的接口,那我基类的接口就不提供给你了(当然你可以用显式声明名字空间作用域的方法,见[C++基础]重载、覆盖、多态与函数隐藏(1))。而不会理会你基类的接口是有重载特性的。若是你要在派生类里继续保持重载的特性,那你就自己再给出接口重载的特性吧。所以在派生类里,只要函数名一样,基类的函数版本就会被无情地屏蔽。在编绎器中,屏蔽是通过名字空间作用域实现的。
所以,在派生类中要保持基类的函数重载版本,就应该重写所有基类的重载版本。重载只在当前类中有效,继承会失去函数重载的特性。也就是说,要把基类的重载函数放在继承的派生类里,就必须重写。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,具体规则我们也来做一小结:
n 如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类无virtual关键字,基类的函数将被隐藏。(注意别与重载混淆,虽然函数名相同参数不同应称之为重载,但这里不能理解为重载,因为派生类和基类不在同一名字空间作用域内。这里理解为隐藏)
n 如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类有virtual关键字,基类的函数将被隐式继承到派生类的vtable中。此时派生类vtable中的函数指向基类版本的函数地址。同时这个新的函数版本添加到派生类中,作为派生类的重载版本。但在基类指针实现多态调用函数方法时,这个新的派生类函数版本将会被隐藏。
n 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。(注意别与覆盖混淆,这里理解为隐藏)。
n 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数有virtual关键字。此时,基类的函数不会被“隐藏”。(在这里,你要理解为覆盖哦^_^)。
插曲:基类函数前没有virtual关键字时,我们要重写更为顺口些,在有virtual关键字时,我们叫覆盖更为合理些,戒此,我也希望大家能够更好的理解C++中一些微妙的东西。费话少说,我们举例说明吧。
例6
#include <iostream> using namespace std; class Base{ public: virtual void fun() { cout << "Base::fun()" << endl; }//overload virtual void fun(int i) { cout << "Base::fun(int i)" << endl; }//overload }; class Derive : public Base{ public: void fun() { cout << "Derive::fun()" << endl; }//override void fun(int i) { cout << "Derive::fun(int i)" << endl; }//override void fun(int i,int j){ cout<< "Derive::fun(int i,int j)" <<endl;}//overload }; int main() { Base *pb = new Derive(); pb->fun(); pb->fun(1); //下面一句错误,故屏蔽掉 //pb->fun(1,2);virtual函数不能进行overload,error C2661: 'fun' : no overloaded function takes 2 parameters cout << endl; Derive *pd = new Derive(); pd->fun(); pd->fun(1); pd->fun(1,2);//overload delete pb; delete pd; return 0; } /*
输出结果
Derive::fun()
Derive::fun(int i)
Derive::fun()
Derive::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/
例7-1
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } }; class Derive : public Base{}; int main() { Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) delete pb; return 0; }
例7-2
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } }; class Derive : public Base{ public: void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) pb->fun((double)0.01);//Base::fun(int i) delete pb; return 0; }
例8-1
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } }; class Derive : public Base{ public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) delete pb; return 0; }
例8-2
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } }; class Derive : public Base{ public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl; } void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) delete pb; return 0; }
例9
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } }; class Derive : public Base{ public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl; } void fun(char c){ cout <<"Derive::fun(char c)"<< endl; } void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) pb->fun('a');//Derive::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) Derive *pd =new Derive(); pd->fun(1);//Derive::fun(int i) //overload pd->fun('a');//Derive::fun(char c) //overload pd->fun(0.01);//Derive::fun(double d) delete pb; delete pd; return 0; }
例7-1和例8-1很好理解,我把这两个例子放在这里,是让大家作一个比较摆了,也是为了帮助大家更好的理解:
n 例7-1中,派生类没有覆盖基类的虚函数,此时派生类的vtable中的函数指针指向的地址就是基类的虚函数地址。
n 例8-1中,派生类覆盖了基类的虚函数,此时派生类的vtable中的函数指针指向的地址就是派生类自己的重写的虚函数地址。
在例7-2和8-2看起来有点怪怪,其实,你按照上面的原则对比一下,答案也是明朗的:
n 例7-2中,我们为派生类重载了一个函数版本:void fun(double d) 其实,这只是一个障眼法。我们具体来分析一下,基类共有几个函数,派生类共有几个函数:
类型
基类
派生类
Vtable部分
void fun(int i)
指向基类版的虚函数void fun(int i)
静态部分
void fun(double d)
我们再来分析一下以下三句代码
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
这第一句是关键,基类指针指向派生类的对象,我们知道这是多态调用;接下来第二句,运行时基类指针根据运行时对象的类型,发现是派生类对象,所以首先到派生类的vtable中去查找派生类的虚函数版本,发现派生类没有覆盖基类的虚函数,派生类的vtable只是作了一个指向基类虚函数地址的一个指向,所以理所当然地去调用基类版本的虚函数。最后一句,程序运行仍然埋头去找派生类的vtable,发现根本没有这个版本的虚函数,只好回头调用自己的仅有一个虚函数。
这里还值得一提的是:如果此时基类有多个虚函数,此时程序编绎时会提示”调用不明确”。示例如下
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } virtual void fun(char c){ cout <<"Base::fun(char c)"<< endl; } }; class Derive : public Base{ public: void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(0.01);//error C2668: 'fun' : ambiguous call to overloaded function delete pb; return 0; }
好了,我们再来分析一下例8-2。
n 例8-2中,我们也为派生类重载了一个函数版本:void fun(double d) ,同时覆盖了基类的虚函数,我们再来具体来分析一下,基类共有几个函数,派生类共有几个函数:
Type
Classe de base
Classe dérivée
Partie Vtable
void fun(int i)
void fun(int i)
Partie statique
void fun(double d)
D'après le tableau, nous pouvons voir que le pointeur de fonction dans la vtable de la classe dérivée pointe vers sa propre adresse de fonction virtuelle réécrite.
Analysons les trois lignes de code suivantes
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i )
pb->fun((double)0.01);//Derive::fun(int i)
Il n'est pas nécessaire d'en dire plus sur la première phrase. Dans la deuxième phrase, c'est. est naturel d'appeler la fonction virtuelle de la classe dérivée, la troisième phrase, hé, ça fait encore bizarre. En fait, le programme C est très stupide lors de l'exécution, je me suis plongé dans la table vtable de la classe dérivée. et j'y ai jeté un œil. Bon sang, je n'y ai même pas pensé. Je n'arrive vraiment pas à trouver la version que je veux. Pourquoi les pointeurs de la classe de base ne font-ils pas le tour et la recherchent ? que ma vue est limitée. La classe de base est tellement ancienne. Elle doit être presbyte. Ses yeux ne peuvent voir que la partie non-Vtable (c'est-à-dire la partie statique) et la partie Vtable que vous souhaitez gérer. le double d) de la classe dérivée est si loin que vous ne pouvez pas le voir ! De plus, la classe dérivée doit s'occuper de tout. N'avez-vous pas vos propres droits ? en discutant, chacun a ses propres affaires ^_^
Hélas ! Allez-vous soupirer ? Les pointeurs de classe de base peuvent effectuer des appels polymorphes, mais les appels de classes dérivées ne peuvent jamais être effectués (voir l'exemple 6) ~~. ~
Regardons à nouveau l'exemple 9
L'effet de cet exemple est le même que celui de l'exemple 6, avec la même approche mais le même objectif. Je crois qu'après avoir compris les exemples ci-dessus, c'est aussi un petit baiser.
Résumé :
La surcharge sélectionne la version de la fonction à appeler en fonction de la liste des paramètres de la fonction, tandis que le polymorphisme sélectionne la version de la fonction virtuelle à appeler en fonction du type réel de l'objet d'exécution. d'état est obtenu en remplaçant la fonction virtuelle virtuelle de la classe de base par la classe dérivée. Si la classe dérivée ne remplace pas la fonction virtuelle virtuelle de la classe de base, la classe dérivée héritera automatiquement de la version de la fonction virtuelle virtuelle de la classe de base. . À ce stade, peu importe si l'objet pointé par le pointeur de classe de base est un type de base ou un type dérivé, la fonction virtuelle virtuelle de la version de la classe de base sera appelée si la classe dérivée remplace la fonction virtuelle virtuelle de ; la classe de base, elle sera appelée au moment de l'exécution en fonction de l'objet. Le type réel est utilisé pour sélectionner la version de la fonction virtuelle virtuelle à appeler. Par exemple, si le type d'objet pointé par le pointeur de classe de base est un type dérivé, le La version virtuelle de la fonction virtuelle de la classe dérivée sera appelée, réalisant ainsi le polymorphisme.
L'intention initiale de l'utilisation du polymorphisme est de déclarer la fonction comme virtuelle dans la classe de base et de remplacer la version de fonction virtuelle virtuelle de la classe de base de remplacement dans la classe dérivée. Notez que le prototype de fonction et la classe de base. pour le moment, gardez-le cohérent, c'est-à-dire le même nom et le même type de paramètre ; si vous ajoutez une nouvelle version de fonction à la classe dérivée, vous ne pouvez pas appeler dynamiquement la nouvelle version de fonction de la classe dérivée via le pointeur de classe de base. version sert uniquement de version surchargée de la classe dérivée. Toujours la même phrase, la surcharge n'est valable que dans la classe actuelle. Que vous la surchargez dans une classe de base ou une classe dérivée, les deux ne sont pas liées l'une à l'autre. Si nous comprenons cela, nous pouvons également comprendre avec succès les résultats des exemples 6 et 9.
La surcharge est liée statiquement et le polymorphisme est lié dynamiquement. Pour expliquer plus en détail, la surcharge n'a rien à voir avec le type d'objet vers lequel pointe réellement le pointeur, et le polymorphisme est lié au type d'objet vers lequel pointe réellement le pointeur. Si le pointeur de classe de base appelle la version surchargée de la classe dérivée, le compilateur C le considère comme illégal. Le compilateur C pense uniquement que le pointeur de classe de base ne peut appeler que la version surchargée de la classe de base et que la surcharge ne fonctionne que dans le nom. Espace de la classe actuelle.Valable dans le domaine, l'héritage perdra la fonctionnalité de surcharge. Bien entendu, si le pointeur de classe de base appelle une fonction virtuelle à ce moment-là, il sélectionnera également dynamiquement la version de la fonction virtuelle de la classe de base ou de la classe actuelle. version de fonction virtuelle virtuelle de la classe dérivée. Pour effectuer des opérations spécifiques, cela est déterminé par le type d'objet vers lequel pointe réellement le pointeur de la classe de base, donc la surcharge n'a rien à voir avec le type d'objet vers lequel pointe réellement le pointeur, et le polymorphisme est lié au type d'objet vers lequel le pointeur pointe réellement.
Enfin, pour clarifier, les fonctions virtuelles virtuelles peuvent également être surchargées, mais la surcharge ne peut être valable que dans le cadre de l'espace de noms actuel
Combien d'objets String ont été créés ?
Regardons d'abord un morceau de code :
Code Java
String str=new String("abc");
Ce qui suit ce code est souvent la question, c'est-à-dire, qu'est-ce que c'est exactement ligne de code ? Combien d’objets String ont été créés ? Je pense que tout le monde connaît cette question et la réponse est bien connue, 2. Ensuite, nous partirons de cette question et passerons en revue certaines connaissances JAVA liées à la création d'objets String.
我们可以把上面这行代码分成String str、=、"abc"和new String()四部分来看待。String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给它,显然也没有创建对象;现在只剩下new String("abc")了。那么,new String("abc")为什么又能被看成"abc"和new String()呢?我们来看一下被我们调用了的String的构造器:
Java代码
public String(String original) {
//other code ...
}
大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种:
使用new创建对象。
调用Class类的newInstance方法,利用反射机制创建对象。
我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。由此我们又要引入另外一种创建String对象的方式的讨论——引号内包含文本。
这种方式是String特有的,并且它与new的方式存在很大区别。
Java代码
String str="abc";
毫无疑问,这行代码创建了一个String对象。
Java代码
String a="abc";
String b="abc";
那这里呢?答案还是一个。
Java代码
String a="ab"+"cd";
再看看这里呢?答案仍是一个。有点奇怪吗?说到这里,我们就需要引入对字符串池相关知识的回顾了。
在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
我们再回头看看String a="abc";,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。因此,我们不难理解前面三个例子中头两个例子为什么是这个答案了。
对于第三个例子:
Java代码
String a="ab"+"cd";
由于常量的值在编译的时候就被确定了。在这里,"ab"和"cd"都是常量,因此变量a的值在编译时就可以确定。这行代码编译后的效果等同于:
Java代码
String a="abcd";
因此这里只创建了一个对象"abcd",并且它被保存在字符串池里了。
现在问题又来了,是不是所有经过“+”连接后得到的字符串都会被添加到字符串池中呢?我们都知道“==”可以用来比较两个变量,它有以下两种情况:
如果比较的是两个基本类型(char,byte,short,int,long,float,double,boolean),则是判断它们的值是否相等。
如果表较的是两个对象变量,则是判断它们的引用是否指向同一个对象。
下面我们就用“==”来做几个测试。为了便于说明,我们把指向字符串池中已经存在的对象也视为该对象被加入了字符串池:
Java代码
public class StringTest { public static void main(String[] args) { String a = "ab";// 创建了一个对象,并加入字符串池中 System.out.println("String a = \"ab\";"); String b = "cd";// 创建了一个对象,并加入字符串池中 System.out.println("String b = \"cd\";"); String c = "abcd";// 创建了一个对象,并加入字符串池中 String d = "ab" + "cd"; // 如果d和c指向了同一个对象,则说明d也被加入了字符串池 if (d == c) { System.out.println("\"ab\"+\"cd\" 创建的对象 \"加入了\" 字符串池中"); } // 如果d和c没有指向了同一个对象,则说明d没有被加入字符串池 else { System.out.println("\"ab\"+\"cd\" 创建的对象 \"没加入\" 字符串池中"); } String e = a + "cd"; // 如果e和c指向了同一个对象,则说明e也被加入了字符串池 if (e == c) { System.out.println(" a +\"cd\" 创建的对象 \"加入了\" 字符串池中"); } // 如果e和c没有指向了同一个对象,则说明e没有被加入字符串池 else { System.out.println(" a +\"cd\" 创建的对象 \"没加入\" 字符串池中"); } String f = "ab" + b; // 如果f和c指向了同一个对象,则说明f也被加入了字符串池 if (f == c) { System.out.println("\"ab\"+ b 创建的对象 \"加入了\" 字符串池中"); } // 如果f和c没有指向了同一个对象,则说明f没有被加入字符串池 else { System.out.println("\"ab\"+ b 创建的对象 \"没加入\" 字符串池中"); } String g = a + b; // 如果g和c指向了同一个对象,则说明g也被加入了字符串池 if (g == c) { System.out.println(" a + b 创建的对象 \"加入了\" 字符串池中"); } // 如果g和c没有指向了同一个对象,则说明g没有被加入字符串池 else { System.out.println(" a + b 创建的对象 \"没加入\" 字符串池中"); } } }
运行结果如下:
String a = "ab";
String b = "cd";
"ab"+"cd" 创建的对象 "加入了" 字符串池中
a +"cd" 创建的对象 "没加入" 字符串池中
"ab"+ b 创建的对象 "没加入" 字符串池中
a + b 创建的对象 "没加入" 字符串池中
从上面的结果中我们不难看出,只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,对此我们不再赘述。
但是有一种情况需要引起我们的注意。请看下面的代码:
Java代码
public class StringStaticTest { // 常量A public static final String A = "ab"; // 常量B public static final String B = "cd"; public static void main(String[] args) { // 将两个常量用+连接对s进行初始化 String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s等于t,它们是同一个对象"); } else { System.out.println("s不等于t,它们不是同一个对象"); } } }
这段代码的运行结果如下:
s等于t,它们是同一个对象
这又是为什么呢?原因是这样的,对于常量来讲,它的值是固定的,因此在编译期就能被确定了,而变量的值只有到运行时才能被确定,因为这个变量可以被不同的方法调用,从而可能引起值的改变。在上面的例子中,A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:
Java代码
String s=A+B;
等同于:
Java代码
String s="ab"+"cd";
我对上面的例子稍加改变看看会出现什么情况:
Java代码
public class StringStaticTest { // 常量A public static final String A; // 常量B public static final String B; static { A = "ab"; B = "cd"; } public static void main(String[] args) { // 将两个常量用+连接对s进行初始化 String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s等于t,它们是同一个对象"); } else { System.out.println("s不等于t,它们不是同一个对象"); } } }
它的运行结果是这样:
s不等于t,它们不是同一个对象
只是做了一点改动,结果就和刚刚的例子恰好相反。我们再来分析一下。A和B虽然被定义为常量(只能被赋值一次),但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。
由于字符串池中对象的共享能够带来效率的提高,因此我们提倡大家用引号包含文本的方式来创建String对象,实际上这也是我们在编程中常采用的。
接下来我们再来看看intern()方法,它的定义如下:
Java代码
public native String intern();
这是一个本地方法。在调用这个方法时,JAVA虚拟机首先检查字符串池中是否已经存在与该对象值相等对象存在,如果有则返回字符串池中对象的引用;如果没有,则先在字符串池中创建一个相同值的String对象,然后再将它的引用返回。
我们来看这段代码:
Java代码
public class StringInternTest { public static void main(String[] args) { // 使用char数组来初始化a,避免在a被创建之前字符串池中已经存在了值为"abcd"的对象 String a = new String(new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern(); if (b == a) { System.out.println("b被加入了字符串池中,没有新建对象"); } else { System.out.println("b没被加入字符串池中,新建了对象"); } } }
运行结果:
b没被加入字符串池中,新建了对象
如果String类的intern()方法在没有找到相同值的对象时,是把当前对象加入字符串池中,然后返回它的引用的话,那么b和a指向的就是同一个对象;否则b指向的对象就是JAVA虚拟机在字符串池中新建的,只是它的值与a相同罢了。上面这段代码的运行结果恰恰印证了这一点。
最后我们再来说说String对象在JAVA虚拟机(JVM)中的存储,以及字符串池与堆(heap)和栈(stack)的关系。我们首先回顾一下堆和栈的区别:
栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据可以共享,速度仅次于寄存器(register),快于堆。
堆(heap):用于存储对象。
我们查看String类的源码就会发现,它有一个value属性,保存着String对象的值,类型是char[],这也正说明了字符串就是字符的序列。
当执行String a="abc";时,JAVA虚拟机会在栈中创建三个char型的值'a'、'b'和'c',然后在堆中创建一个String对象,它的值(value)是刚才在栈中创建的三个char型值组成的数组{'a','b','c'},最后这个新创建的String对象会被添加到字符串池中。如果我们接着执行String b=new String("abc");代码,由于"abc"已经被创建并保存于字符串池中,因此JAVA虚拟机只会在堆中新创建一个String对象,但是它的值(value)是共享前一行代码执行时在栈中创建的三个char型值值'a'、'b'和'c'。
说到这里,我们对于篇首提出的String str=new String("abc")为什么是创建了两个对象这个问题就已经相当明了了。
更多Java中继承、多态、重载和重写介绍相关文章请关注PHP中文网!