Tres características principales de C++ en detalle: el principio subyacente del polimorfismo

Tabla de contenido

Uno, el principio del polimorfismo.

1.1 Tabla de funciones virtuales

1.2 La implementación subyacente de la reescritura de funciones virtuales (cobertura)

1.3 La ubicación de almacenamiento de la nueva dirección de función virtual de la subclase

1.4 Ubicación de almacenamiento de la tabla virtual

 1.5 El principio del polimorfismo

1.6 Enlace dinámico y enlace estático

Segundo, herencia múltiple

2.1 Tabla de funciones virtuales de herencia múltiple

 2.2 La ubicación de almacenamiento de la nueva dirección de función virtual de la subclase

2.3 ¿Por qué las direcciones de funciones virtuales se reescriben en las dos tablas virtuales de manera diferente?

 Resumir


Preámbulo

El artículo anterior habló principalmente sobre el contenido básico y el uso del polimorfismo. Este artículo lo llevará a comprender los principios subyacentes del polimorfismo. Hay muchos experimentos en este artículo. Se recomienda que pueda experimentar con él usted mismo después de leerlo. definitivamente será aceptado Los bienes son abundantes.

Uno, el principio del polimorfismo.

1.1 Tabla de funciones virtuales

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "全价票" << endl;
	}

	int _a;
};

class Student :public Person
{
public:
	virtual void Buyticket()
	{
		cout << "半价票" << endl;
	}

	int _b;
};
int main()
{
	cout << sizeof(Person) << endl;
	return 0;
}

¿Los veteranos del código anterior pueden calcular el tamaño del espacio Persona?

Salió la respuesta, es 8, podemos mirar el tamaño de la función normal no virtual

 En la imagen de arriba, encontramos que el tamaño normal puede ser 4 como calculan la mayoría de los hierros antiguos, mientras que el que tiene funciones virtuales es 8, entonces, ¿dónde se usan los 4 bytes adicionales? Podemos depurar y echar un vistazo.

 Después de la depuración y la observación, descubrimos que hay un puntero adicional _vfptr delante del objeto, entonces, ¿cómo se llama este _vfptr? Este puntero se denomina puntero de tabla de función virtual (Puntero de función virtual), que apunta a la tabla de función virtual Al menos un puntero de tabla de función virtual apunta a la tabla de función virtual en una clase que contiene funciones virtuales, y la dirección de la función virtual se almacenará en la tabla de funciones virtuales Entonces, ¿qué hay en la tabla de subclases? Sigamos mirando hacia abajo.

Observando lo anterior, podemos encontrar que las tablas de funciones virtuales apuntadas por p y s son diferentes, y las dos son independientes.La tabla de funciones virtuales de la subclase almacena la dirección de la función virtual reescrita, que también es polimorfismo.Por lo tanto, La segunda condición importante para la implementación polimórfica debe ser llamar a funciones virtuales a través del puntero o referencia de la clase padre . Todos deben entender que, tomando como ejemplo la subclase, el puntero o referencia de la clase padre corta la subclase. Por lo tanto, los punteros y las referencias aún apuntan al espacio original y, por lo tanto, _vfptr almacena funciones virtuales reescritas por subclases, de modo que las llamadas a funciones virtuales de las subclases y las clases principales se pueden distinguir claramente .

Por el contrario, ¿por qué no se puede llamar por valor? Si llama por valor, necesita usar la sobrecarga de asignación. Normalmente, la tabla virtual no se copiará, por lo que siempre será la tabla virtual de la clase principal , y el polimorfismo no se puede realizar. Pero si queremos copiar la tabla virtual tabla, tal polimorfismo Puede darse cuenta, pero causará confusión.Por ejemplo, en la siguiente situación, como un objeto de clase principal, ¿la vtabla de p es una clase principal o una clase secundaria? Se convertirá en una subclase, ¿es esto normal? Un objeto de clase principal usa una tabla virtual de subclase, lo que obviamente es anormal.

int main()
{
	Person p;
	Student s;

	p = s;

	return 0;
}

A continuación, echemos un vistazo a por qué la reescritura de funciones virtuales también se puede llamar cobertura.

1.2 La implementación subyacente de la reescritura de funciones virtuales (cobertura)

class Person
{
public:
	virtual void Func1()
	{
		cout << "Func1" << endl;
	}

	virtual void Func2()
	{
		cout << "Func2" << endl;
	}

	int _a=1;
};

class Student :public Person
{
public:
	virtual void Func1()
	{
		cout << "Func1" << endl;
	}

	int _b=2;
};

int main()
{
	Person p;
	Student s;

	return 0;
}

 Tomemos el código anterior como ejemplo, en el ejemplo anterior, podemos ver que la subclase Studnet solo reescribe la función virtual Func1 de la clase padre Person, así que entremos al modo de depuración para ver cuáles son las tablas virtuales de Student y Person. .diferente

Al observar la figura anterior, encontramos que después de reescribir Func1 en la subclase , las direcciones de función virtual de Func1 en la tabla virtual principal y Func1 en la tabla virtual de la subclase son diferentes, mientras que la subclase Func2 no se reescribe. Func2 en la tabla virtual principal y la tabla virtual de la subclase es la misma .

