Здравствуйте VVV, Вы писали:
VVV>Если посмотреть с другой стороны, то любой вызов C++ функции можно описать в терминам C функции, т.е. это будет функция, где первым параметром передаётся указатель на какую-то структуру данных (this). Посему, при вызове такой функции из C этот параметр (this) можно задать каким угодно (и NULL в том числе).
В том то и дело, что тут есть одна существенная тонкость, из-за которой вызов метода объекта в С++
нельзя так запросто описать в терминах C функций.
Предположим, что у нас имеется вот такой класс A
struct A
{
void foo(int i) { cout << (this != NULL ? "not NULL" : "NULL") << " " << i << endl; }
};
Перепишем метод 'A::foo' в виде "эквивалентной" С функции, как ты предлагаешь (если я тебя правильно понял):
void foo(A* this_, int i) { cout << (this_ != NULL ? "not NULL" : "NULL") << " " << i << endl; }
Пусть от класса A и класса B множественно унаследован класс C:
struct B { int x; };
struct C : B, A {};
Рассмотрим вот такой вызов метода 'foo' через null-указатель типа 'C*':
C* p = NULL;
p->foo(5); // (1)
Как будет выглядеть соответствующий вызов С функции 'foo'? Он будет выглядеть вот так:
foo(p, 5); // (2)
Эквивалентны ли эти вызовы (закроем глаза на неопределенное поведение в вызове (1))? Нет, не эквивалентны. При компиляции, например, MSVC++, эти вызовы распечатают разные результаты на экране. Почему так происходит?
Происходит это потому, что в вызове (2) компилятор вынужден сделать стандартную pointer conversion — конверсию типа первого параметра от типа 'C*' к типу 'A*'. При этом, согласно спецификации языка С++, null-указатель одного типа должен быть переведен в null-указатель другого типа. По этой причине в результате вызова (2) внутрь функции 'foo' будет передан null-указатель и неравенство 'this_ != NULL' не выполнится.
А в вызове (1) ситуация совершенно иная. Для того, чтобы взывать метод класса A через указатель типа 'C*', этот указатель тоже необходимо определенным образом "преобразовать". То, что происходит с указателем в таком случае, не является никакой "официальной" конверсией языка С++. Компилятор обязан просто как-то "сдвинуть" указатель типа 'C*' так, чтобы он стал указывать на экземпляр класса A внутри экземпляра класса C. Как компиялтор будет это делать — детали реализации. Никаких требований о том, чтобы null-указатель одного типа при этом первращался именно в null-указатель другого типа, нет и никогда не было. Эти требования не нужны по той простой причине, что стандарт С++ целиком и полностью запрещает такие вызовы. Ни один уважающий себя компилятор не будет тратить время на проверку равенства указателя NULL. Компилятор просто сдвинет указатель на определенное количесво байт. При этом null-указатель перестанет быть null-указателем. По этой самой причине в результате вызова (1) внутри метода 'A::foo' указатель this не будет null-указателем и неравенство 'this_ != NULL' выполнится.
Вот если бы мы сами заставили компилятор выполнить полноценную конверсию указателя при вызове метода
((A*) p)->foo(5); // (3)
тогда результаты выполнения вызовов (2) и (3) совпали бы. Но пользоваться в программах именно такими вызовами только ради того, чтобы сохранить "правильность" преобразования null-указателей, никто, разумеется, не будет. К тому же это потенциальный удар по производительности.
Отсюда мораль: с ситауациях с множественным наследованием (а согласно стандарту языка — в ситуациях с любым наследованием) не надо думать о вызовах методов классов, как о вызовах обычных С функций с дополнительным параметром. Это несколько разные вещи.