BAZA WIEDZY
KURSY
Bazy danych w PHP
Kurs AdvancedAJAX
Kurs ASP
Kurs ASP.NET
Kurs C++
Kurs CSS
Kurs HTML
Kurs HTML drugi
Kurs JavaScript
Kurs MySQL
Kurs PHP
Kurs RSS
Kurs XHTML
Obiekty DOM
MANUALE
CSS1 - W3C
DOM - w budowie
PHP 2005
PHP 2006
Wyrażenia regularne
SHOUTBOX
STAT
Online: 15 | UU: 520

Łańcuchy znaków

Ciągi znaków (ang. strings ) stanowią drugi, po liczbach, ważny rodzaj informacji przetwarzanych przez programy. Chociaż zajmują więcej miejsca w pamięci niż dane binarne, a operacje na nich trwają dłużej, mają wiele znaczących zalet. Jedną z nich jest fakt, iż są bardziej zrozumiałe dla człowieka niż zwykłe sekwencje bitów. W czasie, gdy moce komputerów rosną bardzo szybko, wymienione wcześniej wady nie są natomiast aż tak dotkliwe. Wszystko to powoduje, że dane tekstowe są coraz powszechniej spotykane we współczesnych aplikacjach.

Duża jest w tym także rola Internetu. Takie standardy jak HTML czy XML są przecież formatami tekstowymi.

Dla programistów napisy były od zawsze przyczyną częstych bólów głowy. W przeciwieństwie bowiem do typów liczbowych, mają one zmienny rozmiar , który nie może być ustalony raz podczas uruchamiania programu. Ilość pamięci operacyjnej, którą zajmuje każdy napis musi być dostosowywana do jego długości (liczby znaków) i zmieniać się podczas działania aplikacji. Wymaga to dodatkowego czasu (od programisty i od komputera), uwagi oraz dokładnego przemyślenia (przez programistę, nie komputer ;D) mechanizmów zarządzania pamięcią.

Zwykli użytkownicy pecetów - szczególnie ci, którzy pamiętają jeszcze zamierzchłe czasy DOSa - także nie mają dobrych wspomnień związanych z danymi tekstowymi. Odwieczne kłopoty z polskimi "ogonkami" nadal dają o sobie znać, choć na szczęście coraz rzadziej musimy oglądać na ekranie dziwne "krzaczki" zamiast znajomych liter w rodzaju 'ą', 'ć', 'ń' czy 'ź'.

Wydaje się więc, że przed koderem piszącym programy przetwarzające tekst piętrzą się niebotyczne wręcz trudności. Problemy są jednak po to, aby je rozwiązywać (lub by inni rozwiązywali je za nas ;)), więc oba wymienione dylematy doczekały się już wielu bardzo dobrych pomysłów.

Rozszerzające się wykorzystanie standardu Unicode ograniczyło już znacznie kłopoty związane ze znakami specyficznymi dla niektórych języków. Kwestią czasu zdaje się chwila, gdy znikną one zupełnie.

Powstało też mnóstwo sposobów na efektywne składowanie napisów o zmiennej długości w pamięci komputera. Wprawdzie w tym przypadku nie ma jednego, wiodącego trendu zapewniającego przenośność między wszystkimi platformami sprzętowymi lub chociaż aplikacjami, jednak i tak sytuacja jest znacznie lepsza niż jeszcze kilka lat temu 1 . Koderzy mogą więc sobie pozwolić na uzasadniony optymizm :)

Wsparci tymi pokrzepiającymi faktami możemy teraz przystąpić do poznawania elementów języka C++, które służą do pracy z łańcuchami znaków.

Napisy według C++

Trudno w to uwierzyć, ale poprzednik C++ - język C - w ogóle nie posiadał odrębnego typu zmiennych, mogącego przechowywać napisy. Aby móc operować danymi tekstowymi, trzeba było używać mało poręcznych tablic znaków (typu char ) i samemu dbać o zagadnienia związane z przydzielaniem i zwalnianiem pamięci.

