Tablice — deklaracja, reprezentacja wewnętrzna
Tablica jest zmienną złożoną z elementów tego samego typu. Obejmuje ona ciągły obszar pamięci operacyjnej dokładnie tak duży, aby zmieścić wszystkie jej elementy. Deklaracja tablicy — zmiennej tablicowej:
typ_elemetu nazwa_tablicy[ <wyrażenie_ stałe> ]
wyrażenie_ stałe — wyrażenie określające liczbę elementów tablicy, wartość tego wyrażenia musi być znana na etapie kompilacji.
Dziesięcioelementowa tablica liczb całkowitych
Różne warianty deklaracji C C++
int tab[ 10 ]; poprawne poprawne
#define N 10 . . .
int tab[ N ];
poprawne poprawne
const int N = 10;
. . .
int tab[ N ]
niepoprawne poprawne
Uwaga ð Kwalifikator typu const może wystąpić z każdą specyfikacją typu.
Zmienna z kwalifikatorem const powinna być zainicjowana ale potem nie może zmieniać wartości.
Uwaga ð Zmienna z kwalifikatorem const w języku C nie jest traktowana jako stała i nie może być wykorzystywana do określania rozmiaru tablicy.
W języku C/C++ rzadko spotyka się deklaracje typów tablicowych — nie ma to wiele wspólnego z deklaracją typów np. w języku Pascal, i jest mało użyteczne.
10 elementów
0 1 2 3 4 5 6 7 8 9
tab
Odwołania do elementów tablicy:
tab[ 0 ] = 1; tab[ N - 1 ] = 5; a = 2*tab[3]; int i = 0, j = N – 1;
a = tab[ i ] + tab [ j ];
Uwaga ð W języku C i C++ nie ma żadnych wbudowanych mechanizmów zabezpieczających przed odwoływaniem się do „elementów” leżących poza zakresem indeksowym tablic.
0 1 2 3 4 5 6 7 8 9
tab 10 11 12
? ? 10
Obszar poza zakresem tablicy ! tab[ 12 ] = 10;
Tablice — typowe operacje
Iteracja for jest najczęściej stosowaną iteracją do przetwarzania tablic.
Przykład 1 — ustawianie wartości wszystkich elementów tablicy, np. zerowanie:
int i;
. . .
for( i = 0; i < N; tab[ i++ ] = 0 ) ;
Przykład 2 — wczytywanie danych do tablicy:
int i;
char linia[ 80 ];
. . .
for( i = 0; i < N; i++ ) {
printf( ”\n>” );
gets( linia );
tab[ i ] = atoi( linia );
}
lub nieco krócej:
int i;
char linia[ 80 ];
. . .
for( i = 0; i < N; printf( ”\n>” ), tab[ i++ ] = atoi( gets( linia ) ) ) ;
Przykład 3 — wyprowadzanie zawartości tablicy do stout:
int i;
char linia[ 80 ];
. . .
for( i = 0; i < N; printf( ”\n%d”, tab[ i++ ] ) ) ;
Przykład 4 — sumowanie liczb zapisanych w tablicy:
int i, suma = 0;
char linia[ 80 ];
. . .
for( i = 0; i < N; i++ ) suma = suma + tab[ i ];
lub nieco krócej:
int i, suma;
char linia[ 80 ];
. . .
for( i = 0, suma = 0; i < N; suma += tab[ i++ ] ) ;
Przykład 5 — suma co drugiego elementu podzielnego przez 3:
int i, suma;
char linia[ 80 ];
. . .
for( i = 0, suma = 0; i < N; i+= 2 )
suma += ( tab[ i ] % 3 == 0 ) ? tab[ i ] : 0; /* Niby sprytne, ale ... */
Przykład 5 — cd., inna wersja — niby mniej sprytna a szybsza:
int i, suma;
char linia[ 80 ];
. . .
for( i = 0, suma = 0; i < N; i+= 2 ) if( tab[ i ] % 3 == 0 )
suma += tab[ i ];
Uwaga na takie konstrukcje:
suma = tab[ i++ ] + tab[ i ]; tab[ ++i ] = a * ++i;
Tablice wolno inicjalizować na etapie deklaracji:
int tab[ 10 ] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int dni_miesiecy[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
float przychody[ 12 ] = { 0, 0, 0 }; /* Za mało wartości początkowych */
• Jeżeli liczba wartości początkowych jest mniejsza od rozmiaru tablicy, to brakujące elementy otrzymują wartość zero (zmienne zewnętrzne, statyczne i automatyczne). Podanie zbyt wielu wartości początkowych jest błędem.
• Nie ma sposobu na zainicjowanie środkowego elementu bez podania wszystkich wartości pośrednich.
Kopiowanie zawartości tablic
Nie wolno przypisywać całych tablic. Musi to być wykonane poprzez przekopiowanie zawartości tablicy element po elemencie.
#define N 5
int a[] = { 1, 2, 3, 4, 5 };
int b[ N ];
int i;
. . .
b = a; /* <---- Nigdy tak! */
. . .
for( i = 0; i < N; i++ ) /* Niestety trzeba tak */
b[ i ] = a[ i ];
Czasem jest wygodnie napisać funkcję kopiującą:
void copyinttable( int d[], int s[], int n ) /* Jak z tymi parametrami? */
{ /* Niby tak najprościej ... */
int i = 0;
for( i = 0; i < n; i++ ) d[ i ] = s[ i ];
}
/* A może jednak tak */
{
while( --n >= 0 ) d[ n ] = s[ n ];
}
Choć może lepiej wykorzystać funkcję memmove lub memcpy (nagłówek mem.h, zgodne z ANSI C):
memmove( b, a, N * sizeof( int ) ); memmove( b, a, N * sizeof( a[ 0 ] ) );
memcpy( b, a, N * sizeof( int ) ); memcpy( b, a, N * sizeof( a[ 0 ] ) );
A funkcję memset można wykorzystać do wyzerowania tablicy:
memset( b, 0, 5 * sizeof( a[ 0 ] ) );
Tablice znaków — deklaracja, reprezentacja wewnętrzna
Do reprezentacji łańcuchów znakowych w języku C wykorzystuje się tablice znakowe. W języku C przyjęto koncepcję łańcuchów ze znacznikiem końca (ang.
null terminated strings).
Tablice znakowe można inicjować w zwykły sposób, przewidziany dla tablic:
#define N 80
char imie[ N ] = {’A’, ’g’, ’a’};
char imie[] = {’A’, ’g’, ’a’, ’\0’}; /* !!! */
choć można wykorzystywać wygodniejszą formę:
char imie[ N ] = ”Aga”;
Uwaga ð powyższe przypisanie wystąpić może jedynie przy definicji zmiennej!
Reprezentacja wewnętrzna:
0 1 2 3
A g a \0
Litera³ ³añcuchowy :
Zmienna imie :
0 1 2 3 4 5 78
A g a \0 . . .
79
Deklaracja łańcucha zainicjowanego napisem pustym:
char imie[ N ] = {’\0’}; lub char imie[ N ] = ””;
i jego reprezentacja wewnętrzna:
Zmienna imie:
0 1 2 3 4 5 78
\0 . . .
79
imie[ 0 ] = ’\0’; /* Najprostszy sposób uczynienia łańcucha pustym */
Tablice znaków — typowe operacje
Przetwarzanie tablic polega zwykle na „przemaszerowaniu” zmienna indeksową po tablicy, dopóki nie ma końca napisu oznaczanego znakiem ’\0’:
int i;
char s[ N ];
. . .
for( i = 0; s[ i ] != ’\0’; i++ )
< tu jakieś operacje na każdym znaku s[ i ] >
Wyprowadzanie napisu do stdout znak po znaku:
void put_string( char s[] ) {
int i;
for( i = 0; s[ i ] != ’\0’; putchar( s[ i++ ] ) ) ;
}
Przetwarzanie tablic — string.h
Do manipulowania tablicami znakowymi opracowano szereg funkcji bibliotecznych (plik nagłówkowy string.h) ... większość z nich można łatwo napisać samemu!
Przykład: Gdy trzeba znać liczbę znaków w łańcuchu, używamy funkcji strlen.
Jej rezultatem jest liczba znaków napisu, przekazanego tej funkcji parametrem.
#define N 80
char imie[N] = “Aga“;
. . .
printf( “\nLiczba znaków w łańcuchu:%s wynosi:%d“, imie, strlen( imie ) );
Dwie możliwe implementacje funkcji strlen int strlen( char s[] )
{
int len = 0;
while( s[ len ] != '\0' ) len++;
return len;
}
int strlen( char s[] ) {
int len;
for( len = 0; s[ len ] != '\0'; len++ ) ;
return len;
}/* ... ale można jeszcze prościej ;-) */
Przykładowe funkcje operujące na tablicach znakowych:
char a[ N ] = ”Ala”;
char b[ N ];
char C[ N ];
/* Przepisanie zawartości a do b, teraz w b to samo co w a: ”Ala” */
strcpy( b, a );
/* Tak samo należy przepisywać zawartość literałów łańcuchowych */
strcpy( c, ” ma kota” );
/* Można również tak ”zerować” tablicę znakową” */
strcpy( b, ”” ); /* Ale szybciej jest tak: b[ 0 ] = ’\0’ */
/* Łączenie (sklejanie) napisów. Zawartość c zostaje doklejona do a : ”Ala ma kota”” */
strcat( a, c );
/* Porównywanie łańcuchów znakowych realizuje funkcja strcmp */
printf( ”\nŁańcuchy znaków %s i % s ”, a, b ) if( strcmp( a, c )== 0 )
printf( ”są jednakowe” );
else
printf( ”są różne” );
/* Porównywanie łańcuchów znakowych bez uwzględnienia wielkości liter, funkcja stricmp */
strcpy( a, ”ALA” );
strcpy( b, ”ala” );
printf( ”\nŁańcuchy znaków %s i % s ”, a, b ) if( stricmp( a, b ) == 0 )
printf( ” są jednakowe” );
else
printf( ” są różne” );
Przykładowe funkcje operujące na tablicach znakowych, cd.:
strcpy( a, ”ALA” );
strcpy( b, ”ala” );
/* Konwersja - małe litery na duże: strupr, duże litery na małe: strlwr */
strlwr( a ); /* Po wywołaniu strlwr zmienna a zawiera napis ”ala” */
strupr( b ); /* Po wywołaniu strupr zmienna a zawiera napis ”ALA” */
Zaraz, zaraz tutaj coś jest nie tak ... przecież w języku C parametry przekazywane są przez wartość. Dlaczego „wnętrze” funkcji modyfikuje parametr aktualny wywołania funkcji? Czy tablice są przekazywane inaczej niż parametry innych typów?
Przykładowe implementacje funkcji strupr i strlwr void strupr( char s[] )
{
int i;
for( i = 0; s[ i ] != '\0'; i++ ) s[ i ] = toupper( s[ i ] );
}
void strlwr( char s[] ) {
int i;
for( i = 0; s[ i ] != '\0'; i++ ) s[ i ] = tolower( s[ i ] );
}
Przykładowe implementacje funkcji strcpy void strcpy( char d[], char s[] )
{
int i;
for( i = 0; s[ i ] != '\0'; i++ ) d[ i ] = s[ i ];
d[ i ] = ’\0’;
}
void strcpy( char d[], char s[] ) {
int i = 0;
while( ( d[ i ] = s[ i ] ) != ‘\0’ ) i++;
}
Jak to działa?
char s1[ 80 ] = "Język C";
char s2[ 20 ];
. . .
strcpy( s2, s1 );
0 1 2 3 4 5
J ê z y k . . .
79
C \0
6
0 1 2 3 4 5
J ê z y k . . .
19
C \0
6
i++ . . .
s1
s2 s
d
void strcpy( char d[], char s[] ) {
int i;
for( i = 0; s[ i ] != '\0'; i++ ) d[ i ] = s[ i ];
d[ i ] = '\0';
}
Przykładowa implementacje funkcji strcat void strcat( char d[], char s[] )
{
int i = 0, j = 0;
while( d[ i ] != ’\0’ ) i++;
while( ( d[ i++ ] = s[ j++ ] ) != ’\0’ ) ;
}
Co się stanie, gdy tablica docelowa jest za krótka?
void main() {
char s1[ 5 ] = "AAAA";
char c1 = 'A';
char c2 = 'B';
char s2[ 5 ] = "BBBB";
strcpy( s2, "XXXXXXXXXXXXXXXXXXXX" );
printf( "\ns1 : %s\nc1 : %c\nc2 : %c\ns2 : %s", s1, c1, c2, s2 );
}
Wyniki działania programu:
Gdzie oryginalna zawartość tablicy s1?
Co się stało ze zmienną c2?
Dlaczego zmienna c1 jest OK?
Jak się ustrzec przed przekroczeniem zakresu tablic?
Nigdy nie należy zakładać, że „się uda”, czyli że tablica docelowa jest wystarczająco długa. Należy szacować, przewidywać, jeszcze raz przewidywać i programować defensywnie.
Biblioteka funkcji operujących na tablicach znaków zawiera funkcje wykonujące operacje analogiczne do przedstawionych uprzednio, pozwalające na kontrolę liczby znaków biorących udział np. w kopiowaniu. Są to np. funkcje: strncpy, strncat, strnset.
#define N 10
#define M 80
char s1[ N ];
char s2[ M ] = "Język C jest świetny lecz pełen pułapek";
strncpy( s1, s2, N - 1 );
s1[ N - 1 ] = '\0'; /* strncpy nie zawsze przekopiuje ’\0’!!! */
strncpy( s1, s2, sizeof( s1 ) - 1 );
s1[ sizeof( s1 ) - 1 ] = ‘\0’;
puts( s1 ); /* strncpy nie zawsze przekopiuje ’\0’!!! */