C/Podstawowe procedury wejścia i wyjścia

< C

Komputer byłby całkowicie bezużyteczny, gdyby użytkownik nie mógł się z nim porozumieć (tj. wprowadzić danych lub otrzymać wyników pracy programu). Programy komputerowe służą w największym uproszczeniu do obróbki danych - więc muszą te dane jakoś od nas otrzymać, przetworzyć i przekazać nam wynik.

Takie wczytywanie i "wyrzucanie" danych w terminologii komputerowej nazywamy wejściem (input) i wyjściem (output). Bardzo często mówi się o wejściu i wyjściu danych łącznie - input/output, albo po prostu I/O.

W C do komunikacji z użytkownikiem służą odpowiednie funkcje, które możemy znależć w standardowej bibliotece stdio ( plik stdio.h) . Zresztą, do wielu zadań w C służą funkcje. Używając funkcji, nie musimy wiedzieć, w jaki sposób komputer wykonuje jakieś zadanie, interesuje nas tylko to, co ta funkcja robi. Funkcje niejako "wykonują za nas część pracy", ponieważ nie musimy pisać być może dziesiątek linijek kodu, żeby np. wypisać tekst na ekranie (wbrew pozorom - kod funkcji wyświetlającej tekst na ekranie jest dość skomplikowany). Jeszcze taka uwaga - gdy piszemy o jakiejś funkcji, zazwyczaj podając jej nazwę dopisujemy na końcu nawias:

printf()
scanf()

żeby było jasne, że chodzi o funkcję, a nie o coś innego.

Wyżej wymienione funkcje to jedne z najczęściej używanych funkcji w C - pierwsza służy do wypisywania danych na ekran, natomiast druga do wczytywania danych z klawiatury. W zasadzie standard C nie definiuje czegoś takiego jak ekran i klawiatura - mowa w nim o standardowym wyjściu i standardowym wejściu. Zazwyczaj jest to właśnie ekran i klawiatura, ale nie zawsze. W szczególności użytkownicy Linuksa lub innych systemów uniksowych mogą być przyzwyczajeniu do przekierowania wejścia/wyjścia z/do pliku czy łączenie komend w potoki (ang. pipe). W takich sytuacjach dane nie są wyświetlane na ekranie, ani odczytywane z klawiatury.


Zanim będzie można odczytywać lub zapisywać zawartość pliku, należy ustanowić połączenie lub kanał komunikacji z plikiem. Ten proces nazywa się otwieraniem pliku. Możesz otworzyć plik do odczytu, zapisu lub obu. Połączenie z otwartym plikiem jest reprezentowane jako strumień lub deskryptor pliku. Przekazujesz to jako argument do funkcji, które wykonują rzeczywiste operacje odczytu lub zapisu, aby powiedzieć im, na którym pliku mają działać. Niektóre funkcje oczekują strumieni, a inne są zaprojektowane do działania na deskryptorach plików. Po zakończeniu wczytywania lub zapisywania pliku można zakończyć połączenie, zamykając plik. Po zamknięciu strumienia lub deskryptora pliku nie można już wykonywać na nim żadnych operacji wejścia ani wyjścia.

Metody analizy argumentów I/O

edytuj

Sposoby analizowania argumentów wiersza poleceń w C ( ang. parsing command line arguments or Parsing Program Arguments )[1] [2]

  • gotowe biblioteki
  • własna sposób ( ręczny). Nie jest to polecane w przypadku programów, które zostałyby przekazane komuś innemu, ponieważ jest zbyt wiele rzeczy, które mogą się nie udać lub obniżyć jakość. Popularny błąd polegający na zapominaniu o „--” ( ang. double-dash albo precyzyjniej double-hyphen) w celu zatrzymania parsowania opcji.

Argumenty I/O

edytuj

Argumenty I/O to argumenty programu , czyli argumenty funkcji main

Ręczne I/O

edytuj

Etapy

  • wczytujemy parametry
  • sprawdzamy czy są poprawne
  • przetwarzamy parametry


Funkcje wyjścia

edytuj

Funkcja printf

edytuj

