Ένα βασικό γνώρισμα της γλώσσας C είναι η χρήση δεικτών. Οι δείκτες είναι μεταβλητές που περιέχουν την διεύθυνση μιας άλλης μεταβλητής. Έχουμε δει μέχρι στιγμής ότι οι μεταβλητές που ορίζουμε περιέχουν τιμές που εκφράζουν δεδομένα κάποιου συγκεκριμένου τύπου π.χ. ακεραίου, χαρακτήρα κ.τ.λ. Οι δείκτες διαφέρουν από τις κοινές μεταβλητές στο γεγονός ότι οι τιμές των δεκτών δεν έχουν κάποια άμεση αξία για τον προγραμματιστή διότι δείχνουν τις θέσεις της μνήμης του υπολογιστικού συστήματος στις οποίες βρίσκονται αποθηκευμένες κοινές μεταβλητές.
Οι δείκτες όπως όλες οι μεταβλητές ορίζονται στην αρχή του προγράμματος ή στην συνάρτηση που τους χρησιμοποιεί. Οι δείκτες χρησιμοποιούν όλους τους γνωστούς κανόνες για τον ορισμό του ονόματος τους και τη δήλωση πριν την χρήση τους. Μπορούν να οριστούν για οποιαδήποτε τύπο δεδομένων, σχηματίζοντας δείκτες ακεραίων, δείκτες χαρακτήρων κ.τ.λ.
Ο ορισμός μιας μεταβλητής δείκτη γίνεται με τον προσδιορισμό του ονόματος του δείκτη και του τύπου δεδομένων στο οποίο «δείχνει». Για παράδειγμα αν η μεταβλητή pvar είναι ένας δείκτης ακεραίου, μπορεί να οριστεί ως εξής:
int *pvar;
Ο ορισμός αυτός διευκρινίζει ότι στο πρόγραμμα θα χρησιμοποιηθεί ένας δείκτης με όνομα pvar που το περιεχόμενο της διεύθυνσης μνήμης στην οποία δείχνει γράφεται ως εξής *pvar
Πριν χρησιμοποιήσουμε ένα δείκτη θα πρέπει να του εκχωρήσουμε την διεύθυνση του δεδομένου που δείχνει. Αν θέλουμε να εκχωρήσουμε σε ένα δείκτη την διεύθυνση μιας απλής μεταβλητής, η διεύθυνση της μεταβλητής αποδίδεται με τον συνδυασμό του χαρακτήρα & (τελεστή διεύθυνσης) μπροστά από την μεταβλητή. Για παράδειγμα:
int a=10;
int *p;
p = &a;
Εδώ ορίσαμε μια μεταβλητή τύπου int με αρχική τιμή 10. Στην συνέχεια ορίσαμε ένα δείκτη p σε τύπο int . Η έκφραση &a αποδίδει την διεύθυνση της μεταβλητής a όπως βρίσκεται στην μνήμη, και εκχωρείται στον δείκτη με την εντολή p = &a; Η τιμή της μεταβλητής a μπορεί να δοθεί και με ένα άλλο ισοδύναμο τρόπο χρησιμοποιώντας το δείκτη, με την έκφραση *p (όπου το σύμβολο * είναι ο τελεστής περιεχομένου διεύθυνσης) Δηλαδή η τιμή της μεταβλητής a είναι ίδια με την τιμή που εκφράζεται με *p.
Παράδειγμα: Για να μάθουμε περισσότερα για τους δείκτες, ας προεκτείνουμε τον κώδικα του παραπάνω παραδείγματος:
(1) #include <avr/io.h>
(2) int main(void)
(3) {
(4) int a=10;
(5) int *p;
(6) p=&a;
(7) PORTA = *p;
(8) a=25;
(9) PORTA = *p;
(10) *p = 50;
(11) PORTA = a;
(12) (*p)++;
(13) PORTA=a;
(14) }
Στη γραμμή (4) δηλώνεται μια ακέραια μεταβλητή a και της αποδίδουμε αρχική τιμή το 10. Στη γραμμή (5) δηλώνεται ένας δείκτης σε ακέραιο. Στη γραμμή (6) αρχικοποιούμε τον δείκτη με την διεύθυνση της μεταβλητής a. Στην γραμμή (7) οδηγούμε την τιμή *p που δείχνει ο δείκτης p στη θύρα PORTA.
Στη γραμμή (8) δίνουμε τη νέα τιμή 25 στη μεταβλητή a. Στη γραμμή (9) οδηγούμε την τιμή *p (δηλ. την τιμή 25) την οποία δείχνει ο δείκτης p, στην θύρα PORTA. Στη γραμμή (10) το *p παίρνει την τιμή 50. Στην γραμμή (11) οδηγούμε το περιεχόμενο της μεταβλητής α, στην θύρα PORTA. Η PORTA παίρνει τώρα την τιμή 50, αφού οι τιμές των a και *p είναι ταυτόσημες.
Στη γραμμή (12) αυξάνεται κατά ένα το περιεχόμενο *p που δείχνει ο δείκτης p, στην νέα τιμή 51 και με την εντολή της γραμμής (13) εκχωρείται στην PORTA, αφού οι τιμές των a και *p είναι ίσες.
Σχέση Δεικτών και πινάκων
Στον προγραμματισμό με τη γλώσσα C οι δείκτες και οι πίνακες είναι δυο στενά συνδεμένες έννοιες. Μπορούμε να δουλέψουμε με πίνακες χρησιμοποιώντας δείκτες αλλά και να αναφερόμαστε σε δείκτες με τον ίδιο τρόπο που αναφερόμαστε σε στοιχεία πινάκων. Στην πραγματικότητα η γλώσσα C αντιμετωπίζει τους δείκτες και τους πίνακες σαν δύο όψεις του ίδιου νομίσματος. Για παράδειγμα, ας υποθέσουμε ότι ορίζουμε ένα πίνακα με την εντολή:
int year[5] = {2000, 2005, 2010, 2015, 2020};
Όπως είδαμε η εντολή αυτή ορίζει και αποδίδει αρχικές τιμές στον ακέραιο πίνακα year[ ] πέντε στοιχείων. Οι τιμές των στοιχείων τοποθετούνται σε διαδοχικές θέσεις μνήμης, όπως φαίνεται παρακάτω:

