Советы по программированию



Автор — Рафаэль А. Финкель

Оригинал статьи

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


C

Глобальные переменные, строки, буферы, динамическое выделение памяти, целые числа, структура данных, указатели, ввод/вывод, параметры командной строки, особенности языка, несколько исходных файлов, компоновка нескольких объектных файлов, отладка.

Глобальные переменные
C позволяет объявлять переменные вне какой-либо функции. Эти переменные называются глобальными.

  • Глобальная переменная выделяется один раз при запуске программы и остаётся в памяти до завершения программы.
  • Глобальная переменная видима для всех функций в одном и том же файле.
  • Вы можете сделать глобальную переменную, объявленную в файле A.c, видимой для всех функций в других файлах (B.c, C.c, D.c и т.д.), добавив модификатор extern в файлах B.c, C.c, D.c и т.д., как в этом примере:
  extern int theVariable;
  • Если переменную используют в нескольких файлах, объявите её extern в заголовочном файле foo.h (описан ниже) и подключите этот файл с помощью #include foo.h в файлах B.c, C.c, D.c и т.д.
  • Переменную необходимо объявить ровно в одном файле без модификатора extern, иначе память для неё не будет выделена.

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


Строки
Строки в C представлены указателями на массивы символов, заканчивающиеся нулевым символом.

  • Строка объявляется как char *variableName.
  • Если строка является константой, вы можете просто присвоить ей строковый литерал:
  char *myString = "Пример строки";
  • Пустая строка содержит только завершающий символ null:
  myString = ""; // пустая строка, длина 0, содержит null
  • Указатель NULL не является допустимым значением строки:
  myString = NULL; // недопустимое значение строки
  • Вы можете использовать такое значение для обозначения конца массива строк:
  argv[0] = "progName";
  argv[1] = "firstParam";
  argv[2] = "secondParam";
  argv[3] = NULL; // терминатор

Если строка вычисляется во время выполнения, необходимо зарезервировать достаточно места для её хранения, включая завершающий символ null:

char *myString;
myString = (char *) malloc(strlen(someString) + 1); // выделяем место
strcpy(myString, someString); // копируем строку someString в myString

Чтобы избежать утечек памяти, освобождайте выделенное с помощью malloc место с помощью free. Параметром функции free должен быть адрес, возвращённый malloc:

free((void *) myString);

Чтобы сохранить читаемость кода, вызывайте free() в той же функции, где вызываете malloc(). Между этими вызовами можно вызывать другие функции для работы со строкой.


Копирование строк
Будьте особенно внимательны, чтобы не скопировать больше байтов, чем может вместить целевая структура данных. Переполнение буфера является наиболее распространённой причиной уязвимостей в безопасности программ. Используйте strncpy() и strncat() вместо strcpy() и strcat().


Использование строк в C++
Если вы используете C++, перед передачей строковых объектов в системный вызов необходимо преобразовать их в строки в стиле C:

string myString; // Это объявление работает только в C++
...
someCall(myString.c_str());


К сожалению, c_str() возвращает неизменяемую строку. Если требуется изменяемая строка, вы можете либо скопировать данные с помощью strcpy(), либо выполнить приведение типа:

someCall(const_cast<char *>(myString.c_str()));


Приведение типа менее безопасно, так как someCall() может изменить строку, что приведёт к ошибкам в частях программы, предполагающих, что myString является константой, как это обычно бывает со строками в C++.

Буферы

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

  • Массив байтов не является строкой, даже если он объявлен как char * или char [].
  • Буферы могут не содержать ASCII-символы и не обязательно заканчиваются символом null (\0).
  • Для определения длины данных в буфере нельзя использовать strlen() (поскольку буфер может содержать нулевые байты). Вместо этого длина данных определяется значением, возвращаемым системным вызовом (например, read), который создал данные.
  • Для работы с буферами используйте memcpy() или bcopy() вместо strcpy() и strcat().

Пример записи буфера из 123 байт в файл:

