Що робить таке використання покажчиків непередбачуваним?


108

Зараз я навчаюсь вказівниками, і мій професор наводив цей приклад коду:

//We cannot predict the behavior of this program!

#include <iostream>
using namespace std;

int main()
{
    char * s = "My String";
    char s2[] = {'a', 'b', 'c', '\0'};

    cout << s2 << endl;

    return 0;
}

Він написав у коментарях, що ми не можемо передбачити поведінку програми. Що саме робить це непередбачуваним? Я не бачу в цьому нічого поганого.


2
Ви впевнені, що правильно відтворили код професора? Хоча формально можна стверджувати, що ця програма може спричинити "непередбачувану" поведінку, робити це не має сенсу. І я сумніваюся, що будь-який професор використає щось настільки езотеричне, щоб проілюструвати студентам «непередбачуване».
ANT

1
@Lightness Races на орбіті: Укладачам дозволяється «приймати» неправильно сформований код після видачі необхідних діагностичних повідомлень. Але специфікація мови не визначає поведінку коду. Тобто через помилку при ініціалізації sпрограма, якщо її прийняв якийсь компілятор, формально має непередбачувану поведінку.
ANT

2
@TheParamagneticCroissant: Ні. Ініціалізація в сучасний час непрацездатна.
Гонки легкості на орбіті

2
@ Парамагнітний круасан: Як я вже говорив вище, мова не потребує неправильно сформованого коду, щоб "не вдалося скласти". Компілятори просто необхідні для постановки діагностики. Після цього їм дозволяється продовжувати роботу та "успішно" складати код. Однак поведінка такого коду не визначається специфікацією мови.
ANT

2
Я хотів би дізнатися, яку відповідь вам дав ваш професор.
Daniël W. Crompton

Відповіді:


125

Поведінка програми не існує, оскільки вона неправильно сформована.

char* s = "My String";

Це незаконно. До 2011 року його було знято з роботи протягом 12 років.

Правильний рядок:

const char* s = "My String";

Крім цього, програма чудово. Ваш професор повинен пити менше віскі!


10
з -pedantic це робить: main.cpp: 6: 16: попередження: ISO C ++ забороняє перетворювати рядкову константу в 'char *' [-Wpedantic]
marcinj

17
@black: Ні, той факт, що перетворення незаконне, робить програму неправильною. Це було застарілим у минулому . Ми вже не в минулому.
Гонки легкості на орбіті

17
(Що нерозумно, тому що це було метою 12-річної депресії)
Гонки легкості на орбіті

17
@black: Поведінка неправильно сформованої програми не "ідеально визначена".
Гонки легкості на орбіті

11
Незважаючи на це, питання стосується C ++, а не якоїсь конкретної версії GCC.
Гонки легкості на орбіті

81

Відповідь: це залежить від того, на який C ++ стандарт ви компілюєте. Весь код ідеально сформований у всіх стандартах ‡ за винятком цього рядка:

char * s = "My String";

Тепер рядковий літерал має тип, const char[10]і ми намагаємося ініціалізувати на нього неконст-покажчик. Для всіх інших типів, крім charсімейства рядкових літералів, така ініціалізація завжди була незаконною. Наприклад:

const int arr[] = {1};
int *p = arr; // nope!

Однак у програмі pre-C ++ 11 для літеральних рядків був виняток у §4.2 / 2:

Строковий літерал (2.13.4), який не є широким літеральним рядком, може бути перетворений у рецензію типу " вказівник на char "; [...]. В будь-якому випадку результат є вказівником на перший елемент масиву. Це перетворення враховується лише тоді, коли є явний відповідний цільовий тип вказівника, а не тоді, коли є загальна потреба перетворення з lvalue в rvalue. [Примітка. Ця конверсія застаріла . Див. Додаток D. ]

Тож у C ++ 03 код ідеально чудовий (хоча і застарілий) та має чітку передбачувану поведінку.

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

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

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

З цим, решта програми ідеально, оскільки ви ніколи більше не торкаєтесь s. Читання створеного constоб’єкта за допомогою не constвказівника цілком нормально. Запис створеного constоб'єкта за допомогою такого вказівника є невизначеною поведінкою:

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

