Чому ми повинні згадати тип даних змінної у C


19

Зазвичай в C ми повинні сказати комп'ютеру тип даних у змінному оголошенні. Наприклад, у наступній програмі я хочу надрукувати суму двох чисел з плаваючою комою X та Y.

#include<stdio.h>
main()
{
  float X=5.2;
  float Y=5.1;
  float Z;
  Z=Y+X;
  printf("%f",Z);

}

Я повинен був сказати компілятору тип змінної X.

  • Не може компілятор Xсамостійно визначити тип ?

Так, це можна, якщо я це роблю:

#define X 5.2

Тепер я можу написати свою програму, не повідомляючи компілятору тип X:

#include<stdio.h>
#define X 5.2
main()
{
  float Y=5.1;
  float Z;
  Z=Y+X;
  printf("%f",Z);

}  

Отже, ми бачимо, що мова С має якусь особливість, за допомогою якої вона може визначати тип даних самостійно. У моєму випадку було визначено, що Xце тип float.

  • Чому ми мусимо згадувати тип даних, коли ми декларуємо щось головне ()? Чому компілятор не може самостійно визначити тип даних змінної, main()як це робиться в #define.

14
Насправді ці дві програми не рівноцінні, оскільки вони можуть дати дещо різні результати! 5.2є a double, тому перша програма floatточно округляє подвійні літерали , потім додає їх як плавні, тоді як друга округляє подвійне представлення 5.1 назад doubleі додає його до doubleзначення 5.2 за допомогою doubleдодавання, потім округляє результат цього обчислення до floatточності . Оскільки округлення відбувається в різних місцях, результат може відрізнятися. Це лише один приклад для типів змінних, що впливають на поведінку інакше ідентичної програми.

12
Коли ви це робите #define X 5.2, Xце не змінна, а константа, тому вона буквально замінюється бути препроцесором з 5.2будь-якого місця, про яке ви згадали X. Ви не можете перепризначити X.
сценарій