char *fileName = "/tmp/foo";
#define BUFSIZE 4096
char buf[BUFSIZE]; // буфер, содержащий максимум BUFSIZE байт
...
int outFile; // файловый дескриптор
int bytesToWrite; // количество байт для записи
char *outPtr = buf;
...
if ((outFile = creat(fileName, 0660)) < 0) { // ошибка
    perror(fileName); // печать причины ошибки
    exit(1);
}
bytesToWrite = 123; // пример значения
while ((bytesWritten = write(outFile, outPtr, bytesToWrite)) < bytesToWrite) {
    if (bytesWritten < 0) { // ошибка
        perror("write");
        exit(1);
    }
    outPtr += bytesWritten;
    bytesToWrite -= bytesWritten;
}

Чтобы компилятор выделил память для буферов, их нужно объявлять с фиксированным размером:

#define BUFSIZE 1024
char buf[BUFSIZE];

Если размер буфера неизвестен на момент компиляции, используйте динамическое выделение памяти:

char *buf = (char *) malloc(bufferSize);

Динамическое выделение памяти

Вы можете выделять и освобождать память динамически. Например:

Индивидуальные экземпляры любого типа:

typedef ... myType;
myType *myVariable = (myType *) malloc(sizeof(myType));
// Теперь можно использовать *myVariable
...
free((void *) myVariable);

Одномерные массивы любого типа:

myType *myArray = (myType *) malloc(arrayLength * sizeof(myType));
// myArray[0] .. myArray[arrayLength - 1] теперь выделены
...
free((void *) myArray);

Двумерные массивы:
Представляются как массив указателей, каждый из которых указывает на массив:

myType **myArray = (myType **) malloc(numRows * sizeof(myType *));
for (int rowIndex = 0; rowIndex < numRows; rowIndex++) {
    myArray[rowIndex] = (myType *) malloc(numColumns * sizeof(myType));
}
// Инициализация значений
...
for (int rowIndex = 0; rowIndex < numRows; rowIndex++) {
    free((void *) myArray[rowIndex]);
}
free((void *) myArray);

Целые числа

Целые числа в C обычно занимают 4 байта. Например, число 254235 представляется как двоичное значение 00000000, 00000011, 11100001, 00011011.

Для записи целого числа в файл:

write(outFile, &myInteger, sizeof(myInteger));

Чтобы работать с отдельными байтами целого числа, преобразуйте его в структуру:

int IPAddress; // представляется как целое число
typedef struct {
    char byte1, byte2, byte3, byte4;
} IPDetails_t;

IPDetails_t *details = (IPDetails_t *) (&IPAddress);
printf("Байт 1: %o, Байт 2: %o, Байт 3: %o, Байт 4: %o\n",
       details->byte1, details->byte2, details->byte3, details->byte4);

Вывод данных

Для форматирования вывода используйте printf или его вариант fprintf.
Строка формата использует %d, %s, %f для обозначения вывода целого числа, строки или числа с плавающей точкой соответственно. Специальные символы \t и \n обозначают табуляцию и новую строку.
Пример:

printf("Я думаю, что число %d — это %s\n", 13, "счастливое");

Примечание:
Смешивание printf(), fprintf() и cout может привести к изменению порядка вывода, поскольку они используют независимые буферы, которые очищаются только при их заполнении.


Основная функция (main)

main() принимает параметры, представляющие параметры командной строки.
Один из вариантов записи:

int main(int argc, char *argv[]);
  • argc — количество параметров.
  • argv — массив строк, представляющий параметры.

По соглашению первый элемент argv — это имя программы:

int main(int argc, char *argv[]) {
    printf("У меня %d параметров; моё имя — %s, а мой первый параметр — %s\n", 
           argc, argv[0], argv[1]);
}

Полезные возможности языка

  • Используйте оператор ++ для увеличения значения переменной или сдвига указателя. Обычно оператор ставят после переменной (myInt++), чтобы сначала использовать значение, а затем увеличить его.
  • Упрощённые выражения:
  myInt -= 3; // то же, что и myInt = myInt - 3
  myInt *= 42; // то же, что и myInt = myInt * 42
  myInt += 1;  // предпочтительно использовать вместо myInt++
  • Представление чисел:
  • Десятичные: 123
  • Восьмеричные: 0453 (с префиксом 0)
  • Шестнадцатеричные: 0xffaa (с префиксом 0x).

