Вопросы для собеседования
1. Что такое указатель и какие операции над указателями определены? Каким образом производится объявление, инициализация указателя? Правила адресной арифметики.
2. Каким образом определяется тип переменной — указателя? По каким правилам выполняются арифметические операции с переменными-указателями?
3. Что такое индексное выражение, приведенный индекс, для чего они нужны и как используются?
4. Какие операции с распределением памяти ЭВМ и как можно выполнить в процессе исполнения программы (динамически)?
5. Что такое функция в Си-программе? Что такое прототип и определение функции?
6. Что такое формальные и фактические аргументы функции?
7. Какие фактические параметры при вызове функции могут соответствовать формальному аргументу, являющемуся: а) идентификатором статического массива; б) указателем на функцию; в) идентификатором переменной одного из базовых типов?
8. Можно ли разные формальные аргументы функции обозначать одинаковыми идентификаторами? А фактические?
9. Перечислите все возможные способы передачи информации из программы в вызываемую функцию и обратно.
Ответы.
1,2.Указатели.
Указатель —спец.переменные, которые содержат адрес других переменных.
Форма объявления указателя:
(список указателей)
Поле модификатор определяет класс памяти или модель памяти указателя или особые преобразования указателя компилятором и является необязательным. Поле тип является обязательным и определяет тип объекта, на который указывает указатель. Возможны следующие типы: базовые (в том числе void), перечислимые, структуры, объединения, другие указатели.
Указатель может хранить и адрес входа в функцию, тогда это указатель на функцию. Указатель на тип void имеет особое значение и может указывать на объект любого типа, но ни с самим указателем типа void, ни с объектом, на который он указывает, нельзя выполнять никаких операций, кроме присвоения адресного значения, если тип указателя явно не преобразуется к типу объекта.
Поле список_указателей обязательно и содержит один или несколько идентификаторов указателей, разделенных запятыми. При объявлении указателя компилятор выделяет место в памяти компьютера, необходимое для размещения адреса объекта данного типа (размер адреса не равен размеру объекта). Форма объявления указателя в поле список_указателей:
*модификатор имя=инициализатор.
Поле модификатор содержит модификатор, определяющий класс или модель памяти. Для модели памяти модификатор может принимать значения near, far, huge. Эти ключевые слова используются только в среде программирования Borland C++ 3.1.
Символ * — признак указателя. При объявлении нескольких указателей * ставится перед каждым указателем. Инициализатор — адрес, который инициализирует указатель. Рекомендуется использовать в качестве инициализаторов адреса переменных, константы или инициализированные указатели.
Пример.
int *p, a, *f;
В данном примере объявляются указатели p и f и переменная а.
С указателями связаны две специфические унарные операции: взятие адреса и обращение по адресу. Форма записи операции обращения по адресу: *операнд. Операция производит обращение к объекту, адрес которого храниться в указателе. Операнд — указатель. Форма записи операции взятия адреса: операнд. Операнд — скалярный объект. Операция возвращает адрес скалярного объекта.
Знаки данных операций имеют аналоги среди бинарных операций и в контексте программы они различаются по количеству операндов, участвующих в операциях.
Пример 7.
short int i=10, j=3, k;
short int *p=i, *s;
s=j;
*p+=1;
k=i**p+*s;
printf (“%d”,k);
s=k;
*s+=10;
printf (“ %d”,k);
124 134
Как и обычные переменные, указатели инициализируются нулевым значением (константа NULL определена в файле stdio.h) при компиляции только если они объявлены на внешнем уровне или с классом памяти static. Для остальных указателей инициализация не проводится. Они указывают на произвольную область памяти, поэтому, прежде чем использовать указатель, его значение необходимо явно определить. Ни в коем случае нельзя присваивать значение указателю непосредственно.
Пример.
int *g, *j;
g=00644D1C; \*нельзя!*\
j=NULL;
Кроме того, инициализация объекта, на который указывает указатель неявно не производиться никогда, за исключением массивов, объявленных на внешнем уровне или с классом памяти static. Значение объекта, на который указывает указатель, также необходимо определить до его использования.
Операции над указателями.
Над указателями можно производить операции сложения и вычитания указателей с целыми числами, присвоение и сравнение указателя, кроме того возможна операция вычитания указателей одного типа (складывать указатели нельзя!). Важное значение имеет тип указателя, так как указатель указывает на объект какого-то типа, то есть значения указателя могут изменяться исходя из значения размера переменной данного типа.
Пример.
short int m[4]={0,1,2,3};
short int *p=m[0];
*s=m[2];
printf (“%p %p %d %d”,p,s,s-p,*(p+1));
p++;
printf (“%p %d”,p,*p);
if (ps)
printf (“ps”);
else
printf (“p
FDFC
FE00
FDFE
p
В примере при увеличении указателя p на 1 (p+1) значение указателя увеличивается на 2, так как это указатель типа short int, а данной тип имеет размер 2 байта. Если бы тип указателя был double, значение указателя изменилось бы на 8. При вычитании указателей одного типа результатом также является не сама численная разница значений указателей (адресов), а ее отношение к размеру типа указателей в байтах s-p=4/2 численная разница адресов/размеру в байтах.
При выполнении сравнения указателей производится сравнение из численных значений (адресов).
3.
Индексное выражение задает элемент массива и имеет вид:
выражение-1 [ выражение-2 ]
Тип индексного выражения является типом элементов массива, а значение представляет величину, адрес которой вычисляется с помощью значений выражение-1 и выражение-2.
Обычно выражение-1 — это указатель, например, идентификатор массива, а выражение-2 — это целая величина. Однако требуется только, чтобы одно из выражений было указателем, а второе целочисленной величиной. Поэтому выражение-1 может быть целочисленной величиной, а выражение-2 указателем. В любом случае выражение-2 должно быть заключено в квадратные скобки. Хотя индексное выражение обычно используется для ссылок на элементы массива, тем не менее индекс может появляться с любым указателем.
Индексные выражения для ссылки на элементы одномерного массива вычисляются путем сложения целой величины со значениями указателя с последующим применением к результату операции разадресации (*).
Так как одно из выражений, указанных в индексном выражении, является указателем, то при сложении используются правила адресной арифметики, согласно которым целая величина преобразуется к адресному представлению, путем умножения ее на размер типа, адресуемого указателем.
Приведенный индекс — возможность адресоваться к элементам N-мерного массива с использованием N-p координат (p-произвольно, p
смещение=индекс 1*размер 2*…*размер N + индекс 2*размер 3*…*размер N + … + + индекс N-1*размер N + индекс N.
4.
Секция кода содержит код программы, функции программиста и библиотечные функции.
Секция данных содержит инициализирующие данные (глобальные и статические переменные) и не инициализируемые данные (константы).
Стек используется для хранения автоматических объектов, передачи параметров и сохранения адреса возврата при вызове функции.
Ближняя куча (near heap) — динамически выделенная память.
Свободная память — память, не используемая программой. Тело загрузки программы на исполнение, положение и границы секций кода и данных фиксированы. Начало кучи и стека (начало стека расположено снизу, а не сверху) также фиксированы, а их границы изменяются в процессе выполнения в зависимости от действий программы (направление увеличения их размеров показано стрелкой).
Регистр процессора — ячейка памяти внутри процессора. Процессоры x86 для адресации в реальном режиме работы используют четыре сегментных регистра.
CS — регистр кода, по нему происходит адресация кода программы. DS — регистр данных, по нему происходит адресация данных, SS — регистр стека по нему происходит адресация стека, ES — дополнительный регистр, как правило, дублирует регистр данных.
Регистр указывает на определенный адрес памяти, из которого можно обращаться к 64 К ячейкам памяти (размер одного сегмента) по смещению относительно сегментного регистра (чтобы адресоваться к ячейкам памяти, лежащим за пределами доступных 64 К необходимо изменять значения сегментного регистра, что при программировании на языке С явно делать крайне нежелательно).
Указатель называется ближним или near, если он содержит только значение смещения (сегментный адрес берется из соответствующего сегментного регистра). Данный указатель может адресоваться к 64 К ячейкам памяти. Указатель называется дальним или far, если он содержит и сегментный адрес и смещение относительно сегментного адреса. Данный указатель может адресоваться к 1 М ячейкам памяти. Указатель может быть указателем типа huge. Отличие указателей типа far от указателей типа huge состоит в том, что при выполнении арифметических операций с указателями типа far изменяется только смещение. Сегментная часть остается постоянной, и указатель типа far может адресоваться только к 64 К ячейкам памяти. Чтобы его сместить, необходимо в программе явно изменять сегментную часть (проводить нормализацию). А при выполнении арифметических операций с указателями типа huge изменяться также будет и сегментная часть, поэтому адресоваться можно к любой ячейке в пределах одного мегабайта. Такой указатель хранится в памяти в нормализованном виде. Длину указателя определяет специальный модификатор (near, far, huge), используемый при объявлении указателя. Следует учитывать, что одна ячейка памяти — это один байт. Если указатель указывает на объект, размер которого отличен от одного байта, то в пределах одного сегмента указатель может обращаться не к 64 К элементам, а к 64 К/размер типа элементам.
Модели памяти.
Модель TINY.
Общий объем памяти для кода, данных и стека 64 К. Все указатели — ближние.
Модель SMALL.
Общий объем памяти для кода 64 К, для данных и стека — 64 К. Все указатели по умолчанию ближние, но для данных могут использоваться дальние указатели. Начиная с этой модели в программе появляется возможность использования дальней кучи для динамического использования памяти. К дальней куче могут обращаться только дальние указатели.
Модель MEDIUM.
Общий объем памяти для каждого модуля — 64 К. Для данных и стека тоже 64 К. Указатели данных по умолчанию ближние. Указатели функций по умолчанию дальние
Модель COMPACT.
Рекомендуется в случае с малым объемом кода, но большим объемом данных. Общий объем памяти для кода 64 К. Для данных — 64 К, для стека — 64 К. Указатели данных по умолчанию дальние, указатели функций по умолчанию ближние. Начиная с этой модели отсутствует ближняя куча
Модель LARGE.
Общий объем памяти для каждого модуля 64 К, для данных — 64 К, для стека — 64 К. Все указатели дальние.
Модель HUGE
Общий объем памяти для каждого модуля64 К. Для данных каждого модуля 64 К, для стека — 64 К. Все указатели дальние.
Динамическое распределение памяти.
Динамическое выделение памяти используется для выделения памяти для хранения данных в процессе работы программ, когда общий объем данных и сами данные на этапе написания программы неизвестны, а становятся известны лишь на этапе выполнения программы. Динамическое распределение памяти позволяет получить необходимый объем памяти для ближней и дальней кучи в зависимости от типа указателя и модели памяти. Для этого существуют специальные библиотечные функции, описанные в файле alloc.h. Процесс работы с динамической памятью:
1. Получение динамической памяти заданного объема.
2. Работа с данными, распределяемыми в динамической памяти (если необходими, то возможно перераспределение выделенного объема памяти с сохранением находящихся в нем данных).
3. Освобождение динамической памяти по окончании работы с данными.
Для получения блока динамической памяти используются функции malloc и calloc. Форма записи:
void *malloc(size_t size);
void *calloc(size_t nitems, size_t size);
Функция malloc в качестве аргумента принимает размер запрашиваемого блока в байтах.
Функция calloc в качестве первого аргумента принимает число элементов, под которые необходимо выделить память. В качестве второго аргумента — размер одного элемента в байтах.
Тип size_t аналогичен типу unsigned int. Функции в случае успешного выделения возвращают указатель на тип void, содержащий адрес выделенного блока, который нужно явно преобразовать к указателю на необходимый тип данных. В случае ошибки (как правило, связанной с тем, что такого объема свободной памяти нет) функции возвращают значение NULL. NULL — стандартная константа языка С, которая обозначает нулевой указатель. После выполнения динамического распределения памяти необходимо обязательно проверять, какое значение возвратили функции. Если значение указателя NULL, то работа с памятью невозможна (в большинстве случаев следует завершить программу).
Для перераспределения уже выделенного объема памяти используется функция realloc. Форма записи:
void *realloc(void *block, size_t size);
Функция изменяет размер ранее выделенного блока. В качестве первого аргумента функция принимает адрес ранее выделенного блока. В качестве второго аргумента — новый размер блока, при этом новый блок может оказаться в другом месте кучи. Функция в случае успешного выделения возвращает указатель на тип void, содержащий адрес нового блока, который нужно явно преобразовать к указателю на необходимый тип данных. Функция возвращает NULL, если размер нового блока больше размера старого блока и в памяти нет свободного места для размещения нового блока.
Для освобождения блока динамической памяти используется функция free. Форма записи:
void *free(void *block);
Функция выполняет освобождение блока памяти, адрес которого принимает в качестве аргумента. Все блоки динамической памяти должны быть освобождены по окончанию работы с ними, иначе они так и останутся занятыми по окончанию работы программы до перезагрузки системы, что приведет к утечке оперативной памяти.
В среде Borland C++ 3.1 для работы с дальней кучей необходимо использовать дальние указатели и соответствующие функции с приставкой far.
5. Функции.
Функция — совокупность объявлений и операторов, предназначенных для выполнения отдельной задачи и заключенных в специальный блок. Необходимость в использовании функции возникает при решении сложных задач, когда нужно выполнять набор однотипных функций с различными данными. Любая программа на языке С должна содержать хотя бы одну функцию (main). С использованием функций в языке С связано три основных понятия: объявление, определение и вызов.
Определение функции — описание действий, выполняемых функцией. Форма записи:
модификатор 1 тип модификатор 2 имя (список_формальныхпараметров)
{
тело функции
}
Поле модификатор 1 содержит спецификации класса памяти. Поле модификатор 2 (модификатор стека) используется только в средах разработки фирмы Borland для изменения типа функции (cdecl или pascal). Подробнее — при рассмотрении классов памяти.
Поле тип определяет тип возвращаемого функцией в вызывающую функцию значения. Если тип не указан явно, то по умолчанию считается, что функция возвращает значение типа int. Если функция не возвращает значения, она должна иметь тип void. Функция может возвращать любой скалярный объект или указатель, но не может возвращать массив или другую функцию. Для этого используются указатели.
Поле имя определяет идентификатор функции.
Поле список_формальных_параметров содержит объявление параметров функции, указанных через запятую, которые могут быть использованы в теле функции. Если функция не имеет параметров, то в данном поле указывается слово void. Формальные параметры — локальные переменные, существующие только в пределах тела функции и принимающие значения, переданные функции при вызове в соответствии с порядком следования их имен в списке параметров. Синтаксические правила объявления формальных параметров функции аналогичны синтаксическим правилам объявления переменных. Тип формальных параметров может быть любым, но типы формальных параметров должны соответствовать типам формальных параметров, указанных в объявлении функции и типам фактических параметров, передаваемых в тело функции при ее вызове.
Поле тело функции задает последовательность операторов, выполняемых функцией.
Объявление функции (задание прототипа функции) — задание формы обращения к функции. Форма записи:
модификатор 1 тип модификатор 2 имя (список_формальных_параметров);
Смысл полей модификатор 1 и модификатор 2, тип и имя такой же, как и при определении функции.
Поле список_формальных_параметров определяет количество и типы передаваемых функцией аргумента в данном списке при объявлении функции достаточно указать только тип параметра. Идентификатор параметра задавать необязательно. Если все же в объявлении функции используется идентификаторы аргументов, то они необязательно должны совпадать с идентификаторами аргументов при определении функции, но строго должны совпадать их типы.
Вызов функции — передача управления на первый оператор тела функции:
имя(список_фактических_параметров)
Поле имя содержит адрес функции. Это либо указатель на функцию, либо идентификатор функции (который по своей природе также является указателем на тело функции, либо выражение, тип значения которого — указатель на функцию).
Поле список_фактических_параметров содержит аргументы, которые имеют конкретные значения, передаваемые функцией при вызове. Их отличие от формальных параметров в том, что последние не содержат конкретных значений, и при каждом вызове функции принимают значение фактических параметров вызова. Если функция возвращает какое-либо значение, то вызов функции может входить в другое выражение.
Вызов функции приводит к следующим действиям:
1. Вычисляется значение полей выражения (адрес функции) и список фактических параметров. Если типы фактических параметров не совпадают с типами формальных параметров в объявлении и определении функции, то производиться неявное преобразование типов фактических параметров к типам формальных параметров. Соответствие между фактическими и формальными параметрами задается порядком их следования в списке.
2. В стек копируется адрес возврата из функции (адрес следующего за вызовом функции команды).
3. Значения фактических параметров копируются в ячейки памяти, предназначенные для хранения аргументов функции (в стек).
4. Управление передается по адресу функции, который является первым оператором тела функции.
5. Последовательно выполняются операторы, составляющие тело функции. Выполнение оператора return в теле функции возвращает управление в вызывающую функцию. При отсутствии оператора return управление возвращается после выполнения последнего оператора тела функции.
6. При возврате из функции производится очистка стека: удаляются фактические параметры и адрес возврата, по которому передается управление в вызывающую функцию. Если функция возвращает значение оператором return, то производится возврат значения в точку вызова.
7. Управление передается на следующую за вызовом функции команду.
Параметры функции.
В языке С существует 2 способа передачи параметров в функцию. Передача по имени и передача по ссылке. Кроме того, функция может в процессе работы изменять значение глобальных переменных программы. Если используется передача по имени, то при вызове функции в качестве фактического параметра указывается имя переменной, а в стеке выделяется место для формального параметра, куда копируется значение фактического параметра функции, далее функция работает только со значениями параметров в стеке. При выходе из функции стек очищается, и данные значения, даже если они были модифицированы функцией, теряются.
В языке С вызванная функция не может изменять значения переменных, указанных в качестве фактических параметров функции, передаваемых по имени при обращении к ней. После выхода из функции значение переменной будет таким, каким оно было до входа в функцию. Если необходимо, чтобы функция могла изменять значения переменных, передаваемых ей в качестве фактических параметров, используют передачу параметра в функцию по ссылке. В этом случае в качестве фактического параметра указывается адрес переменной, который и заносится в стек. Вызванная функция обращается с параметром через ссылку, находящуюся в стеке (через адрес), тем самым работая с основной переменной, а не с ее копией. После выхода из функции значение переменной будет таким, каким оно было при выходе из функции. Если в качестве аргумента функции используется массив, то его можно передать в качестве параметра в функцию только по ссылке. В этом случае передается указатель на первый элемент массива. При указании многомерного массива в качестве фактического параметра функции лучше всего явно указывать значение его размерности по всем координатам или предавать указатель на первый элемент массива.
В функцию в качестве параметра может быть передано имя другой функции, которая может быть использована в процессе выполнения