телевизори. Конзоли. Проектори и аксесоари. Технологии. Цифрова телевизия

Късно подвързване. Късно свързване с COM компоненти

Късно робство с COM компоненти

Преди клиентският изпълним файл да може да извика методите и свойствата на съставен обект, той трябва да знае адресите на паметта на тези методи и свойства. Има две различни технологии, които клиентските програми могат да използват, за да определят тези адреси.

Ранно свързаните програми научават адреси в началото на процеса на компилиране/изпълнение – по време на компилация. Когато се компилира програма за ранно свързване, компилаторът използва библиотеката с типове на компонента, за да включи адресите на методите и свойствата на компонента в изпълнимия файл на клиента, така че адресите да могат да бъдат достъпни много бързо и без грешки. далеко използвайте ранно обвързване.

От своя страна програмите с късно свързване научават адресите на свойствата и методите късно в процеса на компилиране/изпълнение. в момента, в който се извикват тези свойства и методи. Късно обвързаният код обикновено осъществява достъп до клиентски обекти чрез основни типове данни, като например обект, и използва средата за изпълнение, за да определя динамично адресите на методите. Въпреки че кодът с късно свързване позволява използването на някои сложни техники за програмиране като полиморфизъм, той идва с някои свързани разходи, които ще видим скоро.

Но първо, нека проверим как се извършва късното свързване с помощта на отражение в C# (Отражението е начин, който кодът използва по време на изпълнение, за да определи информация за интерфейсите на класове от страна на сървъра; вижте глава 5.)

При късно свързване към COM обект в C# програма, не е необходимо да създавате RCW за COM компонент. Вместо това се извиква методът на клас GetTypeFromProgID на класа Type, за да създаде обект, който представлява типа COM обект. Класът Type е член на пространството от имена System.Runtime.InteropServices и в кода по-долу конфигурираме обект Type за същия компонент за достъп до COM данни, използван в предишните примери:


Тип objCustomerTableType;

Когато има Type обект, който капсулира информация за типа на COM обект, той се използва за създаване на екземпляр на самия COM обект. Това се постига чрез предаване на Type обект към метода CreateInstance на класа Activator.CreateInstance създава екземпляр на COM обект и връща късно свързваща препратка към него, която може да бъде съхранена в препратка към тип обект.

обект objCustomerTable;
objCustomerTable = Activator.CreateInstance(objCustomerTableType);

За съжаление, не е възможно да се извикват методи директно върху препратка от тип object. За да имате достъп до COM обект, трябва да използвате метода InvokeMember на обекта Type, който е създаден първи. Когато се извика методът InvokeMember, му се предава препратка към COM обект заедно с името на извиквания COM метод, както и масив от тип обект от всички входящи аргументи на метода.

ObjCustomerTableType.InvokeMember("Изтриване", BindingFlags.InvokeMethod, null, objCustomerTable, aryInputArgs);

Нека отново да си припомним последователността от действия:

1. Създайте обект Type за тип COM обект, като използвате метода на класа Type.GetTypeFromProgID() .

2. Използвайте този Type обект, за да създадете COM обект с помощта на Activator.CreateInstance() .

3. Методите се извикват на COM обект чрез извикване на метода InvokeMember на обекта Type и предаване на препратка към обект като входен аргумент. По-долу е примерен код, който комбинира всичко това в един блок:

използване на System.Runtime.InteropServices;
Тип objCustomerTableType;
обект objCustomerTable;
objCustomerTableType=Type.GetTypeFromProgID("DataAccess.CustomerTable");
objCustomerTable=Activator.CreateInstance(ObjCustomerTableType);
objCustomerTableType.InvokeMember("Изтриване", BindingFlags, InvokeMethod, null, objCustomerTable, aryInputArgs);
objCustomerTableType = Type.GetTypeFromProgID("DataAccess.CustomerTable");

Въпреки че функциите за късно свързване на C# избягват трудностите на RCW, има някои недостатъци, свързани с него.

Първо: късното свързване може да бъде опасно. Когато се използва ранно свързване, компилаторът може да направи запитване до библиотеката с типове на COM компонента, за да гарантира, че всички методи, извикани на COM обекти, действително съществуват. При късното свързване няма нищо, което да попречи на правописна грешка в извикването на InvokeMember() да причини грешка по време на изпълнение.

2

