Основы программирования на C

Carder

Professional
Messages
2,619
Reaction score
1,883
Points
113
Эта анимация показывает выполнение простой программы на C.  К концу статьи вы поймете, как это работает!


Эта анимация показывает выполнение простой программы на C. К концу статьи вы поймете, как это работает!
Язык программирования C - популярный и широко используемый язык программирования для создания компьютерных программ. Программисты во всем мире принимают C, потому что он дает программисту максимальный контроль и эффективность.

Если вы программист или хотите стать программистом, изучение C дает вам несколько преимуществ:
  • Вы сможете читать и писать код для большого количества платформ - все, от микроконтроллеров до самых передовых научных систем, может быть написано на C, а многие современные операционные системы написаны на C.
  • Переход к объектно-ориентированному языку C ++ становится намного проще. C ++ является расширением C, и практически невозможно изучить C ++ без предварительного изучения C.
В этой статье мы пройдемся по всему языку и покажем вам, как стать программистом на C, начиная с самого начала. Вы будете поражены разнообразием вещей, которые вы сможете создать, когда узнаете C!

Что такое C?​

c-compile.gif

C - это компьютерный язык программирования. Это означает, что вы можете использовать C для создания списков инструкций, которым должен следовать компьютер. C - один из тысяч языков программирования, используемых в настоящее время. C существует уже несколько десятилетий и завоевал широкое признание, поскольку дает программистам максимальный контроль и эффективность. C - простой язык для изучения. По своему стилю он немного более загадочен, чем у некоторых других языков, но вы довольно быстро выходите за его рамки.
C - это то, что называется компилируемым языком. Это означает, что как только вы напишете свою программу на C, вы должны запустить ее через компилятор C, чтобы превратить вашу программу в исполняемый файлчто компьютер может запускать (выполнять). Программа C - это форма, удобочитаемая человеком, а исполняемый файл, который выходит из компилятора, - это машиночитаемая и исполняемая форма. Это означает, что для написания и запуска программы на C у вас должен быть доступ к компилятору C. Если вы используете машину UNIX (например, если вы пишете сценарии CGI на языке C на компьютере UNIX вашего хоста, или если вы студент, работающий на машине UNIX в лаборатории), компилятор C доступен бесплатно. Он называется «cc» или «gcc» и доступен в командной строке. Если вы студент, то школа, скорее всего, предоставит вам компилятор - узнайте, что школа использует, и узнайте об этом. Если вы работаете дома на компьютере с Windows, вам нужно будет загрузить бесплатный компилятор C или приобрести коммерческий компилятор. Широко используемый коммерческий компилятор - это среда Microsoft Visual C ++ (она компилирует программы как на C, так и на C ++). К сожалению, эта программа стоит несколько сотен долларов. Если у вас нет сотен долларов, которые можно потратить на коммерческий компилятор, вы можете использовать один из бесплатных компиляторов, доступных в Интернете. Видеть http://delorie.com/djgpp/ в качестве отправной точки для поиска.
Мы начнем с очень простой программы на языке C и продолжим работу над ней. Я предполагаю, что вы используете командную строку UNIX и gcc в качестве среды для этих примеров; в противном случае весь код будет работать нормально - вам просто нужно понять и использовать любой доступный компилятор.
Давайте начнем!

Простейшая программа на C​

Давайте начнем с простейшей программы на C и будем использовать ее для понимания основ C и процесса компиляции C. Введите следующую программу в стандартный текстовый редактор (vi или emacs в UNIX, Блокнот в Windows или TeachText в Macintosh). Затем сохраните программу в файл с именем samp.c. Если вы оставите .c, вы, вероятно, получите какую-то ошибку при его компиляции, поэтому убедитесь, что вы помните .c. Также убедитесь, что ваш редактор не добавляет автоматически некоторые дополнительные символы (например, .txt) к имени файла. Вот первая программа:

Code:
#include <stdio.h>

int main ()
{
    printf ("Это результат моей первой программы! \ n");
    return 0;
}

c-compile.gif

Компьютерная программа - ключ к цифровому городу: если вы знаете язык, вы можете заставить компьютер делать практически все, что захотите. Узнайте, как писать компьютерные программы на C.

При выполнении эта программа инструктирует компьютер распечатать строку «Это результат моей первой программы!» - затем программа закрывается. Вы не можете найти ничего проще!
Чтобы скомпилировать этот код, выполните следующие действия:
  • На компьютере UNIX введите gcc samp.c -o samp (если gcc не работает, попробуйте cc). Эта строка вызывает компилятор C под названием gcc, просит его скомпилировать samp.c и просит разместить создаваемый исполняемый файл под именем samp. Чтобы запустить программу, введите samp (или на некоторых машинах UNIX ./samp ).
  • На машине с DOS или Windows, использующей DJGPP, в командной строке MS-DOS введите gcc samp.c -o samp.exe. Эта строка вызывает компилятор C под названием gcc, просит его скомпилировать samp.c и просит его разместить создаваемый исполняемый файл под именем samp.exe. Чтобы запустить программу, введите samp .
  • Если вы работаете с каким-либо другим компилятором или системой разработки, прочтите и следуйте инструкциям для компилятора, который вы используете для компиляции и выполнения программы.
Вы должны увидеть вывод "Это результат моей первой программы!" при запуске программы. Вот что произошло, когда вы скомпилировали программу:
Если вы неправильно наберете программу, она либо не скомпилируется, либо не запустится. Если программа не компилируется или работает некорректно, отредактируйте ее еще раз и посмотрите, где вы ошиблись при вводе текста. Исправьте ошибку и попробуйте еще раз.

Позиция
Когда вы входите в эту программу, поместите #include так, чтобы знак фунта находился в столбце 1 (крайний левый край). В противном случае интервалы и отступы могут быть любыми, как вам нравится. В некоторых системах UNIX вы найдете программу cb, C Beautifier, которая будет форматировать код за вас. Показанные выше интервалы и отступы - хороший пример для подражания.

Простейшая программа на C: что происходит?​

Давайте пройдемся по этой программе и начнем видеть, что делают разные строки:
  • Эта программа на C начинается с #include <stdio.h>. Эта строка включает в вашу программу «стандартную библиотеку ввода-вывода». Стандартная библиотека ввода-вывода позволяет вам читать ввод с клавиатуры (так называемый «стандартный ввод »), записывать вывод на экран (так называемый «стандартный вывод»), обрабатывать текстовые файлы, хранящиеся на диске, и так далее. Это чрезвычайно полезная библиотека. C имеет большое количество стандартных библиотек, таких как stdio, включая библиотеки строк, времени и математики. Библиотека просто пакет кода, который кто - то написал, чтобы сделать вашу жизнь проще (мы обсудим библиотеку немного позже).
  • Строка int main () объявляет главную функцию. Каждая программа на C должна иметь где-нибудь в коде функцию с именем main. Вскоре мы узнаем больше о функциях. Во время выполнения выполнение программы начинается с первой строки основной функции.
  • В C символы { и } обозначают начало и конец блока кода. В этом случае блок кода, составляющий основную функцию, состоит из двух строк.
  • Оператор printf в C позволяет отправлять вывод на стандартный вывод (для нас на экран). Часть в кавычках называется строкой формата и описывает, как данные должны быть отформатированы при печати. Строка формата может содержать строковые литералы, такие как «Это результат моей первой программы!», Символы для возврата каретки (\ n) и операторы в качестве заполнителей для переменных (см. Ниже). Если вы используете UNIX, вы можете набрать man 3 printf, чтобы получить полную документацию по функции printf. В противном случае см. Документацию, прилагаемую к вашему компилятору, для получения подробной информации о функции printf.
  • Возврат 0;строка заставляет функцию возвращать код ошибки 0 (нет ошибки) оболочке, которая начала выполнение. Подробнее об этой возможности чуть позже.

Переменные​

Как программист, вы часто хотите, чтобы ваша программа «запоминала» значение. Например, если ваша программа запрашивает значение у пользователя или вычисляет значение, вы захотите запомнить его где-нибудь, чтобы использовать его позже. Ваша программа запоминает вещи с помощью переменных. Например:
Code:
int b;
В этой строке говорится: «Я хочу создать пространство с именем b, которое может содержать одно целое значение». Переменная имеет имя (в данном случае b) и тип (в данном случае int, целое число). Вы можете сохранить значение в b, сказав что-то вроде:
Code:
b = 5;
Вы можете использовать значение в b, сказав что-то вроде:
Code:
printf ("% d", b);

В C есть несколько стандартных типов переменных:
  • int - целые (целые числа) значения
  • float - значения с плавающей запятой
  • char - односимвольные значения (например, "m" или "Z")
По мере продвижения мы увидим примеры этих других типов.

Printf​

Оператор printf позволяет отправлять вывод на стандартный вывод.Для нас стандартным выходом обычно является экран (хотя вы можете перенаправить стандартный выход в текстовый файл или другую команду).

Вот еще одна программа, которая поможет вам узнать больше о printf:
Code:
#include <stdio.h>

int main()
{
    int a, b, c;
    a = 5;
    b = 7;
    c = a + b;
    printf("%d + %d = %d\n", a, b, c);
    return 0;
}

Введите эту программу в файл и сохранить его как add.c. Скомпилируйте его с помощью строки gcc add.c -o add, а затем запустите, набрав add (или ./add). Вы увидите строку «5 + 7 = 12» в качестве вывода.
Вот объяснение различных строк в этой программе:
  • Строка int a, b, c;объявляет три целочисленные переменные с именами a, b и c. Целочисленные переменные содержат целые числа.
  • Следующая строка инициализирует переменную с именем a значением 5.
  • Следующая строка устанавливает b равным 7.
  • Следующая строка добавляет a и b и «присваивает» результат c. Компьютер добавляет значение в a (5) к значению в b (7), чтобы сформировать результат 12, а затем помещает это новое значение (12) в переменную c. Переменной c присваивается значение 12. По этой причине знак = в этой строке называется «оператором присваивания».
  • Затем оператор printf выводит строку «5 + 7 = 12». В % D заполнители в PRINTF заявление выступает в качестве заполнителей для значений. Есть три заполнителя% d, а в конце строки printf есть три имени переменных: a, b и c. C сопоставляет первый% d с a и заменяет 5 там. Он сопоставляет второй% d с b и заменяет 7. Он сопоставляет третий% d с c и заменяет 12. Затем он выводит завершенную строку на экран: 5 + 7 = 12. Знаки +, = и интервал являются a часть строки формата и автоматически встраивается между операторами% d, как указано программистом.

Printf: чтение пользовательских значений​


Предыдущая программа хороша, но было бы лучше, если бы она считывала значения 5 и 7 от пользователя вместо использования констант. Попробуйте вместо этого эту программу:
Code:
#include <stdio.h>

int main()
{
    int a, b, c;
    printf("Enter the first value:");
    scanf("%d", &a);
    printf("Enter the second value:");
    scanf("%d", &b);
    c = a + b;
    printf("%d + %d = %d\n", a, b, c);
    return 0;
}
c-exec.gif


Вот как эта программа работает, когда вы ее выполняете:
Внесите изменения, затем скомпилируйте и запустите программу, чтобы убедиться, что она работает. Обратите внимание, что scanf использует тот же тип строки формата, что и printf ( для получения дополнительной информации введите man scanf ). Также обратите внимание на & перед a и b. Это оператор адреса в C: он возвращает адрес переменной (это не будет иметь смысла, пока мы не обсудим указатели). Вы должны использовать оператор & в scanf для любой переменной типа char, int или float, а также для типов структур (к которым мы вскоре вернемся). Если вы опустите оператор &, вы получите сообщение об ошибке при запуске программы. Попробуйте, чтобы увидеть, как выглядит такая ошибка времени выполнения.
Давайте рассмотрим некоторые варианты, чтобы полностью понять printf. Вот простейший оператор printf:
Code:
printf ("Hello");
Этот вызов printf имеет строку формата, которая сообщает printf, что нужно послать слово «Hello» стандартному выходу. Сравните это с этим:
Code:
printf ("Hello \ n");
Разница между ними в том, что вторая версия отправляет слово «Hello» с последующим возвратом каретки к стандартному выходу.
В следующей строке показано, как вывести значение переменной с помощью printf.
Code:
printf ("% d", b);
% D представляет собой заполнитель, который будет заменен на значение переменной Ь, когда выполняется оператор Printf. Часто вам нужно вставить значение в другие слова. Один из способов добиться этого:
Code:
printf("The temperature is ");
printf("%d", b);
printf(" degrees\n");

Проще сказать следующее:
Code:
printf("The temperature is %d degrees\n", b)

Вы также можете использовать несколько заполнителей% d в одном операторе printf:
Code:
printf ("% d +% d =% d \ n", a, b, c);
В операторе printf чрезвычайно важно, чтобы количество операторов в строке формата точно соответствовало количеству и типу следующих за ней переменных. Например, если строка формата содержит три оператора% d, то за ней должны следовать ровно три параметра, и они должны иметь те же типы в том же порядке, что и те, которые указаны операторами.

Вы можете распечатать все обычные типы C с помощью printf, используя разные заполнители:
  • int (целочисленные значения) использует % d
  • float (значения с плавающей запятой) использует % f
  • char (односимвольные значения) использует % c
  • character strings (массивы символов, обсуждаемые позже) используют % s
Вы можете узнать больше о нюансах printf на UNIX-машине, набрав man 3 printf. Любой другой компилятор C, который вы используете, вероятно, будет поставляться с руководством или файлом справки, содержащим описание printf.

Ошибки C, которых следует избегать:
  • Использование неправильного регистра символов - регистр имеет значение в C, поэтому вы не можете вводить Printf или PRINTF. Это должен быть printf.
  • Забыть использовать & в scanf
  • Слишком много или слишком мало параметров после оператора формата в printf или scanf
  • Забыть объявить имя переменной перед ее использованием

Scanf​

Функция scanf позволяет вам принимать ввод из стандартного in, которым для нас обычно является клавиатура. Функция scanf может делать много разных вещей, но может быть ненадежной, потому что не очень хорошо обрабатывает человеческие ошибки. Но для простых программ он достаточно хорош и прост в использовании.
Самое простое приложение scanf выглядит так:
Code:
scanf ("% d", & b);
Программа прочитает целочисленное значение, которое пользователь вводит с клавиатуры (% d для целых чисел, как и printf, поэтому b должен быть объявлен как int) и поместит это значение в b.

Функция scanf использует те же заполнители, что и printf:
  • int использует % d
  • float использует % f
  • char использует % c
  • character strings (обсуждаемые позже) используют % s
Вы должны поставить & перед переменной, используемой в scanf. Причина станет ясной, когда вы узнаете об указателях. Знак & легко забыть, и если вы его забудете, ваша программа почти всегда выйдет из строя при запуске.
В общем, лучше всего использовать scanf, как показано здесь - для чтения одного значения с клавиатуры. Используйте несколько вызовов scanf для чтения нескольких значений. В любой реальной программе вы будете использовать функции gets или fgets вместо того, чтобы читать текст построчно. Затем вы «проанализируете» строку, чтобы прочитать ее значения. Причина, по которой вы это делаете, заключается в том, чтобы вы могли обнаруживать ошибки во вводе и обрабатывать их по своему усмотрению.
Чтобы полностью понять функции printf и scanf, потребуется немного практики, но после освоения они становятся чрезвычайно полезными.

Попробуйте это:
Измените эту программу так, чтобы она принимала три значения вместо двух и складывала все три вместе:
Code:
#include <stdio.h>

int main ()
{
int a, b, c;
printf ("Введите первое значение:");
scanf ("% d", & a);
printf ("Введите второе значение:");
scanf ("% d", & b);
с = а + Ь;
printf ("% d +% d =% d \ n", a, b, c);
return 0;
}

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

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

