ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 30.03.2021

Просмотров: 167

Скачиваний: 1

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.


str[4]


или


*(p1+4)

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

Необходимость использования круглых скобок, в которые заключено выражение p1+4, обусловлена тем, что оператор "*" имеет более высокий приоритет, чем оператор "+". Без этих круглых скобок выражение бы свелось к получению значения, адресуемого указателем p1, т.е. значения первого элемента массива, которое затем было бы увеличено на 4.

Важно! Убедитесь лишний раз в правильности использования круглых скобок в выражении с указателями. В противном случае ошибку будет трудно отыскать, поскольку внешне программа может выглядеть вполне корректной. Если у вас есть сомнения в необходимости их использования, примите решение в их пользу — вреда от этого не будет.

В действительности в C++ предусмотрено два способа доступа к элементам массивов: с помощью индексирования массивов и арифметики указателей. Дело в том, что арифметические операции над указателями иногда выполняются быстрее, чем индексирование массивов, особенно при доступе к элементам, расположение которых отличается строгой упорядоченностью. Поскольку быстродействие часто является определяющим фактором при выборе тех или иных решений в программировании, то использование указателей для доступа к элементам массива— характерная особенность многих С++-программ. Кроме того, иногда указатели позволяют написать более компактный код по сравнению с использованием индексирования массивов.

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


Это


лишь


простой


тест.

Вот как выглядит версия программы разбиения строки на слова с использованием арифметики указателей.


// Программа разбиения строки на слова:

// версия с использованием указателей.

#include <iostream>

#include <cstdio>

using namespace std;


int main()

{

 char str[80];

 char token[80];

 char *p, *q;

 cout << "Введите предложение: ";

 gets(str);

 p = str;

 // Считываем лексему из строки.

 while(*р) {

  q = token; // Устанавливаем q для указания на начало массива token.

  /* Считываем символы до тех пор, пока не встретится либо пробел, либо нулевой символ (признак завершения строки). */

  while(*p != ' ' && *р) {

   *q = *р;

   q++; р++;

  }

  if(*p) р++; // Перемещаемся за пробел.

  *q = '\0'; // Завершаем лексему нулевым символом.

  cout << token << '\n';

 }

 return 0;

}


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


// Программа разбиения строки на слова:

// версия с использованием индексирования массивов.

#include <iostream>

#include <cstdio>

using namespace std;

int main()

{

 char str[80];

 char token[80];

 int i, j;

 cout << "Введите предложение: ";

 gets(str);

 // Считываем лексему из строки.

 for(i=0; ; i++) {

  /* Считываем символы до тех пор пока не встретится либо пробел, либо нулевой символ (признак завершения строки). */

  for(j=0; str[i]!=' ' && str[i]; j++, i++)

   token[j] = str[i];

  token[j] = '\0'; // Завершаем лексему нулевым символом.

  cout << token << '\n';

  if(!str[i]) break;

 }

 return 0;

}


У этих программ может быть различное быстродействие, что обусловлено особенностями генерирования кода С++-компиляторами. Как правило, при использовании индексирования массивов генерируется более длинный код (с большим количеством машинных команд), чем при выполнении арифметических действий над указателями. Поэтому неудивительно, что в профессионально написанном С++-коде чаще встречаются версии, ориентированные на обработку указателей. Но если вы— начинающий программист, смело используйте индексирование массивов, пока не научитесь свободно обращаться с указателями.


Индексирование указателя

Как было показано выше, можно получить доступ к массиву, используя арифметические действия над указателями. Интересно то, что в C++ указатель, Который ссылается на массив, можно индексировать так, как если бы это было имя массива (это говорит о тесной связи между указателями и массивами). Соответствующий такому подходу синтаксис обеспечивает альтернативу арифметическим операциям над указателями, поскольку он более удобен в некоторых ситуациях. Рассмотрим пример.


// Индексирование указателя подобно массиву.


#include <iostream>

#include <cctype>

using namespace std;

int main()

{

 char str[20] = "I love you";

 char *p;

 int i;

 p = str;

 // Индексируем указатель.

 for(i=0; p[i]; i++) p[i] = toupper(p[i]);

 cout << p; // Отображаем строку.

 return 0;

}


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


I LOVE YOU