Esto se debe a que después de que la subclase hereda la clase principal, la subclase vuelve a escribir Func1, por lo que el Func1 original de la clase principal en la tabla virtual de funciones se sobrescribe con la dirección de función virtual de Func1 reescrita por la subclase .

Por lo tanto, la reescritura de funciones virtuales también se denomina cobertura. La cobertura se refiere a la cobertura de funciones virtuales en la tabla virtual. La reescritura se denomina sintaxis y la cobertura se denomina principio subyacente .

1.3 La ubicación de almacenamiento de la nueva dirección de función virtual de la subclase

Después de hablar sobre el principio de reescritura, analicemos dónde se almacenan las nuevas funciones virtuales de las subclases. ¿Se almacena en la tabla virtual de la clase padre o se crea otra tabla virtual?

class Person
{
public:
	virtual void Func1()
	{
		cout << "Func1" << endl;
	}


	int _a=1;
};

class Student :public Person
{
public:
	virtual void Func1()
	{
		cout << "Func1" << endl;
	}

	virtual void Func2()
	{
		cout << "Func2" << endl;
	}

	int _b=2;
};

int main()
{
	Person p;
	Student s;

	return 0;
}

Tomemos el código anterior como ejemplo para explicar este problema. En el código anterior, el objeto de la subclase Studen reescribe el Func1 del objeto de la clase principal Person. Además, escribimos una nueva función virtual Func2 en la subclase. Esto es para observar la ubicación de almacenamiento de Func2.

Podemos depurar primero para observar si Func2 está almacenado en la tabla virtual, entonces ¿Func2 no está almacenado? Esto es imposible, porque Student también puede convertirse en otra clase padre, por lo que es imposible que su nueva función virtual no se almacene.

 

 Al observar la ventana de monitoreo, encontramos que en la tabla virtual solo se almacena Func1. ¿Es esto cierto? Para saber que la ventana de monitoreo ha sido procesada, podemos observar nuevamente la ventana de memoria.

 

 Observando la ventana de memoria, encontramos que el problema no es tan simple, la primera línea es la dirección de Func1, y la dirección de la segunda línea es muy cercana a Func1, entonces es posible que esta también sea la dirección de un virtual ¿función? ¿Cuál es la dirección de Func2?

A continuación, escribamos un pequeño programa que imprima la tabla virtual para probarlo.

 

 El resultado de la impresión es el siguiente

 Al observar los resultados impresos, encontramos que la dirección de la segunda línea es de hecho la dirección de la función virtual recién escrita Func2 de la clase Student.

Por lo tanto, concluimos que aunque la función virtual recién escrita del objeto de la subclase no se puede ver en la ventana de monitoreo, sí está almacenada en la tabla virtual .

 Entonces tenemos otra pregunta, ¿dónde se almacena la tabla virtual?

1.4 Ubicación de almacenamiento de la tabla virtual

El espacio de memoria se divide aproximadamente en las siguientes áreas. Los veteranos pueden adivinar en qué área está almacenada la tabla virtual.

 El método que usamos para experimentar aquí es escribir y crear cuatro variables, almacenarlas en la pila, el montón, el área estática y el área constante respectivamente, y luego tomar sus direcciones y compararlas con las direcciones de la tabla virtual para inferir la tabla virtual La ubicación donde se almacena la tabla .

class Person
{
public:
	virtual void Func1()
	{
		cout << "Func1" << endl;
	}


	int _a = 1;
};

class Student :public Person
{
public:
	virtual void Func1()
	{
		cout << "Func1" << endl;
	}

	virtual void Func2()
	{
		cout << "Func2" << endl;
	}

	int _b = 2;
};

int main()
{
	int i;//栈
	int* ptr = new int;//堆
	static int a = 0;//静态区
	const char* b = "cccccccccc";//常量区

	Person p;
	Student s;

	printf("栈对象:%p\n", &i);
	printf("堆对象:%p\n", ptr);
	printf("静态区对象:%p\n", &a);
	printf("常量区对象:%p\n", b);
	printf("p虚函数表:%p\n", *((int*)&p));
	printf("s虚函数表:%p\n", *((int*)&s));


	return 0;
}

Los resultados de la prueba son los siguientes

A través de experimentos, encontramos que la ubicación de almacenamiento de la tabla de funciones virtuales está muy cerca del área constante

Puede verse a partir de esto que la ubicación donde se almacena la tabla de funciones virtuales es la misma que la ubicación donde se almacena la función virtual en el segmento de código, es decir, el área constante.

 1.5 El principio del polimorfismo

Hemos hablado mucho más arriba. Entonces, ¿cuál es el principio del polimorfismo?

De hecho, el principio fundamental de la implementación de polimorfismos es declarar la existencia de polimorfismos a través de virtuales, generar punteros de tablas virtuales y administrar funciones virtuales.Si el acceso es una función virtual, encuentre la entidad apuntada real a través del puntero/referencia y obtenga la tabla virtual en la entidad Puntero, acceda a la tabla virtual a través del puntero de la tabla virtual, encuentre el puntero de función virtual que se ejecutará en la tabla virtual y ejecute el comportamiento de la función específica a través del puntero de función virtual.

