Helsingin yliopisto Tietojenkäsittelytieteen laitos
 

Tietojenkäsittelytieteen laitos

Tietoa laitoksesta:

 
Helsingin yliopisto / Tietojenkäsittelytieteen laitos / Copyright © 2000 Jan Lindström. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.

58127-1 C-ohjelmointi : 7 Osoittimet


  • 7 Osoittimet

    7 Osoittimet

    Aloittelijoiden ongelmat pointtereihin näyttävät liittyvän ongelmaan ymmärtää mitä ovat muuttujat. Siksi esitys aloitetaan yleiskuvauksella C-kielen muuttujista.

    Ohjelmassa oleva muuttuja on nimetty alkio, jonka arvo voi vaihdella. Kääntäjä ja linkkeri varaa tietyn määrän muistia, jossa kyseisen muuttujan arvoa säilytetään. Varatun muistin koko riippuu muuttujan tyypistä.

    Muuttujaa määritellessä kääntäjälle ilmoitetaan kaksi asiaa: muuttujan nimi ja tyyppi. Esimerkiksi: määritellään k nimien muuttuja, jonka tyyppi on integer:

        int k;
    
    Kääntäjä varaa kyseiselle muuttujalle järjestelmästä riippuvan tilan muistia, jossa muuttujan arvoa säilytetään. Kääntäjä muodostaa symbolitaulua, johon symboli k lisätään ja merkitään missä osoitteessa muuttujan arvoa säilytetään.

    Jos myöhemmin kirjoitamme:

        k = 2;
    
    odotamme, että ajonaikaisesti lausetta suoritettaessa arvo 2 asetetaan siihen muistiosoitteeseen, joka on varattu k muuttujan arvon talletukseen.

    Tavallaan muuttujaan k on liitetty kaksi arvoa. Toinen on varsinainen muuttujan arvo (eli 2 yllä olevassa esimerkissä) ja toinen on muistiosoite, jossa muuttujan arvoa säilytetään. Kurssikirjassa näistä käytetään nimiä rvalue (right value, äännetään "are value") ja lvalue (left value, äännetään "el value"). Kurssikirjan K&R (sivu 197) määritelmä lvalue termille on: "An object is a named region of storage; an lvalue is an expression referring to an object." eli vapaasti suomennettuna: "olio on nimetty muistialue: lvalue on ilmaisu, joka viittaa olioon". Esimerkiksi:

       int j, k;
    
       k = 2;
       j = 7;    <-- rivi 1
       k = j;    <-- rivi 2
    
    Yllä kääntäjä tulkitsee rivin 1 j:n j-muuttujan osoitteeksi (lvalue) ja luo kopin arvosta 7 kyseiseen osoitteeseen. Rivillä kaksi taas kääntäjä tulkitsee muuttujan j rvalueksi (koska muuttuja on sijoitusoperaattorin oikealla puolella). Siis tässä j viittaa j muuttujalle varatun muistialueen sisältämään arvoon (eli arvoon 7). Arvo 7 kopioidaan k-muuttujalle varattuun muistialueseen (lvalue).

    Oletetaan, että haluamme muuttujan säilyttävän lvalue arvon (siis osoitteen). Kyseisen muuttujan koko riippuu arkkitehtuurista. Tällaista muuttujaa kutsutaan osoitinmuuttujaksi (pointer). C-kielessä osoitinmuuttuja määritellään kirjoittamalla muuttujanimen eteen tähtimerkin. Osoitinmuuttujalla pitää myös olla tyyppi, joka kertoo minkä tyyppistä tietoa osoittimen osoittamassa muistipaikassa säilytetään. Esimerkiksi:

       int *ptr;
    
    ptr on muuttujan nimi. '*' kertoo kääntäjälle, että kyseessä on osoitinmuuttuja. Kääntäjä siis varaa tarvittavan määrän muistitilaa osoitteet tallennukseen. Tyyppi int kertoo kääntäjälle, että osoitinmuuttuja tulee viittaamaan muistialueeseen, jossa tallennetaan int tyyppinen muuttuja.

    Osoitinmuuttuja voi osoittaa myös nk. tyhjään eli NULL osoitteeseen. NULL vakion arvo ei välttämättä kaikissa järjestelmissä ole 0. Eli osoitteen järkevyyttä kannattaa testata lauseella if ( ptr == NULL ).

    Tallennetaanpa osoitinmuuttujan ptr arvoksi muuttujan k osoite. Tämä tapahtuu unaarisella operaattorilla &. Eli:

        ptr = &k;
    
    Huomaa, että & operaattori hakee k:n osoitteen (lvalue) vaikka k on sijoituksen oikealla puolella. Muuttujan k osoite kopioidaan osoitinmuuttujan ptr arvoksi. Nyt sanotaan, että ptr osoittaa k:hon.

    Epäsuoran osoituksen operaattori on tähtimerkki ja sitä käytetään seuraavasti:

        *ptr = 8;
    
    Tällöin kopioidaan arvo 8 ptr:n osoittamaan osoitteeseen. Eli jos ptr osoittaa (sisältää muuttujan k osoitteen), yllä oleva lause asettaa muuttujan k arvoksi 8. Eli käytettäessä *-merkkiä viitataan arvoon, johon osoitinmuuttuja osoittaa ei osoitinmuuttujan arvoon. Voitaisiin myös kirjoittaa:
     printf("%d\n",*ptr);
    

    Otetaanpa hieman pidempi esimerkki:

    
    #include <stdio.h>
    
    int j, k;
    int *ptr;
    
    int main(void)
    {
        j = 1;
        k = 2;
        ptr = &k;
        printf("\n");
        printf("j has the value %d and is stored at %p\n", j, (void *)&j);
        printf("k has the value %d and is stored at %p\n", k, (void *)&k);
        printf("ptr has the value %p and is stored at %p\n", ptr, (void *)&ptr);
        printf("The value of the integer pointed to by ptr is %d\n", *ptr);
    
        return 0;
    }
    

    7.1 Osoitintyypit ja taulukot

    Mietitäänpä miksi on tarvetta tunnistaa muuttujan tyyppi, johon osoitinmuuttuja osoittaa. Yksi selkeä syy on, jos myöhemmin tehdään sijoitus:

         int *ptr;
         *ptr = 2;
    
    nyt kääntäjä tietää kuinka monta tavua kopioidaan osoitinmuuttujan osoittamaan paikkaan. Mutta määrittelemällä osoitinmuuttujalle tyyppi saadaan aikaan muitakin mielenkiintoisia toimintoja. Oletetaan, että olemme määritelleet 10 alkioisen taulukon ja asetetaan ptr osoittamaan ensimmäiseen alkioon. Oletetaan, että ensimmäinen alkio sijaitsee muistiosoitteessa 100. Nyt voidaan kirjoittaa:
        ptr + 1;
    
    Kääntäjä tietää että ptr on osoitin, joka osoittaa int tyyppiseen muuttujaan. Tästä kääntäjä osaa muodostaa koodia, joka kasvattaa ptr osoitinmuuttujan arvoa int muuttujan koolla ( ei siis 1:llä vaan esim 4:lla), jolloin ptr osoittaakin taulukon toiseen alkioon.

    Vastaavasti voisi tietysti käyttää muotoja ++ptr ja ptr++. Nyt näemme kiinnostavan yhteyden taulukoiden ja osoitinmuuttujien välillä. Esimerkiksi:

        int my_array[] = {1,23,17,4,-5,100};
        int *ptr;
        ptr = &my_array[0];       /* point our pointer at the first
                                     integer in our array */ 
    
    Nyt taulukon arvot voitaisiin tulostaa joko indeksoinnilla tai epäsuoralla osoituksella käyttäen osoitinmuuttujaa. Esimerkiksi:

    #include <stdio.h>
    
    int my_array[] = {1,23,17,4,-5,100};
    int *ptr;
    
    int main(void)
    {
        int i;
        ptr = &my_array[0];     /* point our pointer to the first
                                          element of the array */
        printf("\n\n");
        for (i = 0; i < 6; i++)
        {
          printf("my_array[%d] = %d   ",i,my_array[i]);   /*<-- A */
          printf("ptr + %d = %d\n",i, *(ptr + i));        /*<-- B */
        }
        return 0;
    }
    
    Testaappa kyseistä ohjelmaa ja huomaa, että rivit A ja B tulostavat samat arvot. Tämän jälkeen muuta rivi B muotoon:

        printf("ptr + %d = %d\n",i, *ptr++);
    
    ja testaa uudestaa. Muuta rivi B jälleen muotoon:

        printf("ptr + %d = %d\n",i, *(++ptr));
    
    ja testaa uudestaa. Jokaisella kerralla pyri ennustamaan tulos ja huolellisesti tutki todellinen tulos. Jos osoitin halutaan osoittamaan taulukon ensimmäiseen alkioon, niin seuraavat kaksi riviä tekevät saman asian:

        ptr = &my_array[0]; /* Vaihtoehto 1 */
        ptr = my_array;     /* Vaihtoehto 2 */
    
    Huomaa seuraavasta esimerkistä, että osoittimen arvoksi voi sijoittaa taulukon, mutta taulukon arvoksi ei voi sijoittaa osoitinta:

        ptr = my_array; /* Sallittua */
        my_array = ptr; /* Virhe!!!!!*/
    
    Siis ptr on muuttuja, mutta my_array on vakio. Siis taulukon my_array ensimmäisen alkion sijaintia ei pysty muuttamaan taulukon esittelyn jälkeen. Mietitäänpä vielä kerran lvalue termin määritelmää:

    "An object is a named region of storage; an lvalue is an expression referring to an object".

    Tämä nostaa esille mielenkiintoisen ongelman. Koska my_array on nimetty muistialue (region of storage), niin miksi my_array ei ole yllä olevassa sijoituksessa lvalue ? Tätä perustellaan sillä, että taulukon nimi on vakio-osoitin.

    Otetaan esille jo aikaisemmin esimerkissä ilmaantunut void * ilmaisu. Tähän mennessä on nähty osoittimia kokonaislukuihin (int) ja merkkeihin (char). Myöhemmin tulemme vielä käsittelemään osoittimia rakenteisiin ja osoittimia osoittimiin. Lisäksi muistamme, että osoitinmuuttujan koko saattaa vaihdella eri järjestelmissä. Osoitinmuuttujan koko voi vaihdella myös käytetyn tyypin perusteella. Siksi saataa tulla ongelmia jos yrittää sijoittaa long integer tyyppistä muuttujaa short integer tyyppiseen muuttujaa. Vastaavasti ongelmia saattaa tulla myös osoitinmuuttujien sijoittamisessa.

    Tämän ongelman minimoimiseksi C-kieli tarjoaa osoitinmuuttujan, jonka tyyppi on void. Tällaisen muuttujan voi määritellä kirjoittamalla:

    void *vptr;
    
    Siis void osoitin on eräänlainen geneerinen osoitin. Palaamme asiaan tarkemmin myöhemmin.


    7.2 Osoittimet ja Merkkijonot

    Osoittimen tutkiminen selventää osoittimen ja taulukoiden yhteyttä. Lisäksi voimme katsoa, kuinka C-standardit mielenkiintoiset funktiot voidaan toteuttaa lyhyesti (ei välttämättä aina selkeästi). Lisäksi näemme kuinka osoittimia voi välittää funktiolle. Esimerkki merkkijonosta:

    #include <stdio.h>
    
    char strA[80] = "A string to be used for demonstration purposes";
    char strB[80];
    
    int main(void)
    {
    
        char *pA;     /* a pointer to type character */
        char *pB;     /* another pointer to type character */
        puts(strA);   /* show string A */
        pA = strA;    /* point pA at string A */
        puts(pA);     /* show what pA is pointing to */
        pB = strB;    /* point pB at string B */
        putchar('\n');       /* move down one line on the screen */
        while(*pA != '\0')   /* line A (see text) */
        {
            *pB++ = *pA++;   /* line B (see text) */
        }
        *pB = '\0';          /* line C (see text) */
        puts(strB);          /* show strB on screen */
        return 0;
    }
    
    Rivillä A tarkistetaan osoittaako osoitinmuuttuja pA merkkijonon loppuun eli merkkiin '\0'; Rivillä B kopioidaan osoittimen pA osoittama arvo osoittimen pB osoittamaan tilaan ja tämän jälkeen kasvatetaan osoitinta pA ja pB osoittamaan seuraavaa alkiota taulukossa. Rivillä C lisätään pB osoittimen osoittamaan paikkaan merkkijonon loppumerkki.

    Kirjoitetaanpa oma strcpy funktio. Ensimmäinen versio saattaisi näyttää seuraavalta:

       char *my_strcpy(char *destination, char *source)
       {
           char *p = destination;
           while (*source != '\0')
           {
               *p++ = *source++;
           }
           *p = '\0';
           return destination;
       }   
    
    Funktiota käytettäisiin siis seuraavasti:

    #include <stdio.h>
    
    char strA[80] = "A string to be used for demonstration purposes";
    char strB[80];
    
    char *my_strcpy(char *destination, char *source);
    
    int main(void)
    {
        my_strcpy(strB, strA);
        puts(strB);
    }
    
    Koska vain parametrien arvot lähetetään, niin esimerkissä lähetetään vain merkkijonon ensimmäisen alkion osoite arvoparametrinä. Tämä oikeastaan tarkoittaa sitä, että source[i] on sama kuin *(p+i).

    Itseasiassa seuraavat kaksi lausetta ovat syntaksin mukaan mahdollisia:

        char a[10];
        a[3] = 'x';
        3[a] = 'x';
    
    Siis kun kirjoitamme:
        dest[i] = source[i];
    
    Niin voisimme yhtä hyvin kirjoittaa:
        *(dest + i) = *(source + i);
    
    Nyt osaamme siis kirjoittaa todella lyhyen version funktiosta strcpy:

    void strcpy(char *to, char *from)
    {
        while(*to++ = *from++ ); /* Pysähtyy jos ja vain jos *from = '\0' = 0 */
    }
    
    Ylimääräisenä harjoituksena kirjoita mahdollisimman lyhyt osoitinmuuttujia käyttävä versio funktiosta strcmp.


    7.3 Osoittimet ja Moniuloitteiset Taulukot

    Otetaanpa yksi esimerkki:

        char multi[5][10];
    
    Alleviivattu osuus on siis taulukon nimi. Tyyppi on paksunnettu eli char. Merkintä [10] kertoo, että merkkijonon pituus on 10 merkkiä. Merkintä [5] taas kertoo, että määritellään 5 alkion taulukon taulukoita, joissa jokaisessa on 10 merkkiä. Alustetaanpa tämä taulukko jollain datalla:

        multi[0] = {'0','1','2','3','4','5','6','7','8','9'}
        multi[1] = {'a','b','c','d','e','f','g','h','i','j'}
        multi[2] = {'A','B','C','D','E','F','G','H','I','J'}
        multi[3] = {'9','8','7','6','5','4','3','2','1','0'}
        multi[4] = {'J','I','H','G','F','E','D','C','B','A'}
    
    Yksittäiset merkit voitaisiin sijoittaa esimerkiksi lauseilla:
        multi[0][3] = '3';
        multi[1][7] = 'h';
        multi[4][0] = 'J';
    
    Koska taulukot ovat jatkuvassa muistissa, todellinen taulukon kuvaus muistissa olisi seuraavanlainen:

        0123456789abcdefghijABCDEFGHIJ9876543210JIHGFEDCBA
        |_____ taulukon alkuosoite eli &multi[0][0]
    
    Käyttäen hieman matematiikkaa näemme, että seuraavat lauseet viittaavat samaan alkioon taulukossa:

        *(*(multi + row) + col)
        multi[row][col]
    
    Otetaanpa hieman pidempi esimerkki:
    #include <stdio.h>
    #define ROWS 5
    #define COLS 10
    
    int multi[ROWS][COLS];
    
    int main(void)
    {
        int row, col;
        for (row = 0; row < ROWS; row++)
        {
            for (col = 0; col < COLS; col++)
            {
                multi[row][col] = row*col;
            }
        }
    
        for (row = 0; row < ROWS; row++)
        {
            for (col = 0; col < COLS; col++)
            {
                printf("\n%d  ",multi[row][col]);
                printf("%d ",*(*(multi + row) + col));
            }
        }
    
        return 0;
    }
    
    Osoitinmuuttujaa voidaan käyttää funktion parametrinä, eli esimerkiksi:

        int array[3] = {'1', '5', '7'};
        void a_func(int *p);
        /* Tosin tuon edellisen voi kirjoittaa myös muodossa : */
        void b_func(int p[]);
    

    7.4 Osoittimet ja Dynaamisen muistin varauminen

    C-kielisessä ohjelmassa on melko usein tilanteita, jolloin esimerkiksi taulukon lopullisen koon tietää vasta ajonaikaisesti. Tällöin taulukolle täytyy varata muistitila myös ajonaikaisesti. Tämä tapahtuu käyttämällä funktioita malloc(), calloc() tai muita varausfunktioita. Varattu muisti täytyy vain muistaa vapauttaa, kun sitä ei enää tarvita funktiolla free().

    Muistia varattaessa funktiolla kuten malloc() palautuu funktiosta osoitin. Esimerkiksi jos haluamme varata tilaa 10 kokonaisluvulle (int):

        int *iptr;
        iptr = (int *)malloc(10 * sizeof(int));
        if (iptr == NULL)
        { .. VIRHEENKÄSITTELYRUTIINI .. }
    
    Rakenteille (palataan myöhemmin), muuttujille ja yksiulotteisille taulukoille muistinvaraus on helppoa. Kuinka varataan muistia kaksiuloitteiselle taulukolle ?

    Tähän on useita tapoja. Tapa1:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int nrows = 5;     /* Both nrows and ncols could be evaluated */
        int ncols = 10;    /* or read in at run time */
        int row;
        int **rowptr;
        rowptr = malloc(nrows * sizeof(int *));
    
        if (rowptr == NULL)
        {
            puts("\nFailure to allocate room for row pointers.\n");
            exit(0);
        }
    
        printf("\n\n\nIndex   Pointer(hex)   Pointer(dec)   Diff.(dec)");
    
        for (row = 0; row < nrows; row++)
        {
            rowptr[row] = malloc(ncols * sizeof(int));
    
            if (rowptr[row] == NULL)
            {
                printf("\nFailure to allocate for row[%d]\n",row);
                exit(0);
            }
    
            printf("\n%d         %p         %d", row, rowptr[row], rowptr[row]);
    
            if (row > 0)
                printf("              %d",(int)(rowptr[row] - rowptr[row-1]));
        }
    
        return 0;
    }
    
    Käyttämällä tätä tapaa joudutaan tekemään seuraavat malloc kutsut:

        osoitintaulukkoon             1     kutsu
        riveille                      5     kutsua
                                     -----
        yhteensä                      6     kutsua
    
    Tapa2:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int **rptr;
        int *aptr;
        int *testptr;
        int k;
        int nrows = 5;     /* Both nrows and ncols could be evaluated */
        int ncols = 8;    /* or read in at run time */
        int row, col;
    
        /* Varataan muistia taulukolle */
    
        aptr = malloc(nrows * ncols * sizeof(int));
        if (aptr == NULL)
        {
            puts("\nFailure to allocate room for the array");
            exit(0);
        }
    
        /* Varataan tilaa riveillä oleville osoittimille */
    
        rptr = malloc(nrows * sizeof(int *));
        if (rptr == NULL)
        {
            puts("\nFailure to allocate room for pointers");
            exit(0);
        }
    
        /* Asetetaan osoittimet paikoilleen */
    
        for (k = 0; k < nrows; k++)
        {
            rptr[k] = aptr + (k * ncols);
        }
    
        /* Now we illustrate how the row pointers are incremented */
    
        printf("\n\nIllustrating how row pointers are incremented");
        printf("\n\nIndex   Pointer(hex)  Diff.(dec)");
    
        for (row = 0; row < nrows; row++)
        {
            printf("\n%d         %p", row, rptr[row]);
            if (row > 0)
                printf("              %d",(rptr[row] - rptr[row-1]));
        }
        printf("\n\nAnd now we print out the array\n");
        for (row = 0; row < nrows; row++)
        {
            for (col = 0; col < ncols; col++)
            {
                rptr[row][col] = row + col;
                printf("%d ", rptr[row][col]);
            }
            putchar('\n');
        }
    
        puts("\n");
    
        /* and here we illustrate that we are, in fact, dealing with
           a 2 dimensional array in a contiguous block of memory. */
        printf("And now we demonstrate that they are contiguous in memory\n");
    
        testptr = aptr;
        for (row = 0; row < nrows; row++)
        {
            for (col = 0; col < ncols; col++)
            {
                printf("%d ", *(testptr++));
            }
            putchar('\n');
        }
    
        return 0;
    }
    
    Mietitäänpä lopuksi miten varattaisiin tilaa 3-uloitteiselle taulukolle:
    #include <stdio.h>
    #include <stdlib.h>
    #include <stddef.h>
    
    int X_DIM=16;
    int Y_DIM=5;
    int Z_DIM=3;
    
    int main(void)
    {
        char *space;
        char ***Arr3D;
        int y, z;
        ptrdiff_t diff;
    
        /* first we set aside space for the array itself */
    
        space = malloc(X_DIM * Y_DIM * Z_DIM * sizeof(char));
    
        /* next we allocate space of an array of pointers, each
           to eventually point to the first element of a
           2 dimensional array of pointers to pointers */
    
        Arr3D = malloc(Z_DIM * sizeof(char **));
    
        /* and for each of these we assign a pointer to a newly
           allocated array of pointers to a row */
    
        for (z = 0; z < Z_DIM; z++)
        {
            Arr3D[z] = malloc(Y_DIM * sizeof(char *));
    
            /* and for each space in this array we put a pointer to
               the first element of each row in the array space
               originally allocated */
    
            for (y = 0; y < Y_DIM; y++)
            {
                Arr3D[z][y] = space + (z*(X_DIM * Y_DIM) + y*X_DIM);
            }
        }
    
        /* And, now we check each address in our 3D array to see if
           the indexing of the Arr3d pointer leads through in a
           continuous manner */
    
        for (z = 0; z < Z_DIM; z++)
        {
            printf("Location of array %d is %p\n", z, *Arr3D[z]);
            for ( y = 0; y < Y_DIM; y++)
            {
                printf("  Array %d and Row %d starts at %p", z, y, Arr3D[z][y]);
                diff = Arr3D[z][y] - space;
                printf("    diff = %d  ",diff);
                printf(" z = %d  y = %d\n", z, y);
            }
        }
        return 0;
    }
    

    7.5 Osoittimet Funktioihin

    Oletetaan että tehtävänä on kirjoittaa funktio, joka osaa lajitella mitä tahansa alkioita taulukossa järjestykseen. Funktio voisi siis lajitella merkkijonoja, kokonaislukuja ja jopa rakenteita. Lajittelualgoritmi voi olla sama kaikille. Esimerkkialgoritminä käytetään kuplalajittelua (buble sort).

    Kokonaislukuja lajitteleva kuplalajittelu voidaan kirjoittaa seuraavasti:

    #include <stdio.h>
    
    int arr[10] = { 3,6,1,2,3,8,4,1,7,2};
    
    void bubble(int a[], int N);
    
    int main(void)
    {
        int i;
        putchar('\n');
        for (i = 0; i < 10; i++)
        {
            printf("%d ", arr[i]);
        }
        bubble(arr,10);
        putchar('\n');
    
        for (i = 0; i < 10; i++)
        {
            printf("%d ", arr[i]);
        }
        return 0;
    }
    
    void bubble(int a[], int N)
    {
        int i, j, t;
        for (i = N-1; i >= 0; i--)
        {
            for (j = 1; j <= i; j++)
            {
                if (a[j-1] > a[j])
                {
                    t = a[j-1];
                    a[j-1] = a[j];
                    a[j] = t;
                }
            }
        }
    }
    
    Esimerkin funktio osaa lajitella vain kokonaislukutaulukoita. Siksi välimuuttujat ovat kokonaislukuja ja vertailu on kokonaisluvuille. tavoitteena olisi muuntaa tämä funktio sellaiseksi, ettei se riipu käytetystä tietotyypistä.

    Aluksi siirretään vertailuoperaatio omaan funktioon. Uudelleenkirjoittamalla sadaan tulokseksi:

    #include <stdio.h>
    
    int arr[10] = { 3,6,1,2,3,8,4,1,7,2};
    
    void bubble(int a[], int N);
    int compare(int m, int n);
    
    int main(void)
    {
        int i;
        putchar('\n');
        for (i = 0; i < 10; i++)
        {
            printf("%d ", arr[i]);
        }
        bubble(arr,10);
        putchar('\n');
    
        for (i = 0; i < 10; i++)
        {
            printf("%d ", arr[i]);
        }
        return 0;
    }
    
    void bubble(int a[], int N)
    {
        int i, j, t;
        for (i = N-1; i >= 0; i--)
        {
            for (j = 1; j <= i; j++)
            {
                if (compare(a[j-1], a[j]))
                {
                    t = a[j-1];
                    a[j-1] = a[j];
                    a[j] = t;
                }
            }
        }
    }
    
    int compare(int m, int n)
    {
        return (m > n);
    }
    
    Koska tietotyyppi haluttiin tyyppiriippumattomaksi, täytyy käyttää osoittimia ja void tyyppiä. Eli muunnetaan koodi muotoon:

    #include <stdio.h>
    
    int arr[10] = { 3,6,1,2,3,8,4,1,7,2};
    
    void bubble(int *p, int N);
    int compare(int *m, int *n);
    
    int main(void)
    {
        int i;
        putchar('\n');
    
        for (i = 0; i < 10; i++)
        {
            printf("%d ", arr[i]);
        }
        bubble(arr,10);
        putchar('\n');
    
        for (i = 0; i < 10; i++)
        {
            printf("%d ", arr[i]);
        }
        return 0;
    }
    
    void bubble(int *p, int N)
    {
        int i, j, t;
        for (i = N-1; i >= 0; i--)
        {
            for (j = 1; j <= i; j++)
            {
                if (compare(&p[j-1], &p[j]))
                {
                    t = p[j-1];
                    p[j-1] = p[j];
                    p[j] = t;
                }
            }
        }
    }
    
    int compare(int *m, int *n)
    {
        return (*m > *n);
    }
    
    Huomaa muutokset. buble() funktiolle välitetään osoitin kokonaislukuun (tai oikeammin kokonaislukutaulukko). Lisäksi funktion buble sisällä välitetään osoitin vertailtaviin elementteihin. Seuraavaksi muunnetaan osoittimet tyyppiin void:

    #include 
    
    int arr[10] = { 3,6,1,2,3,8,4,1,7,2};
    
    void bubble(int *p, int N);
    int compare(void *m, void *n);
    
    int main(void)
    {
        int i;
        putchar('\n');
    
        for (i = 0; i < 10; i++)
        {
            printf("%d ", arr[i]);
        }
        bubble(arr,10);
        putchar('\n');
    
        for (i = 0; i < 10; i++)
        {
            printf("%d ", arr[i]);
        }
        return 0;
    }
    
    void bubble(int *p, int N)
    {
        int i, j, t;
        for (i = N-1; i >= 0; i--)
        {
            for (j = 1; j <= i; j++)
            {
                if (compare((void *)&p[j-1], (void *)&p[j]))
                {
                    t = p[j-1];
                    p[j-1] = p[j];
                    p[j] = t;
                }
            }
        }
    }
    
    int compare(void *m, void *n)
    {
        int *m1, *n1;
        m1 = (int *)m;
        n1 = (int *)n;
        return (*m1 > *n1);
    }
    
    Tarvitsemme siis funktiossa compare() tyypinmuunnosta (cast). Nyt kiinnitetään huomiota mitä buble() funktiolle annetaan parametrille. Ensimmäinen parametri pitäisi vaihtaa tyyppiin void. Tämä vaatii muutoksia väliaikaismuuttujiin. Tämä voidaan tehdä seuraavasti:

    #include <stdio.h>
    #include <string.h>
    
    long arr[10] = { 3,6,1,2,3,8,4,1,7,2};
    
    void bubble(void *p, size_t width, int N);
    int compare(void *m, void *n);
    
    int main(void)
    {
        int i;
        putchar('\n');
    
        for (i = 0; i < 10; i++)
        {
            printf("%d ", arr[i]);
        }
        bubble(arr, sizeof(long), 10);
        putchar('\n');
    
        for (i = 0; i < 10; i++)
        {
            printf("%ld ", arr[i]);
        }
    
        return 0;
    }
    
    void bubble(void *p, size_t width, int N)
    {
        int i, j;
        unsigned char buf[4];
        unsigned char *bp = p;
    
        for (i = N-1; i >= 0; i--)
        {
            for (j = 1; j <= i; j++)
            {
                if (compare((void *)(bp + width*(j-1)),
                            (void *)(bp + j*width)))  /* 1 */
                {
    /*              t = p[j-1];   */
                    memcpy(buf, bp + width*(j-1), width);
    /*              p[j-1] = p[j];   */
                    memcpy(bp + width*(j-1), bp + j*width , width);
    /*              p[j] = t;   */
                    memcpy(bp + j*width, buf, width);
                }
            }
        }
    }
    
    int compare(void *m, void *n)
    {
        long *m1, *n1;
        m1 = (long *)m;
        n1 = (long *)n;
        return (*m1 > *n1);
    }
    
    Yllä vaihdettiin taulukon tyyppi int:stä long:ksi, jotta tarvittavat muutokset compare funktioon tulisi näkyviin. Tehdäänpä seuraavaksi tarvittavat muutokset, jotta voitaisiin lajitella merkkijonotaulukko:
    #include <stdio.h>
    #include <string.h>
    
    #define MAX_BUF 256
    
    char arr2[5][20] = {  "Mickey Mouse",
                          "Donald Duck",
                          "Minnie Mouse",
                          "Goofy",
                          "Ted Jensen" };
    
    void bubble(void *p, int width, int N);
    int compare(void *m, void *n);
    
    int main(void)
    {
        int i;
        putchar('\n');
    
        for (i = 0; i < 5; i++)
        {
            printf("%s\n", arr2[i]);
        }
        bubble(arr2, 20, 5);
        putchar('\n\n');
    
        for (i = 0; i < 5; i++)
        {
            printf("%s\n", arr2[i]);
        }
        return 0;
    }
    
    void bubble(void *p, int width, int N)
    {
        int i, j, k;
        unsigned char buf[MAX_BUF];
        unsigned char *bp = p;
    
        for (i = N-1; i >= 0; i--)
        {
            for (j = 1; j <= i; j++)
            {
              k = compare((void *)(bp + width*(j-1)), (void *)(bp + j*width));
              if (k > 0)
                {
                 memcpy(buf, bp + width*(j-1), width);
                 memcpy(bp + width*(j-1), bp + j*width , width);
                 memcpy(bp + j*width, buf, width);
                }
            }
        }
    }
    
    int compare(void *m, void *n)
    {
        char *m1 = m;
        char *n1 = n;
        return (strcmp(m1,n1));
    }
    
    Kuten huomaat varsinainen buble() funktio pysyi muuttumattomana. Aino lisätarve on välittää vertailufunktio parametrinä. Tähän tarvitaan osoitinmuuttujaa funktioon.

    Osoittimet funktioihin täytyy sopia osoittamiinsa funktioihin sekä paluutyyppi että argumenttien lukumäärä ja tyypit pitää olla samoja. Eli esimerkissämme funktio-osoitin määriteltäisiin lauseella:

       int (*fptr)(const void *p1, const void *p2);
    
    Huomaa, että jos kirjoitat:
        int *fptr(const void *p1, const void *p2);
    
    niin kyseessä on funktio, joka palauttaa osoitteen kokonaislukuun (int). Tämä johtuu siitä että sulkuoperaattorilla on korkeampi prioriteetti. Eli sulkuoperaattori evaluoidaan ennen * operaattoria.

    Voimme nyt määriteelä buble() funktion uudestaan lisäämällä neljännen argumentin, joka on osoite funktioon:

        void bubble(void *p, int width, int N,
                    int(*fptr)(const void *, const void *));
    
    Eli lopullisessa muodossaan esimerkki olisi:
    #include 
    #include 
    
    #define MAX_BUF 256
    
    long arr[10] = { 3,6,1,2,3,8,4,1,7,2};
    char arr2[5][20] = {  "Mickey Mouse",
                          "Donald Duck",
                          "Minnie Mouse",
                          "Goofy",
                          "Ted Jensen" };
    
    void bubble(void *p, int width, int N,
                int(*fptr)(const void *, const void *));
    int compare_string(const void *m, const void *n);
    int compare_long(const void *m, const void *n);
    
    int main(void)
    {
        int i;
        puts("\nBefore Sorting:\n");
    
        for (i = 0; i < 10; i++)               /* show the long ints */
        {
            printf("%ld ",arr[i]);
        }
        puts("\n");
    
        for (i = 0; i < 5; i++)                  /* show the strings */
        {
            printf("%s\n", arr2[i]);
        }
        bubble(arr, 4, 10, compare_long);          /* sort the longs */
        bubble(arr2, 20, 5, compare_string);     /* sort the strings */
        puts("\n\nAfter Sorting:\n");
    
        for (i = 0; i < 10; i++)             /* show the sorted longs */
        {
            printf("%d ",arr[i]);
        }
        puts("\n");
    
        for (i = 0; i < 5; i++)            /* show the sorted strings */
        {
            printf("%s\n", arr2[i]);
        }
        return 0;
    }
    
    void bubble(void *p, int width, int N,
                int(*fptr)(const void *, const void *))
    {
        int i, j, k;
        unsigned char buf[MAX_BUF];
        unsigned char *bp = p;
    
        for (i = N-1; i >= 0; i--)
        {
            for (j = 1; j <= i; j++)
            {
                k = fptr((void *)(bp + width*(j-1)), (void *)(bp + j*width));
                if (k > 0)
                {
                    memcpy(buf, bp + width*(j-1), width);
                    memcpy(bp + width*(j-1), bp + j*width , width);
                    memcpy(bp + j*width, buf, width);
                }
            }
        }
    }
    
    int compare_string(const void *m, const void *n)
    {
        char *m1 = (char *)m;
        char *n1 = (char *)n;
        return (strcmp(m1,n1));
    }
    
    int compare_long(const void *m, const void *n)
    {
        long *m1, *n1;
        m1 = (long *)m;
        n1 = (long *)n;
        return (*m1 > *n1);
    }
    


    Jan Lindström (Jan.Lindstrom@cs.Helsinki.FI)