Вот как работает эта программа. Сначала в массив str вводится строка "I love you". Затем адрес начала этой строки присваивается указателю р. После этого каждый символ строки str с помощью функции toupper() преобразуется в его прописной эквивалент посредством индексирования указателя р. Помните, что выражение р[i] по своему действию идентично выражению *(p+i).


Указатели и строковые литералы


Возможно, вас удивит способ обработки С++-компиляторами строковых литералов, подобных следующему.


cout << strlen("С++-компилятор");


Если С++-компилятор обнаруживает строковый литерал, он сохраняет его в таблице строк программы и генерирует указатель на нужную строку. Поэтому следующая программа совершенно корректна и при выполнении выводит на экран фразу: Работа с указателями - сплошное удовольствие!.


#include <iostream>

using namespace std;

int main()

{

 char *s;

 s = "Работа с указателями - сплошное удовольствие!\n";

 cout << s;

 return 0;

}

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

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

Поскольку указатель на таблицу строк конкретной программы при использовании строкового литерала генерируется автоматически, то можно попытаться использовать этот факт для модификации содержимого данной таблицы. Однако такое решения вряд ли можно назвать удачным. Дело в том, что С++-компиляторы создают оптимизированные таблицы, в которых один строковый литерал может использоваться в двух (или больше) различных местах программы. Поэтому "насильственное" изменение строки может вызвать нежелательные побочные эффекты. Более того, строковые литералы представляют собой константы, и некоторые современные С++-компиляторы попросту не позволят менять их содержимое. А при попытке сделать это будет сгенерирована ошибка времени выполнения.


Все познается в сравнении

Выше отмечалось, что значение одного указателя можно сравнивать с другим. Но, чтобы сравнение указателей имело смысл, сравниваемые указатели должны быть каким-то образом связаны друг с другом. Чаще всего такая связь устанавливается в случае, когда оба указателя указывают на элементы одного и того же массива. Например, даны два указателя (с именами А и В), которые ссылаются на один и тот же массив. Если А меньше В, значит, указатель А указывает на элемент, индекс которого меньше индекса элемента, адресуемого указателем В. Такое сравнение особенно полезно для определения граничных условий.

Сравнение указателей демонстрируется в следующей программе. В этой программе создается две переменных типа указатель. Одна (с именем start) первоначально указывает на начало массива, а вторая (с именем end) — на его конец. По мере ввода пользователем чисел массив последовательно заполняется от начала к концу. Каждый раз, когда в массив вводится очередное число, указатель start инкрементируется. Чтобы определить, заполнился ли массив, в программе просто сравниваются значения указателей start и end. Когда start превысит end, массив будет заполнен "до отказа". Программе останется лишь вывести содержимое заполненного массива на экран.



// Пример сравнения указателей.

#include <iostream>

using namespace std;

int main()

{

 int num[10];

 int *start, *end;

 start = num;

 end = &num[9];

 while(start <= end) {

  cout << "Введите число: ";

   cin >> *start;

  start++;

 }

 start = num; /* Восстановление исходного значения указателя */

 while(start <= end) {

  cout << *start << ' ';

  start++;

 }

 return 0;

}


Как показано в этой программе, поскольку start и end оба указывают на общий объект (в данном случае им является массив num), их сравнение может иметь смысл. Подобное сравнение часто используется в профессионально написанном С++-коде.


Массивы указателей

Указатели, подобно данным других типов, могут храниться в массивах. Вот, например, как выглядит объявление 10-элементного массива указателей на int-значения.


int *ipa[10];

Здесь каждый элемент массива ipa содержит указатель на целочисленное значение.

Чтобы присвоить адрес int-переменной с именем var третьему элементу этого массива указателей, запишите следующее.


ipa[2] = &var;

Помните, что здесь ipa — массив указателей на целочисленные значения. Элементы этого массива могут содержать только значения, которые представляют собой адреса переменных целочисленного типа. Вот поэтому переменная var предваряется оператором Чтобы присвоить значение переменной var целочисленной переменной х с помощью массива ipa, используйте такой синтаксис.


x = *ipa[2];

Поскольку адрес переменной var хранится в элементе ipa[2], применение оператора "*" к этой индексированной переменной позволит получить значение переменной var.

Подобно другим массивам, массивы указателей можно инициализировать. Как правило, инициализированные массивы указателей используются для хранения указателей на строки. Например, чтобы создать функцию, которая выводит счастливые предсказания, можно следующим образом определить массив fortunes,