16
Так само як замітка: це благо і прокляття. З одного боку, ви повинні набрати кілька символів, коли компілятор дійсно міг би зробити це за вас (C ++ ' autoнасправді робить те, що ви хочете). З іншого боку, якщо ви думаєте, що знаєте, що робить ваш код, і ви насправді набрали щось інше, статичне введення тексту подібне до цього виявить помилку раніше, перш ніж це стане величезною проблемою. Кожна мова виявляє рівновагу: статичне введення тексту, інформування типів, динамічне введення тексту. Для деяких завдань додаткове набір тексту насправді того вартий. Для інших це марно.
Корт Аммон - Відновіть Моніку

Вивчіть Ocaml та / або Haskell .... ви будете раді їх здібностям виводу типу.
Базиль Старинкевич

Відповіді:


46

Ви порівнюєте декларації змінної з #defines, що невірно. З допомогою A #defineви створюєте відображення між ідентифікатором та фрагментом вихідного коду. Потім препроцесор C буквально замінить будь-які виникнення цього ідентифікатора наданим фрагментом. Написання

#define FOO 40 + 2
int foos = FOO + FOO * FOO;

в кінцевому підсумку це те саме, що і для компілятора, як і для написання

int foos = 40 + 2 + 40 + 2 * 40 + 2;

Подумайте про це як про автоматичне копіювання та вставлення.

Також звичайні змінні можна перепризначити, тоді як макрос, створений за #defineдопомогою, не може (хоча ви можете #defineйого повторно ). Вираз FOO = 7буде помилкою компілятора, оскільки ми не можемо призначити "rvalues": 40 + 2 = 7є незаконним.

Отже, навіщо нам взагалі потрібні типи? Деякі мови, мабуть, позбавляються від типів, особливо це часто зустрічається в мовах скриптів Однак вони зазвичай мають щось, що називається "динамічне введення тексту", де змінні не мають фіксованих типів, але мають значення. Хоча це набагато гнучкіше, воно також менш ефективно. C любить продуктивність, тому має дуже просту та ефективну концепцію змінних:

Існує розтяг пам'яті під назвою "стек". Кожна локальна змінна відповідає області в стеку. Тепер питання в тому, скільки байтів має тривати ця область? У C кожен тип має чітко визначений розмір, до якого можна здійснити запит sizeof(type). Компілятору необхідно знати тип кожної змінної, щоб він міг резервувати правильний простір у стеку.

Чому константи, створені з #defineнеобхідністю, не потребують анотації типу? Вони не зберігаються в стеку. Натомість #defineстворює фрагменти вихідного коду для багаторазового використання дещо кориснішим способом, ніж копіювання та вставка. Літерали у вихідному коді, такі як "foo"або 42.87зберігаються компілятором, або вкладені як спеціальні вказівки, або в окремий розділ даних отриманого двійкового файлу.

Однак літерали мають типи. Буквальний рядок - це char *. 42є, intале може також використовуватися для коротших типів (звуження перетворення). 42.8буде a double. Якщо у вас є літерал і ви хочете, щоб він мав інший тип (наприклад, зробити 42.8a floatчи 42an unsigned long int), тоді ви можете використовувати суфікси - літеру після літералу, яка змінює спосіб поводження компілятора з цим літералом. У нашому випадку можна сказати 42.8fчи 42ul.

Деякі мови мають статичну типізацію, як у С, але примітки про тип необов’язкові. Прикладами є ML, Haskell, Scala, C #, C ++ 11 та Go. Як це працює? Магія? Ні, це називається "типовий умовивід". У C # і Go компілятор розглядає праву частину завдання і виводить тип цього. Це досить просто, якщо правий бік є буквальним на зразок 42ul. Тоді очевидно, яким повинен бути тип змінної. В інших мовах також є складніші алгоритми, які враховують спосіб використання змінної. Наприклад, якщо ви це робите x/2, то xце не може бути рядком, але повинно мати деякий числовий тип.


Дякую за пояснення. Я розумію, що коли ми оголошуємо тип змінної (локальної чи глобальної), ми фактично повідомляємо компілятору, скільки місця слід залишити для цієї змінної у стеку. З іншого боку, у #defineнас є константа, яка безпосередньо перетворюється на двійковий код - як би довго це не було - і вона зберігається в пам'яті як є.
user106313

2
@ user31782 - не зовсім. Коли ви оголошуєте змінну, тип повідомляє компілятору, якими властивостями володіє змінна. Однією з таких властивостей є розмір; інші властивості включають, як вони представляють значення та які операції можна виконати над цими значеннями.
Піт Бекер

@PeteBecker Тоді як компілятор знає ці інші властивості #define X 5.2?
user106313

1
Це тому, що передаючи неправильний тип, printfви викликали невизначене поведінку. На моїй машині, який фрагмент друкує інше значення кожного разу, на Ideone воно виходить з ладу після друку нуля.
Matteo Italia

4
@ User31782 - «Схоже , що я можу виконати будь-яку операцію на будь-якому типі даних» Номер X*Yне діє , якщо Xі Yє покажчиками, але це нормально , якщо вони intс; *Xне вірно, якщо Xє int, але добре, якщо це вказівник.
Піт Бекер

4

X у другому прикладі ніколи не є поплавком. Він називається макросом, він замінює визначене значення макросу "X" у джерелі значенням. Прочитується стаття на #define знаходиться тут .

У разі поставленого коду перед компіляцією препроцесор змінює код

Z=Y+X;

до

Z=Y+5.2;

і ось що складається.

Це означає, що ви також можете замінити ці значення на код типу

#define X sqrt(Y)

або навіть

#define X Y

3
Його просто називають макросом, а не варіативним макросом. Варіаційний макрос - це макрос, який приймає змінну кількість аргументів, наприклад #define FOO(...) { __VA_ARGS__ }.
hvd

2
Моє погано, виправлюсь :)
Джеймс Снелл

