Краткие сведения из теории

Лабораторная работа № 5

Указатели в языке программирования С.

Цель работы

Изучить основные возможности работы с указателями.

Краткие сведения из теории

По краткому определению, указатель – это переменная, содержащая адрес другой переменной. Так как указатель содержит адрес переменной (объекта), это дает возможность "косвенного" доступа к этой переменной (объекту) через указатель.

Для понимания работы и назначения указателей рассмотрим упрощенную схему организации памяти компьютера. Память представляет собой массив последовательно пронумерованных или адресованных ячеек, с которыми можно работать по отдельности или связанными кусками. Известно, что различным типам данных отводится определенное количество байтов памяти. Поэтому указатель – это группа ячеек, в которых может храниться адрес. Например, если переменная ch имеет тип char, а ptr (от латинского pointer – указатель) есть указатель на переменную ch, то взятие адреса переменной ch осуществляется с помощью унарного (одноместного) оператора &, т.е.

ptr = &ch;

Приведенная инструкция означает, что переменной ptr присваивается адрес ячейки ch. Принято считать, что ptr указывает на ch. Оператор & применяется только к объектам, расположенным в памяти: к переменным, элементам массива. Операндом оператора & не может быть ни выражение, ни константа, ни регистровая переменная. Унарный оператор & называется еще оператором адресации.

Имена указателям даются в соответствии с правилами, принятыми в языке программирования С для обычных переменных.

Другая унарная операция * называется операцией ссылки по указателю ( indirection ), или разыменования ( dereferencing ). Если применить ее к указателю, то получим объект, на который он указывает. Рассмотрим пример. Пусть х и у – целые переменные, а *ptr – указатель на целую переменную. Поставим задачу присвоения переменной у значения переменной х с помощью указателя. Фрагмент С -кода будет следующий:

int x = 1, y = 2;

int *ptr; // объявили указатель на целую переменную

ptr = &x; // взяли адрес переменной х = 1

y = *ptr; // переменная у стала равной 1

*ptr = 0; // переменная х стала равной 0

В приведенных объявлениях новым является объявление указателя:

int *ptr;

Следует помнить, что любой указатель может указывать только объекты одного конкретного типа данных, заданного при объявлении [7.1].

Унарный оператор * есть оператор косвенного доступа. Примененный к указателю он выдает объект, на который данный указатель указывает.

Одноместные (унарные) операции * и & имеют более высокий приоритет для своих операндов, чем арифметические операции.

Для указателей одного типа можно, например, выполнять присваивание без разыменования. Это вытекает из того, что указатели сами по себе являются переменными. Пусть определен еще один указатель типа int, например, ptr2.

Тогда возможно произвести присвоение:

ptr2 = ptr;

После присвоения указатель ptr2 будет указывать на ту же переменную, что и указатель ptr.

В языке С допустимы следующие (основные) операции над указателями: присваивание; получение значения того объекта, на который ссылается указатель (синонимы: косвенная адресация, разыменование, раскрытие ссылки); получение адреса самого указателя; унарные операции изменения значения указателя; аддитивные операции и операции сравнений (отношений).

С помощью унарных операций "++" и "––" числовые (арифметические) значения переменных типа указатель меняются по-разному в зависимости от типа данных, с которыми связаны эти переменные. Если указатель связан с типом char, то при выполнении операций "++" и "––" его числовое значение изменяется на 1 (единицу). Если указатель связан с типом int, то операции "++" и "––" изменяют числовые значения указателей на 2. Указатель, связанный с типами float или long, унарными операциями "++" и "––" изменяется на 4. Таким образом, при изменении указателя на единицу указатель "переходит к началу" следующего (или предыдущего) поля той длины, которая определяется типом.

Следует особо остановиться на указателях и квалификаторе (модификаторе) const. Как известно, квалификатор const превращает переменную в константу, значение которой не должно меняться. Например, нет смысла изменять число Краткие сведения из теории - student2.ru . Значение константы должно быть инициализировано в месте ее определения. В связи с этим различают указатели на константы и константные указатели. Приведем следующий пример:

long value = 9999L;

const long *pvalue = &value;

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

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

int count = 43;

int *const pcount = &count;

