„Czas życia” dosłownego ciągu znaków w C


84

Czy wskaźnik zwracany przez następującą funkcję nie byłby niedostępny?

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

            return("one");

        case 2:

            return("two");

        default:

            return("whatever");
    }
}

Więc czas życia zmiennej lokalnej w C / C ++ jest praktycznie tylko w funkcji, prawda? Co oznacza, że ​​po char* foo(int)zakończeniu zwracany wskaźnik już nic nie znaczy, prawda?

Jestem trochę zdezorientowany co do czasu życia zmiennej lokalnej. Co to jest dobre wyjaśnienie?


10
Jedyną zmienną, jaką masz w swojej funkcji, jest parametr int rc. Jego żywotność kończy się na każdym z return-s. Wskaźniki, które zwracasz, dotyczą literałów łańcuchowych. Literały łańcuchowe mają statyczny czas trwania: ich żywotność jest co najmniej tak długa, jak czas trwania programu.
Kaz,

14
@PedroAlves Dlaczego nie? Metody pozwalają na abstrakcję; co się stanie, jeśli w przyszłości ciąg zostanie odczytany z zasobu tłumaczeniowego, ale obsługa internacjonalizacji nie jest potrzebna w przypadku V1 (lub V0.5) produktu?
dlev

1
@PedroAlves "Twój kod na pewno zadziała (i możesz to zobaczyć, jeśli spróbujesz się skompilować)" . Wiele (większość? W zasadzie każdy?) Kompilator c zużywa nielegalny kod i często emituje kod, który wydaje się działać. Ale spróbuj tego w innym kompilatorze (lub nawet w innej wersji tego samego kompilatora) i może się przewrócić.
dmckee --- kociak ex-moderator

6
@PedroAlves, funkcja zwracająca pojedynczy stały ciąg może mieć ograniczone zastosowanie, ale co powiesz na taką, która zwraca jeden z wielu stałych ciągów w zależności od stanu wejściowego lub obiektu? Prostym przykładem może być funkcja konwertująca wyliczenie na jego reprezentację w postaci ciągu.
Mark Ransom

4
strerrorNajwyraźniej nigdy nie widziałeś tej funkcji.
Kaz

Odpowiedzi:


86

Tak, czas życia zmiennej lokalnej mieści się w zakresie ( {, }), w którym została utworzona.

Zmienne lokalne mają pamięć automatyczną lub lokalną. Automatyczne, ponieważ są automatycznie niszczone po zakończeniu zakresu, w którym zostały utworzone.

Jednak mamy tutaj literał łańcuchowy, który jest przydzielony w implementacji pamięci tylko do odczytu. Literały łańcuchowe różnią się od zmiennych lokalnych i pozostają żywe przez cały okres istnienia programu. Mają statyczny czas trwania [Ref 1] żywotność.

Słowo ostrzeżenia!

Należy jednak pamiętać, że każda próba zmodyfikowania zawartości literału ciągu jest niezdefiniowanym zachowaniem (UB). Programy użytkownika nie mogą modyfikować zawartości literału ciągu.
W związku z tym zawsze zaleca się użycie constwhile deklarując literał ciągu.

const char*p = "string"; 

zamiast,

char*p = "string";    

W rzeczywistości w C ++ deklarowanie literału tekstowego bez constchociaż nie w C.Jednakże zadeklarowanie literału ciągu znaków za pomocą a constdaje ci tę zaletę, że kompilatory zwykle dają ostrzeżenie w przypadku próby zmodyfikowania literału ciągu w drugi przypadek.

Przykładowy program :

#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; 
} 

Wynik:

cc1: ostrzeżenia traktowane jako błędy
prog.c: W funkcji „main”:
prog.c: 9: error: przekazanie argumentu 1 z „strcpy” usuwa kwalifikatory z typu docelowego wskaźnika

Zwróć uwagę, że kompilator ostrzega przed drugim przypadkiem, ale nie w pierwszym.


