Как работают виртуальные функции C++

C++

Статическое связывание и динамическое связывание

При обсуждении статической и динамической привязки первое, что нужно понять, это привязка. Связь между вызовом функции и самой функцией, а также связь между доступом к членам и адресами памяти переменных называется связыванием. Поняв привязку, поймите статику и динамику.

  • Статическая привязка: относится к процессу объединения вызова функции с кодом, необходимым для ответа на вызов во время компиляции программы, что называется статической привязкой. происходит во время компиляции.
  • Динамическое связывание: относится к оценке фактического типа объекта, на который ссылаются, во время выполнения и вызову соответствующего метода в соответствии с фактическим типом. Процесс объединения вызова функции с кодом, необходимым для ответа на вызов во время выполнения программы, называется динамическим связыванием. Возникает во время выполнения.

Динамическое связывание в C++

В C++ динамическое связывание реализуется через виртуальные функции, что является особой формой полиморфизма. Виртуальная функция реализуется через таблицу виртуальных функций. Эта таблица записывает адреса виртуальных функций, решает проблему наследования и покрытия и обеспечивает возможность вызова правильной функции в соответствии с фактическим типом объекта во время динамического связывания. Где эта таблица виртуальных функций? Стандартная спецификация C++ говорит, что компилятор должен гарантировать, что указатель на таблицу виртуальных функций существует в начале экземпляра объекта (это необходимо для того, чтобы смещение виртуальной функции было выбрано правильно). То есть мы можем получить эту виртуальную таблицу функций через адрес экземпляра объекта, а затем мы можем пройти по указателю функции в ней и вызвать соответствующую функцию.

Как работают виртуальные функции

Чтобы понять динамическое связывание, необходимо понять принцип работы виртуальных функций. Реализованные в C++ виртуальные функции обычно реализуются таблицей виртуальных функций (в спецификации C++ не указано, какой именно метод используется, но большинство производителей компиляторов выбрали именно этот метод). Таблица виртуальных функций класса представляет собой одну непрерывную память, адрес инструкции JMP, записанный в каждой ячейке памяти. Компилятор создает таблицу виртуальных функций для каждого класса, имеющего виртуальные функции, и эта таблица виртуальных функций будет совместно использоваться всеми объектами этого класса. Каждая таблица виртуальных функций класса-члена занимает виртуальную строку. Если имеется виртуальная функция класса N, то таблица виртуальных функций будет иметь размер N * 4 байта.

Виртуальная функция реализуется через таблицу виртуальных функций.В этой таблице это в основном таблица адресов виртуальной функции класса., эта таблица решает проблему наследования и покрытия и гарантирует, что она действительно отражает реальную функцию. Таким образом, память указателя на эту таблицу (расположенную перед экземпляром объекта) выделяется в экземпляре класса с виртуальными функциями, поэтому, когда указатель родительского класса используется для работы с подклассом, этот таблица виртуальных функций Особенно важно указать фактическую функцию, которая должна быть вызвана. Как это указано? Расскажу об этом позже.

Инструкция JMP — это инструкция безусловного перехода на языке ассемблера, и инструкция безусловного перехода может перейти к любому сегменту программы в памяти. Адрес перехода может быть указан в инструкции, он также может быть указан в регистре или указан в памяти.

Сначала мы определяем базовый класс с виртуальными функциями

class Base
{
public:
	virtual void fun1(){
		cout<<"base fun1!\n";
	}
	virtual void fun2(){
		cout<<"base fun2!\n";
	}
	virtual void fun3(){
		cout<<"base fun3!\n";
	}

	int a;
};

Просмотр его макета памяти

这里写图片描述
Мы можем видеть, что в макете памяти базового класса указатель таблицы виртуальной функции хранится в первом месте, и в следующем является переменная элемента базы. Кроме того, существует таблица виртуальной функции, которая хранит все виртуальные функции базового класса.

Поскольку указатель таблицы виртуальных функций обычно помещается перед экземпляром объекта, мы должны иметь возможность доступа к таблице виртуальных функций через код и углубить наше понимание таблицы виртуальных функций с помощью следующего кода:

#include "stdafx.h"
#include<iostream>
using namespace std;

class Base {
public:
	virtual void fun1(){
		cout<<"base fun1!\n";
	}
	virtual void fun2(){
		cout<<"base fun2!\n";
	}
	virtual void fun3(){
		cout<<"base fun3!\n";
	}

	int a;
};

int _tmain(int argc, _TCHAR* argv[])
{
	typedef void(*pFunc)(void);
	Base b;
	cout<<"虚函数表指针地址:"<<(int*)(&b)<<endl;

	//对象最前面是指向虚函数表的指针,虚函数表中存放的是虚函数的地址
	pFunc pfun;
	pfun=(pFunc)*((int*)(*(int*)(&b)));  //这里存放的都是地址,所以才一层又一层的指针
	pfun();
	pfun=(pFunc)*((int*)(*(int*)(&b))+1);
	pfun();
	pfun=(pFunc)*((int*)(*(int*)(&b))+2);
	pfun();

	system("pause");
	return 0;
}

результат операции:

这里写图片描述

Благодаря этому примеру я достаточно хорошо разобрался с указателями таблиц виртуальных функций и таблицами виртуальных функций. Давайте немного углубимся. Как C++ использует указатели базового класса и виртуальные функции для достижения полиморфизма? Здесь нам нужно понять, как таблица виртуальных функций работает в контексте наследования. В настоящее время понимается только одиночное наследование, что касается виртуального наследования, то множественное наследование будет понято позже. Единый код наследования выглядит следующим образом:

