Três características principais do C++ em detalhes - o princípio subjacente do polimorfismo

Índice

Um, o princípio do polimorfismo

1.1 Tabela de função virtual

1.2 A implementação subjacente da reescrita da função virtual (cobertura)

1.3 O local de armazenamento do novo endereço de função virtual da subclasse

1.4 Local de armazenamento da mesa virtual

 1.5 O princípio do polimorfismo

1.6 Vinculação dinâmica e vinculação estática

Em segundo lugar, herança múltipla

2.1 Tabela de funções virtuais de herança múltipla

 2.2 O local de armazenamento do novo endereço de função virtual da subclasse

2.3 Por que os endereços das funções virtuais reescritos nas duas tabelas virtuais são diferentes?

 Resumir


Preâmbulo

O artigo anterior falou principalmente sobre o conteúdo básico e o uso do polimorfismo. Este artigo o levará a entender os princípios subjacentes do polimorfismo. Há muitos experimentos neste artigo. É recomendável que você possa experimentá-lo depois de lê-lo. Ele irá definitivamente ser aceito. Os bens são abundantes.

Um, o princípio do polimorfismo

1.1 Tabela de função virtual

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;
}

Os veteranos do código acima podem calcular o tamanho do espaço Person?

A resposta saiu, é 8, podemos ver o tamanho da função não virtual normal

 Na imagem acima, descobrimos que o tamanho normal pode ser 4 como a maioria dos ferros antigos calcula, enquanto aquele com funções virtuais é 8, então onde estão os 4 bytes extras usados? Podemos depurar e dar uma olhada

 Após depuração e observação, descobrimos que há um ponteiro extra _vfptr na frente do objeto, então como é chamado esse _vfptr? Este ponteiro é chamado de ponteiro de tabela de função virtual (Virtual Function Pointer), que aponta para a tabela de função virtual. Pelo menos um ponteiro de tabela de função virtual aponta para a tabela de função virtual em uma classe contendo funções virtuais e o endereço da função virtual será armazenado na tabela de função virtual.Então , o que está na tabela de subclasse, vamos continuar a olhar para baixo.

Observando o exposto, podemos constatar que as tabelas de funções virtuais apontadas por p e s são diferentes, e as duas são independentes. A tabela de funções virtuais da subclasse armazena o endereço da função virtual reescrita, que também é polimorfismo. Portanto, a segunda condição importante para implementação polimórfica deve ser chamar funções virtuais através do ponteiro ou referência da classe pai . Todos devem entender que, tomando a subclasse como exemplo, o ponteiro ou referência da classe pai fatia a subclasse. Portanto, ponteiros e as referências ainda apontam para o espaço original e, portanto, _vfptr armazena funções virtuais reescritas por subclasses, para que as chamadas de função virtual de subclasses e classes pai possam ser claramente distinguidas .

Por outro lado, por que não pode ser chamado por valor? Se você chamar por valor, você precisa usar sobrecarga de atribuição. Normalmente, a tabela virtual não será copiada, então sempre será a tabela virtual da classe pai e o polimorfismo não pode ser realizado. Mas se quisermos copiar o virtual tabela, tal polimorfismo pode ser percebido, mas causará confusão Por exemplo, na seguinte situação, como um objeto de classe pai, a vtable de p é uma classe pai ou uma classe filha? Ele se tornará uma subclasse, isso é normal?Um objeto de classe pai usa uma tabela virtual de subclasse, o que obviamente é anormal.

int main()
{
	Person p;
	Student s;

	p = s;

	return 0;
}

A seguir, veremos por que a reescrita de funções virtuais também pode ser chamada de cobertura

1.2 A implementação subjacente da reescrita da função virtual (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;
}

 Vamos pegar o código acima como exemplo. No exemplo acima, podemos ver que a subclasse Studnet apenas reescreve a função virtual Func1 da classe pai Person, então vamos entrar no modo de depuração para ver quais são as tabelas virtuais de Student e Person . diferente.

Observando a figura acima, descobrimos que após reescrever Func1 na subclasse , os endereços da função virtual de Func1 na tabela virtual pai e Func1 na tabela virtual da subclasse são diferentes, enquanto a subclasse Func2 não é reescrita. Func2 na tabela virtual pai e na tabela virtual da subclasse é o mesmo .

Isso ocorre porque depois que a subclasse herda a classe pai, a subclasse reescreve Func1, então o Func1 original da classe pai na tabela virtual de funções é substituído pelo endereço da função virtual de Func1 reescrito pela subclasse .

Portanto, a reescrita de funções virtuais também é chamada de cobertura. A cobertura refere-se à cobertura de funções virtuais na tabela virtual. A reescrita é chamada de sintaxe e a cobertura é chamada de princípio subjacente .

1.3 O local de armazenamento do novo endereço de função virtual da subclasse

Depois de falar sobre o princípio da reescrita, vamos discutir onde as novas funções virtuais das subclasses são armazenadas? Ele é armazenado na tabela virtual da classe pai ou outra tabela virtual é criada?

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;
}

Vamos usar o código acima como um exemplo para explicar este problema. No código acima, o objeto de subclasse Studen reescreve o Func1 do objeto de classe pai Person. Além disso, escrevemos uma nova função virtual Func2 na subclasse. Isso é para observar o local de armazenamento de Func2.

Podemos depurar primeiro para observar se Func2 está armazenado na tabela virtual, então Func2 não está armazenado? Isso é impossível, porque Aluno também pode se tornar outra classe pai, então é impossível que sua nova função virtual não seja armazenada.

 

 Ao observar a janela de monitoramento, verificamos que apenas Func1 está armazenado na tabela virtual. É verdade? Para saber que a janela de monitoramento foi processada, podemos observar novamente a janela de memória

 

 Observando a janela de memória, descobrimos que o problema não é tão simples. A primeira linha é o endereço de Func1, e o endereço da segunda linha é muito próximo de Func1. Portanto, é possível que este também seja o endereço de um virtual função? Qual é o endereço de Func2?