Да кажем, че няма функция Hello и ние просто извикваме ob.display основно, тогава тя извиква функцията за показване от клас B, а не от клас A.

Извикването на display() се задава веднъж от компилатора на версията, дефинирана в базовия клас. Това се нарича разрешаване на статично извикване на функция или статично обвързване - извикването на функцията се извършва преди изпълнението на програмата. Това също понякога се нарича ранно свързване, тъй като функцията display() е посочена по време на компилиране на програмата.

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

Сега в тази програма предаването на обекта като извикване по стойност, извикване чрез указател и извикване чрез препратка към функцията Hello работи добре. Сега, ако използваме Polymorphism и искаме да изобразим функция-член на производен клас, ако бъде извикана, трябва да добавим ключова думавиртуален преди функцията за картографиране на база данни. Ако предавате стойността на обект при извикване с помощта на указател и извикване по референция, това е извикване на функция в производния клас, но ако предавате обекта по стойност, това не е защо е така?>

Клас A ( public: void display(); // виртуален void display() ( cout<< "Hey from A" <display() ) int main() ( B obj; Hello(obj); // obj //&ob return 0; )

  • 2 отговора
  • Сортиране:

    Дейност

4

сега как може да извика функцията за показване на производния клас, без да използва виртуалната ключова дума (късно обвързване) преди функцията за показване в основния клас?

Невиртуална функция просто се разрешава от компилатора според статичния тип на обекта (или препратка или указател), който извиква. Така даден обект от производен тип, както и препратка към неговия подобект:

Bb; A&a=b;

ще получите различни резултати от извикване на невиртуална функция:

B.display(); // извиква се като B a.display(); // наречен като A

Ако знаете истинския тип, тогава можете да посочите как искате да наречете тази версия:

Static_cast (a).display(); // наричан като B

но това, което би било ужасно погрешно е, ако обектът a препраща няма тип B.

Сега, ако използваме Polymorphism и искаме да картографираме функция-член на производен клас, ако бъде извикана, трябва да добавим виртуална ключова дума преди функцията map в основата.

Поправям. Ако направите функция виртуална, тя ще бъде разрешена по време на изпълнение според динамичния тип на обекта, дори ако използвате различен референтен тип или указател за достъп до нея. Така че и двата горни примера биха го нарекли B.

Ако предадем стойността на обект чрез извикване чрез указател и извикване чрез референция, той извиква функцията в производния клас, но ако предадем обекта чрез стойност, това не означава защо е така?

Ако го предадете по стойност, тогава вие нарязваненеговото: копиране само на частта A от обект, за да се създаде нов обект от тип A. Така че, независимо дали тази функция е виртуална, извикването й на този обект ще избере версията на A, тъй като това е A и нищо друго освен A.

0

wikipedia казва, че нарязването на обекти се случва, защото няма място за съхраняване на допълнителни производни членове на класа в суперкласа, така че той се нарязва. Защо нарязването на обекти не се случва, ако го предадем по референция или указател? Защо суперкласът получава допълнително място за съхранение? -

данни . Целта на полиморфизма, приложена към обектно-ориентираното програмиране, е да се използва едно име за определяне на действия, общи за даден клас.

В Java обектните променливи са полиморфни. Например:
клас King ( public static void main(String args) ( King king = new King() ; king = new AerysTargaryen() ; king = new RobertBaratheon() ; ) ) клас RobertBaratheon разширява King ( ) клас AerysTargaryen разширява King ( )
Променлива от тип King може да се отнася или до обект от тип King, или до обект от всеки подклас на King.
Да вземем следния пример:

class King ( public void words() ( System .out .println ("Аз съм кралят на андалите!") ; ) public void words(String quotation) ( System .out .println ("Мъдрият човек каза: " + цитат) ; ) публична невалидна реч (Boolean speakLoudly) ( if (speakLoudly) System .out .println ( „АЗ СЪМ КРАЛЯТ НА АНДАЛИТЕ!!!11“) ; else System .out .println ("аз съм... кралят..." ) ; ) ) клас AerysTargaryen разширява King ( @Override public void words() ( System .out .println ("Изгори ги всички... " ) ; ) @Override public void words(String quotation) ( System .out .println (quotation+ " ... И сега ги изгори всичките!" ) ; ) ) class Kingdom ( public static void main(String args) ( King king = нов AerysTargaryen() ; king.speech ("Homo homini lupus est" ) ; ) )
Какво се случва, когато се извика метод, принадлежащ на обекткрал?
1. Компилаторът проверява декларирания тип обект и името на метода, номерира всички методи с иметоречв класа AerusTargarien и всички обществени методиреч в суперкласовеAerusTargarien. Компилаторът вече знае възможните кандидати, когато извиква метод.
2. Компилаторът определя типовете аргументи, предавани на метода. Ако бъде намерен единичен метод, чийто подпис съответства на аргументите, извикването се извършва.Този процесking.speech("Homo homini lupus est") компилаторът ще избере методареч (цитиране на низ), но не реч().
Ако компилаторът намери множество методис подходящи параметри (или никакви), се показва съобщение за грешка.



Компилаторът вече знае името и типовете на параметрите на метода, който ще бъде извикан.
3. В случай, че извиканият метод ечастен, статичен, финалили конструктор, използва се статично обвързване ( ранно обвързване). В други случаи методът, който ще бъде извикан, се определя от действителния тип на обекта, чрез който се осъществява извикването. Тези. използвани по време на изпълнение на програмата динамично свързване (късно свързване).

4. Виртуалната машина предварително създава таблица с методи за всеки клас, която изброява сигнатурите на всички методи и действителните методи, които трябва да бъдат извикани.
Таблица с методи за класКрализглежда така:
  • реч () - Крал.реч()
  • реч (цитиране на низ) -Крал.реч (цитиране на низ)
  • Крал.реч (Булев speakLoudly)
И за класаAerysTargaryen - така:
  • реч () -Ерис Таргариен . реч()
  • реч (цитиране на низ) -Ерис Таргариен. реч (цитиране на низ)
  • реч (Булев speakLoudly) -Крал. реч (Булев speakLoudly)
Методите, наследени от Object, се игнорират в този пример.
При обажданекрал.реч():
  1. Определя се действителният тип на променливатакрал . В случая е такаЕрис Таргариен.
  2. Виртуалната машина определя класа, към който принадлежи методътреч()
  3. Методът се нарича.
Свързване на всички методи вJavaизвършва се полиморфно, чрез късно свързване.Динамичното обвързване има една важна характеристика: позволявамодифициране на програми без прекомпилиране на техните кодове. Това правят програмитединамично разширяващ се ( разтегателен).
Какво се случва, ако извикате динамично обвързан метод на конструиран обект в конструктора? Например:
клас King ( King () ( System . out . println ( "Извикване на конструктор King" ); реч () ; //отменен полиморфен метод в AerysTargaryen) public void words() ( System .out .println („Аз съм кралят на Андалите!“ ) ; ) ) клас AerysTargaryen разширява King ( private String .println; AerysTargaryen() ( System .out .println ( „Обадете се на конструктора на Айрис Таргариен“) ; жертваНаме = "Лиана Старк" ; реч() ; ) @Override public void реч() ( System .out .println ("Burn " + žrtvaName + "!" ) ; ) ) class Kingdom ( public static void main(String args) ( King king = new AerysTargaryen() ; ) ) Резултат:

Обадете се на King конструктор Изгаряне нула! Обадете се на конструктора на Aerys Targaryen Burn Lyanna Stark!
Конструкторът на базов клас винаги се извиква по време на конструирането на производен клас. Извикването автоматично се придвижва нагоре по веригата на наследяване, така че в крайна сметка да бъдат извикани конструкторите на всички базови класове в цялата верига на наследяване.
Това означава, че при извикване на конструктора new AerysTargaryen() ще се нарича:
  1. нов обект()
  2. нов крал ()
  3. новАерисТаргариен()
По дефиниция работата на дизайнера е да даде живот на даден обект. Във всеки конструктор обектът може да бъде само частично формиран - всичко, което се знае е, че обектите на базовия клас са инициализирани. Ако конструкторът е просто още една стъпка към конструирането на клас обект, извлечен от класа на този конструктор, „производните“ части все още не са инициализирани по време на извикването на текущия конструктор.

Въпреки това, динамично обвързано повикване може да отиде до "външната" част на йерархията, тоест до производни класове. Ако извика метод на производен клас в конструктора, това може да доведе до манипулиране на неинициализирани данни, което виждаме в изхода на този пример.

Резултатът от програмата се определя от изпълнението на алгоритъма за инициализация на обекта:

  1. Паметта, разпределена за новия обект, се запълва с двоични нули.
  2. Конструкторите на базов клас се извикват в реда, описан по-горе. В този момент се извиква замененият методреч() (да, преди да извикате конструктора на класаЕрис Таргариен), където се открива, че променливатажертваИме е нула заради първия етап.
  3. Инициализаторите на членовете на класа се извикват в реда, в който са дефинирани.
  4. Тялото на конструктора на производния клас се изпълнява.
По-специално, поради подобни поведенчески проблеми, струва си да се придържате към следното правило за писане на конструктори:
- изпълнявайте в конструктора само най-необходимите и прости действия за инициализиране на обекта
- ако е възможно, избягвайте извикването на методи, които не са дефинирани каточастни или окончателни (което в този контекст е едно и също нещо).
Използвани материали:
  1. Екел Б. -Мислене в Java , 4-то издание - Глава 8
  2. Кей С. Хорстман, Гари Корнел - Core Java 1 - Глава 5
  3. Уикипедия

В статията говорих за не всички познати функции, които може да срещнете, когато работите с вградени функции. Статията породи както няколко значими коментара, така и многостранични дебати (и дори holiwars), които започнаха с факта, че е по-добре изобщо да не се използват вградени функции, и се превърнаха в стандартната тема C vs. C++ срещу Java срещу. C# срещу. PHP срещу. Haskell срещу. ...

Днес е ред на виртуалните функции. И първо, веднага ще направя резервация, че моята статия (по принцип, както и предишната) по никакъв начин не претендира за пълна. И второ, както и преди, тази статия не е за професионалисти. Ще бъде полезно за тези, които вече разбират основите на C++, но нямат достатъчно опит, или за тези, които не обичат да четат книги.

Надявам се всички да знаят какво представляват виртуалните функции и как се използват, тъй като вече не е моя работа да обяснявам това. Сигурен съм, че RFry ще стигне до тях рано или късно в поредицата си от статии за C++.

Ако в материала за inline методите митът не беше съвсем очевиден, то в този е точно обратното. Всъщност, нека да преминем към „мита“.

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

  1. #включи
  2. #включи
  3. използване на std::cout;
  4. използвайки std::endl;
  5. структура А
  6. виртуален ~A()()
  7. << "A::foo()" << endl; }
  8. << "A::bar()" << endl; }
  9. void baz() const ( cout<< "A::baz()" << endl; }
  10. структура B: публична A
  11. виртуален void foo() const ( cout<< "B::foo()" << endl; }
  12. void bar() const ( cout<< "B::bar()" << endl; }
  13. void baz() const ( cout<< "B::baz()" << endl; }
  14. структура C: публична B
  15. виртуален void foo() const ( cout<< "C::foo()" << endl; }
  16. виртуален void bar() const ( cout<< "C::bar()" << endl; }
  17. void baz() const ( cout<< "C::baz()" << endl; }
  18. int main()
  19. cout<< "pA is B:" << endl;
  20. A * pA = нов B;
  21. pA->foo();
  22. pA->bar();
  23. pA->baz();
  24. изтриване на pA;
  25. cout<< "\npA is C:" << endl;
  26. pA = нов C;
  27. pA->foo(); pA->bar(); pA->baz();
  28. изтриване на pA;
  29. връщане EXIT_SUCCESS;

И така, имаме проста класова йерархия. Всеки клас дефинира 3 метода: foo(), bar() и baz(). Нека помислим грешна логикахора, които са под влияние мит:
когато указателят pA сочи към обект от тип B, имаме резултата:

pA е B:
B::foo() // защото в родителския клас A методът foo() е маркиран като виртуален
B::bar() // защото в родителския клас A методът bar() е маркиран като виртуален
A::baz() // защото в родителския клас A методът baz() не е маркиран като виртуален

когато указателят pA сочи към обект от тип C, имаме следния изход:
pA е C:
C::foo() // защото в родителския клас B методът foo() е маркиран като виртуален
B::bar() // защото в родителския клас B методът bar() не е маркиран като виртуален,
// но е маркиран като виртуален в клас A, указателят към който използваме
A::baz() // защото в клас A методът baz() не е маркиран като виртуален

Всичко е ясно с невиртуалната функция baz(). Но има проблем с логиката на извикване на виртуални функции. Мисля, че от само себе си се разбира, че действителният резултат ще бъде:

pA е B:
B::foo()
B::bar()
A::baz()

PA е C:
C::foo()
C::bar()
A::baz()

Заключение: виртуална функция става виртуална до края на йерархията и ключовата дума виртуалене „ключ“ само първия път, а в следващите пъти носи чисто информативна функция за удобство на програмистите.

За да разберете защо това се случва, трябва да разберете как точно работи механизмът на виртуалната функция.

Ранно и късно връзване. Таблица с виртуални функции
Свързването е картографиране на извикване на функция към извикване. В C++ всички функции по подразбиране имат ранно обвързване, тоест компилаторът и линкерът решават коя функция трябва да бъде извикана, преди програмата да се изпълни. Виртуалните функции имат късно обвързване, тоест при извикване на функция необходимото тяло се избира на етапа на изпълнение на програмата.

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

  1. #включи
  2. #включи
  3. struct Empty();
  4. struct EmptyVirt ( виртуален ~EmptyVirt()());
  5. struct NotEmpty (int m_i;);
  6. struct NotEmptyVirt
  7. виртуален ~NotEmptyVirt() ()
  8. int m_i;
  9. struct NotEmptyNonVirt
  10. void foo() const()
  11. int m_i;
  12. int main()
  13. std::cout<< sizeof (Empty) << std::endl;
  14. std::cout<< sizeof (EmptyVirt) << std::endl;
  15. std::cout<< sizeof (NotEmpty) << std::endl;
  16. std::cout<< sizeof (NotEmptyVirt) << std::endl;
  17. std::cout<< sizeof (NotEmptyNonVirt) << std::endl;
  18. връщане EXIT_SUCCESS;
* Този изходен код беше маркиран с инструмента за открояване на изходния код.

Резултатът може да се различава в зависимост от платформата, но в моя случай (Win32, msvc2008) беше както следва:

Какво можете да разберете от този пример? Първо, размерът на "празен" клас винаги е по-голям от нула, защото компилаторът умишлено вмъква фиктивен член в него. Както пише Eckel, „представете си процеса на индексиране в масив от обекти с нулев размер и всичко ще стане ясно“ ;) Второ, виждаме, че размерът на „непразния“ клас NotEmptyVirt, когато добавя виртуална функция към то, увеличено със стандартния размер на указател към void; и в "празния" клас EmptyVirt фиктивният член, който компилаторът преди това добави, за да направи класа ненулев размер, беше замененикъм показалеца. В същото време добавянето на невиртуална функция към клас не влияе на размера (благодарение на nullbie за съвета). Името на указателя на таблицата се различава в зависимост от компилатора. Например, компилаторът на Visual Studio 2008 го нарича __vfptr, а самата таблица 'vftable' (тези, които не вярват, могат да погледнат в дебъгера:) В литературата указател към таблица с виртуални функции обикновено се нарича VPTR , а самата таблица се нарича VTABLE, така че ще се придържам към същата нотация.

