Czy można zmodyfikować ciąg znaków w C?

81

Przez kilka godzin walczyłem z różnymi samouczkami w języku C i książkami związanymi ze wskaźnikami, ale naprawdę chcę wiedzieć, czy można zmienić wskaźnik znaku po jego utworzeniu.

Oto, czego próbowałem:

Więc czy istnieje sposób na zmianę wartości wewnątrz łańcuchów zamiast adresów wskaźników?

Matthew Stopa
źródło

Odpowiedzi:

158

Kiedy piszesz „ciąg” w swoim kodzie źródłowym, jest on zapisywany bezpośrednio w pliku wykonywalnym, ponieważ ta wartość musi być znana w czasie kompilacji (dostępne są narzędzia do rozdzielania oprogramowania i znajdowania w nim wszystkich zwykłych ciągów tekstowych). Kiedy piszesz char *a = "This is a string", lokalizacja „To jest ciąg znaków” znajduje się w pliku wykonywalnym, a lokalizacja awskazuje na plik wykonywalny. Dane w obrazie wykonywalnym są tylko do odczytu.

To, co musisz zrobić (jak wskazywały inne odpowiedzi), to utworzyć tę pamięć w lokalizacji, która nie jest tylko do odczytu - na stercie lub w ramce stosu. Jeśli deklarujesz tablicę lokalną, na stosie jest tworzona przestrzeń dla każdego elementu tej tablicy, a literał ciągu (który jest przechowywany w pliku wykonywalnym) jest kopiowany do tego miejsca na stosie.

można również skopiować te dane ręcznie, przydzielając część pamięci na stercie, a następnie używając polecenia strcpy()do skopiowania literału ciągu do tej przestrzeni.

Za każdym razem, gdy przydzielasz miejsce za pomocą malloc()pamiętaj, aby zadzwonić, free()gdy skończysz z nim (czytaj: wyciek pamięci).

Zasadniczo musisz śledzić, gdzie są twoje dane. Za każdym razem, gdy napiszesz ciąg w swoim źródle, ten ciąg jest tylko do odczytu (w przeciwnym razie potencjalnie zmieniłbyś zachowanie pliku wykonywalnego - wyobraź sobie, że napisałeś, char *a = "hello";a następnie zmieniłeś a[0]na 'c'. Potem napisał gdzieś indziej printf("hello");. Gdybyś mógł zmienić pierwszy znak "hello", a Twój kompilator zapisał go tylko raz (powinien), a następnie printf("hello");wyświetli cello!)

Carson Myers
źródło
12
Ostatnia sekcja wyjaśniła mi wiele, dlaczego należy to przeczytać tylko do czytania. Dziękuję Ci.
CDR
1
-1: nie mówi, aby używać const char * i nic nie gwarantuje, że ciągi literałów są przechowywane w pamięci wykonywalnej.
Bastien Léonard
Czy nie potrzebujesz const dla dwóch podanych przeze mnie rozwiązań - również, jeśli ciąg znaków jest znany w czasie kompilacji i wkompilowany w plik wykonywalny - gdzie indziej zostałby zapisany? W gcc, jeśli napiszę char * a = "hallo."; lub char b [] = "hello." ;, wtedy zestaw wyprowadza "LC0: .ascii" Hallo. \ 0 "LC1: .ascii" Hello. \ 0 "" oba są w pamięci wykonywalnej ... Kiedy tak nie jest ?
Carson Myers
1
Właśnie wypróbowany z GCC 4.4, umieszcza ciągi literałów w .rodata (dane tylko do odczytu). Sprawdziłem z objdump i listą zespołu. Nie sądzę, aby standard wymagał, aby dosłowne ciągi znaków były tylko do odczytu, więc myślę, że można je nawet umieścić w .data.
Bastien Léonard
Ponadto nie widzę żadnej korzyści w niekwalifikowaniu wskaźnika jako const. Może ukryć błędy, jeśli później zdecydujesz się zmienić ciąg.
Bastien Léonard
29

Nie, nie możesz go modyfikować, ponieważ ciąg może być przechowywany w pamięci tylko do odczytu. Jeśli chcesz go zmodyfikować, możesz zamiast tego użyć tablicy np

Lub alternatywnie możesz przydzielić pamięć za pomocą malloc, np

Jonathan Maddison
źródło
5
Aby uzupełnić kod, dobrze będzie, jeśli możesz również dodać wywołanie free ().
Naveen
15

Wielu ludzi jest zdezorientowanych różnicą między char * i char [] w połączeniu z literałami łańcuchowymi w C. Kiedy piszesz:

