Last modified: December 03, 2024

This article is written in: 🇵🇱

Napisy w języku C i C++

Napisy są fundamentalnym elementem wielu aplikacji programistycznych, służąc do przechowywania i manipulacji tekstem, takim jak dane wejściowe użytkownika, komunikaty systemowe, informacje o błędach i wiele innych. W językach C i C++, napisy są reprezentowane na różne sposoby, co wynika z ewolucji tych języków oraz dążenia do zwiększenia bezpieczeństwa i łatwości użycia.

Napisy w języku C (C-string)

W języku C napisy są reprezentowane jako tablice znaków typu char, zakończone specjalnym znakiem '\0', znanym jako znak null (null terminator). Ten znak wskazuje koniec napisu i pozwala funkcjom bibliotecznym na określenie długości napisu w czasie wykonania.

Deklaracja i inicjalizacja napisów w C

Istnieje kilka sposobów deklarowania i inicjalizacji napisów w C:

I. Wskaźnik do stałego łańcucha znaków:

const char *napisA = "Ala ma kota";

W tym przypadku napisA jest wskaźnikiem do stałego łańcucha znaków przechowywanego w pamięci tylko do odczytu (zazwyczaj w segmencie tekstowym programu). Próba modyfikacji tego napisu prowadzi do niezdefiniowanego zachowania.

II. Tablica znaków z inicjalizacją literałem:

char napisB[] = "Ala ma kota";

Tutaj napisB jest tablicą znaków, która jest kopią literału napisu. Ta tablica może być modyfikowana, ponieważ jest przechowywana w pamięci dostępnej do zapisu (zazwyczaj na stosie lub w pamięci dynamicznej).

III. Tablica znaków z inicjalizacją listą znaków:

char napisC[] = {'A', 'l', 'a', ' ', 'm', 'a', ' ', 'k', 'o', 't', 'a', '\0'};

Ten sposób jest równoważny poprzedniemu, ale wymaga jawnego dodania znaku null na końcu tablicy.

Znaczenie znaku null

Znak null '\0' jest kluczowy w reprezentacji napisów w C. Funkcje biblioteczne operujące na napisach zakładają, że napisy są zakończone tym znakiem. Brak znaku null może prowadzić do błędów, takich jak odczyt poza granicami tablicy (buffer overrun), co może skutkować naruszeniem ochrony pamięci i awarią programu.

Operacje na napisach w C

Język C dostarcza bogaty zestaw funkcji w standardowych bibliotekach do manipulacji napisami. Kluczowe biblioteki to <string.h>, <ctype.h> i <stdlib.h>.

Biblioteka <string.h>

Funkcje w tej bibliotece służą do manipulacji i porównywania napisów:

Funkcja Opis
size_t strlen(const char *s); Zwraca długość napisu s, czyli liczbę znaków przed znakiem null.
char *strcpy(char *dest, const char *src); Kopiuje napis src do bufora dest. Uwaga: dest musi mieć wystarczający rozmiar, aby pomieścić src.
char *strncpy(char *dest, const char *src, size_t n); Kopiuje maksymalnie n znaków z src do dest.
char *strcat(char *dest, const char *src); Dołącza napis src do końca dest. dest musi mieć wystarczający rozmiar.
char *strncat(char *dest, const char *src, size_t n); Dołącza maksymalnie n znaków z src do dest.
int strcmp(const char *s1, const char *s2); Porównuje napisy s1 i s2. Zwraca wartość ujemną, zero lub dodatnią w zależności od wyniku porównania.
int strncmp(const char *s1, const char *s2, size_t n); Porównuje maksymalnie n znaków z s1 i s2.
char *strchr(const char *s, int c); Wyszukuje pierwsze wystąpienie znaku c w napisie s.
char *strrchr(const char *s, int c); Wyszukuje ostatnie wystąpienie znaku c w napisie s.
char *strstr(const char *haystack, const char *needle); Wyszukuje podciąg needle w napisie haystack.

Uwaga dotycząca bezpieczeństwa: Funkcje takie jak strcpy i strcat są podatne na błędy przepełnienia bufora (buffer overflow) i nie powinny być używane w nowym kodzie. Bezpieczniejsze alternatywy to strncpy i strncat, jednak one również mają swoje ograniczenia. W praktyce zaleca się korzystanie z funkcji takich jak strlcpy i strlcat (jeśli są dostępne) lub funkcji specyficznych dla danego systemu operacyjnego.

