inf‎ > ‎

lab5

#include <conio.h>

void wyslij_znak(char c) {
 _putch(c);
}

void odebrano_znak(char c) {
    wyslij_znak(c);
}

int main() {
    char c;
    do {
        c = _getch(); //odczytaj znak z klawiatury
        if(c != 13)
            odebrano_znak(c); //przekaż ten znak funkcji odebrano_znak(…)
        else
            printf("\r\nwykryto ENTER\r\n");
     } while(c!=27); //Esc kończy program
    return 0;
}

Powyższy kod definiuje 2 funkcje: wyslij_znak(...) oraz odebrano_znak(...). Kiedy używacie Code::Blocks pod Windows, do ich realizacji posługujemy się _putch(...) oraz _getch() z CONIO (nie mylić z STDIO!).
W odróżnieniu od stdio, conio nie wymaga wciśnięcia klawisza Enter po wprowadzeniu znaku. Znak z klawiatury od razu trafia do funkcji _getch(). Jest to zachowanie bardzo podobne do terminala znakowego, który wysyła i odbiera znaki z mikrokontrolera np. przez port szeregowy (Bluetooth albo konwerter USB-UART).

Kiedy funkcja odebrano_znak(...) nie robi nic innego tylko wywołuje wyslij_znak(...), obserwujemy mało interesujący scenariusz — prawie wszystko to, co zostanie wciśnięte na klawiaturze, trafia do funkcji wyslij_znak(...), która to wyświetla w okienku tekstowym. Dodajmy jednak jakiś kawałek kodu, który modyfikuje wpisywane znaki (tutaj akurat pojawia się zamiana małych łacińskich liter na wielkie):

#include <conio.h>
void wyslij_znak(char c) {
 _putch(c);
}
void odebrano_znak(char c) {
    if(c >= 'a' && c <= 'z') c -= ('a' - 'A');
    wyslij_znak(c);
}
int main() {
    char c;
    do {
        c = _getch(); //odczytaj znak z klawiatury
        if(c != 13)
            odebrano_znak(c); //przekaż ten znak funkcji odebrano_znak(…)
        else
            printf("\r\nwykryto ENTER\r\n");
     } while(c!=27); //Esc kończy program
    return 0;
}

Teraz niezależnie od tego, czy wciśniecie 'a' albo 'A', w konsoli pojawi się wielkie 'A'. Analogiczny kawałek kodu znacie z laborki 4.
Jak jednak gromadzić znaki w tablicy do późniejszej analizy?
Tablica znaków to miejsce, gdzie zwykle zmieści się więcej niż 1 znak. Problem w tym, że trzeba precyzyjnie określić, w które miejsce tablicy (na którą pozycję) ma trafić kolejny wprowadzony znak.
Funkcja odebrano_znak(...) 'widzi' tylko aktualnie odebrany kod znaku, nie wie, ile ich było wcześniej, nie wie też, ile ich będzie później.
Zacznijmy od przygotowania miejsca na odebrane znaki (tzn. tablicy znaków) — na początek załóżmy, że wystarczy 100-elementowa tablica:

char t[100];

Dodatkowo będę pamiętał, gdzie wpisać bieżący odebrany znak (czyli inaczej ostatni użyty element tablicy). Tablice w C zawsze indeksujemy od 0, więc pierwszy odebrany znak powinien trafić na pozycję 0. Jednocześnie tablica znakowa, o ile ma być formalnie poprawnym napisem w C, musi się kończyć znakiem o kodzie 0. Logiczne jest, że ten znak (0) muszę dopisać ZA ostatnim znakiem, który właśnie odebrałem. W sumie mam coś takiego:

#include <conio.h>
char t[100];
int last = 0; //początkowa pozycja ostatniego znaku, w przyszłości się będzie zmieniać
void wyslij_znak(char c) {
 _putch(c);
}
void odebrano_znak(char c) {
    //if(c >= 'a' && c <= 'z') c -= ('a' - 'A');
    t[last] = c; //dopisz odebrany znak do tablicy
    last++; //przesuń indeks na kolejny znak
    t[last] = 0; //ZA odebranym wcześniej znakiem (czyli 1 pozycję dalej) wpisz znak 0 - co oznacza koniec napisu w C. Koniec znajduje się bezpośrednio za ostatnim znakiem napisu, logiczne?
    wyslij_znak(c); //wyślij echona konsolę tekstową
}

Przydać się może funkcja wyslij_tekst(char t[]) {...}