Какво е таблица с виртуални функции и за какво служи? Таблицата с виртуални функции съхранява адресите на всички виртуални методи на клас (по същество това е масив от указатели), както и всички виртуални методи на базовите класове на този клас.

Ще имаме толкова таблици с виртуални функции, колкото има класове, съдържащи виртуални функции - по една таблица на клас. Обектите на всеки клас съдържат просто указателна масата, а не на самата маса! Учителите, както и тези, които провеждат интервюта, обичат да задават въпроси по тази тема. (Примери за трудни въпроси, които могат да хванат начинаещите: „ако един клас съдържа таблица с виртуални функции, тогава размерът на обекта на класа ще зависи от броя на виртуалните функции, съдържащи се в него, нали?“; „имаме масив от указатели към базовия клас, всеки от които сочи към обект от един от производните класове - колко таблици с виртуални функции ще имаме?“ и т.н.).

И така, за всеки клас ще създадем таблица с виртуални функции. На всяка виртуална функция от базовия клас се присвоява последователен индекс (по реда на декларациите на функцията), който впоследствие ще се използва за определяне на адреса на тялото на функцията в таблицата VTABLE. При наследяване на базов клас, производният клас „получава“ таблицата с адреси на виртуални функции на базовия клас. Ако някой виртуален метод в производен клас е заменен, тогава в таблицата с виртуални функции на този клас адресът на тялото на съответния метод просто ще бъде заменен с нов. При добавяне на нови VTABLE виртуални методи към производен клас производнаклас се разширява и таблицата на базовия клас естествено остава същата, каквато беше. Следователно чрез указател към базовия клас е невъзможно виртуално да се извикат методи на производен клас, които не са били в базовия клас - в крайна сметка базовият клас „не знае нищо“ за тях (ще разгледаме всичко това по-късно, използвайки пример).