W przykładzie "Witaj świecie!" użyliśmy już jednej z dostępnych funkcji wyjścia, a mianowicie funkcji printf(). Z punktu widzenia swoich możliwości jest to jedna z bardziej skomplikowanych funkcji, a jednocześnie jest jedną z najczęściej używanych. Przyjrzyjmy się ponownie kodowi programu "Witaj świecie!".

 #include <stdio.h>
 
 int main(void)
 {
   printf("Witaj swiecie!\n");
   return 0;
 }

Po skompilowaniu i uruchomieniu, program wypisze na ekranie:

Witaj swiecie!


W naszym przykładowym programie, chcąc by funkcja printf() wypisała tekst na ekranie, umieściliśmy go w cudzysłowach wewnątrz nawiasów.

Ogólnie, wywołanie funkcji printf() wygląda następująco:

printf(format, argument1, argument2, ...);

czyli funkcja może przyjąć zmienną liczbę argumentów.


Przykładowo:

 int i = 500;
 printf("Liczbami całkowitymi są na przykład %i oraz %i.\n", 1, i);

wypisze

Liczbami całkowitymi są na przykład 1 oraz 500.

Format to napis ujęty w cudzysłowy, który określa ogólny kształt, schemat tego, co ma być wyświetlone. Format jest drukowany tak, jak go napiszemy, jednak niektóre znaki specjalne zostaną w nim podmienione na co innego. Przykładowo, znak specjalny \n jest zamieniany na znak nowej linii [4]. Natomiast procent jest podmieniany na jeden z argumentów. Po procencie następuje specyfikacja, jak wyświetlić dany argument. W tym przykładzie %i (od int) oznacza, że argument ma być wyświetlony jak liczba całkowita. W związku z tym, że \ i % mają specjalne znaczenie, aby wydrukować je, należy użyć ich podwójnie:

 printf("Procent: %% Backslash: \\");

drukuje:

Procent: % Backslash: \

(bez przejścia do nowej linii). Na liście argumentów możemy mieszać ze sobą zmienne różnych typów, liczby, napisy itp. w dowolnej liczbie. Funkcja printf przyjmie ich tyle, ile tylko napiszemy. Należy uważać, by nie pomylić się w formatowaniu:

int i = 5;
printf("%i %s %i", 5, 4, "napis"); /* powinno być: "%i %i %s" */

Przy włączeniu ostrzeżeń (opcja -Wall lub -Wformat w GCC) kompilator powinien nas ostrzec, gdy format nie odpowiada podanym elementom.

Najczęstsze użycie printf():

  • printf("%i", i); gdy i jest typu int; zamiast %i można użyć %d
  • printf("%f", i); gdy i jest typu float lub double
  • printf("%c", i); gdy i jest typu char (i chcemy wydrukować znak)
  • printf("%s", i); gdy i jest napisem (typu char*)

Funkcja printf() nie jest żadną specjalną konstrukcją języka i łańcuch formatujący może być podany jako zmienna.[5] W związku z tym możliwa jest np. taka konstrukcja:

 #include <stdio.h>
 
 int main(void)
 {
   char buf[100];
   scanf("%99s", buf); /* funkcja wczytuje tekst do tablicy buf */
   printf(buf);
   return 0;
 }

Program wczytuje tekst, a następnie wypisuje go. Jednak ponieważ znak procentu jest traktowany w specjalny sposób, toteż jeżeli na wejściu pojawi się ciąg znaków zawierający ten znak mogą się stać różne dziwne rzeczy. Między innymi z tego powodu w takich sytuacjach lepiej używać funkcji puts() lub fputs() opisanych niżej lub wywołania: printf("%s", zmienna);.

Więcej o funkcji printf()

Funkcja puts

edytuj

Funkcja puts() przyjmuje jako swój argument ciąg znaków, który następnie bezmyślnie wypisuje na ekran kończąc go znakiem przejścia do nowej linii. W ten sposób, nasz pierwszy program moglibyśmy napisać w ten sposób:

 #include <stdio.h>
 
 int main(void)
 {
   puts("Witaj swiecie!");
   return 0;
 }

W swoim działaniu funkcja ta jest w zasadzie identyczna do wywołania: printf("%s\n", argument); jednak prawdopodobnie będzie działać szybciej. Jedynym jej mankamentem może być fakt, że zawsze na końcu podawany jest znak przejścia do nowej linii. Jeżeli jest to efekt niepożądany (nie zawsze tak jest) należy skorzystać z funkcji fputs() opisanej niżej lub wywołania printf("%s", argument);.