Biblioteka <ctype.h>

Ta biblioteka zawiera funkcje do klasyfikacji i manipulacji znakami:

Funkcja Opis
int isalpha(int c); Sprawdza, czy znak c jest literą alfabetu.
int isdigit(int c); Sprawdza, czy znak c jest cyfrą.
int isalnum(int c); Sprawdza, czy znak c jest alfanumeryczny.
int isspace(int c); Sprawdza, czy znak c jest znakiem białym (spacja, tabulacja, nowa linia itp.).
int toupper(int c); Konwertuje znak c do wielkiej litery, jeśli to możliwe.
int tolower(int c); Konwertuje znak c do małej litery, jeśli to możliwe.

Biblioteka <stdlib.h>

Zawiera funkcje do konwersji napisów na wartości liczbowe i odwrotnie:

Funkcja Opis
int atoi(const char *nptr); Konwertuje napis nptr na wartość int.
long int strtol(const char *nptr, char **endptr, int base); Konwertuje napis nptr na wartość long int, z możliwością określenia podstawy systemu liczbowego.
double atof(const char *nptr); Konwertuje napis nptr na wartość double.
double strtod(const char *nptr, char **endptr); Konwertuje napis nptr na wartość double, zwracając wskaźnik do pierwszego znaku po liczbie w *endptr.
char *strtok(char *str, const char *delim); Dzieli napis str na tokeny, używając separatorów zdefiniowanych w delim.

Uwaga dotycząca bezpieczeństwa: Funkcja atoi nie obsługuje błędów i nie jest bezpieczna. Zaleca się użycie strtol lub strtod, które pozwalają na wykrycie błędów konwersji.

Przykłady użycia napisów w C

Inicjalizacja i wypisanie napisu:

#include <stdio.h>

int main() {
    char napis[] = "Witaj, świecie!";
    printf("%s\n", napis);
    return 0;
}

Łączenie dwóch napisów:

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

int main() {
    char napis1[50] = "Witaj, ";
    char napis2[] = "świecie!";

    strcat(napis1, napis2);

    printf("%s\n", napis1); // "Witaj, świecie!"
    return 0;
}

Obliczanie długości napisu:

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

int main() {
    char napis[] = "Programowanie";
    size_t dlugosc = strlen(napis);

    printf("Długość napisu: %zu\n", dlugosc);
    return 0;
}

Porównywanie dwóch napisów:

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

int main() {
    char napis1[] = "ABC";
    char napis2[] = "ABC";

    if (strcmp(napis1, napis2) == 0) {
        printf("Napisy są identyczne.\n");
    } else {
        printf("Napisy są różne.\n");
    }
    return 0;
}

Zarządzanie pamięcią i bezpieczeństwo

Podczas pracy z napisami w C należy zwrócić szczególną uwagę na alokację pamięci i zarządzanie buforami. Błędy takie jak przepełnienie bufora mogą prowadzić do poważnych luk bezpieczeństwa, w tym możliwości wykonania złośliwego kodu.

Aby uniknąć takich problemów:

Napisy w języku C++ (std::string)

Chociaż język C++ jest zgodny z C i pozwala na użycie tradycyjnych C-stringów, oferuje również bardziej zaawansowaną i bezpieczniejszą klasę std::string do reprezentacji napisów. Klasa ta jest częścią standardowej biblioteki C++ i znajduje się w nagłówku <string>.

Zalety użycia std::string

Tworzenie i inicjalizacja std::string

#include <string>

std::string napis1; // Pusty napis
std::string napis2("Ala ma kota"); // Inicjalizacja napisem
std::string napis3(napis2); // Kopia istniejącego napisu

Podstawowe operacje na std::string

I. Dodawanie napisów:

std::string napis1 = "Ala";
std::string napis2 = " ma kota";
std::string wynik = napis1 + napis2; // "Ala ma kota"

II. Dostęp do znaków:

char znak = napis1[0]; // 'A'
napis1[0] = 'E'; // napis1 teraz to "Ela"

Uwaga: Dostęp poza granicami napisu (napis1.at(index)) generuje wyjątek std::out_of_range.