Сега конструкторът на класа трябва да извърши допълнителна операция: да инициализира VPTR указателя с адреса на съответната виртуална функционална таблица. Тоест, когато създаваме обект от производен клас, първо се извиква конструкторът на базовия клас, инициализирайки VPTR с адреса на „неговата“ виртуална функционална таблица, след което се извиква конструкторът на производен клас, който презаписва тази стойност.

Когато извиква функция през адреса на базовия клас (чете се - чрез указател към базовия клас), компилаторът трябва първо да използва VPTR указателя за достъп до таблицата с виртуални функции на класа и от него да получи адреса на тялото на извиканата функция и едва след това направете извикването.

От всичко по-горе можем да заключим, че механизмът за късно свързване изисква допълнително процесорно време (инициализиране на VPTR от конструктора, получаване на адреса на функцията при извикване) в сравнение с ранното свързване.

Мисля, че всичко ще стане по-ясно с пример. Помислете за следната йерархия:

В този случай получаваме две таблици с виртуални функции:

База
0
Base::foo()
1 Base::bar()
2 Base::baz()

И
Наследени
0
Base::foo()
1 Наследено::bar()
2 Base::baz()
3 Наследено::qux()

Както можете да видите, в таблицата на производния клас адресът на втория метод беше заменен със съответния заменен. Код за доказване:

  1. #включи
  2. #включи
  3. използване на std::cout;
  4. използвайки std::endl;
  5. struct Base
  6. Base() (cout<< "Base::Base()" << endl; }
  7. виртуален ~Base() ( cout<< "Base::~Base()" << endl; }
  8. виртуален void foo() ( cout<< "Base::foo()" << endl; }
  9. виртуална празна лента() ( cout<< "Base::bar()" << endl; }
  10. виртуална празнота baz() ( cout<< "Base::baz()" << endl; }
  11. struct Наследено: публична база
  12. Наследено() ( cout<< "Inherited::Inherited()" << endl; }
  13. виртуален ~Inherited() ( cout<< "Inherited::~Inherited()" << endl; }
  14. виртуална празна лента() ( cout<< "Inherited::bar()" << endl; }
  15. виртуален void qux() ( cout<< "Inherited::qux()" << endl; }
  16. int main()
  17. Base * pBase = ново наследено;
  18. pBase->foo();
  19. pBase->bar();
  20. pBase->baz();
  21. //pBase->qux(); // Грешка
  22. изтриване на pBase;
  23. връщане EXIT_SUCCESS;