Nam, programistom C++, nic takiego na szczęście nie grozi :) Nasz ulubiony język jest bowiem wyposażony w kilka bardzo przydatnych i łatwych w obsłudze mechanizmów, które udostępniają możliwość manipulacji tekstem.

Rozwiązania, o których będzie mowa poniżej, są częścią Biblioteki Standardowej języka C++. Jako że jest ona dostępna w każdym kompilatorze tego języka, sposoby te są najbardziej uniwersalne i przenośne, a jednocześnie wydajne. Korzystanie z nich jest także bardzo wygodne i łatwe.
Oprócz nich istnieją również inne metody obsługi łańcuchów znaków. Na przykład biblioteki MFC i VCL (wspomagające programowanie w Windows) posiadają własne narzędzia, służące temu właśnie celowi 2 . Nawet jeżeli skorzystasz kiedyś z tych bibliotek, będziesz mógł wciąż używać opisanych tutaj mechanizmów standardowych.

Aby móc z nich skorzystać, należy przede wszystkim włączyć do swojego kodu plik nagłówkowy string :

#include <string>

Po tym zabiegu zyskujemy dostęp do całego arsenału środków programistycznych, służących operacjom tekstowym.

1 Dużą zasługę ma w tym ustandaryzowanie języka C++, w którym powstaje ponad połowa współczesnych aplikacji. W przyszłości znaczącą rolę mogą odegrać także rozwiązania zawarte w platformie .NET.

2 MFC (Microsoft Foundation Classes) zawiera przeznaczoną do tego klasę CString , zaś VCL (Visual Component Library) posiada typ String , który jest częścią kompilatora C++ firmy Borland.

Typy zmiennych tekstowych

Istnieją dwa typy zmiennych tekstowych, które różnią się rozmiarem pojedynczego znaku. Ujmuje je poniższa tabelka:

nazwa

typ znaku

rozmiar znaku

zastosowanie

std::string

char

1 bajt

tylko znaki ANSI

std::wstring

wchar_t

2 bajty

znaki ANSI i Unicode

Tabela 6. Typy łańcuchów znaków

std::string jest ci już dobrze znany, gdyż używaliśmy go niejednokrotnie. Przechowuje on dowolną (w granicach dostępnej pamięci) ilość znaków, z których każdy jest typu char . Zajmuje więc dokładnie 1 bajt i może reprezentować jeden z 256 symboli zawartych w tablicy ANSI.

Wystarcza to do przechowywania tekstów w językach europejskich (choć wymaga specjalnych zabiegów, tzw. stron kodowych), jednak staje się niedostateczne w przypadku dialektów o większej liczbie znaków (na przykład wschodnioazjatyckich). Dlatego wykoncypowano, aby dla pojedynczego symbolu przeznaczać większą ilość bajtów i w ten sposób stworzono MBCS ( Multi-Byte Character Sets - wielobajtowe zestawy znaków) w rodzaju Unicode.

Nie mamy tu absolutnie czasu ani miejsca na opisywanie tego standardu. Warto jednak wiedzieć, że C++ posiada typ łańcuchowy, który umożliwia współpracę z nim - jest to std::wstring (ang. wide string - "szeroki" napis). Każdy jego znak jest typu wchar_t (ang. wide char - "szeroki" znak) i zajmuje 2 bajty. Łatwo policzyć, że umożliwia tym samym przechowywanie jednego z aż 65536 (256 2 ) możliwych symboli, co stanowi znaczny postęp w stosunku do ANSI :)

Korzystanie z std::wstring niewiele różni się przy tym od używania jego bardziej oszczędnego pamięciowo kuzyna. Musimy tylko pamiętać, żeby poprzedzać literką L wszystkie wpisane do kodu stałe tekstowe, które mają być trzymane w zmiennych typu std::wstring . W ten sposób bowiem mówimy kompilatorowi, że chcemy zapisać dany napis w formacie Unicode. Wygląda to choćby tak:

std::wstring strNapis = L "To jest tekst napisany znakami dwubajtowymi" ;

Dobra wiadomość jest taka, że jeśli zapomniałbyś o wspomnianej literce L , to powyższy kod w ogóle by się nie skompilował ;D

Jeżeli chciałbyś wyświetlać takie "szerokie" napisy w konsoli i umożliwić użytkownikowi ich wprowadzanie, musisz użyć specjalnych wersji strumieni wejścia i wyjścia. Są to odpowiednio std::wcin i std::wcout Używa się ich w identyczny sposób, jak poznanych wcześniej "zwykłych" strumieni std::cin i std::cout .

Manipulowanie łańcuchami znaków

OK, gdy już znamy dwa typy zmiennych tekstowych, jakie oferuje C++, czas zobaczyć możliwe działania, które możemy na nich przeprowadzać.

Inicjalizacja

Najprostsza deklaracja zmiennej tekstowej wygląda, jak wiemy, mniej więcej tak:

std::string strNapis;

Wprowadzona w ten sposób nowa zmienna jest z początku całkiem pusta - nie zawiera żadnych znaków. Jeżeli chcemy zmienić ten stan rzeczy, możemy ją zainicjalizować odpowiednim tekstem - tak:

std::string strNapis = "To jest jakis tekst" ;

albo tak:

std::string strNapis( "To jest jakis tekst" );

Ten drugi zapis bardzo przypomina wywołanie funkcji. Istotnie, ma on z nimi wiele wspólnego - na tyle dużo, że możliwe jest nawet zastosowanie drugiego parametru, na przykład:

std::string strNapis( "To jest jakis tekst" , 7 );

Jaki efekt otrzymamy tą drogą? Otóż do naszej zmiennej zostanie przypisany jedynie fragment podanego tekstu - dokładniej mówiąc, będzie to podana w drugim parametrze ilość znaków, liczonych od początku napisu. U nas jest to zatem sekwencja "To jest" .

Co ciekawe, to wcale nie są wszystkie sposoby na inicjalizację zmiennej tekstowej. Poznamy jeszcze jeden, który jest wyjątkowo użyteczny. Pozwala bowiem na uzyskanie ściśle określonego "kawałka" danego tekstu. Rzućmy okiem na poniższy kod, aby zrozumieć tą metodę:

std::string strNapis1 = "Jakis krotki tekst" ;
std::string strNapis2(strNapis1, 6 , 6 );

Tym razem mamy aż dwa parametry, które razem określają fragment tekstu zawartego w zmiennej strNapis1 . Pierwszy z nich ( 6 ) to indeks pierwszego znaku tegoż fragmentu - tutaj wskazuje on na siódmy znak w tekście (gdyż znaki liczymy zawsze od zera !). Drugi parametr (znowuż 6 ) precyzuje natomiast długość pożądanego urywka - będzie on w tym przypadku sześcioznakowy.

Jeżeli takie opisowe wyjaśnienie nie bardzo do ciebie przemawia, spójrz na ten poglądowy rysunek:


Schemat 7. Pobieranie wycinka tekstu ze zmiennej typu std::string

Widać więc czarno na białym (i na zielonym :)), że kopiowaną częścią tekstu jest wyraz "krotki" .

Podsumowując, poznaliśmy przed momentem trzy nowe sposoby na inicjalizację zmiennej typu tekstowego:

std:: [ w ] string nazwa_zmiennej ( [ L ] " tekst " );
std:: [ w ] string nazwa_zmiennej ( [ L ] " tekst " , ilość_znaków );
std:: [ w ] string nazwa_zmiennej ( inna_zmienna , początek [ , długość ] );

Ich składnia, podana powyżej, dokładnie odpowiada zaprezentowanym wcześniej przykładowym kodom. Zaskoczenie może jedynie budzić fakt, że w trzeciej metodzie nie jest obowiązkowe podanie długości kopiowanego fragmentu tekstu. Dzieje się tak, gdyż w przypadku jej pominięcia pobierane są po prostu wszystkie znaki od podanego indeksu aż do końca napisu.