Ветвление и зацикливание​

c-if.gif

В C как операторы if, так и циклы while основаны на идее логических выражений. Вот простая программа на C, демонстрирующая оператор if:
Code:
#include int main () {int b; printf ("Введите значение:"); scanf ("% d", & b); if (b <0) printf ("Значение отрицательное n"); return 0; }
Эта программа принимает номер от пользователя. Затем он проверяет число, используя оператор if, чтобы убедиться, что оно меньше нуля. Если это так, программа печатает сообщение. В противном случае программа молчит. Часть программы (b <0) - это логическое выражение. C оценивает это выражение, чтобы решить, печатать сообщение или нет. Если логическое выражение имеет значение True, то C выполняет единственную строку, следующую сразу за оператором if (или блок строк в фигурных скобках, следующий сразу за оператором if). Если логическое выражение равно False, то C пропускает строку или блок строк, следующие сразу за оператором if.

Вот немного более сложный пример:
Code:
#include <stdio.h>

int main ()
{
int b;
printf ("Введите значение:");
scanf ("% d", & b);
если (b <0)
printf ("Значение отрицательное \ n");
return 0;
}
В этом примере разделы else if и else также оценивают нулевые и положительные значения.

Вот более сложное логическое выражение:
Code:
if ((x==y) && (j>k))
    z=1;
else
    q=10;

В этом заявлении говорится: «Если значение в переменной x равно значению в переменной y, и если значение в переменной j больше, чем значение в переменной k, то установите для переменной z значение 1, в противном случае установите для переменной q значение 10. " Вы будете использовать подобные операторы if в своих программах на языке C для принятия решений. В общем, большинство решений, которые вы примете, будут простыми, как в первом примере; но иногда все становится сложнее.
Обратите внимание, что C использует == для проверки равенства, в то время как он использует = для присвоения значения переменной. && в C представляет собой логическое И операцию.

Вот все булевы операторы в C:
Code:
равенство ==
меньше чем <
Больше чем>
<= <=
> => =
неравенство! =
и &&
или ||
нет !
c-while.gif

Вы обнаружите, что операторы while так же просты в использовании, как операторы if. Например:
в то время как (a <b)
Code:
{
printf ("% d \ n", а);
а = а + 1;
}
Это приводит к многократному выполнению двух строк в фигурных скобках, пока a не станет больше или равно b. Оператор while в целом работает, как показано справа.

C также предоставляет структуру do-while:
Code:
#include <stdio.h>

int main ()
{
int a;

printf ("Введите число:");
scanf ("% d", & a);
если)
{
printf ("Значение True \ n");
}
return 0;
}

Цикл for в C - это просто сокращенный способ выражения оператора while. Например, предположим, что у вас есть следующий код на C:
Code:
x=1;
while (x<10)
{
    blah blah blah
x ++; / * x ++ - это то же самое, что сказать x = x + 1 * /
}

Вы можете преобразовать это в цикл for следующим образом:
Code:
for(x=1; x<10; x++)
{
    blah blah blah
}

Обратите внимание, что цикл while содержит шаг инициализации (x = 1), тестовый шаг (x <10) и шаг приращения (x ++). Цикл for позволяет поместить все три части в одну строку, но в эти три части можно поместить что угодно. Например, предположим, что у вас есть следующий цикл:
Code:
a=1;
b=6;
while (a < b)
{
    a++;
    printf("%d\n",a);
}

Вы также можете поместить это в оператор for:
Code:
for (a = 1, b = 6; a <b; a ++, printf ("% d \ n", a));
Это немного сбивает с толку, но возможно. Оператор запятая позволяет разделить несколько разных операторов в разделах инициализации и увеличения цикла for (но не в разделе теста). Многие программисты на C любят упаковывать много информации в одну строку кода на языке C; но многие люди думают, что это затрудняет понимание кода, поэтому они разбивают его.

= vs. == в логических выражениях
Знак == является проблемой в C, потому что время от времени вы можете забыть и ввести просто = в логическом выражении. Это легко сделать, но для компилятора есть очень важное отличие. С будет принимать либо = и == в логическом выражении - поведение программы изменяется заметно между ними, однако.
Булевы выражения оценивают целые числа в C, и целые числа могут использоваться внутри логических выражений. Целочисленное значение 0 в C равно False, а любое другое целочисленное значение - True. В C допустимо следующее:
Если a не 0, выполняется инструкция printf.
В C, как и утверждение, если (а = б) средства, «Присвоить б к, а затем проверить для его логического значения». Итак, если a становится 0, оператор if принимает значение False; в противном случае это правда. Значение изменяется в процессе. Это не предполагаемое поведение, если вы хотели ввести == (хотя эта функция полезна при правильном использовании), поэтому будьте осторожны с использованием = и == .

Цикл: реальный пример​

Предположим, вы хотите создать программу, которая печатает таблицу преобразования Фаренгейта в Цельсия. Это легко сделать с помощью цикла for или цикла while:
Code:
#include <stdio.h>

int main()
{
    int a;
    a = 0;
    while (a <= 100)
    {
        printf("%4d degrees F = %4d degrees C\n",
            a, (a - 32) * 5 / 9);
        a = a + 10;
    }
    return 0;
}

Если вы запустите эту программу, она создаст таблицу значений, начиная с 0 градусов по Фаренгейту и заканчивая 100 градусами F. Результат будет выглядеть следующим образом:
Code:
0 градусов F = -17 градусов C
10 градусов F = -12 градусов C
20 градусов F = -6 градусов C
30 градусов F = -1 градус C
40 градусов F = 4 градуса C
50 градусов F = 10 градусов C
60 градусов F = 15 градусов C
70 градусов F = 21 градус C
80 градусов F = 26 градусов C
90 градусов F = 32 градуса C
100 градусов F = 37 градусов C
Значения в таблице указаны с шагом 10 градусов. Вы можете видеть, что вы можете легко изменить начальное, конечное или приращение значений таблицы, создаваемой программой.

Если вы хотите, чтобы ваши значения были более точными, вы можете вместо этого использовать значения с плавающей запятой:
Code:
#include <stdio.h>

int main()
{
    float a;
    a = 0;
    while (a <= 100)
    {
        printf("%6.2f degrees F = %6.2f degrees C\n",
            a, (a - 32.0) * 5.0 / 9.0);
        a = a + 10;
    }
    return 0;
}

Вы можете видеть, что объявление для a было изменено на float, а символ % f заменяет символ % d в операторе printf. Кроме того, к символу% f применено некоторое форматирование: значение будет напечатано с шестью цифрами перед десятичной точкой и двумя цифрами после десятичной точки.
Теперь предположим, что мы хотели изменить программу, чтобы температура 98,6 была вставлена в таблицу в нужном месте. То есть, мы хотим, чтобы таблица увеличивалась каждые 10 градусов, но мы также хотим, чтобы таблица включала дополнительную строку для 98,6 градусов по Фаренгейту, потому что это нормальная температура тела для человека. Следующая программа достигает цели:
Code:
#include <stdio.h>

int main()
{
    float a;
    a = 0;
    while (a <= 100)
    {
	if (a > 98.6)
        {
            printf("%6.2f degrees F = %6.2f degrees C\n",
                98.6, (98.6 - 32.0) * 5.0 / 9.0);
        }
        printf("%6.2f degrees F = %6.2f degrees C\n",
            a, (a - 32.0) * 5.0 / 9.0);
        a = a + 10;
    }
    return 0;
}

Эта программа работает, если конечное значение равно 100, но если вы измените конечное значение на 200, вы обнаружите, что в программе есть ошибка. Он слишком много раз печатает линию на 98,6 градуса. Мы можем решить эту проблему несколькими способами. Вот один из способов:
Code:
#include <stdio.h>

int main()
{
    float a, b;
    a = 0;
    b = -1;
    while (a <= 100)
    {
	if ((a > 98.6) && (b < 98.6))
        {
            printf("%6.2f degrees F = %6.2f degrees C\n",
                98.6, (98.6 - 32.0) * 5.0 / 9.0);
        }
        printf("%6.2f degrees F = %6.2f degrees C\n",
            a, (a - 32.0) * 5.0 / 9.0);
        b = a;
        a = a + 10;
    }
    return 0;
}

Ошибки C, которых следует избегать:
  • Ввод =, когда вы имеете в виду == в выражении if или while
  • Забыть увеличить счетчик внутри цикла while - если вы забудете увеличить счетчик, вы получите бесконечный цикл (цикл никогда не заканчивается).
  • Случайно положил; в конце цикла for или оператора if, чтобы оператор не имел эффекта - Например: for (x = 1; x <10; x ++); printf ("% d \ n", х); выводит только одно значение, потому что точка с запятой после оператора for действует как одна строка, которую выполняет цикл for.

Попробуйте это:
  • Попробуйте изменить программу преобразования Фаренгейта в Цельсию так, чтобы она использовала scanf для принятия от пользователя начального, конечного и приращения значения таблицы.
  • Добавьте строку заголовка к создаваемой таблице.
  • Попробуйте найти другое решение ошибки, исправленной в предыдущем примере.
  • Создайте таблицу, в которой фунты преобразуются в килограммы или мили в километры.

Массивы​

c-array.gif

В этом разделе мы создадим небольшую программу на C, которая генерирует 10 случайных чисел и сортирует их. Для этого мы будем использовать новый порядок переменных, называемый массивом .
Массив позволяет объявлять набор значений одного типа и работать с ним. Например, вы можете захотеть создать коллекцию из пяти целых чисел. Один из способов сделать это - напрямую объявить пять целых чисел:
Code:
int a, b, c, d, e;
Это нормально, но что, если вам нужна тысяча целых чисел? Более простой способ - объявить массив из пяти целых чисел:
Code:
int a [5];
Доступ к пяти отдельным целым числам внутри этого массива осуществляется по индексу. Все массивы начинаются с нулевого индекса и переходят к n-1 в C. Таким образом, int a [5];содержит пять элементов. Например:
Code:
int a [5];

а [0] = 12;
а [1] = 9;
а [2] = 14;
а [3] = 5;
а [4] = 1;

Одна из приятных особенностей индексирования массивов заключается в том, что вы можете использовать цикл для управления индексом. Например, следующий код инициализирует все значения в массиве равными 0:
Code:
int a[5];
int i;

for (i=0; i<5; i++)
    a[i] = 0;

Следующий код последовательно инициализирует значения в массиве и затем распечатывает их:
Code:
#include <stdio.h>

int main()
{
    int a[5];
    int i;

    for (i=0; i<5; i++)
        a[i] = i;
    for (i=0; i<5; i++)
        printf("a[%d] = %d\n", i, a[i]);
}

Массивы используются в C. Все время используются массивы. Чтобы понять их общее использование, запустите редактор и введите следующий код:
Code:
#include <stdio.h>

#define MAX 10

int a [MAX];
int rand_seed = 10;

/ * из K&R
- возвращает случайное число от 0 до 32767. * /
int rand()
{
    rand_seed = rand_seed * 1103515245 +12345;
    return (unsigned int)(rand_seed / 65536) % 32768;
}

int main()
{
    int i,t,x,y;

/ * заполняем массив * /
   for (i=0; i < MAX; i++)
    {
        a[i]=rand();
        printf("%d\n",a[i]);
    }

/ * через минуту сюда будет добавлено больше * /

return 0;
}

Этот код содержит несколько новых концепций. В строке #define объявляется константа с именем MAX и устанавливается значение 10. Имена констант традиционно пишутся заглавными буквами, чтобы сделать их очевидными в коде. Строка int a [MAX] ;показывает, как объявить массив целых чисел в C. Обратите внимание, что из-за позиции объявления массива он является глобальным для всей программы.
Строка int rand_seed = 10 также объявляет глобальную переменную, на этот раз с именем rand_seed, которая инициализируется значением 10 при каждом запуске программы. Это значение является начальным значением для следующего кода случайных чисел. В реальном генераторе случайных чисел начальное число должно инициализироваться как случайное значение, такое как системное время. Здесь функция rand будет выдавать одни и те же значения каждый раз, когда вы запускаете программу.
Строка int rand () - это объявление функции. Функция rand не принимает параметров и возвращает целочисленное значение. Мы узнаем больше о функциях позже. Следующие четыре строки реализуют функцию rand. Мы пока их игнорируем.
Основная функция нормальная. Объявлены четыре локальных целых числа, и массив заполняется 10 случайными значениями с помощью цикла for. Обратите внимание, что массив a содержит 10 отдельных целых чисел. Вы указываете на конкретное целое число в массиве, используя квадратные скобки. Таким образом, a [0] относится к первому целому числу в массиве, a [1] относится ко второму и так далее. Строка, начинающаяся с / * и заканчивающаяся * /, называется комментарием. Компилятор полностью игнорирует строку. Вы можете оставлять в комментариях заметки для себя или других программистов.
Теперь добавьте следующий код вместо дополнительных материалов ... комментарий:
Code:
/ * пузырьковая сортировка массива * /
for (x=0; x < MAX-1; x++)
    for (y=0; y < MAX-x-1; y++)
        if (a[y] > a[y+1])
        {
            t=a[y];
            a[y]=a[y+1];
            a[y+1]=t;
        }
/ * распечатать отсортированный массив * /
printf ("-------------------- \ n");
для (i = 0; i <MAX; i ++)
printf ("% d \ n", a [i]);

Этот код сортирует случайные значения и печатает их в отсортированном порядке. Каждый раз, когда вы запускаете его, вы будете получать одни и те же значения. Если вы хотите изменить сортируемые значения, изменяйте значение rand_seed каждый раз при запуске программы.
Единственный простой способ по-настоящему понять, что делает этот код, - выполнить его «вручную». То есть, предположим, что MAX равно 4, чтобы сделать его немного более управляемым, возьмите лист бумаги и представьте, что вы компьютер. Нарисуйте массив на бумаге и поместите в него четыре случайных несортированных значения. Выполните каждую строку раздела сортировки кода и определите, что именно происходит. Вы обнаружите, что каждый раз при прохождении внутреннего цикла большие значения в массиве смещаются в нижнюю часть массива, а меньшие значения всплывают вверх.

Попробуй это:
  • В первом фрагменте кода попробуйте изменить цикл for, который заполняет массив, до одной строки кода. Убедитесь, что результат совпадает с исходным кодом.
  • Выньте код пузырьковой сортировки и поместите его в отдельную функцию. Заголовок функции будет пустым, bubble_sort (). Затем переместите переменные, используемые пузырьковой сортировкой, в функцию и сделайте их там локальными. Поскольку массив является глобальным, вам не нужно передавать параметры.
  • Инициализируйте начальное число случайных чисел разными значениями.

Ошибки C, которых следует избегать:
  • В C нет проверки диапазона, поэтому, если вы индексируете за пределами конца массива, он не скажет вам об этом. В конечном итоге это приведет к сбою или выдаче мусора.
  • Вызов функции должен включать (), даже если параметры не переданы. Например, C примет x = rand;, но звонок работать не будет. Вместо этого адрес памяти функции rand будет помещен в x. Вы должны сказать x = rand ();.

Подробнее о массивах​

Типы переменных
В C есть три стандартных типа переменных:
  • Целое число: int
  • Плавающая точка: float
  • Характер: char
Тип int - это 4-байтовое целое число. Число с плавающей запятой - это 4-байтовое значение с плавающей запятой. Char - это однобайтовый символ (например, «a» или «3»). Строка объявляется как массив символов.

Существует ряд производных типов:
  • double (8-байтовое значение с плавающей запятой)
  • короткий (2-байтовое целое)
  • unsigned short или unsigned int (положительные целые числа, без знака)

Операторы и приоритет операторов​