* Този изходен код беше маркиран с инструмента за открояване на изходния код.

Какво се случва, когато програмата стартира? Първо декларираме указател към обект от тип Base, на който присвояваме адреса на новосъздадения обект от тип Inherited. Това извиква Base конструктора, инициализирайки VPTR с VTABLE адреса на Base класа и след това Inherited конструктора, който презаписва VPTR стойността с VTABLE адреса на Inherited класа. Когато се извикат pBase->foo(), pBase->bar() и pBase->baz(), компилаторът извлича действителния адрес на тялото на функцията от виртуалната функционална таблица чрез VPTR указателя. как става това Независимо от конкретния тип обект, компилаторът знае, че адресът на функцията foo() е на първо място, bar() е на второ и т.н. (както казах, в реда на декларациите на функции). Така, за да извика например функцията baz(), той получава адреса на функцията под формата VPTR+2 - отместването от началото на таблицата с виртуални функции, запазва този адрес и го замества в командата за извикване. По същата причина извикването на pBase->qux() води до грешка: въпреки факта, че действителният тип на обекта е наследен, когато присвоим неговия адрес на указател към Base, възниква прехвърляне нагоре и няма четвърти метод в таблицата VTABLE на базовия клас, така че VPTR+3 ще сочи към „чужда“ памет (за щастие такъв код дори не се компилира).

