Last modified: September 25, 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:

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:

Biblioteka <stdlib.h>

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

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