14. Указатели

План:
Дялове в оперативната памет при изпълнение на програма на С++
Тип указател
Използване на указатели в класове
Указатели и масиви, адресна аритметика
Указатели и С-низове

** Дялове в оперативната памет при изпълнение на програма на С++

Когато стартираме транслирана програма на С++ за изпълнение, операционната система отделя оперативна памет за нея, която се състои от следните части:

CODE 
DATA 
STACK (системен стек, run-time stack)
HEAP (динамична памет)
  • стойности на указатели

** Тип указател (pointer)

* Константа или променлива от тип указател съхранява адрес от оперативната памет на компютъра. 

* Дефиниране на променлива тип указател (език C)

Синтаксис: <име на тип> * <име на променлива>

Примери: Дефиниции на указателите pk, boss и pn.
int* pk;         
// без инициализация
Employee * boss;
   // без инициализация

int *pn = NULL;  // с инициализация

Константата NULL означава адрес 0.

* Запазване на място в динамичната памет - унарна операция new (език C++)

Синтаксис:
new <име на тип>
Запазва място (памет) за една променлива от зададения тип и съдържа адреса на началото на запазената памет.
 
Синтаксис: new <име на тип>[<число>]
Запазва място за <число> променливи (масив!) от зададения тип и съдържа адреса на първата променлива.

Пример 1:
- дефиниране на променлива тип указател (pn) и инициализация на указателя: 
int *pn = new int; 

Пример 2:
- дефиниране на масив (с име указател arr) в динамичната памет с помощта на операция new:
int *arr = new int[100];

Пример 3:
-дефиниране на променлива тип указател (boss), инициализация на указателя (операция new) и задаване на начална стойност (конструктор) на обекта на този адрес:
Employee *boss = new Employ("John", 3200);

-конструиране на обект от тип int, инициализация на указателя (операция new) и задаване на начална стойност  на променливата на този адрес:
int *pn = new int(10);

STACK

Име: boss
Тип: Employee* (указател)
Стойност: адрес XXX


Динамична памет 
адрес XXX
Име: *boss (временно!)
Тип: Employee
Стойност:
"John"
3200
STACK

Име: pn
Тип: int* (указател)
Стойност: адрес YYY
Динамична памет
адрес YYY
Име: *pn (временно!)
Тип: int
Стойност: 10

* Операция стойност на указател (dereference) - унарна операция * (език C)

Синтаксис:
 *<указател>
Стойността на операцията е обектът, намиращ се на адрес
а, записан в <указател>.

Примери:
Employee *boss = new Employee("John", 3200); // началник е John, * означава дефиниране на указател
Employee harry("Harry", 1500);
*boss = harry;                            // началник вече е Harry, * означава операция *, lvalue;
// операция присвояване (Employee)

int *pn = new int(10);
cout << *pn;    
// 10; операция *, стойността на оперцията е използвана като rvalue
*pn = 12;
              // операция *, стойността на оперцията е lvalue; операция присвояване (int)
cout << *pn;    // 12; операция *, стойността на оперцията е използвана като rvalue

* Обекти и променливи без имена
Операция new създава обект без име, като достъпът до този обект става с указатели.

Примери: Указатели boss и h осигуряват дастъп до обекта Employee("John", 3200).
Employee *boss = new Employee("John", 3200); // *boss е John
Еmployee *h;

h = boss;     // операция присвояване (Employee*); *boss и *h е все John
boss = NULL;
 
// операция присвояване (Employee*); само *h е John, *boss е грешка!


* Операция стрелка - бинарна операция -> (език C)

Синтаксис:
<указател към клас> -> <член на класа>
Стойността на операцията е функцията или обекта, членове на класа.

Пример: Достъп до функцията get_salary() от класа Employee с двойка операции * и
и с операция ->:

cout << (*boss).get_salary();
  // операция *; операция .
cout << boss->get_salary();   
// операция ->
cout << *boss.get_salary();    // операция .; операция *
                              
// синтактична грешка! *(boss.get_salary())!

* Освобождаване на памет (унищожаване на обект) - унарна операция delete (C++):

Синтаксис:
 delete <указател>
Освобождава заетата с операция new динамична памет.

