Федеральное агентство по образованию ТВЕРСКОЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ __________________________________...
7 downloads
246 Views
223KB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
Федеральное агентство по образованию ТВЕРСКОЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ ______________________________________________________________ Кафедра автоматизации технологических процессов
В. Г. Васильев Использование указателей в программах на языках С / С++ / C#
Методические указания к практическим занятиям для студентов 3 курса специальности 210200 "Автоматизация технологических процессов и производств" по курсу " Системное программное обеспечение ЭВМ"
Tверь 2008
2
УДК 004.43(075.8) ББК 32.973 – 018.1я7 Методические указания к практическим занятиям для студентов 3 курса специальности 210200 "Автоматизация технологических процессов и производств" предназначены для ознакомления студентов с методами доступа к памяти персональных компьютеров с применением указателей из программ, написанных на языках С/С++/C#. Пособие содержит примеры программ, иллюстрирующих практическую реализацию изложенных теоретических положений. Методические указания обсуждены на заседании кафедры и рекомендованы к печати (протокол № 8 от 12.02.2008 г.). Составитель В.Г.Васильев.
© Тверской государственный технический университет, 2008 © Васильев В.Г., 2008
3
СОДЕРЖАНИЕ ВВЕДЕНИЕ 1. УКАЗАТЕЛИ В ПРОГРАММАХ ДЛЯ ОС WINDOWS 1.1 Указатели на базовые типы данных 1.2 Основные операции, связанные с применением указателей 1.3 Указатель на неопределенный тип данных 1.4 Указатели на структурные переменные и объекты классов 1.5 Указатели на функции 1.6 Массивы и указатели 1.7 Использование указателей для связи функций 1.8 Массив указателей 1.9 Указатель на указатель или адрес адреса 1.10 Динамическое выделение памяти под многомерные массивы 1.11 Ошибки в программах, возникающие из-за неправильного использования указателей и методы их устранения. 2. УКАЗАТЕЛИ В ПРОГРАММАХ ДЛЯ ОС DOS 2.1 Сегментная адресация памяти процессора Intel 8086 2.2 Ближние и дальние указатели 2.2 Полезные макросы для работы с указателями из сред разработки 16разрядных DOS – приложений 3. УКАЗАТЕЛИ В ПРОГРАММАХ НА ЯЗЫКЕ C# ДЛЯ ПЛАТФОРМЫ .NET. 3.1 Небезопасный код 3.2 Ключевое слово unsafe 4 ЗАКЛЮЧЕНИЕ ЛИТЕРАТУРА ВВЕДЕНИЕ Применение языков программирования С/C++ наиболее эффективно для решения задач системного программирования и для разработки различных инструментальных средств. Данные языки позволяют осуществлять работу с адресами памяти компьютеров через специальные переменные, называемые указателями. Поэтому свободное владение механизмом применения указателей является одним из ключевых моментов разработки сложных и эффективных программ. Данная работа состоит из трех разделов. Первый раздел посвящен применению указателей при разработке программ для OS Windows. При разработке таких программ в подавляющем числе приложений используется уже не язык С, а его наследник С++. Однако механизм применения указателей в программах одинаков в обоих языках.
4
Второй раздел посвящен применению указателей при разработке программ для OS DOS. Применение указателей при разработке DOS- программ имеет некоторые особенности, которые полезно знать. В частности, при написании программ, требующих монопольного использования процессора (однозадачный режим). Третий раздел посвящен применению указателей при разработке программ на языке C# для платформы .NET. Здесь применение указателей весьма специфично. 1. УКАЗАТЕЛИ В ПРОГРАММАХ ДЛЯ ОС WINDOWS 1.1 Указатели на базовые типы данных Указатель - это поименованный адрес памяти компьютера. Это означает, что в Си – программах адресам переменных могут быть даны имена, что позволяет обращаться к переменным, используя их символическое обозначение. К примеру, адрес здания по проспекту Ленина, д. 25 имеет имя: корпус «ХТ ТГТУ». Использование имени часто оказывается более удобным, чем использование адреса. Для примера рассмотрим следующую Си - программу. /* программа # 1.1 */ void main( void ) { char Ch; int count; } В программе объявлено две переменных: первая символьного типа, а вторая целого. Для первой переменной при компиляции программы будет выделен один байт, а для второй - четыре байта памяти (в 32–х разрядной машине). Для того чтобы узнать адрес байта переменной Ch и адрес первого байта переменной count, необходимо объявить два указателя и присвоить им адреса соответствующих переменных, как показано в программе #1.2: /* программа # 1.2 */ void main ( void ) { char Ch, *point_Ch; int count,*point_count; Ch = 'T'; count = 0; point_Ch = &Ch; // операция получения адреса и инициализация указателя point_count =&count; // операция получения адреса и инициализация указателя
5
return; } Приведенные объявления в программе читаются следующим образом: point_Ch есть указатель на тип сhar. Переменная point_count есть указатель на тип int. Указатель может быть объявлен на любой тип данных без исключения. Это может быть структура, класс, функция, объединение и пр. Выбор имен (идентификаторов) указателей дело программиста. Обязательным является символ '*' перед именем идентификатора. Именно этот символ характеризует данную переменную как указатель. При компиляции программы будет выделена память для хранения двух адресов. В 32 – разрядной машине это - 4 байта независимо от того, с каким типом данных будет работать указатель. Символ '&', стоящий перед именем переменной, означает операцию получения адреса этой переменной. В двух последних операторах программы переменной point_Ch присваивается адрес переменной Ch, а переменной point_count - адрес переменной count. Другими словами, в двух последних операторах выполняется инициализация указателей. Теперь адрес переменной Ch имеет имя point_Ch, а переменной count - point_count. Для того программу.
чтобы
узнать
адреса переменных Ch и count, усложним
/* программа # 1.3 */ # include <stdio.h> void main ( void ) { char Ch, *point_Ch; int count,*point_count; Ch = 'T'; count = 0; point_Ch = &Ch; point_count =&count; printf("Адрес переменной Ch = %08X \n",point_Ch); printf("Адрес переменной count = %p \n",point_count); } В начале поясним, что означает спецификация формата 0Х8 в первом операторе printf. 0 - напечатать ведущие (незначащие) нули в адресе. 8Хотвести 8 позиции для выдачи на экран значения адреса, которое должно быть представлено шестнадцатеричным числом. Х прописное означает, что если в шестнадцатеричном числе имеются символы, то они должны быть напечатаны прописными буквами. Во втором операторе printf используется спецификация формата p, которая специально предназначена для печати адресов. Адрес
6
самого указателя также может быть получен через операцию &. Например, с помощью оператора программы printf("Адрес переменной point_Ch = %08X\n",&point_Ch); На экран будет выведен адрес самого указателя point Ch (точнее адрес первого байта, поскольку самая переменная 4-x байтная). 1.2 Основные операции, связанные с применением указателей Инициализация указателя. Прежде чем использовать указатель он должен быть инициализирован адресом переменной. 1.
point_Ch = &Ch; // операция получения адреса и инициализация указателя. Указатель рекомендуется инициализировать немедленно. В противном случае он должен быть установлен в ноль. int *point_count = NULL; 2. Операция получения адреса самого указателя. Пример: char *point_Ch; printf("Адрес переменной point_Ch = %08X\n",&point_Ch); 3. Операция чтения содержимого памяти ПК по адресу, на который ссылается указатель. /* программа # 1.4 */ # include <stdio.h> void main ( void ) { int count, count_1, *point_count; count = 5; point_count =&count; count_1 = *point_count; printf("Значение count_1 = %d\n",count_1); // или сразу так printf("Значение count_1 = %d\n",*point_count); }
7
В операторе count_1 = *point_count; перед переменной point_count стоит оператор ‘*’ и сам оператор стоит в правой части оператора присваивания. Это означает операцию чтения (получения) числа по адресу. В итоге переменной count_1 будет присвоено число 5. Обратите внимание, что перед тем, как выполнить операцию чтения памяти по адресу, выполнена инициализация указателя (указателю присвоен адрес переменной count_1). В программе 1.5 /* программа # 1.5 */ # include <stdio.h> void main ( void ) { int count,count_1, *point_count; count = 5; count_1 = *point_count; printf("Значение count_1 = %d\n",count_1); } содержится грубейшая ошибка - отсутствует операция инициализации указателя адресом переменной. При загрузке программы point_count будет содержать случайный набор бит, оставшийся от предыдущей загрузки памяти другой программой, который чисто формально можно толковать как адрес. Но в этом случае, программа работать не будет. Одно из основных правил работы с указателями состоит в том, что в программе не должно быть не инициализированных указателей. 4.
Операция записи чисел по адресу, на который ссылается указатель. В программе 1.6 /* программа # 1.6*/
# include <stdio.h> void main ( void ) { int count, *point_count; count = 5; point_count = &count; *point_count = 10; printf("Значение count = %d\n",count); } указатель point_count стоит в левой части оператора присваивания. По адресу, на который ссылается указатель, будет записано число 10. Эквивалентный оператор программы count = 10.Это нетрудно показать следующим образом.
8
Поскольку point_count = &count, то оператор *point_count = 10 может быть записан так: *&count = 10; Здесь в начале будет получен адрес переменной count( операция *), а затем по этому адресу записано число 10, что равносильно оператору count = 10.Таким образом, операция * уничтожает операцию &. Таким образом, если операция ‘*’ (ее еще называют операцией операцией снятия ссылки ) находится в левой части операции присваивания, то выполняется операция присваивания (записи по данному адресу). И наоборот, если операция ‘*’ находится в правой части операции присваивания, то выполняется операция чтения по адресу. Если в первом случае данные могут быть испорчены (опасная операция) , то во втором - нет. Объясним теперь, почему при объявлении указателей необходимо указывать тип данных, на который ссылается указатель. Причина в том, что в операциях чтения и записи по адресу операции выполняются с тем числом байт памяти, которые в данной системе программирования отводятся для хранения данных соответствующего типа. К примеру, если указатель объявлен на тип данных double (8 байт), то в операциях чтения и записи работа будет осуществляться с 8 байтами. Если указатель объявлен на тип int, то c 4байтами пр. Приведем пример программы #1.7, поясняющей возможность побайтного чтения памяти. /* программа # 1.7 */ # include < stdio.h> void main() { char *point_Ch; unsigned long l; int i; l = 0x89ABCDEF; point_Ch = (char *)&l; for ( i = 0; i < sizeof ( long) ; i++) { printf("%x ", point_Ch,*point_Ch & 0x00ff); point_Ch ++; }
9
} Здесь указателю point_Ch присвоен адрес длиной целой l = 0x89ABCDEF. В памяти эта переменная занимает 4 байта. В цикле на каждой итерации значение адреса увеличивается на единицу. В операторе point_Ch ++ осуществляется переход к следующему байту памяти. При этом каждый раз читается только один байт, так как указатель point_Ch ссылается на тип char. В операторе printf(...) выводится значение по адресу. При выполнении этой программы был получен такой результат: efcdab89 Исходное число выведено на экран в обратном виде, так как младшие разряды числа хранятся по младшим адреса памяти, а старшие – по старшим. ЗАМЕЧАНИЕ. Подумайте, как сделать программу, чтобы число на экране выглядело правильно. 5. Арифметические операции с указателем Программа # 1.7 демонстрирует еще одну операцию с указателями. Указатель это число, под которым подразумевается адрес памяти. Сложение или вычитание влечет за собой изменение адреса. В приведенной программе это реализовано оператором point_Ch ++; где ++ - операция инкремента на единицу. Фактически адрес увеличивается не на единицу, а на то количество байт памяти, с каким типом данных работает указатель. 6.
И, наконец, последняя операция - присваивание одному указателю значения другого, что и показано в программе #1.8: /* программа # 1.8 */
# include < stdio.h> void main() { char *point_Ch; unsigned long l,*point_l; point_l = &l; point_Ch = (char *)point_l; } В последнем операторе выполнено приведение типов данных. Если этого не сделать, то компилятор выдаст ошибку при компиляции программы.
10
1.3 Указатель на неопределенный тип данных Существует специальный тип указателя, называемый указателем на неопределенный тип. Для объявления такого указателя вместо спецификатора типа указателя задается ключевое слово void void * point; Ключевое слово void в объявлении указателя позволяет отсрочить определение типа, на который ссылается указатель. Часто это дает возможность сделать программы более эффективными. Рассмотрим пример. void main () { char a; int *point; point = &a; } В этой программе указателю на тип int присваивается адрес переменной символьного типа. При этом компилятор выдаcт ошибку. Однако, ошибки не будет если поступить так, как это сделано в программе # 1.9 /* программа #1. 9 */ void main () { char a; int b; float c; void *point; point = &a; point = &b; point = &c; } Таким образом, если указатель объявить с ключевым словом void, то ему можно присваивать адреса любых типов данных. Но для того, чтобы оперировать таким указателем или объектом, который он адресует, необходимо явно задать требуемый тип данных в каждой операции с указателем. Это делается с помощью операции приведения типа, как показано в программе # 1.10. /* программа # 1.10 */ void main () { double c;void *point; point = &c;
11
*((double *)point) = 3.1415926; /* приводим указатель к типу double и выполняем операцию записи по адресу printf("Значение числа PI = %lf\n",c);
*/
} Фактически оператор программы: *((double *)point) = 3.1415926; означает, что в данный момент мы работаем с данными типа double. Часто применение указателей на тип void бывает полезным. 1.4 Указатели на структурные переменные и объекты классов В приведенных примерах были использованы указатели на базовые типы данных языка Си. Однако использование указателей гораздо шире. Пусть в программе объявлена структура, имя которой _BOOK_. struct _BOOK_ { char autor[20]; int cost; } book, *point_book; Оператором программы point_book = & book; будет инициализирован указатель адресом переменной book. Для доступа к полям структуры через указатель используется операция "стрелка", которая в Си программах выглядит так '->'.К примеру, поместим в поле структуры autor фамилию автора книги: strcpy( point_book -> autor,"ИВАНОВ И. И."); Функция strcpy () предназначена для копирования строк. Эквивалентная, но более длинная и потому редко используемая запись, выглядит таким образом: strcpy( (*point_book ). autor,"ИВАНОВ И. И."); В данном случае это запись по адресу. В следующем примере показана возможность чтения поля структуры и присваивания значения cost поля переменной cost_book: int cost_book; cost_book = point_book -> cost ;
12
//
cost_book = (*point_book). cost ; можно и так, но это длинно.
Аналогичным образом осуществляется доступ к членам классов. /* программа # 1.11 */ class A { public: int z; }; void main () { A s , *point; // создаем объект s типа A point = &s; // инициализируем указатель адресом объекта s; point ->z = 100; // записываем в z число 100 } Часто требуется выделять память для создания объектов классов динамически. И в этом случае требуется использовать указатель. /* программа # 1.12 */ class A { public: int z; }; void main () { A * point = new A; // создаем объект типа A и инициализируем указатель адресом это объекта. point ->z = 100; // записываем в z число 100 …………………… // Другие операторы ……………………. delete point; // незабываем возвращать память ОС } Каждому оператору new должен соответствовать свой оператор delete. 1.5 Указатели на функции Указатель на функцию является адресом точки входа в функцию. Имя функции также является символическим адресом ее точки входа. Другими
13
словами, символическим именем, или указателем. Покажем это на следующем примере. /* программа # 1.13 */ # include < stdio.h> void bell(void); void main() { void (*f)( void ); f = bell; f(); (*f)(); // можно и так } /* эта функция издает гудок */ void bell (void) { putchar(7); } Объявление void (*f)( void );означает, что f является указателем на функцию, возвращающую тип void c пустым списком формальных параметров. К примеру объявление int (*func)( int, float );означает что func ecть указатель на функцию возвращающую тип int c двумя формальными параметрами типа int и float. В объявлении указателя на функцию очень важным является синтаксис. Обратите на это внимание. Если в программе объявлен указатель, то он должен быть инициализирован. Это сделано в операторе f = bell. В данном случае указателю f присвоен адрес функции bell. Идентификатор bell - есть символический адрес памяти, с которого будет выполняться функция. Далее в программе последовательно дважды выполняется обращение к функции bell через указатель. При этом компьютер дважды издаст звуковой сигнал. В программах указатели на функции, в основном, используются • для сохранения адресов программ обработчиков прерываний; • для передачи функций в качестве формальных параметров другим функциям. Следующая программа # 1.14 демонстрирует такую возможность при вычислении интеграла методом площадей.
14
/* программа # 1.14 */ /* прототипы функций */ float integral (float (*)(float ),float ,float ,float ); float func ( float ); # include < stdio.h> void main ( void ) { float integ; /* - нижний предел интегрирования = 0.0; - верхний предел интегрирования = 12.0; - шаг по оси x = 0.2; */ integ = integral (func, (float)0.0,(float )12.0,(float) 0.02); printf ("Интеграл = %f\n",integ); } /* вычисление интеграла методом площадей */ float integral (float (*f)(float ),float l_limit, float u_limit, float delta_x) { /* float (*f)(float ) - указатель на интегрируемую функцию, возвращающую тип float и с одним формальным параметром типа float; float l_limit - нижний предел интегрирования ; float u_limit - верхний предел интегрирования ; float delta_x - шаг по оси x; */ float x, summa =0.0; x=l_limit + delta_x / 2.0; while ( x <= u_limit) { summa += delta_x * f( x); x = x + delta_x; } return summa; } /* интегрирумая функция у = х */ float func ( float x ) { return x; }
15
При выполнении этой программы был получен такой результат: Интеграл = 72.000137,в то время как точное значение равно 72.0. 1.6 Массивы и указатели Между массивами и указателями существует тесная связь, поэтому их обычно рассматривают вместе. Массив представляет собой группу однотипных элементов, которые располагаются в памяти компьютера последовательно. Рассмотрим пример. /* программа # 1.15 */ void print_string (char *); /* прототип функции print_string */ void main() { char *string = "HELLO,WORLD"; print_string (string); } /*эта функция печатает строку # include <stdio.> void print_string (char *array_ch) { printf ("s%",array_ch) } В этой программе объявлен указатель на тип char. Укaзатель инициализирован адресом строки "HELLO,WORLD", которая представляет собой массив символов, располагающийся в памяти байт за батом. string, следовательно, указывает, на символ 'H'.string+1 - на символ 'E'.string+2 - на символ 'L' и т. д. Далее в программе следует вызов функции print_string, которая печатает строку. Функция имеет один формальный параметр укaзатель на тип char. При вызове функции ей передается адрес строки "HELLO,WORLD". Адрес строки поименован идентификатором string. Таким образом, имя (идентификатор) массива является адресом его первого элемента. Вызов функции print_string может выглядеть и так: print_string (&string[0]); В данном случае используется операция получения адреса первого элемента массива, поименованного идентификатором string. При вызове функции print_string (&string[6]);
16
на экран будет выдана стрoка WORLD, так как счет элементов массива начинается с нуля (символ 'W' является седьмым элементом). Рассмотрим другую программу. /* программа # 1.16 */ void array_1(int ,int *); void main() { int array[20]; array_1 (20,array); } /*эта функция заполняет массив единицами */ void array_1(int n,int *array) { int i; for (i = 0 ; i < n; i++) *(array + i) = 1; /// что эквивалентно array[i] = 1; } В этой программе объявлен целочисленный массив из 20 элементов. При вызове функции array_1 массив заполняется единицами. Формальными параметрами являются число элементов массива и адрес первого элемента, который поименован в данной программе идентификатором array. В теле функции в операторе *(array + i) = 1; в скобках вычисляется адрес элемента массива, а затем используется операция '*' - запись по адресу. Эквивалентный оператор: array[i] = 1; Отсюда ясно, почему индексация элементов массива в Си начинается с нуля: используется базовый адрес массива + смещение. Если смещение равно нулю (i =0), то мы адресуемся к первому элементу массива. 1.7 Использование указателей для связи функций В большинстве языков программирования параметры подпрограммам (функциям) передаются либо по ссылке (by reference), либо по значению (by value). В первом случае подпрограмма (функция) работает с адресом переменной, переданным ей в качестве фактического параметра. Во втором случае ей доступна не сама переменная, а только ее значение (копия числа). Различие здесь такое: переменную, переданную по ссылке, функция может модифицировать в вызвавшей ее функции, а переданную по значению - нет. В языке Си параметры передаются только по значению. Общепринятый способ обеспечить функции непосредственный доступ к какой-либо переменной из вызывающей подпрограммы состоит в том, чтобы вместо самой переменной в качестве параметра передавать ее адрес.
17
Рассмотрим программу, в которой осуществляется обмен значений переменных с помощью функции swap (). /* программа # 1.16 */ void swap(int *,int *); void main() { int a = 10,b = 20; swap (&a,&b); } /*эта функция выполняет обмен значений переменных */ void swap(int *x,int *y) { int c; /* временная переменная */ c = *x; /* запоминаем значение переменной, на которую ссылается указатель х */ /* с = х; это неверно так как переменной с будет присвоен адрес ,а не само значение переменной, на которую ссылается указатель х */ *x =*y; /* переменной,на которую ссылается указатель х, присваиваем значение переменной, на которую ссылается указатель y */ *y = c; /* переменной,на которую ссылается указатель y, присваиваем значение из буфера */ При вызове функции передаются адреса переменных a и b. Это значит, что формальные параметры функции должны быть описаны как указатели на соответствующий тип данных. При таком описании формальных параметров вызываемой программе становятся доступными адреса переменных из вызывающей программы, которые являются в ней локальными. Если известны адреса, то применимы операции чтения и возврата по адресу. Часто в программах требуется передавать функциям объекты классов или структурные переменные. Если передавать объект по значению, то будут иметь место большие накладные расходы на копирование памяти. Проще передавать указатель на объект – всего 4 байта памяти, содержащие адрес объекта. Доступ к члену класса в этом случае внутри функции должен осуществляться оператором ‘->’. #include #pragma hdrstop #pragma argsused void calcul ( class A * ptr); class A
18
{ public: int a; }; int main(int argc, char* argv[]) { A a; calcul (&a); return 0; } void calcul ( class A * ptr) { ptr-> a = 100; // доступ к члену а класса A } 1.8 Массив указателей Часто в программах требуется объявить массив строк. Это можно сделать, используя массив указателей, как показано ниже. /* программа # 1.17 */ # define DIM(x) (sizeof(x) / sizeof( x[0] )) void main() { /* char *col[3] - массив указателей (адресов) из трех элементов*/ char *color[] = { /* это инициализация массива адресами строк */ "RED", "BLUE","GREEN " }; short i; /* выводим названия цветов на экран */ for ( i = 0; i < DIM (color); I ++) printf("%s\n", color[i]); } В этой программе первому элементу массива color присваивается адрес строки "RED" (индекс массива i = 0), второму элементу массива color[1] адрес строки "BLUE" и т.д. Затем значения массива выводятся на экран с помощью функции printf( ).
19
1.9 Указатель на указатель или адрес адреса Объявление в программе: int **point -означает, что переменная point используется для хранения адреса адреса. Указателю point при распределении памяти будет выделено также 4 байта. Проиллюстрируем возможность использования таких указателей следующей программой. /* программа # 1.18 */ void main() { int a,*point_1,**point_2,***point_3; a = 5; point_1 = &a;/* инициализируем указатель адресом переменной а */ point_2 = &point_1; /* это адрес адреса переменной а */ point_3 = &point_2; /* это адрес адреса адреса переменной а */ ***point_3 = 10; /* присваиваем переменной а значение 10 */ } В последнем операторе выполняется запись числа по адресу адреса адреса переменной а. В этом примере показана простая, двойная и тройная косвенные адресации. 1.10 Динамическое выделение памяти под многомерные массивы Часто двойная косвенная адресация используется для выделения памяти под двумерные массивы, тройная - под трехмерные и т. д. Программа # 1.19 иллюстрирует выделение памяти под двумерный массив типа float, возврат память OS и печать матрицы. Основная идея программы - представление двумерного массива как совокупности некоторого числа одномерных массивовстрок матрицы. Подробности содержатся в комментариях программы. /* программа # 1.19 */ #include #include #include <stdio.h> #pragma hdrstop #pragma argsused // Протипы функций, используемых в программе float ** Get_mem ( int n, int m ); void Del_mem ( float ** A, int n ); void print_matrix (float **A, int n, int m, char *format);
20
int main(int argc, char* argv[]) { int n,m; n =10; m=10; float ** Matrix = Get_mem ( n, m); // Выделяем память под двумерный массив // 10*10 for ( int i =0; i < n ; i ++) // строим единичную матрицу- на главной диагонали, которой - единицы for ( int j =0; j < m ; j ++) if ( i == j ) Matrix [i][j]= 1.0; print_matrix (Matrix, n, m, "%3.1f "); Del_mem (Matrix,n); // Возвращаем память ОС getch (); return 0; } // Выделяем память для матрицы в n строк и m – столбцов float ** Get_mem ( int n, int m ) { float ** buffer; buffer = new float *[n]; // выделяем память под массив из n элементов. Этот //массив будет хранить адреса строк (массивов) матрицы. Поэтому в операторе //new тип данных float *(адреса данных типа float). Адрес данного массива записываем в переменную buffer, смысл которой - адрес адресов (массив указателей). for ( int i =0; i < n ; i ++) // выделяем память n –раз под массивы из m //элементов - строки матрицы buffer[i] = new float [m]; // Обнуляем матрицу for ( int i =0; i < n ; i ++) for ( int j =0; j < m ; j ++) buffer[i][j] = 0.0; return buffer; // возвращаем адрес массива указателей } // возвращаем память OS
21
void Del_mem ( float ** A, int n ) { for ( int i =0; i < n ; i ++) // удаляем память – строки матрицы delete[] A[i]; // Так правильно удаляется память, выделенная под массив // delete[] delete[] A; // удаляем сам массив указателей return; } // Вывод матрицы на экран void print_matrix (float **A, int n, int m, char *format) { for ( int i =0; i < n ; i ++) { for ( int j =0; j < m ; j ++) printf (format, A[i][j]); printf ("\n"); } return; } 1.11 Ошибки в программах, возникающие из-за неправильного использования указателей и методы их устранения. Большинство ошибок в программах на языке С/ С++ является следствием неправильного использования указателей. Во-первых, применение неинициализированного указателя недопустимо. Во-вторых, если память выделяется динамически с помощью оператора new, то нельзя забывать об операторе delete. В противном случае будет иметь место утечка памяти. Втретьих, к указателям не установленным в ноль оператор delete нельзя применять дважды. delete [] massiv; // выполняется код программы …………………………… …………………………… …………………………… // и повторно delete [] massiv; // так нельзя Однако, если указатель установить в ноль, то повторное использование delete уже безопасно. delete [] massiv; massiv = NULL; delete [] massiv; // так безопасно
22
Поэтому используйте следующий код перед тем, как применять оператор delete: if (massiv) { delete[] massiv; massiv = NULL; } Оператор гарантирует, что высвобождение памяти будет выполняться только в тот случае, если выражение истинно – указатель massiv не обнулен. 2. УКАЗАТЕЛИ В ПРОГРАММАХ ДЛЯ ОС DOS Прародитель современных процессоров корпорации Intel процессор Intel 8086 имел 20 -разрядную шину адреса. Нетрудно подсчитать, что два в двадцатой степени равно 1048576. Поэтому процессор мог адресоваться к 1048576 байтам памяти, или к 1 Мбайту. В DOS нумерацию памяти принято считать с нуля. Поэтому порядковый номер самого первого байта памяти в десятичной системе счисления - 0, а последнего - 1048575 (в 16-й системе счисления диапазон адресов 00000h -FFFFFh). 2.1 Сегментная адресация памяти процессора Intel 8086 Физический адрес - это 20-битное беззнаковое целое в диапазоне 0-FFFFFh, которое идентифицирует положение байта в пространстве памяти 1 Мбайт. Регистры процессора Intel 8086 были 16-ти разрядные. Следовательно, максимальное значение, которое может быть записано в регистр процессора FFFFh. Таким образом, для того чтобы хранить адрес памяти из всего адресного пространства процессора, не хватает одного 16-го разряда или 4-х двоичных разрядов. Поэтому в процессорах семейства Intel 80x86 используется двухкомпонентная адресация. В этой связи существует понятие логического адреса.
23
Это происходит следующим образом. Процессор расширяет сегментный регистр (в котором хранится начальный адрес сегмента) четырьмя нулевыми битами (это равносильно умножению адреса начала сегмента в шестнадцатеричной системе счисления на 10h) и прибавляет к полученному значению смещение адреса как показано на рисунке:
Например, если адрес сегмента равен 1234h, смещение равно 1116h, то полный (исполнительный) 20 -разрядный адрес будет 12340h+1116h=13456h. Таким образом, оперируя 16-разрядными адресами сегмента и смещением, процессор может адресовать 1Мбайт памяти. Для хранения сегментных адресов и смещений процессор имеет специальные регистры: CS,DS,ES,SS. Эти регистры содержат соответственно адреса сегментов кода, данных, дополнительных данных и стека. Физический адрес памяти принято записывать таким образом: XXXXh:XXXXh (адрес сегмента : адрес смещения в сегменте). 2.2 Ближние и дальние указатели В DOS - программах можно использовать указатели типа near (близкие) и far (дальние). Для Windows-программ понятие ближнего и дальнего указателя не существует. В языке С++ нет и ключевых слов near и far. Для явного объявления ближнего указателя используется ключевое слово near, например, float near * point;. Если просто float * point;, то по умолчанию данный указатель ближний и в нем хранится смещение некоторой переменной относительно начала сегмента, в котором размещена данная переменная (сегмент стека или сегмент данных программы). Для хранения значения смещения достаточно двух байт памяти. Поэтому всем переменным типа указатель, которые объявлены явно или по умолчанию как близкие, выделяется два байта памяти. Ключевое слово far используется для объявления дальнего указателя, например, float far * point;.
24
В этом случае переменной point, будет выделено уже 4 байта для хранения сегментного адреса переменной и смещения ее в сегменте. Сказанное иллюстрируется следующей программой: /* программа # 2.1 */ # include <stdio.h> void main ( void ) { char Ch,far *point_Ch; int count,far *point_count; Ch = 'T'; count = 0; point_Ch = &Ch; point_count =&count; printf("Адрес переменной Ch = %Fp\n",point_Ch); printf("Адрес переменной count = %Fp\n",point_count); } Спецификация формата Fp используется для выдачи на экран значений дальних указателей. При выполнении этой программы был получен такой результат: Адрес переменной Ch = 4BB6:1762 Адрес переменной count =4BB6:175C Адрес переменной count =4BCE:175C - это логический адрес, состоящий их двух компонент - адреса сегмента 4BB6 и смещения 175C этой переменной относительно начала сегмента. Нетрудно видеть, что значение адреса сегмента для переменных Ch и count одинаково, так как они объявлены внутри тела функции и, следовательно, их место на стеке программы. Число 4BB6адрес сегмента стека данной программы. В DOS- программах far указатели используются для обращения к ячейкам памяти машины за пределами адресного пространства выделенного программе. Рассмотрим следующий пример. C адреса 0000:0400 начинается область памяти, которую называют областью данных BIOS. Первые 8 байт предназначены для хранения адресов 4 коммуникационных портов (по два байта на адрес – число типа int ). Требуется прочитать адрес порта COM1. /* программа # 2.2 */ # include <stdio.h> void main ( void ) { int far *point;
25
point = (int far *)0x00000400L; printf("Значение = %x\n",*point&0x00ff); } В данном случае переменной point, объявленной как far указатель, присвоен адрес 0000:0400. Для размещения числа 0x00000400 в памяти требуется 4 байта - длинное целое (использован модификатор L). Вследствие этого выбран указатель типа far и сделано его приведению к типу (int far *) для того, чтобы левая часть оператора присваивания бала эквивалентной правой. При выполнении последнего оператора программы на экран будет выдан адрес порта – 3f8. Еще один пример демонстрирует использование указателей при создании функции, которая организует программную задержку на second секунд. void delay ( float second ) { /* 4-x байтная переменная по адресу 0000:046C -содержит текущее значение числа тиков таймера .Частота тиков таймера ПК - 18.2 раза/сек*/ float delay; long far *pcurtick = (long far *)0x0000046CL; delay = second * (float)18.2 + *pcurtick; while ((float)*pcurtick <= delay); return ; } Главным в двух данных примерах является демонстрация возможности инициализации указателя числом (конкретным адресом). При разработке Windows – программ этого делать нельзя. Обработчик события в Windowsпрограмме void __fastcall TForm1::Panel1Click(TObject *Sender) { int far *point; point = (int far *)0x00000400L; Label1 -> Caption = *point; return; } компилируется, но выполнение программы завершается выдачей окна с сообщением об ошибке.
26
2.3 Полезные макросы для работы с указателями из сред разработки 16разрядных DOS – приложений (Turbo C, Quick C) Макросы FP_SEG и FP_OFF могут быть использованы для инициализации или получения смещения и адреса сегмента переменной, на который ссылается far указатель. Определение этих макросов находится в файле <dos.h> и выглядит следующим образом: #define FP_SEG(fp) (*((unsigned *)&(fp) + 1)) #define FP_OFF(fp) (*((unsigned *)&(fp))) Прототипы: unsigned FP_OFF(void far *address); unsigned FP_SEG(void far *address); Возвращаемое значение: (FP_OFF) - беззнаковое целое, представляющее собой смещение внутри сегмента. (FP_SEG) - беззнаковое целое, представляющее собой адрес сегмента. Программа # 2.3 демонстрирует использования макросов FP_SEG и FP_OFF /* получение адреса сегмента и смещения переменной */ /* программа # 2.3 */ # include < dos.h > # include < stdio.h > void main ( void ) { char far * point, ch = 'T'; unsigned int seg_val,off_val; point = (char far * )&ch; seg_val = FP_SEG (point) ; off_val = FP_OFF (point) ; printf ("Адрес сегмента переменной ch = %04X\n",seg_val); printf ("Смещение переменной ch относительно " "начала сегмента = %04X\n",off_val); return; }
27
В этом примере макросы FP_SEG и FP_OFF стоят в правой части оператора присваивания. Следовательно, они возвращают смещение и адрес сегмента переменной. Обратите также внимание, как можно расчленить длинную строку в операторе printf(). Программа # 2.4 демонстрирует возможность использования макросов FP_SEG и FP_OFF для инициализации far указателя. /* программа # 2.4 */ # include < dos.h > # include < stdio.h > /*прототипы используемых функций */ void write_byte ( unsigned int ,unsigned , char ); char read_byte ( unsigned int ,unsigned ); void main ( void ) { /* адрес B800:0000 - адрес видеопамяти */ write_byte (0xB800,0x0000,'$'); /* записываем символ '$'*/ /* читаем и выводим на экран записанный символ */ printf("%c\n", read_byte (0xB800,0x0000)); return; } /* функция читает байт памяти, адрес которого задается в виде адреса сегмента и смещения в сегменте */ char read_byte ( unsigned int seg_val,unsigned int off_val) { char far *point; FP_SEG (point) = seg_val; FP_OFF (point) = off_val; return (*point); } /*функция записывает байт в память по адресу, который задается в виде aдреса сегмента и смещения в сегменте */ void write_byte ( unsigned int seg_val,unsigned int off_val, char byte) { char far *point;
28
FP_SEG (point) = seg_val; FP_OFF (point) = off_val; *point = byte; return; } Программа # 2.5 демонстрирует возможность использования макроса FP_MAKE для инициализации far указателя. /* программа # 2.5 */ #define FP_MAKE(seg,off) ((void far *) \ ((((unsigned long) (unsigned)(seg)) << 16L) | \ ((unsigned long) (unsigned) (off)))) # include < stdio.h> void main ( void ) { char far *point; point = FP_MAKE (0xB800,0x0000);/*инициализируем указатель*/ printf("Значение указателя = %Fp\n",point); } 3. УКАЗАТЕЛИ В ПРОГРАММАХ НА ЯЗЫКЕ C# ДЛЯ ПЛАТФОРМЫ .NET. 3.2 Небезопасный код Указатели в языке C# рекомендуется применять только в случае крайней необходимости. Тем не менее, применение указателей все же необходимо. К примеру, у Вас имеется функция из математической библиотеки, в которой для передачи параметров используются указатели. Не переписывать же эту функцию – это потенциальный источник ошибок! Использование такой функции предоставляет так называемый небезопасный (unsafe) в C# код. Небезопасным называется код, выполнение которого среда выполнения программ ( в .NET - CLR) не контролирует. Он работает напрямую с адресами областей памяти посредством указателей и этот код должен быть явным образом помечен ключевым словом unsafe. 3.2 Ключевое слово unsafe Оператор unsafe имеет следующий синтаксис: unsafe блок Все операторы, входящие в блок, выполняются в небезопасном контексте. Ключевое слово unsafe может использоваться либо как спецификатор, либо
29
как оператор. В первом случае оно определяет небезопасный контекст для описываемого типа данных, например, public unsafe struct SRT { public int value; public int *a; public float *z; } Вся структура в этом примере помечена как небезопасная, что делает возможным использование в ней указателей a и z. В другом варианте небезопасными являются только некоторые поля структуры public struct SRT { public int value; public unsafe int *a; public unsafe float *z; } В тексте программы, где используются указатели, используются следующий код: unsafe { struct SRT str, *ptr; ptr = & str; ptr-> value = 100; } 4.ЗАКЛЮЧЕНИЕ Необходимость применения указателей в программах возникает в том случае, когда требуется работать с адресами памяти непосредственно, например, при взаимодействии с операционной системой, написании драйверов или программ, время выполнения которых критично. Обычное применение указателей: динамическое выделение памяти под массивы и другие объекты, связь функций, побайтное чтение памяти. Работа с указателями требует осторожности и внимания от начинающего программиста. ЛИТЕРАТУРА 1. Л. М. Романовская, Т.В.Русс, С.Г. Свитковский. Программирование в среде Си для ПЭВМ ЕС. М. - "Финансы и статистика".- 1991. 2. Т. А. Павловская. C#. Программирование на языке высокого уровня. С. П. Б: «Питер», 2007
30
Использование указателей в программах на языках С / С++ / C# Методические указания к практическим занятиям для студентов 3 курса специальности 210200 "Автоматизация технологических процессов и производств" по курсу "Системное программное обеспечение ЭВМ"
Составитель В. Г. Васильев Технический редактор Г.В. Комарова Подписано в печать 29.02.08 Формат 60 х 84/16 Физ. печ.л 2,0
Усл.-печ.л. 1,86 РИЦ ТГТУ
Бумага писчая Уч.-изд. л. 1,74