Якщо я визначаю змінну певного типу (яка, наскільки я знаю, просто виділяє дані для вмісту змінної), як вона відслідковує, який тип змінної це?
Якщо я визначаю змінну певного типу (яка, наскільки я знаю, просто виділяє дані для вмісту змінної), як вона відслідковує, який тип змінної це?
Відповіді:
Змінні (або загальніше: "об'єкти" у значенні C) не зберігають їх тип під час виконання. Що стосується машинного коду, то тут є лише нетипізована пам'ять. Натомість операції над цими даними трактують дані як певний тип (наприклад, як поплавок або як вказівник). Типи використовуються лише компілятором.
Наприклад, у нас може бути структура або клас struct Foo { int x; float y; };
і змінна Foo f {}
. Як можна скласти доступ до поля auto result = f.y;
? Компілятор знає, що f
це об'єкт типу Foo
і знає макет Foo
-об'єктів. Залежно від конкретних деталей платформи, це може бути складено як "Візьміть покажчик до початку f
, додайте 4 байти, потім завантажте 4 байти та інтерпретуйте ці дані як поплавці". У багатьох наборах інструкцій машинного коду (включаючи x86-64 ) Існують різні інструкції процесора для завантаження поплавків або ints.
Одним із прикладів, коли система типів C ++ не може відстежувати тип для нас, є союз union Bar { int as_int; float as_float; }
. Об'єднання містить до одного об'єкта різних типів. Якщо ми зберігаємо об'єкт у об'єднанні, це активний тип об'єднання. Ми мусимо лише намагатися повернути цей тип із союзу, все інше було б невизначеною поведінкою. Або ми “знаємо” під час програмування того, що є активним типом, або можемо створити тегований союз, де ми зберігаємо тег типу (як правило, enum) окремо. Це звичайна методика на C, але оскільки ми повинні тримати об'єднання та тег типів у синхронізації, це досить схильне до помилок. void*
Покажчик схожий на союз , але може містити тільки об'єкти покажчиків, крім покажчиків на функції.
C ++ пропонує два кращі механізми поводження з об'єктами невідомих типів: ми можемо використовувати об'єктно-орієнтовані методи для виконання стирання типу (взаємодіємо з об'єктом лише за допомогою віртуальних методів, щоб нам не потрібно було знати фактичний тип), або ми можемо використання std::variant
, свого роду безпечний тип з'єднання.
Є один випадок, коли C ++ зберігає тип об'єкта: якщо клас об'єкта має будь-які віртуальні методи ("поліморфний тип", також інтерфейс). Ціль виклику віртуального методу невідома під час компіляції та вирішується під час виконання на основі динамічного типу об'єкта («динамічна відправка»). Більшість компіляторів реалізують це, зберігаючи віртуальну таблицю функцій ("vtable") на початку об'єкта. Vtable можна також використовувати для отримання типу об'єкта під час виконання. Тоді ми можемо провести різницю між відомим статичним типом виразу в часі компіляції та динамічним типом об'єкта під час виконання.
C ++ дозволяє нам перевіряти динамічний тип об'єкта з typeid()
оператором, який дає нам std::type_info
об'єкт. Або компілятор знає тип об'єкта під час компіляції, або компілятор зберігає необхідну інформацію про тип всередині об'єкта і може отримати його під час виконання.
void*
).
typeid(e)
самоаналіз статичного типу виразу e
. Якщо статичний тип є поліморфним типом, вираження буде оцінено і буде отримано динамічний тип об'єкта. Ви не можете вказати typeid на пам'ять невідомого типу та отримати корисну інформацію. Наприклад, typeid союзу описує об'єднання, а не об'єкт у об'єднанні. Typeid а void*
- це просто недійсний покажчик. І не можна знехтувати a, void*
щоб отримати його вміст. У C ++ боксу немає, якщо явно не запрограмовано таким чином.
Інша відповідь добре пояснює технічний аспект, але я хотів би додати загальне "як думати про машинний код".
Машинний код після компіляції досить німий, і він насправді просто передбачає, що все працює за призначенням. Скажіть, у вас є така проста функція, як
bool isEven(int i) { return i % 2 == 0; }
Він бере int і випльовує bool.
Після компіляції ви можете подумати про це як щось подібне до цієї автоматичної апельсинової соковижималки:
Він бере апельсини, і повертає сік. Чи розпізнає він тип об’єктів, які він потрапляє? Ні, вони просто повинні бути апельсинами. Що станеться, якщо воно отримає яблуко замість апельсина? Можливо, воно зламається. Не має значення, оскільки відповідальний власник не намагатиметься використовувати це таким чином.
Наведена вище функція схожа: вона створена для прийому вкладишів, і вона може зламатися або зробити щось неактуальне, коли подається щось інше. Це (як правило) не має значення, тому що компілятор (як правило) перевіряє, що це ніколи не відбувається, і це дійсно ніколи не відбувається у добре сформованому коді. Якщо компілятор виявить можливість того, що функція отримає неправильне введене значення, вона відмовляється компілювати код і натомість повертає помилки типу.
Застереження полягає в тому, що є деякі випадки неправильно сформованого коду, який передасть компілятор. Приклади:
void*
щоб , orange*
коли є яблуко на іншому кінці покажчика,Як було сказано, складений код схожий на соковижималку - він не знає, що обробляє, він просто виконує вказівки. І якщо вказівки неправильні, вона порушується. Ось чому вищевказані проблеми на C ++ призводять до неконтрольованих збоїв.
void*
примушує до foo*
, звичайним арифметичним акцій, union
типу каламбурів, NULL
проти nullptr
, навіть просто маючи поганий покажчик є UB, і т.д. Але я не думаю , що перерахувати всі ці речі могли б значно поліпшити свою відповідь, так що, ймовірно , краще залишити це як є.
void*
не конвертується неявно foo*
, а union
тип покарання не підтримується (має UB).
Змінна має ряд основних властивостей у такій мові, як C:
У вашому вихідному коді розташування (5) є концептуальним, і це місце посилається назвою, (1). Отже, оголошення змінної використовується для створення місця та простору для значення (6), а в інших рядках джерела ми посилаємось на це місце та значення, яке воно має, називаючи змінну в якомусь виразі.
Спрощення лише дещо, як тільки ваша програма перекладається компілятором у машинний код, місцеположення (5) - це деяке місце пам’яті чи реєстрації процесора, а будь-які вирази вихідного коду, що посилаються на змінну, переводяться в послідовності машинного коду, на які посилається ця пам'ять або місце реєстрації процесора.
Таким чином, коли трансляція завершена і програма працює на процесорі, імена змінних фактично забуваються в машинному коді, а вказівки, згенеровані компілятором, стосуються лише призначених місць змінних (а не їх назви). Якщо ви налагоджуєте і вимагаєте налагодження, місце змінної, пов’язаної з іменем, додається до метаданих програми, хоча процесор все ще бачить інструкції машинного коду, використовуючи розташування (не ті метадані). (Це надмірне спрощення, оскільки деякі назви містяться у метаданих програми для підключення, завантаження та динамічного пошуку - процесор все ще виконує інструкції машинного коду, про які йдеться в програмі, і в цьому машинному коді імена мають було перетворено на місця.)
Те саме стосується і типу, сфери та терміну експлуатації. Інструкції машинного коду, згенеровані компілятором, знають машинну версію місця, де зберігається значення. Інші властивості, як-от тип, компілюються в перекладений вихідний код у вигляді конкретних інструкцій, що мають доступ до місцезнаходження змінної. Наприклад, якщо відповідна змінна є підписаним 8-бітовим байтом проти непідписаним 8-бітовим байтом, то вирази у вихідному коді, що посилаються на змінну, будуть переведені в, скажімо, підписані байтові навантаження проти неподписаних байтових навантажень, у міру необхідності для задоволення правил мови (С). Тип змінної таким чином кодується в перекладі вихідного коду в машинні інструкції, які командують ЦП як інтерпретувати пам'ять або місце реєстрації ЦП кожного разу, коли він використовує розташування змінної.
Суть полягає в тому, що ми повинні сказати процесору, що робити за допомогою інструкцій (і більше інструкцій) у наборі інструкцій машинного коду процесора. Процесор дуже мало пам’ятає про те, що він щойно робив або йому було сказано - він виконує лише наведені інструкції, і завдання компілятора або мови програмування монтажу - дати йому повний набір послідовностей інструкцій для правильного маніпулювання змінними.
Процесор безпосередньо підтримує деякі основні типи даних, такі як байт / слово / int / довго підписаний / непідписаний, плаваючий, подвійний тощо. Процесор, як правило, не скаржиться чи не заперечує, якщо ви по черзі обробляти те саме місце пам'яті, що і підписане чи без підпису, для Наприклад, хоча це, як правило, є логічною помилкою в програмі. Завдання програмування - інструктувати процесор при кожній взаємодії зі змінною.
Крім цих основних примітивних типів, ми повинні кодувати речі в структурах даних і використовувати алгоритми для маніпулювання ними з точки зору цих примітивів.
У C ++ об’єкти, що беруть участь в ієрархії класів для поліморфізму, мають вказівник, як правило, на початку об'єкта, що стосується конкретної для класу структури даних, яка допомагає у віртуальній розсилці, кастингу тощо.
Підсумовуючи це, процесор в іншому випадку не знає і не пам’ятає про використання місця зберігання - він виконує інструкції машинного коду програми, які розповідають, як маніпулювати зберіганням в регістрах процесора та основній пам'яті. Програмування, таким чином, є завданням програмного забезпечення (та програмістів) використовувати вміст для зберігання інформації та представляти послідовний набір інструкцій машинного коду для процесора, який сумлінно виконує програму в цілому.
useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);
, clang і gcc схильні вважати, що вказівник не unionArray[j].member2
може отримати доступ, unionArray[i].member1
хоча обидва походять від одного і того ж unionArray[]
.
якщо я визначаю змінну певного типу, то як вона відстежує тип змінної.
Тут є дві відповідні фази:
Компілятор C компілює код C на машинну мову. У компіляторі є вся інформація, яку вона може отримати з вашого вихідного файлу (і бібліотек, і будь-якого іншого матеріалу, який він потребує, щоб виконати свою роботу). Компілятор C відслідковує, що означає що. Компілятор C знає, що якщо ви оголосите змінну такою char
, що вона є, вона буде char.
Це робиться за допомогою так званої "таблиці символів", в якій перераховані назви змінних, їх тип та інша інформація. Це досить складна структура даних, але ви можете вважати це просто відстеженням того, що означають людиночитані імена. У двійковому виході від компілятора більше не з'являються такі імена змінних, як це (якщо ми ігноруємо необов'язкову інформацію про налагодження, яку може запитати програміст).
Вихід компілятора - компільований виконуваний файл - це машинна мова, яка завантажується в оперативну пам’ять вашою ОС і виконується безпосередньо вашим процесором. У машинній мові поняття "тип" взагалі не існує - у ньому є лише команди, які працюють в деякому місці в оперативній пам'яті. Ці команди дійсно мають фіксованого типу вони працюють з (тобто, може бути команда машинного мови «додати ці два 16-розрядних цілих чисел , що зберігаються в місцях RAM 0x100 і 0x521»), але немає ніякої інформації в будь-якому місці в системі , що байти в цих місцях насправді представляють цілі числа. Там немає ніякого захисту від помилок типу взагалі тут.
char *ptr = 0x123
на C). Я вважаю, що моє використання слова "вказівник" повинно бути досить чітким у цьому контексті. Якщо ні, сміливо дайте мені голову, і я додам речення до відповіді.
Є кілька важливих спеціальних випадків, коли C ++ зберігає тип під час виконання.
Класичне рішення - це дискриміноване об'єднання: структура даних, що містить один із декількох типів об’єкта, плюс поле, яке вказує, який тип він містить. Шаблонна версія знаходиться в стандартній бібліотеці C ++ як std::variant
. Зазвичай тег був би enum
, але якщо вам не потрібні всі біти пам’яті для ваших даних, це може бути бітове поле.
Інший поширений випадок цього - це динамічне введення тексту. Коли у вас class
є virtual
функція, програма буде зберігати вказівник на цю функцію у віртуальній таблиці функцій , яку вона ініціалізує для кожного екземпляра, class
коли вона побудована. Зазвичай це буде означати одну віртуальну таблицю функцій для всіх екземплярів класу, і кожен екземпляр містить покажчик на відповідну таблицю. (Це економить час і пам'ять, оскільки таблиця буде набагато більшою, ніж один покажчик.) Коли ви викликаєте цю virtual
функцію через вказівник або посилання, програма шукатиме покажчик функції у віртуальній таблиці. (Якщо він знає точний тип під час компіляції, він може пропустити цей крок.) Це дозволяє коду викликати реалізацію похідного типу замість базового класу.
Тут важливим є те, що кожен ofstream
містить вказівник на ofstream
віртуальну таблицю, кожен ifstream
на ifstream
віртуальну таблицю тощо. Для ієрархій класів вказівник віртуальної таблиці може служити тегом, який повідомляє програмі, який тип об'єкта класу має!
Хоча мовний стандарт не говорить людям, які розробляють компілятори, як вони повинні реалізувати час виконання під кришкою, саме так можна очікувати dynamic_cast
та typeof
працювати.