User Tools

Site Tools


cpp:pointers

This is an old revision of the document!


Δείκτες

Οι δείκτες είναι μεταβλητές που προσδιορίζουν τιμές διευθύνσεων στη μνήμη αντί για τιμές μεταβλητών. Η δήλωση ενός δείκτη γίνεται ως εξής:

<type> *var_name;

Για παράδειγμα,

char *c_ptr;    // δείκτης που δείχνει σε μεταβλητές τύπου char
int *i_ptr;     // δείκτης που δείχνει σε μεταβλητές τύπου int
double *d_ptr;  // δείκτης που δείχνει σε μεταβλητές τύπου double
float *f_ptr;   // δείκτης που δείχνει σε μεταβλητές τύπου float

Αρχικοποίηση δεικτών

Η διεύθυνση μνήμης μίας μεταβλητής μπορεί να δοθεί με χρήση του τελεστή & (reference operator). Οποιοσδήποτε δείκτης μπορεί να αρχικοποιηθεί λαμβάνοντας την διεύθυνση μνήμης μίας μεταβλητής ως εξής:

char c = 'a'; char *c_ptr = &a;
int i = 5; int *i_ptr = &i;
double d = 3.14159; double *d_ptr = &d;
float f = 3.14159; float *f_ptr = &f;

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

char *c_ptr=0; ή char *c_ptr=nullptr;
int *i_ptr; ή char *i_ptr=nullptr;

Παράδειγμα αρχικοποίησης δεικτών

char foo = 'a';
char *foo_ptr = &foo;

Ο παραπάνω κώδικας αρχικοποίησης ενός δείκτη απεικονίζεται γραφικά για ένα σύστημα 32bit στο σχήμα που ακολουθεί. Η μεταβλητή foo_ptr αποθηκεύει τη διεύθυνση της μεταβλητής foo (0x55884421).

Λήψη του περιεχομένου της τιμής ενός δείκτη (pointer dereference)

Μπορείτε να χρησιμοποιήσετε την τιμή ενός δείκτη για να προσπελάσετε τη μεταβλητή στην οποία αυτός δείχνει με χρήση του τελεστή * (dereference operator) πριν από το όνομα του δείκτη ως εξής:

char foo = 'a';
char *foo_ptr = &foo;
char bar = *foo_ptr;

Η μεταβλητή bar αποθηκεύει το περιεχόμενο της διεύθυνσης που δείχνει η μεταβλητή foo_ptr, δηλ το περιεχόμενο της μεταβλητής foo που είναι ο χαρακτήρας 'a'.

  • Ο τελεστής * (dereference operator) όταν τίθεται μπροστά από μία μεταβλητή δείκτη επιστρέφει το περιεχόμενο της διεύθυνσης στην οποία δείχνει ο δείκτης.
  • Ο τελεστής & (address-of operator) επιστρέφει τη διεύθυνση στην οποία δείχνει μία μεταβλητή.

Αριθμητική δεικτών

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

char c='a'; short s=10; int i=100;
char *mychar = &c;
short *myshort = &s;
long *myint = &i;

Στο παραπάνω παράδειγμα η μεταβλητή mychar δείχνει σε δεδομένα μήκους ενός byte. Ας υποθέσουμε επίσης ότι η μεταβλητή myshort δείχνει σε δεδομένα μήκους 2 bytes (εξαρτάται από το hw) και η μεταβλητή myint δείχνει σε δεδομένα μήκους 4 bytes (εξαρτάται και πάλι από το hw).

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

mychar++;
myshort++;
myint++;

το αποτέλεσμα των παραπάνω πράξεως είναι το εξής:

  1. η μεταβλητή mychar μετακινείται κατά sizeof(char) (1 byte)
  2. η μεταβλητή myshort μετακινείται κατά sizeof(short) (2 bytes)
  3. η μεταβλητή myint μετακινείται κατά sizeof(int) (4 bytes)

Θα πρέπει να έχετε υπόψη ότι όλοι οι δείκτες καταλαμβάνουν τον ίδιο χώρο στη μνήμη (4-byte για συστήματα 32-bit και 8-byte για συστήματα 64-bit) ανεξάρτητα από τον τύπο δεδομένων στον οποίο δείχνουν.

