Главна страница

Упражнение 1 - Преговор на C++

Указатели

Паметта на компютъра е разделена на клетки по 1 байт. Всяка клетка си има адрес. Когато декларираме променлива, за нея се отделя място в паметта. В повечето случаи ние не се интересуваме от адреса на променливата, но в някои случаи тя ще ни трябва. За да разберем адреса на променлива използваме оператора & пред променливата: &n.
Указателят е променлива, стойността, на която е адреса на друга променлива. С оператора * пред указател достъпваме стойността, към която сочи той.

& връща адреса на променливата и може да се чете „адресът на“ (reference operator)
* връща (достъпва) стойността на променливата и може да се чете „стойността сочена от“ (dereference operator)

Когато декларираме указател трябва да укажем типа на променливата, към която ще сочи той. Това става с * между типа и името на променливата.

int* pointerToInt;

double* pointerToDouble;

Тази * е различна от dereference оператора и не трябва да се бърка с него. Просто и в двата случая се използва знакът *.
Можем да приемем, че типът на pointerToInt е int*, а на pointerToDouble - double*. Но, за да декларираме 2 указателя към int на един ред трябва да напишем:

int * p1, * p2;

Ако напишем:

int * p1, p2;

само типът на p1 ще е int*, а на p2 ще е int.

#include <iostream>

using namespace std;

int main() {
	int n = 5;
	int* addressOfVariable;		// декларираме указател от тип int
	addressOfVariable = &n;		// присвояваме му адреса на променливата n
	int a;
	a = *addressOfVariable;		// на a присвояваме стойността сочена от addressOfVariable
	*addressOfVariable = 7;		// стойността сочена от addressOfVariable става 7

	cout << n << endl;
	cout << a << endl;
	return 0;
}
#include <iostream>

using namespace std;

int main () {
	int firstValue = 5, secondValue = 15;
	int * p1, * p2;
	
	p1 = &firstValue;	// p1 = адреса на firstValue
	p2 = &secondValue;	// p2 = адреса на secondValue
	*p1 = 10;	 	// стойността сочена от p1 = 10
	*p2 = *p1;		// стойността сочена от p2 = стойността сочена от p1
	p1 = p2;		// указателят p1 = указателя p2, сега и двата сочат към един и същи адрес
	*p1 = 20;		// стойността сочена от p1 = 20
	
	cout << "firstValue is " << firstValue << endl;
	cout << "secondValue is " << secondValue << endl;
	return 0;
}

Един указател може да не бъде насочен към никъде. Тогава се казва, че неговата стойност е NULL. Всъщност тя е 0.

int* p = 0;

int* p = NULL;

Горните два реда са еквивалентни (в повечето случаи).

Променливите от тип масив всъщност са указатели към първия елемент на масива.

Динамична памет

На кратко, паметта за променливите, която се заделя преди самото изпълнение на програмата се нарича статична. Тя се заделя в стека. Паметта, която се заделя по време на изпълнение на програмата се нарича динамична и се заделя в heap-а.
Примерно ако искаме да декларираме масив, но броят на елементите му да бъде зададен от потребителя по време на изпълнение на програмата, трябва да го декларираме, като динамична памет. (Обикновеният масив се декларира с константа за броя на елементите му).

Динамична памет се заделя с операторите new и new[].

int* pointerToInt;
int* pointerToArrayOfInt;
pointerToInt = new int;
pointerToArrayOfInt = new int[5];

На 3-я ред new заделя памет за една променлива от тип int и връща указател към нея. А на последния ред се заделя блок от памет (масив) за 5 променливи и се връща указател към първата от тях.

Когато свършим работата си с динамично заделената памет трябва да я освободим. Това става с операторите delete и delete[].

delete pointerToInt;
delete[] pointerToArrayOfInt;

delete указва, че паметта сочена от указателя е свободна, а delete[], че блокът от памет (масивът) към началото, на който сочи указателят е свободен.

#include <iostream>

using namespace std;

int main () {
	int i, n;
	int* p;

	cout << "Колко числа искате да въведете? ";
	cin >> n;

	p = new int[n];		// заделяме блок от паметта за n броя int
				// и насочваме указателя p към първия от тях

	for (i = 0; i < n; i++) {
		cout << "Въведете число: ";
		cin >> p[i];
	}

	cout << "Въведохте: ";
	for (i = 0; i < n; i++)
		cout << p[i] << " ";
	cout << endl;

	delete[] p;		// освобождаваме заделената памет

	return 0;
}