Вторая строчка приведенного кода определяет и инициализирует константный указатель pcount, который "привязан" к адресу переменной count. Если определить новую переменную того же типа, то взять адрес новой переменной с помощью константного указателя pcount будет нельзя, компилятор сделает сообщение об ошибке и работа программы будет невозможной. В тоже время возможно изменить значение константного указателя через другое числовое значение. Но это повлечет за собой изменение переменной, на которую указатель ссылается. Например,

int count = 43;

int *const pcount = &count;

*pcount = 345;

В соответствии с приведенным кодом переменная count будет иметь значение 345.

Соответственно, если константный указатель ссылается на константный объект (например, на константную переменную), то в этом случае ни значение объекта, на который ссылается такой указатель, ни значение самого указателя (когда будет сделана попытка присвоить иное числовое значение указателю) не может быть изменено в программе. Например,

const int card = 21;

const int *const pcard = &card

Указанные особенности для указателей с квалификатором const присущи и для переменных (объектов) других типов.

Указатели, значения которых изменять нельзя используются, например, при заполнении константных таблиц.

Порядок выполнения работы

Пример 1. Напишите программу определения адресов целых чисел от 0 до 9 и строчных букв латинского алфавита.

Программный код решения примера:

#include <stdio.h>#include <conio.h> int main (void){ int i, j = 0; char c = 'a', *ptr2; ptr2 = &c; printf("\n\t Figures, symbols and their addresses:\n"); for (i = 0; i < 10; ++i) printf("\n\t %3d) %2d --> %5p", i + 1, i, &i); printf("\n"); for ( ; *ptr2 <= 'z'; (*ptr2)++) printf("\n\t %3d) %2c --> %5p", ++j, *ptr2, ptr2); printf("\n\n Press any key: "); _getch(); return 0;}

Результат выполнения программы показан на рис. 7.1.

Краткие сведения из теории - student2.ru


Рис. 7.1.Адреса цифр и строчных букв

В программе использован спецификатор формата %5p для определения адреса переменных. Число 5 определяет отступ от левого края на пять позиций.

Задание 1

1. Добавьте вывод кодов цифр и букв, для которых определены адреса в памяти компьютера.

2. В программе вместо операторов цикла for примените операторы while.

3. В программу введите указатель на тип int и применить этот указатель по аналогии с указателем *ptr2.

4. Добавьте определение адресов прописных букв латинского алфавита и вывести их дополнительным столбцом к адресам строчных букв.

5. Выведите в столбец свою фамилию (буквами латинского алфавита), имя и адреса соответствующих букв фамилии и имени.

Пример 2. Напишите программу однозначного задания типа разностей указателей, и определения адресов заданных указателей.

Для решения данного примера подключим заголовок stddef.h для определения типа разности указателей с помощью зарезервированного имени типа ptrdiff_t.

Программный код решения примера:

#include <stdio.h>#include <conio.h>#include <stddef.h> int main (void) { int x, y; int *px, *py; ptrdiff_t z; // инициализация указателей px = &x; py = &y; // разница двух указателей z = px - py; printf("\n The difference of two pointers to %p and %p is: %d", px, py, (int) z); printf("\n\n The addresses are: px = %p, py = %p\n", &px, &py); printf("\n Press any key: "); _getch(); return 0;}

Результат выполнения программы показан на рис. 7.2.

Краткие сведения из теории - student2.ru


Рис. 7.2.

Задание 2

1. Поменяйте местами переменные х и у. Проанализируйте результат выполнения программы.

2. Для переменных произведите инициализацию в соответствии с номером компьютера, за которым выполняется лабораторная работа, и своего дня рождения.

3. Рассмотрите решение примера для следующих типов: char, long int, long long int, unsigned int, float, double, long double.

4. Вывод результатов осуществите с помощью одной функции printf().

Пример 3. Напишите программу арифметических операций с указателями.

При выполнении примера следует иметь в виду, что операции "&" и "*" имеют более высокий приоритет, чем обычные арифметические операции. Программный код решения примера:

#include <stdio.h>#include <conio.h> int main (void) { int x = 2, y = 7, a, b, *ptr, *ptr2; ptr = &a; ptr2 = &b; *ptr = x - y; *ptr2 = y - x - *ptr + 100; printf("\n\t Arithmetic operations with pointers:\n"); printf("\t a = %d, b = %d\n", a, b); printf("\n Press any key: "); _getch(); return 0; }

Результат выполнения программы показан на рис. 7.3.