Em seguida, vamos escrever um pequeno programa que imprima a tabela virtual para provar isso

 

 O resultado da impressão é o seguinte

 Observando os resultados impressos, descobrimos que o endereço da segunda linha é de fato o endereço da função virtual recém-escrita Func2 da classe Aluno.

Portanto, concluímos que, embora a função virtual recém-escrita do objeto da subclasse não possa ser vista na janela de monitoramento, ela está de fato armazenada na tabela virtual .

 Então temos outra pergunta, onde fica armazenada a tabela virtual?

1.4 Local de armazenamento da mesa virtual

O espaço de memória é dividido aproximadamente nas seguintes áreas. Os veteranos podem adivinhar em qual área a mesa virtual está armazenada.

 O método que usamos para experimentar aqui é escrever e criar quatro variáveis, armazená-las na pilha, heap, área estática e área constante, respectivamente, e então pegar seus endereços e comparar seus endereços com os endereços da tabela virtual para inferir a tabela virtual O local onde a tabela está armazenada .

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;
}

Os resultados do teste são os seguintes

Por meio de experimentos, descobrimos que o local de armazenamento da tabela de função virtual é muito próximo da área constante

Pode-se ver a partir disso que o local onde a tabela de funções virtuais é armazenada é o mesmo que o local onde a função virtual é armazenada no segmento de código, ou seja, a área constante.

 1.5 O princípio do polimorfismo

Falamos tanto acima. Então, qual é o princípio do polimorfismo?

De fato, o princípio fundamental da implementação do polimorfismo é declarar a existência do polimorfismo por meio do virtual, gerar ponteiros de tabela virtual e gerenciar funções virtuais . Se o acesso for uma função virtual, encontre a entidade real apontada por meio do ponteiro/referência e obtenha a tabela virtual na entidade. Ponteiro, acesse a tabela virtual por meio do ponteiro de tabela virtual, encontre o ponteiro de função virtual a ser executado na tabela virtual e execute o comportamento específico da função por meio do ponteiro de função virtual.

1.6 Vinculação dinâmica e vinculação estática

1. A ligação estática, também conhecida como ligação inicial (early binding), determina o comportamento do programa durante a compilação do programa, também conhecido como polimorfismo estático , como: sobrecarga de função
2. Ligação dinâmica, também conhecida como ligação tardia ( Late binding) é determinar o comportamento específico do programa de acordo com o tipo específico obtido durante a execução do programa e chamar a função específica, também conhecida como polimorfismo dinâmico .

Em segundo lugar, herança múltipla

O exemplo acima é sobre herança única. Em seguida, vamos falar sobre a tabela de função virtual de herança múltipla.

2.1 Tabela de funções virtuais de herança múltipla

Antes de falar sobre a tabela de funções virtuais de herança múltipla, você pode pensar em uma pergunta: quantas tabelas virtuais existem na subclasse de herança múltipla?

 

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;
}

Vamos pegar o código acima como exemplo para ver o modelo de memória de herança múltipla

 Através da observação, descobrimos que existem duas tabelas de funções virtuais em herança múltipla, e o modelo de memória é o seguinte

 2.2 O local de armazenamento do novo endereço de função virtual da subclasse

No caso de herança simples acima, a nova função virtual da subclasse é armazenada na tabela virtual da classe pai, mas essa herança múltipla tem duas classes pais, então onde está o endereço da nova função 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;
}

Aqui podemos verificar imprimindo a tabela virtual antes. Mas há um problema aqui, ou seja, como encontrar o ponteiro da Base2? O método que adotamos aqui é fatiar e fatiar Base2, e o ponteiro se deslocará automaticamente para a posição de Base2.

 A partir dos resultados acima, podemos descobrir que na herança múltipla, o endereço da função virtual recém-criada da subclasse é armazenado na tabela virtual da primeira classe pai .

Pelos resultados do experimento acima, não sei se os veteranos encontraram algum problema, ou seja, reescrevemos Func1 na subclasse, mas o endereço de Func1 nas duas tabelas virtuais de Base1 e Base2 é diferente. Por quê?

2.3 Por que os endereços das funções virtuais reescritos nas duas tabelas virtuais são diferentes?

Podemos dividir Base1 e Base2 separadamente e chamar Func1 para observar suas respectivas compilações para ver suas respectivas direções.

 Compile os resultados experimentais da seguinte forma

 Observando os resultados, descobrimos que chamar Func1 por ptr1 é uma chamada direta, e o processo de chamar Func1 por ptr2 é realmente muito difícil, especialmente se houver um sub 8 no meio, por que isso?

Na verdade, chamar Func1 é, em última análise, chamado por *este ponteiro. Observe que *este ponteiro aponta para a classe Filho. A razão pela qual ptr1 pode ser chamado diretamente é porque o local apontado por ptr1 é o mesmo que *this, então pode ser diretamente Chame Func1, mas ptr2 não funciona. ptr2 aponta para a posição de Base2, que não se sobrepõe com a posição apontada por *this. Será encapsulado, ajuste a posição apontada por ptr2 para a posição apontada por *this, e depois chame Func1. Ou seja, há um comando -8 no meio para desempenhar esta função .

 

 Resumir

O conteúdo acima é todo o conteúdo da parte do princípio polimórfico. Os experimentos acima são concluídos por blogueiros. Espero que o povo de ferro possa ganhar algo

Acho que você gosta

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