План лекции
  • История вопроса
    • C и UNIX
      • Доисторическая ситуация
      • 1970 год, Кен Томпсон, Деннис Ритчи
      • Переносимость (в смысле -- не ассемблер)
      • Эффективность (как у ассемблера)
      • Следствие -- понятия в C "машинные" -- нет bool, в частности, нет отдельно "символьного типа", как в паскале
      • C -- старый язык, многое по современным представлениям неудобно
    • C++
      • 1980 год, Бьярн Страуструп
      • Дополнительные возможности к C
      • Первоначальное название -- C with classes
      • Сохранить эффективность
      • Возможность написания больших программ
      • Сохранение совместимости со старыми программами на C -- почти удалось, много на этом потеряли
  • Как из кода получается программа (C)
    • Программа из нескольких файлов
    • Компиляция и сборка, объектные файлы
    • Команды сборки и компиляции
    • Компиляция всего сразу
    • Что занимает больше времени -- компиляция или сборка
    • полезность раздельной компиляции
    • Что проверяется на этапе компиляции
    • Что проверяется на этапе сборки
    • Потенциальная проблема с числом параметров у функции
    • Объявление функции
    • Идея заголовочного файла, препроцессор
    • Почему нельзя поместить код в заголовочный файл
    • Проблема многократного включения
    • Макросы
    • Защита от многократного включения
    • Опасность других использований макросов


История языка


Язык С был создан для того, чтобы можно было использовать одну операционную систему для разных машин. Около 1970 г. Кен Томпсон и Деннис Ритчи, сотрудники компании Bell Labs, решили создать ОС Unix, которая писалась на языке С и была предназначена для удобного программирования на данном языке. С тех пор он был портирован на многие другие ОС и стал одним из самых используемых языков программирования.

Рассмотрим плюсы и минусы языка С

Положительные качества довольно очевидны: это язык высокого уровня, настолько эффективный, насколько это возможно. Но так как язык создавался давно, то образец, по которому он создавался, уже устарел, но менять его нельзя, иначе придется переписывать большинство ПО.
Этот минус можно увидеть на примере: при программировании на данном языке стандартной ошибкой является запись if ( A=0) вместо if (A == 0), которая не отслеживается компилятором и приводит к результату, которого не ожидал программист.Вместо того, чтобы сделать проверку, равно ли А нулю, мы присвоили А значение нуля и условие в If всегда будет ложью. Многие вещи в С сделаны для эффективной работы машины, а не для удобства программиста.

В 1980 году Страуструп решил выпустить расширение языка С. Его назвали его C with classes, но вскоре его переименовали в С++. Так как это расширение С, то большинство минусов С перешло в новый язык, только несколько функций С не принадлежат С++. Это расширение позволяет более удобно писать большие программы.

Как из кода получается программа (С)

Минимальная программа

Рассмотрим простую программу

int main(){
  return 0;
}

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

Программа из нескольких файлов


Теперь рассмотрим простую программу вывода Hello.
void hello(){
  printf("Hello!\n");
}

int main(){
  hello();
  return 0;
}

Чтобы скомпилировать эту программу в Unix, надо написать в командной строке следующие команды:
gcc main.c
Эта команда создает файл a.out. Не очень удобно, если все файлы будут названы одним именем, поэтому лучше писать:
gcc main.c -o program, тогда именем файла будет program.
Научившись создавать простые программы, научимся создавать программу из двух файлов. Запишем функцию hello() в один файл hello.c, а функцию main() в файл main.c.
Чтобы сделать из этого программу следует написать в командной строке
gcc hello.c main.c - o program
Чтобы запустить получившуюся программу, надо ввести в командной строке:
./program
А чтобы скомпилировать их по отдельности:
gcc -c hello.c //на выходе будет файл hello.о, -с означает, что мы только компилируем данную программу, hello.o называется объектным кодом.
gcc -c main.c // на выходе будет файл main.о
gcc hello.o main.o - o program // этот этап называется linking

Компиляция и сборка


Рассмотрим подробнее этот процесс.

В первых двух командах мы просто компилируем программы. Компиляция - перевод текста программы в машинный код и оптимизация его. Вместо функции hello() в main будет написано просто "вызвать функцию hello".

Третья команда называется Linking( или сборка). В процессе выполнения её вместо "вызвать функцию hello" будет написано "вызвать функцию по адресу". Очевидно, что большая часть работы выполняется при компиляции.

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

Потенциальная проблема с числом параметров


Предположим, что мы решили в функцию hello добавить аргумент, но в функции main забыли передать функции hello значение.
---файл hello.c----
void hello(int n){
  printf("%d\n",n);
}

---файл main.c----
int main(){
  hello();
  return 0;
}

При раздельной компиляции ни одна из команд gcс, учавствующих в раздельной компиляции этой программы, не выдаст нам ошибки. По отдельности эти файлы компилируются правильно. Чтобы избежать эту ошибку, необходимо в каждом файле объявить все функции. Тогда при компиляции файла hello.c компилятор будет сверять количество параметров в объявлении и определении, а в файле main.c компилятор будет сравнивать количество параметров в вызове и объявлении:
---файл hello.с----
void hello(int n); // объявление функции hello(int n)
void hello(int n){
  printf("%d\n",n);
}

---файл main.с----
void hello(int n);
int main(){
  hello();
  return 0;
}

В таком случаем при компиляции main.с нам выдадут ошибку. Но если у нас 20000 функций и 2000 файлов, то утомительно объявлять каждый раз все функции.

Заголовочный файл


Для того, чтобы решить задачу объявления всех функций в каждом файле, создают отдельный файл header, куда пишутся объявление всех функций.
---файл header.h---
void hello(int);

---файл hello.с----
#include "header.h" //include просто заменяет при preprocessing данную строку на содержимое файла header.h
void hello(int n){
  printf("%d\n",n);
}

---файл main.с----
#include "header.h"
int main(){
  hello();
  return 0;
}

Если написать определение функции hello в header.h, то выдадут ошибку, т.к. будет дважды определена одна функция. Ошибку выдадут при linking.

Обратим внимание, что у нас в программе есть ещё одна функция - printf, которая является функцией стандартной библиотеки. По причинам, озвученным выше, стоит подключать ещё и header стандартной библиотеки: #include < stdio.h > (<>- данные скобки означают, что header надо взять из места, где лежат стандартные headers)

Многократное включение


Последнее на данное занятие: ошибка зацикливания headers.Пример:
---файл first.h----
#include "second.h"

---файл second.h----
#include "first.h"

Получается, что у нас эти файлы бесконечно включаются в друг друга.
Чтобы избежать этой ошибки всегда делаем проверку:
---файл first.h----
#if ndef _FIRST_H_ // если макрос ещё не определен,то
#define _FIRST_H_ // определяем его
#include "second.h" 
...
#endif

---файл second.h----
#if ndef _Second_H_
#define _Second_H_
#include "first.h"
...
#endif

Рассмотрим по порядку эту программу. Запускаем first.h, макрос _FIRST_H ещё не определен, значит определяем его и переходим к second.h. Макрос _Second_H_ ещё не определен, значит определяем его и переходим к first.h. Макрос _FIRST_H уже определен, значит идем за endifтут конец файла. Проблема зацикливания решена.