Краткие сведения из теории - student2.ru


Рис. 7.3.Результат арифметических операций с указателями

Следует обратить внимание на то, что переменные a и b сначала не были определены, а в результате стали иметь некоторые значения.

Задание 3

1. В программе примените типы double и float.

2. После взятия адресов от переменных a и b, измените значения указателей на значения Х и 10*Х, где Х – номер компьютера, за которым выполняется лабораторная работа. Выполните указанные арифметические действия и выведите значения переменных a и b.

3. Напишите программу для выполнения операций вычитания, умножения и деления с участием указателей.

4. Для вывода результатов примените только одну функцию printf().

Пример 4. Напишите программу двухуровневой адресации для объектов целого типа.

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

#include <stdio.h>#include <conio.h> int main (void) { int x, y = 8; int *ptr, **ptr2; x = 7; ptr = &x; ptr2 = &ptr; **ptr2 = *ptr + 10; printf("\n\t The value of x = %d. 1-st pointer is: %d. 2-nd pointer is: %d\n", x, *ptr, **ptr2); ptr = &y; ptr2 = &ptr; **ptr2 = 88; printf("\n\t The value of y = %d\n", y); printf("\n Press any key: "); _getch(); return 0;}

Результат выполнения программы показан на рис. 7.4.

Краткие сведения из теории - student2.ru


Рис. 7.4.Результат двухуровневой адресации

Задание 4

1. Выведите на экран пользователя адреса указателей.

2. Организуйте цикл инкрементирования первого указателя начиная с Х до 10*Х, где Х – номер компьютера, за которым выполняется лабораторная работа. Сделайте вывод значений переменной, на которую делает ссылку первый указатель, и значений второго указателя.

3. Напишите программу трехуровневой адресации при задании целых чисел, равных Х и 10*Х, где Х – номер компьютера, за которым выполняется лабораторная работа.

Пример 5. Напишите программу по определению и инициализации переменных разных типов и одного указателя типа void *. Последовательно присваивая указателю адреса переменных, выведите значения переменных с помощью разыменования указателя.

Программный код решения примера:

#include <stdio.h>#include <conio.h> int main (void){ int x = 99; double y = 6.78; char symbol = '#'; void *ptr; ptr = &x;printf("\n\t The value of variable through a pointer: %d\n", *(int *) ptr); ptr = &y;printf("\n\t The value of variable through a pointer: %lf\n", *(double *) ptr); ptr = &symbol;printf("\n\t The value of variable through a pointer: %c\n", *(char *) ptr); printf("\n Press any key: "); _getch(); return 0;}

Результат выполнения программы показан на рис. 7.5.

Краткие сведения из теории - student2.ru


Рис. 7.5.

Особенностью использования указателя типа void является то, что при разыменовании указателя необходимо производить преобразования типов. Прежде чем произвести разыменование указателя, его приводят к указателю соответствующего типа.

Задание 5

1. В программу добавьте переменные типа float, unsigned, long и обеспечьте ввод их значений с клавиатуры. Выведите адреса и значения переменных с помощью разыменования указателя.

2. Задайте порядок (нумерованную последовательность) инициализации переменных и создайте вывод значений указателя на основе переключателя switch. Номер инициализируемой переменной задайте с клавиатуры.

3. В программу введите операцию двухуровневой адресации с применением указателя типа void. Выведите значения двух указателей с помощью их разыменования.

Пример 6. Напишите программу по реализации условия: определить и инициализировать переменную типа double. Определите указатели типа char *, int *, double *, void *, инициализируйте их адресом переменной. Выведите на экран пользователя значения указателей, их размеры и длины участков памяти, которые связаны с выражениями, разыменовывающими указатели.

Программный код решения примера:

#include <stdio.h>#include <conio.h> int main (void) { double d = 6.78; char *cp; int *ip; double *dp; void *vp; // Адресация с приведением типов cp = (char *)&d; ip = (int *)&d; dp = (double *)&d; vp = &d; printf("\n\t Address:\n\t char = %p\n\t int = %p\n\t double = %p\n\t void = %p\n", cp, ip, dp, vp); // Размеры указателей и памяти разыменованных указателей:printf("\n\t The dimension of the object type \"pointer\":\n\t char = %d\n\t int = %d\n\t double = %d\n\t void = %d\n", sizeof(cp), sizeof(ip), sizeof(dp), sizeof(vp)); printf("\n\t The size of the memory pointer:\n\t char = %d\n\t int = %d\n\t double = %d\n", sizeof(*cp), sizeof(*ip), sizeof(*dp)); printf("\n Press any key: "); _getch(); return 0; }