... faktycznie wskazujesz foo na stały blok pamięci (w rzeczywistości to, co kompilator robi z „hello world” w tym przypadku, zależy od implementacji).

Zamiast tego użycie znaku [] mówi kompilatorowi, że chcesz utworzyć tablicę i wypełnić ją zawartością, „witaj świecie”. foo jest wskaźnikiem do pierwszego indeksu tablicy char. Oba są wskaźnikami do znaków, ale tylko znak [] będzie wskazywał lokalnie przydzielony i modyfikowalny blok pamięci.

Jeff Ober
źródło
7

Pamięć dla a & b nie jest przydzielana przez Ciebie. Kompilator może wybrać lokalizację pamięci tylko do odczytu do przechowywania znaków. Więc jeśli spróbujesz to zmienić, może to spowodować błąd seg. Proponuję więc samodzielnie stworzyć tablicę znaków. Coś jak:char a[10]; strcpy(a, "Hello");

Naveen
źródło
1
Problem z tablicami znaków polega na tym, że przekazuję wskaźnik tablicy znaków do funkcji, aby móc manipulować tam łańcuchem, a następnie wysłać go ponownie. Wygląda na to, że niestety muszę użyć malloc.
Matthew Stopa
1
Nie, nadal możesz używać obiektu przydzielonego na stosie. Na przykład, jeśli masz funkcję void f (char * p); następnie z main () możesz przejść f (a). Spowoduje to przekazanie adresu pierwszego znaku do funkcji. Ponadto, jeśli zdecydujesz się przejść przez malloc (), nie zapomnij zwolnić pamięci za pomocą funkcji free ().
Naveen
5

Wygląda na to, że udzielono odpowiedzi na twoje pytanie, ale teraz możesz się zastanawiać, dlaczego char * a = "String" jest przechowywany w pamięci tylko do odczytu. Cóż, w rzeczywistości jest niezdefiniowany przez standard c99, ale większość kompilatorów wybiera to w ten sposób w przypadku takich instancji, jak:

standard c99 (pdf) [strona 130, rozdział 6.7.8]:

Deklaracja:

definiuje "zwykłe" obiekty tablicy znaków s i t, których elementy są inicjowane za pomocą literałów ciągu znaków. Ta deklaracja jest identyczna z char

Zawartość tablic można modyfikować. Z drugiej strony deklaracja

definiuje p z typem „wskaźnik do znaku” i inicjuje go tak, aby wskazywał na obiekt typu „tablica znaków” o długości 4, którego elementy są inicjowane literałem ciągu znaków. Jeśli zostanie podjęta próba użycia p do zmodyfikowania zawartości tablicy, zachowanie jest niezdefiniowane.

Sweeney
źródło
4

Możesz również użyć strdup:

Na przykład:

Maxime Chéramy
źródło
Nie jest to odpowiedź na pytanie, ale nadal bardzo przydatna funkcja, dzięki!
mknaf
1
+1 za naukę strdup. Nie jestem jednak pewien, kiedy chciałbym go użyć.
Bozon Z
Kiedy robisz coś takiego jak var = malloc(strlen(str) + 1); strcpy(var, str);, prawdopodobnie powinieneś strdupzamiast tego użyć .
Maxime Chéramy
3

Wszystkie są dobrymi odpowiedziami wyjaśniającymi, dlaczego nie można modyfikować literałów łańcuchowych, ponieważ są one umieszczone w pamięci tylko do odczytu. Jednakże, gdy dochodzi do sytuacji, jest na to sposób. Sprawdź ten przykład:

Napisałem to jako część moich nieco głębszych przemyśleń na temat stałej poprawności , które mogą Cię zainteresować (mam nadzieję :)).

Mam nadzieję, że to pomoże. Powodzenia!


źródło
Zauważ, że zmiana literału ciągu jest niezdefiniowanym zachowaniem.
Steohan
0

Musisz skopiować ciąg do innego, nie tylko do odczytu bufora pamięci i tam go zmodyfikować. Użyj strncpy () do skopiowania ciągu, strlen () do wykrycia długości łańcucha, malloc () i free () do dynamicznego przydzielenia buforu dla nowego ciągu.

Na przykład (C ++ jak pseudokod):

ostry
źródło
0
Nathan Fellman
źródło
6
Malloc potrzebuje 1 bajtu więcej. Nie zapomnij znaku zakończenia NULL, którego strcpy oczekuje i również skopiuje. To zbyt częsty błąd.
xcramps