Як змінні в C ++ зберігають свій тип?


42

Якщо я визначаю змінну певного типу (яка, наскільки я знаю, просто виділяє дані для вмісту змінної), як вона відслідковує, який тип змінної це?


8
Кого / на що ви маєте на увазі " це " у " як це слідкувати "? Компілятор чи процесор чи щось інше, як мова чи програма?
Ерік Ейдт


8
@ErikEidt IMO ОП, очевидно, означає "саму змінну" "вона". Звичайно, двословна відповідь на питання "це не так".
alephzero

2
чудове запитання! особливо актуально сьогодні, враховуючи всі модні мови, які зберігають їх тип.
Тревор Бойд Сміт

@alephzero Це, очевидно, було провідним питанням.
Луань

Відповіді:


105

Змінні (або загальніше: "об'єкти" у значенні 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об'єкт. Або компілятор знає тип об'єкта під час компіляції, або компілятор зберігає необхідну інформацію про тип всередині об'єкта і може отримати його під час виконання.


3
Дуже всебічно.
Дедуплікатор

9
Зауважте, що для доступу до типу поліморфного об'єкта компілятор все-таки повинен знати, що об'єкт належить до певної сім'ї спадкування (тобто мати введене посилання / вказівник на об’єкт, ні void*).
Руслан

5
+0, оскільки перше речення не відповідає дійсності, два останні абзаци виправляють його.
Марцін

3
Як правило, те, що зберігається на початку поліморфного об'єкта, - це вказівник на таблицю віртуальних методів, а не на саму таблицю.
Пітер Грін

3
@ v.oddou У своєму абзаці я проігнорував деякі деталі. typeid(e)самоаналіз статичного типу виразу e. Якщо статичний тип є поліморфним типом, вираження буде оцінено і буде отримано динамічний тип об'єкта. Ви не можете вказати typeid на пам'ять невідомого типу та отримати корисну інформацію. Наприклад, typeid союзу описує об'єднання, а не об'єкт у об'єднанні. Typeid а void*- це просто недійсний покажчик. І не можна знехтувати a, void*щоб отримати його вміст. У C ++ боксу немає, якщо явно не запрограмовано таким чином.
амон

51

Інша відповідь добре пояснює технічний аспект, але я хотів би додати загальне "як думати про машинний код".

Машинний код після компіляції досить німий, і він насправді просто передбачає, що все працює за призначенням. Скажіть, у вас є така проста функція, як

bool isEven(int i) { return i % 2 == 0; }

Він бере int і випльовує bool.

Після компіляції ви можете подумати про це як щось подібне до цієї автоматичної апельсинової соковижималки:

автоматична апельсинова соковижималка

Він бере апельсини, і повертає сік. Чи розпізнає він тип об’єктів, які він потрапляє? Ні, вони просто повинні бути апельсинами. Що станеться, якщо воно отримає яблуко замість апельсина? Можливо, воно зламається. Не має значення, оскільки відповідальний власник не намагатиметься використовувати це таким чином.

Наведена вище функція схожа: вона створена для прийому вкладишів, і вона може зламатися або зробити щось неактуальне, коли подається щось інше. Це (як правило) не має значення, тому що компілятор (як правило) перевіряє, що це ніколи не відбувається, і це дійсно ніколи не відбувається у добре сформованому коді. Якщо компілятор виявить можливість того, що функція отримає неправильне введене значення, вона відмовляється компілювати код і натомість повертає помилки типу.

Застереження полягає в тому, що є деякі випадки неправильно сформованого коду, який передасть компілятор. Приклади:

  • неправильний тип лиття: явні зліпки вважаються правильними, і саме на програміста , щоб гарантувати , що він не кастинг , void*щоб , orange*коли є яблуко на іншому кінці покажчика,
  • проблеми управління пам'яттю, такі як нульові вказівники, звисаючі покажчики або використання після використання; компілятор не в змозі знайти більшість із них,
  • Я впевнений, що мені ще чогось не вистачає.

Як було сказано, складений код схожий на соковижималку - він не знає, що обробляє, він просто виконує вказівки. І якщо вказівки неправильні, вона порушується. Ось чому вищевказані проблеми на C ++ призводять до неконтрольованих збоїв.


4
Компілятор намагається перевірити, чи передана функція об'єкту правильного типу, але і C, і C ++ занадто складні, щоб компілятор доводив це у кожному випадку. Отже, ваші яблуко-апельсини порівняння з соковижималкою досить повчальні.
Calchas

@Calchas Дякуємо за ваш коментар! Це речення справді було надмірним спрощенням. Я трохи детальніше зупинився на можливих проблемах, вони насправді досить пов'язані з питанням.
Frax

5
вау чудова метафора для машинного коду! ваша метафора також зроблена на 10 разів краще за малюнком!
Тревор Бойд Сміт

2
"Я впевнений, що ще щось мені не вистачає". - Звичайно! C в void*примушує до foo*, звичайним арифметичним акцій, unionтипу каламбурів, NULLпроти nullptr, навіть просто маючи поганий покажчик є UB, і т.д. Але я не думаю , що перерахувати всі ці речі могли б значно поліпшити свою відповідь, так що, ймовірно , краще залишити це як є.
Кевін

@Kevin Я не думаю, що тут потрібно додавати C, оскільки питання позначене лише як C ++. І в C ++ void*не конвертується неявно foo*, а unionтип покарання не підтримується (має UB).
Руслан

3

Змінна має ряд основних властивостей у такій мові, як C:

  1. Ім'я
  2. Тип
  3. Область застосування
  4. Ціле життя
  5. Місцезнаходження
  6. Значення

У вашому вихідному коді розташування (5) є концептуальним, і це місце посилається назвою, (1). Отже, оголошення змінної використовується для створення місця та простору для значення (6), а в інших рядках джерела ми посилаємось на це місце та значення, яке воно має, називаючи змінну в якомусь виразі.

Спрощення лише дещо, як тільки ваша програма перекладається компілятором у машинний код, місцеположення (5) - це деяке місце пам’яті чи реєстрації процесора, а будь-які вирази вихідного коду, що посилаються на змінну, переводяться в послідовності машинного коду, на які посилається ця пам'ять або місце реєстрації процесора.

Таким чином, коли трансляція завершена і програма працює на процесорі, імена змінних фактично забуваються в машинному коді, а вказівки, згенеровані компілятором, стосуються лише призначених місць змінних (а не їх назви). Якщо ви налагоджуєте і вимагаєте налагодження, місце змінної, пов’язаної з іменем, додається до метаданих програми, хоча процесор все ще бачить інструкції машинного коду, використовуючи розташування (не ті метадані). (Це надмірне спрощення, оскільки деякі назви містяться у метаданих програми для підключення, завантаження та динамічного пошуку - процесор все ще виконує інструкції машинного коду, про які йдеться в програмі, і в цьому машинному коді імена мають було перетворено на місця.)

Те саме стосується і типу, сфери та терміну експлуатації. Інструкції машинного коду, згенеровані компілятором, знають машинну версію місця, де зберігається значення. Інші властивості, як-от тип, компілюються в перекладений вихідний код у вигляді конкретних інструкцій, що мають доступ до місцезнаходження змінної. Наприклад, якщо відповідна змінна є підписаним 8-бітовим байтом проти непідписаним 8-бітовим байтом, то вирази у вихідному коді, що посилаються на змінну, будуть переведені в, скажімо, підписані байтові навантаження проти неподписаних байтових навантажень, у міру необхідності для задоволення правил мови (С). Тип змінної таким чином кодується в перекладі вихідного коду в машинні інструкції, які командують ЦП як інтерпретувати пам'ять або місце реєстрації ЦП кожного разу, коли він використовує розташування змінної.

Суть полягає в тому, що ми повинні сказати процесору, що робити за допомогою інструкцій (і більше інструкцій) у наборі інструкцій машинного коду процесора. Процесор дуже мало пам’ятає про те, що він щойно робив або йому було сказано - він виконує лише наведені інструкції, і завдання компілятора або мови програмування монтажу - дати йому повний набір послідовностей інструкцій для правильного маніпулювання змінними.

Процесор безпосередньо підтримує деякі основні типи даних, такі як байт / слово / int / довго підписаний / непідписаний, плаваючий, подвійний тощо. Процесор, як правило, не скаржиться чи не заперечує, якщо ви по черзі обробляти те саме місце пам'яті, що і підписане чи без підпису, для Наприклад, хоча це, як правило, є логічною помилкою в програмі. Завдання програмування - інструктувати процесор при кожній взаємодії зі змінною.

Крім цих основних примітивних типів, ми повинні кодувати речі в структурах даних і використовувати алгоритми для маніпулювання ними з точки зору цих примітивів.

У C ++ об’єкти, що беруть участь в ієрархії класів для поліморфізму, мають вказівник, як правило, на початку об'єкта, що стосується конкретної для класу структури даних, яка допомагає у віртуальній розсилці, кастингу тощо.

Підсумовуючи це, процесор в іншому випадку не знає і не пам’ятає про використання місця зберігання - він виконує інструкції машинного коду програми, які розповідають, як маніпулювати зберіганням в регістрах процесора та основній пам'яті. Програмування, таким чином, є завданням програмного забезпечення (та програмістів) використовувати вміст для зберігання інформації та представляти послідовний набір інструкцій машинного коду для процесора, який сумлінно виконує програму в цілому.


1
Обережно з "коли переклад буде завершено, ім'я забуто" ... посилання здійснюється через імена ("невизначений символ xy") і цілком може статися під час запуску при динамічному посиланні. Дивіться blog.fesnel.com/blog/2009/08/19/… . Немає символів налагодження, навіть позбавлених: Вам потрібна назва функції (і, я припускаю, глобальна змінна) для динамічного посилання. Тож можна забути лише назви внутрішніх об’єктів. До речі, хороший список змінних властивостей.
Пітер - Відновіть Моніку

@ PeterA.Schneider, ви абсолютно праві, у великій картині речей, що пов'язувачі та завантажувачі також беруть участь та використовують назви (глобальних) функцій та змінних, що походять від вихідного коду.
Ерік Ейдт

Додатковим ускладненням є те, що деякі компілятори інтерпретують правила, які згідно стандарту призначені для того, щоб компілятори припускали, що певні речі не будуть псевдонімами, оскільки дозволяють їм вважати операції, що включають різні типи, як непідвладні, навіть у випадках, які не передбачають псевдоніму, як написано . Враховуючи щось подібне useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);, clang і gcc схильні вважати, що вказівник не unionArray[j].member2може отримати доступ, unionArray[i].member1хоча обидва походять від одного і того ж unionArray[].
supercat

