Создание нового проекта консольного типа, страница 8

Создайте функцию main, в которой поочередно вызывайте эти модули и анализируйте значения адресов. Попробуйте убрать скобки при операциях delete и вновь проверьте результат. При отладке этой программы следует быть очень аккуратным, так как здесь в полной мере могут проявиться те особенности работы с указателями, которые отмечены в начале параграфа. Так, если в последнем цикле for по ошибке вместо i<3 ввести, например i<5, то возможно придется перезагружать всю систему с потерей незаписанного ввода. В Windows 95 это происходит именно так. Код программы весьма ненадежен, в особенности потому, что использованы числовые константы (4, 3, 2) в качестве спецификаторов размерностей массивов. Его цель — проиллюстрировать правила освобождения памяти и возможные ситуации при этом возникающие. Возможность свободной модификации критически важных параметров таких объектов, как многомерный массив, является одним из тех недостатков структурного подхода, которые привели к необходимости поисков новых подходов. Результатом таких поисков является объектно-ориентированный подход к разработке программ, к изучению которого мы скоро перейдем. После освобождения памяти указатели a, d и dd продолжают, тем не менее, указывать на те же адреса, что и до освобождения (однако, эта память уже не наша). В этом легко убедиться, если вставить до и после операций delete вывод:

printf ("\n a=%p d=%p dd=%p dd[1]=%p",a,d,dd,dd[1]);

В Windows NT 4.0 повторное обращение к освобожденной памяти приводит к полному краху. Чтобы в этом убедиться можно вставить после вызова FreeMem такую строку:

printf ("\n *a=%2.0f d[0]=%2.0f ",*a,d[0]);

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

Вопросы и упражнения

Проанализируйте программу и объясните что она выведет.

int a[3][3]={{1,2,3}, {4,5,6}, {7,8,9}};

int *pp[3]={a[0],*(a+1),a[2]};

int *p=*a;

void main()

{

for (int i=0; i<3; i++)

  printf ("\na[%d][2-%d]=%d *a[%d]=%d *(*(a+%d)+%d)=%d",

      i,i,a[i][2-i],i,*a[i],i,i,*(*(a+i)+i));

for (puts("\n"), i=0;  i<3;  i++)

  printf ("\npp[%d]=%p  *pp[%d]=%d  p[%d]=%d",

        i,pp[i],i,*pp[i],i,p[i]);

}

Указатели на функции

Указатели делятся на две основные категории: указатели на переменные и указатели на функции. Обе категории содержат адреса памяти, но они имеют существенно различные свойства и назначение. Для них также справедливы и различные правила использования. Указатели на функцию используются с целью вызова различных функций, удовлетворяющих определенному прототипу. Выполнение каких-то арифметических операций над указателем на функцию лишено смысла и не допускается в языке C++. Указатели на переменные, обычно на массивы, допускают произведение арифметических действий над собой с целью, например, пробега по массиву. Хотя указатели и содержат числа со свойствами целых без знака (unsigned int), тем не менее для них существуют свои собственные правила и ограничения при выполнении операций присвоения, преобразования и адресной арифметики. Следующий пример демонстрирует использование указателя на функции. Объявление: double (*pFunc)(double); определяет переменную pFunc, имеющую тип указателя на функции. Эта переменная способна указывать на любую функцию, которая возвращает вещественное число и требует одно вещественное число в качестве параметра. Обрамляющие скобки обязательны, так как без них выражение задает прототип функции, которая возвращает указатель на double и требует параметр типа double.

void main()

{       // Использование указателя на функции

double (*pFunc)(double);     // Указатель

unsigned c=1;

while (c)

{                       // Цикл проверки

  printf ("\n\tSelect a function (0 - to quit)\n"

    "\n 0.  Quit"

    "\n 1.  Sqrt"

    "\n 2.  Sin"

    "\n 3.  Tan\n\n\t");

  c = getch()-'0';          // Реакция пользователя

  switch(c)

  {

  case 1: pFunc = sqrt; break;

  case 2: pFunc = sin;  break;

  case 3: pFunc = tan;  break;

  case 0: break;

  default: c =0 ;    break;

  }

  if (c)

  {

   double y = pFunc(1.);    // Вызов с помощью указателя

   printf ("\n y(1.) = %f\n",y);

  }

}

}

Обратите внимание на выражение getch()-'0', использованное для анализа символа, введенного пользователем. Вычитая код символа '0' (равный 48), мы преобразуем код введенного символа в целое число, равное расстоянию символа в таблице кодов от символа '0'. Таким образом, если была введена цифра, то выражение дает число, равное этой цифре. Если введена не цифра, то переменная unsigned c содержит некоторое недопустимое число, которое игнорируется и цикл запроса ввода повторяется.

Варианты самостоятельных заданий

Средней сложности

¨  Разработайте программу, которая создает квадратную матрицу (размер задается пользователем на этапе выполнения).

¨  Заполните ее произвольными числами, например, с помощью функции rand().

¨  Динамически создайте вектор той же размерности, что и матрица.

¨  Заполните его такими значениями, чтобы каждый терм произведения первой строки матрицы с этим вектором был равен единице.

¨  Напишите функцию умножения матрицы на вектор и с ее помощью проверьте результат.

Средней сложности

¨  Создайте динамический массив указателей на строки текста (размер задается пользователем на этапе выполнения).

¨  Введите эти строки.

¨  Отсортируйте их с помощью функции AnySort или с помощью Вашей (вновь созданной) функции.