Говоря более абстрактно, я отношусь к большой группе профессионалов в области компьютерных наук, считающих, что отсутствие теоретических и практических знаний о работе с памятью порождает проблемы при решении высокоуровневых задач, таких как обработка структур данных, создание алгоритмов и разработка операционных систем.
17.2. Основы
Начнем нашу поступательную разработку класса
vector
с очень простого примера.
vector<double> age(4); // вектор с четырьмя элементами типа double
age[0]=0.33;
age[1]=22.0;
age[2]=27.2;
age[3]=54.2;
Очевидно, что этот код создает объект класса
vector
с четырьмя элементами типа
double
и присваивает им значения
0.33
,
22.0
,
27.2
и
54.2
. Эти четыре элемента имеют номера 0, 1, 2 и 3. Нумерация элементов в стандартных контейнерах языка С++ всегда начинается с нуля. Нумерация с нуля используется часто и является универсальным соглашением, которого придерживаются все программисты, пишущие программы на языке С++. Количество элементов в объекте класса
vector
называется его размером. Итак, размер вектора
age
равен четырем. Элементы вектора нумеруются (индексируются) от
0
до
size-1
. Например, элементы вектора
age
нумеруются от
0
до
age.size()–1
. Вектор age можно изобразить следующим образом:
Как реализовать эту схему в компьютерной памяти? Как хранить значения и обеспечивать к ним доступ? Очевидно, что мы должны определить класс и назвать его
vector
. Далее, нужен один член класса для хранения размера вектора и еще один член для хранения его элементов. Как же представить множество элементов, количество которых может изменяться? Для этого можно было бы использовать стандартный класс
vector
, но в данном контексте это было бы мошенничеством: мы же как раз этот класс и разрабатываем.
Итак, как представить стрелку, изображенную на рисунке? Представим себе, что ее нет. Мы можем определить структуру данных фиксированного размера.
class vector {
int size,age0,age1,age2,age3;
// ...
};
Игнорируя некоторые детали, связанные с обозначениями, получим нечто, похожее на следующий рисунок.
Это просто и красиво, но как только мы попробуем добавить элемент с помощью функции
push_back()
, окажемся в затруднительном положении: мы не можем добавить элемент, так как количество элементов зафиксировано и равно четырем. Нам нужно нечто большее, чем структура данных, хранящая фиксированное количество элементов. Операции, изменяющие количество элементов в объекте класса
vector
, такие как
push_back()
, невозможно реализовать, если в классе
vector
количество элементов фиксировано. По существу, нам нужен член класса, ссылающийся на множество элементов так, чтобы при расширении памяти он мог ссылаться на другое множество элементов. Нам нужен адрес первого элемента. В языке C++ тип данных, способный хранить адрес, называют
указателем (pointer). Синтаксически он выделяется суффиксом
*
, так что
double*
означает указатель на объект типа
double
. Теперь можем определить первый вариант класса
vector
.
// очень упрощенный вектор элементов типа double (вроде vector<double>)
class vector {
int sz; // размер
double* elem; // указатель на первый элемент (типа double)
public:
vector(int s); // конструктор: размещает в памяти s чисел
// типа double,
// устанавливает на них указатель elem,
// хранит число s в члене sz
int size() const { return sz; } // текущий размер
};
Прежде чем продолжить проектирование класса vector, изучим понятие “указатель” более подробно. Понятие “указатель” — вместе с тесно связанным с ним понятием “массив” — это ключ к понятию “память” в языке C++.
17.3. Память, адреса и указатели
Память компьютера — это последовательность байтов. Эти байты нумеруются от нуля до последнего.
Адресом (address) называют число, идентифицирующее ячейку в памяти. Адрес можно считать разновидностью целых чисел. Первый байт памяти имеет адрес 0, второй — 1 и т.д. Мегабайты памяти можно визуализировать следующим образом:
Все, что расположено в памяти, имеет адрес. Рассмотрим пример.
int var = 17;
Эта инструкция резервирует участок памяти, размер которого определяется размером типа
int
, для хранения переменной
var
и записывает туда число
17
. Кроме того, можно хранить адреса и применять к ним операции. Объект, хранящий адрес, называют
указателем. Например, тип, необходимый для хранения объекта типа
int
, называется указателем на
int
и обозначается как
int*
.
int* ptr = &var; // указатель ptr хранит адрес переменной var
Для определения адреса объекта используется оператор взятия адреса, унарный
&
. Итак, если переменная var хранится в участке памяти, первая ячейка которого имеет адрес 4096 (или 2
12), то указатель
ptr
будет хранить число 4096.