Литмир - Электронная Библиотека
A
A
Программирование. Принципы и практика использования C++ Исправленное издание - _185.png

По существу, память компьютера можно рассматривать как последовательность байтов, пронумерованную от

0
до
size-1
. Для некоторых машин такое утверждение носит слишком упрощенный характер, но для нашей модели этого пока достаточно.

Каждый тип имеет соответствующий тип указателя. Рассмотрим пример.

char ch = 'c';

char* pc = &ch; // указатель на char

int ii = 17;

int* pi = ⅈ // указатель на int

Если мы хотим увидеть значение объекта, на который ссылаемся, то можем применить к указателю оператор разыменования, унарный

*
. Рассмотрим пример.

cout << "pc==" << pc << "; содержимое pc==" << *pc << "\n";

cout << "pi==" << pi << "; содержимое pi==" << *pi << "\n";

Значением

*pc
является символ
c
, а значением
*pi
— целое число
17
. Значения переменных
pc
и
pi
зависят от того, как компилятор размещает переменные
ch
и
ii
в памяти. Обозначение, используемое для значения указателя (адрес), также может изменяться в зависимости от того, какие соглашения приняты в системе; для обозначения значений указателей часто используются шестнадцатеричные числа (раздел A.2.1.1).

Оператор разыменования также может стоять в левой части оператора присваивания.

*pc = 'x'; // OK: переменной char, на которую ссылается

           // указатель pc,

           // можно присвоить символ 'x'

*pi = 27;  // OK: указатель int* ссылается на int, поэтому *pi —

           // это int

*pi = *pc; // OK: символ (*pc) можно присвоить переменной

           // типа int (*pi)

 

Программирование. Принципы и практика использования C++ Исправленное издание - _003.png
 Обратите внимание: несмотря на то, что значение указателя является целым числом, сам указатель целым числом не является. “На что ссылается
int
?” — некорректный вопрос. Ссылаются не целые числа, а указатели. Тип указателя позволяет выполнять операции над адресами, в то время как тип
int
позволяет выполнять (арифметические и логические) операции над целыми числами. Итак, указатели и целые числа нельзя смешивать.

int i = pi; // ошибка: нельзя присвоить объект типа int*

            // объекту типа int

pi = 7;     // ошибка: нельзя присвоить объект типа int объекту

            // типа int*

Аналогично, указатель на

char
(т.е.
char*
) — это не указатель на
int
(т.е.
int*
). Рассмотрим пример.

pc = pi; // ошибка: нельзя присвоить объект типа int*

         // объекту типа char*

pi = pc; // ошибка: нельзя присвоить объект типа char*

         // объекту типа int*

Почему нельзя присвоить переменную

pc
переменной
pi
? Один из ответов — символ
char
намного меньше типа
int
.

char ch1 = 'a';

char ch2 = 'b';

char ch3 = 'c';

char ch4 = 'd';

int* pi = &ch3; // ссылается на переменную,

                // имеющую размер типа char

                // ошибка: нельзя присвоить объект char* объекту

                // типа int*

                // однако представим себе, что это можно сделать

*pi = 12345;    // попытка записи в участок памяти, имеющий размер

                // типа char

*pi = 67890;

Как именно компилятор размещает переменные в памяти, зависит от его реализации, но, скорее всего, это выглядит следующим образом.

Программирование. Принципы и практика использования C++ Исправленное издание - _186.png

Если бы компилятор пропустил такой код, то мы могли бы записать число

12345
в ячейку памяти, начинающуюся с адреса
&ch3
. Это изменило бы содержание окрестной памяти, т.е. значения переменных
ch2
и
ch4
. В худшем (и самом реальном) случае мы бы перезаписали часть самой переменной
pi
! В этом случае следующее присваивание
*pi=67890
привело бы к размещению числа
67890
в совершенно другой области памяти. Очень хорошо, что такое присваивание запрещено, но таких механизмов защиты на низком уровне программирования очень мало.

В редких ситуациях, когда нам требуется преобразовать переменную типа

int
в указатель или конвертировать один тип показателя в другой, можно использовать оператор
reinterpret_cast
(подробнее об этом — в разделе 17.8).

Итак, мы очень близки к аппаратному обеспечению. Для программиста это не очень удобно. В нашем распоряжении лишь несколько примитивных операций и почти нет библиотечной поддержки. Однако нам необходимо знать, как реализованы высокоуровневые средства, такие как класс

vector
. Мы должны знать, как написать код на низком уровне, поскольку не всякий код может быть высокоуровневым (см. главу 25). Кроме того, для того чтобы оценить удобство и относительную надежность высокоуровневого программирования, необходимо почувствовать сложность низкоуровневого программирования. Наша цель — всегда работать на самом высоком уровне абстракции, который допускает поставленная задача и сформулированные ограничения. В этой главе, а также в главах 18–19 мы покажем, как вернуться на более комфортабельный уровень абстракции, реализовав класс
vector
.

222
{"b":"847443","o":1}