Примери: 
delete boss; // boss е указател със стойност адрес в динамичната памет
delete pn;   // pn е указател със стойност адрес в динамичната памет
delete [] arr; // освобождава паметта, запазена с new (целия масив)


* Невалидни указатели

-- Опит за работа със стойност на указател след освобождаване на заетата памет

Пример: След операция delete, pk става невалиден указател.
int *pk = new int(10); 
cout << *pk; // 10; операция * 
delete pk;   // операция delete   
*pk = 100;   // операция *, операция присвояване (int); невалиден указател
// на адрес pk няма променлива тип int        
Опасна грешка!

delete pk;   // операция delete
pk = NULL;
 
*pk = 100;   // ОС идентифицира грешката

-- Неопределена (незаредена) променлива

Пример: Операция * връща невалидна променлива.
int *pk;

*pk = 100; // невалиден указател
// указателят pk съдържа неопределена стойност (незаредена променлива)
Опасна грешка

int* pk = NULL; 
*pk = 100;   // ОС идентифицира грешката


* Адрес на променлива или обект - унарна операция & (език C)

Синтаксис:
 & <име на променлива>
Стойността на операцията е адреса на променливата.

Пример: Адрес на променлива в динамичната памет.
int *p = new int(20);
int *p1 = &(*p);
// инициализира p1 с адреса на променливата *p
int *p2 = p;    
// инициализира p2 с адреса, записан в p


* Указатели в системния стек (run-time stack).

Пример: Локални обект harry и указател h, инициализиран с адреса на harry.
Employee harry("Harry", 4300);
// обект в системния стек 
Employee *h = &harry;          // h съдържа адрес на обект в системниея стек

int k = 10; 
int *pk = &k; 
cout << k; 
cout << (*pk);
STACK 
Адрес XXX
Име: k
Тип: int
Стойност: 10
STACK 
 
Име: pk
Тип: int* (указател)
Стойност: Адрес XXX


* Задължително освобождаване на заета динамична памет.

Паметта, заета от локалните променливи (в системния стек) се освобождава автоматично.
Паметта, заета от стойности на указатели (в динамичната памет) се освобождава с операция delete.
 За всяка операция new трябва да има съответна операция delete.

Пример:
void g()
 {    Employee* boss;
     // 1-st allocation: pointer variable allocated on the stack (name boss)
     boss = new Employee(. . .);
     // 2-nd allocation: Employee object allocated on the heap (no name!)
     . . .
 
} // memory for boss automatically reclaimed
   // NO! Memory leak! Missing delete!

* Дефиниране на променлива-псевдоним - друго име на същата променлива
Както при параметри-псевдоними на функции.

Пример: ak е друго има на променливата k.
int k = 10; 
int &ak = k;       // дефиниране на променлива-псевдоним
if (ak == k) cout << "YES, of course";

* Параметри на функции - псевдоними и указатели

Пример: Разменяне на стойностите на две променливи

// формалните параметри са параметри-псевдоними
void swap1(int &a, int &b) // разменя стойностите на променливите a и b
{
    int w = a;
    a = b;
    b = w;
}

// формалните параметри са параметри-променливи от тип указатели
void swap2(int *pa, int *pb)
// разменя стойностите на променливите с адреси указателите a и b
{
    int w = *pa;
    *pa = *pb;
    *pb = w;
}
int main()
{
    int x = 10, y = 12;
    swap1(x, y);   
// фактическите параметри са промелниви от тип int
    cout << x << " " << y << endl;
    swap2(&x, &y);
// фактическите параметри са адреси на променливи от тип int
    cout << x << " " << y << endl;
    return 0;
}


** Използване на указатели в класове

* Пример (optional attribute): Класът Department представя департамент с име (тип string) и администратор (тип Employee), като в някой департамент може да няма администратор.

1. Решение без указатели с допълнителна данна exists, която показва има ли администратор:

class Department {
. . .
private:
string name;
bool exists; // true ако има администратор
Employee recept; // обектът съществува само ако има администратор
};

2. Решение с указател. Ако в департамента има администратор, то указателят ще сочи към този обект (ще съдържа адреса на този обект), ако няма - указателят ще съдържа NULL.
class Department {
. . .
private:
string name;
Employee* receptionist;
};

Пример (sharing):  Някои департаменти могат да имат администратор и/или секретар, като някъде един и същи служител може да заема и двете длъжности.

