"Час життя" рядкового літералу в C


84

Чи не буде недоступним покажчик, який повертає наступна функція?

char *foo(int rc)
{
    switch (rc)
    {
        case 1:

            return("one");

        case 2:

            return("two");

        default:

            return("whatever");
    }
}

Тож час життя локальної змінної в C / C ++ практично лише в межах функції, так? Що означає, що після char* foo(int)завершення вказівник, який він повертає, вже нічого не означає, правда?

Я трохи заплутаний у житті локальної змінної. Що таке гарне роз'яснення?


10
Єдиний "var" у вашій функції - це параметр int rc. Його термін служби закінчується на кожному з- returnх. Покажчики, які ви повертаєте, стосуються рядкових літералів. Рядкові літерали мають статичну тривалість зберігання: тривалість життя принаймні така, як у програми.
Каз,

14
@PedroAlves Чому ні? Методи дозволяють абстрагуватися; що, якщо в майбутньому рядок буде читатися з ресурсу перекладу, але підтримка інтернаціоналізації не потрібна для V1 (або V0.5) продукту?
dlev

1
@PedroAlves "Ваш код справді працює (і ви можете побачити його, якщо спробувати скомпілювати)," Це не слід. Багато (більшість? По суті кожен?) Компілятор c будуть споживати незаконний код і часто видаватимуть код, який, здається, працює. Але спробуйте в іншому компіляторі (або навіть в іншій версії того самого компілятора), і він може впасти.
dmckee --- екс-модератор кошеня

6
@PedroAlves, функція, яка повертає один константний рядок, може бути обмеженою, але як щодо функції, яка повертає будь-який із числа константних рядків, залежно від вводу чи стану об'єкта? Простим прикладом може бути функція, яка перетворює перерахування у його рядкове представлення.
Марк Ренсом

4
Ви ніколи не бачили strerrorфункції, очевидно.
Каз

Відповіді:


86

Так, час дії локальної змінної знаходиться в межах області ( {, }), в якій вона створюється.

Локальні змінні мають автоматичне або локальне зберігання. Автоматичні, оскільки вони автоматично знищуються, коли область дії, в якій вони створені, закінчується.

Однак те, що ви маєте тут, - це рядковий літерал, який виділяється у визначеній реалізацією пам’яті лише для читання. Рядкові літерали відрізняються від локальних змінних, і вони залишаються живими протягом усього терміну роботи програми. Вони мають статичну тривалість життя [Посилання 1] .

Слово обережності!

Однак зауважте, що будь-яка спроба змінити вміст рядкового літералу є невизначеною поведінкою (UB). Користувацьким програмам не дозволяється змінювати вміст рядкового літералу.
Отже, завжди рекомендується використовувати constчас, оголошуючи рядок літералом.

const char*p = "string"; 

замість,

char*p = "string";    

Насправді в C ++ застаріло оголошувати рядковий літерал без, constхоча і не в C. Однак оголошення рядкового літералу з a constдає вам ту перевагу, яку компілятори зазвичай дають вам попередження на випадок спроби змінити рядковий літерал у другий випадок.

Зразок програми :

#include<string.h> 
int main() 
{ 
    char *str1 = "string Literal"; 
    const char *str2 = "string Literal"; 
    char source[]="Sample string"; 
 
    strcpy(str1,source);    // No warning or error just Uundefined Behavior 
    strcpy(str2,source);    // Compiler issues a warning 
 
    return 0; 
} 

Вихід:

cc1: попередження, що трактуються як помилки
prog.c: У функції 'main':
prog.c: 9: error: передавання аргументу 1 'strcpy' відкидає кваліфікатори від типу цільового покажчика

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


Щоб відповісти на питання, яке задають кілька користувачів тут:

Яка справа з інтегральними літералами?

Іншими словами, чи дійсний наступний код?

int *foo()
{
    return &(2);
} 

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

Щось на зразок:

prog.c:3: error: lvalue required as unary ‘&’ operand
     

Рядкові літерали є значеннями l, тобто: Ви можете взяти адресу рядкового літералу, але не можете змінити його вміст.
Тим НЕ менше, будь-які інші литералов ( int, float, char, і т.д.) є г-значення (стандарт С використовує термін значення виразу для них) і їх адреси не можуть бути прийняті на всіх.


[Посилання 1] Стандарт C99 6.4.5 / 5 "Рядові літерали - семантика":

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

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


Що робити, якщо користувач повертає щось подібне. char * a = & "abc"; повернути a; Чи це не буде дійсним?
Ashwin

@Ashwin: Тип рядкового літералу - char (*)[4]. Це тому, що тип "abc" є, char[4]і вказівник на масив із 4 символів оголошується як char (*)[4], отже, якщо вам потрібно взяти адресу, потрібно зробити це як char (*a)[4] = &"abc";і так, це дійсно.
Алок Зберегти

@Als "abc" є char[4]. (Через '\0')
asaelr

1
Може бути , це також буде хороша ідея , щоб попередити , що char const s[] = "text";це НЕ робить sбуквений символ, і , отже , s буде знищено в кінці області, тому всі ті, що вижили покажчики на нього будуть звисати.
celtschk,