Δείκτες και πίνακες

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

int myarray [5];
int *myarray_ptr = myarray;

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

myarray = myarray_ptr+1;

Δείτε το παρακάτω παράδειγμα όπου χρησιμοποιούνται πίνακας από ακεραίους και ένας δείκτης σε ακέραιο που δείχνει στις επιμέρους τιμές του πίνακα. Τα σχόλια περιέχουν τη θέση του πίνακα που προσπελάζεται κάθε φορά.

pointers-arrays.cpp
#include <iostream>
using namespace std;
 
int main ()
{
  int numbers[5];
  int * p;
  p = numbers;  *p = 10;          // numbers[0]
  p++;  *p = 20;                  // numbers[1]
  p = &numbers[2];  *p = 30;      // numbers[2]
  p = numbers + 3;  *p = 40;      // numbers[3]
  p = numbers;  *(p+4) = 50;      // numbers[4]
 
  for (int n=0; n<5; n++)
    cout << numbers[n] << ", ";
  cout << endl;
  return 0;
}

Οι δείκτες οφείλουν να δείχνουν σε διευθύνσεις μνήμης που ανήκουν στη διεργασία που εκτελείται. Στην πραγματικότητα όμως οι δείκτες μπορούν να πάρουν οποιαδήποτε τιμή ακόμη και τιμές εκτός του εύρους διευθύνσεων που έχουν ανατεθεί από το λειτουργικό σύστημα στην τρέχουσα διεργασία. Η ανάγνωση από διεύθυνση ή η εγγραφή σε διεύθυνση που δεν ανήκει στη διεργασία δημιουργεί σφάλμα που τερματίζει το πρόγραμμα. Δείτε το παρακάτω παράδειγμα όπου ένας δείκτης αρχικοποιείται σε τιμή εκτός των ορίων του πίνακα intArray.

intArrayPointer.cpp
#include <iostream>
using namespace std;
int main() {
  int intArray[5];
  int *ptr = intArray+10000;
  *ptr = 10;
}

Δείκτες αμετάβλητου περιεχομένου και δείκτες αμετάβλητης διεύθυνσης (const)

Είδαμε ότι οι δείκτες είναι μεταβλητές που περιέχουν διευθύνσεις μνήμης στις οποίες αποθηκεύονται άλλες μεταβλητές. Μέσω των δεικτών μπορούμε να διαβάσουμε και να γράψουμε το περιεχόμενο μία διεύθυνσης μνήμης. Υπάρχουν όμως περιπτώσεις που ένας δείκτης είναι επιθυμητό να διαβάζει μόνο τα περιεχόμενα των διευθύνσεων μνήμης στα οποία δείχνει χωρίς να μπορεί να τα μεταβάλλει. Σε αυτή την περίπτωση αρκεί να δηλώσετε τον τύπο δεδομένων στον οποίο δείχνει ο δείκτης ως const ως εξής:

int x;
int y = 10;
const int * p = &y;
x = *p;   // μπορείτε να διαβάσετε το περιεχόμενο του δείκτη
*p = x;   // error: ΔΕΝ μπορείτε να μεταβάλλετε το περιεχόμενο του δείκτη. 

Η χρήση δεικτών σε δεδομένα τύπου const είναι ευρέως διαδεδομένη σε συναρτήσεις, όταν θέλουμε να χρησιμοποιήσουμε δείκτες μόνο για το διάβασμα των περιεχομένων αποκλείοντας το γράψιμο στη συγκεκριμένη περιοχή μνήμης. Δείτε το παρακάτω παράδειγμα:

const_content.cpp
#include <iostream>
using namespace std;
 
// stop pointer does not modify its contents
void increment_all (int* start, const int* stop) {
  while (start != stop) {
    ++(*start);  // increment value pointed
    ++start;     // increment pointer
  }
}
 
// start & stop pointers do not modify their contents
void print_all (const int* start, const int* stop)
{
  while (start != stop) {
    cout << *start << endl;
    ++start;     // increment pointer
  }
}
 
int main ()
{
  int numbers[] = {10,20,30};
  increment_all (numbers,numbers+3);
  print_all (numbers,numbers+3);
  return 0;
}