Отладка C-программ

  1. Ошибка сегментации (segmentation fault):
    Обычно возникает из-за выхода за пределы массива, неинициализированного указателя или значения NULL.
  2. Используйте отладочные операторы:
    Добавляйте операторы printf() в программу, чтобы найти источник ошибки.
  3. Программа gdb:
    Для использования эффективной отладки выполните компиляцию с флагом -g.
    Команда для отладки:
   gdb myProgram

Основные команды:

  • where: Показать стек вызовов.
  • list: Показать исходный код.
  • break: Установить точку останова в указанной строке.
  • next: Выполнить следующую строку.
  • step: Войти в функцию.

Unix

Стандартные файлы

Каждый процесс в Unix начинает работу с тремя стандартными файлами:

  1. Стандартный ввод (stdin): Обычно подключён к клавиатуре.
  2. Стандартный вывод (stdout): Обычно подключён к экрану.
  3. Стандартная ошибка (stderr): Также подключена к экрану.

Вы можете перенаправить их с помощью оболочки:

  • ls > output.txt — вывод в файл.
  • wc < input.txt — ввод из файла.
  • ls | wc — передача вывода одной программы другой.

Инструменты разработки

  1. Текстовый редактор:
  • Vim: Поддерживает подсветку синтаксиса, автодополнение, поиск по тегам и другие функции.
  • Emacs: Аналогичный функционал с дополнительными возможностями.
  • Простые редакторы: Подходят для новичков, но не поддерживают автоматическую индентацию или подсветку.
  1. Программа make:
    Используется для автоматизации компиляции. Пример Makefile:
   SOURCES = main.c utils.c
   OBJECTS = main.o utils.o
   CFLAGS = -g -Wall

   program: $(OBJECTS)
       $(CC) $(CFLAGS) $(OBJECTS) -o program
  1. Поиск определений:
    Программа grep помогает искать переменные и определения:
   grep "struct timeval {" /usr/include/*/*.h

Упражнения

Выполните эти упражнения на языке C.
Напишите программу, названную atoi, которая открывает файл данных, имя которого передаётся в командной строке, и читает из него одну строку, содержащую целое число, представленное в ASCII-символах. Программа преобразует эту строку в целое число, умножает это число на 3 и выводит результат в стандартный поток вывода. Программа не должна использовать функцию atoi(). Вы должны использовать программу make. Ваш Makefile должен содержать три правила: atoi, run (выполняет программу с вашими стандартными тестовыми данными и перенаправляет вывод в новый файл) и clean (удаляет временные файлы). Убедитесь, что программа корректно обрабатывает некорректные данные и завершается с понятным сообщением об ошибке, если файл данных отсутствует или недоступен. Отлаживайте программу, запуская её через gdb, устанавливая точку останова на main() и используя команду step для пошагового выполнения.

Изучите руководство по программе cat. Напишите свою версию программы cat. Ваша версия должна принимать несколько (или ни одного) параметров, представляющих имена файлов. Поддержка параметров-опций не требуется.

Напишите программу removeSuffix, которая принимает один параметр: имя файла с суффиксами. Файл с суффиксами содержит по одной записи на строку. Запись представляет собой непустую строку, которую мы называем суффиксом, за которой следует символ > и другая (возможно пустая) строка, которую мы называем заменой. Ваша программа должна сохранить все суффиксы и их замены в хеш-таблицу с использованием внешних цепочек. Затем ваша программа должна читать стандартный ввод. Для каждого слова w, разделённого пробелами, найдите самый длинный суффикс s, который присутствует в w, и модифицируйте w, удаляя s и вставляя его замену, создавая w'. Выведите одну строку на каждое модифицированное слово в формате w>w'. Не выводите ни одного слова, которое не было модифицировано.