class Base {
public:
	virtual void fun1(){
		cout<<"base fun1!\n";
	}
	virtual void fun2(){
		cout<<"base fun2!\n";
	}
	virtual void fun3(){
		cout<<"base fun3!\n";
	}

	int a;
};

class Child:public Base {
public:
	void fun1(){
		cout<<"Child fun1\n";
	}
	void fun2(){
		cout<<"Child fun2\n";
	}
	virtual void fun4(){
		cout<<"Child fun4\n";
	}
};

Сравнение схемы памяти:

这里写图片描述
这里写图片描述
В сравнении мы видим:

  • При одиночном наследовании класс Child перезаписывает одноименную виртуальную функцию в классе Base, что отражается в таблице виртуальных функций, поскольку соответствующая позиция заменяется новой функцией в классе Child, а функция, которая не переопределяется остается неизменной.
  • Для подклассов собственная виртуальная функция, непосредственно присоединенная к таблице виртуальных функций.

Кроме того, мы заметили, что есть только один указатель vfptr (указывающий на другую таблицу виртуальных функций) в классе Child и классе Base, Как мы сказали ранее, этот указатель указывает на таблицу виртуальных функций, и мы выводим vfptr класса Child и класс Base соответственно. :

int _tmain(int argc, _TCHAR* argv[]) {
	typedef void(*pFunc)(void);
	Base b;
	Child c;
	cout<<"Base类的虚函数表指针地址:"<<(int*)(&b)<<endl;
	cout<<"Child类的虚函数表指针地址:"<<(int*)(&c)<<endl;

	system("pause");
	return 0;
}

результат операции:

这里写图片描述

Видно, что класс Child и класс Base имеют свои собственные указатель таблицы виртуальных функций vfptr и таблицу виртуальных функций vftable соответственно.

Следующий код показывает, что родительский класс и базовый класс имеют разные таблицы виртуальных функций, один и тот же класс имеет одну и ту же таблицу виртуальных функций и адреса разных объектов одного и того же класса (адреса, по которым хранятся указатели таблицы виртуальных функций). разные.

int _tmain(int argc, _TCHAR* argv[]) {
	Base b;
	Child c1,c2;
	cout<<"Base类的虚函数表的地址:"<<(int*)(*(int*)(&b))<<endl;
	cout<<"Child类c1的虚函数表的地址:"<<(int*)(*(int*)(&c1))<<endl;	//虚函数表指针指向的地址值
	cout<<"Child类c2的虚函数表的地址:"<<(int*)(*(int*)(&c2))<<endl;

	system("pause");
	return 0;
}

результат операции:

这里写图片描述

При определении объекта производного класса сначала вызовите конструктор его базового класса, затем инициализируйте vfptr и, наконец, вызовите конструктор производного класса (с бинарной точки зрения, так называемый подкласс базового класса представляет собой большую структуру, в который Четыре байта в начале указателя this хранят указатель таблицы виртуальных функций.При выполнении конструктора подкласса сначала вызывается конструктор базового класса, указатель this используется как параметр, а конструктор базового класса заполняется с vfptr базового класса, а затем вернитесь к конструктору подкласса, заполните vfptr подкласса и перезапишите vfptr, заполненный базовым классом. Таким образом, инициализация vfptr завершена). То есть vfptr указывает на vftable, что происходит во время конструктора.

Пример динамической привязки:

#include "stdafx.h"
#include<iostream>
using namespace std;

class Base {
public:
	virtual void fun1(){
		cout<<"base fun1!\n";
	}
	virtual void fun2(){
		cout<<"base fun2!\n";
	}
	virtual void fun3(){
		cout<<"base fun3!\n";
	}

	int a;
};

class Child:public Base {
public:
	void fun1(){
		cout<<"Child fun1\n";
	}
	void fun2(){
		cout<<"Child fun2\n";
	}
	virtual void fun4(){
		cout<<"Child fun4\n";
	}
};


int _tmain(int argc, _TCHAR* argv[])
{
	Base* p=new Child;
	p->fun1();
	p->fun2();
	p->fun3();

	system("pause");
	return 0;
}

результат операции:

这里写图片描述
В сочетании с расположением памяти выше:
这里写图片描述

Фактически, объект подкласса создается при создании нового Child.Как упоминалось выше, объект подкласса дополняет указатель таблицы виртуальных функций vfptr указывает на таблицу виртуальных функций класса Child во время конструктора и назначает адрес этого объекта для Базовый тип.Указатель p из , когда вызывается p->fun1(), оказывается виртуальной функцией, и указатель виртуальной функции вызывается для поиска адреса соответствующей виртуальной функции в таблице виртуальных функций, вот &Child::fun1. То же верно и для вызова p->fun2(). При вызове p->fun3() подкласс не перезаписывает виртуальную функцию родительского класса, но по-прежнему просматривает таблицу виртуальных функций, вызывая указатель виртуальной функции, и обнаруживает, что соответствующий адрес функции — &Base::fun3. Таким образом, результат вышеуказанной операции показан на рисунке выше.

Теперь вы поняли, почему указатель базового класса на экземпляр подкласса может вызывать функцию подкласса (виртуальную)? В каждом экземпляре объекта есть указатель vfptr.Компилятор сначала выберет значение vfptr, которое является адресом таблицы виртуальных функций vftable, а затем вызовет целевую функцию в vftable в соответствии с этим значением. Следовательно, пока vfptr отличается, таблица виртуальных функций, на которую указывает vftable, отличается, и адреса виртуальных функций соответствующих классов хранятся в разных таблицах виртуальных функций, таким образом реализуя «эффект» полиморфизма.

Обратите внимание на общедоступную учетную запись WeChat, продвигайте внутреннюю разработку, блокчейн и другие технологии!