#include <conio.h>
#include <string.h>
#include <stdio.h>
char t[100];
int last = 0; //pozycja ostatniego znaku
void wyslij_znak(char c) {
 _putch(c);
}
void odebrano_znak(char c) {
    t[last] = c; //dopisz odebrany znak do tablicy
    last++; //przesuń indeks na kolejny znak
    t[last] = 0; //ZA odebranym wcześniej znakiem (czyli 1 pozycje dalej) wpisz znak 0 - co oznacza koniec napisu w C.
    wyslij_znak(c); //wyślij echo
}
void wyslij_tekst(char t[]) {
    int len = strlen(t);
    int i;
    for(i=0; i<len; i++) wyslij_znak(t[i]);
}
int main() {
    char c;
    do {
        c = _getch(); //odczytaj znak z klawiatury
        if(c != 13)
            odebrano_znak(c); //przekaż ten znak funkcji odebrano_znak(…)
        else
            wyslij_tekst("\r\nwykryto ENTER\r\n");
     } while(c!=27); //Esc kończy program
    return 0;
}

Użycie dodatkowej zmiennej i w funkcji wyslij_tekst(...) jest zupełnie zbędne. Lepiej posłużyć się wskaźnikami znakowymi.

void wyslij_tekst1(char t[])
{
    int len = strlen(t);
    int i;
    for(i=0; i<len; i++) wyslij_znak(t[i]);
}

void wyslij_tekst2(char *t)
{
    int len = strlen(t);
    int i;
    for(i=0; i<len; i++) wyslij_znak(t[i]);
}

void wyslij_tekst3(char *t)
{
    while(*t) wyslij_znak(*t++);
}

Powyżej przedstawiłem kilka przykładowych sposobów rozwiązania tego samego zadania — wyświetlanie napisu w C. Ostatni (3) jest preferowany, bo zajmuje najmniej kodu i nie wymaga dodatkowych zmiennych.

OK, czas na ciąg dalszy. Tym razem obejrzymy sobie, czy rzeczywiście w tablicy t[] zbierają się odebrane znaki. W tym celu po KAŻDYM odebranym znaku i dopisaniu go do tablicy t wyświetlamy jej bieżącą zawartość. Z kilku różnych funkcji wyświetlających napis wybrałem wyslij_tekst3(char*) — jest najbardziej wypasiona i doskonale wpisuje się w konwencje języka C.

#include <conio.h>
char t[100];
int last = 0; //pozycja ostatniego znaku
void wyslij_znak(char c) {
 _putch(c);
}
void wyslij_tekst3(char *t) {
    while(*t) wyslij_znak(*t++);
}
void odebrano_znak(char c) {
    if(c != 13) { //to nie jest znak Enter (\r)
        t[last] = c; //dopisz odebrany znak do tablicy
        last++; //przesuń indeks na kolejną pozycj (konkretnie: ZA odebranym wlasnie znakiem)
        t[last] = 0; //ZA odebranym wcześniej znakiem (czyli 1 pozycje dalej) wpisz znak 0 - co oznacza koniec napisu w C.
        wyslij_tekst3(t); //wyświetl bieżącą zawartośc tablicy t (czyli aktualny napis)
        wyslij_tekst3("\r\n"); //wykonaj dodatkowo rozkaz przejscia kursora do nowego wiersza, aby całość była czytelniejsza.
    } else
      wyslij_tekst3("\r\nwykryto ENTER\r\n");
}
int main() {
    char c;
    do {
        c = _getch(); //odczytaj znak z klawiatury
        odebrano_znak(c); //przekaż ten znak funkcji odebrano_znak(…)
     } while(c!=27); //Esc zakończy program
    return 0;
}

Wypadałoby teraz inaczej zareagować na klawisz ENTER. Zamiast informować użytkownika, że udało mu się trafić w ów ENTER, lepiej będzie, gdy zrobimy coś pożytecznego. Zanim to zrobimy, warto zastanowić się, jak wygląda porównywanie napisów w C. Trywialny sposób to użycie gotowej funkcji strcmp(a, b); gdzie a i b to wskaźniki znakowe lub tablice znakowe. Spróbujmy jednak samodzielnie zdefiniować funkcję int czy_rowne(char* a, char *b); .

Zacznijmy od 2 napisów - jeden z nich to wskaźnik znakowy, drugi to tablica:
char *a = "Napis1";
char b[] = { "Napis2" };