Операторы в C аналогичны операторам в большинстве языков:
Code:
+ - сложение
- - вычитание
/ - разделение
* - умножение
% - мод
Оператор / выполняет целочисленное деление, если оба операнда являются целыми числами, и выполняет деление с плавающей запятой в противном случае. Например:
пустая функция()
Code:
{
float a;
а = 10/3;
printf ("% f \ n", а);
}
Этот код выводит на печать значение с плавающей запятой, поскольку a объявлен как тип float, но a будет иметь значение 3.0, поскольку код выполнял целочисленное деление.
Приоритет операторов в C также аналогичен приоритету операторов в большинстве других языков. Сначала происходит деление и умножение, затем сложение и вычитание. Результат вычисления 5 + 3 * 4 равен 17, а не 32, потому что оператор * имеет более высокий приоритет, чем + в C. Вы можете использовать круглые скобки, чтобы изменить нормальный порядок приоритета: (5 + 3) * 4 равно 32. 5 + 3 вычисляется первым, потому что он указан в скобках. Позже мы перейдем к вопросу о приоритетах - это становится несколько сложным в C после введения указателей.

Приведение типов​

C позволяет выполнять преобразование типов на лету. Особенно часто это происходит при использовании указателей. Приведение типов также происходит во время операции присваивания для определенных типов. Например, в приведенном выше коде целочисленное значение было автоматически преобразовано в число с плавающей запятой.
Вы выполняете приведение типов в C, помещая имя типа в круглые скобки и помещая его перед значением, которое вы хотите изменить. Таким образом, в приведенном выше коде замена строки a = 10/3;с a = (float) 10/3;в результате получается 3,33333, поскольку 10 перед делением преобразуется в значение с плавающей запятой.

Typedef​

Вы объявляете именованные, определяемые пользователем типы в C с помощью оператора typedef. В следующем примере показан тип, который часто встречается в коде C:
Code:
#define TRUE  1
#define FALSE 0
typedef int boolean;

void main()
{
    boolean b;

    b=FALSE;
бла бла бла
}

Этот код позволяет вам объявлять логические типы в программах на C.
Если вам не нравится слово «float» для реальных чисел, вы можете сказать:
Code:
typedef float real;
а потом скажите:
Code:
real r1, r2, r3;
Вы можете размещать операторы typedef где угодно в программе C, если они предшествуют их первому использованию в коде.

Структуры​

Структуры в C позволяют группировать переменные в пакет. Вот пример:
Code:
struct rec
{
int a, b, c;
float d, e, f;
};

struct rec r;

Как показано здесь, всякий раз, когда вы хотите объявить структуры типа rec, вы должны сказать struct rec. Эту строку очень легко забыть, и вы получите много ошибок компилятора, потому что вы по рассеянности упустите структуру. Вы можете сжать код в форму:
Code:
struct rec
{
int a, b, c;
float d, e, f;
} р;
где объявление типа для rec и переменная r объявлены в одном операторе. Или вы можете создать оператор typedef для имени структуры. Например, если вам не нравится произносить struct rec r каждый раз, когда вы хотите объявить запись, вы можете сказать:
Code:
typedef struct rec_type;
а затем объявите записи типа rec_type, сказав:
Code:
rec_type r;
Вы получаете доступ к полям структуры, используя точку, например, ra = 5;.

Массивы​

Вы объявляете массивы, вставляя размер массива после обычного объявления, как показано ниже:
Code:
int a [10]; / * массив целых чисел * /
char s [100]; / * массив символов
(строка C) * /
float f [20]; / * массив реалов * /
struct rec r [50]; / * массив записей * /

Приращение​

Длинный путь короткий путь
Code:
i = i + 1; i ++;
i = i-1; i--;
i = i + 3; i + = 3;
i= i * j; i * = j;

Попробуй это:
  • Попробуйте разные фрагменты кода, чтобы проверить приведение типов и приоритет. Попробуйте int, char, float и т.д.
  • Создайте массив записей и напишите код для сортировки этого массива по одному целочисленному полю.

Ошибки C, которых следует избегать:
Как описано в статье, использование оператора / с двумя целыми числами часто приводит к неожиданным результатам, поэтому думайте об этом всякий раз, когда будете его использовать.

Функции​

Большинство языков позволяют создавать какие-либо функции. Функции позволяют разбивать длинную программу на именованные разделы, чтобы эти разделы можно было повторно использовать в программе. Функции принимают параметры и возвращают результат. Функции C могут принимать неограниченное количество параметров. В общем, C не заботится, в каком порядке вы помещаете свои функции в программу, если имя функции известно компилятору до ее вызова.
О функциях мы уже немного поговорили. Функция rand, которую мы видели ранее, настолько проста, насколько это возможно. Он не принимает никаких параметров и возвращает целочисленный результат:
Code:
int rand ()
/ * из K&R
- производит случайное число от 0 до 32767. * /
{
    rand_seed = rand_seed * 1103515245 +12345;
    return (unsigned int)(rand_seed / 65536) % 32768;
}

INT рандов () строка объявляет функцию рэнд к остальной части программы и указывает, что рант не будет принимать никаких параметров и возвращать целочисленный результат. У этой функции нет локальных переменных, но если бы ей потребовались локальные переменные, они бы пошли прямо под открытием { (C позволяет вам объявлять переменные после любого { - они существуют до тех пор, пока программа не достигнет соответствия }, а затем они исчезнут. Локальные переменные функции поэтому переменные исчезают, как только в функции достигается соответствие }. Пока они существуют, локальные переменные находятся в системном стеке.) Обратите внимание, что нет ;после ()в первой строке. Если вы случайно вставите его, вы получите от компилятора огромный каскад бессмысленных сообщений об ошибках. Также обратите внимание, что даже если нет параметров, вы должны использовать (). Они сообщают компилятору, что вы объявляете функцию, а не просто объявляете int.
Оператор return важен для любой функции, возвращающей результат. Он указывает значение, которое функция вернет, и вызывает немедленный выход из функции. Это означает, что вы можете поместить в функцию несколько операторов return, чтобы дать ей несколько точек выхода. Если вы не поместите оператор return в функцию, функция вернется, когда достигнет }, и вернет случайное значение (многие компиляторы предупредят вас, если вы не вернете определенное значение). В C функция может возвращать значения любого типа: int, float, char, struct и т. д.
Есть несколько правильных способов вызвать функцию rand. Например: x = rand ();. Переменной x присваивается значение, возвращаемое rand в этом операторе. Обратите внимание, что вы должны использовать () в вызове функции, даже если параметр не передается. В противном случае x присваивается адрес памяти функции rand, что обычно не соответствует вашим намерениям.

Вы также можете вызвать rand таким образом:
Code:
если (rand ()> 100)
Или так:
Code:
rand ();
В последнем случае функция вызывается, но значение, возвращаемое rand, отбрасывается. Возможно, вы никогда не захотите делать это с помощью rand, но многие функции возвращают какой-то код ошибки через имя функции, и если вас не интересует код ошибки (например, потому что вы знаете, что ошибка невозможна), вы можете отбросить это таким образом.

Функции могут использовать возвращаемый тип void, если вы не собираетесь ничего возвращать. Например:
Code:
void print_header ()
{
printf ("Номер программы 1 \ n");
printf ("Маршалл Брейн \ n");
printf ("Версия 1.0, от 26.12.91 \ n");
}
Эта функция не возвращает значения. Вы можете вызвать это с помощью следующего утверждения:
Code:
print_header ();
Вы должны включить () в вызов. Если вы этого не сделаете, функция не будет вызвана, даже если она будет правильно компилироваться во многих системах.

Функции C могут принимать параметры любого типа. Например:
Code:
int fact(int i)
{
int j, k;

j = 1;
for (k = 2; k <= i; k ++)
j = j * k;
return j;
}
возвращает факториал i, который передается как целочисленный параметр. Разделите несколько параметров запятыми:
Code:
int add (int i, int j)
{
    return i + j;
}

C эволюционировал с годами. Иногда вы можете встретить такие функции, как add, написанные в «старом стиле», как показано ниже:
Code:
int add (i, j)
int i;
int j;
{
return i + j;
}
Важно уметь читать код, написанный в старом стиле. Нет никакой разницы в способе его выполнения; это просто другое обозначение. Вы должны использовать «новый стиль» (известный как ANSI C) с типом, объявленным как часть списка параметров, если только вы не знаете, что отправите код кому-то, у кого есть доступ только к «старому стилю» (не ANSI) компилятор.

Функции: прототипы функций​

Сейчас считается хорошим тоном использовать прототипы функций для всех функций в вашей программе. Прототип объявляет имя функции, ее параметры и тип возвращаемого значения для остальной части программы до фактического объявления функции. Чтобы понять, почему прототипы функций полезны, введите следующий код и запустите его:
Code:
#include <stdio.h>

void main()
{
    printf("%d\n",add(3));
}

int add(int i, int j)
{
    return i+j;
}
Этот код компилируется на многих компиляторах без предупреждения, хотя add ожидает два параметра, но получает только один. Это работает, потому что многие компиляторы C не проверяют соответствие параметров ни по типу, ни по количеству. Вы можете потратить огромное количество времени на отладку кода, в котором вы просто ошибочно передаете слишком много или слишком мало параметров. Приведенный выше код компилируется правильно, но дает неправильный ответ.

Чтобы решить эту проблему, C позволяет размещать прототипы функций в начале (фактически, в любом месте) программы. Если вы это сделаете, C проверяет типы и количество всех списков параметров. Попробуйте скомпилировать следующее:
Code:
#include <stdio.h>

int add (int,int); /* function prototype for add */

void main()
{
    printf("%d\n",add(3));
}

int add(int i, int j)
{
    return i+j;
}
Прототип заставляет компилятор отмечать ошибку в инструкции printf.

Разместите по одному прототипу для каждой функции в начале своей программы. Они могут сэкономить вам много времени на отладку, а также решают проблему, возникающую при компиляции функций, которые вы используете до их объявления. Например, следующий код не будет компилироваться:
Code:
#include <stdio.h>

void main()
{
    printf("%d\n",add(3));
}

float add(int i, int j)
{
    return i+j;
}
Вы можете спросить, почему он компилируется, когда add возвращает int, а не когда возвращает float? Поскольку старые компиляторы C по умолчанию возвращают значение типа int. Использование прототипа решит эту проблему. Компиляторы «старого стиля» (не ANSI) допускают создание прототипов, но список параметров для прототипа должен быть пустым. Компиляторы старого стиля не выполняют проверку ошибок в списках параметров.

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

Библиотеки​

Библиотеки очень важны в C, потому что язык C поддерживает только самые базовые функции, которые ему нужны. C даже не содержит функций ввода-вывода для чтения с клавиатуры и записи на экран. Все, что выходит за рамки основного языка, должно быть написано программистом. Полученные фрагменты кода часто помещаются в библиотеки, чтобы их можно было легко использовать повторно. Мы уже видели стандартную библиотеку ввода-вывода или stdio: стандартные библиотеки существуют для стандартного ввода-вывода, математических функций, обработки строк, управления временем и т.д. Вы можете использовать библиотеки в своих собственных программах, чтобы разбить свои программы на модули. Это упрощает их понимание, тестирование и отладку, а также позволяет повторно использовать код из других программ, которые вы пишете.
Вы можете легко создавать свои собственные библиотеки. В качестве примера мы возьмем код из предыдущей статьи этой серии и сделаем библиотеку из двух его функций. Вот код, с которого мы начнем:
Code:
#include <stdio.h>

#define MAX 10

int a[MAX];
int rand_seed=10;

int rand()
/* from K&R
   - produces a random number between 0 and 32767.*/
{
    rand_seed = rand_seed * 1103515245 +12345;
    return (unsigned int)(rand_seed / 65536) % 32768;
}

void main()
{
    int i,t,x,y;

    /* fill array */
    for (i=0; i < MAX; i++)
    {
        a[i]=rand();
        printf("%d\n",a[i]);
    }

    /* bubble sort the array */
    for (x=0; x < MAX-1; x++)
        for (y=0; y < MAX-x-1; y++)
            if (a[y] > a[y+1])
            {
                t=a[y];
                a[y]=a[y+1];
                a[y+1]=t;
            }

    /* print sorted array */
    printf("--------------------\n");
    for (i=0; i < MAX; i++)
        printf("%d\n",a[i]);
}
Этот код заполняет массив случайными числами, сортирует их с помощью пузырьковой сортировки, а затем отображает отсортированный список.
Возьмите код пузырьковой сортировки и используйте то, что вы узнали в предыдущей статье, чтобы создать из него функцию. Поскольку и массив a, и константа MAX известны во всем мире, создаваемая функция не требует параметров и не должна возвращать результат. Однако вы должны использовать локальные переменные для x, y и t.

После того, как вы проверили функцию, чтобы убедиться, что она работает, передайте количество элементов в качестве параметра вместо использования MAX:
Code:
#include <stdio.h>

#define MAX 10

int a[MAX];
int rand_seed=10;

/* from K&R
   - returns random number between 0 and 32767.*/
int rand()
{
    rand_seed = rand_seed * 1103515245 +12345;
    return (unsigned int)(rand_seed / 65536) % 32768;
}

void bubble_sort(int m)
{
    int x,y,t;
     for (x=0; x < m-1; x++)
        for (y=0; y < m-x-1; y++)
            if (a[y] > a[y+1])
            {
                t=a[y];
                a[y]=a[y+1];
                a[y+1]=t;
            }
}

void main()
{
    int i,t,x,y;
    /* fill array */
    for (i=0; i < MAX; i++)
    {
        a[i]=rand();
        printf("%d\n",a[i]);
    }
    bubble_sort(MAX);
    /* print sorted array */
    printf("--------------------\n");
    for (i=0; i < MAX; i++)
        printf("%d\n",a[i]);
}

Кроме того, можно обобщить bubble_sort функции еще пропускания в в качестве параметра:
Code:
bubble_sort(int m, int a[])
В этой строке говорится: «Примите в качестве параметра целочисленный массив a любого размера». В теле функции bubble_sort ничего менять не нужно. Чтобы вызвать bubble_sort, измените вызов на:
Code:
bubble_sort(MAX, a);
Обратите внимание, что & a не использовалось в вызове функции, хотя сортировка изменит a. Причина этого станет ясна, когда вы разберетесь с указателями.

Создание библиотеки​

Поскольку функции rand и bubble_sort в предыдущей программе полезны, вы, вероятно, захотите повторно использовать их в других программах, которые вы пишете. Вы можете поместить их в служебную библиотеку, чтобы упростить повторное использование.
Каждая библиотека состоит из двух частей: файла заголовка и собственно файла кода. Заголовочный файл, обычно обозначаемый суффиксом .h, содержит информацию о библиотеке, которую должны знать программы, использующие его. Как правило, файл заголовка содержит константы и типы, а также прототипы функций, доступных в библиотеке. Введите следующий файл заголовка и сохранить его в файл с именем util.h .
Code:
/* util.h */
extern int rand();
extern void bubble_sort(int, int []);
Эти две строки являются прототипами функций. Слово «extern» в C обозначает функции, которые будут связаны позже. Если вы используете компилятор старого стиля, удалите параметры из списка параметров bubble_sort.

Введите следующий код в файл с именем util.c.
Code:
/* util.c */
#include "util.h"

int rand_seed=10;

/* from K&R
- produces a random number between 0 and 32767.*/
int rand()
{
    rand_seed = rand_seed * 1103515245 +12345;
    return (unsigned int)(rand_seed / 65536) % 32768;
}

void bubble_sort(int m,int a[])
{
    int x,y,t;
     for (x=0; x < m-1; x++)
        for (y=0; y < m-x-1; y++)
            if (a[y] > a[y+1])
            {
                t=a[y];
                a[y]=a[y+1];
                a[y+1]=t;
            }
}