Результат выполнения программы показан на рис. 7.6.

Краткие сведения из теории - student2.ru


Рис. 7.6.Адреса и размеры указателей разных типов

Как видно из полученного результата, размеры участков памяти, выделенных указателям разных типов, одинаковы.

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

2. Выведите значения указателей заданных типов. Определите указатель с правильным доступом к значению переменной d = 6.78.

3. Объявление указателей и взятие адреса сделайте в одной строчке для соответствующего типа.

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

Пример 7. Напишите программу, в которой с помощью указателя и функции scanf_s() читаются данные с клавиатуры, а также определяются и инициализируются указатели на константы и константные указатели. Программный код решения примера:

#include <stdio.h>#include <conio.h>#include <math.h>#include <stdlib.h>#include <time.h> int main (void) { double x, *px = &x, e = exp(1); const double pi = acos(0.0); const double *pexp = &e; const int base = 10; const int *const pbase = &base; const double *ptr_pi = &pi; int i; time_t t; printf("\n Enter a real number: "); scanf_s("%lf", px); printf("\n The value of the entered number is \"%g\"\n", x); printf("\n The base of natural logarithms \is \"%0.14f\"\n", *pexp); printf("\n The base of the decimal logarithm is \"%d\"\n",\ *pbase); srand((unsigned) time(&t)); // рандомизация for (i = 0; i < rand(); i++) { rand(); }// Случайное вещественное число из интервала [-100.0; 100.0] x = -100.0 + (100.0 - (-100.0))* (double)rand() / RAND_MAX; printf("\n The modified value of x: %g\n \Pointer to the variable x: %g\n", x, *px); printf("\n The value of pi through the pointer \and the function acos(0): %0.14f\n", *ptr_pi * 2); printf("\n\n ... Press any key: "); _getch(); return 0; }

В программе для получения числа пи ( Краткие сведения из теории - student2.ru ) используется функция acos(0), так как косинус Краткие сведения из теории - student2.ru /2 равен нулю. Затем полученный результат умножается на два. Дополнительная рандомизация осуществляется в цикле, одним из параметров которого является случайная функция rand(), возвращающая целое число. При этом предусмотрено приведение типов. Изменение числа х осуществляется по равномерному случайному закону из интервала [-100.0; 100.0].

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

Краткие сведения из теории - student2.ru


Рис. 7.7.Пример работы программы с указателями

Задание 7

1. Выполните инкрементирование указателей на константы и константных указателей. Объясните полученный результат.

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

3. В качестве константы примите некоторое шестнадцатеричное число (с буквами). Определите указатель на константу и в цикле от 1 до 16 измените значение указателя с последующим выводом результатов на консоль.

4. Выполните возможные арифметические операции с константными указателями и с указателями на константы.

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

4. Содержание отчёта

1. Название, цель работы

2. Результаты выполнения работы со всеми распечатками, скриншотами, комментариями и наблюдениями согласно разделу «Выполнение работы».

3. Ответы на контрольные вопросы.

Вопросы к защите

  1. Какое общее назначение указателей в языке С?
  2. Какие арифметические операции допускаются для указателей?
  3. Какие унарные операторы используются с указателями? Как они называются?
  4. Для каких типов данных может быть использован указатель?
  5. Как числовые значения указателей изменяются при их инкрементировании в зависимости от типов данных.
  6. С помощью какого формата осуществляется вывод на консоль адресов переменных заданного типа?
  7. Что такое многоуровневая адресация? Как она организуется в языке С?
  8. Как осуществляется инициализация указателей на вещественные типы данных?
  9. Как осуществляется инициализация указателей на символьный тип данных?
  10. Какой смысл имеет значение указателя NULL?
  11. Что произойдет, если применить к указателю со значением NULL операцию разыменования?
  12. Как следует определять и инициализировать указателя на константу?
  13. Как следует определять и инициализировать константный указатель?
  14. Какое отличие константного указателя от указателя на константу?

Приложение

Самостоятельная работа по лабораторной работе № 5

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

Наши рекомендации