Jason. Cała informatyka w jednym miejscu!

Znowu skoncentruję się na języku C, jeśli chodzi o premierowy artykuł na mojej stronce. Na podstawie wielu spostrzeżeń w Internecie zauważyłem, że wiele razy był poruszany jeden istotny temat kręcący się wokół tekstów, czyli łańcuch znaków w języku C. Tutaj sprawa robi się dużo bardziej mozolna niż przy dzisiejszych abstrakcyjnych rozwiązaniach operujących na łańcuchach. Dowiedzcie się jaki najczęściej popełniany błąd jest przez początkujących w języku C, a na który ja sam również wpadłem kilka razy. Chodzi o literał znakowy.

ŁAŃCUCH ZNAKÓW W JĘZYKU C DZIAŁA ZUPEŁNIE INACZEJ

W dobie obecnie dominujących języków programowania nie robi się żadnych zagadnień z samego tekstu. Wprowadzamy jeden tekst przy inicjalizacji zmiennej, możemy go później zmienić jak chcemy, możemy dodać inny łańcuch do już istniejącego, wyszukać w nim ciąg znaków i tak dalej. Ale w języku C takie możliwości są "marzeniem ściętej głowy". Do konkatenowania mamy funkcje "strcpy" oraz "strncpy" (być może jeszcze jakieś inne), a próba podstawienia innego łańcucha do tej samej zmiennej kończy się naruszeniem ochrony pamięci!!!

char *text = "Oto tekst";

text = "Oto nowy tekst";	// naruszenie ochrony pamięci!

Zaraz...jakim cudem zwykłe przypisanie nowego łańcucha do tej samej zmiennej może powodować wystąpienie jednego z błędów? W tym języku niestety to możliwe. Łańcuch znaków w języku C jest przechowywany jako wskaźnik o których pisałem wcześniej, zatem jest również jednocześnie tablicą składającą się z pojedynczych znaków posiadającą na końcu znak specjalny w postaci '\0' (tzw. "null terminator"). To jednak wciąż nie tłumaczy dziwnej reakcji programu na powyższy fragment kodu.

ŁAŃCUCH ZNAKÓW TYLKO DO ODCZYTU

Przypisując łańcuch znaków w powyższy sposób, sprawiamy że zawartość zmiennej będzie dostępna tylko do odczytu! Podstawiając do wskaźnika taki literał znakowy, zostanie on potraktowany jako segment danych, z którego można jedynie odczytać łańcuch ("read-only data" nazywany skrótowo "rodata"). To jest główna przyczyna występowania błędu naruszenia ochrony pamięci. Nie ma żadnej ochrony przed takimi próbami zapisu danych tylko do odczytu jak modyfikator "final" w języku Java.

Język C jest często wybieranym językiem ze względu na efektywne działanie, ale to właśnie dlatego że nie zaprząta sobie głowy sprawdzaniem przed kompilacją czy doprowadzamy do jakiejś katastrofy w czasie działania programu (umyślnie lub nie). On zawsze zakłada, że wiemy co robimy. Taki przypadek nazywany jest "niezdefiniowanym zachowaniem" (ang. "undefined behavior"), to znaczy że standard języka nie określa jednoznacznie co się w danej linijce kodu wydarzy i możemy spodziewać się teoretycznie wszystkiego.

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

Znamy już przyczynę występowania jednego z najczęściej popełnianych błędów przez początkujących koderów w języku C. Teraz zaprezentuję jak sprawić, żeby taki łańcuch można było później bezkarnie modyfikować bez żadnych kwiatków. Są dwie drogi.

ZDEFINIOWAĆ TABLICĘ ZNAKÓW ZAMIAST WSKAŹNIKA

Jest to banalne rozwiązanie, gdyż wymaga jedynie przestawienia się na zapis tablicowy. Łańcuch znaków w języku C można jeszcze utworzyć w taki oto sposób:

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

#define SIZE 30

int main(void)
{
	char text[SIZE] = "Oto tekst.";

	printf("%s\n", text);
	
	text[1] = 'k';

	printf("%s\n", text);
	strcpy(text, "Oto nowy tekst.");
	printf("%s\n", text);
	getchar();
	
	return 0;
}

Skorzystałem z dyrektywy "define" w celu ustalenia maksymalnego rozmiaru tablicy dla przechowywanego tekstu. Przypominając o znaku końca łańcucha ('\0'), mamy w rzeczywistości w tym przykładzie 29 znaków. Jest to głównie potrzebne przy funkcjach operujących na łańcuchach, takich jak "strcpy". Podałem celowo dłuższy tekst w celu przedstawienia tego problemu. C sobie poradzi przy identyfikacji rozmiaru bez jego jawnego podania, aczkolwiek przy funkcji "strcpy" doszłoby wówczas do niezdefiniowanego zachowania z powodu niewystarczającej liczby wolnych "miejsc" w tablicy. Program może paść, może sobie z tym poradzić, może dojść do jakichś nieoczekiwanych reakcji systemu. Nikt nie wie co się stanie przy niezdefiniowanych zachowaniach. Lepiej unikać takich nieprzyjemności.

ZAALOKOWAĆ RĘCZNIE PAMIĘĆ DLA ŁAŃCUCHA

To już bardziej złożony manewr, bo wymagający pewnego doświadczenia w obsłudze wskaźników oraz funkcji pozwalających na dynamiczne zarządzanie pamięcią. To nie jest artykuł poświęcony ręcznemu zarządzaniu pamięcią, więc odstąpię od zbyt przerażających szczegółów. W tym przypadku wystarczy Wam informacja, że jak alokujecie pamięć wywołując "malloc", to jesteście również odpowiedzialni za jej zwolnienie przy pomocy funkcji "free":

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

#define SIZE 40

int main(void)
{
	char *text = malloc((SIZE + 1)*sizeof(char));

	strcpy(text, "Oto tekst dynamicznie alokowany.");
	printf("%s\n", text);

	text[13] = '!';

	printf("%s\n", text);
	free(text);
	getchar();
	
	return 0;
}

Łańcuch znaków w języku C wymaga starannej dbałości o szczegóły, jeśli ma choć częściowo dorównywać nowszym językom programowania w kwestii wygodnej obsługi. Zauważcie również, że dorzuciłem zmianę pojedynczego znaku PO wstawieniu łańcucha przez wywołanie "strcpy". Z jednego konkretnego powodu: funkcja od razu dodaje znak końca łańcucha ('\0') po wstawieniu wszystkich znaków do naszej pamięci. Gdybyście umieścili taką instrukcję przed tą funkcją, ujrzelibyście "krzak" albo znak różniący się od wpisanego. Ta sama instrukcja przed zaalokowaniem pamięci? Błąd naruszenia ochrony pamięci.


To wszystko. Zwracajcie baczną uwagę na wszelkie takie pułapki, bo język C W OGÓLE Was przed tym nie ochroni. Możecie jedynie liczyć na łaskawość kompilatora jeśli wypisze Wam ostrzeżenie, ale nie zawsze takiego doświadczycie.

PODOBNE ARTYKUŁY