Обратите внимание, что файл включает собственный заголовочный файл (util.h) и использует кавычки вместо символов < and >, которые используются только для системных библиотек. Как видите, это выглядит как обычный код C. Обратите внимание, что переменная rand_seed, поскольку ее нет в файле заголовка, не может быть просмотрена или изменена программой, использующей эту библиотеку. Это называется сокрытием информации. Добавление слова static перед int приводит к полному скрытию.
Введите следующую основную программу в файл с именем main.c.
Code:
#include <stdio.h>
#include "util.h"

#define MAX 10

int a[MAX];

void main()
{
    int i,t,x,y;
    /* fill array */
    for (i=0; i < MAX; i++)
    {
        a[i]=rand();
        printf("%d\n",a[i]);
    }

    bubble_sort(MAX,a);

    /* print sorted array */
    printf("--------------------\n");
    for (i=0; i < MAX; i++)
        printf("%d\n",a[i]);
}
Этот код включает служебную библиотеку. Основное преимущество использования библиотеки заключается в том, что код в основной программе намного короче.

Компиляция и запуск с библиотекой​

Чтобы скомпилировать библиотеку, введите в командной строке следующее (при условии, что вы используете UNIX) (замените gcc на cc, если ваша система использует cc):
Code:
gcc -c -g util.c
-C заставляет компилятор производить объектный файл для библиотеки. Объектный файл содержит машинный код библиотеки. Он не может быть выполнен, пока не будет связан с файлом программы, содержащим основную функцию. Машинный код находится в отдельном файле с именем util.o .
Чтобы скомпилировать основную программу, введите следующее:
Code:
gcc -c -g main.c
Эта строка создает файл с именем main.o, содержащий машинный код для основной программы. Чтобы создать окончательный исполняемый файл, содержащий машинный код для всей программы, свяжите два объектных файла, набрав следующее:
Code:
gcc -o main main.o util.o
Это связывает main.o и util.o для формирования исполняемого файла с именем main. Чтобы запустить его, введите main.
Make-файлы немного упрощают работу с библиотеками. Вы узнаете о make-файлах дальше.

Make-файлы​

Набирать все строки gcc снова и снова может быть неудобно, особенно если вы вносите много изменений в код и в нем есть несколько библиотек. Средство make решает эту проблему. Вы можете использовать следующий make-файл для замены приведенной выше последовательности компиляции:
Code:
main: main.o util.o
        gcc -o main main.o util.o
main.o: main.c util.h
        gcc -c -g main.c
util.o: util.c util.h
        gcc -c -g util.c
Введите это в файл с именем makefile и введите make для сборки исполняемого файла. Обратите внимание, что вы должны ставить табуляцию перед всеми строками gcc. (Восьми пробелов будет недостаточно - это должна быть табуляция. Все остальные строки должны быть слева на одном уровне.)
Этот make-файл содержит два типа строк. Линии, расположенные заподлицо слева, являются линиями зависимости. Строки, которым предшествует табуляция, являются исполняемыми строками, которые могут содержать любую допустимую команду UNIX. В строке зависимости говорится, что какой-то файл зависит от другого набора файлов. Например, main.o: main.c util.h говорит, что файл main.o зависит от файлов main.c и util.h. Если какой- либо из этих двух файлов изменений, следующая исполняемая строка (ы) должна быть выполнена, чтобы воссоздают main.o.
Обратите внимание, что последний исполняемый файл, созданный всем make-файлом, является главным в строке 1 make-файла. Окончательный результат Makefile всегда должен идти на линии 1, который в этом Makefile говорит, что файл основной зависит от main.o и util.o. В случае любого из этих изменений выполните строку gcc -o main main.o util.o, чтобы воссоздать main.
Можно поместить несколько строк для выполнения под строкой зависимости - все они должны начинаться с табуляции. Большая программа может иметь несколько библиотек и основную программу. Makefile автоматически перекомпилирует все, что нужно перекомпилировать из-за изменений.
Если вы не работаете на машине UNIX, ваш компилятор почти наверняка имеет функциональность, эквивалентную make-файлам. Прочтите документацию к вашему компилятору, чтобы узнать, как его использовать.
Теперь вы понимаете, почему вы включали stdio.h в предыдущие программы. Это просто стандартная библиотека, которую кто-то давно создал и предоставил другим программистам, чтобы облегчить им жизнь.

Текстовые файлы​

Текстовые файлы на C просты и понятны. Все функции и типы текстовых файлов в C взяты из библиотеки stdio .
Когда вам нужен текстовый ввод-вывод в программе на C, и вам нужен только один источник для входной информации и один приемник для выходной информации, вы можете положиться на stdin (стандартный вход ) и stdout (стандартный выход). Затем вы можете использовать перенаправление ввода и вывода в командной строке для перемещения различных информационных потоков через программу. В <stdio.h> есть шесть различных команд ввода-вывода, которые можно использовать с stdin и stdout:
  • printf - выводит форматированный вывод на стандартный вывод
  • scanf - читает форматированный ввод со стандартного ввода
  • put - выводит строку в стандартный вывод
  • gets - читает строку из стандартного ввода
  • putc - выводит символ на стандартный вывод
  • getc, getchar - читает символ из стандартного ввода

Преимущество stdin и stdout в том, что они просты в использовании. Точно так же возможность перенаправления ввода-вывода очень мощная. Например, может быть, вы хотите создать программу, которая читает из стандартного ввода и подсчитывает количество символов:
Code:
#include <stdio.h>
#include <string.h>

void main()
{
    char s[1000];
    int count=0;
     while (gets(s))
        count += strlen(s);
    printf("%d\n",count);
}

Введите этот код и запустите его. Он ожидает ввода от стандартного ввода, поэтому введите несколько строк. Когда вы закончите, нажмите CTRL-D, чтобы обозначить конец файла (eof). Функция gets читает строку, пока не обнаружит eof, затем возвращает 0, так что цикл while заканчивается. Когда вы нажимаете CTRL-D, вы видите количество символов в stdout (на экране). (Используйте man gets или документацию вашего компилятора, чтобы узнать больше о функции gets.)
Теперь предположим, что вы хотите подсчитать символы в файле. Если вы скомпилировали программу в исполняемый файл с именем xxx, вы можете ввести следующее:
Code:
xxx <имя файла
Вместо приема ввода с клавиатуры будет использоваться содержимое файла с именем filename. Вы можете добиться того же результата, используя трубы:
Code:
cat <имя файла | ххх
Вы также можете перенаправить вывод в файл:
Code:
xxx <имя файла> out
Эта команда помещает количество символов, созданное программой, в текстовый файл с именем out.

Иногда вам нужно напрямую использовать текстовый файл. Например, вам может потребоваться открыть определенный файл и прочитать или записать в него. Возможно, вы захотите управлять несколькими потоками ввода или вывода или создать программу, например текстовый редактор, который может сохранять и вызывать данные или файлы конфигурации по команде. В этом случае используйте функции текстового файла в stdio:
  • fopen - открывает текстовый файл
  • fclose - закрывает текстовый файл
  • feof - определяет маркер конца файла в файле
  • fprintf - выводит форматированный вывод в файл
  • fscanf - читает форматированный ввод из файла
  • fputs - выводит строку в файл
  • fgets - читает строку из файла
  • fputc - печатает символ в файл
  • fgetc - читает символ из файла

Текстовые файлы: открытие​

Вы используете fopen, чтобы открыть файл. Он открывает файл для указанного режима (три наиболее распространенных: r, w и a для чтения, записи и добавления). Затем он возвращает указатель файла, который вы используете для доступа к файлу. Например, предположим, что вы хотите открыть файл и записать в него числа от 1 до 10. Вы можете использовать следующий код:
Code:
#include <stdio.h>
#define MAX 10

int main()
{
    FILE *f;
    int x;
    f=fopen("out","w");
    if (!f)
        return 1;
    for(x=1; x<=MAX; x++)
        fprintf(f,"%d\n",x);
    fclose(f);
    return 0;
}
Оператор fopen здесь открывает файл с именем out в режиме w. Это деструктивный режим записи, который означает, что если out не существует, он создается, но если он существует, он уничтожается, а на его месте создается новый файл. Команда fopen возвращает указатель на файл, который хранится в переменной f. Эта переменная используется для ссылки на файл. Если по какой-то причине файл не открывается, f будет содержать NULL.
Оператор fprintf должен выглядеть очень знакомо: он похож на printf, но в качестве первого параметра использует указатель файла. Оператор fclose закрывает файл, когда вы закончите.

Возвращаемые значения основной функции
Эта программа - первая программа в этой серии, которая возвращает значение ошибки из основной программы. Если команда fopen не удалась, f будет содержать значение NULL (ноль). Мы проверяем наличие этой ошибки с помощью оператора if. Оператор if проверяет значение True / False переменной f. Помните, что в C 0 - это ложь, а все остальное - истина. Поэтому, если при открытии файла произошла ошибка, f будет содержать ноль, что соответствует False. !- НЕ оператор. Он инвертирует логическое значение. Итак, оператор if можно было бы написать так:
Это эквивалентно. Однако if (!f) встречается чаще.
Если есть ошибка файла, мы возвращаем 1 из основной функции. В UNIX вы можете проверить это значение в командной строке. Подробнее см. Документацию по оболочке.

Текстовые файлы: чтение​

Чтобы прочитать файл, откройте его в режиме r. В общем, использование fscanf для чтения - не лучшая идея : если файл не отформатирован идеально, fscanf не обработает его правильно. Вместо этого используйте fgets, чтобы читать каждую строку, а затем разбирать нужные фрагменты.

Следующий код демонстрирует процесс чтения файла и вывода его содержимого на экран:
Code:
#include <stdio.h>

int main()
{
    FILE *f;
    char s[1000];

    f=fopen("infile","r");
    if (!f)
        return 1;
    while (fgets(s,1000,f)!=NULL)
        printf("%s",s);
    fclose(f);
    return 0;
}

Оператор fgets возвращает значение NULL в маркере конца файла. Он считывает строку (в данном случае до 1000 символов) и затем выводит ее на стандартный вывод. Обратите внимание, что оператор printf не включает \ n в строку формата, потому что fgets добавляет \ n в конец каждой считываемой строки. Таким образом, вы можете определить, является ли строка незавершенной, если она превышает максимальную длину строки, указанную во втором параметре, до fgets.

Ошибки C, которых следует избегать:
Не набирайте случайно close вместо fclose. Функция закрытия существует, поэтому компилятор принимает ее. Он даже будет работать, если программа откроет или закроет только несколько файлов. Однако, если программа открывает и закрывает файл в цикле, в конечном итоге у нее заканчиваются доступные дескрипторы файлов и / или пространство памяти, и происходит сбой, потому что закрытие не закрывает файлы правильно.

Указатели​

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

Указатели: Почему?​

Представьте, что вы хотите создать текстовый редактор - программу, которая позволяет вам редактировать обычные текстовые файлы ASCII, такие как «vi» в UNIX или «Блокнот» в Windows. Текстовый редактор - довольно обычная вещь для кого-то, потому что, если подумать, текстовый редактор, вероятно, является наиболее часто используемым программным обеспечением программиста. Текстовый редактор - это интимная связь программиста с компьютером: здесь вы вводите все свои мысли, а затем манипулируете ими. Очевидно, что со всем, что вы часто используете и с чем тесно работаете, вы хотите, чтобы все было правильно. Поэтому многие программисты создают свои собственные редакторы и настраивают их в соответствии со своим индивидуальным стилем работы и предпочтениями.
Итак, однажды вы садитесь за работу над своим редактором. Подумав о желаемых функциях, вы начинаете думать о «структуре данных» для вашего редактора. То есть вы начинаете думать о том, как сохранить редактируемый документ в памяти, чтобы вы могли управлять им в своей программе. Что вам нужно, так это способ сохранить вводимую вами информацию в форме, которой можно будет быстро и легко управлять. Вы считаете, что один из способов сделать это - организовать данные на основе строк символов. Учитывая то, что мы обсуждали до сих пор, единственное, что у вас есть на данный момент, - это массив. Вы думаете: «Типичная строка составляет 80 символов, а типичный файл - не более 1000 строк». Поэтому вы объявляете двумерный массив, например:
Code:
char doc [1000] [80];
Это объявление запрашивает массив из 1000 строк по 80 символов. Этот массив имеет общий размер 80 000 символов.

Однако, подумав о своем редакторе и его структуре данных, вы можете понять три вещи:
  • Некоторые документы представляют собой длинные списки. Каждая строка короткая, но строк тысячи.
  • Некоторые текстовые файлы специального назначения содержат очень длинные строки. Например, в определенном файле данных могут быть строки, содержащие 542 символа, каждый из которых представляет пары аминокислот в сегментах ДНК.
  • В большинстве современных редакторов можно одновременно открывать несколько файлов.
Допустим, вы установили максимум 10 открытых файлов одновременно, максимальную длину строки 1000 символов и максимальный размер файла 50 000 строк. Ваша декларация теперь выглядит так:
Code:
char doc [50000] [1000] [10];
Это не кажется необоснованным, пока вы не достанете калькулятор, не умножите 50 000 на 1000 на 10 и не поймете, что массив содержит 500 миллионов символов! У большинства компьютеров сегодня будут проблемы с массивом такого размера. У них просто нет ОЗУ или даже пространства виртуальной памяти, чтобы поддерживать массив такого размера. Если бы пользователи попытались запустить три или четыре копии этой программы одновременно даже в самой крупной многопользовательской системе, это сильно повлияло бы на оборудование.
Даже если компьютер примет запрос на такой большой массив, вы увидите, что это непомерная трата места. Кажется странным объявлять массив из 500 миллионов символов, когда в подавляющем большинстве случаев вы запускаете этот редактор для просмотра 100 строчных файлов, которые занимают не более 4000 или 5000 байтов. Проблема с массивом заключается в том, что вам нужно с самого начала объявить его максимальный размер в каждом измерении.Эти максимальные размеры часто перемножаются и образуют очень большие числа. Кроме того, если вам нужно иметь возможность редактировать нечетный файл со строкой в 2000 символов, вам не повезло. На самом деле у вас нет способа предсказать и обработать максимальную длину строки текстового файла, потому что технически это число бесконечно.
Указатели предназначены для решения этой проблемы. С помощью указателей вы можете создавать динамические структуры данных. Вместо того, чтобы заранее объявлять в массиве о потреблении памяти в наихудшем случае, вы выделяете память из кучиво время работы программы. Таким образом, вы можете без потерь использовать тот объем памяти, который требуется документу. Кроме того, когда вы закрываете документ, вы можете вернуть память в кучу, чтобы другие части программы могли ее использовать. С помощью указателей память можно повторно использовать во время работы программы.
Кстати, если вы прочитали предыдущее обсуждение и у вас возник один из главных вопросов: «Что такое байт на самом деле?», То статья «Как работают биты и байты» поможет вам понять концепции, а также такие вещи, как «мега», «гига» и «тера». Пойдите, посмотрите, а затем вернитесь.

Основы указателя​

c-pointer1.gif

На этом чертеже были объявлены три переменные i, j и p, но ни одна из трех не была инициализирована.

Чтобы понять указатели, полезно сравнить их с обычными переменными.
«Обычная переменная» - это место в памяти, которое может содержать значение. Например, когда вы объявляете переменную i как целое число, для нее выделяются четыре байта памяти. В вашей программе вы обращаетесь к этому месту в памяти по имени i. На уровне машины это место имеет адрес памяти. Четыре байта по этому адресу известны вам, программисту, как i, а четыре байта могут содержать одно целое значение.
Указатель другой. Указатель - это переменная, которая указывает на другую переменную. Это означает, что указатель содержит адрес в памяти другой переменной. Другими словами, указатель не содержит значения в традиционном смысле; вместо этого он содержит адрес другой переменной. Указатель «указывает на» эту другую переменную, удерживая копию ее адреса.
Поскольку указатель содержит адрес, а не значение, он состоит из двух частей. Сам указатель содержит адрес. Этот адрес указывает на значение. Есть указатель и указанное значение. Этот факт может немного сбивать с толку, пока вы не освоитесь с ним, но как только вы освоитесь, он станет чрезвычайно мощным.

