O que é polimorfismo? Qual é o seu mecanismo de implementação? Qual é a diferença entre sobrecarregar e reescrever? Estes são os quatro conceitos muito importantes que revisaremos desta vez: herança, polimorfismo, sobrecarga e sobrescrição.
herança
Simplificando, herança consiste em gerar um novo tipo baseado em um tipo existente adicionando novos métodos ou redefinindo métodos existentes (conforme discutido abaixo, esse método é chamado de reescrita). Herança é uma das três características básicas da orientação a objetos - encapsulamento, herança e polimorfismo. Cada classe que escrevemos ao usar JAVA é herdada, porque na linguagem JAVA, a classe java.lang.Object é a classe base mais fundamental (). ou classe pai ou superclasse) de todas as classes Se uma classe recém-definida que definimos não especificar explicitamente de qual classe base ela herda, então o JAVA será padronizado para ela herdar da classe Object.
Podemos dividir as classes em JAVA nos três tipos a seguir:
Classe: Uma classe definida usando classe e não contém métodos abstratos.
Classe abstrata: Uma classe definida usando classe abstrata, que pode ou não conter métodos abstratos.
Interface: Uma classe definida usando interface.
As seguintes regras de herança existem entre esses três tipos:
As classes podem estender classes, abstrair classes e implementar interfaces.
Classes abstratas podem herdar (estender) classes, podem herdar (estender) classes abstratas e podem herdar (implementar) interfaces.
As interfaces só podem estender interfaces.
Observe que as diferentes palavras-chave extends e implements usadas em cada caso de herança nas três regras acima não podem ser substituídas à vontade. Como todos sabemos, depois que uma classe comum herda uma interface, ela deve implementar todos os métodos definidos nesta interface, caso contrário, ela só poderá ser definida como uma classe abstrata. A razão pela qual não uso o termo "implementação" para a palavra-chave implements aqui é porque conceitualmente ela também representa um relacionamento de herança e, no caso da classe abstrata implements interface, ela não precisa implementar esta definição de interface. método, então é mais razoável usar herança.
As três regras acima também obedecem às seguintes restrições:
Tanto as classes quanto as classes abstratas só podem herdar no máximo uma classe, ou no máximo uma classe abstrata, e essas duas situações são mutuamente exclusivas, ou seja, ou herdam uma classe ou uma classe abstrata.
Quando classes, classes abstratas e interfaces herdam interfaces, elas não são restritas pelo número, elas podem herdar um número ilimitado de interfaces. É claro que, para uma classe, ela deve implementar todos os métodos definidos em todas as interfaces que herda.
Quando uma classe abstrata herda uma classe abstrata ou implementa uma interface, ela pode não implementar parcial, completamente ou completamente os métodos abstratos da classe abstrata pai ou as interfaces definidas na interface da classe pai.
Quando uma classe herda uma classe abstrata ou implementa uma interface, ela deve implementar todos os métodos abstratos da classe abstrata pai ou todas as interfaces definidas na interface da classe pai.
O benefício que a herança traz para a nossa programação é a reutilização (reutilização) das classes originais. Assim como a reutilização de módulos, a reutilização de classes pode melhorar a eficiência do nosso desenvolvimento. Na verdade, a reutilização de módulos é o efeito sobreposto da reutilização de um grande número de classes. Além da herança, também podemos usar composição para reutilizar classes. A chamada combinação consiste em definir a classe original como um atributo da nova classe e conseguir a reutilização chamando os métodos da classe original na nova classe. Se não houver relação incluída entre o tipo recém-definido e o tipo original, ou seja, a partir de um conceito abstrato, as coisas representadas pelo tipo recém-definido não são uma das coisas representadas pelo tipo original, como pessoas amarelas É um tipo de ser humano, e existe uma relação entre eles, incluir e ser incluído, então neste momento a combinação é a melhor escolha para conseguir o reaproveitamento. O exemplo a seguir é um exemplo simples da combinação:
public class Sub { private Parent p = new Parent(); public void doSomething() { // Reutilize o método p.method() da classe Parent } } class Parent { public void method() { / /faço alguma coisa aqui } }
Claro, para tornar o código mais eficiente, também podemos inicializá-lo quando precisarmos usar o tipo original (como Parent p).
Usar herança e combinação para reutilizar classes originais é um modelo de desenvolvimento incremental. A vantagem desse método é que não há necessidade de modificar o código original, portanto não trará novos bugs ao código original e não há necessidade de fazê-lo. teste novamente devido a modificações no código original, o que é obviamente benéfico para o nosso desenvolvimento. Portanto, se estivermos mantendo ou transformando um sistema ou módulo original, principalmente quando não temos um conhecimento profundo deles, podemos optar pelo modelo de desenvolvimento incremental, que pode não só melhorar muito nossa eficiência de desenvolvimento, mas também evitar riscos causados por modificações no código original.
Polimorfismo
Polimorfismo é outro conceito básico importante. Conforme mencionado acima, é uma das três características básicas da orientação a objetos. O que exatamente é polimorfismo? Vejamos primeiro o exemplo a seguir para ajudar a entender:
//Interface de interface do carro Car { // Nome do carro String getName(); // Obtém o preço do carro int getPrice() } // Classe BMW BMW implementa Car { public String getName() { return "BMW" } public int; getPrice() { return 300000; } } // classe CheryQQ CheryQQ implementa Car { public String getName() { return "CheryQQ" } public int getPrice(); { return 20000; } } // Loja de vendas de carros public class CarShop { // Receita de vendas de carros private int money = 0; // Vender um carro public void sellCar(Car car) { System.out.println("Modelo de carro: " + car.getName() + " Preço unitário: " + car.getPrice()); // Aumenta a receita do dinheiro das vendas de carros += car.getPrice() } // Receita total das vendas de carros public int getMoney() { return money; } public static void main(String[] args) { CarShop aShop = new CarShop(); // Vender um BMW aShop.sellCar(new BMW()); .sellCar(new CheryQQ()); System.out.println("Receita total: " + aShop.getMoney());
Resultados em execução:
Modelo: BMW Preço unitário: 300.000
Modelo: CheryQQ Preço unitário: 20.000
Receita total: 320.000
A herança é a base para a realização do polimorfismo. Literalmente entendido, o polimorfismo é um tipo (ambos do tipo Carro) que mostra vários estados (o nome da BMW é BMW e o preço é 300.000; o nome da Chery é CheryQQ e o preço é 2.000). Associar uma chamada de método ao sujeito (ou seja, ao objeto ou classe) ao qual o método pertence é chamado de ligação, que é dividida em dois tipos: ligação antecipada e ligação tardia. Suas definições são explicadas abaixo:
Vinculação antecipada: vinculação antes da execução do programa, implementada pelo compilador e vinculador, também chamada de vinculação estática. Por exemplo, métodos estáticos e métodos finais Observe que os métodos privados também estão incluídos aqui porque são implicitamente finais.
Ligação tardia: ligação de acordo com o tipo do objeto em tempo de execução, implementada pelo mecanismo de chamada de método, por isso também é chamada de ligação dinâmica ou ligação em tempo de execução. Todos os métodos, exceto a vinculação antecipada, são vinculativos tardios.
O polimorfismo é implementado no mecanismo de ligação tardia. O benefício que o polimorfismo nos traz é que ele elimina a relação de acoplamento entre classes e facilita a expansão do programa. Por exemplo, no exemplo acima, para adicionar um novo tipo de carro para venda, você só precisa deixar a classe recém-definida herdar a classe Car e implementar todos os seus métodos sem fazer nenhuma modificação no código original. ) do método da classe CarShop pode lidar com novos modelos de carros. O novo código é o seguinte:
// Santana Car class Santana implements Car { public String getName() { return "Santana" } public int getPrice() { return 80000;
Sobrecarga e substituição
Sobrecarga e reescrita são conceitos para métodos Antes de esclarecer esses dois conceitos, vamos primeiro entender qual é a estrutura do método (o nome em inglês é assinatura, alguns são traduzidos como “assinatura”, embora seja usado de forma mais ampla, mas esta tradução não é. preciso). Estrutura refere-se à estrutura de composição de um método, incluindo especificamente o nome e os parâmetros do método, cobrindo o número, tipo e ordem de aparecimento dos parâmetros, mas não inclui o tipo de valor de retorno do método, modificadores de acesso e modificações como símbolo abstrato, estático e final. Por exemplo, os dois métodos a seguir têm a mesma estrutura:
método public void (int i, String s) { // faça algo } método public String (int i, String s) { // faça algo }
Esses dois são métodos com configurações diferentes:
método public void (int i, String s) { // faça algo } método public void (String s, int i) { // faça algo }
Depois de entender o conceito de Gestalt, vamos dar uma olhada em sobrecarga e reescrita. Veja suas definições:
Substituição, o nome em inglês é substituição, significa que no caso de herança, um novo método com a mesma estrutura do método da classe base é definido na subclasse, que é chamada de subclasse que substitui o método da classe base. Este é um passo necessário para alcançar o polimorfismo.
Sobrecarga, o nome em inglês é sobrecarga, refere-se à definição de mais de um método com o mesmo nome, mas com estruturas diferentes na mesma classe. Na mesma classe não é permitido definir mais de um método do mesmo tipo.
Vamos considerar uma questão interessante: os construtores podem ficar sobrecarregados? A resposta é sim, claro, muitas vezes fazemos isso na programação real. Na verdade, o construtor também é um método. O nome do construtor é o nome do método, os parâmetros do construtor são os parâmetros do método e seu valor de retorno é uma instância da classe recém-criada. Entretanto, o construtor não pode ser substituído por uma subclasse, porque uma subclasse não pode definir um construtor com o mesmo tipo da classe base.
Sobrecarga, substituição, polimorfismo e ocultação de função
Muitas vezes é visto que alguns iniciantes em C++ têm uma vaga compreensão de sobrecarga, substituição, polimorfismo e ocultação de funções. Escreverei algumas de minhas opiniões aqui, na esperança de ajudar os iniciantes em C++ a esclarecer suas dúvidas.
Antes de compreendermos a relação complexa e sutil entre sobrecarga, substituição, polimorfismo e ocultação de função, devemos primeiro revisar conceitos básicos como sobrecarga e cobertura.
Primeiro, vejamos um exemplo muito simples para entender o que é ocultação de função.
#include <iostream>usando 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; : 'fun': a função não aceita 0 parâmetros d.fun(1); Derive *pd =new Derive(); //A frase a seguir está errada, então está bloqueada //pd->fun();erro C2660: 'fun': a função não aceita 0 parâmetros pd->fun(1); delete pd;}
/*Funções em escopos diferentes de namespace não constituem sobrecarga. Subclasses e classes pai são dois escopos diferentes.
Neste exemplo, as duas funções estão em escopos diferentes, portanto não ficam sobrecarregadas, a menos que o escopo seja um escopo de namespace. */
Neste exemplo, a função não está sobrecarregada ou substituída, mas oculta.
Os próximos cinco exemplos explicam especificamente o que é esconder-se.
Exemplo 1
#include <iostream>usando 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();//Correto, a classe derivada não possui uma declaração de função com o mesmo nome da classe base, então todas as funções sobrecarregadas com o mesmo nome em a classe base será usada como funções candidatas. d.fun(1);//Correto, a classe derivada não possui uma declaração de função com o mesmo nome da classe base, então todas as funções sobrecarregadas com o mesmo nome na classe base serão usadas como funções candidatas. retornar 0;}
Exemplo 2
#include <iostream>usando 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: //Nova versão da função, todas as versões sobrecarregadas da classe base são bloqueadas, aqui chamamos isso de função hide hide //Se houver uma declaração de uma função com o mesmo nome da classe base na classe derivada, a função com o mesmo nome na classe base não será usada como função candidata, mesmo que a classe base tenha múltiplas versões de funções sobrecarregadas com diferentes listas de parâmetros. 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); //A frase a seguir está errada, então está bloqueada //d.fun();erro C2660: 'fun' : function does não aceita 0 parâmetros retornar 0;}
Exemplo 3
#include <iostream>usando 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: //Substitui uma das versões de função da classe base de substituição. Da mesma forma, todas as versões sobrecarregadas da classe base são escondido. //Se houver uma declaração de uma função com o mesmo nome da classe base na classe derivada, a função com o mesmo nome na classe base não será usada como função candidata, mesmo que a classe base tenha múltiplas versões de funções sobrecarregadas com diferentes listas de parâmetros. void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Deriva d; d.fun(); //A frase a seguir está errada, então está bloqueada //d.fun(1);erro C2660: 'fun' : a função não aceita 1 parâmetro return 0;}
Exemplo 4
#include <iostream>usando 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();//Corrigir d.fun(1); //Retorno correto 0;}/*Resultado de saída Derive::fun()Base::fun(int i)Pressione qualquer tecla para continuar*/
Exemplo 5
#include <iostream>usando 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; .fun();//Corrigir d.fun(1);//Corrigir d.fun(1,2);//Retorno correto 0;}/*Resultado de saída Base::fun()Base::fun(int i)Derive::fun(int i,int j)Pressione qualquer tecla para continuar*/
Ok, vamos primeiro fazer um pequeno resumo das características entre sobrecarga e sobrescrição.
Características de sobrecarga:
n o mesmo escopo (na mesma classe);
n O nome da função é o mesmo, mas os parâmetros são diferentes;
n A palavra-chave virtual é opcional.
Substituir significa que uma função de classe derivada cobre uma função de classe base. As características de substituição são:
n escopos diferentes (localizados em classes derivadas e classes base respectivamente);
n O nome da função e os parâmetros são iguais;
n As funções da classe base devem ter a palavra-chave virtual. (Se não houver palavra-chave virtual, é chamado de ocultação oculta)
Se a classe base tiver várias versões sobrecarregadas de uma função e você substituir (substituir) uma ou mais versões de função na classe base na classe derivada ou adicionar novas versões da função na classe derivada (mesmo nome de função, parâmetros diferentes) , todas as versões sobrecarregadas da classe base serão bloqueadas, o que chamamos de ocultas aqui. Portanto, em geral, quando você deseja usar uma nova versão de função em uma classe derivada e deseja usar a versão de função da classe base, você deve substituir todas as versões sobrecarregadas da classe base na classe derivada. Se você não deseja substituir a versão da função sobrecarregada da classe base, você deve usar o Exemplo 4 ou o Exemplo 5 para declarar explicitamente o escopo do namespace da classe base.
Na verdade, o compilador C++ acredita que não há relacionamento entre funções com o mesmo nome de função e parâmetros diferentes. São simplesmente duas funções não relacionadas. Acontece que a linguagem C++ introduziu os conceitos de sobrecarga e substituição para simular o mundo real e permitir que os programadores lidem com problemas do mundo real de forma mais intuitiva. A sobrecarga está no mesmo escopo de namespace, enquanto a substituição está em escopos de namespace diferentes. Por exemplo, a classe base e a classe derivada são dois escopos de namespace diferentes. Durante o processo de herança, se uma classe derivada tiver o mesmo nome de uma função da classe base, a função da classe base ficará oculta. Claro, a situação discutida aqui é que não há palavra-chave virtual na frente da função da classe base. Discutiremos a situação quando houver uma palavra-chave virtual separadamente.
Uma classe herdada substitui uma versão funcional da classe base para criar sua própria interface funcional. Neste momento, o compilador C++ pensa que, como agora você deseja usar a interface reescrita da classe derivada, a interface da minha classe base não será fornecida a você (é claro, você pode usar o método de declaração explícita do escopo do namespace, consulte [Noções básicas de C++] Sobrecarga, substituição, polimorfismo e ocultação de função (1)). Ele ignora que a interface da sua classe base possui características de sobrecarga. Se você quiser continuar mantendo o recurso de sobrecarga na classe derivada, forneça você mesmo o recurso de sobrecarga da interface. Portanto, em uma classe derivada, desde que o nome da função seja o mesmo, a versão da função da classe base será implacavelmente bloqueada. No compilador, o mascaramento é implementado através do escopo do namespace.
Portanto, para manter a versão sobrecarregada da função da classe base na classe derivada, você deve substituir todas as versões sobrecarregadas da classe base. A sobrecarga só é válida na classe atual e a herança perderá as características de sobrecarga de função. Em outras palavras, se você quiser colocar a função sobrecarregada da classe base na classe derivada herdada, deverá reescrevê-la.
"Oculto" aqui significa que a função da classe derivada bloqueia a função da classe base com o mesmo nome. Façamos também um breve resumo das regras específicas:
n Se a função da classe derivada tiver o mesmo nome da função da classe base, mas os parâmetros forem diferentes. Neste momento, se a classe base não tiver a palavra-chave virtual, as funções da classe base ficarão ocultas. (Tenha cuidado para não confundir com sobrecarga. Embora a função com o mesmo nome e parâmetros diferentes deva ser chamada de sobrecarga, ela não pode ser entendida como sobrecarga aqui porque a classe derivada e a classe base não estão no mesmo escopo de namespace. Isto é entendido como esconderijo)
n Se a função da classe derivada tiver o mesmo nome da função da classe base, mas os parâmetros forem diferentes. Neste momento, se a classe base tiver a palavra-chave virtual, as funções da classe base serão herdadas implicitamente na vtable da classe derivada. Neste momento, a função na classe derivada vtable aponta para o endereço da função da versão da classe base. Ao mesmo tempo, esta nova versão da função é adicionada à classe derivada como uma versão sobrecarregada da classe derivada. Mas quando o ponteiro da classe base implementa o método de função de chamada polimórfica, esta nova versão da função de classe derivada ficará oculta.
n Se a função da classe derivada tiver o mesmo nome que a função da classe base e os parâmetros também forem os mesmos, mas a função da classe base não tiver a palavra-chave virtual. Neste momento, as funções da classe base estão ocultas. (Cuidado para não confundir com cobertura, que aqui se entende como ocultação).
n Se a função da classe derivada tiver o mesmo nome que a função da classe base e os parâmetros também forem os mesmos, mas a função da classe base tiver a palavra-chave virtual. Neste momento, as funções da classe base não estarão "ocultas". (Aqui você tem que entender isso como cobertura ^_^).
Interlúdio: Quando não há palavra-chave virtual na frente da função da classe base, precisamos reescrevê-la de maneira mais suave. Quando há a palavra-chave virtual, é mais razoável chamá-la de substituição. todos podem entender melhor C++. Sem mais delongas, vamos ilustrar com um exemplo.
Exemplo 6
#include <iostream>usando namespace std; class Base{public: virtual void fun() { cout << "Base::fun()" << endl }//sobrecarga virtual void fun(int i) { cout << "Base::fun(int i)" << endl }//overload}; class Deriva : public Base{public: void fun() { cout << "Derive::fun()" << endl; }//substituir void fun(int i) { cout << "Derive::fun(int i)" << endl }//substituir void fun(int i,int j){ cout<< "Derive::fun (int i,int j)" <<endl;}//sobrecarga}; int main(){ Base *pb = new Derive(); pb->fun(); pb->fun(1); //A frase a seguir está errada, então está bloqueada //pb->fun(1,2); a função virtual não pode ser sobrecarregada, erro C2661: 'fun' : nenhuma função sobrecarregada leva 2 parâmetros cout << endl * pd = new Derive();
Resultados de saída
Deriva::diversão()
Derive::fun(int i)
Deriva::diversão()
Derive::fun(int i)
Deriva::fun(int i,int j)
Pressione qualquer tecla para continuar
*/
Exemplo 7-1
#include <iostream> usando namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< class Derive : public Base{}; int main(){ Base *pb = new pb->fun(1);//Base::fun(int i) delete pb;}
Exemplo 7-2
#include <iostream> usando namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< 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;}
Exemplo 8-1
#include <iostream> usando namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< class Derive : public Base{public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl }}; pb->fun(1);//Deriva::fun(int i) delete pb;
Exemplo 8-2
#include <iostream> usando namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< 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);//Deriva::fun(int i) delete pb; return 0;}
Exemplo 9
#include <iostream> usando 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 pb->fun(1); //Derivar::fun(int i) pb->fun('a');//Derivar::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) Derive *pd =new Derive();//Derive::fun(int i) // sobrecarga pd->fun('a');//Derive::fun(char c) //sobrecarga pd->fun(0.01);//Derive::fun(double d) excluir pb; excluir pd;
Os exemplos 7-1 e 8-1 são fáceis de entender. Coloquei esses dois exemplos aqui para todos fazerem uma comparação e ajudar todos a entenderem melhor:
n No Exemplo 7-1, a classe derivada não cobre a função virtual da classe base. Neste momento, o endereço apontado pelo ponteiro de função na tabela v da classe derivada é o endereço da função virtual da classe base.
n No Exemplo 8-1, a classe derivada substitui a função virtual da classe base. Neste momento, o endereço apontado pelo ponteiro de função na tabela v da classe derivada é o endereço da própria função virtual substituída da classe derivada.
Os exemplos 7-2 e 8-2 parecem um pouco estranhos. Na verdade, se você compará-los de acordo com os princípios acima, a resposta será clara:
n No Exemplo 7-2, sobrecarregamos uma versão de função para a classe derivada: void fun(double d) Na verdade, isso é apenas um encobrimento. Vamos analisar especificamente. Existem diversas funções na classe base e diversas funções na classe derivada:
tipo classe base classe derivada
Seção Vtable
diversão vazia (int i)
Aponta para a versão da classe base da função virtual void fun(int i)
parte estática
diversão vazia (duplo d)
Vamos analisar as três linhas de código a seguir novamente:
Base *pb = new Deriva();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
A primeira frase é a chave. O ponteiro da classe base aponta para o objeto da classe derivada. Sabemos que esta é uma chamada polimórfica. o tipo do objeto de tempo de execução, então primeiro vá para a vtable da classe derivada para encontrar a versão da função virtual da classe derivada. Verifica-se que a classe derivada não cobre a função virtual da classe base. A classe derivada apenas aponta para o endereço da função virtual da classe base, portanto é natural chamar a versão da classe base da função virtual. Na última frase, o programa ainda procura a vtable da classe derivada e descobre que não existe nenhuma função virtual desta versão, então ele tem que voltar e chamar sua própria função virtual.
Também vale a pena mencionar aqui que se a classe base tiver múltiplas funções virtuais neste momento, o programa solicitará "Chamada pouco clara" ao compilar o programa. Os exemplos são os seguintes
#include <iostream> usando namespace std; classe 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 Deriva : public Base{public: void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(0.01);//erro C2668: 'fun' : chamada ambígua para função sobrecarregada delete pb; return 0;}
Ok, vamos analisar o Exemplo 8-2 novamente.
n No Exemplo 8-2, também sobrecarregamos uma versão de função para a classe derivada: void fun(double d) e também cobrimos a função virtual da classe base. Vamos analisá-la em detalhes. , e a classe derivada possui diversas funções. A classe possui diversas funções:
tipo classe base classe derivada
Seção Vtable
diversão vazia (int i)
diversão vazia (int i)
parte estática
diversão vazia (duplo d)
Na tabela, podemos ver que o ponteiro de função na vtable da classe derivada aponta para seu próprio endereço de função virtual substituído.
Vamos analisar as três linhas de código a seguir novamente:
Base *pb = new Deriva();
pb->fun(1);//Deriva::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
Não há necessidade de dizer mais sobre a primeira frase. A segunda frase é chamar a versão da função virtual da classe derivada, é claro. A terceira frase, ei, parece estranho novamente. estúpido. Ao executar, eles mergulham na tabela vtable da classe derivada, apenas olhei para ela e percebi que não havia nenhuma versão que eu realmente não conseguisse descobrir. , por que os ponteiros da classe base não olham em volta e procuram por ela? Haha, acontece que a visão é limitada. A classe base é tão antiga que deve ser presbiopia. Parte Vtable (ou seja, a parte estática) e a parte Vtable que você deseja gerenciar, o vazio da classe derivada fun(double d) está tão longe que você não consegue ver. Além disso, a classe derivada tem que cuidar de tudo, a classe derivada não tem algum poder próprio? cuidem-se ^_^
Infelizmente, você vai suspirar? O ponteiro da classe base pode fazer chamadas polimórficas, mas nunca pode fazer chamadas sobrecarregadas para classes derivadas (consulte o Exemplo 6) ~~~
Vejamos o Exemplo 9 novamente,
O efeito deste exemplo é o mesmo do Exemplo 6, com o mesmo propósito. Acredito que depois de você entender os exemplos acima, isso também é um beijinho.
resumo:
A sobrecarga seleciona a versão da função a ser chamada com base na lista de parâmetros da função, enquanto o polimorfismo seleciona a versão da função virtual a ser chamada com base no tipo real do objeto de tempo de execução. O polimorfismo é implementado por meio de classes derivadas para classes base. A função é implementada substituindo-a. Se a classe derivada não substituir a função virtual virtual da classe base, a classe derivada herdará automaticamente a função virtual virtual da classe base. Versão da função. Neste momento, não importa se o objeto apontado pelo ponteiro da classe base é um tipo base ou um tipo derivado, a função virtual virtual da versão da classe base será chamada se a classe derivada substituir a função virtual virtual; da classe base, ela será chamada em tempo de execução de acordo com O tipo real do objeto é usado para selecionar a versão da função virtual virtual a ser chamada. Por exemplo, se o tipo de objeto apontado pelo ponteiro da classe base for um tipo derivado. , a versão da função virtual virtual da classe derivada será chamada, alcançando assim o polimorfismo.
A intenção original de usar o polimorfismo é declarar a função como virtual na classe base e substituir a versão da função virtual virtual da classe base na classe derivada. Observe que o protótipo da função neste momento é consistente com a classe base. ou seja, o mesmo nome e o mesmo tipo de parâmetro; se você adicionar uma nova versão de função à classe derivada, não poderá chamar dinamicamente a nova versão de função da classe derivada por meio do ponteiro da classe base. como uma versão sobrecarregada da classe derivada. Ainda a mesma frase, a sobrecarga só é válida na classe atual. Quer você a sobrecarregue em uma classe base ou em uma classe derivada, as duas não estão relacionadas entre si. Se entendermos isso, também poderemos compreender com sucesso os resultados de saída nos Exemplos 6 e 9.
A sobrecarga é vinculada estaticamente, o polimorfismo é vinculado dinamicamente. Para explicar melhor, a sobrecarga não tem nada a ver com o tipo de objeto para o qual o ponteiro realmente aponta, e o polimorfismo está relacionado ao tipo de objeto para o qual o ponteiro realmente aponta. Se um ponteiro de classe base chama uma versão sobrecarregada de uma classe derivada, o compilador C++ considera isso ilegal. O compilador C++ pensa apenas que o ponteiro da classe base só pode chamar a versão sobrecarregada da classe base, e a sobrecarga só funciona no namespace. da classe atual. Válido dentro do domínio, a herança perderá o recurso de sobrecarga. É claro que se o ponteiro da classe base chamar uma função virtual neste momento. Em seguida, ele também selecionará dinamicamente a versão da função virtual virtual da classe base ou a versão da função virtual virtual da classe derivada para realizar operações específicas. Isso é determinado pelo tipo de objeto realmente apontado pelo ponteiro da classe base, portanto, sobrecarga e ponteiros. O tipo de objeto para o qual o ponteiro realmente aponta não tem nada a ver com isso. O polimorfismo está relacionado ao tipo de objeto para o qual o ponteiro realmente aponta;
Finalmente, para esclarecer, as funções virtuais virtuais também podem ser sobrecarregadas, mas a sobrecarga só pode ser efetiva dentro do escopo do namespace atual. Quantos objetos String foram criados?
Vejamos primeiro um trecho de código:
Código Java
String str=new String("abc");
Este código é frequentemente seguido pela pergunta, ou seja, quantos objetos String são criados por esta linha de código? Acredito que todos estejam familiarizados com esta questão, e a resposta é bem conhecida, 2. A seguir, partiremos desta questão e revisaremos alguns conhecimentos de JAVA relacionados à criação de objetos String.
Podemos dividir a linha de código acima em quatro partes: String str, =, “abc” e new String(). String str define apenas uma variável do tipo String chamada str, portanto não cria um objeto = inicializa a variável str e atribui uma referência (ou identificador) a um objeto a ela, e obviamente não cria um objeto; String("abc") é deixada. Então, por que new String("abc") pode ser considerado como "abc" e new String()? Vamos dar uma olhada no construtor String que chamamos:
Código Java
String pública(String original) {
//outro código...
}
Como todos sabemos, existem dois métodos comumente usados para criar instâncias (objetos) de uma classe:
Use new para criar objetos.
Chame o método newInstance da classe Class e use o mecanismo de reflexão para criar o objeto.
Usamos new para chamar o método construtor acima da classe String para criar um objeto e atribuir sua referência à variável str. Ao mesmo tempo, notamos que o parâmetro aceito pelo método construtor chamado também é um objeto String, e esse objeto é exatamente “abc”. A partir disso, temos que apresentar outra maneira de criar um objeto String - texto contido entre aspas.
Este método é exclusivo de String e é muito diferente do novo método.
Código Java
Stringstr="abc";
Não há dúvida de que esta linha de código cria um objeto String.
Código Java
String a="abc";
Stringb="abc";
E aqui? A resposta ainda é uma.
Código Java
String a="ab"+"cd";
E aqui? A resposta ainda é uma. Um pouco estranho? Neste ponto, precisamos apresentar uma revisão do conhecimento relacionado ao string pool.
Há um pool de strings na Máquina Virtual JAVA (JVM), que armazena muitos objetos String e pode ser compartilhado, melhorando a eficiência. Como a classe String é final, seu valor não pode ser alterado depois de criada, portanto não precisamos nos preocupar com a confusão do programa causada pelo compartilhamento de objetos String. O pool de strings é mantido pela classe String, e podemos chamar o método intern() para acessar o pool de strings.
Vejamos String a="abc";. Quando esta linha de código é executada, a máquina virtual JAVA primeiro pesquisa no conjunto de strings para ver se tal objeto com o valor "abc" já existe. O valor de retorno do método String class equals(Object obj). Se houver, nenhum novo objeto será criado e uma referência ao objeto existente será retornada diretamente; caso contrário, o objeto será criado primeiro, depois adicionado ao conjunto de strings e então sua referência será retornada; Portanto, não nos é difícil compreender porque é que os dois primeiros dos três exemplos anteriores têm esta resposta.
Para o terceiro exemplo:
Código Java
String a="ab"+"cd";
Porque o valor da constante é determinado em tempo de compilação. Aqui, "ab" e "cd" são constantes, portanto o valor da variável a pode ser determinado em tempo de compilação. O efeito compilado desta linha de código é equivalente a:
Código Java
String a="abcd";
Portanto, apenas um objeto "abcd" é criado aqui e é salvo no conjunto de strings.
Agora surge a pergunta novamente: todas as strings obtidas após a conexão "+" serão adicionadas ao pool de strings? Todos nós sabemos que "==" pode ser usado para comparar duas variáveis. Ele tem as duas situações a seguir:
Se dois tipos básicos (char, byte, short, int, long, float, double, boolean) forem comparados, será avaliado se seus valores são iguais.
Se a tabela comparar duas variáveis de objeto, será avaliado se suas referências apontam para o mesmo objeto.
A seguir usaremos "==" para fazer alguns testes. Para facilitar a explicação, consideramos apontar para um objeto que já existe no conjunto de strings como o objeto que está sendo adicionado ao conjunto de strings:
Código Java
public class StringTest { public static void main(String[] args) { String a = "ab";// Cria um objeto e adiciona-o ao conjunto de strings System.out.println("String a = /"ab/" ; "); String b = "cd";// Um objeto é criado e adicionado ao conjunto de strings System.out.println("String b = /"cd/";"); String c = "abcd"; // Um objeto é criado e adicionado ao conjunto de strings String d = "ab" + "cd" // Se d e c apontam para o mesmo objeto, significa que d também foi adicionado ao conjunto de strings if (d ==; c ) { System.out.println("/"ab/"+/"cd/" O objeto criado/" é adicionado ao/" pool de strings"); // Se d e c não apontam para o mesmo objeto, significa que d não foi adicionado ao conjunto de strings else { System.out.println("/"ab/"+/"cd/" O objeto criado/"não foi adicionado/" ao conjunto de strings"); c Aponta para o mesmo objeto, significa que e também foi adicionado ao conjunto de strings if (e == c) { System.out.println(" a +/"cd/" Thecreated object/"joined/" string meio da piscina"); } // Se e e c não apontam para o mesmo objeto, significa que e não foi adicionado ao conjunto de strings else { System.out.println(" a +/"cd/" criado objeto/"não adicionado/" ao string pool" ); } String f = "ab" + b; // Se f e c apontam para o mesmo objeto, significa que f também foi adicionado ao string pool if (f == c) { System.out .println("/ Objeto criado por "ab/"+ b /"Added/" to the string pool"); } // Se f e c não apontam para o mesmo objeto, significa que f não foi adicionado ao string pool else { System.out.println("/" ab/" + b objeto criado/"não adicionado/" ao conjunto de strings"); } String g = a + b; // Se g e c apontam para o mesmo objeto, significa que g também foi adicionado ao conjunto de strings if (g == c) { System.out.println("a + b O objeto criado/"adicionado/" ao pool de strings"); } // Se g e c não apontam para o mesmo objeto, significa que g não foi adicionado ao pool de strings else { System.out.println ("a + b objeto criado/"não adicionado/" ao conjunto de strings");
Os resultados da execução são os seguintes:
String a = "ab";
String b = "cd";
O objeto criado por "ab"+"cd" é "juntado" no conjunto de strings
O objeto criado por + "cd" "não é adicionado" ao conjunto de strings.
O objeto criado por "ab"+ b "não é adicionado" ao conjunto de strings.
O objeto criado por a + b "não é adicionado" ao conjunto de strings. A partir dos resultados acima, podemos ver facilmente que apenas novos objetos gerados usando conexões "+" entre objetos String criados usando aspas para incluir texto serão adicionados. . no conjunto de cordas. Para todas as expressões de conexão "+" que contêm objetos criados no novo modo (incluindo nulo), os novos objetos que elas geram não serão adicionados ao conjunto de strings e não entraremos em detalhes sobre isso.
Mas há uma situação que exige a nossa atenção. Por favor, veja o código abaixo:
Código Java
public class StringStaticTest { // Constante A public static final String A = "ab"; // Constante B public static final String B = "cd"; para inicializar s String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s é igual a t, eles são o mesmo objeto"); { System.out.println("s não é igual a t, eles não são o mesmo objeto");
O resultado da execução deste código é o seguinte:
s é igual a t. Eles são o mesmo objeto. A razão é esta. Para uma constante, seu valor é fixo, portanto pode ser determinado em tempo de compilação, enquanto o valor de uma variável só pode ser determinado em tempo de execução, pois esta variável pode ser chamada por diferentes métodos, portanto pode causar o. valor a ser alterado. No exemplo acima, A e B são constantes e seus valores são fixos, portanto o valor de s também é fixo e é determinado quando a classe é compilada. Quer dizer:
Código Java
Sequência s=A+B;
Equivalente a:
Código Java
String s="ab"+"cd";
Deixe-me mudar um pouco o exemplo acima e ver o que acontece:
Código Java
public class StringStaticTest { // Constante A public static final String A; // Constante B public static final String B; // Inicialize s conectando as duas constantes com + String s = A + B; String t = "abcd"; System.out.println("s é igual a t, eles são o mesmo objeto"); else { System.out.println("s não é igual a t, eles não são o mesmo objeto");
O resultado de seu funcionamento é este:
s não é igual a t. Eles não são o mesmo objeto, mas foram ligeiramente modificados. O resultado é exatamente o oposto do exemplo agora. Vamos analisar novamente. Embora A e B sejam definidos como constantes (só podem ser atribuídos uma vez), eles não são atribuídos imediatamente. Antes de o valor de s ser calculado, quando eles são atribuídos e qual valor eles são atribuídos são todas variáveis. Portanto, A e B se comportam como uma variável antes de receberem um valor. Então s não pode ser determinado em tempo de compilação, mas só pode ser criado em tempo de execução.
Como o compartilhamento de objetos no pool de strings pode melhorar a eficiência, incentivamos todos a criar objetos String incluindo texto entre aspas. Na verdade, é isso que costumamos usar na programação.
A seguir, vamos dar uma olhada no método intern(), que é definido da seguinte forma:
Código Java
público nativo String estagiário();
Este é um método nativo. Ao chamar esse método, a máquina virtual JAVA primeiro verifica se um objeto com valor igual ao objeto já existe no pool de strings. Em caso afirmativo, ela retorna uma referência ao objeto no pool de strings; objeto no conjunto de strings. Objeto String com o mesmo valor e, em seguida, retorna sua referência.
Vejamos este código:
Código Java
public class StringInternTest { public static void main(String[] args) { // Use um array char para inicializar a para evitar que um objeto com o valor "abcd" já exista no conjunto de strings antes de a ser criado String a = new String (new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern(); System.out.println("b foi adicionado ao conjunto de strings, nenhum novo objeto foi criado"); } else { System.out.println("b não foi adicionado ao conjunto de strings, nenhum novo objeto foi criado"); } } }
Resultados em execução:
b não foi adicionado ao conjunto de strings e um novo objeto foi criado Se o método intern() da classe String não encontrar um objeto com o mesmo valor, ele adiciona o objeto atual ao conjunto de strings e então retorna seu. referência, então b e a apontam para o mesmo objeto, caso contrário, o objeto apontado por b é recém-criado pela máquina virtual JAVA no pool de strings, mas seu valor é o mesmo que a. O resultado da execução do código acima apenas confirma este ponto.
Por fim, vamos falar sobre o armazenamento de objetos String na Máquina Virtual JAVA (JVM) e o relacionamento entre o pool de strings e o heap e a pilha. Vamos primeiro revisar a diferença entre heap e pilha:
Pilha: salva principalmente tipos básicos (ou tipos integrados) (char, byte, short, int, long, float, double, boolean) e referências de objetos. Os dados podem ser compartilhados e sua velocidade perde apenas para o registro. pilha.
Heap: usado para armazenar objetos.
Quando olharmos o código-fonte da classe String, descobriremos que ele possui um atributo value, que armazena o valor do objeto String. O tipo é char[], o que também mostra que uma string é uma sequência de caracteres.
Ao executar String a="abc";, a máquina virtual JAVA cria três valores de char 'a', 'b' e 'c' na pilha e, em seguida, cria um objeto String no heap, seu valor (value ) é uma matriz de três valores char recém-criados na pilha {'a', 'b', 'c'} Finalmente, o objeto String recém-criado será adicionado ao conjunto de strings. Se executarmos o código String b=new String("abc"); , uma vez que "abc" foi criado e salvo no pool de strings, a máquina virtual JAVA criará apenas um novo objeto String no heap, mas será o The. valor são os três valores do tipo char 'a', 'b' e 'c' criados na pilha quando a linha de código anterior foi executada.
Neste ponto, já estamos bastante claros sobre a questão de por que String str=new String("abc") levantada no início deste artigo cria dois objetos.