Από τον παραπάνω παράδειγμα παρατηρήστε ότι ο δείκτης start στη συνάρτηση print_all δεν μεταβάλλει τα δεδομένα στη μνήμη, όμως το περιεχόμενο του δεν παραμένει σταθερό, αλλά μεταβάλλεται. Στην περίπτωση που επιθυμείτε τη χρήση ενός δείκτη ο οποίος δεν μεταβάλλει τη διεύθυνση στην οποία δείχνει, αλλά είναι σε θέση να μεταβάλλει το περιεχόμενο της διεύθυνσης αυτής τότε μπορείτε να δηλώσετε ένα δείκτη της μορφή

char c = 'a'; 
char * const ptr = &c;

όπως στο παρακάτω παράδειγμα:

const_address.cpp
#include <string.h>
typedef struct {
  char name[32];
  char age;
} person_t;
 
void set_age(person_t * const p, int age) {
  p->age = age;  // this is OK
  //p++;         // this is not allowed!
}
 
int main() {
  person_t peter;
  strcpy(peter.name, "Peter Smith");
  set_age(&peter, 25);
}

Τέλος εάν επιθυμείτε να έχετε ένα δείκτη ο οποίος δεν μεταβάλλει τη διεύθυνση στην οποία δείχνει και δεν μπορεί να μεταβάλλει το περιεχόμενο της διεύθυνσης αυτής τότε μπορείτε να τον δηλώσετε ως εξής:

char c = 'a'; const char * const ptr = &c;

Δείτε το παρακάτω παράδειγμα χρήσης ενός τέτοιου δείκτη

const_address_const_ptr.cpp
#include <string.h>
#include <iostream>
using namespace std;
 
typedef struct {
  char name[32];
  char age;
} person_t;
 
void set_age(person_t * const p, int age) {
  p->age = age;  // this is OK
  //p++;         // this is not allowed!
}
 
int get_age(const person_t * const p) {
  //p->age++;  // this is not allowed!
  //p++;    // this is not allowed!
  return p->age;  // this is OK
}
 
int main() {
  person_t peter;
  strcpy(peter.name, "Peter Smith");
  set_age(&peter, 25);
  std::cout << "Age of" << peter.name << " is " << get_age(&peter) << endl;
}

Συμπερασματικά, η χρήση του προσδιοριστή const μπορεί να δημιουργήσει δείκτες της εκάστοτε κατηγορίας ως εξής:

int x;
      int *       p1 = &x;  // non-const pointer to non-const int
const int *       p2 = &x;  // non-const pointer to const int
      int * const p3 = &x;  // const pointer to non-const int
const int * const p4 = &x;  // const pointer to const int 

Δείκτες και αλφαριθμητικά

Σε προηγούμενη ενότητα είδαμε ότι τα αλφαριθμητικά είναι ακολουθίες από χαρακτήρες που τερματίζουν στον χαρακτήρα '\0'. Οι ακολουθίες χαρακτήρων μπορούν να αντιμετωπισθούν και ως πίνακες από χαρακτήρες. Δείτε το παρακάτω παράδειγμα ανάγνωσης και εγγραφής των περιεχόμενων ενός αλφαριθμητικού. Παρατηρήστε ότι μπορούμε να χρησιμοποιήσουμε τον τελεστή [] για να πάρουμε το περιεχόμενο θέσης πίνακα αν και το αλφαριθμητικό ορίστηκε με την βοήθεια ενός δείκτη σε χαρακτήρα (char).

string_modification.cpp
#include <string.h>
#include <iostream>
using namespace std;
int main() {
  const char *str = "Hello World";
  for(int i=0; i<strlen(str); i++)
    cout << i << ". " << str[i] << endl;
}

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

Μπορείτε να έχετε δείκτες οι οποίοι δείχνουν σε άλλους δείκτες οι οποίοι με την σειρά τους δείχνουν σε δεδομένα. Ενδεικτικά δείτε το παρακάτω παράδειγμα δεικτών. Οι τέσσερις τελευταίες γραμμές του παρακάτω κώδικα είναι ισοδύναμες και θέτουν το περιεχόμενο της μεταβλητής a σε '@'. Οι μεταβλητές a,b,c,d απεικονίζονται στο σχήμα που ακολουθεί.