Програмата memory-overflow.cpp заделя памет без да я освобождава, докато накрая операционната система принудително я спре.

Ето графика на използваната памет при работа на програма, под операционната система Debian.

Препълване на паметта

ВНИМАНИЕ: Някои операционни системи е възможно да „увиснат“ при изпълнението на програмата!

Програмите освобождават динамичната памет тогава, когато тя вече не им е нужна. Поради това редът на освобождаване не е непременно същият, като реда на заделяне. По този начин в паметта се получават дупки - парчета свободна памет оградена от парчета памет, която все още се използва.

Разпокъсване на паметта

Ако се образуват много на брой дупки, общото количество свободна памет ще бъде голямо, но няма да има достатъчно голямо парче, в което да се поберат нашите данни. На горната карта на паметта има 37 свободни клетки, но не можем да заделим памет за масив, който да използва дори 5 клетки. Тази ситуация се нарича разпокъсване на паметта (memory fragmentation). Операционните системи се грижат за премахването на дупките и поддържат паметта подредена. Този процес е сложен и исизква немалко процесорно време. Ето защо казваме, че операторът new е бавен, особено когато искаме да заделим голям блок памет.

Структури

Със struct се декларира нов тип данни:

struct point {	// структура с 2 полета int
	int x;
	int y;
};

Можем да декларираме и указател към променлива, типът, на която е структура. За да достъпваме полетата на структурата чрез указателя ползваме оператора ->

С #define N 3 се казва на препроцесора да замени всички срещания на N, в кода, с 3.

#include <iostream>
#define N 3

using namespace std;

struct point {			// структура с 2 полета int
	int x;
	int y;
};

int main() {
	point a;		// декларираме променлива от тип point
	a.x = 1;		// с '.' след променливата достъпваме полетата на структурата
	a.y = 10;

	point b;

	point* ptr;		// декларираме указател към point
	ptr = &b;		// на p присвояваме адреса на b
	ptr->x = 2;		// с '->' след указател към структура (или обект) достъпваме полетата
	ptr->y = 20;

	point array[N];		// декларираме масив от тип point
	array[0] = a;
	array[1] = *ptr;
	array[2].x = 3;
	array[2].y = 30;

	for(int i = 0; i < N; i++) {
		cout << array[i].x << ", ";
		cout << array[i].y << endl;
	}

	return 0;
}

Псевдоними и предаване на параметри на функция

Когато предаваме параметър на функция можем да го предадем по стойност или по референция. При предаването по стойност се прави копие на предаваната променлива и се работи с копието. Промените направени по него не се отразяват на самата променлива.
При предаване по референция всъщност се подава указател (референция) към променливата и всички промени по параметъра извършени във функцията се отразяват върху променливата извън функцията.

#include <iostream>

using namespace std;

void passByValue(int a) {	// предаване по стойност
	a++;
	cout << "Във функцията passByValue а = " << a << endl;
}

void passByRef_1(int* a) {	// предаване по референция
	(*a)++;
	cout << "Във функцията passByRef_1 *а = " << *a << endl;
}

void passByRef_2(int& a) {	// предаване по референция. 'а' е параметър-псевдоним
	a++;
	cout << "Във функцията passByRef_2 а = " << a << endl;
}

int main() {
	int n = 5;
	passByValue(n);
	cout << "След изпълнение на passByValue n = " << n << "\n\n";

	passByRef_1(&n);
	cout << "След изпълнение на passByRef_1 n = " << n << "\n\n";

	passByRef_2(n);
	cout << "След изпълнение на passByRef_2 n = " << n << "\n\n";

	return 0;
}

Функциите passByRef_1 и passByRef_2 са еквивалентни. Използването на параметър-псевдоним при passByRef_2 улеснява кода ѝ, но може да доведе до неяснота, защото от нейното извикване не става ясно, че тя може да променя подадения ѝ параметър.

Клас, обект

Класът е структура от данни заедно с функциите, които обработват тези данни. Обектът е конкретна инстанция (екземпляр) на класа. Класът може да се приеме за тип, а обектът за променлива.

При класовете имаме спецификатори за достъп, с които указваме правата за достъп до променливите и функциите (общо членовете) на класа.
Достъп до private членовете може да се осъществи както от други членове на същия клас така и от техни приятели "friends".
Достъп до protected може да се осъществи освен от други членове на същия клас и техни приятели, така и от членовете на класовете наследници.
Достъп до public членовете може да се осъществи отвсякъде, където класът е видим.

Променливите на класа се наричат още полета (fields), а функциите на класа - методи (methods).