Да се ​​върнем към мита. Става очевидно, че при този подход към реализацията на виртуални функции е невъзможно да се направи функцията виртуална само за едно ниво на йерархията.

Също така става ясно защо виртуалните функции работят само когато са достъпни от адреса на обект (чрез указатели или препратки). Както казах, в този ред
Base * pBase = ново наследено;
възниква upcast: Inherited* се прехвърля към Base*, но във всеки случай указателят просто съхранява адреса на „началото“ на обекта в паметта. Ако upcast се извършва директно върху обект, тогава той всъщност се „отрязва“ до размера на обекта на базовия клас. Следователно е логично ранното свързване да се използва за извикване на функции „чрез обект“ - компилаторът вече „знае“ действителния тип на обекта.

Всъщност това е всичко. Очаквам вашите коментари. Благодаря за вниманието.

P.S. Тази статия е маркирана като „Гаранция за скорост“
(Skor, ако четеш това, това е за теб;)

P.P.S. Да, забравих да кажа... Джаваистите сега ще започнат да викат, че в Java по подразбиране всички функции са виртуални.
_________
Текстът е подготвен през

Използване на отражение, късно свързване и атрибути

В тази статия предлагам да разгледаме сложен пример за използване на отражение, късно свързване и атрибути. Да приемем, че задачата е била да се разработи така нареченото разширяемо приложение, към което могат да бъдат свързани инструменти на трети страни.

