¿Qué es el polimorfismo? ¿Cuál es su mecanismo de implementación? ¿Cuál es la diferencia entre sobrecargar y reescribir? Estos son los cuatro conceptos muy importantes que vamos a repasar en esta ocasión: herencia, polimorfismo, sobrecarga y sobrescritura.
herencia
En pocas palabras, la herencia consiste en generar un nuevo tipo basado en un tipo existente agregando nuevos métodos o redefiniendo los métodos existentes (como se explica a continuación, este método se llama reescritura). La herencia es una de las tres características básicas de la orientación a objetos: encapsulación, herencia y polimorfismo. Cada clase que escribimos cuando usamos JAVA está heredando, porque en el lenguaje JAVA, la clase java.lang.Object es la clase base más fundamental (. o clase principal o superclase) de todas las clases. Si una clase recién definida que definimos no especifica explícitamente de qué clase base hereda, entonces JAVA heredará de la clase Objeto de forma predeterminada.
Podemos dividir las clases en JAVA en los siguientes tres tipos:
Clase: una clase definida mediante clase y que no contiene métodos abstractos.
Clase abstracta: una clase definida mediante una clase abstracta, que puede contener o no métodos abstractos.
Interfaz: una clase definida mediante la interfaz.
Existen las siguientes reglas de herencia entre estos tres tipos:
Las clases pueden ampliar clases, clases abstractas e implementar interfaces.
Las clases abstractas pueden heredar (extender) clases, pueden heredar (extender) clases abstractas y pueden heredar (implementar) interfaces.
Las interfaces solo pueden ampliar interfaces.
Tenga en cuenta que las diferentes palabras clave extendidas e implementadas utilizadas en cada caso de herencia en las tres reglas anteriores no se pueden reemplazar a voluntad. Como todos sabemos, una vez que una clase ordinaria hereda una interfaz, debe implementar todos los métodos definidos en esta interfaz; de lo contrario, solo se puede definir como una clase abstracta. La razón por la que no uso el término "implementación" para la palabra clave implements aquí es porque conceptualmente también representa una relación de herencia y, en el caso de la interfaz de implementos de clase abstracta, no tiene que implementar esta definición de interfaz. método, por lo que es más razonable utilizar la herencia.
Las tres reglas anteriores también cumplen con las siguientes restricciones:
Tanto las clases como las clases abstractas solo pueden heredar como máximo una clase, o como máximo una clase abstracta, y estas dos situaciones son mutuamente excluyentes, es decir, heredan una clase o una clase abstracta.
Cuando las clases, las clases abstractas y las interfaces heredan interfaces, no están restringidas por el número. En teoría, pueden heredar un número ilimitado de interfaces. Por supuesto, para una clase, debe implementar todos los métodos definidos en todas las interfaces que hereda.
Cuando una clase abstracta hereda una clase abstracta o implementa una interfaz, puede no implementar parcial, total o completamente los métodos abstractos de la clase abstracta principal o las interfaces definidas en la interfaz de la clase principal.
Cuando una clase hereda una clase abstracta o implementa una interfaz, debe implementar todos los métodos abstractos de la clase abstracta principal o todas las interfaces definidas en la interfaz de la clase principal.
El beneficio que aporta la herencia a nuestra programación es la reutilización (reutilización) de clases originales. Al igual que la reutilización de módulos, la reutilización de clases puede mejorar nuestra eficiencia de desarrollo. De hecho, la reutilización de módulos es el efecto superpuesto de la reutilización de una gran cantidad de clases. Además de la herencia, también podemos usar la composición para reutilizar clases. La llamada combinación consiste en definir la clase original como un atributo de la nueva clase y lograr la reutilización llamando a los métodos de la clase original en la nueva clase. Si no existe una relación incluida entre el tipo recién definido y el tipo original, es decir, desde un concepto abstracto, las cosas representadas por el tipo recién definido no son una de las cosas representadas por el tipo original, como las personas amarillas. Es un tipo de ser humano, y existe una relación entre ellos, incluyendo y siendo incluido, por lo que en este momento la combinación es una mejor opción para lograr la reutilización. El siguiente ejemplo es un ejemplo simple de la combinación:
public class Sub { private Parent p = new Parent(); public void doSomething() { // Reutilizar el método p.method() de la clase Parent // otro código } } class Parent { public void método() { / / hacer algo aquí } }
Por supuesto, para que el código sea más eficiente, también podemos inicializarlo cuando necesitemos usar el tipo original (como Parent p).
Usar herencia y combinación para reutilizar clases originales es un modelo de desarrollo incremental. La ventaja de este método es que no es necesario modificar el código original, por lo que no traerá nuevos errores al código original y no es necesario. Volver a probar debido a modificaciones en el código original, lo que obviamente es beneficioso para nuestro desarrollo. Por lo tanto, si estamos manteniendo o transformando un sistema o módulo original, especialmente cuando no tenemos un conocimiento profundo de ellos, podemos elegir el modelo de desarrollo incremental, que no solo puede mejorar en gran medida nuestra eficiencia de desarrollo, sino también evitar los riesgos causados por modificaciones al código original.
Polimorfismo
El polimorfismo es otro concepto básico importante. Como se mencionó anteriormente, es una de las tres características básicas de la orientación a objetos. ¿Qué es exactamente el polimorfismo? Primero veamos el siguiente ejemplo para ayudar a comprender:
//Interfaz del coche interface Car { // Nombre del coche String getName() // Obtener el precio del coche int getPrice() } // Clase BMW BMW implementa Car { public String getName() { return "BMW" public int; getPrice() { return 300000 } } // CheryQQ clase CheryQQ implementa Car { public String getName() { return "CheryQQ" } public int getPrice(); { return 20000; } } // Tienda de venta de automóviles public class CarShop { // Ingresos por ventas de automóviles private int money = 0; // Vender un automóvil public void sellCar(Auto car) { System.out.println("Modelo de automóvil: " + car.getName() + " Precio unitario: " + car.getPrice()); // Aumentar los ingresos por ventas de automóviles += car.getPrice() } // Ingresos totales por ventas de automóviles public int; getMoney() { devolver dinero } public static void main(String[] args) { CarShop aShop = new CarShop() // Vender un BMW aShop.sellCar(new BMW() // Vender un BMW Chery QQ aShop); .sellCar(new CheryQQ()); System.out.println("Ingresos totales: " + aShop.getMoney());
Resultados de ejecución:
Modelo: BMW Precio unitario: 300.000
Modelo: CheryQQ Precio unitario: 20.000
Ingresos totales: 320.000
La herencia es la base para realizar el polimorfismo. Literalmente entendido, el polimorfismo es un tipo (ambos tipos de automóvil) que muestran múltiples estados (el nombre de BMW es BMW y el precio es 300 000; el nombre de Chery es CheryQQ y el precio es 2000). Asociar una llamada a un método con el sujeto (es decir, el objeto o clase) al que pertenece el método se denomina enlace, que se divide en dos tipos: enlace temprano y enlace tardío. Sus definiciones se explican a continuación:
Enlace temprano: enlace antes de que se ejecute el programa, implementado por el compilador y el enlazador, también llamado enlace estático. Por ejemplo, métodos estáticos y métodos finales. Tenga en cuenta que los métodos privados también se incluyen aquí porque son implícitamente finales.
Enlace tardío: enlace según el tipo de objeto en tiempo de ejecución, implementado mediante el mecanismo de llamada al método, por lo que también se denomina enlace dinámico o enlace en tiempo de ejecución. Todos los métodos, excepto la vinculación anticipada, son de vinculación tardía.
El polimorfismo se implementa en el mecanismo de unión tardía. El beneficio que nos aporta el polimorfismo es que elimina la relación de acoplamiento entre clases y facilita la expansión del programa. Por ejemplo, en el ejemplo anterior, para agregar un nuevo tipo de automóvil a la venta, solo necesita dejar que la clase recién definida herede la clase Car e implemente todos sus métodos sin realizar ninguna modificación al código original sellCar(Car car. ) de la clase CarShop Method puede manejar modelos de automóviles nuevos. El nuevo código es el siguiente:
// Santana Car clase Santana implementa Car { public String getName() { return "Santana" } public int getPrice() { return 80000;
Sobrecarga y anulación
La sobrecarga y la reescritura son conceptos de métodos. Antes de aclarar estos dos conceptos, primero comprendamos cuál es la estructura del método (el nombre en inglés es firma, algunos se traducen como "firma", aunque se usa más ampliamente, pero esta traducción no). preciso). La estructura se refiere a la estructura de composición de un método, que incluye específicamente el nombre y los parámetros del método, y cubre el número, el tipo y el orden de aparición de los parámetros, pero no incluye el tipo de valor de retorno del método, los modificadores de acceso ni las modificaciones. como símbolo abstracto, estático y final. Por ejemplo, los dos métodos siguientes tienen la misma estructura:
método público void (int i, String s) { // hacer algo } método público String (int i, String s) { // hacer algo }
Estos dos son métodos con diferentes configuraciones:
método público void (int i, String s) { // hacer algo } método público void (String s, int i) { // hacer algo }
Después de comprender el concepto de Gestalt, echemos un vistazo a la sobrecarga y la reescritura. Consulte sus definiciones:
Anular, el nombre en inglés es anular, significa que en el caso de herencia, se define en la subclase un nuevo método con la misma estructura que el método de la clase base, que se denomina subclase que anula el método de la clase base. Este es un paso necesario para lograr el polimorfismo.
Sobrecarga, el nombre en inglés es sobrecarga, se refiere a definir más de un método con el mismo nombre pero con diferentes estructuras en una misma clase. En una misma clase no se permite definir más de un método del mismo tipo.
Consideremos una pregunta interesante: ¿Se pueden sobrecargar los constructores? La respuesta es, por supuesto, sí, a menudo hacemos esto en la programación real. De hecho, el constructor también es un método. El nombre del constructor es el nombre del método, los parámetros del constructor son los parámetros del método y su valor de retorno es una instancia de la clase recién creada. Sin embargo, una subclase no puede anular el constructor, porque una subclase no puede definir un constructor con el mismo tipo que la clase base.
Sobrecarga, anulación, polimorfismo y ocultación de funciones.
A menudo se ve que algunos principiantes de C++ tienen una comprensión vaga de la sobrecarga, la sobrescritura, el polimorfismo y la ocultación de funciones. Escribiré algunas de mis propias opiniones aquí, con la esperanza de ayudar a los principiantes de C++ a aclarar sus dudas.
Antes de comprender la compleja y sutil relación entre sobrecarga, sobrescritura, polimorfismo y ocultación de funciones, primero debemos revisar conceptos básicos como sobrecarga y cobertura.
Primero, veamos un ejemplo muy simple para comprender qué es la función oculta.
#include <iostream>usando el espacio de nombres 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 //La siguiente oración es incorrecta, por lo que está bloqueada //d.fun();error C2660 : 'divertido': la función no toma 0 parámetros d.fun(1); Derive *pd =new Derive(); //La siguiente oración es incorrecta, por lo que está bloqueada //pd->fun();error C2660: 'divertido': la función no toma 0 parámetros pd->fun(1); eliminar pd;}
/* Las funciones en diferentes ámbitos que no son espacios de nombres no constituyen una sobrecarga. Las subclases y las clases principales son dos ámbitos diferentes.
En este ejemplo, las dos funciones están en ámbitos diferentes, por lo que no están sobrecargadas a menos que el ámbito sea un ámbito de espacio de nombres. */
En este ejemplo, la función no está sobrecargada ni anulada, sino oculta.
Los siguientes cinco ejemplos explican específicamente qué es esconderse.
Ejemplo 1
#include <iostream>usando el espacio de nombres std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//sobrecarga 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();// Correcto, la clase derivada no tiene una declaración de función con el mismo nombre que la clase base, entonces todas las funciones sobrecargadas con el mismo nombre en la clase base se utilizará como funciones candidatas. d.fun(1);// Correcto, la clase derivada no tiene una declaración de función con el mismo nombre que la clase base, entonces todas las funciones sobrecargadas con el mismo nombre en la clase base se usarán como funciones candidatas. devolver 0;}
Ejemplo 2
#include <iostream>usando el espacio de nombres std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//sobrecarga void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //Nueva versión de la función, todas las versiones sobrecargadas de la clase base están bloqueadas, aquí la llamamos ocultar función ocultar //Si hay una declaración de una función con el mismo nombre de la clase base en la clase derivada, la función con el mismo nombre en la clase base no se usará como función candidata, incluso si la clase base tiene múltiples versiones de funciones sobrecargadas con 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); //La siguiente oración es incorrecta, por lo que está bloqueada //d.fun();error C2660: 'divertido': la función no tomar 0 parámetros devolver 0;}
Ejemplo 3
#include <iostream>usando el espacio de nombres std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//sobrecarga void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //Anula una de las versiones de función de la clase base de anulación. De manera similar, todas las versiones sobrecargadas de la clase base son oculto. //Si hay una declaración de una función con el mismo nombre de la clase base en la clase derivada, la función con el mismo nombre en la clase base no se usará como función candidata, incluso si la clase base tiene múltiples versiones de funciones sobrecargadas con diferentes listas de parámetros. void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derivar d; d.fun(); //La siguiente oración es incorrecta, por lo que está bloqueada //d.fun(1);error C2660: 'diversión': la función no toma 1 parámetro devuelve 0;}
Ejemplo 4
#include <iostream>usando el espacio de nombres std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//sobrecarga void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: usando Basic::fun; void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; //Retorno correcto 0;}/*Resultado de salida Derive::fun()Base::fun(int i)Presione cualquier tecla para continuar*/
Ejemplo 5
#include <iostream>usando el espacio de nombres std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//sobrecarga void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: usando 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();//Correcto d.fun(1);//Correcto d.fun(1,2);//Devolución correcta 0;}/*Resultado de salida Base::fun()Base::fun(int i)Derivar::fun(int i,int j)Presione cualquier tecla para continuar*/
Bien, primero hagamos un pequeño resumen de las características entre sobrecarga y sobrescritura.
Características de la sobrecarga:
n el mismo ámbito (en la misma clase);
n El nombre de la función es el mismo pero los parámetros son diferentes;
n La palabra clave virtual es opcional.
Anular significa que una función de clase derivada cubre una función de clase base. Las características de anulación son:
n ámbitos diferentes (ubicados en clases derivadas y clases base respectivamente);
n El nombre de la función y los parámetros son los mismos;
n Las funciones de clase base deben tener la palabra clave virtual. (Si no hay una palabra clave virtual, se llama ocultar oculta)
Si la clase base tiene múltiples versiones sobrecargadas de una función y usted anula (anula) una o más versiones de función en la clase base en la clase derivada, o agrega otras nuevas en la versión de función de la clase derivada (mismo nombre de función, diferentes parámetros) , entonces se bloquean todas las versiones sobrecargadas de la clase base, a lo que aquí llamamos oculta. Entonces, en general, cuando desea usar una nueva versión de función en una clase derivada y desea usar la versión de función de la clase base, debe anular todas las versiones sobrecargadas en la clase base en la clase derivada. Si no desea anular la versión de función sobrecargada de la clase base, debe usar el Ejemplo 4 o el Ejemplo 5 para declarar explícitamente el alcance del espacio de nombres de la clase base.
De hecho, el compilador de C ++ cree que no existe ninguna relación entre funciones con el mismo nombre de función y diferentes parámetros. Son simplemente dos funciones no relacionadas. Es solo que el lenguaje C++ introdujo los conceptos de sobrecarga y sobrescritura para simular el mundo real y permitir a los programadores lidiar con problemas del mundo real de manera más intuitiva. La sobrecarga se realiza en el mismo ámbito de espacio de nombres, mientras que la anulación se realiza en ámbitos de espacio de nombres diferentes. Por ejemplo, la clase base y la clase derivada son dos ámbitos de espacio de nombres diferentes. Durante el proceso de herencia, si una clase derivada tiene el mismo nombre que una función de clase base, la función de clase base estará oculta. Por supuesto, la situación que se analiza aquí es que no hay una palabra clave virtual delante de la función de clase base. Discutiremos la situación en la que hay una palabra clave virtual por separado.
Una clase heredada anula una versión funcional de la clase base para crear su propia interfaz funcional. En este momento, el compilador de C++ piensa que, dado que ahora desea usar la interfaz reescrita de la clase derivada, no se le proporcionará la interfaz de mi clase base (por supuesto, puede usar el método de declarar explícitamente el alcance del espacio de nombres). consulte [Conceptos básicos de C++] Sobrecarga, sobrescritura, polimorfismo y ocultación de funciones (1)). Ignora que la interfaz de su clase base tiene características de sobrecarga. Si desea continuar manteniendo la función de sobrecarga en la clase derivada, proporcione la función de sobrecarga de la interfaz usted mismo. Por lo tanto, en una clase derivada, siempre que el nombre de la función sea el mismo, la versión de la función de la clase base será bloqueada sin piedad. En el compilador, el enmascaramiento se implementa a través del alcance del espacio de nombres.
Por lo tanto, para mantener la versión sobrecargada de la función de clase base en la clase derivada, debe anular todas las versiones sobrecargadas de la clase base. La sobrecarga solo es válida en la clase actual y la herencia perderá las características de la sobrecarga de funciones. En otras palabras, si desea colocar la función sobrecargada de la clase base en la clase derivada heredada, debe reescribirla.
"Oculto" aquí significa que la función de la clase derivada bloquea la función de la clase base con el mismo nombre. Hagamos también un breve resumen de las reglas específicas:
n Si la función de la clase derivada tiene el mismo nombre que la función de la clase base, pero los parámetros son diferentes. En este momento, si la clase base no tiene la palabra clave virtual, las funciones de la clase base estarán ocultas. (Tenga cuidado de no confundirlo con sobrecarga. Aunque la función con el mismo nombre y diferentes parámetros debe llamarse sobrecarga, aquí no puede entenderse como sobrecarga porque la clase derivada y la clase base no están en el mismo alcance del espacio de nombres. Esto es entendido como esconderse)
n Si la función de la clase derivada tiene el mismo nombre que la función de la clase base, pero los parámetros son diferentes. En este momento, si la clase base tiene la palabra clave virtual, las funciones de la clase base se heredarán implícitamente en la tabla virtual de la clase derivada. En este momento, la función en la clase derivada vtable apunta a la dirección de función de la versión de la clase base. Al mismo tiempo, esta nueva versión de la función se agrega a la clase derivada como una versión sobrecargada de la clase derivada. Pero cuando el puntero de la clase base implementa un método de función de llamada polimórfico, esta nueva versión de la función de clase derivada estará oculta.
n Si la función de la clase derivada tiene el mismo nombre que la función de la clase base y los parámetros también son los mismos, pero la función de la clase base no tiene la palabra clave virtual. En este momento, las funciones de la clase base están ocultas. (Ojo, no confundirlo con cobertura, que aquí se entiende como ocultamiento).
n Si la función de la clase derivada tiene el mismo nombre que la función de la clase base y los parámetros también son los mismos, pero la función de la clase base tiene la palabra clave virtual. En este momento, las funciones de la clase base no estarán "ocultas". (Aquí hay que entenderlo como cobertura ^_^).
Interludio: cuando no hay una palabra clave virtual delante de la función de clase base, necesitamos reescribirla más suavemente. Cuando existe la palabra clave virtual, es más razonable llamarla anular. Todos pueden entender mejor C++. Algo sutil. Sin más, ilustremos con un ejemplo.
Ejemplo 6
#include <iostream>usando el espacio de nombres std; class Base{public: virtual void fun() { cout << "Base::fun()" << endl }//sobrecarga virtual void fun(int i) { cout << "Base::fun(int i)" << endl; }//overload}; clase Derive: public Base{public: void fun() { cout << "Derive::fun()" << endl; }//anular void fun(int i) { cout << "Derive::fun(int i)" << endl }//anular 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); //La siguiente oración es incorrecta, por lo que está bloqueada //pb->fun(1,2); la función virtual no se puede sobrecargar, error C2661: 'diversión': ninguna función sobrecargada toma 2 parámetros cout << endl; pd = new Derive(); pd->fun(); pd->fun(1); pd->fun(1,2);//sobrecarga eliminar pd;
Resultados de salida
Derivar::diversión()
Derivar::diversión(int i)
Derivar::diversión()
Derivar::diversión(int i)
Derivar::diversión(int i,int j)
Presione cualquier tecla para continuar
*/
Ejemplo 7-1
#incluye <iostream> usando el espacio de nombres std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; int main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) eliminar pb;}
Ejemplo 7-2
#include <iostream> usando el espacio de nombres std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(doble d){ cout <<"Derive::fun(doble d)"<< endl } }; int main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) pb->fun((double)0.01);//Base::fun(int i) eliminar pb;
Ejemplo 8-1
#include <iostream> usando el espacio de nombres std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(int i){ cout <<"Derive::fun(int i)"<< endl }}; int main(){ Base *pb = new Derive(); pb->fun(1);//Derivar::fun(int i) eliminar pb; devolver 0;}
Ejemplo 8-2
#include <iostream> usando el espacio de nombres std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(int i){ cout <<"Derive::fun(int i)"<< endl } void fun(doble d){ cout <<"Derive::fun(double; d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) pb->fun((doble) 0.01);//Derive::fun(int i) eliminar pb; devolver 0;}
Ejemplo 9
#include <iostream> usando el espacio de nombres 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(doble d){ cout <<"Derive::fun(doble d)"<< endl; } };int main(){ Base *pb = new 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) // sobrecarga pd->fun('a');//Derive::fun(char c) //sobrecarga pd->fun(0.01);//Derive::fun(doble d) eliminar pb; eliminar pd; devolver 0;}
Los ejemplos 7-1 y 8-1 son fáciles de entender. Pongo estos dos ejemplos aquí para que todos hagan una comparación y ayuden a todos a comprender mejor:
n En el Ejemplo 7-1, la clase derivada no cubre la función virtual de la clase base. En este momento, la dirección apuntada por el puntero de función en la tabla virtual de la clase derivada es la dirección de la función virtual de la clase base.
n En el Ejemplo 8-1, la clase derivada anula la función virtual de la clase base. En este momento, la dirección apuntada por el puntero de función en la tabla virtual de la clase derivada es la dirección de la función virtual anulada de la propia clase derivada.
Los ejemplos 7-2 y 8-2 parecen un poco extraños. De hecho, si los compara según los principios anteriores, la respuesta será clara:
n En el Ejemplo 7-2, sobrecargamos una versión de función para la clase derivada: void fun(double d) De hecho, esto es sólo un encubrimiento. Analicémoslo específicamente. Hay varias funciones en la clase base y varias funciones en la clase derivada:
tipo clase base clase derivada
sección de tabla virtual
diversión vacía (int i)
Apunta a la versión de clase base de la función virtual void fun(int i)
parte estática
diversión vacía (doble d)
Analicemos nuevamente las siguientes tres líneas de código:
Base *pb = nueva Derivada();
pb->diversión(1);//Base::diversión(int i)
pb->fun((doble)0.01);//Base::diversión(int i)
La primera oración es la clave: el puntero de la clase base apunta al objeto de la clase derivada. Sabemos que esta es una llamada polimórfica. La segunda oración se encuentra en tiempo de ejecución como un objeto de clase derivada. el tipo del objeto de tiempo de ejecución, así que primero vaya a la vtable de la clase derivada para encontrar la versión de la función virtual de la clase derivada. Se descubre que la clase derivada no cubre la función virtual de la clase base. La clase derivada solo hace un puntero a la dirección de la función virtual de la clase base, por lo que es natural llamar a la versión de clase base de la función virtual. En la última oración, el programa todavía está buscando la vtable de la clase derivada y descubre que no existe ninguna función virtual de esta versión, por lo que tiene que regresar y llamar a su propia función virtual.
También vale la pena mencionar aquí que si la clase base tiene múltiples funciones virtuales en este momento, el programa mostrará una "llamada poco clara" al compilar el programa. Los ejemplos son los siguientes
#include <iostream> usando el espacio de nombres std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl } virtual void fun(char c){ cout < <"Base::diversión(char c)"<< endl }}; clase Derivar: base pública{público: void diversión(doble d){ cout <<"Derivar::diversión(doble) d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(0.01);//error C2668: 'fun': llamada ambigua a función sobrecargada eliminar pb; return 0;}
Bien, analicemos nuevamente el ejemplo 8-2.
n En el Ejemplo 8-2, también sobrecargamos una versión de función para la clase derivada: void fun (doble d), y también cubrimos la función virtual de la clase base. Analicémosla en detalle. , y la clase derivada tiene varias funciones. La clase tiene varias funciones:
tipo clase base clase derivada
sección de tabla virtual
diversión vacía (int i)
diversión vacía (int i)
parte estática
diversión vacía (doble d)
En la tabla, podemos ver que el puntero de función en la tabla virtual de la clase derivada apunta a su propia dirección de función virtual anulada.
Analicemos nuevamente las siguientes tres líneas de código:
Base *pb = nueva Derivada();
pb->diversión(1);//Derivar::diversión(int i)
pb->fun((double)0.01);//Derive::fun(int i)
No hay necesidad de decir más sobre la primera oración. La segunda oración es llamar a la versión de función virtual de la clase derivada como algo natural. La tercera oración, oye, se siente extraño de nuevo. Estúpido cuando se ejecuta, se sumergen en la tabla vtable de la clase derivada, solo la miré y me di cuenta de que no había ninguna versión que quisiera. , ¿Por qué los punteros de la clase base no miran a su alrededor y lo buscan? Jaja, resulta que la vista de la clase base es muy antigua, por lo que debe ser presbicia. Todo lo que sus ojos pueden ver es su propia no visión. Parte de Vtable (es decir, la parte estática) y la parte de Vtable que desea administrar, el vacío de la clase derivada La diversión (doble d) está tan lejos que no puedes verlo. Además, la clase derivada tiene que encargarse de todo. ¿No tiene la clase derivada algún poder propio? ¡Oye, no más discusiones, todos pueden! cuidarse a sí mismos ^_^
¡Ay! ¿Vas a suspirar? El puntero de la clase base puede realizar llamadas polimórficas, pero nunca puede realizar llamadas sobrecargadas a clases derivadas (consulte el Ejemplo 6)~~~
Veamos el Ejemplo 9 nuevamente,
El efecto de este ejemplo es el mismo que el del Ejemplo 6, con el mismo propósito. Creo que después de comprender los ejemplos anteriores, esto también es un pequeño beso.
resumen:
La sobrecarga selecciona la versión de la función que se llamará en función de la lista de parámetros de la función, mientras que el polimorfismo selecciona la versión de la función virtual que se llamará en función del tipo real del objeto de tiempo de ejecución. El polimorfismo se implementa a través de clases derivadas a clases base. La función se implementa anulándola. Si la clase derivada no anula la función virtual virtual de la clase base, la clase derivada heredará automáticamente la función virtual virtual de la clase base. Versión de la función en este momento, no importa si el objeto señalado por el puntero de la clase base es un tipo base o un tipo derivado, se llamará a la función virtual virtual de la versión de la clase base si la clase derivada anula la función virtual virtual; de la clase base, se llamará en tiempo de ejecución de acuerdo con El tipo real del objeto se utiliza para seleccionar la versión de la función virtual que se llamará. Por ejemplo, si el tipo de objeto señalado por el puntero de la clase base es un tipo derivado. , se llamará a la versión de la función virtual virtual de la clase derivada, logrando así el polimorfismo.
La intención original de usar polimorfismo es declarar la función como virtual en la clase base y anular la versión de la función virtual virtual de la clase base en la clase derivada. Tenga en cuenta que el prototipo de función en este momento es consistente con la clase base. es decir, el mismo nombre y el mismo tipo de parámetro; si agrega una nueva versión de función a la clase derivada, no puede llamar dinámicamente a la nueva versión de función de la clase derivada a través del puntero de clase base. como una versión sobrecargada de la clase derivada. Sigue siendo la misma frase, la sobrecarga solo es válida en la clase actual, ya sea que la sobrecargue en una clase base o en una clase derivada, las dos no están relacionadas entre sí. Si entendemos esto, también podremos comprender con éxito los resultados de los ejemplos 6 y 9.
La sobrecarga está vinculada estáticamente, el polimorfismo está vinculado dinámicamente. Para explicarlo mejor, la sobrecarga no tiene nada que ver con el tipo de objeto al que realmente apunta el puntero, y el polimorfismo está relacionado con el tipo de objeto al que realmente apunta el puntero. Si un puntero de clase base llama a una versión sobrecargada de una clase derivada, el compilador de C++ lo considera ilegal. El compilador de C++ solo piensa que el puntero de clase base solo puede llamar a la versión sobrecargada de la clase base y la sobrecarga solo funciona en el espacio de nombres. de la clase actual Válido dentro del dominio, la herencia perderá la función de sobrecarga. Por supuesto, si el puntero de la clase base llama a una función virtual en este momento. Luego, también seleccionará dinámicamente la versión de la función virtual virtual de la clase base o la versión de la función virtual virtual de la clase derivada para realizar operaciones específicas. Esto está determinado por el tipo de objeto realmente apuntado por el puntero de la clase base, por lo que se sobrecarga y los punteros. El tipo de objeto al que realmente apunta el puntero no tiene nada que ver con él; el polimorfismo está relacionado con el tipo de objeto al que realmente apunta el puntero.
Finalmente, para aclarar, las funciones virtuales también se pueden sobrecargar, pero la sobrecarga solo puede ser efectiva dentro del alcance del espacio de nombres actual. ¿Cuántos objetos String se han creado?
Primero veamos un fragmento de código:
código java
Cadena cadena = nueva cadena ("abc");
Este código suele ir seguido de la pregunta, es decir, ¿cuántos objetos String se crean con esta línea de código? Creo que todo el mundo está familiarizado con esta pregunta y la respuesta es bien conocida: 2. A continuación, partiremos de esta pregunta y revisaremos algunos conocimientos de JAVA relacionados con la creación de objetos String.
Podemos dividir la línea de código anterior en cuatro partes: String str, =, "abc" y new String(). String str solo define una variable de tipo String llamada str, por lo que no crea un objeto; = inicializa la variable str y le asigna una referencia (o identificador) a un objeto, y obviamente no crea un objeto; ahora solo es nuevo. Se deja la cadena ("abc"). Entonces, ¿por qué se puede considerar la nueva Cadena ("abc") como "abc" y la nueva Cadena ()? Echemos un vistazo al constructor String que llamamos:
código java
Cadena pública (Cadena original) {
//otro código...
}
Como todos sabemos, existen dos métodos comúnmente utilizados para crear instancias (objetos) de una clase:
Utilice nuevo para crear objetos.
Llame al método newInstance de la clase Class y utilice el mecanismo de reflexión para crear el objeto.
Usamos new para llamar al método constructor anterior de la clase String para crear un objeto y asignar su referencia a la variable str. Al mismo tiempo, notamos que el parámetro aceptado por el método constructor llamado también es un objeto String, y este objeto es exactamente "abc". A partir de esto tenemos que introducir otra forma de crear un objeto String: texto entre comillas.
Este método es exclusivo de String y es muy diferente del nuevo método.
código java
Cadena cadena="abc";
No hay duda de que esta línea de código crea un objeto String.
código java
Cadena a="abc";
Cadena b="abc";
¿Qué pasa aquí? La respuesta sigue siendo una.
código java
Cadena a="ab"+"cd";
¿Qué pasa aquí? La respuesta sigue siendo una. ¿Un poco raro? En este punto, debemos introducir una revisión del conocimiento relacionado con el grupo de cadenas.
Hay un grupo de cadenas en la máquina virtual JAVA (JVM), que almacena muchos objetos String y se puede compartir, por lo que mejora la eficiencia. Dado que la clase String es final, su valor no se puede cambiar una vez creada, por lo que no tenemos que preocuparnos por la confusión del programa causada por compartir objetos String. El grupo de cadenas lo mantiene la clase String y podemos llamar al método intern () para acceder al grupo de cadenas.
Volvamos a String a="abc";. Cuando se ejecuta esta línea de código, la máquina virtual JAVA primero busca en el grupo de cadenas para ver si dicho objeto con el valor "abc" ya existe. El valor de retorno de la clase String es igual al método (Object obj). Si lo hay, no se creará ningún objeto nuevo y se devolverá directamente una referencia al objeto existente; de lo contrario, el objeto se creará primero, luego se agregará al grupo de cadenas y luego se devolverá su referencia. Por tanto, no nos resulta difícil entender por qué los dos primeros de los tres ejemplos anteriores tienen esta respuesta.
Para el tercer ejemplo:
código java
Cadena a="ab"+"cd";
Porque el valor de la constante se determina en tiempo de compilación. Aquí, "ab" y "cd" son constantes, por lo que el valor de la variable a se puede determinar en tiempo de compilación. El efecto compilado de esta línea de código es equivalente a:
código java
Cadena a="abcd";
Por lo tanto, aquí solo se crea un objeto "abcd" y se guarda en el grupo de cadenas.
Ahora surge nuevamente la pregunta: ¿se agregarán todas las cadenas obtenidas después de la conexión "+" al grupo de cadenas? Todos sabemos que "==" se puede usar para comparar dos variables. Tiene las dos situaciones siguientes:
Si se comparan dos tipos básicos (char, byte, short, int, long, float, double, boolean), se juzga si sus valores son iguales.
Si la tabla compara dos variables de objeto, se juzga si sus referencias apuntan al mismo objeto.
A continuación usaremos "==" para hacer algunas pruebas. Para facilitar la explicación, consideramos que apuntar a un objeto que ya existe en el grupo de cadenas es el objeto que se agrega al grupo de cadenas:
código java
public class StringTest { public static void main(String[] args) { String a = "ab";// Crea un objeto y agrégalo al grupo de cadenas System.out.println("String a = /"ab/"; "); String b = "cd";// Se crea un objeto y se agrega al grupo de cadenas System.out.println("String b = /"cd/";"); String c = "abcd"; // Se crea un objeto y se agrega al grupo de cadenas String d = "ab" + "cd" // Si d y c apuntan al mismo objeto, significa que d también se ha agregado al grupo de cadenas if (d == c ) { System.out.println("/"ab/"+/"cd/" El objeto creado/" se agrega a/" el grupo de cadenas" } // Si d y c no apuntan al mismo objeto, significa que d no se ha agregado al grupo de cadenas else { System.out.println("/"ab/"+/"cd/" El objeto creado/"no se agrega/" al grupo de cadenas"); c Apunta al mismo objeto, significa que e también se ha agregado al grupo de cadenas if (e == c) { System.out.println(" a +/"cd/" El objeto creado/cadena "joined/" grupo medio"); } // Si e y c no apuntan al mismo objeto, significa que e no se ha agregado al grupo de cadenas else { System.out.println(" a +/"cd/" objeto creado/"no agregado/" al string pool" ); } String f = "ab" + b; // Si f y c apuntan al mismo objeto, significa que f también se ha agregado al grupo de cadenas if (f == c) { System.out .println("/ Objeto creado por "ab/"+ b /"Agregado/" al grupo de cadenas"); } // Si f y c no apuntan al mismo objeto, significa que f no se ha agregado al grupo de cadenas else { System.out.println("/" ab/" + b objeto creado/"no agregado/" al grupo de cadenas"); } String g = a + b // Si gyc apuntan al mismo objeto, significa que g también se ha agregado al; grupo de cadenas if (g == c) { System.out.println(" a + b El objeto creado/"agregado/" al grupo de cadenas"); } // Si gyc no apuntan al mismo objeto, significa que g no se ha agregado al grupo de cadenas else { System.out.println (" a + b objeto creado/"no agregado/" al grupo de cadenas");
Los resultados de ejecución son los siguientes:
Cadena a = "ab";
Cadena b = "cd";
El objeto creado por "ab"+"cd" está "unido" al grupo de cadenas
El objeto creado por + "cd" "no se agrega" al grupo de cadenas.
El objeto creado por "ab"+ b "no se agrega" al grupo de cadenas.
El objeto creado por a + b "no se agrega" al grupo de cadenas. De los resultados anteriores, podemos ver fácilmente que solo se agregarán los objetos nuevos generados mediante el uso de conexiones "+" entre objetos de cadena creados usando comillas para incluir texto. . en el grupo de cuerdas. Para todas las expresiones de conexión "+" que contienen objetos creados en modo nuevo (incluido nulo), los nuevos objetos que generan no se agregarán al grupo de cadenas y no entraremos en detalles sobre esto.
Pero hay una situación que exige nuestra atención. Por favor mire el código a continuación:
código java
public class StringStaticTest { // Constante A public static final String A = "ab"; // Constante B public static final String B = "cd" public static void main(String[] args) { // Usa dos constantes +Connect; para inicializar s String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s es igual a t, son el mismo objeto"); { System.out.println("s no es igual a t, no son el mismo objeto");
El resultado de ejecutar este código es el siguiente:
s es igual a t. Son el mismo objeto. La razón es la siguiente. Para una constante, su valor es fijo, por lo que se puede determinar en tiempo de compilación, mientras que el valor de una variable solo se puede determinar en tiempo de ejecución, porque esta variable puede ser llamada por diferentes métodos, por lo que puede causar valor a cambiar. En el ejemplo anterior, A y B son constantes y sus valores son fijos, por lo que el valor de s también es fijo y se determina cuando se compila la clase. Es decir:
código java
Cadena s=A+B;
Equivalente a:
código java
Cadena s="ab"+"cd";
Permítanme cambiar ligeramente el ejemplo anterior y ver qué sucede:
código java
public class StringStaticTest { // Constante A public static final String A; // Constante B public static final static { A = "ab" = "cd"; public static void main(String[] args) { // Inicializa s conectando las dos constantes con + String s = A + B; String t = "abcd" if (s == t) { System.out.println("s es igual a t, son el mismo objeto"); } else { System.out.println("s no es igual a t, no son el mismo objeto");
El resultado de su funcionamiento es este:
s no es igual a t. No son el mismo objeto pero se han modificado ligeramente. El resultado es exactamente lo contrario del ejemplo anterior. Analicémoslo de nuevo. Aunque A y B se definen como constantes (solo se pueden asignar una vez), no se asignan inmediatamente. Antes de calcular el valor de s, cuándo se asignan y qué valor se asignan son todas variables. Por lo tanto, A y B se comportan como una variable antes de que se les asigne un valor. Entonces s no se puede determinar en tiempo de compilación, solo se puede crear en tiempo de ejecución.
Dado que compartir objetos en el grupo de cadenas puede mejorar la eficiencia, animamos a todos a crear objetos String incluyendo texto entre comillas. De hecho, esto es lo que usamos a menudo en programación.
A continuación, echemos un vistazo al método intern(), que se define de la siguiente manera:
código java
interno público nativo de cadena();
Este es un método nativo. Al llamar a este método, la máquina virtual JAVA primero verifica si ya existe un objeto con un valor igual al objeto en el grupo de cadenas. Si es así, devuelve una referencia al objeto en el grupo de cadenas; objeto en el grupo de cadenas. Objeto de cadena con el mismo valor y luego devuelve su referencia.
Veamos este código:
código java
public class StringInternTest { public static void main(String[] args) { // Use una matriz de caracteres para inicializar a para evitar que un objeto con el valor "abcd" ya exista en el grupo de cadenas antes de que se cree a String a = new String ( nuevo char[] { 'a', 'b', 'c', 'd' }); String b = a.intern(); System.out.println("b se agregó al grupo de cadenas, no se creó ningún objeto nuevo"); } else { System.out.println("b no se agregó al grupo de cadenas, no se creó ningún objeto nuevo"); } } }
Resultados de ejecución:
b no se ha agregado al grupo de cadenas y se ha creado un nuevo objeto. Si el método intern() de la clase String no encuentra un objeto con el mismo valor, agrega el objeto actual al grupo de cadenas y luego devuelve su. referencia, luego b y a Apunta al mismo objeto; de lo contrario, el objeto señalado por b es creado recientemente por la máquina virtual JAVA en el grupo de cadenas, pero su valor es el mismo que a. El resultado de ejecución del código anterior simplemente confirma este punto.
Finalmente, hablemos sobre el almacenamiento de objetos String en la Máquina Virtual JAVA (JVM) y la relación entre el grupo de cadenas y el montón y la pila. Primero revisemos la diferencia entre montón y pila:
Pila: guarda principalmente tipos básicos (o tipos integrados) (char, byte, short, int, long, float, double, boolean) y las referencias de objetos se pueden compartir, y su velocidad es solo superada por el registro. montón.
Montón: utilizado para almacenar objetos.
Cuando miramos el código fuente de la clase String, encontraremos que tiene un atributo de valor, que almacena el valor del objeto String. El tipo es char [], lo que también muestra que una cadena es una secuencia de caracteres.
Al ejecutar String a="abc";, la máquina virtual JAVA crea tres valores de caracteres 'a', 'b' y 'c' en la pila, y luego crea un objeto String en el montón, su valor (valor) es una matriz de tres valores de caracteres recién creados en la pila {'a', 'b', 'c'}. Finalmente, el objeto String recién creado se agregará al grupo de cadenas. Si luego ejecutamos el código String b=new String("abc"); dado que "abc" se creó y guardó en el grupo de cadenas, la máquina virtual JAVA solo creará un nuevo objeto String en el montón, pero es The. El valor son los tres valores de tipo char 'a', 'b' y 'c' creados en la pila cuando se ejecutó la línea de código anterior.
En este punto, ya tenemos bastante clara la pregunta de por qué String str=new String("abc") planteada al principio de este artículo crea dos objetos.