Указатели и операции над указателями
Указатель - это адрес памяти, распределяемой для размещения идентификатора (в качестве идентификатора может выступать имя переменной, массива, структуры, строкового литерала). В том случае, если переменная объявлена как указатель, то она содержит адрес памяти, по которому может находится скалярная величина любого типа. При объявлении переменной типа указатель, необходимо определить тип объекта данных, адрес которых будет содержать переменная, и имя указателя с предшествующей звездочкой (или группой звездочек). Формат объявления указателя:
спецификатор-типа [ модификатор ] * описатель .
Спецификатор-типа задает тип объекта и может быть любого основного типа, типа структуры, смеси (об этом будет сказано ниже).
Для того, чтобы можно было выполнить арифметические и логические операции над указателями или над объектами, на которые они указывают, необходимо при выполнении каждой операции явно определить тип объектов. Такие определения типов может быть выполнено с помощью операции приведения типов.
В качестве модификаторов при объявлении указателя могут выступать ключевые слова const, near, far, huge. Ключевое слово const указывает, что указатель не может быть изменен в программе. Размер переменной объявленной как указатель, зависит от архитектуры компьютера и от используемой модели памяти, для которой будет компилироваться программа. Указатели на различные типы данных не обязательно должны иметь одинаковую длину.
Для модификации размера указателя можно использовать ключевые слова near, far, huge.
Примеры:
unsigned int * a; /* переменная а представляет собой указатель на тип unsigned int (целые числа без знака) */
double * x;/* переменная х указывает на тип данных с плавающей точкой удвоенной точности */
Над указателями можно выполнять унарные операции: инкремент и декремент. При выполнении операции ++ и – значение указателя увеличивается или уменьшается на длину типа, на который ссылается используемый указатель. Пример:
int *ptr, a[10];
ptr = &a[5];
ptr++; /равно адресу элемента a[6]/
ptr--; /равно адресу элемента a[5]/
В бинарных операциях сложения и вычитания могут участвовать указатель и величина типа int. При этом результатом операции будет указатель на исходный тип, а его значение будет на указанное число элементов больше или меньше исходного.
Пример:
int *ptr1, *ptr2, a[10];
int i=2;
ptr1=a[i+4]; /* равно адресу элемента a[6] */
ptr2=ptr1-i; /* равно адресу элемента a[4] */
В операции вычитания могут участвовать два указателя на один и тот же тип. Результат такой операции имеет тип int и равен числу элементов исходного типа между уменьшаемым и вычитаемым, причем если первый адрес младше, то результат имеет отрицательное значение.
Пример: int *ptr1, *ptr2, a[10];
int i;
ptr2=a+4;
ptr1=a+9;
i=ptr1-ptr2; /* равно 5 */
i=ptr2-ptr1; /* равно -5 */
Значения двух указателей на одинаковые типы можно сравнивать в операциях ==, !=, <, <=, >, >= при этом значения указателей рассматриваются просто как целые числа, а результат сравнения равен 0 (ложь) или 1 (истина). Пример: int *ptr1, *ptr2, a[10];
ptr1=a+5;
ptr2=a+7;
if (prt1>ptr2) a[3]=4;
В данном примере значение ptr1 меньше значения ptr2 и поэтому оператор a[3]=4 не будет выполнен.
Массивы и указатели
Указатель - это переменная, содержащая адрес другой переменной.
Массив – структурированный тип данных, все элементы которого имеют один и тот же тип и расположены в памяти подряд.
При выделении массива ему выделяется память так же, как и массивам других алгоритмических языков. Но как только память для массива выделена, имя массива воспринимается как константный указатель того типа, к которому отнесены элементы массива, значением которого служит адрес первого элемента массива (с индексом 0).
Раз имя массива есть указатель, то к нему применимы все правила адресной арифметики, связанной с указателями. Т.к. имя массива есть не просто указатель, а указатель константа, то значение имени массива невозможно изменить.
Существует исключения, например, применение имени массива в операции sizeof. В этой операции массив «вспоминает» о своем отличии от обычного указателя, и результатам является размер в байтах участка памяти, выделенного не для указателя, а для массива в целом. Исключением является и применение операции & (получение адреса) к имени массива. Результат – адрес начального (с нулевым индексом) элемента массива.
И к указатели, и к массиву применима индексация, но массив не перемещаемый указатель.
Классы памяти
Спецификатор класса памяти в объявлении переменной может быть auto, register, static или extern. Если класс памяти не указан, то он определяется по умолчанию из контекста объявления. Объекты классов auto и register имеют локальное время жизни. Спецификаторы static и extern определяют объекты с глобальным временем жизни. При объявлении переменной на внутреннем уровне может быть использован любой из четырех спецификаторов класса памяти, а если он не указан, то подразумевается класс памяти auto.
Переменная с классом памяти auto имеет локальное время жизни и видна только в блоке, в котором объявлена. Память для такой переменной выделяется при входе в блок и освобождается при выходе из блока. При повторном входе в блок этой переменной может быть выделен другой участок памяти. Переменная с классом памяти auto автоматически не инициализируется. Она должна быть проинициализирована явно при объявлении путем присвоения ей начального значения. Значение неинициализированной переменной с классом памяти auto считается неопределенным. Спецификатор класса памяти register предписывает компилятору распределить память для переменной в регистре, если это представляется возможным. Использование регистровой памяти обычно приводит к сокращению времени доступа к переменной. Переменная, объявленная с классом памяти register, имеет ту же область видимости, что и переменная auto. Число регистров, которые можно использовать для значений переменных, ограничено возможностями компьютера, и в том случае, если компилятор не имеет в распоряжении свободных регистров, то переменной выделяется память как для класса auto. Класс памяти register может быть указан только для переменных с типом int или указателей с размером, равным размеру int. Переменные, объявленные на внутреннем уровне со спецификатором класса памяти static, обеспечиваю возможность сохранить значение переменной при выходе из блока и использовать его при повторном входе в блок. Такая переменная имеет глобальное время жизни и область видимости внутри блока, в котором она объявлена. В отличие от переменных с классом auto, память для которых выделяется в стеке, для переменных с классом static память выделяется в сегменте данных, и поэтому их значение сохраняется при выходе из блока.
Пример:
/* объявления переменной i на внутреннем уровне с классом памяти static. */
/* исходный файл file1.c */
main()
{ ...
}
fun1()
{ static int i=0; ...
}
/* исходный файл file2.c */
fun2()
{ static int i=0; ...
}
fun3()
{ static int i=0; ...
}
В приведенном примере объявлены три разные переменные с классом памяти static, имеющие одинаковые имена i. Каждая из этих переменных имеет глобальное время жизни, но видима только в том блоке (функции), в которой она объявлена. Эти переменные можно использовать для подсчета числа обращений к каждой из трех функций.
10. Структура и объединение. Доступ к полям. Сходства и отличия. Приведите примеры.
Cтруктуры - это составной объект, в который входят элементы любых типов, за исключением функций. В отличие от массива, который является однородным объектом, структура может быть неоднородной. Тип структуры определяется записью вида:
struct { список определений }
В структуре обязательно должен быть указан хотя бы один компонент. Определение структур имеет следующий вид:
тип-данных описатель;
где тип-данных указывает тип структуры для объектов, определяемых в описателях. В простейшей форме описатели представляют собой идентификаторы или массивы.
Пример: struct { double x,y; } s1, s2, sm[9];
Переменные s1, s2 определяются как структуры, каждая из которых состоит из двух компонент х и у. Существует и другой способ ассоциирования имени с типом структуры, он основан на использовании тега структуры. Тег структуры аналогичен тегу перечислимого типа. Тег структуры определяется следующим образом: struct тег { список описаний; };
где тег является идентификатором.
В приведенном ниже примере идентификатор student описывается как тег структуры:
struct student { char name[25]; int id, age; char prp; };Использование тегов структуры необходимо для описания рекурсивных структур. Ниже рассмотрен пример использования рекурсивных тегов структуры.
struct node { int data; struct node * next; } st1_node;Тег структуры node действительно является рекурсивным, так как он используется в своем собственном описании, т.е. в формализации указателя next. Структуры не могут быть прямо рекурсивными, т.е. структура node не может содержать компоненту, являющуюся структурой node, но любая структура может иметь компоненту, являющуюся указателем на свой тип, как и сделано в приведенном примере.
Доступ к компонентам структуры осуществляется с помощью указания имени структуры и следующего через точку имени выделенного компонента, например:
Объединения (смеси)
Объединение подобно структуре, однако в каждый момент времени может использоваться (или другими словами быть ответным) только один из элементов объединения. Тип объединения может задаваться в следующем виде:
union { описание элемента 1; ... описание элемента n; };Главной особенностью объединения является то, что для каждого из объявленных элементов выделяется одна и та же область памяти, т.е. они перекрываются. Хотя доступ к этой области памяти возможен с использованием любого из элементов, элемент для этой цели должен выбираться так, чтобы полученный результат не был бессмысленным.
Доступ к элементам объединения осуществляется тем же способом, что и к структурам. Тег объединения может быть формализован точно так же, как и тег структуры.
Объединение применяется для следующих целей:
- инициализации используемого объекта памяти, если в каждый момент времени только один объект из многих является активным;
- интерпретации основного представления объекта одного типа, как если бы этому объекту был присвоен другой тип.
Память, которая соответствует переменной типа объединения, определяется величиной, необходимой для размещения наиболее длинного элемента объединения. Когда используется элемент меньшей длины, то переменная типа объединения может содержать неиспользуемую память. Все элементы объединения хранятся в одной и той же области памяти, начиная с одного адреса.
11. объявление, прототип и определение функций. Приведите примеры.
Функция - это совокупность объявлений и операторов, обычно предназначенная для решения определенной задачи. Каждая функция должна иметь имя, которое используется для ее объявления, определения и вызова. В любой программе на СИ должна быть функция с именем main (главная функция), именно с этой функции, в каком бы месте программы она не находилась, начинается выполнение программы. При вызове функции ей при помощи аргументов (формальных параметров) могут быть переданы некоторые значения (фактические параметры), используемые во время выполнения функции. Функция может возвращать некоторое (одно !) значение. Это возвращаемое значение и есть результат выполнения функции, который при выполнении программы подставляется в точку вызова функции, где бы этот вызов ни встретился. Допускается также использовать функции не имеющие аргументов и функции не возвращающие никаких значений. Действие таких функций может состоять, например, в изменении значений некоторых переменных, выводе на печать некоторых текстов и т.п.. использованием функций в языке СИ связаны три понятия - определение функции (описание действий, выполняемых функцией), объявление функции (задание формы обращения к функции) и вызов функции. В языке СИ нет требования, чтобы определение функции обязательно предшествовало ее вызову. Определения используемых функций могут следовать за определением функции main, перед ним, или находится в другом файле. Однако для того, чтобы компилятор мог осуществить проверку соответствия типов передаваемых фактических параметров типам формальных параметров до вызова функции нужно поместить объявление (прототип) функции. Объявление функции имеет такой же вид, что и определение функции, с той лишь разницей, что тело функции отсутствует, и имена формальных параметров тоже могут быть опущены. Если прототип функции не задан, а встретился вызов функции, то строится неявный прототип из анализа формы вызова функции. Тип возвращаемого значения создаваемого прототипа int, а список типов и числа параметров функции формируется на основании типов и числа фактических параметров используемых при данном вызове. Таким образом, прототип функции необходимо задавать в следующих случаях:
1. Функция возвращает значение типа, отличного от int.
2. Требуется проинициализировать некоторый указатель на функцию до того, как эта функция будет определена.
Наличие в прототипе полного списка типов аргументов параметров позволяет выполнить проверку соответствия типов фактических параметров при вызове функции типам формальных параметров, и, если необходимо, выполнить соответствующие преобразования. Функция не может возвращать массив или функцию, но может возвращать указатель на любой тип, в том числе и на массив и на функцию. Тип возвращаемого значения, задаваемый в определении функции, должен соответствовать типу в объявлении этой функции. Функция возвращает значение если ее выполнение заканчивается оператором return, содержащим некоторое выражение. Указанное выражение вычисляется, преобразуется, если необходимо, к типу возвращаемого значения и возвращается в точку вызова функции в качестве результата. Если оператор return не содержит выражения или выполнение функции завершается после выполнения последнего ее оператора (без выполнения оператора return), то возвращаемое значение не определено. Для функций, не использующих возвращаемое значение, должен быть использован тип void, указывающий на отсутствие возвращаемого значения. Если функция определена как функция, возвращающая некоторое значение, а в операторе return при выходе из нее отсутствует выражение, то поведение вызывающей функции после передачи ей управления может быть непредсказуемым.