Aby odpowiedzieć na pytanie zadane przez kilku użytkowników:

O co chodzi z literałami całkowitymi?

Innymi słowy, czy poniższy kod jest prawidłowy?

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

Odpowiedź brzmi: nie, ten kod jest nieprawidłowy. Jest źle sformułowany i spowoduje błąd kompilatora.

Coś jak:

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

Literały łańcuchowe są l-wartościami, tj .: Możesz wziąć adres literału ciągu, ale nie możesz zmienić jego zawartości.
Jednak inne literałami ( int, float, char, etc.) są wartości R (średnia C używa określenia wartości wyrażenia dla nich) i ich adres nie może być wykorzystany w ogóle.


[Ref 1] C99 standard 6.4.5 / 5 "Literały łańcuchowe - semantyka":

W fazie translacji 7, bajt lub kod o wartości zero jest dołączany do każdej wielobajtowej sekwencji znaków, która wynika z literału ciągu lub literałów. Sekwencja znaków wielobajtowych jest następnie używana do zainicjowania tablicy o statycznym czasie trwania i długości wystarczającej do zawarcia sekwencji . W przypadku literałów łańcuchów znaków elementy tablicy mają typ char i są inicjowane pojedynczymi bajtami wielobajtowej sekwencji znaków; w przypadku literałów szerokich ciągów elementy tablicy mają typ wchar_t i są inicjowane sekwencją szerokich znaków ...

Nie jest określone, czy te tablice są różne, pod warunkiem, że ich elementy mają odpowiednie wartości. Jeśli program spróbuje zmodyfikować taką tablicę, zachowanie jest niezdefiniowane .


A co jeśli użytkownik zwraca coś takiego? char * a = & "abc"; return a; Czy to nie będzie ważne?
Ashwin

@Ashwin: typ literału ciągu to char (*)[4]. Dzieje się tak, ponieważ typ "abc" to char[4]i wskaźnik do tablicy składającej się z 4 znaków jest zadeklarowany jako char (*)[4], więc jeśli potrzebujesz wziąć jego adres, musisz to zrobić tak, jak char (*a)[4] = &"abc";i Tak, jest poprawny.
Alok Zapisz

@Als „abc” to char[4]. (Z powodu '\0')
asaelr

1
Możliwe byłoby również dobry pomysł, aby ostrzec, że char const s[] = "text";nie nie uczynić sznak literalny, a zatem s będą niszczone na końcu zakresu, więc wszelkie wskazówki, które przeżyły do niego będzie zwisać.
celtschk

1
@celtschk: Bardzo bym chciał, ale Q odnosi się konkretnie do literałów łańcuchowych, więc trzymałbym się tego tematu. Jednak dla zainteresowanych moja odpowiedź tutaj, Jaka jest różnica między char a [] = "string" a char * p = „ciąg”? powinno być raczej pomocne.
Alok Save

74

To jest ważne. Literały ciągów mają statyczny czas trwania, więc wskaźnik nie zwisa.

Dla C, co jest wymagane w sekcji 6.4.5, paragraf 6:

W fazie translacji 7, bajt lub kod o wartości zero jest dołączany do każdej wielobajtowej sekwencji znaków, która wynika z literału ciągu lub literałów. Sekwencja znaków wielobajtowych jest następnie używana do zainicjowania tablicy o statycznym czasie trwania i długości wystarczającej do zawarcia sekwencji.

A dla C ++ w sekcji 2.14.5, akapity 8-11:

8 Zwykłe literały łańcuchowe i literały łańcuchowe UTF-8 są również określane jako wąskie literały łańcuchowe. Wąski literał łańcuchowy ma typ „tablica n const char”, gdzie n jest rozmiarem łańcucha, jak zdefiniowano poniżej, i ma statyczny czas trwania (3.7).