Незалежно від того, чи компілятор правильно трактує специфікацію мови, чи ні, її завдання полягає в створенні послідовностей інструкцій машинного коду, що виконує програму. Це означає, що (оптимізація модуля та багато інших факторів) для кожного змінного доступу до вихідного коду він повинен генерувати деякі інструкції машинного коду, які повідомляють процесору, який розмір та інтерпретацію даних використовувати для місця зберігання. Процесор нічого не пам’ятає про змінну, тому кожен раз, коли передбачається отримати доступ до змінної, їй слід вказувати, як саме це зробити.
Ерік Ейдт

2

якщо я визначаю змінну певного типу, то як вона відстежує тип змінної.

Тут є дві відповідні фази:

  • Час компіляції

Компілятор C компілює код C на машинну мову. У компіляторі є вся інформація, яку вона може отримати з вашого вихідного файлу (і бібліотек, і будь-якого іншого матеріалу, який він потребує, щоб виконати свою роботу). Компілятор C відслідковує, що означає що. Компілятор C знає, що якщо ви оголосите змінну такою char, що вона є, вона буде char.

Це робиться за допомогою так званої "таблиці символів", в якій перераховані назви змінних, їх тип та інша інформація. Це досить складна структура даних, але ви можете вважати це просто відстеженням того, що означають людиночитані імена. У двійковому виході від компілятора більше не з'являються такі імена змінних, як це (якщо ми ігноруємо необов'язкову інформацію про налагодження, яку може запитати програміст).

  • Час виконання

