Отправляет email-рассылки с помощью сервиса Sendsay

C и C++ для начинающих

  Все выпуски  

C и C++ для начинающих


Служба Рассылок Subscribe.Ru проекта Citycat.Ru
Доброго времени суток! Сегодня речь пойдет об указателях.

Задача такая: написать функцию, осуществляющую обмен значений двух целых чисел. Для обмена выберем широкоизвестный способ "треугольника" с временной переменной.
 #include <stdio.h>

 void Exchange (int x, int y)
 {
  int t;

  t = x;
  x = y;
  y = t;
  return;
 }

 /* Теперь проверим ее работу */
 int main (void)
 {
  int a, b;

  a = 5;
  b = 10;
  printf ("Сперва a = %d, b = %d.\n", a, b);
  Exchange (a, b);
  printf ("А теперь а = %d, b = %d.\n", a, b);

  return 0;
 }
Однако, вопреки ожиданиям, при запуске программы будет напечатано следующее:
 Сперва a = 5, b = 10.
 А теперь a = 5, b = 10.
Как видим, функция Exchange "очень похожа на настоящую, только не работает". Если мы пройдемся по программе отладчиком (как это сделать - намеренно не говорю, обратитесь ксправке по той системе разработки, которую применяете), то мы увидим, что внутри функции Exchange значения переменных меняются - проблема в передаче их назад в вызывающую функцию.

Как мы помним, при передаче параметра функции передается копия фактического параметра и обратно не возвращается. Таким образом, фунцкия может изменять его так, как ей нравится - на вызывающей функции это никак не отразится. Но как нам выйти из данного положения? Можно, конечно, при помощи return передать обратно одну из измененных переменных, но не больше. Как же нам передать два числа?

Вспомните четвертый выпуск рассылки, посвященный базовым понятиям. Там я писал, что переменная, кроме всего прочего, характеризуется адресом - ее положением в памяти. Что будет, если мы получим адрес некоторой переменной? Ответ очевиден - мы сможем неявно узнать ее значение и изменить его. К счастью, язык С облаает простыми средстваим для манипуляции с адресами.

int *a; Объявляет переменную a типа "указатель на int", то есть содержащую адрес некоторого целого значения.
&x Возвращает адрес переменной a.
*p Возвращает "переменную" по адресу, находящемуся в пременной p.

Почему я в последнем случае написал "переменную", а не "значение переменной"? Потому что это выражение (*p) может стоять как в правой части оператора присваивания, так и в левой - в первом случае будет возвращено значение, хранящееся в памяти по адресу p, во втором - в память по этому адресу запишется значение. Выражения, обладающие такими свойствами, называют L-выражениями (L-Value).

Теперь, объединяя все это, получаем программу:
 #include <stdio.h>

 void Exchange (int *x, int *y)
 {
  int t;

  t = *x;
  *x = *y;
  *y = t;
  return;
 }

 /* Теперь проверим ее работу */
 int main (void)
 {
  int a, b;

  a = 5;
  b = 10;
  printf ("Сперва a = %d, b = %d.\n", a, b);
  Exchange (&a, &b);
  printf ("А теперь а = %d, b = %d.\n", a, b);

  return 0;
 }
Запуская программу, получим следующее:
 Сперва a = 5, b = 10.
 А теперь a = 10, b = 5.
Как мы можем убедиться, она работает. Еще раз пройдусь по программе.
  1. В функции переменные x и y теперь объявлены как указатели на int.
  2. Используется оператор *x для получения значения переменной и присваивания ей нового значения.
  3. В вызывающей программе вместо передачи значений переменных передаются их адреса.

Обратите внимание на различие между выражениями x = y и *x = *y. Во втором случае в память по адресу, хранящемуся в переменной x будет записано значение, находящееся в памяти по адресу, хранящемуся в переменной y. Во втором - в переменную x будет записано значение переменной y. В сложных случаях (когда переменные создаются во время работы программы) такая ошибка (если, конечно, это ошибка) может привести к катастрофическим последствиям.

Да, логично предположить, что операция взятия адреса может быть применена только к тому, у чего есть адрес, то есть к L-выражению. Попытка скомпилировать нечто вроде c = &(a + b), равно как и c = &(&a) приведет к ошибке.

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