9 Literał łańcuchowy rozpoczynający się na literę u, na przykład u"asdf", jest char16_tciągiem znaków. char16_tCiągiem znaków jest typu „układ n const char16_t”, w którym n oznacza wielkość łańcuchu, jak zdefiniowano poniżej; ma statyczny czas trwania i jest inicjowany podanymi znakami. Pojedynczy znak c może dawać więcej niż jeden char16_tznak w postaci par zastępczych.

10 Literał ciągu rozpoczynający się na literę U, na przykład U"asdf", jest char32_tciągiem znaków. char32_tCiągiem znaków jest typu „układ n const char32_t”, w którym n oznacza wielkość łańcuchu, jak zdefiniowano poniżej; ma statyczny czas trwania i jest inicjowany podanymi znakami.

11 Literał ciągu rozpoczynający się od litery L, na przykład L"asdf", jest literałem szerokiego łańcucha. Szeroki literał łańcucha ma typ „tablica n const wchar_t”, gdzie n jest rozmiarem łańcucha, jak zdefiniowano poniżej; ma statyczny czas trwania i jest inicjowany podanymi znakami.


Do Twojej wiadomości: ta odpowiedź została scalona ze stackoverflow.com/questions/16470959/ ...
Shog9

14

Literały łańcuchowe obowiązują dla całego programu (i nie są przydzielane poza stosem), więc będą prawidłowe.

Ponadto, literały łańcuchowe są tylko do odczytu, więc (dla dobrego stylu) może powinieneś zmienić foonaconst char *foo(int)


A co jeśli użytkownik zwraca coś takiego? char * a = & "abc"; return a; Czy to nie będzie ważne?
Ashwin

&"abc"nie jest char*. jest to adres tablicy, a jej typ to char(*)[4]. Jednak oba return &"abc";i char *a="abc";return a;są ważne.
asaelr

@asaelr: Właściwie to coś więcej niż dobry styl , sprawdź moją odpowiedź, aby poznać szczegóły.
Alok Zapisz

@Als Cóż, jeśli napisze cały program, może uniknąć zmiany łańcucha bez pisania consti będzie to całkowicie legalne, ale nadal w złym stylu.
asaelr

jeśli dotyczy całego programu, dlaczego musimy to mallocować?
TomSawyer

7

Tak, to ważny kod, patrz przypadek 1 poniżej. Możesz bezpiecznie zwrócić ciągi C z funkcji przynajmniej w ten sposób:

  • const char*do literału ciągu. Nie może być modyfikowany i nie może być zwolniony przez dzwoniącego. Rzadko jest użyteczne w celu zwrócenia wartości domyślnej z powodu problemu zwalniania opisanego poniżej. Może to mieć sens, jeśli faktycznie potrzebujesz gdzieś przekazać wskaźnik funkcji, więc potrzebujesz funkcji zwracającej ciąg znaków.

  • char*lub const char*do statycznego bufora znaków. Dzwoniący nie może go uwolnić. Może być modyfikowana (albo przez wywołującego, jeśli nie jest stałą, albo przez funkcję, która ją zwraca), ale funkcja zwracająca to nie może (łatwo) mieć wielu buforów, więc nie jest (łatwo) bezpieczna wątkowo, a wywołujący może potrzebować aby skopiować zwróconą wartość przed ponownym wywołaniem funkcji.

  • char*do bufora przydzielonego za pomocą malloc. Można go zmodyfikować, ale zwykle musi być jawnie zwolniony przez obiekt wywołujący i ma narzut alokacji sterty. strdupjest tego typu.

  • const char*lub char*do bufora, który został przekazany jako argument do funkcji (zwrócony wskaźnik nie musi wskazywać na pierwszy element bufora argumentów). Pozostawia odpowiedzialność za zarządzanie buforem / pamięcią dzwoniącemu. Wiele standardowych funkcji łańcuchowych jest tego typu.

