Опубликован: 16.09.2005 | Уровень: для всех | Доступ: свободно
Лекция 8:

Основы языка Си: структура Си-программы, базовые типы и конструирование новых типов, операции и выражения

Арифметика указателей

С указателями можно выполнять следующие операции:

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

Прибавление к указателю p целого числа n означает увеличение адреса, который содержится в переменной p, на суммарный размер n элементов того типа, на который ссылается указатель. Указатель как бы сдвигается на n элементов вправо, если считать, что индексы элементов массива возрастают слева направо. Аналогично вычитание целого числа n из указателя означает сдвиг указателя влево на n элементов. Пример:

int *p, *q;
int a[100];
p = &(a[5]); // записываем в p адрес 5-го
             //     элемента массива a
p += 7;      // p будет содержать адрес 12-го эл-та
q = &(a[10]);
--q;         // q содержит адрес элемента a[9]

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

Разность двух указателей - это количество элементов данного типа, которое умещается между двумя адресами. Результатом вычитания указателей является целое число. Физически оно вычисляется как разность значений двух адресов, деленная на размер одного элемента заданного типа. Операции сложения указателя с целым числом и разности двух указателей взаимно обратны:

int *p, *q;
int a[100];
int n;
p = &(a[5]);
q = &(a[12]);
n = q - p;      // n == 7
q = p + n;      // q == &(a[12])

Подчеркнем, что указатели нельзя складывать! В отличие от разности указателей, операция сложения указателей (т.е. сложения адресов памяти) абсолютно бессмысленна.

int *p, *q, *r;
int a[100];
p = &(a[5]);
q = &(a[12]);
r = p + q;  // Ошибка! Указатели нельзя складывать.

Связь между указателями и массивами

В языке Си имя массива a является указателем на его первый элемент, т.е. выражения a и &(a[0]) эквивалентны. Учитывая арифметику указателей, получаем эквивалентность следующих выражений:

a[i]  ~  *(a+i)

Действительно, при прибавлении к a целого числа i происходит сдвиг на i элементов вправо. Поскольку имя массива является адресом его начального элемента, получается адрес i -го элемента массива a. Применяя операцию звездочка *, получаем сам элемент a[i]. Точно так же эквивалентны выражения

&(a[i])  ~  a+i    (адрес эл-та a[i]).

Эта особенность арифметики указателей позволяет вообще не использовать квадратные скобки, т.е. обращение к элементу массива; вместо этого можно использовать указатели и операцию звездочка *.

Обратно, пусть p - указатель. Синтаксис языка Си позволяет трактовать его как адрес начала массива и применять к нему операцию доступа к элементу массива с заданным индексом. Эквивалентны следующие выражения:

p[i]  ~  *(p+i)

Таким образом, выбор между массивами и указателями - это выбор между двумя эквивалентными способами записи программ. Указатели, возможно, нравятся системным программистам, которые привыкли к работе с адресами объектов. Массивы больше отвечают традиционному стилю. В объектно-ориентированных языках, таких как Java или C#, указателей либо нет вовсе, либо их разрешено использовать лишь в специфических ситуациях. Массивы же присутствуют в подавляющем большинстве алгоритмических языков.

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

double a[100], s;
int i;
...
s = 0.0;
i = 0

while (i < 100) {
    s += a[i];
    ++i;
}
double a[100], s;
double *p, *g;
...
s = 0.0;
p = a;  // адрес начала массива
g = a+100;  // адрес за концом
while (p < g) {
    s += *p;
    ++p;
}

Операция приведения типа

Операция приведения типа ( type cast ) является одной из самых важных в Си. Без знакомства с синтаксисом этой операции (весьма непривычного для начинающих) и сознательного ее использования написать на Си что-нибудь более или менее полезное невозможно.

Операция приведения типа используется, когда значение одного типа преобразуется к другому типу, в том случае, если существует некоторый разумный способ такого преобразования. Операция обозначается именем типа, заключенным в круглые скобки; она записывается перед ее единственным аргументом. Рассмотрим два примера. Пусть требуется преобразовать целое число к вещественному типу. Как известно, целые и вещественные числа по-разному представляются в компьютере, см. раздел 3.3.1. Тем не менее, существует однозначный способ преобразования целого числа типа int к вещественному типу double. В первом примере значение целой переменной n приводится к вещественному типу и присваивается вещественной переменной x:

double x;
int n;
. . .
x = (double) n; // Операция приведения к типу double

В данном случае никакой потери информации не происходит, поэтому такое приведение допустимо и по умолчанию:

x = n; // Эквивалентно x = (double) n;

Во втором примере вещественное значение преобразуется к целому типу. При этом дробная часть вещественного числа отбрасывается, а знак числа сохраняется:

double x, y;
int n, k;
. . .
x = 3.7;
y = (-1.5);
n = (int) x;    // n присваивается значение 3
k = (int) y;    // k присваивается значение -1

В результате выполнения операции приведения вещественного числа к целому типу происходит отбрасывание дробной части числа, т.е. потеря информации. Поэтому, если использовать операцию приведения типа неявно (т.е. в результате простого присваивания целой переменной вещественного значения), например,

double x; int n;
. . .
n = x; // неявное приведение вещественного к целому

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

Операция приведения типа чаще всего используется для преобразования указателей. Например, стандартная функция захвата динамической памяти malloc возвращает указатель общего типа void* (см. раздел 3.7.3). Значение указателя обобщенного типа нельзя присвоить указателю на конкретный тип (язык C++ запрещает такие присвоения, Си-компиляторы иногда разрешают преобразования указателей по умолчанию, выдавая предупреждения, - но в любом случае это дурной стиль!). Для преобразования указателей разного типа нужно использовать операцию приведения типа в явном виде. В следующем примере в динамической памяти захватывается участок размером в 400 байт, его адрес присваивается указателю на массив из 100 целых чисел:

int *a; // Описываем указатель на массив типа int
. . .
// Захватываем участок памяти размером в 400 байт
// (поскольку sizeof(int) == 4), приводим указатель
// на него от типа void* к типу int* и присваиваем
// приведенное значение указателю a:
a = (int*) malloc(100 * sizeof(int));

Отметим, что допустимо неявное преобразование любого указателя к указателю обобщенного типа void*. Обратное, как указано выше, считается грубой ошибкой в C++ и дурным стилем (возможно, сопровождаемым предупреждением компилятора) в Си:

int *a;       // Указатель на целое число
void *p;      // Указатель обобщенного типа
. . .
a = p;        // Ошибка! В C++ запрещено неявное
              // приведение типа от void* к int*
a = (int*) p; // Корректно: явное приведение типа

p = a;  // Корректно: любой указатель можно
        // неявно привести к обобщенному
Кирилл Юлаев
Кирилл Юлаев
Как происходит отслеживание свободного экстента?
Федор Антонов
Федор Антонов
Оплата и обучение
Андрей Ерохин
Андрей Ерохин
Россия, Москва
Евгений Ледяев
Евгений Ледяев
Россия, Барнаул