Wejście i wyjście

Mało dowcipne rozpoczęcie tego tekstu powinno brzmieć: W czasie zarazy nie wychodzimy. Ale zakładam, że Państwo już trochę przywykliście do zakazów różnego rodzaju.

Zatem czego należy się wystrzegać podczas wejścia-wyjścia? I parę „dobrych” rad.

Funkcja printf()

Należy unikać sytuacji, że specyfikacja wydruku jest niezgodna z typem zmiennej. Na przykład:

	float x = 25.5;
	printf("%d\n",x);

W wyniku da 1321658176. A każde kolejne uruchomienie potrafi dać inny wynik

Podobnie będzie w tym przypadku:

	double x = 25.5;
	printf("%d\n",x);

Pewnym wyjątkiem są zmienne typu double i float. Obydwa typy mogą używać specyfikacji %f (co spowodowane jest domyślną promocją zmiennych typu float do typu double).

Specyfikacja formatu mówi w jaki sposób należy dokonywać konwersji zawartości binarnej do postaci dziesiętnej.

Kompilator zgłasza ostrzeżenie

warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘double’ [-Wformat=]

Funkcja scanf()

Numeryczne

Podobnie zachowuje się funkcja scanf().

Efektem działania programu

#include <stdio.h>
int main(int argc, char **argv)
{
    int x;
    scanf("%f", &x);
    printf("%d\n", x);
    return 0;
}
#include <stdio.h>
int main(int argc, char **argv)
{
    int n = 0;
    int znak;
    while ( ( znak = getchar() ) != EOF )
        n++;
    printf("Liczba znakow: %d\n", n);
    return 0;
}

po wprowadzeniu 123 jest 1123418112.

Natomiast w przypadku kodu

#include <stdio.h>
int main(int argc, char **argv)
{
    double x;
    scanf("%d", &x);
    printf("%f\n", x);
    return 0;
}

po wprowadzeniu 123 jest 0.00000.

Tekstowe

Trzeba pamiętać, że funkcja scanf() pobiera tekst do pierwszego odstępu (lub znaku nowej linii). Kolejną sprawą, o której trzeba pamiętać to zarezerwowanie odpowiednio dużej tablicy na pobieranie tekstu.

Popatrzmy na następujący program:

#include <stdio.h>
int main(int argc, char **argv)
{
    char a[4] = "aaa", b[4] = "bbb", c[4] = "ccc";
    scanf("%s", a);
    printf("%s\n", a);
    printf("%s\n", b);
    printf("%s\n", c);
    return 0;
}

Po uruchomieniu wprowadzam tekst alamakota.

Wynik działania programu jest następujacy:

alamakota
akota
a

Jak to wytłumaczyć?

Tablice a, b i c zajmują ciągły obszar pamięci. Funkcja natbiboptions: numbers,square biblio-style: oficyna-url biblio-title: Literatura link-citations: true geometry:

  • scale=0.8 theme: NewPwr classoption: table aspectratio: 169scanf() dostaje adres początku tablicy a. Wprowadzany tekst alamakota (10 bajtów) wpisywany jest do kolejnych komórek pamięci wypełniają tablicę a (litery: alam), b (akot) i c (a\0). Czyli funkcja nadpisuje dotychczasową zawartość tablic.

Funkcja printf()drukuje zawartość pamięci od podanego adresu do wystąpienia znaku \0.

Bardzo podobnie zachowa się funkcja gets() (która właściwie została usunięta ze specyfikacji języka C).

Funkcja fgets()

Do wprowadzania tekstu należy używać funkcji fgets(). Jej prototyp jest nast epujący:

char *fgets(char *s, int size, FILE *stream);

Pierwszy parametr to adres tablicy tekstowej do której wpisujemy tekst, drugi to jej długość, a trzeci to specyfikacja strumienia, z którego czytamy1. W przypadku czytania z klawiatury używamy specyfikacji stdin.

Funkcja pobierze ze wskazanego strumienia tylko tyle znaków, żeby wypełnić tablicę, dodając na końcu znak \0 (o kodzie ASCII 0).