Więcej o funkcji puts()

Funkcja fputs

edytuj

Opisując funkcję fputs() wybiegamy już trochę w przyszłość (a konkretnie do opisu operacji na plikach), ale warto o niej wspomnieć już teraz, gdyż umożliwia ona wypisanie swojego argumentu bez wypisania na końcu znaku przejścia do nowej linii:

 #include <stdio.h>
 
 int main(void)
 {
   fputs("Witaj swiecie!\n", stdout);
   return 0;
 }

W chwili obecnej możesz się nie przejmować tym zagadkowym stdout wpisanym jako drugi argument funkcji. Jest to określenie strumienia wyjściowego (w naszym wypadku standardowe wyjście - standard output).

Więcej o funkcji fputs()

Funkcja putchar

edytuj

Funkcja putchar() służy do wypisywania pojedynczych znaków. Przykładowo jeżeli chcielibyśmy napisać program wypisujący w prostej tabelce wszystkie liczby od 0 do 99 moglibyśmy to zrobić tak:

 #include <stdio.h>
 
 int main(void) 
 {
   int i = 0;
   for (; i<100; ++i) 
   {
     /* Nie jest to pierwsza liczba w wierszu */
     if (i % 10) 
     {
       putchar(' ');
     }
     printf("%2d", i);
     /* Jest to ostatnia liczba w wierszu */
     if ((i % 10)==9) 
     {
       putchar('\n');
     }
   }
   return 0;
 }

Więcej o funkcji putchar()

Funkcje wejścia

edytuj

Funkcja scanf()

edytuj

Teraz pomyślmy o sytuacji odwrotnej. Tym razem to użytkownik musi powiedzieć coś programowi. W poniższym przykładzie program podaje kwadrat liczby, podanej przez użytkownika:

 #include <stdio.h>
 
 int main ()
 {
   int liczba = 0;
   printf ("Podaj liczbę: ");
   scanf ("%d", &liczba);
   printf ("%dx%d=%d\n", liczba, liczba, liczba*liczba); 
   return 0;
 }

Zauważyłeś, że w tej funkcji przy zmiennej pojawił się nowy operator - & (etka). Jest on ważny, gdyż bez niego funkcja scanf() nie skopiuje odczytanej wartości liczby do odpowiedniej zmiennej! Właściwie oznacza przekazanie do funkcji adresu zmiennej, by funkcja mogła zmienić jej wartość. Nie musisz teraz rozumieć jak to się odbywa. Wszystko zostanie wyjaśnione w rozdziale Wskaźniki.

Oznaczenia są podobne takie jak przy printf(), czyli scanf("%i", &liczba); wczytuje liczbę typu int, scanf("%f", &liczba); – liczbę typu float, a scanf("%s", tablica_znaków); ciąg znaków. Ale czemu w tym ostatnim przypadku nie ma etki? Otóż, gdy podajemy jako argument do funkcji wyrażenie typu tablicowego zamieniane jest ono automatycznie na adres pierwszego elementu tablicy. Będzie to dokładniej opisane w rozdziale poświęconym wskaźnikom.

Należy jednak uważać na to ostatnie użycie. Rozważmy na przykład poniższy kod:

 #include <stdio.h>
 
 int main(void)
 {
   char tablica[100];     /* 1 */
   scanf("%s", tablica);  /* 2 */
   return 0;
 }

Robi on niewiele. W linijce 1 deklarujemy tablicę 100 znaków czyli mogącą przechować napis długości 99 znaków. Nie przejmuj się jeżeli nie do końca to wszystko rozumiesz - pojęcia takie jak tablica czy ciąg znaków staną się dla Ciebie jasne w miarę czytania kolejnych rozdziałów. W linijce 2 wywołujemy funkcję scanf(), która odczytuje tekst ze standardowego wejścia. Nie zna ona jednak rozmiaru tablicy i nie wie ile znaków może ona przechować przez co będzie czytać tyle znaków, aż napotka biały znak (format %s nakazuje czytanie pojedynczego słowa), co może doprowadzić do przepełnienia bufora. Niebezpieczne skutki czegoś takiego opisane są w rozdziale poświęconym napisom. Na chwilę obecną musisz zapamiętać, żeby zaraz po znaku procentu podawać maksymalną liczbę znaków, które może przechować bufor, czyli liczbę o jeden mniejszą, niż rozmiar tablicy. Bezpieczna wersją powyższego kodu jest:

 #include <stdio.h>
 
 int main(void)
 {
   char tablica[100];
   scanf("%99s", tablica);
   return 0;
 }