Да, сразу хочу заметить, что в школе вас всех учили, что a + b = b + a. Забудьте об этом раз и насвегда! :-) Во всяком случае, в С это неверно. Выполнение этого правила есть всего лишь частный случай, проявляющийся далеко не всегда. Простой пример: я уже говорил, что указатель можно сложить с целым числом. Целое число с указателем сложить нельзя.

В С может быть объявлен указатель на все, что угодно. (Есть одно исключение - с ним мы познакомимся позже). Например, на указатель:
 int **p;
- и даже на функцию:
 #include <stdio.h>

 void MyFunc (int a, char *b)
 {
  printf ("a = %d, *b = %d\n", a, *b);
 }

 int main (void)
 {
  void (*f)(int a, char *b);
  int a;
  char b;

  a = 5;
  b = 10;
  f = MyFunc;

  (*f)(a, &b);
  return 0;
 }
Обратите внимание на отличие между int (*f)(int a) и int *f(int a). В первом случае f - это указатель на функцию, возвращающую целое значение, а во втором - это функция, возвращающая указатель на целое значение. Когда я расскажу вам о массивах, я приведу правило разбора сложных типов. В С объявления типов, как вы уже могли догадаться, не столь очевидны, как, скажем, в Паскале.

Несмотря на то, что переменная типа void объявлена быть не может, указатель на void может быть объявлен. Он совместим по присваиванию со всеми остальными указателями (опять-таки за одним исключением, о котором - очень нескоро). Так как размер значения типа void равен нулю, то к ней невозможно применить операцию взятия значения (*b.)

Да, я совсем забыл рассказать вам о приведении типов. Приведение типов необходимо вам тогда, когда значение одного типа нужно трактовать как значение некоторого другого типа. Записывается приведение так:
 (имя_типа) значение
Здесь значение приводится к типу имя_типа.

Приводить можно только простые типы (то есть все те, о которых шла речь до этого). Вообще приведение типа возможно тогда, когда исходный и конечный типы имели или сходный тип (то есть вроде как одинаковый, но не совсем, например, int и char, int и float), или одинаковый размер (например, int и void*). Это значит, что нельзя привести float в char*. Но это можно сделать косвенно:
 char* d;
 float s;

 d = (char*) ((int) s);
Понравилось? :-) То есть мы сперва приводим float к int (это возможно - родственный тип), а затем int к char* (одинаковый размер). Вот только, боюсь, результат смысловой нагрузки нести не будет...

Вообще-то приведение типов использовать нужно, но делать это надо очень осторожно, всякий раз по размерам прикидывая, что получится. Пример:
 #include <stdio.h>

 int main (void)
 {
  unsigned short int a;
  unsigned char *p1;
  unsigned short int *p2;
  long int *p4;

  a = 32770;

  p1 = (unsigned char*) &a;
  p2 = (unsigned short int*) &a;
  p4 = (long int*) &a;

  printf ("*p1 = %d\n*p2= %d\n*p4 = %d\n", *p1, *p2, *p4);
  return 0;
 }
Вот результаты работы этой программы на моем компьютере:

 *p1 = 2
 *p2= 32770
 *p4 = -859013118
В чем дело? Как вы помните, unsigned short int занимает в памяти 2 байта, char - 1 байт, long int - 4 байта. При приведении (char) a результат был бы тот же - старший байт просто отбрасывается. А вот при приведении (long int) a старшие байты нового числа заполняются компилятором должным образом. При приведении же *((long int*) &a) старшие байты никак не заполняются. А ввиду того, что там находится некоторый мусор, мы получаем неверное значение. Это следует иметь в виду.

Таким образом, если нам неободимо взять значение по переменной типа void*, нам необходимо сперва преобразовать ее к требуемому типу.

На сегодня все. Домашнее задание:
  1. Напишите функцию, которая большее из двух данных чисел заменяетих полусуммой, а меньшее - полуразностью.
  2. Напишите функцию, которая принимает как параметр целочисленную функцию целочисленного аргуметна и печатает на экране таблюцу ее значений от 0 до 20.

До встречи!

Ведущий рассылки, av

http://subscribe.ru/
E-mail: ask@subscribe.ru

В избранное