Конструкторът е функция, която се изпълнява само веднъж при създаването на обекта. В нея се извършват инициализация на променливите и/или други действия нужни за работата на обекта. Конструкторът има същото име като името на класа.
Деструкторът е функция, изпълняваща се, когато обекта се унищожава.
Използването на деструктор е особено подходящо, когато обекта използва динамична памет и тази памет трябва да се освободи при унищожаване на обекта. Деструкторът има същото име като името на класа, но с ~ отпред.

Конструкторът по подразбиране (default constructor) е този, който няма параметри. Ако не декларираме никакъв конструктор компилаторът подразбира, че класът трябва да има default constructor и предоставя такъв автоматично.

Освен него, компилаторът предоставя още три специални член-функции, които се декларират неявно ако не декларираме собствени. Това са копиращият конструктор, операторът за присвояване и деструкторът по подразбиране. Копиращият конструктор и операторът за присвояване копират стойностите на всички променливи от обекта подаден, като параметър или стоящ от дясно на '=' в текущия обект.

Ако при деклариране на обект искаме да използваме неговия конструктор по подразбиране не трябва да използваме ().
Point a; // правилно

Point a(); // неправилно

Операторът за обсег "::" ни позволява да дефинираме функциите на класа извън него, като в него само ги декларираме, т.е. опишем само прототипа им (сигнатурата им).

Ключовата дума this представлява указател към обекта, в който се използва.

#include <iostream>

using namespace std;

class Point {
private:			// private членове. До тях нямаме директен достъп от main функцията
	int x;
	int y;
public:
	Point(int x, int y) {	// конструктор с параметри
		this->x = x;	// на член променливата x присвояваме параметъра x
		this->y = y;
	}
	void print() {
		cout << '(' << x << ", " << y << ")\n";
	}
};

int main() {
	Point a(1, 2);		// тук се вика конструкторът с параметри
	Point b(30, 40);
	Point c(500, 600);

	cout << "Обектът a: ";
	a.print();		// извикваме член функцията print

	cout << "Обектът b: ";
	b.print();

	cout << "Обектът c: ";
	c.print();

	return 0;
}

Ако сме декларирали някакъв конструктор (както по-горе) компилаторът спира да предоставя конструктора по подразбиране.

#include <iostream>
#include <math.h>
#define N 3

using namespace std;

class Point {
private:
	float* distance;
	int x;
	int y;
public:
	Point() {			// конструктор по подразбиране
		distance = 0;		// насочваме указателя да не сочи към никъде
	}

	Point(int x, int y);		// прототип на конструктор с параметри
	~Point();			// деструктор
	void setX(int x);
	void setY(int y);
	int getX();
	int getY();
	float getDistance();
};

int main() {
	Point a;			// тук се вика конструкторът по подразбиране
	a.setX(3);
	a.setY(4);

	Point b(30, 40);		// тук се вика конструкторът с параметри
	b.getDistance();

	Point c(b);			// тук се вика копиращият конструктор предоставен от компилатора
	b = a;				// тук се вика операторът за присвояване предоставен от компилатора

	c.setX(3);
	c.setY(4);

	Point array[N];			// декларираме масив от обекти
	array[0] = a;
	array[1] = b;
	array[2] = c;

	for(int i = 0; i < N; i++) {
		cout << '(' << array[i].getX() << ", " << array[i].getY() << "): ";
		cout << array[i].getDistance() << endl;
	}

	return 0;
}

Point::Point(int x, int y) {
	distance = 0;
	this->x = x;			// на член променливата x присвояваме параметъра x
	this->y = y;
}

void Point::setX(int x) {
	this->x = x;
}

void Point::setY(int y) {
	this->y = y;
}

int Point::getX() {
	return x;
}

int Point::getY() {
	return y;
}

float Point::getDistance() {
	if(!distance) {				// ако още не е заделена памет за distance
		distance = new float;		// заделяме нова
		*distance = sqrt(x*x + y*y);	// и присвояваме изчислената стойност
	}
	return *distance;
}

Point::~Point() {
	if(distance) delete distance;	// ако сме заделили динамична памет я освобождаваме
}

ИзразМоже да се чете, като
*xстойността сочена от x
&xадресът на x
x.yчленът y на обекта x
x->yчленът y на обекта сочен от x
(*x).yчленът y на обекта сочен от x (еквивалентно на горното)
x[0]първият обект сочен от x
x[1]вторият обект сочен от x
x[n](n+1)-ят обект сочен от x

Главна страница