pointers2pointers.cpp
char a = '$';
char *b = &a;   // δείκτης στη μεταβλητή a
char **c = &b;  // δείκτης στο δείκτη b
char ***d = &c; // δείκτης στο δείκτη c
 
a = '@'         // το περιεχόμενο της μεταβλητής a γίνεται '@'.
*b = '@';       // το περιεχόμενο του δείκτη b είναι η μεταβλητή a.
**c = '@';      // το περιεχόμενο του δείκτη c είναι ο δείκτης b.
***d = '@';     // το περιεχόμενο του δείκτη d είναι ο δείκτης c.

 Οι μεταβλητές a,b,c,d

Δείκτες τύπου void

Ο δείκτης τύπου void είναι ένας ειδικός τύπος δείκτη ο οποίος έχει το πλεονέκτημα ότι μπορεί να μετατραπεί σε οποιονδήποτε τύπο δείκτη. Επίσης οποιοσδήποτε τύπος δείκτη μπορεί να μετατραπεί σε δείκτη τύπου void. Ο περιορισμός των δεικτών τύπου void είναι ότι δεν υπάρχει τρόπος να προσπελάσουμε το περιεχόμενο ενός void* δείκτη χωρίς προηγουμένως να τον μετατρέψουμε σε ένα διαφορετικό τύπο δείκτη. Δείτε το παρακάτω παράδειγμα που χρησιμοποιεί ένα δείκτη void* για να προσπελάσει τα δεδομένα του.

void_ptr.cpp
#include <iostream>
using namespace std;
 
void increase (void* data, int psize)
{
  if ( psize == sizeof(char) ) {
    char* pchar; pchar=(char*)data; ++(*pchar); 
  }
  else if (psize == sizeof(int) ) {
    int* pint; pint=(int*)data; ++(*pint);
  }
}
 
int main ()
{
  char a = 'a';
  int b = 2;
  increase(&a,sizeof(a));
  increase(&b,sizeof(b));
  cout << a << ", " << b << endl;
  return 0;
}

Δεν επιτρέπεται η αριθμητική δεικτών σε δείκτες τύπου void*.

null pointers

Δείκτες που δεν είναι αρχικοποιημένοι συχνά αρχικοποιούνται στην τιμή 0 ή NULL. Η C++ εκτός από τις τιμές 0 και null παρέχει και την δεσμευμένη λέξη nullptr που αντιπροσωπεύει και αυτή την τιμή 0 για ένα δείκτη. Οι παρακάτω ορισμοί είναι ισοδύναμοι.

nullptr.cpp
#include <iostream>
char *p = 0;
char *q = NULL;
char *f = nullptr;

Η τιμή NULL ορίζεται σε διαφορετικά C++ header αρχεία της standard βιβλιοθήκης, ανάμεσα τους και το iostream.

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

Η C++ επιτρέπει την χρήση δεικτών σε συναρτήσεις. Οι δείκτες σε συναρτήσεις ορίζονται όπως οι συναρτήσεις με την διαφορά ότι το όνομα του δείκτη περικλύεται από παρενθέσεις και πριν από το όνομα προηγείται ο τελεστής *. Δείτε το παρακάτω παράδειγμα ορισμού και χρήσης ενός δείκτη σε συνάρτηση.

functionPointer.cpp
#include <iostream>
using namespace std;
 
int addition (int a, int b){ return a+b; }
int subtraction (int a, int b){ return a-b; }
 
int operation (int x, int y, int (*functioncall)(int,int)){
  int r = (*functioncall)(x,y);
  return r;
}
 
int main(){
  int m,n;
  int (*sub_ptr)(int,int) = subtraction;
 
  m = operation (7, 5, addition);
  n = operation (20, m, sub_ptr);
  cout << "Result: " << n << endl;
  return 0;
}

Παρατηρήστε ότι κατά τον ορισμό ενός δείκτη σε συνάρτηση είναι απαραίτητο να ορίσετε τον τύπο και τη σειρά των ορισμάτων που λαμβάνει η συνάρτηση.

cpp/pointers.1619497416.txt.gz · Last modified: 2021/04/27 03:23 (external edit)