Какво точно се разбира под разширяемо приложение? Нека разгледаме IDE на Visual Studio 2010. По време на разработката това приложение включваше специални „кукички“, за да предостави на други производители на софтуер възможността да свързват своите специални модули. Ясно е, че разработчиците на Visual Studio 2010 не са могли да добавят препратки към несъществуващи външни .NET сборки (т.е. да се възползват от ранното свързване), така че как са успели да осигурят необходимите методи за закачане в приложението? По-долу е един възможен начин за решаване на този проблем.

    Първо, всяко разширяемо приложение трябва да има някакъв вид механизъм за въвеждане, който позволява на потребителя да посочи добавката (например диалогов прозорец или подходящ флаг на командния ред). Това налага използването на динамично натоварване.

    Второ, всяко разширяемо приложение трябва да може да определи дали даден модул поддържа функционалността (например набор от интерфейси), необходима за свързването му със средата. Това изисква използването на отражение.

    Трето, всяко приложение, което се разширява, трябва да получи препратка към необходимата инфраструктура (например набор от типове интерфейси) и да извика своите членове за изпълнение на основните функции. Това може да изисква използването на късно свързване.

Ако едно разширяемо приложение първоначално е програмирано да изисква специфични интерфейси, то получава способността да определя по време на изпълнение дали типът интерес може да бъде извикан и, след като типът успешно премине такава проверка, му позволява да поддържа допълнителни интерфейси и да има достъп до тяхната функционалност в полиморфен начин. Точно такъв подход са предприели разработчиците на Visual Studio 2010 и в това няма нищо особено сложно.

Първата стъпка е да създадете сборка с типовете, които всяка конзолна добавка трябва да използва, за да може да се свърже с приложението, което разширявате. За да направите това, създайте проект от тип Class Library и дефинирайте следните два типа в него:

Използване на системата; пространство от имена PW_CommonType ( публичен интерфейс IApplicationFunc ( void Go(); ) публичен клас InfoAttribute: System.Attribute ( публичен низ CompanyName ( get; set; ) public string CompanyUrl ( get; set; ) ) )

След това трябва да създадете тип, който имплементира интерфейса IApplicationFunc. За да запазим примера за създаване на разширяемо приложение прост, нека запазим този тип прост. Нека създадем нов проект от типа Class Library в C# и дефинираме тип клас в него, наречен MyCompanyInfo:

Използване на системата; използване на PW_CommonType; използвайки System.Windows..Go() ( MessageBox.Show("Важна информация!"); ) ) )

И накрая, последната стъпка е да създадете най-разширяемото Windows Forms приложение, което ще позволи на потребителя да избере желаната добавка чрез стандартния диалогов прозорец за отваряне на файл на Windows.

Сега трябва да добавите връзка към сборката PW_CommonType.dll, но не и към кодовата библиотека CompanyInfo.dll. Освен това трябва да импортирате пространствата от имена System.Reflection и PW_CommonType в главния файл с код на формуляр (за да го отворите, щракнете с десния бутон върху визуалния дизайнер на формуляр и изберете Преглед на кода от контекстното меню). Спомнете си, че целта на това приложение е да се види как да се използва късно свързване и отражение, за да се тестват отделни двоични файлове, произведени от други доставчици, за тяхната способност да действат като модули за добавки.



Свързани публикации