В следующем примере кода показан типичный указатель:
Code:
#include <stdio.h>

int main()
{
    int i,j;
    int *p;  / * указатель на целое число * /
  p = &i;
    *p=5;
    j=i;
    printf("%d %d %d\n", i, j, *p);
    return 0;
}

Первое объявление в этой программе объявляет две обычные целочисленные переменные с именами i и j. Строка int * p объявляет указатель с именем p. Эта строка просит компилятор объявить переменную p, которая является указателем на целое число. * Указывает на то, что указатель был объявлен вместо обычной переменной. Вы можете создать указатель на что угодно: на объект с плавающей запятой, структуру, символ и т. Д. Просто используйте *, чтобы указать, что вам нужен указатель, а не обычная переменная.
Строка p = & i;определенно будет для вас новым. В языке C & называется адресным оператором. Выражение & i означает «адрес памяти переменной i». Таким образом, выражение p = & i;означает: «Назначить p адрес i ». После выполнения этого оператора p "указывает на" i. Прежде чем вы это сделаете, p содержит случайный неизвестный адрес, и его использование, вероятно, вызовет ошибку сегментации или аналогичный сбой программы.
Один из хороших способов визуализировать происходящее - нарисовать картинку. После объявления i, j и p мир будет выглядеть как на изображении выше.
На этом чертеже были объявлены три переменные i, j и p, но ни одна из трех не была инициализирована. Таким образом, две целочисленные переменные изображены в виде прямоугольников с вопросительными знаками - они могут содержать любое значение на данном этапе выполнения программы. Указатель нарисован в виде круга, чтобы отличить его от обычной переменной, содержащей значение, а случайные стрелки указывают, что в данный момент он может указывать куда угодно.
После строки p = & I;, p инициализируется и указывает на i, например:
c-pointer2.gif


Как только p указывает на i, у ячейки памяти i есть два имени. Он по-прежнему известен как i, но теперь он также известен как * p. Вот как C говорит о двух частях переменной-указателя: p - это местоположение, содержащее адрес, а * p - это местоположение, на которое указывает этот адрес. Следовательно, * p = 5 означает, что местоположение, на которое указывает p, должно быть установлено на 5, например:
c-pointer3.gif

Поскольку местоположение * p также является i, i также принимает значение 5. Следовательно, j = i;устанавливает j в 5, а оператор printf возвращает 5 5 5.
Главная особенность указателя - его двухчастный характер. Сам указатель содержит адрес. Указатель также указывает на значение определенного типа - значение по адресу, который содержит точка. Сам указатель в данном случае - p. Указанное значение - * p.

Указатели: общие сведения об адресах памяти​

c-pointer4a.gif

Переменная f занимает четыре байта ОЗУ в памяти. У этого места есть конкретный адрес, в данном случае 248 440.

Предыдущее обсуждение станет немного яснее, если вы поймете, как адреса памяти работают в аппаратном обеспечении компьютера. Если вы еще не читали его, сейчас самое время прочитать, как работают биты и байты, чтобы полностью понять биты, байты и слова.
Все компьютеры имеют память, также известную как RAM (оперативная память). Например, на вашем компьютере прямо сейчас может быть установлено 16, 32 или 64 мегабайта оперативной памяти. ОЗУ содержит программы, которые в настоящее время выполняет ваш компьютер, а также данные, которыми они в данный момент управляют (их переменные и структуры данных). Память можно рассматривать просто как массив байтов. В этом массиве каждая ячейка памяти имеет свой собственный адрес - адрес первого байта равен 0, за ним идут 1, 2, 3 и так далее. Адреса памяти действуют так же, как индексы обычного массива. Компьютер может получить доступ к любому адресу в памяти в любое время (отсюда и название «оперативная память»). Он также может группировать байты вместе, если это необходимо для формирования более крупных переменных, массивов и структур. Например, переменная с плавающей запятой занимает в памяти 4 непрерывных байта. Вы можете сделать в программе следующее глобальное объявление:
float f;
Этот оператор говорит: «Объявите место с именем f, которое может содержать одно значение с плавающей запятой». Когда программа запускается, компьютер резервирует место для переменной f где-нибудь в памяти. Это место имеет фиксированный адрес в памяти, например:
Когда вы думаете о переменной f, компьютер думает о конкретном адресе в памяти (например, 248 440). Поэтому, когда вы создаете такой оператор:
Code:
f = 3,14;
Компилятор может преобразовать это в «Загрузить значение 3,14 в ячейку памяти 248 440». Компьютер всегда думает о памяти с точки зрения адресов и значений по этим адресам.

Между прочим, существует несколько интересных побочных эффектов того, как ваш компьютер обрабатывает память. Например, предположим, что вы включаете следующий код в одну из своих программ:
Code:
int i, s[4], t[4], u=0;

for (i=0; i<=4; i++)
{
    s[i] = i;
    t[i] =i;
}
printf("s:t\n");
for (i=0; i<=4; i++)
    printf("%d:%d\n", s[i], t[i]);
printf("u = %d\n", u);

Результат, который вы увидите в программе, вероятно, будет выглядеть так:
Code:
s: t
1: 5
2: 2
3: 3
4: 4
5: 5
u = 5
Почему t [0] и u неверны? Если вы внимательно посмотрите на код, вы увидите, что циклы for записывают один элемент за концом каждого массива. В памяти массивы располагаются рядом друг с другом, как показано здесь:

c-pointer5.gif

Массивы, расположенные рядом друг с другом.

Поэтому, когда вы пытаетесь записать в s [4], которого не существует, система вместо этого записывает в t [0], потому что t [0] - это то место, где должно быть s [4]. Когда вы пишете в t [4], вы действительно пишете в u. Что касается компьютера, s [4] - это просто адрес, и он может писать в него. Однако, как вы можете видеть, даже если компьютер выполняет программу, она неверна или неверна. Программа портит массив t в процессе работы. Если вы выполните следующую инструкцию, это приведет к более серьезным последствиям:
Code:
s [1000000] = 5;
Расположение s [1000000], скорее всего, находится вне области памяти вашей программы. Другими словами, вы записываете в память, которой ваша программа не владеет. В системе с защищенным пространством памяти (UNIX, Windows 98 / NT) этот вид операторов заставит систему прекратить выполнение программы. Однако в других системах (Windows 3.1, Mac) система не знает, что вы делаете. В конечном итоге вы повредите код или переменные в другом приложении. Эффект от нарушения может варьироваться от нулевого до полного отказа системы. В памяти i, s, t и u располагаются рядом друг с другом по определенным адресам. Следовательно, если вы пишете за пределами переменной, компьютер будет делать то, что вы говорите, но в конечном итоге приведет к повреждению другой области памяти.
Поскольку C и C ++ не выполняют никакой проверки диапазона при доступе к элементу массива, важно, чтобы вы, как программист, сами уделяли особое внимание диапазонам массивов и не выходили за соответствующие границы массива. Непреднамеренное чтение или запись за пределами массива всегда приводит к неправильному поведению программы.

В качестве другого примера попробуйте следующее:
Code:
#include <stdio.h>

int main ()
{
int i, j;
int * p; / * указатель на целое число * /
printf ("% d% d \ n", p, & i);
р = & i;
printf ("% d% d \ n", p, & i);
return 0;
}

Этот код сообщает компилятор, чтобы распечатать адреса провели в р, вместе с адресом в I.Переменная p начинается с какого-то сумасшедшего значения или с 0. Адрес i обычно имеет большое значение. Например, когда я запустил этот код, я получил следующий результат:
Code:
0 2147478276
2147478276 2147478276
что означает, что адрес i - 2147478276. После того, как оператор p = & i;был выполнен, p содержит адрес i. Попробуйте и это:
Code:
#include <stdio.h>

void main()
{
int * p; / * указатель на целое число * /

printf ("% d \ n", * p);
}
Этот код сообщает компилятору напечатать значение, на которое указывает p. Однако p еще не инициализирован; он содержит адрес 0 или какой-то случайный адрес. В большинстве случаев возникает ошибка сегментации (или какая-либо другая ошибка времени выполнения), что означает, что вы использовали указатель, указывающий на недопустимую область памяти. Почти всегда неинициализированный указатель или неправильный адрес указателя являются причиной ошибок сегментации.

Сказав все это, теперь мы можем взглянуть на указатели в совершенно новом свете. Возьмем, к примеру, эту программу:
Code:
#include <stdio.h>

int main ()
{
int i;
int * p; / * указатель на целое число * /
р = & i;
* р = 5;
printf ("% d% d \ n", i, * p);
return 0;
}

Вот что происходит:
c-pointer6.gif

Переменная i занимает 4 байта памяти. Указатель p также занимает 4 байта (на большинстве машин, используемых сегодня, указатель потребляет 4 байта памяти. На большинстве процессоров сегодня адреса памяти имеют длину 32 бита, хотя наблюдается возрастающая тенденция к 64-битной адресации). Местоположение i имеет конкретный адрес, в данном случае 248 440. Указатель p содержит этот адрес, когда вы говорите p = & i;. Следовательно, переменные * p и i эквивалентны.
Указатель p буквально содержит адрес i. Когда вы говорите что-то подобное в программе:
printf ("% d", p);
в результате получается фактический адрес переменной i.

Указатели: указание на один и тот же адрес​

c-pointer7.gif

Вот отличный аспект C: любое количество указателей может указывать на один и тот же адрес. Например, вы можете объявить p, q и r как целочисленные указатели и установить их все так, чтобы они указывали на i, как показано здесь:
Code:
int i;
int *p, *q, *r;

p = &i;
q = &i;
r = p;
Обратите внимание, что в этом коде r указывает на то же, на что указывает p, то есть на i. Вы можете назначать указатели друг на друга, и адрес копируется с правой стороны на левую во время назначения. После выполнения приведенного выше кода все будет выглядеть так:
Переменная i теперь имеет четыре имени: i, * p, * q и * r. Не существует ограничений на количество указателей, которые могут содержать (и, следовательно, указывать) один и тот же адрес.

Указатели: распространенные ошибки​

Ошибка №1 - неинициализированные указатели
Один из самых простых способов создать ошибку указателя - попытаться ссылаться на значение указателя, даже если указатель не инициализирован и еще не указывает на действительный адрес. Например:
Code:
int * p;

* р = 12;

Указатель p не инициализирован и указывает на случайное место в памяти, когда вы его объявляете. Это может быть указание на системный стек или глобальные переменные, или на пространство кода программы, или на операционную систему. Когда вы говорите * p = 12;, программа просто попытается записать 12 в любое случайное место, на которое указывает p. Программа может взорваться немедленно, или может подождать полчаса, а затем взорваться, или она может незаметно повредить данные в другой части вашей программы, и вы, возможно, никогда этого не осознаете. Это может затруднить отслеживание этой ошибки. Убедитесь, что вы инициализировали все указатели на действительный адрес, прежде чем разыменовать их.

Ошибка № 2 - Недействительные ссылки на указатели
Недопустимая ссылка указателя возникает, когда указывается значение указателя, даже если указатель не указывает на действительный блок.
Один из способов создать эту ошибку - сказать p = q;, когда q неинициализировано. Указатель p также станет неинициализированным, и любая ссылка на * p будет недействительной ссылкой на указатель.
Единственный способ избежать этой ошибки - нарисовать изображения каждого шага программы и убедиться, что все указатели куда-то указывают. Недопустимые ссылки на указатели вызывают необъяснимый сбой программы по тем же причинам, что и в Ошибке №1.

Ошибка № 3 - Ссылка на нулевой указатель
Ссылка на нулевой указатель возникает всякий раз, когда указатель, указывающий на ноль, используется в инструкции, которая пытается ссылаться на блок. Например, если p - указатель на целое число, следующий код недействителен:
Code:
р = 0;
* р = 12;
Блок, на который указывает p, отсутствует. Следовательно, попытка чтения или записи чего-либо из этого блока или в этот блок является недопустимой ссылкой на нулевой указатель. Как мы увидим в следующих статьях, есть веские веские причины указать указатель на ноль. Однако разыменование такого указателя недопустимо.
Все эти ошибки фатальны для программы, которая их содержит. Вы должны следить за своим кодом, чтобы этих ошибок не произошло. Лучший способ сделать это - поэтапно рисовать картинки выполнения кода.

Использование указателей для параметров функций​

c-pointer-swap.gif

Большинство программистов на C сначала используют указатели для реализации в функциях того, что называется параметрами переменных. Фактически вы использовали параметры переменных в функции scanf - вот почему вам пришлось использовать & (оператор адреса) для переменных, используемых в функции scanf. Теперь, когда вы разбираетесь в указателях, вы можете видеть, что на самом деле происходит.
Чтобы понять, как работают переменные параметры, давайте посмотрим, как мы могли бы реализовать функцию подкачки в C. Чтобы реализовать функцию подкачки, вам нужно передать две переменные, а функция поменять местами их значения. Вот одна попытка реализации - введите и выполните следующий код и посмотрите, что произойдет:
Code:
#include <stdio.h>

void swap(int i, int j)
{
    int t;

    t=i;
    i=j;
    j=t;
}

void main()
{
    int a,b;

    a=5;
    b=10;
    printf("%d %d\n", a, b);
    swap(a,b);
    printf("%d %d\n", a, b);
}
Когда вы запустите эту программу, вы обнаружите, что подкачки не происходит. Значения a и b передаются в обмен, и функция обмена меняет их местами, но когда функция возвращает, ничего не происходит.

Чтобы эта функция работала правильно, вы можете использовать указатели, как показано ниже:
Code:
#include <stdio.h>

void swap(int *i, int *j)
{
    int t;
    t = *i;
    *i = *j;
    *j = t;
}

void main()
{
    int a,b;
    a=5;
    b=10;
    printf("%d %d\n",a,b);
    swap(&a,&b);
    printf("%d %d\n",a,b);
}
Чтобы понять, что делает этот код, распечатайте его, нарисуйте два целых числа a и b и введите в них 5 и 10. Теперь нарисуйте два указателя i и j вместе с целым числом t. Когда своп называется, он передается в адреса из а и б. Таким образом, i указывает на a (нарисуйте стрелку от i до a), а j указывает на b (нарисуйте еще одну стрелку от b до j). После инициализации указателей вызовом функции * i- другое имя для a, а * j - другое имя для b. Теперь запустите код в свопе. Когда в коде используются * i и * j, это действительно означает a и b. Когда функция завершится, a и b поменялись местами.
Предположим, вы случайно забыли & при вызове функции подкачки, и что строка подкачки случайно выглядит так: swap (a, b);. Это вызывает ошибку сегментации. Когда вы выходите из &, то значение из передаются вместо его адреса. Следовательно, i указывает на недопустимое место в памяти, и при использовании * i происходит сбой системы.
По этой же причине происходит сбой scanf, если вы забыли переданные ему переменные & on. Функция scanf использует указатели, чтобы поместить считываемое значение обратно в переданную вами переменную. Без &, scanf передается неверный адрес и вылетает.
Переменные параметры - одно из наиболее распространенных применений указателей в C. Теперь вы понимаете, что происходит!

Динамические структуры данных​

Динамические структуры данных - это структуры данных, которые увеличиваются и уменьшаются по мере необходимости за счет выделения и освобождения памяти из места, называемого кучей. Они чрезвычайно важны в C, потому что позволяют программисту точно контролировать потребление памяти.
Динамические структуры данных выделяют блоки памяти из кучи по мере необходимости и связывают эти блоки вместе в некую структуру данных с помощью указателей. Когда структуре данных больше не нужен блок памяти, она вернет блок в кучу для повторного использования. Эта переработка позволяет очень эффективно использовать память.
Чтобы полностью понять динамические структуры данных, нам нужно начать с кучи.