Kiedy opuścimy parametr długość , wtedy trzeci sposób inicjalizacji staje się bardzo podobny do drugiego. Nie możesz jednak ich mylić, gdyż w każdym z nich liczby podawane jako drugi parametr znaczą coś innego. Wyrażają one albo ilość znaków , albo indeks znaku , czyli wartości pełniące zupełnie odrębne role.

Łączenie napisów

Skoro zatem wiemy już wszystko, co wiedzieć należy na temat deklaracji i inicjalizacji zmiennych tekstowych, zajmijmy się działaniami, jakie możemy nań wykonywać.

Jedną z najpowszechniejszych operacji jest złączenie dwóch napisów w jeden - tak zwana konkatenacja . Można ją uznać za tekstowy odpowiednik dodawania liczb, szczególnie że przeprowadzamy ją także za pomocą operatora + :

std::string strNapis1 = "gra" ;
std::string strNapis2 = "ty" ;
std::string strWynik = strNapis1 + strNapis2;

Po wykonaniu tego kodu zmienna strWynik przechowuje rezultat połączenia, którym są oczywiście "graty" :D Widzimy więc, iż scalenie zostaje przeprowadzone w kolejności ustalonej przez porządek argumentów operatora + , zaś pomiędzy poszczególnymi składnikami nie są wstawiane żadne dodatkowe znaki. Nie rozminę się chyba z prawdą, jeśli stwierdzę, że można było się tego spodziewać :)

Konkatenacja może również zachodzić między większą liczbą napisów, a także między tymi zapisanymi w sposób dosłowny w kodzie:

std::string strImie = "Jan" ;
std::string strNazwisko = "Nowak" ;
std::string strImieINazwisko = strImie + " " + strNazwisko;

Tutaj otrzymamy personalia pana Nowaka zapisane w postaci ciągłego tekstu, ze spacją wstawioną pomiędzy imieniem i nazwiskiem.

Jeśli chciałbyś połączyć dwa teksty wpisane bezpośrednio w kodzie (np. "jakis tekst" i "inny tekst" ), choćby po to żeby rozbić długi napis na kilka linijek, nie możesz stosować do niego operatora +. Zapis "jakis tekst" + "inny tekst" będzie niepoprawny i odrzucony przez kompilator.
Zamiast niego wpisz po prostu "jakis tekst" "inny tekst" , stawiając między obydwoma stałymi jedynie spacje, tabulatory, znaki końca wiersza itp.

Podobieństwo łączenia znaków do dodawania jest na tyle duże, iż możemy nawet używać skróconego zapisu poprzez operator += :

std::string strNapis = "abc" ;
strNapis += "def" ;

W powyższy sposób otrzymamy więc sześć pierwszych małych liter alfabetu - "abcdef" .

Pobieranie pojedynczych znaków

Ostatnią przydatną operacją na napisach, jaką teraz poznamy, jest uzyskiwanie pojedynczego znaku o ustalonym indeksie.

Być może nie zdajesz sobie z tego sprawy, ale już potrafisz to zrobić. Zamierzony efekt można bowiem osiągnąć, wykorzystując jeden ze sposobów na inicjalizację łańcucha:

std::string strNapis = "przykladowy tekst" ;
std::string strZnak(strNapis, 9 , 1 ); // jednoznakowy fragment od ind. 9

Tak oto uzyskamy dziesiąty znak (przypominam, indeksy liczymy od zera!) z naszego przykładowego tekstu - czyli 'w' .

Przyznasz jednak, że taka metoda jest co najmniej kłopotliwa i byłoby ciężko używać jej na co dzień. Dobry C++ ma więc w zanadrzu inną konstrukcję, którą zobaczymy w niniejszym przykładowym programie:

// CharCounter - zliczanie znaków
#include <string>
#include <iostream>
#include <conio.h>