char *fortunes[] = {

 "Вскоре деньги потекут к Вам рекой.\n",

 "Вашу жизнь озарит новая любовь.\n",


 "Вы будете жить долго и счастливо.\n",

 "Деньги, вложенные сейчас в дело, принесут доход.\n",

 "Близкий друг будет искать Вашего расположения.\n"

};


Не забывайте, что C++ обеспечивает хранение всех строковых литералов в таблице строк, связанной с конкретной программой, поэтому массив нужен только Для хранения указателей на эти строки. Таким образом, для вывода второго сообщения достаточно использовать инструкцию, подобную следующей.


cout << fortunes[1];

Ниже программа предсказаний приведена целиком. Для получения случайных чисел используется функция rand(), а для получения случайных чисел в диапазоне от 0 до 4 — оператор деления по модулю, поскольку именно такие числа могут служить для доступа к элементам массива по индексу.


#include <iostream>

#include <cstdlib>

#include <conio.h>

using namespace std;

char *fortunes[] = {

 "Вскоре деньги потекут к Вам рекой.\n",

 "Вашу жизнь озарит новая любовь.\n",

 "Вы будете жить долго и счастливо.\n",

 "Деньги, вложенные сейчас в дело, принесут доход.\n",

 "Близкий друг будет искать Вашего расположения.\n"


};


int main()

{

 int chance;

 cout <<"Чтобы узнать свою судьбу, нажмите любую клавишу: ";

 // Рандомизируем генератор случайных чисел.

 while(!kbhit()) rand();

 cout << '\n';

 chance = rand();

 chance = chance % 5;

 cout << fortunes[chance];

 return 0;

}


Обратите внимание на цикл while, который вызывает функцию rand() до тех пор, пока не будет нажата какая-либо клавиша. Поскольку функция rand() всегда генерирует одну и ту же последовательность случайных чисел, важно иметь возможность программно использовать эту последовательность с некоторой произвольной позиции. (В противном случае каждый раз после запуска программа будет выдавать одно и то же "предсказание".) Эффект случайности достигается за счет повторяющихся обращений к функции rand(). Когда пользователь нажмет клавишу, цикл остановится на некоторой, случайной позиции последовательности генерируемых чисел, и эта позиция определит номер сообщения, которое будет выведено на экран. Напомню, что функция kbhit() представляет собой довольно распространенное расширение библиотеки функций C++, обеспечиваемое многими компиляторами, но не входит в стандартный пакет библиотечных функций C++.


Соглашение о нулевых указателях 

Объявленный, но не инициализированный указатель будет содержать произвольное значение. При попытке использовать указатель до присвоения ему конкретного значения можно разрушить не только собственную программу, но даже и операционную систему (отвратительнейший, надо сказать, тип ошибки!). Поскольку не существует гарантированного способа избежать использования неинициализированного указателя, С++-программисты приняли процедуру, которая позволяет избегать таких ужасных ошибок. По соглашению, если указатель содержит нулевое значение, считается, что он ни на что не ссылается. Это значит, что, если всем неиспользуемым указателям присваивать нулевые значения и избегать использования нулевых указателей, можно избежать случайного использования неинициализированного указателя. Вам следует придерживаться этой практики программирования.

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


float *р = 0; // ртеперь нулевой указатель.

Для тестирования указателя используется инструкция if (любой из следующих ее вариантов).


if(р) // Выполняем что-то, если рне нулевой указатель.


if(!p) // Выполняем что-то, если рнулевой указатель.

Соблюдая упомянутое выше соглашение о нулевых указателях, вы можете избежать многих серьезных проблем, возникающих при использование указателей.


Многоуровневая непрямая адресация

Можно создать указатель, который будет ссылаться на другой указатель, а тот — на конечное значение. Эту ситуацию называют многоуровневой непрямой адресацией (multiple indirection) или использованием указателя на указатель. Идея многоуровневой непрямой адресации схематично проиллюстрирована на рис. 6.2. Как видите, значение обычного указателя (при одноуровневой непрямой адресации) представляет собой адрес переменной, которая содержит некоторое значение. В случае применения указателя на указатель первый содержит адрес второго, а тот ссылается на переменную, содержащую определенное значение.