Объясните все возможные причины использования заголовков в мультифайловых проектах С

Рейтинг: 3Ответов: 2Опубликовано: 26.03.2015

Посмотрите в начало стр. 5 здесь и объясните популярно, зачем в принципе нужен заголовок calc.h, если проект спокойно компилируется и запускается без заголовков вообще через:

Andrey@AsusP5B ~/2
$ gcc driver.c calc.c -o prog

Andrey@AsusP5B ~/2
$ ./prog

Square of 5 is 25

Andrey@AsusP5B ~/2
$

Это что, новым компиляторам уже на заголовки наплевать или на прототипы функций, или это чистота написания кода, или просто лучше заголовки лучше сразу создавать, чтобы не было путаницы или х.з., так как в будущем они понадобятся для других целей... В теории, ПОЧЕМУ?

Ответы

▲ 3Принят

Все работает, потому что си компилятор полагается на то, что Вы самостоятельно угадали сигнатуру. Возьмите driver.c, закомментируйте #include "calc.h" и "сделайте ошибку в вызове функции, где-то так:

printf("\nSquare of %d is %d\n", x, square());

Код скомпилируется и даже никто не ругнется. Но вот только работать он будет странно (как именно странно - зависит от ситуации, может упасть, а может выводить странные результаты). У меня, к примеру, выводит просто "Square of 5 is 1".

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

Но почему же оно все-таки компилируется? Да все просто. Когда компилятор видит объявление функции, он генерирует для нее код и запоминает адрес начала в специальной таблице в виде имя-адрес (да, там нет параметров!). Когда нужно вставить вызов функции, просто смотрит в таблицу и поставляет "call адрес". Сами параметры перед этим напихает в стек push'ем.

Но тут есть ещё одна хитрость. На самом деле вначале туда проставляются не адреса. Адреса уже проставляет линковщик. Именно по этой причине порядок компиляции файлов не столь важен.

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

В с++ этот трюк уже не проходит. Там компилятор делает "манглироване имен". То есть в таблицу функций вставляет имя функции с хитрым описанием типов параметров. Это нужно, так как в С++ есть возможность создать несколько функций с одним именем и разным кол-вом/типом параметров.

▲ 1

В данном случае все работает без calc.h потому, что функция square() из calc.c возвращает int, а это значение результата функции по умолчанию.

Измените в calc.c на double square (int x) и Вы увидите, как все сломается.


Update

Смотрите, вот код и результаты

avp@avp-xub11:hashcode$ more cf1.c cf2.c | cat
::::::::::::::
cf1.c
::::::::::::::
#include <stdio.h>

int main () {
  int x = 5;
  const char *fmt = "square of %d is "
#ifdef TINT
    "%d\n"
#else
    "%f\n"
#endif
    ;

  printf(fmt, x, square(x));

  return 0;
}
::::::::::::::
cf2.c
::::::::::::::
#ifdef TINT
int
#else
double
#endif
square (int x) {
  return  x * x;
}
avp@avp-xub11:hashcode$ gcc cf1.c cf2.c -DTINT
avp@avp-xub11:hashcode$ ./a.out 
square of 5 is 25
avp@avp-xub11:hashcode$ gcc cf1.c cf2.c
avp@avp-xub11:hashcode$ ./a.out 
square of 5 is -0.000000
avp@avp-xub11:hashcode$

Update 2

Добавим calc.h

#ifndef _CALC_H // это так называемый guard, почти всегда нужен в заголовочных файлах
#define _CALC_H

#ifndef TINT
double
#else
int
#endif
square (int);

#endif

который в зависимости от TINT определяет либо int либо double square().

avp@avp-xub11:hashcode$ more cf1.c cf2.c | cat
::::::::::::::
cf1.c
::::::::::::::
#include <stdio.h>

#include "calc.h"

int main () {
  int x = 5;
  const char *fmt = "square of %d is "
#ifdef TINT
    "%d\n"
#else
    "%f\n"
#endif
    ;

  printf(fmt, x, square(x));

  return 0;
}
::::::::::::::
cf2.c
::::::::::::::
#include "calc.h"

#ifdef TINT
#define CAST 
int
#else
#define CAST (double)
double
#endif
square (int x) {
  return CAST x * x;
}
avp@avp-xub11:hashcode$ gcc cf1.c cf2.c -DTINT
avp@avp-xub11:hashcode$ ./a.out 
square of 5 is 25
avp@avp-xub11:hashcode$ gcc cf1.c cf2.c 
avp@avp-xub11:hashcode$ ./a.out 
square of 5 is 25.000000
avp@avp-xub11:hashcode$

И все, как видите, заработало.

Конечно, в простых случаях (а на практике лучше все делать проще (это я тут от лени начал ваять шаблоны (templates))) никакой препроцессор (для условной компиляции, конечно же) не нужен.