III. Pobieranie długości napisu:

size_t dlugosc = napis1.length();

IV. Porównywanie napisów:

if (napis1 == napis2) {
// Napisy są identyczne
}

V. Wyszukiwanie w napisie:

size_t pozycja = napis1.find("ma");
if (pozycja != std::string::npos) {
// Znaleziono podnapis
}

VI. Zamiana podnapisu:

napis1.replace(0, 3, "Ola"); // Zamienia pierwsze 3 znaki na "Ola"

VII. Wydzielanie podnapisu:

std::string podnapis = napis1.substr(4, 2); // Wydziela 2 znaki od pozycji 4

Interoperacyjność z C-stringami

Chociaż std::string jest wygodny w użyciu, czasami konieczna jest interakcja z kodem, który wymaga tradycyjnych C-stringów (np. funkcje biblioteki C). Klasa std::string udostępnia metodę c_str(), która zwraca wskaźnik do tablicy znaków zakończonej znakiem null:

const char *c_napis = napis1.c_str();

Uwaga: Wskaźnik zwrócony przez c_str() jest ważny tylko do momentu zmodyfikowania napisu. Jeśli planujesz modyfikować napis po pobraniu wskaźnika, musisz skopiować ciąg znaków do osobnego bufora.

Bezpieczeństwo i wydajność

Przykłady użycia std::string

Łączenie i manipulacja napisami:

#include <iostream>
#include <string>

int main() {
    std::string napis = "Ala ma kota";
    std::string napis2 = " i psa";

    // Połączenie napisów
    napis += napis2;

    std::cout << napis << std::endl; // "Ala ma kota i psa"

    // Zamiana fragmentu napisu
    napis.replace(4, 2, "nie ma");

    std::cout << napis << std::endl; // "Ala nie ma kota i psa"

    // Wyszukiwanie
    size_t pozycja = napis.find("kota");
    if (pozycja != std::string::npos) {
        std::cout << "Znaleziono 'kota' na pozycji: " << pozycja << std::endl;
    }

    // Wydzielanie podnapisu
    std::string zwierze = napis.substr(pozycja, 4); // "kota"
    std::cout << "Zwierzę: " << zwierze << std::endl;

    return 0;
}

Konwersja liczb na napisy i odwrotnie:

W C++11 i nowszych dostępne są funkcje takie jak std::to_string, które ułatwiają konwersję liczb na napisy:

#include <iostream>
#include <string>

int main() {
    int liczba = 42;
    std::string napis = "Liczba: " + std::to_string(liczba);

    std::cout << napis << std::endl; // "Liczba: 42"

    // Konwersja napisu na liczbę
    std::string liczba_napis = "123";
    int liczba2 = std::stoi(liczba_napis);

    std::cout << "Liczba2: " << liczba2 << std::endl; // 123

    return 0;
}

Zaawansowane operacje na napisach

Operacje na napisach w C++ to kluczowy element przetwarzania tekstu, szczególnie w aplikacjach związanych z analizą danych, przetwarzaniem języka naturalnego oraz systemami wielojęzycznymi. Poniżej opisano kilka zaawansowanych technik operacji na napisach, które znacznie rozszerzają możliwości programisty.

I. Wyrażenia regularne:

W C++11 wprowadzono bibliotekę <regex>, która umożliwia manipulację napisami za pomocą wyrażeń regularnych. Jest to niezwykle potężne narzędzie, które pozwala na dopasowywanie wzorców, wyszukiwanie i manipulację fragmentami tekstu. Wyrażenia regularne mogą być stosowane do walidacji danych, ekstrakcji informacji oraz skomplikowanej manipulacji tekstu.

Przykład poniżej demonstruje podstawową operację wyszukiwania dopasowań w tekście za pomocą wyrażenia regularnego. Program dopasowuje wzorzec, który identyfikuje kto posiada jakie zwierzę, a następnie wyświetla wyniki.

#include <iostream>
#include <string>
#include <regex>