Динамические структуры данных: куча​

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


Операционная система и несколько приложений, а также их глобальные переменные и пространства стека потребляют часть памяти. Когда программа завершает выполнение, она освобождает свою память для повторного использования другими программами. Обратите внимание, что часть пространства памяти остается неиспользованной в любой момент времени.
На типичном персональном компьютере или рабочей станции сегодня установлено от 16 до 64 мегабайт оперативной памяти. Использование техники, называемой виртуальной памятью, система может менять местами память на жестком диске машины и за ее пределами, чтобы создать для ЦП иллюзию, что у него гораздо больше памяти, например от 200 до 500 мегабайт. Хотя эта иллюзия полна в том, что касается ЦП, иногда с точки зрения пользователя она может сильно замедлить работу. Несмотря на этот недостаток, виртуальная память - чрезвычайно полезный метод недорогого «увеличения» объема ОЗУ в машине. Давайте предположим для этого обсуждения, что типичный компьютер имеет общий объем памяти, например, 50 мегабайт (независимо от того, реализована ли эта память в реальной или виртуальной памяти).
Операционная система на машине отвечает за пространство памяти 50 мегабайта. Операционная система использует пространство несколькими способами, как показано здесь.
Это, конечно, идеализация, но основные принципы верны. Как видите, в памяти хранится исполняемый код для различных приложений, запущенных в данный момент на машине, а также исполняемый код самой операционной системы. С каждым приложением связаны определенные глобальные переменные. Эти переменные также потребляют память. Наконец, каждое приложение использует область памяти, называемую стеком., который содержит все локальные переменные и параметры, используемые любой функцией. Стек также запоминает порядок, в котором вызываются функции, чтобы возврат функций происходил правильно. Каждый раз при вызове функции ее локальные переменные и параметры «помещаются» в стек. Когда функция возвращается, эти локальные переменные и параметры «выталкиваются». Из-за этого размер стека программы постоянно колеблется во время выполнения программы, но имеет некоторый максимальный размер.
Когда программа завершает выполнение, операционная система выгружает ее, свои глобальные переменные и пространство стека из памяти. Новая программа может использовать это пространство позже. Таким образом, память компьютерной системы постоянно «перерабатывается» и повторно используется программами по мере их выполнения и завершения.
В общем, возможно, 50 процентов общего объема памяти компьютера могут быть неиспользованными в любой момент. Операционная система владеет неиспользуемой памятью и управляет ею, и все вместе это называется кучей. Куча чрезвычайно важна, потому что она доступна для использования приложениями во время выполнения с использованием функций C malloc (выделение памяти) и free. Куча позволяет программам выделять память именно тогда, когда она им нужна во время выполнения программы, а не предварительно выделять ее с помощью объявления массива определенного размера.

Динамические структуры данных: Malloc и Free​

Блок справа - это блок памяти, выделенный malloc.


Блок справа - это блок памяти, выделенный malloc.
Допустим, вы хотите выделить определенный объем памяти во время выполнения вашего приложения. Вы можете вызвать функцию malloc в любое время, и она запросит блок памяти из кучи. Операционная система зарезервирует блок памяти для вашей программы, и вы можете использовать его как угодно. Когда вы закончите с блоком, вы возвращаете его в операционную систему для повторного использования, вызывая функцию free. Затем другие приложения могут зарезервировать его позже для собственного использования.
Например, следующий код демонстрирует простейшее возможное использование кучи:
Code:
int main()
{
    int *p;

    p = (int *)malloc(sizeof(int));
    if (p == 0)
    {
        printf("ERROR: Out of memory\n");
        return 1;
    }
    *p = 5;
    printf("%d\n", *p);
    free(p);
    return 0;
}

Первая строка в этой программе вызывает функцию malloc. Эта функция выполняет три функции:
  1. Оператор malloc сначала проверяет объем доступной памяти в куче и спрашивает: «Достаточно ли памяти для выделения блока памяти запрошенного размера?» Объем памяти, необходимый для блока, известен из параметра, переданного в malloc - в этом случае sizeof (int) составляет 4 байта. Если доступной памяти недостаточно, функция malloc возвращает нулевой адрес, чтобы указать на ошибку (другое имя для нуля - NULL, и вы увидите, что оно используется во всем коде C). В противном случае malloc продолжается.
  2. Если память доступна в куче, система «выделяет» или «резервирует» блок из кучи указанного размера. Система резервирует блок памяти, чтобы он случайно не использовался более чем одним оператором malloc.
  3. Затем система помещает в указательную переменную (в данном случае p) адрес зарезервированного блока. Сама указательная переменная содержит адрес. Выделенный блок может содержать значение указанного типа, и указатель указывает на него.
