Що за провокаційне запитання!
Навіть побіжне сканування відповідей та коментарів у цій темі дозволить виявити, наскільки емоційним виявляється ваш, здавалося б, простий і прямий запит уперед.
Це не повинно дивувати.
Безперечно, нерозуміння навколо концепції і використання в покажчиках є домінуючу причину серйозних збоїв в програмуванні в цілому.
Визнання цієї реальності легко проявляється в повсюдності мов, розроблених спеціально для вирішення, і бажано, щоб уникнути проблем, які вказують вказівники взагалі. Подумайте, що C ++ та інші похідні C, Java та її відносин, Python та інших сценаріїв - лише як найвидатніші та найпоширеніші, і більш-менш упорядковані всерйоз вирішення цього питання.
Розвиток більш глибокого розуміння принципів , що лежать в основі, отже , повинні бути доречні до кожній людині , яка прагне до досконалості в програмуванні - особливо на рівні систем .
Я уявляю, що це саме те, що має показати ваш вчитель.
А природа С робить його зручним транспортним засобом для цієї розвідки. Менш чітко, ніж збірка - хоча і, можливо, більш зрозуміла - і все ж набагато чіткіше, ніж мови, засновані на більш глибокій абстракції середовища виконання.
Створений для полегшення детермінованого перекладу намірів програміста в інструкції, які машини можуть зрозуміти, C - це система системного рівня . Хоча класифікується як високий рівень, він дійсно відноситься до категорії «середній»; але оскільки такого не існує, позначення "системи" повинно бути достатньо.
Ця характеристика значною мірою відповідає за те, що вона стала мовою вибору для драйверів пристроїв , коду операційної системи та вбудованих реалізацій. Крім того, заслужена альтернатива в додатках, де оптимальна ефективність є найважливішою; де це означає різницю між виживанням та вимиранням, а тому є необхідністю на відміну від розкоші. У таких випадках приваблива зручність переносимості втрачає всю її привабливість, а вибір недолікової продуктивності найменшого загального знаменника стає немислимим згубним варіантом.
Що робить C - та деякі його похідні - зовсім особливими, це те, що він дозволяє своїм користувачам повний контроль - коли це те, чого вони хочуть - без покладаючи на них пов'язаних обов'язків, коли вони цього не роблять. Тим не менш, він ніколи не пропонує більше, ніж найтонший ізолятор від машини , тому правильне використання вимагає глибокого розуміння концепції покажчиків .
По суті, відповідь на ваше запитання є піднесено простою і задовільно солодкою - на підтвердження ваших підозр. При умови , однак, що один надає необхідне значення для кожного поняття в цій заяві:
- Акти вивчення, порівняння та маніпулювання покажчиками завжди і обов'язково справедливі, тоді як висновки, отримані з результату, залежать від обгрунтованості значень, що містяться, і, отже, не повинні бути.
Перший є незмінно безпечним і потенційно власне ,той час як останні можуть тільки колиабо бути власне , коли вона була створена , як сейф . Дивно - для деяких - тому встановлення обгрунтованості останнього залежить і вимагає від першого.
Звичайно, частина плутанини виникає внаслідок ефекту рекурсії, притаманної в рамках принципу вказівника, - і проблем, що виникають при диференціації змісту від адреси.
Ви цілком правильно переконали,
Мене спонукають думати, що будь-який вказівник можна порівняти з будь-яким іншим вказівником, незалежно від того, де вони окремо вказують. Більше того, я вважаю, що арифметика вказівника між двома вказівниками добре, незалежно від того, куди вони окремо вказують, оскільки арифметика просто використовує адреси пам'яті для зберігання покажчиків.
І кілька учасників підтвердили: покажчики - це просто цифри. Іноді щось ближче до складних чисел, але все ж не більше числа.
Кумедна прискіпливість, в якій ця суперечка була отримана тут, розкриває більше про природу людини, ніж програмування, але залишається достойною уваги та деталізації. Можливо, ми це зробимо пізніше ...
Як один коментар починає натякати; уся ця плутанина і занепокоєння випливає з необхідності розрізнити те, що справедливо від безпечного , але це надмірне спрощення. Ми також повинні розрізняти, що є функціональним, а що надійним , що практичним і що може бути належним , а далі: що належне в конкретних обставинах від того, що може бути належним у більш загальному розумінні . Не кажучи вже про; різниця між відповідністю та пристойністю .
Для цього нам спочатку потрібно оцінити саме тещо покажчик знаходиться .
- Ви продемонстрували міцний захват на цю концепцію, і, як і деякі інші, ви можете вважати, що ці ілюстрації є покровительними спрощеними, але рівень очевидності плутанини тут вимагає такої простоти в уточненні.
Як вказували декілька: термін вказівник - це лише особлива назва того, що є просто індексом , і, таким чином, не більше ніж будь-яке інше число .
Це вже повинно бути очевидним, враховуючи той факт, що всі сучасні основні комп'ютери - це обов'язково двійкові машини працюють виключно з чисел і на них . Квантові обчислення можуть це змінити, але це вкрай малоймовірно, і воно не досягло віку.
Технічно, як ви зазначили, покажчики є більш точними адресами ; очевидне розуміння, яке природно вводить корисну аналогію співвіднесення їх з "адресами" будинків або ділянок на вулиці.
У моделі плоскої пам’яті: вся системна пам’ять організована в єдиній лінійній послідовності: всі будинки міста лежать на одній дорозі, і кожен будинок однозначно ідентифікується лише за своєю кількістю. Чудово простий.
В сегментованих схемах: ієрархічна організація пронумерованих доріг вводиться вище, ніж нумерованих будинків, так що необхідні складові адреси.
- Деякі реалізації все ще більш суперечливі, і сукупність різних "доріг" не повинна дорівнювати суміжній послідовності, але жодне з цього нічого не змінює базового.
- Нам обов'язково вдається розкласти кожну таку ієрархічну зв'язок назад в рівну організацію. Чим складніша організація, тим більше обручів нам доведеться перестрибувати, щоб це зробити, але це повинно бути можливим. Дійсно, це стосується і «реального режиму» на x86.
- Інакше відображення посилань на локації не буде біективним , оскільки надійне виконання - на системному рівні - вимагає цього ПОВИНЕН бути.
- повинно бути декілька адрес невідображатись у єдиних місцях пам'яті;
- Сингулярні адреси ніколи не повинні відображатись у кількох місцях пам'яті.
Приводячи нас до подальшого повороту, який перетворює головоломку в такий захоплююче складний клубок . Вище було доцільним припустити, що покажчики - це адреси, для простоти та ясності. Звичайно, це не правильно. Вказівник - це не адреса; покажчик - це посилання на адресу , він містить адресу . Як і конверт, має посилання на будинок. Якщо замислитись над цим, це може призвести до того, що ви зрозумієте, що малося на увазі з пропозицією рекурсії, що міститься в концепції. Все-таки; у нас є тільки стільки слів, і ми говоримо про адреси посилань на адреси і таке, незабаром зупиняє більшість мізків за недійсним винятком оп-коду . І здебільшого наміри легко вибираються з контексту, тому повернемося до вулиці.
Поштові працівники цього нашого уявного міста дуже схожі на тих, кого ми знаходимо у «реальному» світі. Ніхто, ймовірно, не постраждає від інсульту, коли ви розмовляєте чи запитуєте про недійсну адресу, але кожен останній буде лаяти, коли ви попросите їх діяти на цій інформації.
Припустимо, на нашій особливій вулиці всього 20 будинків. Далі зробіть вигляд, що якась хибна чи дислексична душа скерувала лист, дуже важливий, на номер 71. Тепер ми можемо запитати у нашого перевізника Френка, чи є така адреса, і він просто і спокійно повідомить: ні . Ми навіть можемо очікувати , що він оцінити , наскільки далеко за межами вулиці це місце буде лежати , якщо вона дійсно існує: приблизно в 2,5 рази далі , ніж в кінці. Ніщо з цього не викличе у нього ніякого роздратування. Однак, якби ми попросили його доставити цей лист або забрати предмет з того місця, він, швидше за все, буде відвертим щодо свого незадоволення та відмови. виконувати його.
Покажчики - це лише адреси, а адреси - просто числа.
Перевірте висновок наступного:
void foo( void *p ) {
printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}
Називайте його на скільки завгодно покажчиків, дійсних чи ні. Будь ласка , опублікуйте свої висновки, якщо це не вдалося на вашій платформі або ваш (сучасний) компілятор скаржиться.
Тепер, оскільки покажчики - це просто цифри, їх неминуче справедливо порівнювати. В одному сенсі саме це демонструє ваш вчитель. Усі наступні твердження цілком справедливі - і належні! - C, і коли компіляція буде працювати без проблем , навіть якщо жоден вказівник не потребує ініціалізації, і значення, які вони містять, можуть бути невизначені :
- Ми обчислюємо лише
result
чітко для ясності , і друкуємо його, щоб змусити компілятора обчислити те, що в іншому випадку було б зайвим, мертвим кодом.
void foo( size_t *a, size_t *b ) {
size_t result;
result = (size_t)a;
printf(“%zu\n”, result);
result = a == b;
printf(“%zu\n”, result);
result = a < b;
printf(“%zu\n”, result);
result = a - b;
printf(“%zu\n”, result);
}
Звичайно, програма неправильно формується, коли або a або b не визначено (читати: неправильно ініціалізовано ) в момент тестування, але це абсолютно не має значення для цієї частини нашої дискусії. Ці фрагменти, як і наступні твердження, гарантуються - «стандартним» - для компіляції та запуску бездоганно, незважаючи на IN- недійсність будь-якого вказівника.
Проблеми виникають лише тоді, коли недійсний покажчик буде відмежований . Коли ми просимо Френка забрати або доставити за недійсною, неіснуючою адресою.
Дано будь-який довільний вказівник:
int *p;
Хоча ця операція повинна компілювати та запускати:
printf(“%p”, p);
... як це має бути:
size_t foo( int *p ) { return (size_t)p; }
... наступні два, на відміну від цього, все одно легко компілюються, але не спрацьовують у виконанні, якщо покажчик не є дійсним - під цим ми просто маємо на увазі, що він посилається на адресу, до якої даний додаток отримав доступ :
printf(“%p”, *p);
size_t foo( int *p ) { return *p; }
Наскільки тонкі зміни? Різниця полягає в різниці між значенням покажчика - який є адреса, а значення змісту: будинки на цей номер. Жодна проблема не виникає, поки покажчик не буде відмежований ; поки не буде зроблена спроба отримати доступ до адреси, на яку він посилається. Намагаючись доставити або забрати пакет за межі ділянки дороги ...
У більш широкому сенсі , той же принцип обов'язково відноситься до більш складним прикладів, включаючи вищезгадану необхідність в створенні необхідної достовірності:
int* validate( int *p, int *head, int *tail ) {
return p >= head && p <= tail ? p : NULL;
}
Реляційне порівняння та арифметика пропонують однакову корисність для тестування еквівалентності, і однаково справедливі - в принципі. Однак те, що означають результати такого обчислення , - це зовсім інше питання - і саме це питання вирішується цитатами, які ви включили.
У C масив - це суміжний буфер, безперебійний лінійний ряд пам'яті. Порівняння та арифметика, застосована до покажчиків, які посилаються на місця в такому сингулярному ряду, мають природний характер і, очевидно, значущі як стосовно один одного, так і до цього "масиву" (який просто ідентифікується базою). Точно те саме стосується кожного блоку, виділеного через malloc
, або sbrk
. Оскільки ці зв’язки є неявними , компілятор може встановити дійсні зв’язки між ними, а тому може бути впевнений, що розрахунки забезпечать очікувані відповіді.
Виконуючи подібну гімнастику на покажчиках, які посилаються на окремі блоки чи масиви, не пропонують такої притаманної та очевидної корисності. Тим більше, що будь-яке відношення існує в один момент, може бути визнано недійсним шляхом перерозподілу, що випливає, де це велика ймовірність змінитись, навіть перевернутись. У таких випадках компілятор не може отримати необхідну інформацію для встановлення впевненості, яку він мав у попередній ситуації.
Ви , однак, як програміст, може мати такі знання! І в деяких випадках це зобов’язано використовувати.
Там ЯВЛЯЮТЬСЯ Таким чином, обставини , при яких навіть це повністю ДІЙСНИЙ і зовсім PROPER.
Насправді, саме це malloc
доводиться робити всередині країни, коли настає час спробувати об'єднати меліоровані блоки - на переважній більшості архітектур. Те саме стосується і розподільника операційної системи, як і позаду sbrk
; якщо більш очевидно , часто для більш розрізнених організацій, то більш критично - і доречно також на платформах, де цього malloc
може не бути. І скільки з них не написано на С?
Обґрунтованість, безпека та успішність дії неминуче є наслідком рівня розуміння, на якому вона передує та застосовується.
У запропонованих вами цитатах Керніган та Річі займаються тісно пов’язаною, але, тим не менш, окремою проблемою. Вони визначають ті обмеження на мову , і пояснити , як ви можете скористатися наявними можливостями компілятора , щоб захистити вас , принаймні виявлення потенційно помилкові конструкції. Вони описують довжини, на які механізм може розробитись , щоб допомогти вам у вирішенні завдань програмування. Укладач - твій слуга, ти - господар. Мудрий господар, однак, той, хто глибоко знайомий з можливостями різних своїх слуг.
У цьому контексті невизначена поведінка служить для вказівки на потенційну небезпеку та можливість заподіяння шкоди; не означати неминучої, незворотної приреченості чи кінця світу, як ми його знаємо. Це просто означає, що ми, «маючи на увазі компілятор», - не в змозі зробити будь-яку думку про те, якою може бути ця річ, або представляти, і з цієї причини ми вирішимо помити свої справи. Ми не будемо нести відповідальність за будь-які нещасні випадки, які можуть бути наслідком використання або неправильного використання цього засобу .
Насправді це просто говорить: "Поза цим моментом, ковбой : ти сам ..."
Ваш професор прагне продемонструвати вам найтонші нюанси .
Зауважте, яку велику обережність вони поставили під час створення їх прикладу; і як крихкий він все ще є. За адресою a
, в
p[0].p0 = &a;
компілятор примушується виділяти фактичну пам’ять для змінної, а не розміщувати її в регістрі. Однак, оскільки це автоматична змінна, програміст не має контролю над тим, куди це призначено, і тому не в змозі зробити жодних дійсних припущень щодо того, що буде після неї. Ось чому, щоб код працював так, як очікувалося, його a
потрібно встановити рівним нулю.
Просто зміна цього рядка:
char a = 0;
до цього:
char a = 1; // or ANY other value than 0
призводить до того, що поведінка програми стає невизначеною . Як мінімум, перша відповідь тепер буде 1; але проблема набагато зловісніша.
Тепер код запрошує катастрофи.
Незважаючи на те, що він досі справжній і навіть відповідає стандарту , він зараз непрацюючий, і хоч це обов'язково складено, але може не виконати виконання з різних причин. На даний момент не існує безліч проблем - жоден з яких компілятор знаходиться в стані , щоб розпізнати.
strcpy
почнеться за адресою a
та виходитиме за межі цього, щоб споживати - і переносити - байт за байтом, поки він не зустріне нуль.
p1
Покажчик був инициализирован до блоку рівно 10 байт.
Якщо a
випадково буде розміщено в кінці блоку і процес не має доступу до наступного, наступне читання p0 [1] - призведе до сегмента за замовчуванням. Цей сценарій навряд чи в архітектурі x86, але можливий.
Якщо область за межами адреси a
є доступною, чи не будуть відбуватися ніяких помилок читання, але програма все ще не врятована від нещастя.
Якщо нульовий байт трапиться протягом десяти, що починається за адресою a
, він все одно може вижити, оскільки тоді strcpy
він зупиниться і, принаймні, ми не зазнаємо порушення запису.
Якщо він НЕ порушений для читання негаразд, але не нульовий байт не відбувається в цьому проміжку 10, strcpy
буде продовжувати і намагатися писати за межами блоку , виділеним malloc
.
Якщо ця область не є власністю процесу, слід негайно запустити сегментатор.
Ще більш катастрофічна - і тонка --- ситуація виникає , коли наступний блок знаходиться в власності процесу, то помилка не може бути виявлена, сигнал не може бути підвищена, і таким чином це може «з'явитися» ще «працювати» , хоча він фактично буде перезаписати інші дані, структури управління алокатора або навіть код (у певних операційних середовищах).
Це є чому пов'язаний покажчик помилки можуть бути настільки важко , щоб відстежувати . Уявіть, що ці рядки закопані глибоко в тисячах рядків хитромудро пов'язаного коду, який написав хтось інший, і вас направлять поглибитись.
Тим не менш , програма все одно повинна складатись, оскільки вона залишається абсолютно дійсною та стандартною відповідною C.
Такі помилки, жоден стандарт і жоден компілятор не можуть захистити від необережних. Я думаю, що саме цього вони мають намір навчити вас.
Paranoid люди постійно прагнуть змінити на природу в C , щоб позбутися від цих проблемних можливостей і так врятувати нас від самих себе; але це нечесно . Це відповідальність, яку ми зобов’язані взяти на себе, коли вирішимо переслідувати владу та отримати свободу, яку нам пропонує більш прямий та всебічний контроль над машиною. Промоутери та переслідувачі досконалості у виконанні ніколи не приймуть нічого менше.
Переносність та загальність, яку він представляє, - це принципово окремий розгляд, і все , до чого прагне стандарт :
Цей документ визначає форму та встановлює інтерпретацію програм, виражених мовою програмування C. Її метою є сприяти портативності , надійності, ремонтопридатності та ефективному виконанню мовних програм C на різних обчислювальних системах .
Ось чому цілком належним чином залишати його відмінним від визначення та технічної специфікації самої мови. Всупереч тому, що багато хто, здається, вірить Спільністю є антитезою до винятковим і зразковим .
Прийти до висновку:
- Вивчення та маніпулювання самими покажчиками незмінно справедливі та часто плідні . Інтерпретація результатів може бути або не може бути осмисленою, але лихо ніколи не запрошується до вказівника відмежований ; поки не буде зроблена спроба отримати доступ до адреси, пов'язаної з цим.
Це не було правдою, програмуючи так, як ми це знаємо - і любимо - не було б можливим.
C
с тим, що є безпечним вC
. Порівнювати два покажчики на один і той же тип завжди можна (перевіряючи рівність, наприклад), однак, використовуючи арифметику вказівника та порівняння,>
і безпечно<
лише при використанні в заданому масиві (або блоці пам'яті).