class Department {
. . .
private:
string name;
Employee* receptionist; // NULL ако няма; адреса на администратора, ако има
Employee* secretary; // NULL ако няма; адреса на секретаря, ако има
};


// department.cpp
#include
<string> #include <iostream> using namespace std; #include "ccc_empl.h" /**
A department in an organization. */ class Department { public: Department(string n); void set_receptionist(Employee* e); void set_secretary(Employee* e); void print() const; private: string name; Employee* receptionist; Employee* secretary; }; /** Constructs a department with a given name. @param n the department name */ Department::Department(string n) { name = n; receptionist = NULL; secretary = NULL; } /** Sets the receptionist for this department. @param e the receptionist */ void Department::set_receptionist(Employee* e) { receptionist = e; } /** Sets the secretary for this department. @param e the secretary */ void Department::set_secretary(Employee* e) { secretary = e; } /** Prints a description of this department. */ void Department::print() const { cout << "Name: " << name << "\n" << "Receptionist: "; if (receptionist == NULL) cout << "None"; else cout << receptionist->get_name() << " " << receptionist->get_salary(); cout << "\nSecretary: "; if (secretary == NULL) cout << "None"; else if (secretary == receptionist) cout << "Same"; else cout << secretary->get_name() << " " << secretary->get_salary();
cout << "\n"; } int main() { Department shipping("Shipping"); Employee* harry = new Employee("Hacker, Harry", 45000); shipping.set_secretary(harry);


Department qc("Quality Control"); 
Employee
* tina = new Employee("Tester, Tina", 50000); qc.set_receptionist(tina); qc.set_secretary(tina);
  tina->set_salary(55000);

Department cs("Computer Support");
Employee* mary = new Employee("Brown, Mary", 33000);
cs.set_receptionist(mary);
Employee* boby = new Employee("Hill, Boby", 44000);
cs.set_secretary(boby);

  shipping.print(); qc.print();
cs.print();

  return 0; }

nkirov@cpp % c++ department.cpp ccc_empl.cpp
nkirov@cpp % ./a.out
Name: Shipping
Receptionist: None
Secretary: Hacker, Harry 45000
Name: Quality Control
Receptionist: Tester, Tina 55000
Secretary: Same
Name: Computer Support
Receptionist: Brown, Mary 33000
Secretary: Hill, Boby 44000

Pointers in C / C++ [Full Course]


** Указатели и масиви, адресна аритметика

* Името на масив е константен указател.  Променлива указател може да служи за име на масив.

Пример:
int a[3] = {10, 20, 30}; 
int *pa = a;
// отпечатва 3 пъти стойността на a[0]
cout << a[0] <<" "<< pa[0] <<" "<< *pa;  // 10 10 10

* Адресна аритметика

Синтаксис: <указател> + <число>
Стойността на операцията е адрес, отместен с
<число> променливи от същия тип   

Примери:
// отпечатва 4 пъти стойността на a[1]
cout << a[1] <<" "<< pa[1] <<" "
     << *(pa+1) <<" "<<(pa+1)[0];
// 20 20 20 20

Адрес pa или a pa+1 или a+1 pa+2 или a+2
Стойност 10 20 30
Индекс 0 1 2
Достъп 1 a[0] a[1] a[2]
Достъп 2 *pa *(pa+1) *(pa+2)
Достъп 3 pa[0] (pa+1)[0] (pa+1)[1]
Достъп 4 *a *(a+1) *(a+2)

** Указатели и С-низове

* C++ наследява от езика C по-ниско ниво за работа с низове (С-низове), когато низовете са представени като масиви от char стойности.

* Не се препоръчва използването на С-низове, указатели и масиви в програмите на С++, но понякога е удобно да се използват функции, които получават или да връщат char*  стойности.
char s[] = "Harry"; // същото като
char *s = "Harry";
*s *(s+1) *(s+2) *(s+3) *(s+4) *(s+5)

s[0]

s[1] s[2] s[3] s[4] s[5]
'H' 'a' 'r' 'r' 'y' '\0'

Превръщане на С-низове в "C++ низове" - тип char* в тип string.
char* p = "Harry";
string name(p); // or string name = p;
Превръщане на обекти от тип string в тип char*.
string num = "123";
const char* cnum = num.c_str();
int n = atoi(cnum);