Funkcja scanf() zwraca liczbę poprawnie wczytanych zmiennych lub EOF jeżeli nie ma już danych w strumieniu lub nastąpił błąd. Załóżmy dla przykładu, że chcemy stworzyć program, który odczytuje po kolei liczby i wypisuje ich trzecie potęgi. W pewnym momencie dane się kończą lub jest wprowadzana niepoprawna dana i wówczas nasz program powinien zakończyć działanie. Aby to zrobić, należy sprawdzać wartość zwracaną przez funkcję scanf() w warunku pętli:

 #include <stdio.h>
 
 int main(void)
 {
   int n;
   while (scanf("%d", &n)==1) 
   {
     printf("%d\n", n*n*n);
   }
   return 0;
 }

Podobnie możemy napisać program, który wczytuje po dwie liczby i je sumuje:

 #include <stdio.h>
 
 int main(void)
 {
   int a, b;
   while (scanf("%d %d", &a, &b)==2) 
   {
     printf("%d\n", a+b);
   }
   return 0;
 }

Rozpatrzmy teraz trochę bardziej skomplikowany przykład. Otóż, ponownie jak poprzednio nasz program będzie wypisywał trzecią potęgę podanej liczby, ale tym razem musi ignorować błędne dane (tzn. pomijać ciągi znaków, które nie są liczbami) i kończyć działanie tylko w momencie, gdy nastąpi błąd odczytu lub koniec pliku[6].

 #include <stdio.h>
 
 int main(void)
 {
   int result, n;
   do 
   {
     result = scanf("%d", &n);
     if (result) /* result to to samo co result!=0 */
       printf("%d\n", n*n*n);
     else
       result = scanf("%*s");
   } 
   while (result!=EOF);
   return 0;
 }

Zastanówmy się przez chwilę co się dzieje w programie. Najpierw wywoływana jest funkcja scanf() i następuje próba odczytu liczby typu int. Jeżeli funkcja zwróciła 1 to liczba została poprawnie odczytana i następuje wypisanie jej trzeciej potęgi. Jeżeli funkcja zwróciła 0 to na wejściu były jakieś dane, które nie wyglądały jak liczba. W tej sytuacji wywołujemy funkcję scanf() z formatem odczytującym dowolny ciąg znaków nie będący białymi znakami z jednoczesnym określeniem, żeby nie zapisywała nigdzie wyniku. W ten sposób niepoprawnie wpisana dana jest omijana. Pętla główna wykonuje się tak długo jak długo funkcja scanf() nie zwróci wartości EOF.

Więcej o funkcji scanf()

Funkcja gets

edytuj

Funkcja gets służy do wczytania pojedynczej linii. Może Ci się to wydać dziwne, ale: funkcji tej nie należy używać pod żadnym pozorem. Przyjmuje ona jeden argument - adres pierwszego elementu tablicy, do którego należy zapisać odczytaną linię - i nic poza tym. Z tego powodu nie ma żadnej możliwości przekazania do tej funkcji rozmiaru bufora podanego jako argument. Podobnie jak w przypadku scanf() może to doprowadzić do przepełnienia bufora, co może mieć tragiczne skutki. Zamiast tej funkcji należy używać funkcji fgets().

Więcej o funkcji gets()

Funkcja fgets

edytuj

Funkcja fgets() jest bezpieczną wersją funkcji gets(), która dodatkowo może operować na dowolnych strumieniach wejściowych. Jej użycie jest następujące:

fgets(tablica_znaków, rozmiar_tablicy_znaków, stdin);