unsigned ZliczZnaki(std::string strTekst, char chZnak)
{
unsigned uIlosc = 0 ;
for ( unsigned i = 0 ; i <= strTekst.length() - 1 ; ++i)
{
if (strTekst[i] == chZnak)
++uIlosc;
}
return uIlosc;
}
void main()
{
std::string strNapis;
std::cout << "Podaj tekst, w ktorym maja byc zliczane znaki: " ;
std::cin >> strNapis;

char chSzukanyZnak;
std::cout << "Podaj znak, ktory bedzie liczony: " ;
std::cin >> chSzukanyZnak;

std::cout << "Znak '" << chSzukanyZnak << "' wystepuje w tekscie "
<< ZliczZnaki(strNapis, chSzukanyZnak) << " raz(y)."
<< std::endl;
getch();
}

Ta prosta aplikacja zlicza nam ilość wskazanych znaków w podanym napisie i wyświetla wynik.


Screen 23. Zliczanie znaków w akcji

Czyni to poprzez funkcję ZliczZnaki() , przyjmującą dwa parametry: napis oraz znak, który ma być liczony. Ponieważ jest to najważniejsza część naszego programu, przyjrzymy się jej bliżej :)

Najbardziej oczywistym sposobem na dokonanie podobnego zliczania jest po prostu przebiegnięcie po wszystkich znakach tekstu odpowiednią pętlą for i sprawdzanie, czy nie są równe szukanemu znakowi. Każde udane porównanie skutkuje inkrementacją zmiennej przechowującej wynik funkcji. Wszystko to dzieje się w poniższym kawałku kodu:

for ( unsigned i = 0 ; i <= strTekst.length() - 1 ; ++i)
{
if (strTekst[i] == chZnak)
++uIlosc;
}

Jak już kilkakrotnie i natarczywie przypominałem, indeksy znaków w zmiennej tekstowej liczymy od zera, zatem są one z zakresu < 0 ; n- 1 > , gdzie n to długość tekstu. Takie też wartości przyjmuje licznik pętli for , czyli i . Wyrażenie strTekst.length() zwraca nam bowiem długość łańcucha strTekst .

Wewnątrz pętli szczególnie interesujące jest dla nas porównanie:

if (strTekst[i] == chZnak)

Sprawdza ono, czy aktualnie "przerabiany" przez pętlę znak (czyli ten o indeksie równym i ) nie jest takim, którego szukamy i zliczamy. Samo porównanie nie byłoby dla nas niczym nadzwyczajnym, gdyby nie owe wyławianie znaku o określonym indeksie (w tym przypadku i -tym). Widzimy tu wyraźnie, że można to zrobić pisząc po prostu żądany indeks w nawiasach kwadratowych [ ] za nazwą zmiennej tekstowej.

Ze swej strony dodam tylko, że możliwe jest nie tylko odczytywanie, ale i zapisywanie takich pojedynczych znaków. Gdybyśmy więc umieścili w pętli następującą linijkę:

strTekst[i] = '.' ;

zmienilibyśmy wszystkie znaki napisu strTekst na kropki.

Pamiętajmy, żeby pojedyncze znaki ujmować w apostrofy ( '' ), zaś cudzysłowy ( "" ) stosować dla stałych tekstowych.

***

Tak oto zakończyliśmy ten krótki opis operacji na łańcuchach znaków w języku C++. Nie jest to jeszcze cały potencjał, jaki oferują nam zmienne tekstowe, ale z pomocą zdobytych już wiadomości powinieneś radzić sobie całkiem nieźle z prostym przetwarzaniem tekstu.

Na koniec tego rozdziału poznamy natomiast typ logiczny i podstawowe działania wykonywane na nim. Pozwoli nam to między innymi łatwiej sterować przebiegiem programu przy użyciu instrukcji warunkowych.


| | | |
Copyright © 2006-2013 egrafik.pl | Kontakt | Reklama | Projekty domów
jocker