- •Функции.
- •Вызов функции с переменным числом параметров
- •Функция main и её параметры.
- •Директивы препроцессора (прекомпилера).
- •Объявление указателей.
- •Модификатор const.
- •Операции.
- •Указатели на различные типы.
- •Указатель на void.
- •Применение указателей для передачи данных между функциями.
- •Массивы.
- •Индексация массивов.
- •Хранение массива в памяти. Адреса элементов. Хранение массива в памяти.
- •Массивы и константные указатели.
- •Статическое и динамическое выделение памяти.
- •Функции calloc, malloc, free
- •Функция realloc
- •Передача массивов в качестве аргументов функции.
- •Указатели на функции.
- •Библиотеки функций.
- •Функции форматированного ввода-вывода.
- •Функция printf().
- •%[Флаги] [Ширина] [.Точность] [{h | l | I | i32 | i64}]тип
- •Для чего нужен форматированный вывод.
- •Функция scanf().
- •Функции sprintf() и sscanf().
- •Функции fprintf() и fscanf().
- •Функции неформатированного ввода-вывода.
- •Работа со строковыми данными (стрингами). Представление строковых данных в языке c.
- •Функции работы со строками.
- •Потоковый ввод-вывод
- •Функции форматированного ввода-вывода.
- •Функция printf().
- •%[Флаги] [Ширина] [.Точность] [{h | l | I | i32 | i64}]тип
- •Для чего нужен форматированный вывод.
- •Функция scanf().
- •Функции sprintf() и sscanf().
- •Функции fprintf() и fscanf().
- •Функции неформатированного ввода-вывода.
- •Функции работы с файлами.
- •Потоковый ввод-вывод
- •Работа с потоками
- •Курсор.
- •Ввод-вывод отдельных символов и строк.
- •Форматированный ввод-вывод информации в файл.
- •Блочный потоковый ввод-вывод
- •Смена текущей позиции в файле. Проверка конца файла.
- •Функции доступа к файлам нижнего уровня.
- •Методы сортировки данных.
- •Введение
- •Сравнение методов сортировки
- •Программная реализация алгоритмов сортировки
- •Метод пузырька.
- •Метод обмена.
- •Метод вставки.
- •Метод Шелла.
- •Метод кучи (бинарной кучи).
- •Очередь
- •Линейный список
- •Физическое (машинное) представление линейных списков
- •Программные реализации структур данных. Стек. Реализация в виде массива.
- •Стек. Связанное представление.
- •Очереди. Реализация в виде массива.
- •Дерево. Связанное представление.
- •Рекурсивный вызов функций.
- •Структуры. Объединения. Перечисления.
- •Перечисление (enum).
- •Производные типы данных.
- •Структура (struct).
- •Побитовое описание полей структуры.
- •Объявление переменных, реализующих структуру.
- •Доступ к элементам структуры.
- •Объединение (union).
- •Вложенное описание структур и объединений.
- •Описание структур и объединений в виде пользовательского типа.
- •Передача структур и объединений в виде параметров функции.
- •Инициализация структур и объединений.
- •Выгода от использования структур
Массивы и константные указатели.
Массив (без квадратных скобок) указывает на начало области памяти, где хранятся данные. Этот адрес не может быть изменён, поэтому в большинстве случаев можно считать, что массив является константным указателем на область хранения его данных, и оперировать с ним как с обычным константным указателем. Тем не менее, между ними существует небольшая разница, на практике проявляющаяся в очень редких случаях.
Основных различий два:
результат применения оператора sizeof.
результат применения опреации взятия адреса &.
Оператор sizeof, применённый к массиву возвращает его размер в байтах. Результат же применения оператора sizeof к любому константному (и не константному) указателю всегда равен одной и той же константе, зависящей только от целевой платформы. Для win32 и большинства других платформ она равна 4.
Таким образом, следующая программа:
main()
{
char arr[10];
char* p;
p=arr;
printf("sizeof(arr)=%d, sizeof(p)=%d",sizeof(arr),sizeof(p));
}
в результате выполнения распечатает строку "sizeof(arr)=10, sizeof(p)=4", хотя и массив, и указатель p указывают на одну и ту же область памяти. Более того, как мы увидим позднее, в случае одномерного вектора и к arr, и к p можно полностью идентично применять операцию индексации [].
Второе различие заключается в применении оператора взятия адреса &. Поскольку указатель, и константный в том числе, является переменной и хранится в определённой области памяти, то операция &, применённая к нему, вернёт адрес этой области памяти, где хранится указатель. Если же применить эту операцию к массиву, то мы получим в результате адрес области памяти, в которой хранится значение массива, т.е. тот же саамы адрес, на который указывает сам массив. Т.е., в отличие от указателей, для массива arr значения arr и &arr будут одинаковыми.
Также существует ряд тонкостей в использовании операции индексации для массивов и указателей. Для одномерных массивов не существует никакой разницы в применении этой операции к массиву или к указателю. Например:
main()
{
char arr[10];
char* p;
p=arr;
printf("arr[5]=%d, p[5]=%d", arr[5],p[5]);
}
В результате выполнения этой программы 2 раза будут напечатаны значения 5-го элемента массива. В первом случае значение элемента получено с помощью операции, применённой к массиву, во втором – с помощью указателя, указывающего на ту же область памяти, что и массив. При этом операция индексации p[5] точно так же, как и в случае массива обозначает "сдвинуть указатель на пять блоков и взять значение по полученному адресу".
Различия в применении операции [] начинаются в случае применения многомерных массивов.
Пусть у нас есть двумерный массив:
char arr[2][4];
Если вместо двойного применения операции индексации к массиву arr применить её только один раз, то результатом такой операции будет являться адрес начала соответствующей строки в памяти (это действительно и для массивов больших размерностей), например arr[1] вернёт адрес начала 1й строки.
-
arr[0][0]
arr[0][1]
arr[0][2]
arr[0][3]
arr[1][0]
arr[1][1]
arr[1][2]
arr[1][3]
↑ ↑
arr arr[1]
p
|–– p[1] –––| | (arr[1])[2] |
-
XX
XX
XX
XX
↑
p[1] (указатель)
|– (p[1])[2] –|
Подходя формально, операцию получения элемента [1][2] массива arr можно записать как двойное последовательное применение операции индексации [], т.е. выражение
e=arr[1][2]; можно переписать в виде e=(arr[1])[2];
Тогда первая операция индексации вернёт адрес начала первой строки, а вторая операция – сместит этот указатель на 2 позиции (т.е. на начало второго элемента этой строки) и считает из памяти значение ячейки. Таким образом, мы получим значение элемента arr[1][2], что и требовалось.
Теперь рассмотрим применение точно таких же операций к указателю
char** p=arr;
указывающему на начало массива. Две звёздочки здесь означают, что p является указателем на указатель на char. На самом деле, можно было написать и только одну звёздочку, смысл от этого особенно не изменится, но тогда при двойном применении операции [] пришлось бы использовать явное приведение типов к типу указатель.
Полностью аналогично, выражение e=p[1][2]; можно переписать в виде e=(p[1])[2]; (именно так компилятор расставит порядок приоритетов).
Но, первое применение операции индексации к указателю p вернёт значение, лежащее в ячейке p[1] (т.е. значение ячейки arr[0][1]). Т.е., в отличие от массива, p[1] указывает не на начало первой строки, а на совершенно постороннюю область памяти, адресуемую значением, хранящимся в arr[0][1].
При втором применении операции индексации уже к указателю p[1], она вернёт значение второй ячейки из этой посторонней области памяти, лежащей в области памяти, не имеющей никакого отношения к массиву. Поэтому это значение будет абсолютно бессмысленным. Также программа может выдать сообщение об ошибочной попытке доступа к запрещённой области памяти (т.е. о попытке доступа к памяти, которая не была выделена менеджером памяти).
Поэтому, если вам необходимо организовать хранение данных на основе указателей с многомерной индексацией (а это необходимо при динамическом распределении памяти, описанном ниже), то это делается с помощью выделения нескольких областей памяти. В первой, адресуемой непосредственно указателем p хранятся указатели на N других областей, в которых хранятся данные, или ссылки на другие области данных более низкого уровня и т.д. Например, массив char arr[2][4]; можно сохранить вручную в памяти с помощью указателей следующим образом (где char** p; – указатель для доступа к данным):
-
Addr1
Addr2
↑p
-
p[0][0]
p[0][1]
p[0][2]
p[0][3]
↑p[0]=Addr1
-
p[1][0]
p[1][1]
p[1][2]
p[1][3]
↑p[1] =Addr2
В этом случае операция p[i][j] будет корректно адресовать нужную ячейку памяти.
Такой способ хранения данных очень трудоёмок. Поэтому в случае, когда требуется хранение данных на основе указателей (динамическое) с многомерной индексацией, гораздо проще хранить их в виде одной непрерывной области памяти, пересчитывая многомерные индексы в одномерные по приведённой в предыдущем параграфе формуле.
В любом случае, везде, где возможно применение массивов вместо указателей, лучше применять массивы.
Динамическое выделение памяти. Работа с выделенным блоком памяти (пример). Отличия использования динамически выделенной памяти от обычных массивов. Плюсы и минусы динамического выделения памяти.