Na chwilę obecną nie musisz się przejmować ostatnim argumentem (jest to określenie strumienia, w naszym przypadku standardowe wejście - standard input). Funkcja czyta tekst aż do napotkania znaku przejścia do nowej linii, który także zapisuje w wynikowej tablicy (funkcja gets() tego nie robi). Jeżeli brakuje miejsca w tablicy to funkcja przerywa czytanie, w ten sposób, aby sprawdzić czy została wczytana cała linia czy tylko jej część należy sprawdzić czy ostatnim znakiem nie jest znak przejścia do nowej linii. Jeżeli nastąpił jakiś błąd lub na wejściu nie ma już danych funkcja zwraca wartość NULL.

 #include <stdio.h>
 
 int main(void) {
   char buffer[128], whole_line = 1, *ch;
   while (fgets(buffer, sizeof buffer, stdin)) { /* 1 */
     if (whole_line) {                           /* 2 */
       putchar('>');
       if (buffer[0]!='>') {
         putchar(' ');
       }
     }
     fputs(buffer, stdout);                      /* 3 */
     for (ch = buffer; *ch && *ch!='\n'; ++ch);  /* 4 */
     whole_line = *ch == '\n';
   }
   if (!whole_line) {
     putchar('\n');
   }
   return 0;
 }

Powyższy kod wczytuje dane ze standardowego wejścia - linia po linii - i dodaje na początku każdej linii znak większości, po którym dodaje spację jeżeli pierwszym znakiem na linii nie jest znak większości. W linijce 1 następuje odczytywanie linii. Jeżeli nie ma już więcej danych lub nastąpił błąd wejścia funkcja zwraca wartość NULL, która ma logiczną wartość 0 i wówczas pętla kończy działanie. W przeciwnym wypadku funkcja zwraca po prostu pierwszy argument, który ma wartość logiczną 1. W linijce 2 sprawdzamy, czy poprzednie wywołanie funkcji wczytało całą linię, czy tylko jej część - jeżeli całą to teraz jesteśmy na początku linii i należy dodać znak większości. W linii 3 najzwyczajniej w świecie wypisujemy linię. W linii 4 przeszukujemy tablicę znak po znaku, aż do momentu, gdy znajdziemy znak o kodzie 0 kończącym ciąg znaków albo znak przejścia do nowej linii. Ten drugi przypadek oznacza, że funkcja fgets() wczytała całą linię.

Więcej o funkcji fgets()

Funkcja getchar()

edytuj

Jest to bardzo prosta funkcja, wczytująca 1 znak z klawiatury. W wielu przypadkach dane mogą być buforowane przez co wysyłane są do programu dopiero, gdy bufor zostaje przepełniony lub na wejściu jest znak przejścia do nowej linii. Z tego powodu po wpisaniu danego znaku należy nacisnąć klawisz enter, aczkolwiek trzeba pamiętać, że w następnym wywołaniu zostanie zwrócony znak przejścia do nowej linii. Gdy nastąpił błąd lub nie ma już więcej danych funkcja zwraca wartość EOF (która ma jednak wartość logiczną 1 toteż zwykła pętla while (getchar()) nie da oczekiwanego rezultatu):

 #include <stdio.h>
 
 int main(void)
 {
   int c;
   while ((c = getchar())!=EOF) {
     if (c==' ') {
       c = '_';
     }
     putchar(c);
   }
   return 0;
 }

Ten prosty program wczytuje dane znak po znaku i zamienia wszystkie spacje na znaki podkreślenia. Może wydać się dziwne, że zmienną c zdefiniowaliśmy jako trzymającą typ int, a nie char. Właśnie taki typ (tj. int) zwraca funkcja getchar() i jest to konieczne ponieważ wartość EOF wykracza poza zakres wartości typu char (gdyby tak nie było to nie byłoby możliwości rozróżnienia wartości EOF od poprawnie wczytanego znaku). Więcej o funkcji getchar()


Przypisy

  1. gnu libc manual : Parsing-Program-Arguments
  2. stackoverflow question: parsing-command-line-arguments-in-c
  3. | POSIX.1-2017 from The Open Group Base Specifications Issue 7, 2018 edition. 12.2 Utility Syntax Guidelines. Guideline 10
  4. Zmiana ta następuje w momencie kompilacji programu i dotyczy wszystkich literałów napisowych. Nie jest to jakaś szczególna własność funkcji printf(). Więcej o tego typu sekwencjach i ciągach znaków w szczególności opisane jest w rozdziale Napisy.
  5. unix.com : passing-printf-formatting-parameters-variables
  6. Jak rozróżniać te dwa zdarzenia dowiesz się w rozdziale Czytanie i pisanie do plików.