Ορίσματα Προγράμματος, Δείκτες σε Συναρτήσεις


Δείκτες σε Δείκτες

Η έννοια του δείκτη σε δείκτη έχει ήδη συζητηθεί. Ο δείκτης σε δείκτη μπορεί να είναι οποιουδήποτε τύπου. Για παράδειγμα:

char ch; /* a character */
char *pch; /* a pointer to a character */
char **ppch; /* a pointer to a pointer to a character */

Οι δηλώσεις φαίνονται στο Σχήμα 1. Εκεί βλέπουμε οτι ο δείκτης **ppch περιέχει τη διεύθυνση του δείκτη *pch ο οποίος περιέχει τη διεύθυνση της μεταβλητής ch. Τι σημαίνει αυτό πρακτικά;

 

Σχήμα 1 Δείκτες σε Δείκτες

Θυμηθείτε οτι η δήλωση char * συνήθως δείχνει όχι μόνο σε έναν απλό χαρακτήρα αλλά σε μια συμβολοσειρά (πίνακας χαρακτήρων που τερματίζεται με NULL, \0). Έτσι, συνήθως δείκτης **ppch είναι δείκτης σε μια συμβολοσειρά  *pch (Σχήμα 2)

 

Σχήμα 2 Δείκτης σε Συμβολοσειρά 

Αν προχωρήσουμε ένα βήμα ακόμη μπορούμε να έχουμε δείκτη σε πίνακα συμβολοσειρών. To ppch δείχνει στο pch[0] το οποίο είναι η διεύθυνση της μηδενικής συμβολοσειράς, το ppch+1 δείχνει στο pch[1] το οποίο είναι η διεύθυνση της πρώτης συμβολοσειράς, ενώ το ppch+4 στο σχήμα μας είναι NULL, \0.  (Σχήμα 3)

 

Σχήμα 3 Δείκτης σε Πίνακα Συμβολοσειρών.

Μπορούμε να αναφερθούμε στις ξεχωριστές συμβολοσειρές και ως ppch[0], ppch[1], ... Ισοδύναμα μπορούμε να δηλώσουμε τον πίνακα συμβολοσειρών ως char *ppch[].

Αυτή η δήλωση είναι συνηθισμένη στον χειρισμό  της εισόδου από τη γραμμή εντολών.

Ορίσματα Προγράμματος

Η C επιτρέπει την εισαγωγή ορισμάτων από τη γραμμή εντολών, τα οποία μπορούν να χρησιμοποιηθούν από τα προγράμματά μας. 

Τα ορίσματα εισάγονται στη γραμμή εντολών αμέσως μετά το όνομα του προγράμματος, πρίν πιέσουμε το πλήκτρο ENTER. Χωρίζονται μεταξύ τους με κενά, ακριβώς όπως τα ορίσματα των εντολών και σεναρίου φλοιού UNIX/Linux.

Για παράδειγμα στη γραμμή 

   gcc -o prog prog.c

το gcc είναι το όνομα του προγράμματος, ενώ τα -o prog prog.c είναι τα ορίσματα.

Για να μπορέσουμε να χρησιμοποιήσουμε τα ορίσματα πρέπει να δηλώσουμε τη συνάρτηση main() ως εξής:

   main(int argc, char **argv) ή  main(int argc, char *argv[])

Ένα παράδειγμα προγράμματος:


#include<stdio.h>
 
main (int argc, char **argv)
{ /* program to print arguments from command line */
 
  int i;
 
  printf("argc = %d\n\n",argc);
for (i=0; i<argc; ++i)
printf("argv[%d]: %s\n", i, argv[i]);
}

Έστω οτι το μεταγλωττίζουμε το παραπάνω πρόγραμμα ως args.  'Ετσι, αν γράψουμε στη γραμμή εντολών:

args f1 "f2" f3 4 stop!

The output would be:

argc = 6
 
argv[0] = args
argv[1] = f1
argv[2] = f2
argv[3] = f3
argv[4] = 4
argv[5] = stop!
Σημείωση:

Ακολουθεί ένα απόσπασμα τυπικού ελέγχου ορισμάτων στη C.

