Na podstawie własnych obserwacji zauważyłem, że w internecie wiele razy był poruszany jeden istotny temat w języku C kręcący się wokół tekstów, czyli łańcuch znaków ⛓. Obsługa łańcuchów w tym języku jest bardzo niebezpieczna i dużo bardziej skomplikowana, niż przy dzisiejszych językach o jeszcze wyższym poziomie abstrakcji (takich jak Java czy C#). Dowiedz się jak łatwo możesz wpaść na pułapki, które czyhają na początkujących programistów i jak zarządzać łańcuchem tak, aby było poprawnie i bezpiecznie ✅!

ŁAŃCUCH ZNAKÓW W JĘZYKU C "PODLEGA SZCZEGÓLNEJ OCHRONIE"!

Zacznijmy temat od małej "eksplozji" 💥! Skompiluj sobie poniższy kod, uruchom go i zobacz co się stanie (chyba, że nie chcesz ryzykować, to rozumiem 🙂) 👇:

#include <stdio.h>

int main(void)
{
	char* string = "Tekst";

	string = "Nowy tekst";
	
	return 0;
}

To jest niezdefiniowane zachowanie ❌! Chwila...przypisanie nowego łańcucha powoduje awarię programu 🤔? "Jakim cudem?!" - możesz sobie myśleć.

Teraz czas ujawnić prawdziwą "twarz" łańcuchów znaków w języku C 👹!

TAK NAPRAWDĘ, TO NIE MA ŻADNEGO ŁAŃCUCHA W JĘZYKU C

Czy zauważyłeś(-aś) typ danych zmiennej, do której przypisaliśmy łańcuch 😏?

char* string = "Tekst";

Wskaźnik do typu znakowego 😲??? Tak 🫵!

W języku C nie ma typu natywnego "string", który znajdziesz w językach "nieco nowszego rocznika" 😁. W tamtym okresie, "łańcuchem" była tablica składająca się z pojedynczych znaków, czyli ekwiwalent tego, co jest u góry (choć między tymi zapisami jest jedna kolosalna różnica, o której za moment!).

Co jeszcze musisz wiedzieć o łańcuchach w języku C?

ZNAK TERMINATORA

Poza znakami wprowadzonymi jawnie, język dodaje na końcu "ogonek" w postaci tzw. "znaku terminatora" (ang. null terminator):

'\0'

Lewy ukośnik + cyfra zero. Jako jeden ze znaków specjalnych służy do wyznaczania końca łańcucha. Program, po wstawieniu takiej danej "na taśmę", musi mieć informację do której komórki pamięci, traktować wartości jak fragment łańcucha, a kiedy przestać ℹ️. Zatem, nasz przykładowy łańcuch:

"Tekst"

to w rzeczywistości tablica o długości 6 elementów składająca się z pojedynczych znaków wraz ze znakiem zakończenia łańcucha:

char string[6] = {'T', 'e', 'k', 's', 't', '\0'};

To jest pierwszy istotny punkt 1️⃣. Oczywiście bez problemu możesz wpisać łańcuch "normalnie" 😄:

char string[6] = "Tekst";

Zwróć uwagę na liczbę znaków: 6, a nie 5. Pamiętaj, że alokując pamięć dla łańcucha, musisz zarezerwować dodatkową komórkę na znak terminatora - inaczej, Twój łańcuch znaków w języku C, może zostać "ucięty" albo dojdzie do niezdefiniowanego zachowania (są funkcje obsługujące łańcuch, które chronią przed tym, lecz nie wszystkie) ✂️!

WSKAŹNIK WSKAŹNIKOWI NIERÓWNY

Teraz druga pułapka 💣! Mówiliśmy sobie, że tablica jest jednocześnie wskaźnikiem do jej pierwszego elementu. Czyli na pierwszy rzut oka może się wydawać, że oba te zapisy są równoważne 👇:

char* stringAsPointer = "Tekst";
char stringAsArray[6] = "Tekst";

Nie są 🤯!!! Oto dlaczego:

  1. łańcuch znaków zdefiniowany jako wskaźnik, jest łańcuchem tylko do odczytu (ang. read-only data),
  2. łańcuch znaków zdefiniowany jako tablica, może podlegać modyfikacji (przy czym możemy jedynie pojedynczo, znak po znaku).

To dlatego program ulega awarii przy takiej próbie modyfikacji:

char* string = "Tekst";

string = "Nowy tekst";

W tym przypadku dopuszczamy się naruszenia ochrony pamięci (ang. segmentation fault), co w języku C powoduje natychmiastowe przerwanie działania programu ❌! Stała takiej postaci nazywana jest "literałem znakowym", ponieważ jest to segment danych, z którego możemy jedynie odczytać wartość ℹ️:

printf("%s\n", string);

To przejdzie bez zarzutu ✔️. Zwróć uwagę na specyfikator: "%s" (od "string") ‼️! "printf" co ciekawe, posiada zaprogramowaną obsługę "traktowania" podanej zmiennej, jak łańcucha 😄!

To jest istotny punkt nr 2 2️⃣!

SPOSOBY TWORZENIA MODYFIKOWALNYCH ŁAŃCUCHÓW ZNAKÓW

Wiemy już, że łańcuch znaków w języku C jako typ danych nie istnieje, wiemy też czego się wystrzegać ⚠️. Jednak jak się domyślasz, w tym wszystkim musi być ostatecznie jakaś metoda, która pozwala na swobodne zarządzanie łańcuchem i to w obie strony (odczyt-modyfikacja) 💡. Teraz zaprezentuję co mamy do dyspozycji 🔥.

Modyfikacja łańcucha znaków w języku C, może być wykonana na 2 sposoby:

  1. przypisywanie nowego znaku do danej komórki tablicy,
  2. wywołanie funkcji operującej na łańcuchu.

A teraz metody zarządzania. Ujmując krótko, mamy 2 drogi: łatwiejszą i trudniejszą 🔑.

ZDEFINIOWANIE TABLICY ZNAKÓW

Pierwszy sposób jest trywialny - po prostu tworzysz tablicę znaków 👇:

#include <stdio.h>
#include <string.h>

#define STRING_BUFFER 50

void printString(char*);

int main(void)
{
	char string[STRING_BUFFER + 1] = "Tekst";

	printString(string);
	
	string[1] = 'k';

	printString(string);
	strcpy(string, "Nowy tekst");
	printString(string);
	
	return 0;
}

void printString(char* string)
{
	printf("%s\n", string);
}

a później możesz swobodnie modyfikować łańcuch ✅, natomiast zwracając uwagę czy nie przekraczasz rozmiaru ustalonego "bufora" na ten łańcuch ⚠️! Przypominam o znaku terminatora pod koniec, zatem mamy o jeden znak mniej ‼️! Skorzystałem z dyrektywy "define" w celu ustalenia maksymalnego rozmiaru tablicy dla przechowywanego tekstu 👍.

W powyższym przykładzie, najpierw zmieniamy pojedynczy znak:

string[1] = 'k';

a potem modyfikujemy cały łańcuch "zamieniając" na nowy, używając funkcji "strcpy":

strcpy(string, "Nowy tekst");

Obie modyfikacje przejdą pomyślnie, a program poprawnie zakończy swoje działanie, aczkolwiek może się wywalić w sytuacji przekroczenia bufora 🌟. Funkcja "strcpy" podczas przypisywania nowego łańcucha, może doprowadzić do niezdefiniowanego zachowania z powodu niewystarczającej liczby wolnych "miejsc" w tablicy.

Dlatego trzeba na to bardzo uważać, a w razie konieczności "dorzucić" większy bufor na zapas, jeśli nie możemy przewidzieć ile dokładnie pamięci będzie potrzebnej ℹ️.

RĘCZNE ZAALOKOWANIE PAMIĘCI

To trochę trudniejsza operacja bo wymagająca już pewnego doświadczenia w obsłudze wskaźników pozwalającej na dynamiczne zarządzanie pamięcią ⚠️.

Drugą opcją jest skorzystanie z funkcji "malloc" do przydzielenia odpowiedniej liczby komórek pamięci dla łańcucha, a następnie zwolnienie tejże pamięci przy użyciu funkcji "free" 👇:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define STRING_BUFFER 50

void printString(char*);

int main(void)
{
	char* string = malloc((STRING_BUFFER + 1)*sizeof(char));

	strcpy(string, "Tekst");
	printString(string);

	string[5] = '!';

	printString(string);
	free(string);
	
	return 0;
}

void printString(char* string)
{
	printf("%s\n", string);
}

Tutaj również modyfikujemy na 2 poznane sposoby. Najpierw przez "strcpy" przypisujemy łańcuch:

strcpy(string, "Tekst");

a potem go leciutko zmieniamy w jednym znaku:

string[5] = '!';

także widzisz, że operacje są takie same, niezależnie od wybranej drogi postępowania 👍. Natomiast w tym przypadku istotna jest kolejność wykonywania 😲!

"strcpy" musi być pierwsze, aby funkcja dodała znak końca łańcucha po wstawieniu wszystkich znaków do naszej pamięci ℹ️! Gdyby pierwsza była zmiana pojedynczego znaku, to nie zobaczylibyśmy łańcucha, tylko jakiś "krzak" ❌.

O wszystkim zostałeś(-aś) poinformowany(-a) 😉. Teraz możesz działać 👊!

Łańcuch znaków w języku C

Łańcuch znaków w języku C jako typ danych, nie istnieje! Występuje on jedynie jako tablica pojedynczych znaków ze znakiem terminatora na końcu!


To wszystko ☑️. Jak zauważyłeś(-aś), łańcuch znaków może cieszyć się wieloma udogodnieniami jakie występują w innych językach wysokiego poziomu. Tutaj jest po prostu masakra i nic nie przypomina operowania na łańcuchach, które możesz znać z innych języków 😁. Musisz zwracać baczną uwagę na wszelkie szczegóły, bo język C w ogóle Cię nie ochroni, gdy będzie gdzieś uchybienie. Możesz jedynie liczyć na łaskawość kompilatora i wypisze ostrzeżenie, natomiast nie zawsze może się pojawić ⚠️.

PODOBNE ARTYKUŁY