Łatwo można się przekonać, że zarówno a jak i b są poprawnymi napisami w C, wystarczy np. wywołać wyslij_tekst3(a); oraz wyslij_tekst3(b); .
Naiwne będzie jednak porównanie postaci if(a==b) wyswietl_tekst3("napisy sa identyczne"); else wyswietl_tekst3("to nie to samo!"); — to po prostu nie zadziała. Zarówno a jak i b są wskaźnikami, czyli inaczej adresami w pamięci, gdzie przechowywane są napisy a oraz b. Ponieważ są to różne lokalizacje zmiennych w pamięci, to nawet gdyby napisy były identyczne, to i tak wynik porównania będzie fałszem (0).

Zamiast tego trzeba porównywać napisy znak po znaku. Pierwsze, co warto zrobić, to upewnić się, że długości napisów są równe, w przeciwnym razie bowiem nie ma sensu porównywanie ich zawartości:
Uwaga! to nie jest kompletny kod, sam zadbaj o odpowiednie pliki nagłówkowe i brakujące funkcje!

int czy_rowne(char* a, char *b) //zwraca 0==fałsz (różne) albo 1==prawda (identyczne)
{
    int i;
    int len_a = strlen(a);
    int len_b = strlen(b);
    if(len_a!=len_b) return 0; //napisy nie są równe, bo mają różne dlugości! zwracamy 0 (fałsz)
    for(i=0; i<len_a; i++)
        if(a[i] != b[i]) return 0; //znaleziono różnice na i-tym znaku, więc napisy nie są równe, nie ma więc sensu ich dalej porownywać. Zwracamy 0 (fałsz)
    return 1; //nie wykrylismy wczesniej roznic w dlugosci, ani na zadnej z analizowanych pozycji (liter) napisow. Najwyrazniej sa one identyczne...
}

int main() {
    char *a = "NapisXXX";
    char b[] = { "NapisXXX" };
    if(czy_rowne(a, b))
        wyslij_tekst3("napisy a i b są identyczne!\r\n");
    else
        wyslij_tekst3("napisy a i b są różne!\r\n");
    return 0;
}
Komunikat będzie brzmiał "napisy a i b są identyczne!", bo też napisy a oraz b są rzeczywiście identyczne w tym przykładzie.
Czy pamiętasz jeszcze, że funkcja wyslij_tekst miała 3 różne wersje? Podobnie będzie z int czy_rowne(char* a, char *b) — jej kod można docelowo zmieścić w zaledwie 2 linijkach (wymagane jest użycie wskaźników). To, co zostało tutaj zaproponowane, to zaledwie zaliczenie na 3.0 — ten kod jest formalnie poprawny, ale niepotrzebnie są sprawdzane długości napisów, nie jest też potrzebna dodatkowa zmienna i w pętli for. Spróbuj ulepszyć ten kod!

Kiedy już mamy gotową funkcję, która potrafi porównać 2 napisy (a dokładniej, pewien napis i zawartość tablicy, w której gromadziliśmy znaki z klawiatury), to już niedaleko nam do programu rozpoznającego polecenia terminala tekstowego.
Sięgnij wyżej — do kodu, gdzie gromadzony były znaki i wykrywane wciśnięcie ENTER. Wystarczyłoby porównywać tablicę t (to, co zostało wprowadzone z klawiatury) z treścią pewnego polecenia (np. "kaboom!").
Zmodyfikujmy fragmenty poznanych już kodów:

void odebrano_znak(char c) {
    wyslij_znak(c); //niech użytkownik widzi to, co wklepał, odsyłamy mu kopię znaku
    if(c != 13) {
        t[last] = c;
        last++;
        t[last] = 0;
    } else {
        if(czy_rowne(t, "kaboom!"))
            wyslij_tekst3("Teraz nastepuje koniec swiata...\r\n");
        if(czy_rowne(t, "help"))
            wyslij_tekst3("Ten program sporo potrafi - rozpoznaje polecenia: help; kaboom!; exit.\r\n");
        if(czy_rowne(t, "exit"))
            exit(0); //wymagany nagłówek <stdlib.h>
        last = 0; //przygotuj sie do odbierania kolejnego polecenia
        t[last] = 0; //tablica t staje sie pustym napisem w C (ma znak 0 na końcu).
    }
}

Warto zaznaczyć, że całość przytoczonego kodu (kiedy go juz poskładasz z kawałków) wymaga użycia 3 plików nagłówkowych:
#include <conio.h>
#include <stdlib.h>
#include <string.h>

Sprytniejsci obejdą się bez stringów.
Comments