1.6 Enlace dinámico y enlace estático

1. El enlace estático, también conocido como enlace anticipado (early binding), determina el comportamiento del programa durante la compilación del programa, también conocido como polimorfismo estático , como por ejemplo: sobrecarga de funciones
2. El enlace dinámico, también conocido como enlace tardío (Late binding) es determinar el comportamiento específico del programa según el tipo específico obtenido durante la ejecución del programa, y ​​llamar a la función específica, también conocida como polimorfismo dinámico .

Segundo, herencia múltiple

Lo anterior se trata de la herencia simple como ejemplo. A continuación, hablemos de la tabla de funciones virtuales de la herencia múltiple.

2.1 Tabla de funciones virtuales de herencia múltiple

Antes de hablar de la tabla de funciones virtuales de herencia múltiple, puede pensar en una pregunta, ¿cuántas tablas virtuales hay en la subclase de herencia múltiple?

 

class Base1
{
public:
	virtual void Func1()
	{
		cout << "Base1:Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Base1:Func2" << endl;
	}

	int _a=1;
};
class Base2
{
public:
	virtual void Func1()
	{
		cout << "Base2:Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Base2:Func2" << endl;
	}

	int _b=2;
};

class Son :public Base1, public Base2
{
public:
	virtual void Func1()
	{
		cout << "Son:Func1" << endl;
	}

	int _c = 3;
};

int main()
{
	Son s;
	return 0;
}

Tomemos el código anterior como ejemplo para ver el modelo de memoria de herencia múltiple

 A través de la observación, encontramos que hay dos tablas de funciones virtuales en herencia múltiple, y el modelo de memoria es el siguiente

 2.2 La ubicación de almacenamiento de la nueva dirección de función virtual de la subclase

En el caso de herencia simple anterior, la nueva función virtual de la subclase se almacena en la tabla virtual de la clase principal, pero esta herencia múltiple tiene dos clases principales, entonces, ¿dónde existe la dirección de la nueva función virtual?

class Base1
{
public:
	virtual void Func1()
	{
		cout << "Base1:Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Base1:Func2" << endl;
	}

	int _a=1;
};
class Base2
{
public:
	virtual void Func1()
	{
		cout << "Base2:Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Base2:Func2" << endl;
	}

	int _b=2;
};

class Son :public Base1, public Base2
{
public:
	virtual void Func1()
	{
		cout << "Son:Func1" << endl;
	}


	virtual void Func3()
	{
		cout << "Son:Func3" << endl;
	}


	int _c = 3;
};

typedef void(*VF_PTR)();
//typedef void(*)() VF_PTR;上面定义的效果就和这个类似,
//但是由于是函数指针,所以只能用上面的定义方式
void print(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];//保存函数地址
		f();//对函数地址调用
	}
	cout << endl;
}

int main()
{
	Son s;


	return 0;
}

Aquí podemos verificar imprimiendo la tabla virtual antes. Pero aquí hay un problema, es decir, ¿cómo encontrar el puntero de Base2? El método que tomamos aquí es dividir y dividir Base2, y el puntero se desplazará automáticamente a la posición de Base2.

 De los resultados anteriores, podemos encontrar que en la herencia múltiple, la dirección de la función virtual recién creada de la subclase se almacena en la tabla virtual de la primera clase principal .

De los resultados del experimento anterior, no sé si los veteranos han encontrado un problema, es decir, reescribimos Func1 en la subclase, pero la dirección de Func1 en las dos tablas virtuales de Base1 y Base2 es diferente ¿Por qué?

2.3 ¿Por qué las direcciones de funciones virtuales se reescriben en las dos tablas virtuales de manera diferente?

Podemos cortar Base1 y Base2 por separado y llamar a Func1 para observar sus respectivas compilaciones y ver sus respectivas tendencias.

 Compilar los resultados experimentales de la siguiente manera

 Al observar los resultados, descubrimos que llamar a Func1 por ptr1 es una llamada directa, y el proceso de llamar a Func1 por ptr2 es realmente muy difícil, especialmente porque hay un sub 8 en el medio, ¿por qué?

De hecho, llamar a Func1 se llama en última instancia con *este puntero. Tenga en cuenta que *este puntero apunta a la clase Hijo. La razón por la que se puede llamar directamente a ptr1 es porque el lugar al que apunta ptr1 es el mismo que *este, por lo que puede ser directamente Llame a Func1, pero ptr2 no funciona. ptr2 apunta a la posición de Base2, que no se superpone con la posición señalada por *this. Se encapsulará, ajuste la posición señalada por ptr2 a la posición señalada por *this, y luego llamar a Func1. Es decir, hay un comando -8 en el medio para desempeñar este papel .

 

 Resumir

Lo anterior es todo el contenido de la parte del principio polimórfico. Los experimentos anteriores son completados por bloggers. Espero que la gente de hierro pueda ganar algo.

Supongo que te gusta

Origin blog.csdn.net/zcxmjw/article/details/129978980
Recomendado
Clasificación