Wadą jej jest to, że wczytuje tekst łącznie ze znakiem przejścia do nowej linii (\n) generowanym przez klawisz enter.

Efektem działania programu

#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
    char a[20];
    fgets(a, 20, stdin);
    printf("%s\n", a);
    for ( int i = 0; i < strlen(a); i++ )
        printf("%3d, %c\n", a[i], a[i]);
    return 0;
}

Po wpisaniu tekstu Ala ma kota będzie:

Ala ma kota
Ala ma kota

 65, A
108, l
 97, a
 32,  
109, m
 97, a
 32,  
107, k
111, o
116, t
 97, a
 10, 

Specyfikacja %3d nakazuje wyprowadzać liczbę na co najmniej trzech polach (uzupełniając ją, ewentualnie, od lewej stron odstępami). Funkcja strlen() podaje długość tekstu, czyli liczy wszystkie znaki aż do wystąpienia znaku o kodzie ASCII 0. („Klawisz Enter” to ten ostatni znak o kodzie ASCII 10.)

Błędy wprowadzania

W przypadku, gdy znaki wprowadzane z klawiatury nie mogą być poprawnie zinterpretowane jako liczba, funkcja scanf() informuje o tym programistę. Poprawne użycie tej funkcji powinno być takie:

int x = scanf("…", &a1, &a2,, &an)

Funkcja służyć ma do czytania $n$ wartości. Gdy zrobi to poprawnie — zmienna x przyjmie wartość $n$. Gdy x jest mniejsze od $n$ oznacza to, że nie udało się przeczytać wszystkich wartości. Gdy x jest ujemne, oznacza to, że nastąpiła próba czytania „poza końcem pliku” (program chce przeczytać więcej danych niż jest w pliku).

Wartości zmiennych do których nie udało się przeczytać danych — pozostają niezmienione.

Do programisty należy obowiązek reagowania na takie sytuacje.

int x = scanf("&d&d", &a, &b);

Chcemy przeczytać dwie wartości typu int. Operator wpisujący dane pomylił się i podał:

12. 2.

scanf() czyta dane z kolejki wejściowej interpretując na bieżąco dane.

Najpierw czyta znak 1 (to jest dobra wartość, która może budować wartość int). Następnie trafia na znak 2, który również może budować wartość int. Kolejny znak to . która nie buduje wartość całkowitej. Program kończy czytanie pozostawiając w strumieniu wejściowym kropkę. Przeczytane cyfry 12 konwertuje na wartość binarną, która trafia pod adres &a.

Ponieważ na liście parametrów jest jeszcze jeden — scanf() kontynuuje pracę. Pierwszy przeczytany ze strumienia wejściowego znak to kropka. Nie może ona służyć do zbudowania liczby całkowitej. Funkcja kończy pracę, zwraca wartość 1 (przeczytała poprawnie jedną wartość). Zmienna b pozostaje niezmieniona. x ma wartość 1.

Z tego powodu, porządnie napisany program, po każdym użyciu funkcji scanf() powinien sprawdzać czy funkcja skończyła się poprawnie.

EOF

Wszystkie funkcje czytające po dojściu do końca pliku zwracają wartość równą stałej EOF (zazwyczaj -1)

Poniższy program tworzy plik test.txt wpisuje do niego 7 liczb:

#include <stdio.h>
int main  ()
{
	FILE * p;
	p = fopen("test.txt", "w");
	if (p == NULL)
		return 2;c
	for(int i=0; i < 7; i++)
		fprintf(p, "%d ", i);
	fclose(p);
	return 0;
}

Zawartość pliku test.txt:

0 1 2 3 4 5 6

Kolejny program czyta liczby z pliku:

#include <stdio.h>
int main ()
{
    FILE * p;
    int liczba;
    p = fopen("test.txt", "r");
    while ( 1 )
    {
        int x = scanf("%d", &liczba);
        if ( x == EOF )
            return 0;
        printf("Przeczytano: %d\n", liczba);
    }
}

Efekt działania programu, to:

Przeczytano: 0
Przeczytano: 1
Przeczytano: 2
Przeczytano: 3
Przeczytano: 4
Przeczytano: 5
Przeczytano: 6
Koniec pliku

Oba programy można połączyć w jeden: najpierw zapisze do pliku, plik zamknie, otworzy w trybie do odczytu i przeczyta dane.

#include <stdio.h>
int main  ()
{
    FILE * p;
    p = fopen("test.txt", "w");
    if ( p == NULL )
        return 2;
    for ( int i = 0; i < 7; i++ )
        fprintf(p, "%d ", i);
    fclose(p);
    int liczba;
    p = fopen("test.txt", "r");
    if ( p == 0 )
        return 2;
    while ( 1 )
    {
        int x = fscanf(p, "%d", &liczba);
        if ( x == EOF )
        {
            printf("Koniec pliku\n");
            return 0;
        }
        printf("Przeczytano: %d\n", liczba);
    }
    return 0;
}

Można też zrezygnować z zamykania pliku. Otworzymy plik w trybie w+ czyli odczytu i zapisu; najpierw wykonywane będzie pisanie:

#include <stdio.h>
int main()
{
    FILE * p;
    p = fopen("test.txt", "w+");
    if ( p == NULL )
        return 2;
    for ( int i = 0; i < 7; i++ )
        fprintf(p, "%d ", i);
    rewind(p);
    int liczba;
    while ( 1 )
    {
        int x = fscanf(p, "%d", &liczba);
        if ( x == EOF )
        {
            printf("Koniec pliku\n");
            fclose(p);
            return 0;
        }
        printf("Przeczytano: %d\n", liczba);
    }
    return 0;
}

Funkcja rewind() „przewija”2 plik na początek.

Sytuację „koniec pliku” podczas wprowadzania z terminala można zasymulować naciskając równocześnie dwa klawisze na początku linii tekstu:

  • Ctrl D (linux),
  • Ctrl Z (Windows).

Tak więc program powinien również sprawdzać, czy strumień danych nie zakończył się.

Czy zawsze trzeba otwierać plik?

W bardzo wielu prostych aplikacjach nie ma potrzeby korzystania z funkcji dostępu do plików na dysku (fopen(), fscanf(), fprintf(),… fclose()). Czytanie ze standardowego wejścia i pisanie na standardowe wyjście czasami może wystarczyć.

Poniżej prosty program kopiujący zawartość strumienia wejściowego do wyjściowego:

#include <stdio.h>
int main(int argc, char **argv)
{
    int znak;
    while ( ( znak = getchar() ) != EOF )
        printf("%c", znak);
    return 0;
}

Zmienna znak jest typu int bo taki jest prototyp funkcji int getchar( void );

Załóżmy, że program nazywa się kopiuj to możemy uruchomić go tak:

./kopiuj < plik_zrodlowy > plik>docelowy 

żeby skopiować plik, albo tak:

./kopiuj < plik_zrodlowy

żeby wypisać jego zawartość na ekranie. Poniższy program podaje długość pliku w bajtach:

#include <stdio.h>
int main(int argc, char **argv)
{
    int n = 0;
    int znak;
    while ( ( znak = getchar() ) != EOF )
        n++;
    printf("Liczba znakow: %d\n", n);
    return 0;
}

Uruchamia się go bardzo podobnie:

./znaki < znaki.c

lub

./znaki < znaki

W pierwszym przypadku liczba znaków to 167, a w drugim 16744. Mogę to zprawdzić używając polecenia ls -l znaki*

-rwxr-xr-x 1 myszka myszka 16744 maj 10 08:38 znaki
-rw-r--r-- 1 myszka myszka   167 maj 10 08:42 znaki.c

Informacja w kolumnie tuż przed datą to długość pliku.


  1. Jest to funkcja z tej samej grupy co fprintf() czy fscanf() i fopen()↩︎

  2. Funkcja na pamiątkę tych (starych) czasów kiedy powszechnie używano taśm magnetycznych. Po zapisaniu danych, żeby je odczytać trzeba było przewinąć taśmę na początek. ↩︎

Poprzedni