Jednym z problemów jest to, że mieszanie ich w jednej funkcji może się skomplikować. Wzywający musi wiedzieć, jak powinien obsłużyć zwrócony wskaźnik, jak długo jest ważny i czy dzwoniący powinien go zwolnić, i nie ma (przyjemnego) sposobu na określenie tego w czasie wykonywania. Nie możesz więc na przykład mieć funkcji, która czasami zwraca wskaźnik do bufora przydzielonego na stertę, którego potrzebuje wywołanie free, a czasami wskaźnik do domyślnej wartości z literału ciągu, którego wywołanie nie może free .


Do Twojej wiadomości: ta odpowiedź została scalona ze stackoverflow.com/questions/16470959/ ...
Shog9

6

Dobre pytanie. Ogólnie rzecz biorąc, miałbyś rację, ale twój przykład jest wyjątkiem. Kompilator statycznie przydziela pamięć globalną dla literału ciągu. Dlatego adres zwrócony przez twoją funkcję jest prawidłowy.

To, że tak jest, jest raczej wygodną cechą C, prawda? Umożliwia funkcji zwrócenie wstępnie utworzonej wiadomości bez zmuszania programisty do martwienia się o pamięć, w której wiadomość jest przechowywana.

Zobacz także poprawną obserwację @ asaelr dotyczącą const.


: Co jeśli użytkownik zwraca coś takiego? char * a = & "abc"; return a; Czy to nie będzie ważne?
Ashwin

Dobrze. Właściwie można po prostu pisaćconst char *a = "abc"; , pomijając &. Powodem jest to, że ciąg w podwójnych cudzysłowach jest tłumaczony na adres swojego początkowego znaku.
THB

3

Zmienne lokalne są ważne tylko w zakresie, w jakim zostały zadeklarowane, jednak nie deklarujesz żadnych zmiennych lokalnych w tej funkcji.

Zwrócenie wskaźnika do literału ciągu znaków z funkcji jest całkowicie poprawne, ponieważ literał łańcuchowy istnieje przez całe wykonanie programu, tak samo jak staticzmienna globalna lub zmienna globalna.

Jeśli martwisz się, że to, co robisz, może być nieprawidłowe i nieokreślone, powinieneś włączyć ostrzeżenia kompilatora, aby sprawdzić, czy faktycznie jest coś, co robisz źle.


A co jeśli użytkownik zwraca coś takiego? char * a = & "abc"; return a; Czy to nie będzie ważne?
Ashwin

@Ashwin: &"abc"nie jest typu char*, jednak oba "abc"i &"abc"są ważne przez cały czas wykonywania programu.
AusCBloke

2

strnigdy nie będzie wiszącym wskaźnikiem, ponieważ wskazuje na adres statyczny którym znajdują się literały łańcuchowe.

Po załadowaniu będzie on głównie tylko do odczytu i będzie globalny dla programu.

Nawet jeśli spróbujesz zwolnić lub zmodyfikować, spowoduje to błąd segmentacji na platformach z ochroną pamięci .


Do Twojej wiadomości: ta odpowiedź została scalona ze stackoverflow.com/questions/16470959/ ...
Shog9

jeśli nigdy nie będzie zwisać, czy muszę go zaimpregnować? Nie?
TomSawyer

0

Na stosie alokowana jest zmienna lokalna. Po zakończeniu funkcji zmienna wychodzi poza zakres i nie jest już dostępna w kodzie. Jeśli jednak masz globalny (lub po prostu - jeszcze nie poza zakresem) wskaźnik, który przypisałeś do wskazywania tej zmiennej, wskaże on miejsce na stosie, w którym ta zmienna się znajdowała. Może to być wartość używana przez inną funkcję lub wartość bez znaczenia.


A co jeśli użytkownik zwraca coś takiego? char * a = & "abc"; return a; Czy to nie będzie ważne?
Ashwin

0

W powyższym przykładzie pokazanym przez ciebie, w rzeczywistości zwracasz przydzielone wskaźniki do dowolnej funkcji, która wywołuje powyższe. Więc nie stałby się lokalnym wskaźnikiem. Co więcej, dla wskaźników, które mają zostać zwrócone, przydzielana jest pamięć w segmencie globalnym.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.