1
@celtschk: Я б дуже хотів, але Q стосується конкретно рядкових літералів, тому я дотримувався б теми, однак, для тих, хто цікавиться моєю відповіддю тут, у чому різниця між char a [] = “string” і char * p = "рядок"? має бути досить корисним.
Алок Зберегти

74

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

Щодо С, що передбачено пунктом 6 розділу 6.4.5:

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

А щодо С ++ у розділі 2.14.5, пункти 8-11:

8 звичайних рядкових літералів та UTF-8 рядкові літерали також називаються вузькорядковими літералами. Вузький рядковий літерал має тип “масив з n const char”, де n - розмір рядка, як визначено нижче, і має статичну тривалість зберігання (3.7).

9 Рядовий літерал, який починається на u, наприклад u"asdf", є char16_tрядковим літералом. char16_tСтроковий літерал має тип «масив п const char16_t», де п розмір рядка , як визначено нижче; він має статичну тривалість зберігання і ініціалізується заданими символами. Один c-char може дати більше одного char16_tсимволу у вигляді сурогатних пар.

10 Рядковий літерал, який починається з U, наприклад U"asdf", є char32_tрядковим літералом. char32_tСтроковий літерал має тип «масив п const char32_t», де п розмір рядка , як визначено нижче; він має статичну тривалість зберігання і ініціалізується заданими символами.

11 Рядковий літерал, який починається з L, наприклад L"asdf", є широким рядковим літералом. Широкий рядковий літерал має тип “масив з n const wchar_t”, де n - розмір рядка, як визначено нижче; він має статичну тривалість зберігання і ініціалізується заданими символами.


FYI: ця відповідь була злитий з stackoverflow.com/questions/16470959 / ...
Shog9

14

Рядкові літерали дійсні для всієї програми (і не виділяються не стеком), тому вона буде дійсною.

Крім того, рядкові літерали доступні лише для читання, тому (для гарного стилю), можливо, вам слід змінити fooнаconst char *foo(int)


Що робити, якщо користувач повертає щось подібне. char * a = & "abc"; повернути a; Чи це не буде дійсним?
Ashwin

&"abc"не є char*. це адреса масиву, а його тип - char(*)[4]. Однак будь-які return &"abc";і char *a="abc";return a;дійсні.
asaelr

@asaelr: Насправді це не просто заради гарного стилю , перевірте мою відповідь для деталей.
Алок Зберегти

@Als Ну, якщо він напише всю програму, він зможе уникнути зміни рядка без написання const, і це буде повністю законно, але все одно це поганий стиль.
asaelr

якщо він дійсний для всієї програми, навіщо нам це робити?
TomSawyer

7

Так, це дійсний код, див. Випадок 1 нижче. Ви можете безпечно повернути рядки C із функції принаймні такими способами:

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

  • char*або const char*до статичного буфера символів. Він не повинен звільняти абонента. Він може бути модифікований (абонентом, якщо не const, або функцією, що повертає його), але функція, що повертає це, не може (легко) мати кілька буферів, тому вона не є (легко) безпечною для потоків, і абоненту може знадобитися скопіювати повернене значення перед повторним викликом функції.

  • char*в буфер, виділений malloc. Він може бути змінений, але зазвичай він повинен бути явно звільнений абонентом і має накладні витрати на розподіл купи. strdupє цього типу.

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

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


FYI: ця відповідь була злитий з stackoverflow.com/questions/16470959 / ...
Shog9

6

Хороше питання. Загалом, ви мали б рацію, але ваш приклад - виняток. Компілятор статично виділяє глобальну пам'ять для рядкового літералу. Отже, адреса, повернута вашою функцією, є дійсною.

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

Див. Також правильне спостереження @ asaelr const.


: Що робити, якщо користувач повертає щось подібне. char * a = & "abc"; повернути a; Чи це не буде дійсним?
Ashwin

Правильно. Насправді, можна просто писати const char *a = "abc";, опускаючи &. Причина полягає в тому, що рядок із подвійними лапками вирішується на адресу свого початкового символу.
thb

3

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

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

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


Що робити, якщо користувач повертає щось подібне. char * a = & "abc"; повернути a; Чи це не буде дійсним?
Ешвін,

@Ashwin: &"abc"не має типу char*, однак обидва "abc"і &"abc"діють протягом усього виконання програми.
AusCBloke

2

strніколи не буде звисаючим покажчиком, оскільки він вказує на статичну адресу, де знаходяться рядкові літерали.

Коли програма буде завантажена, вона буде в основному доступною лише для читання та глобальною .

Навіть якщо ви спробуєте звільнити або змінити, це призведе до помилки сегментації на платформах із захистом пам'яті .


FYI: ця відповідь була злитий з stackoverflow.com/questions/16470959 / ...
Shog9

якщо він ніколи не буде бовтатися, чи потрібно мені його пропустити? Немає?
TomSawyer

0

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


Що робити, якщо користувач повертає щось подібне. char * a = & "abc"; повернути a; Чи це не буде дійсним?
Ешвін,

0

У наведеному вище прикладі ви фактично повертаєте призначені вказівники до будь-якої функції, яка викликає вищезазначене. Тому він не став би локальним покажчиком. Більше того, для покажчиків, які потрібно повернути, пам’ять виділяється в глобальному сегменті.

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