- •Основы программирования на языке С
- •Введение
- •1. Базовые понятия программирования
- •1.1. Алгоритмизация задачи
- •1.2. Схема алгоритма программы
- •1.3. Пример алгоритмизации
- •1.4. Этапы трансляции программы
- •2. Особенности языка С
- •2.1. Характеристики языка
- •2.2. Элементы языка
- •2.3. Стандартные типы данных
- •2.4. Компоненты простой программы
- •Вопросы для самопроверки
- •3. Выражения и операции
- •3.1. Операция и выражение присваивания
- •3.2. Бинарные арифметические операции
- •3.3. Операции увеличения (++) и уменьшения (--)
- •3.4. Преобразования типов при вычислении арифметических выражений
- •3.5. Тернарная или условная операция
- •3.6. Логические операции и операции отношения
- •3.7. Поразрядные (побитовые) операции
- •3.8. Операции сдвига
- •3.9. Операция sizeof
- •3.10. Операция следования
- •3.11. Сводная таблица операций языка С
- •Вопросы для самопроверки
- •4. Операторы
- •4.1. Оператор выражение
- •4.2. Пустой оператор
- •4.3. Объявления и составной оператор
- •4.4. Условный оператор
- •4.5. Оператор выбора switch
- •4.6. Циклы
- •4.7. Оператор break
- •4.8. Оператор безусловного перехода goto
- •4.9. Оператор continue
- •4.10. Оператор return
- •Вопросы для самопроверки
- •5. Массивы
- •5.1. Одномерные статические массивы
- •5.2. Объявление массива. Обращение к элементу массива
- •5.3. Инициализация массива
- •5.4. Многомерные массивы
- •5.5. Выход индекса за границы массива
- •5.6. Приемы работы с массивами в вычислительных задачах
- •5.7. Строка как массив символов
- •Вопросы для самопроверки
- •6. Указатели и ссылки
- •6.1. Понятие указателя
- •6.2. Операция получения адреса &
- •6.3. Операция разыменования (*)
- •6.4. Арифметика указателей
- •6.5. Применение указателей в выражениях
- •6.6. Указатели и массивы
- •6.7. Ссылочный тип данных
- •Вопросы для самопроверки
- •7. Время жизни и область видимости переменной
- •7.1. Общие понятия
- •7.2. Классы памяти
- •7.3. Вложенные блоки в программе
- •8. Функции
- •8.1. Общие понятия
- •8.2. Определение функции
- •8.3. Прототип функции
- •8.4. Переменные в функции
- •8.5. Передача параметров в функцию
- •Вопросы для самопроверки
- •9. Пользовательские типы данных
- •9.1. Структурный тип данных
- •9.2. Битовые поля
- •9.3. Объединения или смеси
- •9.4. Перечисления
- •Вопросы для самопроверки
- •10. Динамическая работа с памятью
- •10.1. Универсальный указатель void
- •10.2. Принципы работы с динамическими массивами
- •Вопросы для самопроверки
- •11. Ввод-вывод данных
- •11.2. Функции ввода-вывода библиотеки iostream
char *s;
j= k+i // ошибка, указатели нельзя складывать k= k+2;// +8 к числовому значению указателя s=s+2;// +2 к числовому значению указателя
Два указателя можно вычитать друг из друга, если они одного типа. Эта операция дает расстояние между объектами. При этом расстояние измеряется в единицах кратных размеру элемента данного типа.
Пример: |
|
int x[5],*i , *r, |
j ; |
i = &x[0] ; |
|
k = &x[4] ; |
|
j = k-i ; |
\\ j = 4 |
Отметим, что j= 4, а не 8 как можно было бы предположить исходя из того, что каждый элемент массива x[] занимает 2 байта.
6.4.4. Сравнение указателей
Если два указателя установлены на одинаковый тип данных, то их можно сравнивать в операциях: == , != , < , <= , > , >= при этом значения указателей рассматриваются просто как целые числа, а результат сравнения равен 0 (ложь) или 1 (истина).
Пример:
int *ptr1, *ptr2, a[10]; ptr1=a+5;
ptr2=a+7;
if (prt1>ptr2) a[3]=4;
В данном примере значение ptr1 меньше значения ptr2 и поэтому оператор a[3]=4 не будет выполнен.
Любой указатель можно проверить на равенство (==) или неравенство (!=) со специальным значением NULL, которое означает отсутствие адреса в указателе.
6.5. Применение указателей в выражениях
Как известно, приоритет операций имеет большое значение при вычислении выражений. Унарные операции & и * имеют более высокий приоритет, чем арифметические и поэтому выполняются раньше них.
Унарные операции *(разадресация), ++ , -- имеют одинаковый приоритет и выполняются слева направо.
Следующий пример иллюстрирует взаимодействие унарных адресных операций при вычислении выражений. Напомним, что операция присваивание – это, по сути, засылка результата вычисления в переменную, стоящую слева от знака равенства.
Пример:
int x[4] = {0,2,4,6},*p , y ;
p = &x[0] ; |
|
y = *p ; |
\\ 1 |
y = *p++ |
\\ 2 |
y = ++*p |
\\ 3 |
y = *++p |
\\ 4 |
y = (*p)++ |
\\ 5 |
y = ++(*p) |
\\ 6 |
В данном примере к массиву, заданному при его определении происходит обращение с помощью указателя p. Пример хорошо показывает двойственную природу указателя: с одной стороны указатель имеет свое собственное значение – адрес переменной, с другой стороны он дает возможность обращения к самой переменной. Поэтому, манипулируя с указателем, мы имеем возможность, с одной стороны, "перебирать" элементы массива, изменяя значение самого указателя (адреса, записанного в него), с другой обращаться к элементам этого массива.
Каждая строка данного примера содержит действия, как над элементами массива, так и над указателем, последовательность выполнения которых иллюстрируется текстом и рисунком 6.2.
Строка 1.
∙Обращение к нулевому элементу (операция разыменования).
∙Пересылка нулевого элемента массива в y.
PDF created with FinePrint pdfFactory Pro trial version http://www.fineprint.com
Строка 2.
∙Обращение к нулевому элементу (операция разыменования).
∙Пересылка нулевого элемента массива в y.
∙Инкремент указателя (перемещение на следующий, 1-ый элемент). Строка 3.
∙Инкремент 1-ого элемента массива.
∙Пересылка 1-ого элемента массива в y.
Строка 4.
∙Инкремент указателя (перемещение на 2-ой элемент).
∙Пересылка 2-ого элемента массива в y.
Строка 5.
∙Инкремент 2-ого элемента массива.
∙Пересылка 2-ого элемента массива в y. Строка 6.
∙Инкремент 2-ого элемента массива.
∙Пересылка 2-ого элемента массива в y.
6.6.Указатели и массивы
Вязыке С между указателями и массивами существует тесная связь. Например, когда объявляется массив в виде int array[25], то этим определяется не только выделение памяти для двадцати пяти элементов массива типа int, но и для указателя с именем array, значение которого равно адресу первого по счету (нулевого) элемента массива.
Сам массив остается безымянным, а доступ к элементам массива осуществляется через указатель с именем array. Следует отметить, что указатель array является константным, значение которого можно использовать в выражениях, но изменить нельзя.
Пример: Имя массива можно приравнять указателю, так как оно также является указателем.
…
int arrey[25]; int *ptr; ptr = array;
…
1. |
*p (y=0) |
0 |
|
2 |
|
4 |
|
|
6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*p (y=0) |
|
|
|
|
P |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2. |
|
0 |
|
2 |
|
4 |
|
|
6 |
|
p++ |
|
0 |
2 |
|
4 |
|
6 |
|
||||||||||||
|
|
|
|
|
|
P |
|
|
|
|
|
|
|
|
|
|
*p (y=3) |
|
|
|
|
P |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||
3. |
x[1]++ |
|
|
0 |
|
3 |
|
4 |
|
|
6 |
|
|
|
|
|
|
|
|
|
|
|
|
||||||||
|
|
|
|
|
|
|
|
0 |
3 |
|
4 |
|
6 |
|
|||||||||||||||||
|
|
|
|
|
|
|
|
|
|
P |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4. |
p++ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*p (y=4) |
|
|
|
|
|
|
P |
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||||||||||||
|
0 |
|
|
3 |
|
4 |
|
6 |
|
|
0 |
3 |
|
4 |
|
6 |
|
||||||||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
P |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
P |
|
|
x[2]++ |
|
|
|
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||||||||
5. |
*p (y=4) |
|
0 |
|
|
3 |
|
4 |
|
6 |
|
|
0 |
3 |
|
5 |
|
6 |
|
||||||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
P |
|
|
|
6. |
x[2]++ |
|
|
|
|
|
|
|
|
|
|
|
|
P |
|
|
*p (y=6) |
|
|
|
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||||||||||||
|
0 |
|
3 |
|
6 |
|
|
6 |
|
|
|
0 |
3 |
|
6 |
|
6 |
|
|||||||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
P |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
P |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Рис. 6.2. Обращение к массиву с применением указателей.
Здесь указатель ptr устанавливается на адрес первого элемента массива, причем присваивание ptr=array можно записать в эквивалентной форме: ptr=&array[0].
Для доступа к элементам массива существует два различных способа.
PDF created with FinePrint pdfFactory Pro trial version http://www.fineprint.com
Первый способ связан с использованием обычных индексных выражений в квадратных скобках, например, инструкция array[10] = 35 записывает константу 35 в одиннадцатый элемент массива array (в элемент с индексом 10), а инструкция array[i+4] = rev записывает значение переменной rev в (i+4)-ый элемент массива array.
Второй способ доступа к элементам массива связан с применением указателей:
*(array+10) |
= 35 |
*(array+i+4)= 7. |
|
Рассматривая имя массива как указатель, мы проделали те же действие, что и в предыдущем примере. Операция * в данном случае – это "разименование" или обращение к объекту через указатель.
Подобным же образом можно обращаться к элементам массива с помощъю независимого (переменного) указателя.
Пример: Сравнение двух способов доступа к элементам массива.
//определение массива mas и указателя q char mas[100] , *q;
//инициализация указателя – установка на нулевой элемент массива q = mas;
//Доступ к элементам массива
//(следующие записи эквивалентны):
mas[0]; |
// |
*q |
mas[10]; |
// |
*(q+10) |
mas[n-1]; |
// |
*(q+n-1) |
Использование индексации для доступа к массиву более наглядно, но использование указателя – более эффективно. При индексировании элемента массива, его адрес каждый раз вычисляется по начальному адресу (имени массива) и смещению (индексу), что снижает эффективность полезных вычислений. Поскольку быстродействие программы – одно из важнейших её свойств, предпочтительнее использовать указатели-переменные.Сравним два варианта работы с массивами на примере функции putstr().
Пример: Вывод строки символов на экран терминала.
…
//создание строки (массив символов с именем s
//с нулевым элементом в конце)
char s[]="строка текста"; int i;
for (i=0 ; s[i] != 0 ; ++i) putchar(s[i]);
…
/* Доступ с помощью указателя*/
…
while (*s !=0) putchar (*s++);
}
…
Первая часть примера работает с массивом при помощи индексирования его элементов, инструкция putchar(s[i]) выводит один символ на экран дисплея. В цикле перебираются элементы массива s до тех пор, пока не встретится элемент с нулевым значением.
Вторая часть примера делает ту же работу, рассматривая имя массива как указатель.
Следующий пример демонстрирует особенности использования адресной арифметики при работе с указателями.
Пример: Обработка элементов массива.
. . . |
|
char s[20]; |
// Определение массива типа char |
// Определение и установка указателя на 0-ой элемент массива
char *p = &s[0]; |
// аналог *p=s |
*(p+10)= 'l'; |
//Записываем символ 'l' в 11-ый элемент |
p++; |
// установка указателя на 2-ой элемент |
*(p+10) = 'k'; |
/ Меняем значение 12-ого элемента |
if ( *(p-1) != ' \n' ) |
// Проверка 1-ого элемента |
...
Следует помнить, что индексация ведется с нуля, поэтому первый элемент массива имеет нулевой индекс, второй элемент – первый индекс и так далее. В примере массив и указатель имеют тип char (размер элемента = 1 байт). Прибавляя к адресу 1, мы сдвигаемся на один байт. А как быть, если мы работаем, например, с типом int, у которого размер - 4 байта? Оказывается, что и в этом случае все показанные выше арифметические операции с указателями дадут правильный результат. Разумеется, к адресу, будет добавляться не 1, а 4, но нам об этом даже не придется думать:
Пример:
PDF created with FinePrint pdfFactory Pro trial version http://www.fineprint.com
. . . |
|
int s[20]; |
// Определение массива типа char |
// Определение и установка указателя на 0-ой элемент массива
int *p = &s[0]; |
// аналог *p=s |
*(p+10)= 'l'; |
//Записываем символ 'l' в 11-ый элемент |
p++; |
// установка указателя на 2-ой элемент |
*(p+10) = 'k'; |
/ Меняем значение 12-ого элемента |
if ( *(p-1) != ' \n' ) |
// Проверка 1-ого элемента |
... |
|
Пример, приведенный ниже, отражает на экране абсолютные значения адресов элементов массивов различных типов, что позволяет наглядно увидеть особенности арифметики указателей.
Пример: Вывести на экран элементы массива и их адреса. #include <stdio.h>
void main() |
|
|
{ |
|
|
char s[]="string",*p; |
// работа с массивом типа char |
|
for (p=s; *p !=0; p++) |
|
//p+1(в абсолютных значениях) |
printf("s: %c\tp=%p\n",*p , p); |
|
|
int q[]={10,20,30,40,50,0},*p1; |
// работа с массивом типа int |
for (p1=q; *p1 !=0; p1++) //p+4(в абсолютных значениях) printf("q[0]=%d\tp1=%p\n",*p1,p1);
}
Для вывода значения указателя (адреса элемента массива) на экран дисплея в функции printf() используется спецификатор формата %p (тип укаазатель).
Итак, мы разобрали одно из назначений указателей – обращение к элементам массивов
(или других сложных типов данных).
Другое, не менее важное применение указателей – передача параметров и возврат значений из функций. Переменные, объявленные в функциях – это локальные (временные) переменными, которые уничтожаются при выходе из функции, поэтому часто возникает необходимость обращения к переменным, определенным за пределами функции, то есть к внешним объектам по отношению к функции. Когда управление передается в функцию (или в программный блок), без применения указателей программа имеет доступ только к параметрам и переменным функции ("локальная память"), переменные же в других функциях (или блоках) недоступны ("внешняя, по отношению к функции память").
Указатели в параметрах функции имеют два важных применения:
∙передачу массива в функцию;
∙доступ к "внешней" памяти из тела функции (или блока);
∙возврат из функции более одного значения.
Более подробно о применении указателей при работе с функциями этом смотрите в разделе "функции".
6.7. Ссылочный тип данных
Ссылочный тип (ссылки), иногда называют псевдонимом или синонимом, и служит он для присвоения объекту дополнительного имени. Ссылку можно рассматривать как константный указатель. Ссылка связывается с конкретным объектом в момент её создания (указатель устанавливается на объект) и в дальнейшем не может быть изменена, то есть не может быть перенаправлена на другой объект, подобно обычному указателю.
Ссылка позволяет косвенно манипулировать объектом, точно так же, как это делается с помощью указателя. Однако эта косвенная манипуляция не требует специального синтаксиса (операции разименования "*"), необходимого для указателей.
Подводя итог вышесказанному, можно отметить, что ссылку можно рассматривать как альтернативное имя объекта, а также как безопасный вариант указателя.
Ссылки имеют три особенности, отличающие их от указателей:
∙При объявлении ссылка обязательно инициализируется, то есть должна быть направлена на уже существующий объект.
∙Ссылка пожизненно указывает на один и тот же объект.
∙При обращении к объекту по ссылке не требуется указывать операцию разыменования (*), так как она выполняется автоматически.
Создание ссылки похоже на создание обычной переменной – тип, имя, инициализатор. Но справа от типа надо поставить символ &, и обязательно явно инициализировать ссылку, то есть в момент создания связать с тем объектом, на который она будет ссылаться:
PDF created with FinePrint pdfFactory Pro trial version http://www.fineprint.com
Пример: |
|
int i; |
// целая переменная |
int& ref = i; // определение ссылки на переменную i |
|
int& ref1; |
// ошибка: ссылка должна быть инициализирована |
После этого можно пользоваться ссылкой ref так же, как самой переменной i:
i = 8; |
// "прямой" доступ к i |
ref = 8; |
// "косвенный" доступ к i через ссылку |
При работе со ссылками необходимо учитывать следующее:
∙Ссылка не может существовать сама по себе – она обязательно связана, с какой либо переменной.
∙У ссылки нет своего адреса – если взять адрес ссылки, то получим адрес связанной с ней переменной.
∙Все, что мы можем сделать со ссылкой – это создать ее.
Ссылку нельзя уничтожить или перенаправить на другую переменную. Она уничтожится автоматически при выходе из блока, в котором была объявлена. Все операции со ссылками реально воздействуют на адресуемые ими объекты. В том числе и операция взятия адреса.
Пример: |
|
… |
|
int val = 1024; |
|
int &refVal = val; |
// ссылка на val |
refVal += 2; |
// val=val+2 |
int ii = refVal; |
// ii = val, |
// устанавливает указатель pi на переменную val. int *pi = &refVal;
…
Если несколько ссылок определяются в одной инструкции через запятую, перед каждой ссылкой должен стоять амперсэнд (&)
int &rval3 = val3, &rval2 = val2;
Если указателю присвоить нулевое значение, это означает, что указатель не установлен (не указывает ни на один объект), если же нулевое значение присвоить ссылке, это будет означать, что ссылка связана с переменной, значение которой равно нулю.
Пример:
…
int *pi = 0; // pi не указывает ни на какой объект. const int &ri = 0;
означает примерно следующее: int temp = 0;
const int &ri = temp;
…
Что касается операции присваивания, то работа с указателями тоже отличается от аналогичной работы со ссылками, следующий пример демонстрирует это:
//работа с указателями
//определение переменных val1 и val2
…
int val1 = 1024, val2 = 2048;
//установка указателей
int *pi1 = &val1, *pi2 = &val2; pi1 = pi2;
…
переменная val1, на которую указывает pi1, остается неизменной, а указатель pi1 получает значение адреса переменной val2. Таким образом, pi1 и pi2 теперь указывают на один и тот же объект val2.
Проделаем подобные операции со ссылками.
…
int &ri1 = val1, &ri2 = val2; //создание ссылок ri1 и ri2 ri1 = ri2;
…
Операция присваивания ri1 = ri2 меняет саму переменную val1 (записывает в неё значение из val2), но ссылка ri1 по-прежнему адресует val1. Пример наглядно показывает, что с точки зрения синтаксиса работа со ссылкой ничем не отличается от работы с переменной, на которую она ссылается.
Следует отметить, что механизм ссылок отсутствует в классическом С, но все современные компиляторы С++ поддерживают ссылки.
Ссылки редко используются как самостоятельные объекты, обычно они употребляются в качестве формальных параметров и возвращаемых значений функций.
PDF created with FinePrint pdfFactory Pro trial version http://www.fineprint.com