Вихід компілятора - компільований виконуваний файл - це машинна мова, яка завантажується в оперативну пам’ять вашою ОС і виконується безпосередньо вашим процесором. У машинній мові поняття "тип" взагалі не існує - у ньому є лише команди, які працюють в деякому місці в оперативній пам'яті. Ці команди дійсно мають фіксованого типу вони працюють з (тобто, може бути команда машинного мови «додати ці два 16-розрядних цілих чисел , що зберігаються в місцях RAM 0x100 і 0x521»), але немає ніякої інформації в будь-якому місці в системі , що байти в цих місцях насправді представляють цілі числа. Там немає ніякого захисту від помилок типу взагалі тут.


Якщо ви випадково посилаєтесь на C # або Java з "байт-орієнтованими мовами", то вказівники жодним чином не опускаються з них; зовсім навпаки: покажчики набагато частіше зустрічаються у C # та Java (і, отже, одна з найпоширеніших помилок на Java - «NullPointerException»). Те, що вони називаються "посиланнями" - це лише питання термінології.
Пітер - Відновіть Моніку

@ PeterA.Schneider, звичайно, є NullPOINTERException, але існує дуже певна відмінність між посиланням і вказівником на мовах, про які я згадав (наприклад, Java, ruby, мабуть, C #, навіть певною мірою Perl) - посилання йдуть разом з їх системою типу, збиранням сміття, автоматичним управлінням пам’яттю тощо; зазвичай неможливо навіть чітко вказати місце пам'яті (наприклад, char *ptr = 0x123на C). Я вважаю, що моє використання слова "вказівник" повинно бути досить чітким у цьому контексті. Якщо ні, сміливо дайте мені голову, і я додам речення до відповіді.
AnoE

покажчики "також йдуть разом із системою типів" і в C ++ ;-). (Насправді, класичні дженерики Java менш сильно набрані, ніж C ++.) Збір сміття - це особливість, яку C ++ вирішив не мандатувати, але це можливо, щоб реалізація надавала її, і це не має нічого спільного з тим, яке слово ми використовуємо для покажчиків.
Пітер - Відновіть Моніку