Затем программа проверяет указатель p, чтобы убедиться, что запрос на выделение успешно выполнен со строкой if (p == 0) (которая также могла быть записана как if (p == NULL) или даже if (! P). сбой выделения (если p равно нулю), программа завершается. Если выделение выполнено успешно, программа затем инициализирует блок значением 5, распечатывает это значение и вызывает бесплатную функцию, чтобы вернуть память в кучу перед программой прекращается.

На самом деле нет никакой разницы между этим кодом и предыдущим кодом, который устанавливает p равным адресу существующего целого числа i. Единственное отличие состоит в том, что в случае переменной i память существовала как часть предварительно выделенного пространства памяти программы и имела два имени: i и * p. В случае выделения памяти из кучи, блок имеет единственное имя * p и выделяется во время выполнения программы. Два общих вопроса:
  • Действительно ли важно проверять, что указатель равен нулю после каждого выделения?Да. Поскольку размер кучи постоянно меняется в зависимости от того, какие программы работают, сколько памяти они выделили и т. Д., Нет никакой гарантии, что вызов malloc будет успешным. Вы должны проверять указатель после любого вызова malloc, чтобы убедиться, что указатель действителен.
  • Что произойдет, если я забуду удалить блок памяти до завершения программы?Когда программа завершается, операционная система «очищается после нее», освобождая пространство исполняемого кода, стек, пространство глобальной памяти и все выделения кучи для повторного использования. Таким образом, оставление отложенных распределений при завершении программы не имеет долгосрочных последствий. Однако это считается дурным тоном, и «утечки памяти» во время выполнения программы вредны, как обсуждается ниже.
Следующие две программы показывают два разных допустимого использования указателей и пытаются различить использование указателя и значение указателя:
Code:
пустая функция()
{
int * p, * q;

p = (int *) malloc (sizeof (int));
q = p;
* р = 10;
printf ("% d \ n", * q);
* q = 20;
printf ("% d \ n", * q);
}
Окончательный вывод этого кода будет 10 из строки 4 и 20 из строки 6.

Следующий код немного отличается:
Code:
пустая функция()
{
int * p, * q;

p = (int *) malloc (sizeof (int));
q = (int *) malloc (sizeof (int));
* р = 10;
* q = 20;
* p = * q;
printf ("% d \ n", * p);
}

[IMG"]https://cdn.hswstatic.com/gif/c-heap2.gif[/IMG]
Окончательный вывод этого кода будет 10 из строки 4 и 20 из строки 6.

c-heap3.gif

Окончательный вывод этого кода будет 20 из строки 6.

Обратите внимание, что компилятор разрешит * p = * q, потому что * p и * q являются целыми числами. Этот оператор говорит: «Переместите целое значение, на которое указывает q, в целое значение, на которое указывает p». Заявление перемещает значения. Компилятор также разрешит p = q, потому что p и q являются указателями, и оба указывают на один и тот же тип (если s - указатель на символ, p = s не допускается, потому что они указывают на разные типы). Утверждение p = q говорит: «Укажите p на тот же блок, на который указывает q». Другими словами, адрес, на который указывает q, перемещается в p, поэтому они оба указывают на один и тот же блок. Этот оператор перемещает адреса.
Из всех этих примеров вы можете видеть, что существует четыре различных способа инициализировать указатель. Когда указатель объявлен, как в int * p, он запускается в программе в неинициализированном состоянии. Он может указывать куда угодно, и поэтому разыменование является ошибкой. Инициализация переменной-указателя включает указание на известное место в памяти.
  1. Один из способов, как уже было показано, - использовать оператор malloc. Этот оператор выделяет блок памяти из кучи, а затем указывает указатель на блок. Это инициализирует указатель, потому что теперь он указывает на известное местоположение. Указатель инициализируется, потому что он был заполнен действительным адресом - адресом нового блока.
  2. Второй способ, который мы видели только что, - использовать такой оператор, как p = q, чтобы p указывал на то же место, что и q. Если q указывает на допустимый блок, то инициализируется p. В указатель p загружается действительный адрес, который содержит q. Однако, если q неинициализирован или недействителен, p получит тот же бесполезный адрес.
  3. Третий способ - указать указатель на известный адрес, например, на адрес глобальной переменной. Например, если i - целое число, а p - указатель на целое число, то оператор p = & i инициализирует p, указывая его на i.
  4. Четвертый способ инициализировать указатель - использовать нулевое значение. Ноль - это специальные значения, используемые с указателями, как показано здесь: p = 0; или: p = NULL; Это физически помещает ноль в p. Адрес указателя p равен нулю. Обычно это изображается как:

Любой указатель может быть установлен на ноль. Однако, когда p указывает на ноль, это не указывает на блок. Указатель просто содержит нулевой адрес, и это значение полезно как тег. Вы можете использовать его в таких заявлениях, как:
Code:
if (p == 0){...}
или же:
Code:
while (p! = 0) {...}

Система также распознает нулевое значение и будет генерировать сообщения об ошибках, если вы случайно разыменовываете нулевой указатель. Например, в следующем коде:
Code:
р = 0; * р = 5;
Программа обычно вылетает. Указатель p не указывает на блок, он указывает на ноль, поэтому значение не может быть присвоено * p. Нулевой указатель будет использоваться как флаг, когда мы дойдем до связанных списков.

Команда malloc используется для выделения блока памяти. Также возможно освободить блок памяти, когда он больше не нужен. Когда блок освобождается, его можно повторно использовать с помощью следующей команды malloc, которая позволяет системе повторно использовать память. Команда, используемая для освобождения памяти, называется свободной и принимает в качестве параметра указатель. Бесплатная команда делает две вещи:
  1. Блок памяти, на который указывает указатель, не зарезервирован и возвращается в свободную память в куче. Затем его можно будет повторно использовать в более поздних новых операторах.
  2. Указатель остается в неинициализированном состоянии, и его необходимо повторно инициализировать, прежде чем его можно будет использовать снова.
Оператор free просто возвращает указатель на исходное неинициализированное состояние и снова делает блок доступным в куче.

В следующем примере показано, как использовать кучу. Он выделяет целочисленный блок, заполняет его, записывает и удаляет:
Code:
#include <stdio.h>

int main()
{
    int *p;
    p = (int *)malloc (sizeof(int));
    *p=10;
    printf("%d\n",*p);
    free(p);
    return 0;
}
Этот код действительно полезен только для демонстрации процесса выделения, освобождения и использования блока в C. Строка malloc выделяет блок памяти указанного размера - в данном случае sizeof (int) bytes (4 байта). Команда sizeof в C возвращает размер в байтах любого типа. Код мог бы так же легко сказать malloc (4), поскольку sizeof (int) на большинстве машин равен 4 байтам. Однако использование sizeof делает код более переносимым и читаемым.
Функция malloc возвращает указатель на выделенный блок. Этот указатель является универсальным. Использование указателя без преобразования типов обычно вызывает предупреждение о типе от компилятора. Приведение типа (int *) преобразует универсальный указатель, возвращаемый malloc, в «указатель на целое число», чего и ожидает p. Оператор free в C возвращает блок в кучу для повторного использования.

Второй пример иллюстрирует те же функции, что и предыдущий, но использует структуру вместо целого числа. В C код выглядит так:
Code:
#include <stdio.h>

struct rec
{
    int i;
    float f;
    char c;
};

int main()
{
    struct rec *p;
    p=(struct rec *) malloc (sizeof(struct rec));
    (*p).i=10;
    (*p).f=3.14;
    (*p).c='a';
    printf("%d %f %c\n",(*p).i,(*p).f,(*p).c);
    free(p);
    return 0;
}

Обратите внимание на следующую строку:
Code:
(* p) .i = 10;
Многие задаются вопросом, почему не работает следующее:
Code:
*p.i=10;
Ответ связан с приоритетом операторов в C. Результатом вычисления 5 + 3 * 4 будет 17, а не 32, потому что оператор * имеет более высокий приоритет, чем + в большинстве компьютерных языков. В языке C расширение .Оператор имеет более высокий приоритет, чем *, поэтому круглые скобки обеспечивают правильный приоритет.
Большинство людей устают постоянно набирать (* p) .i, поэтому C предоставляет сокращенную запись. Следующие два оператора полностью эквивалентны, но второй легче набрать:
Code:
(* p) .i = 10;
p-> i = 10;
Второе вы будете видеть чаще, чем первое, читая чужой код.

Расширенные указатели​

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

Типы указателей​

Возможно, законно и полезно создавать типы указателей в C, как показано ниже:
Code:
typedef int * IntPointer;
...
IntPointer p;

Это то же самое, что сказать:
Code:
int * p;
Этот метод будет использоваться во многих примерах на следующих страницах. Этот метод часто упрощает чтение и понимание данных, а также упрощает включение указателей внутри структур или передачу параметров указателя в функции.

Указатели на структуры​

В C можно создать указатель практически на любой тип, включая типы, определяемые пользователем. Чрезвычайно распространено создание указателей на структуры. Пример показан ниже:
c-dds1.gif

Code:
typedef struct
{
    char name[21];
    char city[21];
    char state[3];
} Rec;
typedef Rec *RecPointer;

RecPointer r;
r = (RecPointer)malloc(sizeof(Rec));

Указатель r - это указатель на структуру. Обратите внимание на тот факт, что r является указателем и, следовательно, занимает четыре байта памяти, как и любой другой указатель. Однако оператор malloc выделяет 45 байт памяти из кучи. * r - это такая же структура, как и любая другая структура типа Rec. В следующем коде показано типичное использование переменной-указателя:
Code:
strcpy((*r).name, "Leigh");
strcpy((*r).city, "Raleigh");
strcpy((*r).state, "NC");
printf("%s\n", (*r).city);
free(r);
Вы работаете с * r так же, как с обычной структурной переменной, но вы должны быть осторожны с приоритетом операторов в C. Если вы опустите круглые скобки вокруг * r, код не будет компилироваться из-за символа "." Оператор имеет более высокий приоритет, чем оператор «*». Поскольку при работе с указателями на структуры набирать так много скобок утомительно, C включает сокращенную нотацию, которая делает то же самое:
Code:
strcpy(r->name, "Leigh");
R-> обозначения в точности эквивалентно (* г)., но занимает на два символа меньше.
c-dds2.gif


Указатели на массивы​

Также возможно создавать указатели на массивы, как показано ниже:
Code:
int *p;
int i;

p = (int *)malloc(sizeof(int[10]));
for (i=0; i<10; i++)
p[i] = 0;
free(p);
or:

    int *p;
int i;

p = (int *)malloc(sizeof(int[10]));
for (i=0; i<10; i++)
*(p+i) = 0;
free(p);

c-dds3.gif

Обратите внимание, что когда вы создаете указатель на массив целых чисел, вы просто создаете обычный указатель на int. Вызов malloc выделяет массив любого желаемого размера, а указатель указывает на первый элемент этого массива. Вы можете либо проиндексировать массив, на который указывает p, используя обычную индексацию массива, либо вы можете сделать это, используя арифметику указателей. C рассматривает обе формы как эквивалентные.
Этот конкретный метод чрезвычайно полезен при работе со строками. Он позволяет выделить достаточно памяти, чтобы точно хранить строку определенного размера.

Массивы указателей​

Иногда можно сэкономить много места или решить определенные проблемы с интенсивным использованием памяти, объявив массив указателей. В приведенном ниже примере кода объявляется массив из 10 указателей на структуры вместо объявления массива структур. Если бы вместо этого был создан массив структур, для этого массива потребовалось бы 243 * 10 = 2430 байт. Использование массива указателей позволяет массиву занимать минимальное пространство до тех пор, пока фактические записи не будут выделены операторами malloc. Приведенный ниже код просто выделяет одну запись, помещает в нее значение и удаляет запись, чтобы продемонстрировать процесс:
Code:
typedef struct
    {
        char s1[81];
        char s2[81];
        char s3[81];
    } Rec;
    Rec *a[10];

    a[0] = (Rec *)malloc(sizeof(Rec));
    strcpy(a[0]->s1, "hello");
    free(a[0]);

Структуры, содержащие указатели​

Структуры могут содержать указатели, как показано ниже:
Code:
typedef struct
    {
        char name[21];
        char city[21];
        char phone[21];
        char *comment;
    } Addr;
    Addr s;
    char comm[100];

    gets(s.name, 20);
    gets(s.city, 20);
    gets(s.phone, 20);
    gets(comm, 100);
    s.comment =
     (char *)malloc(sizeof(char[strlen(comm)+1]));
    strcpy(s.comment, comm);
c-dds4.gif

Этот метод полезен, когда только некоторые записи фактически содержат комментарий в поле комментария. Если для записи нет комментария, то поле комментария будет состоять только из указателя (4 байта). Те записи, у которых есть комментарий, затем выделяют ровно достаточно места для хранения строки комментария в зависимости от длины строки, введенной пользователем.

Указатели на указатели​

c-dds5b.gif

Можно и часто полезно создавать указатели на указатели. Этот метод иногда называют дескриптором и полезен в определенных ситуациях, когда операционная система хочет иметь возможность перемещать блоки памяти в куче по своему усмотрению. В следующем примере демонстрируется указатель на указатель:
Code:
int **p;
    int *q;

    p = (int **)malloc(sizeof(int *));
    *p = (int *)malloc(sizeof(int));
    **p = 12;
    q = *p;
    printf("%d\n", *q);
    free(q);
    free(p);
Windows и Mac OS используют эту структуру, чтобы обеспечить сжатие памяти в куче. Программа управляет указателем p, а операционная система управляет указателем * p. Поскольку операционная система управляет * p, блок, на который указывает * p ( ** p ), может быть перемещен, а * p может быть изменен, чтобы отразить перемещение, не затрагивая программу с помощью p. Указатели на указатели также часто используются в C для обработки параметров указателя в функциях.

Указатели на структуры, содержащие указатели​

Также возможно создавать указатели на структуры, содержащие указатели. В следующем примере используется запись Addr из предыдущего раздела:
Code:
typedef struct
    {
        char name[21];
        char city[21];
        char phone[21];
        char *comment;
    } Addr;
    Addr *s;
    char comm[100];

    s = (Addr *)malloc(sizeof(Addr));
    gets(s->name, 20);
    gets(s->city, 20);
    gets( s->phone, 20);
    gets(comm, 100);
    s->comment =
     (char *)malloc(sizeof(char[strlen(comm)+1]));
    strcpy(s->comment, comm);
c-dds6.gif

Указатель s указывает на структуру, содержащую указатель, указывающий на строку.

В этом примере очень легко создать потерянные блоки, если вы не будете осторожны. Например, вот другая версия примера AP.
Code:
s = (Addr *)malloc(sizeof(Addr));
    gets(comm, 100);
    s->comment =
     (char *)malloc(sizeof(char[strlen(comm)+1]));
    strcpy(s->comment, comm);
    free(s);
c-dds7.gif

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

Связывание​

Наконец, можно создавать структуры, которые могут указывать на идентичные структуры, и эту возможность можно использовать для связывания целой строки идентичных записей в структуре, называемой связным списком.
Code:
typedef struct
    {
        char name[21];
        char city[21];
        char state[21];
        Addr *next;
    } Addr;
    Addr *first;
c-dds8.gif

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

Пример связанного стека​

Хорошим примером динамических структур данных является простая библиотека стека, которая использует динамический список и включает функции для инициализации, очистки, push и pop. Заголовочный файл библиотеки выглядит так:
Code:
/ * Библиотека стека - Эта библиотека предлагает
минимальные стековые операции для
стек целых чисел (легко заменяемый) * /

typedef int stack_data;

extern void stack_init ();
/ * Инициализирует эту библиотеку. * /

extern void stack_clear ();
/ * Очищает стек от всех записей. * /

внешний int stack_empty ();
/ * Возвращает 1, если стек пуст, в противном случае - 0. * /

extern void stack_push (stack_data d);
/ * Помещает значение d в стек. * /

extern stack_data stack_pop ();
/ * Возвращает верхний элемент стека, и удаляет этот элемент.
Возвращает мусор, если стек пуст. * /

Ниже приведен файл кода библиотеки:
Code:
#include "stack.h"
#include <stdio.h>

/ * Библиотека стека - Эта библиотека предлагает
минимальные стековые операции для стека целых чисел * /

struct stack_rec
{
stack_data data;
struct stack_rec *next;
};

struct stack_rec * top = NULL;

void stack_init ()
/ * Инициализирует эту библиотеку. * /
{
top = NULL;
}

void stack_clear ()
/ * Очищает стек от всех записей. * /
{
    stack_data x;

    while (!stack_empty())
    x=stack_pop();
}

int stack_empty()

/ * Возвращает 1, если стек пуст, в противном случае - 0. * /
{
    if (top==NULL)
        return(1);
    else
        return(0);
}
/ * Помещает значение d в стек. * /
{
    struct stack_rec *temp;
    temp=
  (struct stack_rec *)malloc(sizeof(struct stack_rec));
    temp->data=d;
    temp->next=top;
    top=temp;
}

stack_data stack_pop()

/ * Возвращает верхний элемент стека,
и удаляет этот элемент.
Возвращает мусор, если стек пуст. * /
{
    struct stack_rec *temp;
    stack_data d=0;
    if (top!=NULL)
    {
        d=top->data;
        temp=top;
        top=top->next;
        free(temp);
    }
    return(d);
Обратите внимание, как эта библиотека практикует скрытие информации: тот, кто видит только файл заголовка, не может сказать, реализован ли стек с помощью массивов, указателей, файлов или каким-либо другим способом. Также обратите внимание, что C использует NULL. NULL определен в stdio.h, поэтому вам почти всегда придется включать stdio.h при использовании указателей. NULL - это то же самое, что и ноль.

Ошибки C, которых следует избегать:
  • Забыть о скобках при ссылке на запись, как в (* p) .i выше
  • Неспособность удалить какой-либо блок, который вы выделяете - например, вы не должны указывать top = NULL в функции стека, потому что это действие лишает блоки, которые необходимо удалить.
  • Забыть включить stdio.h в какие-либо операции с указателями, чтобы у вас был доступ к NULL.

Что еще стоит попробовать​

Добавить DUP, а количество, и добавить функцию в библиотеку стека, чтобы дублировать верхний элемент стека, возвращает счетчик количества элементов в стеке, а также добавить два верхних элемента в стеке.

Попробуйте это:
  • Создайте программу драйвера и make-файл, а затем скомпилируйте библиотеку стека с драйвером, чтобы убедиться, что он работает.

Использование указателей с массивами​

Массивы и указатели тесно связаны в C. Чтобы использовать массивы эффективно, вы должны знать, как использовать с ними указатели. Для полного понимания отношений между ними, вероятно, потребуется несколько дней изучения и экспериментов, но это того стоит.
Начнем с простого примера массивов на C:
Code:
#define MAX 10

int main()
{
    int a[MAX];
    int b[MAX];
    int i;
    for(i=0; i<MAX; i++)
        a[i]=i;
    b=a;
    return 0;
}

Введите этот код и попробуйте его скомпилировать. Вы обнаружите, что C не будет его компилировать. Если вы хотите скопировать a в b, вы должны вместо этого ввести что-то вроде следующего:
Code:
for (i=0; i<MAX; i++)
    b[i]=a[i];

Или, говоря короче:
Code:
for (i=0; i<MAX; b[i]=a[i], i++);
А еще лучше использовать утилиту memcpy в string.h .
Массивы в C необычны тем, что переменные a и b технически не являются массивами. Вместо этого они являются постоянными указателями на массивы. a и b постоянно указывают на первые элементы своих соответствующих массивов - они содержат адреса a [0] и b [0] соответственно. Поскольку они являются постоянными указателями, вы не можете изменить их адреса. Утверждение a = b;поэтому не работает.

Поскольку a и b являются указателями, с указателями и массивами можно делать несколько интересных вещей. Например, работает следующий код:
Code:
#define MAX 10

void main()
{
    int a[MAX];
    int i;
    int *p;

    p=a;
    for(i=0; i<MAX; i++)
        a[i]=i;
    printf("%d\n",*p);
}

c-array-pointer.gif

Состояние переменных прямо перед началом выполнения цикла for.

Утверждение p = a;работает, потому что это указатель. Технически a указывает на адрес 0-го элемента фактического массива. Этот элемент является целым числом, поэтому a - указатель на одно целое число. Следовательно, объявление p как указателя на целое число и установка его равным a работает. Другой способ сказать то же самое - заменить p = a;с p = & a [0];. Поскольку a содержит адрес a [0], a и & a [0] означают одно и то же.
Теперь, когда p указывает на 0-й элемент a, вы можете делать с ним довольно странные вещи. Переменная является постоянным указателем и не может быть изменен, но р не подлежит таким ограничениям. C на самом деле поощряет вас перемещать его, используя арифметику указателя. Например, если вы скажете p ++;, компилятор знает, что p указывает на целое число, поэтому этот оператор увеличивает p на соответствующее количество байтов, чтобы переместить его к следующему элементу массива. Если p указывал на массив структур длиной 100 байт, p ++;переместится pболее чем на 100 байт. C заботится о деталях размера элемента.

Вы также можете скопировать массив a в b, используя указатели. Следующий код может заменить (для i = 0; i <MAX; a = b, i ++);:
Code:
p=a;
q=b;
for (i=0; i<MAX; i++)
{
    *q = *p;
    q++;
    p++;
}

Вы можете сократить этот код следующим образом:
Code:
p=a;
q=b;
for (i=0; i<MAX; i++)
    *q++ = *p++;

И вы можете сократить его до:
Code:
for (p=a,q=b,i=0; i<MAX; *q++ = *p++, i++);
Что, если вы выйдете за конец массива a или b с помощью указателей p или q ? C не заботится - он беспечно увеличивает p и q, без промедления копируя другие переменные. Вам нужно быть осторожным при индексировании массивов в C, потому что C предполагает, что вы знаете, что делаете.
Вы можете передать массив, такой как a или b, в функцию двумя разными способами. Представьте себе дамп функции, который принимает массив целых чисел в качестве параметра и выводит содержимое массива на стандартный вывод. Есть два способа создать дамп кода:
Code:
void dump(int a[],int nia)
{
    int i;
     for (i=0; i<nia; i++)
        printf("%d\n",a[i]);
}
или же:
Code:
void dump(int *p,int nia)
{
    int i;
     for (i=0; i<nia; i++)
        printf("%d\n",*p++);
}
NIA переменная (number_in_array) требуется, чтобы размер массива известен. Обратите внимание, что функции передается только указатель на массив, а не содержимое массива. Также обратите внимание, что функции C могут принимать в качестве параметров массивы переменного размера.

Струны​

c-string-corrected.gif


Строки в C в значительной степени переплетаются с указателями. Вы должны ознакомиться с концепциями указателей, описанными в предыдущих статьях, чтобы эффективно использовать строки C. Однако, как только вы к ним привыкнете, вы часто сможете очень эффективно выполнять манипуляции со строками.
Строка в C - это просто массив символов. В следующей строке объявляется массив, который может содержать строку длиной до 99 символов.
char str [100];
Он содержит символы, как и следовало ожидать: str [0] - первый символ строки, str [1] - второй символ и так далее. Но почему массив из 100 элементов не может содержать до 100 символов? Поскольку C использует строки с завершающим нулем, это означает, что конец любой строки отмечен значением ASCII 0 (нулевой символ), который также представлен в C как '\ 0' .
Нулевое завершение сильно отличается от того, как многие другие языки обрабатывают строки. Например, в Паскале каждая строка состоит из массива символов с байтом длины, в котором ведется подсчет количества символов, хранящихся в массиве. Эта структура дает Паскалю определенное преимущество, когда вы спрашиваете длину строки. Паскаль может просто вернуть байт длины, тогда как C должен считать символы, пока не найдет '\ 0'. Этот факт в некоторых случаях делает C намного медленнее, чем Pascal, но в других он делает его быстрее, как мы увидим в примерах ниже.
Поскольку C не предоставляет явной поддержки строк в самом языке, все функции обработки строк реализованы в библиотеках. Операции ввода / вывода строк (получение, размещение и т. Д.) Реализованы в <stdio.h>, а набор довольно простых функций обработки строк реализован в <string.h> (в некоторых системах <strings.h >).
Тот факт, что строки не являются родными для C, заставляет вас создавать довольно окольный код. Например, предположим, что вы хотите присвоить одну строку другой строке; то есть вы хотите скопировать содержимое одной строки в другую. В C, как мы видели в прошлой статье, нельзя просто назначить один массив другому. Вы должны копировать его элемент за элементом. Библиотека строк (<string.h> или <strings.h>) содержит функцию strcpy для этой задачи. Вот чрезвычайно распространенный фрагмент кода, который можно найти в обычной программе на C:
Code:
char s[100];
strcpy(s, "hello");

После выполнения этих двух строк следующая диаграмма показывает содержимое s:
На верхней диаграмме показан массив с его символами. На нижней диаграмме показаны эквивалентные значения кода ASCII для символов и показано, как C на самом деле думает о строке (как о массиве байтов, содержащих целочисленные значения). См. Как работают биты и байты для обсуждения кодов ASCII.

Следующий код показывает, как использовать strcpy в C:
Code:
#include <string.h>
int main()
{
    char s1[100],s2[100];
    strcpy(s1,"hello"); /* copy "hello" into s1 */
    strcpy(s2,s1);      /* copy s1 into s2 */
    return 0;
}
strcpy используется всякий раз, когда строка инициализируется в C. Вы используете функцию strcmp в библиотеке строк для сравнения двух строк. Он возвращает целое число, указывающее результат сравнения. Ноль означает, что две строки равны, отрицательное значение означает, что s1 меньше s2, а положительное значение означает, что s1 больше s2 .
Code:
#include <stdio.h>
#include <string.h>
int main()
{
    char s1[100],s2[100];
     gets(s1);
    gets(s2);
    if (strcmp(s1,s2)==0)
        printf("equal\n");
    else if (strcmp(s1,s2)<0)
        printf("s1 less than s2\n");
    else
        printf("s1 greater than s2\n");
    return 0;
}
Другие общие функции в библиотеке строк включают strlen, которая возвращает длину строки, и strcat, которая объединяет две строки. Библиотека строк содержит ряд других функций, с которыми вы можете ознакомиться, прочитав страницу руководства.
Чтобы вы начали создавать строковые функции и помочь вам понять код других программистов (кажется, каждый имеет свой собственный набор строковых функций для специальных целей в программе), мы рассмотрим два примера, strlen и strcpy. Ниже приведена версия strlen строго в стиле Паскаля:
Code:
int strlen(char s[])
{
    int x;
    x=0;
    while (s[x] != '\0')
         x=x+1;
    return(x);
}

Большинство программистов на C избегают этого подхода, потому что он кажется неэффективным. Вместо этого они часто используют подход, основанный на указателях:
Code:
int strlen(char *s)
{
    int x=0;
    while (*s != '\0')
    {
        x++;
        s++;
    }
    return(x);
}

Вы можете сократить этот код до следующего:
Code:
int strlen(char *s)
{
    int x=0;
    while (*s++)
        x++;
    return(x);
}
Я полагаю, что настоящий специалист по C мог бы сделать этот код еще короче.
Когда я компилирую эти три фрагмента кода на MicroVAX с помощью gcc без оптимизации и запускаю каждый 20000 раз на 120-символьной строке, первый фрагмент кода дает время 12,3 секунды, второй 12,3 секунды и третий 12,9 секунды. Что это значит? Для меня это означает, что вы должны писать код таким образом, который вам будет легче понять. Указатели обычно дают более быстрый код, но приведенный выше код strlen показывает, что это не всегда так.

Мы можем пройти через ту же эволюцию с помощью strcpy:
Code:
strcpy(char s1[],char s2[])
{
    int x;
    for (x=0; x<=strlen(s2); x++)
        s1[x]=s2[x];
}

Обратите внимание, что <= важен в цикле for, потому что код затем копирует '\ 0'. Обязательно скопируйте '\ 0'. Если вы не укажете его, в дальнейшем возникают серьезные ошибки, потому что строка не имеет конца и, следовательно, ее длина неизвестна. Также обратите внимание, что этот код очень неэффективен, потому что strlen вызывается каждый раз в цикле for. Чтобы решить эту проблему, вы можете использовать следующий код:
Code:
strcpy(char s1[],char s2[])
{
    int x,len;
     len=strlen(s2);
    for (x=0; x<=len; x++)
        s1[x]=s2[x];
}

Версия указателя аналогична.
Code:
strcpy(char *s1,char *s2)
{
    while (*s2 != '\0')
    {
        *s1 = *s2;
        s1++;
        s2++;
    }
}

Вы можете сжать этот код дальше:
Code:
strcpy(char *s1,char *s2)
{
    while (*s2)
        *s1++ = *s2++;
}

Если хотите, можете даже сказать while (* s1 ++ = * s2 ++);. Первой версии strcpy требуется 415 секунд для копирования 120-символьной строки 10 000 раз, второй версии - 14,5 секунды, третьей версии - 9,8 секунды, а четвертой - 10,3 секунды. Как видите, указатели обеспечивают здесь значительный прирост производительности.
Прототип функции strcpy в библиотеке строк указывает, что она предназначена для возврата указателя на строку:
Code:
char *strcpy(char *s1,char *s2)

Большинство строковых функций в результате возвращают указатель на строку, а strcpy возвращает значение s1 в качестве результата.
Использование указателей со строками иногда может привести к определенному увеличению скорости, и вы можете воспользоваться этим, если немного подумаете о них. Например, предположим, что вы хотите удалить начальные пробелы из строки. Возможно, вы захотите переместить символы поверх пробелов, чтобы удалить их. В C вы можете вообще избежать движения:
Code:
#include <stdio.h>
#include <string.h>

int main()
{
    char s[100],*p;
     gets(s);
    p=s;
    while (*p==' ')
        p++;
    printf("%s\n",p);
    return 0;
}
Это намного быстрее, чем техника движения, особенно для длинных струн.
Вы научитесь многим другим трюкам со строками, когда будете читать другой код. Практика - ключ к успеху.

Специальное примечание о струнах​

Специальное примечание о строковых константах
Предположим, вы создали следующие два фрагмента кода и запустили их:
Code:
Fragment 1

{
    char *s;

    s="hello";
    printf("%s\n",s);
}

Fragment 2

{
    char s[100];

    strcpy(s,"hello");
    printf("%s\n",s);
}

Эти два фрагмента производят одинаковый результат, но их внутреннее поведение совершенно разное. Во фрагменте 2 нельзя сказать s = "hello";. Чтобы понять различия, вы должны понимать, как таблица строковых констант работает в C.
Когда ваша программа компилируется, компилятор формирует файл объектного кода, который содержит ваш машинный код и таблицу всех строковых констант, объявленных в программе. Во фрагменте 1 утверждение s = "hello";заставляет s указывать на адрес строки hello в таблице строковых констант. Поскольку эта строка находится в таблице строковых констант и, следовательно, технически является частью исполняемого кода, вы не можете ее изменить. Вы можете только указать на него и использовать его только для чтения.
Во фрагменте 2 строка hello также существует в таблице констант, поэтому вы можете скопировать ее в массив символов с именем s. Поскольку s не является указателем, оператор s = "hello";не будет работать во фрагменте 2. Он даже не скомпилируется.
Специальное примечание об использовании строк с malloc

Предположим, вы пишете следующую программу:
Code:
int main()
{
    char *s;

    s=(char *) malloc (100);
    s="hello";
    free(s);
    return 0;
}
Он компилируется правильно, но при запуске дает ошибку сегментации на свободной строке. В таНос линии выделяет блок 100 байт в длину и точки с на ней, но теперь ы = «Привет»;линия проблема. Это синтаксически правильно, потому что s - указатель; однако, когда s = "hello";выполняется, s указывает на строку в таблице строковых констант, и выделенный блок становится потерянным. Поскольку s указывает на таблицу строковых констант, строку нельзя изменить; free не работает, потому что не может освободить блок в исполняемой области.

Правильный код следует ниже:
Code:
int main()
{
    char *s;
    s=(char *) malloc (100);
    strcpy(s,"hello");
    free(s);
    return 0;
}

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

    Ошибки C, которых следует избегать:
    Потеря символа \ 0, что легко, если вы не будете осторожны, может привести к очень тонким ошибкам. Убедитесь, что вы копируете \ 0 при копировании строк. Если вы создаете новую строку, убедитесь, что вы поместили в нее \ 0. И если вы копируете одну строку в другую, убедитесь, что принимающая строка достаточно велика, чтобы вместить исходную строку, включая \ 0. Наконец, если вы указываете указателем на некоторые символы, убедитесь, что они заканчиваются на \ 0.

    Приоритет оператора​

    C содержит много операторов, и из-за того, как работает приоритет операторов, взаимодействие между несколькими операторами может сбивать с толку.
    х = 5 + 3 * 6;
    X получает значение 23, а не 48, потому что в C умножение и деление имеют более высокий приоритет, чем сложение и вычитание.
    char * a [10];
    Является ли один указатель на массив из 10 символов, или это массив из 10 указателей на характер? Если вы не знаете соглашения о приоритете в C, нет никакого способа узнать. Точно так же в E.11 мы видели это из-за операторов приоритета, таких как * pi = 10;не работает. Вместо этого форма (* p) .i = 10;необходимо использовать для принудительного приоритета.
    Следующая таблица из языка программирования C Кернигана и Ричи показывает иерархию приоритетов в C. Верхняя строка имеет наивысший приоритет.
    Code:
    Ассоциативность операторов
    ( [ -. Слева направо
    ! - ++ - {- + * & (приведение к типу) sizeof Справа налево
    (в строке выше +, - и * - унарные формы)
    * / % Слева направо
    + - Слева направо
    << >> Слева направо
    <<=>> = Слева направо
    ==! = Слева направо
    & Слева направо
    ^ Слева направо
    | Слева направо
    && Слева направо
    || Слева направо
    ?: Слева направо
    = + = - = * = / =% = & = ^ = | = << = >> = Справа налево
    , Слева направо
    Используя эту таблицу, вы можете увидеть, что char * a [10]; представляет собой массив из 10 указателей на символ. Вы также можете понять, почему скобки нужны, если (* p) .i нужно обрабатывать правильно. После некоторой практики вы запомните большую часть этой таблицы, но время от времени что-то не будет работать, потому что вас поймали на тонкой проблеме приоритета.

    Аргументы командной строки​

    C предоставляет довольно простой механизм для получения параметров командной строки, введенных пользователем. Он передает параметр argv в основную функцию программы. Структуры argv присутствуют в большом количестве более сложных вызовов библиотеки, поэтому понимание их полезно любому программисту на C.

    Введите следующий код и скомпилируйте его:
    Code:
    #include <stdio.h>
    
    int main(int argc, char *argv[])
    {
        int x;
    
        printf("%d\n",argc);
        for (x=0; x<argc; x++)
            printf("%s\n",argv[x]);
        return 0;
    }

    В этом коде основная программа принимает два параметра: argv и argc. Параметр argv - это массив указателей на строку, которая содержит параметры, введенные при вызове программы из командной строки UNIX. Целое число argc содержит количество параметров. Этот конкретный фрагмент кода вводит параметры командной строки. Чтобы попробовать это, скомпилируйте код в исполняемый файл с именем aaa и введите aaa xxx yyy zzz. Код будет печатать параметры командной строки xxx, yyy и zzz, по одному в каждой строке.
    char *argv[] строка представляет собой массив указателей на строки. Другими словами, каждый элемент массива является указателем, и каждый указатель указывает на строку (технически, на первый символ строки). Таким образом, argv [0] указывает на строку, содержащую первый параметр в командной строке (имя программы), argv [1] указывает на следующий параметр и так далее. Переменная argc сообщает вам, сколько указателей в массиве действительны. Вы обнаружите, что предыдущий код не делает ничего, кроме печати каждой допустимой строки, на которую указывает argv.
    Поскольку существует argv, вы можете позволить своей программе довольно легко реагировать на параметры командной строки, введенные пользователем. Например, ваша программа может определять слово help как первый параметр, следующий за именем программы, и выгружать файл справки в стандартный вывод. Имена файлов также могут передаваться и использоваться в ваших операторах fopen.

    Двоичные файлы​

    Двоичные файлы очень похожи на массивы структур, за исключением того, что структуры находятся в файле на диске, а не в массиве в памяти. Поскольку структуры в двоичном файле находятся на диске, вы можете создавать их очень большие коллекции (ограниченные только вашим доступным дисковым пространством). Они также постоянны и всегда доступны. Единственный недостаток - медленность, связанная с временем доступа к диску.
    У двоичных файлов есть две особенности, которые отличают их от текстовых файлов:
    • Вы можете мгновенно перейти к любой структуре в файле, которая обеспечивает произвольный доступ, как в массиве.
    • Вы можете изменить содержимое структуры в любом месте файла в любое время.

    Двоичные файлы также обычно имеют более быстрое время чтения и записи, чем текстовые файлы, потому что двоичный образ записи сохраняется непосредственно из памяти на диск (или наоборот). В текстовом файле все должно быть преобразовано в текст, а это требует времени.
    C очень четко поддерживает концепцию файла структур. Как только вы открываете файл, вы можете читать структуру, писать структуру или искать любую структуру в файле. Эта концепция файла поддерживает концепцию указателя файла. Когда файл открыт, указатель указывает на запись 0 (первая запись в файле). Любая операция чтения читает текущую указанную структуру и перемещает указатель на одну структуру вниз. Любая операция записи выполняет запись в указанную в данный момент структуру и перемещает указатель на одну структуру вниз. Поиск перемещает указатель на запрошенную запись.
    Имейте в виду, что C рассматривает все в файле на диске как блоки байтов, считываемые с диска в память или считываемые из памяти на диск. C использует указатель файла, но может указывать на любое место байта в файле. Следовательно, вам нужно следить за вещами.

    Следующая программа иллюстрирует эти концепции:
    Code:
    #include <stdio.h>
    
    / * случайное описание записи - может быть что угодно * /
    struct rec
    {
        int x,y,z;
    };
    
    / * записывает, а затем читает 10 произвольных записей
    из файла "junk". * /
      from the file "junk". */
    int main()
    {
        int i,j;
        FILE *f;
        struct rec r;
    
    / * создаем файл из 10 записей * /
    f=fopen("junk","w");
        if (!f)
            return 1;
        for (i=1;i<=10; i++)
        {
            r.x=i;
            fwrite(&r,sizeof(struct rec),1,f);
        }
        fclose(f);
    
    / * читаем 10 записей * /
    f=fopen("junk","r");
        if (!f)
            return 1;
        for (i=1;i<=10; i++)
        {
            fread(&r,sizeof(struct rec),1,f);
            printf("%d\n",r.x);
        }
        fclose(f);
        printf("\n");
    
    / * используем fseek для чтения 10 записей
    в обратном порядке * /
      f=fopen("junk","r");
        if (!f)
            return 1;
        for (i=9; i>=0; i--)
        {
            fseek(f,sizeof(struct rec)*i,SEEK_SET);
            fread(&r,sizeof(struct rec),1,f);
            printf("%d\n",r.x);
        }
        fclose(f);
        printf("\n");
    
    / * использовать fseek для чтения всех остальных записей * /
      f=fopen("junk","r");
        if (!f)
            return 1;
        fseek(f,0,SEEK_SET);
        for (i=0;i<5; i++)
        {
            fread(&r,sizeof(struct rec),1,f);
            printf("%d\n",r.x);
            fseek(f,sizeof(struct rec),SEEK_CUR);
        }
        fclose(f);
        printf("\n");
    
    / * использовать fseek для чтения 4-й записи,
    измените его и запишите обратно * /
      f=fopen("junk","r+");
        if (!f)
            return 1;
        fseek(f,sizeof(struct rec)*3,SEEK_SET);
        fread(&r,sizeof(struct rec),1,f);
        r.x=100;
        fseek(f,sizeof(struct rec)*3,SEEK_SET);
        fwrite(&r,sizeof(struct rec),1,f);
        fclose(f);
        printf("\n");
    
    / * прочтите 10 записей, чтобы застраховаться
    4-я запись была изменена * /
      f=fopen("junk","r");
        if (!f)
            return 1;
        for (i=1;i<=10; i++)
        {
            fread(&r,sizeof(struct rec),1,f);
            printf("%d\n",r.x);
        }
        fclose(f);
        return 0;
    }

    В этой программе использовалось описание структуры rec, но вы можете использовать любое описание структуры, какое захотите. Вы можете видеть, что команды fopen и fclose работают точно так же, как и для текстовых файлов.
    Новые функции здесь - fread, fwrite и fseek. Функция fread принимает четыре параметра:
    • Адрес памяти
    • Количество байтов для чтения на блок
    • Количество блоков для чтения
    • Файловая переменная

    Таким образом, строка fread (& r, sizeof (struct rec), 1, f);говорит читать 12 байтов (размер rec ) из файла f (из текущего местоположения указателя файла) в адрес памяти & r. Требуется один блок из 12 байтов. Было бы так же легко прочитать 100 блоков с диска в массив в памяти, изменив 1 на 100.
    Функция fwrite работает так же, но перемещает блок байтов из памяти в файл. Функция fseek перемещает указатель файла на байт в файле. Как правило, вы перемещаете указатель с шагом sizeof (struct rec), чтобы он оставался на границах записи. При поиске вы можете использовать три варианта:
    • SEEK_SET
    • SEEK_CUR
    • SEEK_END
    SEEK_SET перемещает указатель на x байтов вниз от начала файла (с байта 0 в файле). SEEK_CUR перемещает указатель на x байтов вниз от текущей позиции указателя. SEEK_END перемещает указатель с конца файла (поэтому вы должны использовать отрицательные смещения с этой опцией).
    В приведенном выше коде появляется несколько разных вариантов. В частности, обратите внимание на раздел, в котором файл открывается в режиме r +. Это открывает файл для чтения и записи, что позволяет изменять записи. Код ищет запись, читает ее и изменяет поле; затем он ищет назад, потому что чтение сместило указатель, и записывает изменение обратно.
 
Last edited:
Top