Оскільки sв коді немає жодної модифікації , програма чудово діє на C ++ 03, вона не може компілюватись у C ++ 11, але все одно - але, враховуючи, що компілятори це дозволяють, все ще немає визначеної поведінки в ній † . Зважаючи на те, що компілятори досі [неправильно] інтерпретують правила C ++ 03, я не бачу нічого, що могло б призвести до "непередбачуваної" поведінки. Напишіть, sхоча, і всі ставки вимкнено. І в C ++ 03, і ​​в C ++ 11.


† Хоча, знову ж таки, за визначенням неправильно сформований код не дає очікувань розумної поведінки
‡ Як ні, дивіться відповідь Метта Макнабба


Я думаю, що тут "непередбачуваний" покликаний професор означати, що не можна використовувати стандарт, щоб передбачити, що буде робити компілятор із неправильно сформованим кодом (окрім видачі діагностики). Так, це може сприйматись так, як C ++ 03 каже, що до цього слід поводитися, і (загрожує помилкою "Не справжнього шотландеца"), здоровий глузд дозволяє нам з певною впевненістю передбачити, що це єдине, що має розумний автор-компілятор коли-небудь виберуть, чи код збирається взагалі. Знову ж таки, це може трактувати це як значення для перетворення рядкового літералу перед тим, як надати його на non-const. Стандартний C ++ не хвилює.
Стів Джессоп

2
@SteveJessop Я не купую цю інтерпретацію. Це ні визначена поведінка, ні категорія неправильно сформованого коду, яку стандарт позначає як діагностику не потрібна. Це просте порушення типу типу, яке повинно бути дуже передбачуваним (компілює та робить нормальні речі на C ++ 03, не може компілюватись на C ++ 11). Ви не можете використовувати помилки компілятора (або художні ліцензії), щоб припустити, що код непередбачуваний - інакше весь код буде тавтологічно непередбачуваним.
Баррі

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

1
Ні, це не є. Стандарт не визначає поведінку неправильно сформованих програм.
Стів Джессоп

1
@supercat: це справедливий момент, але я не вірю, що це головна причина. Я думаю, що головна причина, що стандарт не визначає поведінку неправильно сформованих програм, полягає в тому, що компілятори можуть підтримувати розширення до мови, додаючи синтаксис, який не є добре сформованим (як це робить Objective C). Дозвіл на реалізацію загальних труднощів після очищення після невдалої компіляції - лише бонус :-)
Стів Джессоп

20

Інші відповіді стосуються того, що ця програма неправильно формується в C ++ 11 через присвоєння const charмасиву a char *.

Однак програма була недостатньо сформована і до C ++ 11.

У operator<<перевантажених в <ostream>. Вимога щодо iostreamвключення ostreamбула додана в C ++ 11.

Історично, більшість реалізацій все-таки iostreamвключали ostream, можливо, для простоти реалізації або, можливо, для того, щоб забезпечити кращу якість якості.

Але було б відповідати iostreamлише для визначення ostreamкласу без визначення operator<<перевантажень.


13

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

В іншому випадку ця програма видається мені чітко визначеною:

  • Правила, які диктують, як масиви символів стають покажчиками символів, коли передаються як параметри (наприклад, з cout << s2), добре визначені.
  • Масив є нульовим байтом, який є умовою operator<<з char*(або const char*).
  • #include <iostream>включає <ostream>, що в свою чергу визначає operator<<(ostream&, const char*), тому все, здається, на свої місця.

12

Ви не можете передбачити поведінку компілятора з причин, зазначених вище. ( Слід зібрати, але не може.)

Якщо компіляція успішна, то поведінка чітко визначена. Ви, звичайно, можете передбачити поведінку програми.

Якщо це не вдалося компілювати, програми немає. У мові компіляції програма є виконуваним, а не вихідним кодом. Якщо у вас немає виконуваного файлу, у вас немає програми, і ви не можете говорити про поведінку того, чого не існує.

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


10

Як зазначали інші, код є нелегітимним під C ++ 11, хоча він був дійсним у попередніх версіях. Отже, компілятору для C ++ 11 потрібно видати щонайменше одну діагностику, але поведінка компілятора або решти системи побудови не визначена поза цим. Ніщо в Стандарті не забороняло б компілятору різко виходити у відповідь на помилку, залишаючи частково записаний об’єктний файл, який лінкер може вважати дійсним, даючи зламаний виконуваний файл.

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

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

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