Гаразд, @ PeterA.Schneider, я насправді не думаю, що ми тут отримуємо рівень. Я видалив абзац, де я згадав покажчики, він все одно нічого не відповів.
AnoE

1

Є кілька важливих спеціальних випадків, коли C ++ зберігає тип під час виконання.

Класичне рішення - це дискриміноване об'єднання: структура даних, що містить один із декількох типів об’єкта, плюс поле, яке вказує, який тип він містить. Шаблонна версія знаходиться в стандартній бібліотеці C ++ як std::variant. Зазвичай тег був би enum, але якщо вам не потрібні всі біти пам’яті для ваших даних, це може бути бітове поле.

Інший поширений випадок цього - це динамічне введення тексту. Коли у вас classє virtualфункція, програма буде зберігати вказівник на цю функцію у віртуальній таблиці функцій , яку вона ініціалізує для кожного екземпляра, classколи вона побудована. Зазвичай це буде означати одну віртуальну таблицю функцій для всіх екземплярів класу, і кожен екземпляр містить покажчик на відповідну таблицю. (Це економить час і пам'ять, оскільки таблиця буде набагато більшою, ніж один покажчик.) Коли ви викликаєте цю virtualфункцію через вказівник або посилання, програма шукатиме покажчик функції у віртуальній таблиці. (Якщо він знає точний тип під час компіляції, він може пропустити цей крок.) Це дозволяє коду викликати реалізацію похідного типу замість базового класу.

Тут важливим є те, що кожен ofstreamмістить вказівник на ofstreamвіртуальну таблицю, кожен ifstreamна ifstreamвіртуальну таблицю тощо. Для ієрархій класів вказівник віртуальної таблиці може служити тегом, який повідомляє програмі, який тип об'єкта класу має!

Хоча мовний стандарт не говорить людям, які розробляють компілятори, як вони повинні реалізувати час виконання під кришкою, саме так можна очікувати dynamic_castта typeofпрацювати.


"мовний стандарт не каже кодерам", ви, напевно, слід підкреслити, що "кодери", про які йдеться, - це люди, які пишуть gcc, clang, msvc тощо, а не люди, які використовують їх для складання своїх C ++.
Калет

@Caleth Гарна пропозиція!
Девіслор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.