int main(int argc, char *argv[])
{
  ...
  if (argc != 3) {
    /* Wrong number of arguments, it should be 3 plus the programm name */
    printf("usage: %s number-of-execs sbrk-size job-name\n", argv[0]);
    exit(1);

  ...
  for (c = 0; c <=argc; c++) {
    if (argc[c] == 'F') {
        F_FLAG=1
  ...

Το παρακάτω παράδειγμα επιδεικνύει μια πιο ρεαλιστική χρήση των ορισμάτων, για την δήλωση των ονομάτων των αρχείων που θα επεξεργαστούμε. H λεπτομερής συζήτηση της διαχείρισης αρχείων, καθώς και τα θέματα σφαλμάτων συζητούνται σε επόμενο κεφάλαιο. Εδώ απλά σημειώνουμε τη χρήση των συναρτήσεων fopen() και fclose() για το άνοιγμα και κλείσιμο αρχείων, καθώς και της συνάτησης exit() για τον επιτυχή ή ανεπιτυχή τερματισμό του προγράμματος.

#include <stdio.h>
#include <stdilb.h>

main(argc, argv)
int argc;
char **argv;
{
int c;
FILE *from, *to;

/*
* Check our arguments.
*/
if (argc != 3) {
fprintf(stderr, "Usage: %s from-file to-file\n", *argv);
exit(1);
}

/*
* Open the from-file for reading.
*/
if ((from = fopen(argv[1], "r")) == NULL) {
perror(argv[1]);
exit(1);
}

/*
* Open the to-file for appending. If to-file does
* not exist, fopen will create it.
*/
if ((to = fopen(argv[2], "a")) == NULL) {
perror(argv[2]);
exit(1);
}

/*
* Now read characters from from-file until we
* hit end-of-file, and put them onto to-file.
*/
while ((c = getc(from)) != EOF)
putc(c, to);

/*
* Now close the files.
*/
fclose(from);
fclose(to);
exit(0);
}

Η επεξεργασία της γραμμής εντολών από το φλοιό του UNIX/Linux είναι ένα τυπικό παράδειγμα χρήσης των ορισμάτων σε προγράμματα. Μια απλή σχετικά μέθοδος επεξεργασίας των ορισμάτων, πέρα από τα μηνύματα αριθμού ορισμάτων που είδαμε παραπάνω, είναι ένας συνδυασμός επανάληψης και επιλογής. Το παρακάτω πρόγραμμα δέχεται τέσσερα ορίμσματα που ξεκινούν με "-" και, ανάλογα με το γράμμα που ακολουθεί το "-", ο αριθμός που εισάγεται εκλαμβάνεται ως ακέραιος, πραγματικός, συμβολοσειρά ή δύο διακριτοί ακέραιοι. Οι συναρτήσεις atoi(), atof() (ASCII to Integer, ASCII to Float) συζητούνται αργότερα αλλά η χρήση τους είναι προφανής.
#include < stdio.h>
#include < stdlib.h>

main(int argc, char** argv)
{
/* Set defaults for all parameters: */

int a_value = 0;
float b_value = 0.0;
char* c_value = NULL;
int d1_value = 0, d2_value = 0;

int i;

/* Start at i = 1 to skip the command name. */

for (i = 1; i < argc; i++) {

/* Check for a switch (leading "-"). */

if (argv[i][0] == '-') {

/* Use the next character to decide what to do. */

switch (argv[i][1]) {

case 'a': a_value = atoi(argv[++i]);
break;

case 'b': b_value = atof(argv[++i]);
break;

case 'c': c_value = argv[++i];
break;

case 'd': d1_value = atoi(argv[++i]);
d2_value = atoi(argv[++i]);
break;
default: printf("Unknown switch %s\n", argv[i]);

}
}
}

printf("a = %d\n", a_value);
printf("b = %f\n", b_value);
if (c_value != NULL) printf("c = \"%s\"\n", c_value);
printf("d1 = %d, d2 = %d\n", d1_value, d2_value);
}
Αν το πρόγραμμα λέγεται test μια τυπική γραμμή εντολών θα μπορούσε να είναι:

test
-a 3 -b 5.6 -c "I am a string" -d 222 111

Αυτού του τύπου η επεξεργασία μπορεί να γίνει ιδιαίτερα πολύπλοκη, αν σκεφτούμε οτι, π.χ. στις εντολές του UNIX/Linux έχουμε μεταβλητό αριθμό ορισμάτων, μεταβλητή διάταξη ορισμάτων, διάκριση ορισμάτων και επιλογών, επιλογές σύντομες (π.χ. -h) και πλήρεις (π.χ. --help). Σε αυτές τις περιπτώσεις η επεξεργασία ορισμάτων διευκολύνεται με τη χρήση της συνάρτησης πρότυπης βιβλιοθήκης C getopt() (οι πρότυπες βιβλιοθήκες της C εισάγονται αργότερα στις σημειώσεις, εδώ απλά θεωρούμε οτι έχουμε μια εξωτερικά ορισμένη συνάρτηση).  

Η σύνταξη της συνάρτησης, που ορίζεται στη βιβλιοθήκη unistd.h, είναι η εξής

int getopt (int argc, char **argv, const char *options)

H getopt() επεξεργάζεται με τη σειρά, ένα-ένα μόνο τα ορίσματα της γραμμής εντολών που αποτελούν επιλογές. Επιστρέφει το πρώτο χαρακτήρα κάθε ορίσματος που ακολουθεί το σύμβολο "-". Η παράμετρος options καθορίζει ποιές επιλογές είναι νόμιμες και αν απαιτούν επιπλέον ορίσματα. Τυπικά η getopt() καλείται σε ένα βρόχο. Όταν η getopt επιστρέφει -1, σημαίνει οτι δεν υπάρχουν ορίσματα  και ο βρόχος τερματίζει.

Το παράδειγμα που ακολουθεί ορίζει τρείς επιλογές, a,b,c από τις οποίες η c απαιτεί επιπλέον όρισμα, γιατί στη παράμετρο options ακολουθεί ":" μετά το c. Οι προ-δηλωμένες μεταβλητές optopt και optarg κρατούν την πιο πρόσφατη επιλογή που διάβασε η getopt() καθώς και το αντίστοιχο όρισμα. Η συνάρτηση isprint() ελέγχει αν ο χαρακτήρας είναι εκτυπώσιμος. 
     #include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main (int argc, char **argv)
{
int aflag = 0;
int bflag = 0;
char *cvalue = NULL;
int index;
int c;

opterr = 0;

while ((c = getopt (argc, argv, "abc:")) != -1)
switch (c)
{
case 'a':
aflag = 1;
break;
case 'b':
bflag = 1;
break;
case 'c':
cvalue = optarg;
break;
case '?':
if (optopt == 'c')
fprintf (stderr, "Option -%c requires an argument.\n", optopt);
else if (isprint (optopt))
fprintf (stderr, "Unknown option `-%c'.\n", optopt);
else
fprintf (stderr,"Unknown option character `\\x%x'.\n", optopt);
return 1;
default:
abort ();
}

printf ("aflag = %d, bflag = %d, cvalue = %s\n", aflag, bflag, cvalue);

for (index = optind; index < argc; index++)
printf ("Non-option argument %s\n", argv[index]);
return 0;
}

Ακολουθούν μερικά παραδείγματα εισόδου στη γραμμή εντολών, με όνομα προγράμματος testopt.

     % testopt
aflag = 0, bflag = 0, cvalue = (null)

% testopt -a -b
aflag = 1, bflag = 1, cvalue = (null)

% testopt -ab
aflag = 1, bflag = 1, cvalue = (null)

% testopt -c foo
aflag = 0, bflag = 0, cvalue = foo

% testopt -cfoo
aflag = 0, bflag = 0, cvalue = foo

% testopt arg1
aflag = 0, bflag = 0, cvalue = (null)
Non-option argument arg1

% testopt -a arg1
aflag = 1, bflag = 0, cvalue = (null)
Non-option argument arg1

% testopt -c foo arg1
aflag = 0, bflag = 0, cvalue = foo
Non-option argument arg1

% testopt -a -- -b
aflag = 1, bflag = 0, cvalue = (null)
Non-option argument -b

% testopt -a -
aflag = 1, bflag = 0, cvalue = (null)
Non-option argument -
Αντίστοιχα υπάρχει και συνάρτηση getopt_long() που επιπλέον εξεσφαλίζει διαχείριση επιλογές πλήρων λέξεων (πχ --help).

Εκτός από τα ορίσματα (και τις επιλογές) της γραμμής εντολών, ένα πρόγραμμα C μπορεί να έχει πρόσβαση και στις τιμές των μεταβλητών περιβάλλοντος του φλοιού UNIX/Linux, μέσα από τον οποίο εκτελείται το συγκεκριμένο πρόγραμμα. Σε αυτή τη περίπτωση η συνάρτηση main() συντάσσεται ως εξής: 

int main (int argc, char *argv[], char *envp[])

όπου env είναι ο δείκτης στο πίνακα των μεταβλητών περιβάλλοντος του φλοιού. Επιπλέον η πρότυπη βιβλιοθήκη stdlib.h παρέχει μια σειρά ορισμών και συναρτήσεων για την επικοινωνία με το περιβάλλον. Έτσι, ο προδηλωμένος δείκτης char **environ δείχνει ακριβώς όπου και ο δείκτης char *envp[], ενώ οι συναρτήσεις getenev(), putenv(), setenv(), unsetenv(), clearenv() επιτρέπουν την ανάγνωση, εγγραφή, προσθήκη, διαγραφή, καθαρισμό μεταβλητών περιβαλλοντος, αντίστοιχα.

Δείκτες σε Συναρτήσεις 

Ο δείκτης σε συνάρτηση είναι μια από τις πιο δύσκολες έννοιες στη C. Oυσιαστικά ο δείκτης κρατά τη διεύθυνση εισόδου (entry point) του εκτελέσιμου στιγμιότυπου της συνάρτησης. Η χρήση τους δεν είναι πολύ συνηθισμένη σε πρoγράμματα εφαρμογών, όμως είναι αρκετά κοινή σε βιβλιοθήκες λογισμικού. Εκεί συνήθως οι δείκτες σε συναρτήσεις εμφανίζονται ως παράμετροι σε κλήσεις άλλων συναρτήσεων. 

Αυτή η τεχνική είναι ιδιαίτερα χρήσιμη όταν έχουμε εναλλακτικές συναρτήσεις που μπορεί να χρησιμοποιηθούν για να εκτελέσουν παρόμοιες εργασίες σε δεδομένα. Τότε μπορούμε να περάσουμε τα δεδομένα και τη συνάρτηση που θα χρησιμοποιήσουμε ως παραμέτρους στη κλήση μιας συνάρτησης ελέγχου(ή οδήγησης). Σύντομα θα δούμε δύο τέτοια παραδείγματα συναρτήσεων ταξινόμησης (qsort) και αναζήτησης (bsearch) . Εύκολα μπορείτε να ενσωματώσετε μια δική σας συνάρτηση μέσω της συνάρτησης ελέγχου.

Η δήλωση δείκτη σε συνάρτηση είναι ως εξής:

int (*pf) ();

Αυτή η δήλωση απλά κρατά μια θέση δείκτη *pf σε μια συνάρτηση που επιστρέφει ένα int. Ακόμη δεν δείχνεται κάποια συγκεκριμένη συνάρτηση.

Αν έχουμε μια συνάρτηση int f() απλά γράφουμε:

pf = &f;

Για την καλύτερη λειτουργία της προτυποποίησης έιναι καλό οι δηλώσεις να έχουν πιο συγκεκριμένη:

int f(int);
int (*pf) (int) = &f;

Τώρα η f() επιστρέφει έναν int και δέχεται ένα int ως παράμετρο.

Με αυτή τη δήλωση τα παρακάτω είναι ισοδύναμα:

ans = f(5);
ans = pf(5);

Ακολουθεί να πληρέστερο παράδειγμα δήλωσης και χρήσης δείκτη σε συνάρτηση.

#include <stdio.h>
void my_int_func(int x)
{
printf( "%d\n", x );
}


int main()
{
/* pointer to function declaration */
void (*foo)(int);

/* pointer to function initialisation */
foo = &my_int_func;

/* call my_int_func (note that you do not need to write (*foo)(2) ) */
foo( 2 );

/* but if you want to, you may */
(*foo)( 2 );

return 0;
}


Η πρότυπη συνάρτηση βιβλιοθήκης qsort() είναι πολύ χρήσιμη, γιατί είναι σχεδιασμένη να ταξινομεί ένα πίνακα  βάσει ενός κλειδιού οποιουδήποτε τύπου αρκεί τα στοιχεία του πίνακα να είναι οποιουδήποτε (συγκεκριμένου, ενιαίου) τύπου.

Η συνάρτηση qsort() ορίζεται στη πρότυπη βιβλιοθήκη stdlib.h:

void qsort(void *base, size_t num_elements, size_t element_size, int (*compare)(void const *, void  const *));

Η παράμετρος base δείχνει στον πίνακα προς ταξινόμηση. Το num_elements ορίζει το μέγεθος του πίνακα (σε bytes) --συνήθως το size_t προκαθορίζεται σε int. To element_size ορίζει το μέγεθος κάθε στοιχείου του πίνακα (σε bytes). Η τελική παράμετρος compare είναι δείκτης σε μια συνάρτηση, που δέχεται δύο παραμέτρους -- δείκτες στα κλειδιά με γενικό τύπο (void) και επιστρέφει ένα int

H συνάρτηση qsort() καλεί τη συνάρτηση compare() η οποία πρέπει να οριστεί από το χρήστη, για να ορίσει το τρόπο σύγκρισης των στοιχείων του πίνακα. Σημειώστε οτι η qsort() διατηρεί την ανεξαρτησία της από τον τύπο δεδομένων του πίνακα αναθέτοντας την ευθύνη της τελικής σύγκρισης στο χρήστη. Ο int που επιστρέφει η compare() τυπικά ακολουθεί παρακάτω σύμβαση (για αύξουσα ταξινόμηση):

μικρότερος του μηδενός
: αν η τιμή της πρώτης παραμέτρου είναι μικρότερη της δεύτερης
ίσος με το μηδέν
: αν η τιμή της πρώτης παραμέτρου είναι ίση με αυτή της δεύτερης
μεγαλύτερος του μηδενός
: αν η τιμή της πρώτης παραμέτρου είναι μεγαλύτερη της δεύτερης

Με αυτό το τρόπο μπορούμε να ταξινομήσουμε αρκετά σύνθετες δομές δεδομένων. Για παράδειγμα, για να ταξινομήσουμε ένα πίνακα με στοιχεία δομής Record και κλειδί int:

typedef struct {
int key;
struct other_data;
} Record;

Μπορούμε να γράψουμε μια συνάρτηση σύγκρισης, recordcompare:

int recordcompare(void const *a, void  const *b) {  
return ( ((Record *)a)->key - ((Record *)b)->key );
}

Έστω οτι έχουμε ένα πίνακα array με στοιχεία Record και μέγεθος πίνακα arraylength. Μπορούμε να καλέσουμε τη συνάρτηση qsort() ως εξής:

qsort(array, arraylength, sizeof(Record), recordcompare);

Ακολουθεί ένα πληρέστερο παράδειγμα χρήσης της qsort() για ταξινόμηση πίνακα ακεραίων.

#include <stdlib.h>

int int_sorter( const void *first_arg, const void *second_arg )
{
int first = *(int*)first_arg;
int second = *(int*)second_arg;
if ( first < second )
{
return -1;
}
else if ( first == second )
{
return 0;
}
else
{
return 1;
}
}

int main()
{
int array[10];
int i;
/* fill array */
for ( i = 0; i < 10; ++i )
{
array[ i ] = 10 - i;
}
qsort( array, 10 , sizeof( int ), int_sorter );
for ( i = 0; i < 10; ++i )
{
printf ( "%d\n" ,array[ i ] );
}

}

Σε επόμενα κεφάλαια θα δούμε άλλες εφαρμογές των δεικτών σε συναρτήσεις.

Ασκήσεις

Άσκηση 12476

Γράψτε ένα πρόγραμμα με όνομα last που εμφανίζει τις τελευταίες n γραμμές ενός αρχείου κειμένου που δίνεται ως όρισμα στη γραμμή εντολών, δηλαδή

last textfile

Ως προεπιλογή το n είναι 5, αλλά το πρόγραμμα επιτρέπει την αλλαγή του n μέσω μια επιλογής της μορφής 

last -n 10 textfile

Άσκηση 12477

Γράψτε ένα πρόγραμμα που να ταξινομεί ένα πίνακα ακεραίων σε αύξουσα ή φθίνουσα σειρά, ανάλογα με την επιλογή που θα δεχτεί από τη γραμμή εντολών. 

Άσκηση 12478

Γράψτε ένα πρόγραμμα που διαβάζει ένα πίνακα εγγραφών 

typedef struct {
char keyword[10];
int other_data;
} Record;

και τον ταξινομεί με βάση το keyword με χρήση της qsort

Άσκηση 12479

Γράψτε μια συνάρτηση με όνομα insort() που να υλοποιεί τη ταξινόμηση εισαγωγής και να έχει ίδια προτυποοίηση όπως η qsort() και να επιτρέπει τη ταξινόμηση οποιουδήποτε τύπου δεδομένων.


Dave Marshall
1/5/1999
μετάφραση και προσαρμογή στα Ελληνικά Κ.Γ. Μαργαρίτης
28/3/2008