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: 169
scanf()
dostaje adres początku tablicya
. Wprowadzany tekstalamakota
(10 bajtów) wpisywany jest do kolejnych komórek pamięci wypełniają tablicęa
(litery:alam
),b
(akot
) ic
(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.