int main() {
   std::string tekst = "Ala ma kota i psa";
   std::regex wzorzec("(\\w+) ma (\\w+)");
   std::smatch dopasowanie;
   
   if (std::regex_search(tekst, dopasowanie, wzorzec)) {
      std::cout << "Dopasowanie: " << dopasowanie[0] << std::endl;
      std::cout << "Osoba: " << dopasowanie[1] << std::endl;
      std::cout << "Zwierzę: " << dopasowanie[2] << std::endl;
   }
   
   return 0;
}

Wyrażenia regularne umożliwiają również bardziej zaawansowane operacje, takie jak:

II. Unicode i międzynarodowe napisy:

W świecie globalizacji obsługa napisów w różnych kodowaniach jest kluczowa. Standard C++11 wprowadził wsparcie dla literałów Unicode, co umożliwia pracę z tekstem w takich kodowaniach jak UTF-8, UTF-16 i UTF-32. Jest to istotne przy tworzeniu aplikacji wielojęzycznych, gdzie wymagane jest poprawne wyświetlanie znaków z różnych alfabetów, takich jak cyrylica, chińskie znaki czy znaki diakrytyczne.

Przykład wykorzystania literałów Unicode w C++:

#include <iostream>
#include <string>

int main() {
   std::u16string tekst = u"Привет мир!";  // UTF-16
   std::u32string innyTekst = U"你好,世界!";  // UTF-32
   
   std::cout << "Długość tekstu w UTF-16: " << tekst.length() << std::endl;
   std::cout << "Długość tekstu w UTF-32: " << innyTekst.length() << std::endl;
   
   return 0;
}

Chociaż wsparcie dla Unicode w C++ jest wbudowane, manipulowanie takimi napisami może być wyzwaniem. Długość napisów w UTF-16 czy UTF-32 nie zawsze odpowiada liczbie znaków, ponieważ niektóre znaki mogą być kodowane jako wieloznakowe sekwencje. Dlatego w wielu przypadkach programiści sięgają po zewnętrzne biblioteki, takie jak ICU (International Components for Unicode), które oferują kompleksowe narzędzia do manipulacji napisami Unicode.

ICU (International Components for Unicode): ICU to popularna biblioteka open-source zapewniająca zaawansowane wsparcie dla międzynarodowych formatów tekstowych, sortowania według lokalnych porządków, konwersji kodowań, obsługi dat i liczb oraz innych aspektów pracy z wielojęzycznymi aplikacjami.

III. Operacje na napisach za pomocą std::string_view:

std::string_view to typ dodany w C++17, który umożliwia efektywniejsze operacje na napisach bez kopiowania danych. std::string_view reprezentuje widok na fragment napisu (ciąg znaków), co pozwala na szybszą i bardziej pamięciooszczędną manipulację tekstem.

#include <iostream>
#include <string_view>

void wypisz_fragment(std::string_view tekst) {
   std::cout << "Fragment tekstu: " << tekst << std::endl;
}

int main() {
   std::string calyTekst = "To jest długi tekst.";
   wypisz_fragment(std::string_view(calyTekst).substr(3, 7));  // "jest d"

   return 0;
}

std::string_view jest idealny w sytuacjach, gdy chcemy jedynie analizować fragmenty tekstu bez potrzeby tworzenia nowych obiektów typu std::string. Jest to często wykorzystywane w sytuacjach, gdzie wydajność jest kluczowa, jak w analizie danych lub podczas operacji na dużych plikach tekstowych.

IV. Biblioteki zewnętrzne do operacji na napisach:

Chociaż standardowa biblioteka C++ oferuje bogate wsparcie dla operacji na napisach, czasem może okazać się niewystarczająca. W takich przypadkach, do bardziej zaawansowanych zastosowań, istnieje wiele zewnętrznych bibliotek, takich jak:

Spis Treści

    Napisy w języku C i C++
    1. Napisy w języku C (C-string)
      1. Deklaracja i inicjalizacja napisów w C
      2. Znaczenie znaku null
      3. Operacje na napisach w C
      4. Przykłady użycia napisów w C
      5. Zarządzanie pamięcią i bezpieczeństwo
    2. Napisy w języku C++ (std::string)
      1. Zalety użycia std::string
      2. Tworzenie i inicjalizacja std::string
      3. Podstawowe operacje na std::string
      4. Interoperacyjność z C-stringami
      5. Bezpieczeństwo i wydajność
      6. Przykłady użycia std::string
      7. Zaawansowane operacje na napisach