1

Коротка відповідь - типи потреб C через історію / представлення обладнання.

Історія: C був розроблений на початку 1970-х років і призначений як мова для програмування систем. Код ідеально швидкий і найкраще використовує можливості апаратного забезпечення.

Визначення типів під час компіляції було б можливим, але і без того повільний час компіляції збільшився (див . Мультфільм "компіляція" XKCD. Це було застосовано до "привітного світу" принаймні 10 років після публікації C ). Визначення типів під час виконання не відповідало б цілям програмування систем. Виконання часу виконання вимагає додаткової бібліотеки часу виконання. C прийшов задовго до першого ПК. Який мав 256 ОЗУ. Не гігабайти чи мегабайти, а кілобайти.

У вашому прикладі, якщо ви пропустите типи

   X=5.2;
   Y=5.1;

   Z=Y+X;

Тоді компілятор міг із задоволенням розібратися, що X&Y - плаваючі та зробив Z те саме. Насправді сучасний компілятор також розібрався б у тому, що X & Y не потрібні, і просто встановити Z на 10.3.

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

Невже дубль буде доречнішим за поплавок? Займає більше пам’яті та повільніше, але точність результату була б вищою.

Можливо, повернене значення функції може бути int (або довгим), тому що децималі не були важливими, хоча перетворення з float в int не обійшлося без витрат.

Повернене значення також може бути подвійним, гарантуючи, що float + float не переливається.

Усі ці питання здаються безглуздими для переважної більшості кодів, написаних сьогодні, але були життєво важливими, коли вироблявся C.


1
це не пояснює , наприклад , чому заяви типу не були зроблені необов'язковими, дозволяючи програмісту вибрати або оголошувати їх явно або покладатися на компілятор для виведення
комар

1
Справді це не @gnat. Я переробив текст, але в цей час не було сенсу робити це. Домен C був розроблений для того, щоб насправді вирішили зберігати 17 в 1 байті, або 2 байти або 4 байти, або як рядок, або як 5 біт у слові.
itj

0

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

У багатьох нових мовах є системи, які дозволяють використовувати змінні, не вказуючи їх тип (ruby, javascript, python тощо)


12
Жодна з згаданих вами мов (Ruby, JS, Python) не має виводу типу як мовної функції, хоча реалізація може використовувати її для підвищення ефективності. Натомість вони використовують динамічне введення тексту, де значення мають типи, але змінних чи інших виразів немає.
амон

2
JS не дозволяє вам опустити тип - це просто не дозволяє вам оголосити його взагалі. Він використовує динамічне введення тексту, де значення мають типи (наприклад, trueє boolean), а не змінні (наприклад, var xможуть містити значення будь-якого типу). Крім того, наводяться умовиводи для таких простих випадків, як ті, що виникають з питань, напевно, були відомі за десять років до виходу С.
сценарій

2
Це не робить заяву неправдивим (для того, щоб змусити щось потрібно також дозволити). Існуючі умовиводи не змінюють факту, що система типу C є результатом свого історичного контексту (на відміну від конкретно викладеного філософського міркування чи технічного обмеження)
Трістан Бернсайд

2
Зважаючи на те, що ML - який майже такий же вік, як і C - має висновок про тип, "він старий" не є хорошим поясненням. Контекст, в якому використовувався і розвивався C (невеликі машини, які вимагали дуже малого сліду для компілятора), здається більш імовірним. Не маю ідеї, чому ви б згадали про динамічне введення мов, а не лише кілька прикладів мов із висновком типу - Haskell, ML, heck C # має його - навряд чи більше незрозумілою особливістю.
Во

2
@BradS. Fortran не є хорошим прикладом, тому що перша літера імені змінної є декларацією типу, якщо ви не використовуєте implicit noneв цьому випадку ви повинні оголосити тип.
dmckee
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.