Παρατηρήστε ότι αφού το όνομα όλων των στοιχείων του πίνακα είναι το ίδιο, δεν χρειάζεται να το επαναλαμβάνουμε κάθε φορά που αναφερόμαστε σε κάποιο από τα στοιχεία αυτά. Με βάση την παρατήρηση αυτή, η γλώσσα C χρησιμοποιεί το όνομα του πίνακα σαν δείκτη του πρώτου στοιχείου και πραγματοποιεί την πρόσβαση κάθε άλλου στοιχείου προσδιορίζοντας την απόσταση του απ’ αυτό. Δηλαδή:

Βλέπουμε δηλαδή ότι το όνομα ενός πίνακα είναι συνώνυμο με τη θέση (διεύθυνση) του πρώτου στοιχείου (year[0]) και ότι η πρόσβαση στα υπόλοιπα στοιχεία γίνεται με πρόσθεση ενός ακέραιου αριθμού, δηλαδή ισχύουν οι ισοδυναμίες:
year[0] ↔ *(year)
year[1] ↔ *(year+1)
year[2] ↔ *(year+2)
year[3] ↔ *(year+3)
year[4] ↔ *(year+4)
Έτσι μπορούμε να επεξεργαζόμαστε τους πίνακες με δυο τρόπους: με τον κλασικό συμβολισμό των στοιχείων του πίνακα (π.χ. year[i]) ή με τη χρήση δεικτών (π.χ. *(year+i)) .
Πρέπει να προσέξουμε την λεγόμενη αριθμητική δεικτών. Δηλαδή όταν προσθέτουμε σε ένα δείκτη μια ακέραια τιμή για παράδειγμα ας πάρουμε year+i τα περιεχόμενα του δείκτη year δεν αυξάνονται κατά i bytes, αλλά τόσα bytes όσος είναι το γινόμενο της ακέραιας τιμής i με το μέγεθος του τύπου του δείκτη.
Λόγω της δυαδικότητας των πινάκων και δεικτών είναι σωστό να γράψουμε:
int array[10];
int *ptr;
ptr = array;
όπου δίνουμε ουσιαστικά την διεύθυνση του πρώτου στοιχείου του πίνακα array[ ] σαν τιμή της μεταβλητής δείκτη ptr το ίδιο αποτέλεσμα πετυχαίνουμε με την εντολή:
ptr = &array[0];
Σημειώστε όμως ότι τα ονόματα των πινάκων δεν μπορούν να τοποθετηθούν από την αριστερή μεριά μιας εντολής απόδοσης τιμής διότι δεν αποτελούν μεταβλητές δείκτη αλλά σταθερές δείκτη. Η παρατήρηση αυτή εξηγεί το λόγο γιατί ένας πίνακας δεν μπορεί να πάρει μια νέα τιμή μέσα στο πρόγραμμα παρά μόνο αλλάζοντας ένα προς ένα τα στοιχεία του.
Δείκτες σε Πίνακες Χαρακτήρων
Η ευελιξία που προσφέρουν οι δείκτες είναι ιδιαίτερα χρήσιμη στην επεξεργασία αλφαριθμητικών. Έχουμε δει σε άλλο άρθρο, ότι το αλφαριθμητικό ορίζεται σαν ειδικός πίνακας χαρακτήρων. Για παράδειγμα μπορούμε να έχουμε:
char name[ ] = “Nikolaos”;
Η γλώσσα C επιτρέπει να ορίσουμε ένα αλφαριθμητικό προσδιορίζοντας ένα δείκτη που να «δείχνει» την αρχή της. Δηλαδή:
char *pname = “Petros”;
Επειδή η τιμή ενός αλφαριθμητικού είναι η διεύθυνση του πρώτου χαρακτήρα του, αυτή η τιμή εκχωρείται στον δείκτη που δείχνει το αλφαριθμητικό.
Οι δυο ορισμοί είναι ουσιαστικά ίδιοι αφού πραγματοποιούν παρόμοιες εργασίες και μπορούν να χρησιμοποιηθούν με τον ίδιο τρόπο σε άλλες εντολές. Η μοναδική διαφορά μεταξύ των δυο ορισμών βρίσκεται στο ότι ενώ η τιμή του δείκτη name είναι σταθερή και αμετάβλητη (σταθερά δείκτη), η τιμή του δείκτη pname μπορεί να μεταβληθεί (μεταβλητή δείκτη). Αυτό σημαίνει ότι ενώ μετά τους παραπάνω ορισμούς μπορούμε να έχουμε μια εντολή της μορφής:
pname = “Georgios”;
Κάτι τέτοιο δεν επιτρέπεται με τον δείκτη name:
name[ ] = “Mihalis”;
Χρήση δεικτών στις συναρτήσεις
Έχουμε παρουσιάσει την έννοια της συνάρτησης σε προηγούμενο άρθρο. Είδαμε ότι μια συνάρτηση ορίζεται με τον τύπο της, το όνομα της, την λίστα των παραμέτρων και το σώμα του κώδικα της.
Όταν μια συνάρτηση καλεί μια άλλη, η καλούσα συνάρτηση μεταβιβάζει τα ορίσματα της στις παραμέτρους της καλούμενης συνάρτησης, που αποτελούν τοπικές μεταβλητές γι’ αυτήν. Η μεταβίβαση αυτή γίνεται με αντιγραφή των ορισμάτων στις παραμέτρους και έτσι με αυτό τον τρόπο κάθε αλλαγή των τοπικών μεταβλητών που αντιπροσωπεύουν τις παραμέτρους της καλούμενης συνάρτησης, αφήνουν ανεπηρέαστες τις μεταβλητές ορισμάτων της καλούσας συνάρτησης.
Όταν θέλουμε η καλούμενη συνάρτηση να μεταβιβάζει τιμές στην καλούσα συνάρτηση, μέσω της λίστας των παραμέτρων, χρησιμοποιούμε την έννοια των δεικτών. Σε αυτή την μέθοδο η καλούσα συνάρτηση μεταβιβάζει διευθύνσεις μεταβλητών στην καλούμενη συνάρτηση και έτσι η τελευταία μπορεί να εκχωρεί τιμές στις διευθύνσεις που τις μεταβιβάζονται, χρησιμοποιώντας την έννοια των δεικτών. Ας δούμε ένα παράδειγμα:
#include <avr/io.h>
void calc_sum(uint8_t x, uint8_t y, uint8_t *s)
{
*s = x+y;
return;
}
int main(void)
{
DDRA=0x00;
DDRB=0x00;
DDRC=0xFF;
uint8_t a, b, sum=0;
a=PINA;
b=PINB;
calc_sum(a, b, &sum);
PORTC = sum;
}
Σε αυτό το παράδειγμα ορίζουμε μια συνάρτηση calc_sum( ) που στην λίστα παραμέτρων της έχουμε δηλώσει ένα δείκτη σε ακέραιο και συγκεκριμένα με την έκφραση int *s . Όταν την καλούμε μέσα από την συνάρτηση main( ) , στο αντίστοιχο όρισμα περνάμε την διεύθυνση της μεταβλητής sum, δηλαδή την &sum. Έτσι με αυτό τον τρόπο, η καλούμενη συνάρτηση μπορεί να δώσει τιμή στη sum γράφοντας στην διεύθυνση &sum, όπως λειτουργεί ένας δείκτης.