C: Declare vs Define в C и C++

Автор: | 08/02/2016
 

C_logoОбъявление (declaration) и определение (definition) в C и C++ имеют небольшое, но очень важное различие. Если не понимать его – то вас ожидают странные ошибки линкера (LD в GCC – /usr/bin/ld), вроде “undefined symbol foo“, “undefined reference to ‘foo’” или даже “undefined reference to vtable for foo” (в C++).

Что такое Объявление в C и C++

Когда вы объявляете переменную, функцию или класс, то все, что вы делаете – это просто говорите, что “вот есть что-то с таким-то именем и таким-то типом”. Далее компилятор (/usr/bin/gcc или /usr/bin/cc) может обработать все (но не все) случаи использования этого имени без необходимости полного описания этого имени. Объявление – без описания – позволяет писать понятный компилятору код, который он может понять без необходимости собирать все детали. Это особенно удобно когда вы работаете с кодом, который разбит на много файлов с исходным кодом и вам необходимо использовать функцию во многих из них. Вам не требуется писать все тело функции в каждом файле – но вам потребуется внести объявление об этой функции.

Итак, что такое Объявление?  Например:

int func();

Это – объявление функции. В нем нет тела функции, но компилятор будет знать что он может использовать это имя для доступа к этой функции, ожидая что она уже описана где-то в другом месте.

Что такое Описание в C и C++

Описание (или определениеdefining) чего-то означает предоставление всей необходимой информации для создания этого чего-то. Описание функции означает предоставление тела функции. Описание класса – код всех его методов и атрибутов. Как только что-то описано – оно может считаться так же и объявленным. Вы часто можете встретить и объявление и описание функции, класса или переменной одновременно.

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

#include <stdio.h>

int func();

int main()
{
    int x = func();
    printf("X == %dn", x);
}

int func()
{
    return 2;
}

Так как компилятор знает значение, возвращаемое функцией func() и количество аргументов функции из ее объявления – он может скомпилировать вызов к этой функции, даже при том, что у него еще нет ее описания. На деле – описание функции func() может находиться вообще в другом файле.

Вы так же можете объявить класс без его описания:

class MyClass;

Однако код, которому потребуются детали класса MyCalss не будет работать. Т.е. – вы не можете сделать что-то вроде такого:

class MyClass;

MyClass an_object;

class MyClass
{
    int _a_field;
};

Так как компилятору требуется знать размер переменной an_object – но он не может сделать, исходя только из объявления класса MyClass().

Объявление и Описание с использованием Extern

Как правило, когда вы объявляете переменную – то вы так же предоставляете и ее описание. Что такое “объявление” на самом деле?

Это значит, что вы указываете компилятору на необходимость создания хранилища в памяти для этой переменной. Например:

int x;
int main()
{
    x = 3;
}

Строка int x; выполняет и объявление и описание переменной. Фактически она говорит: “Создать переменную с именем x и типом integer. Кроме того – это будет глобальная переменная, описанная в объектом файле, связанном с файлом исходного кода“.

Это значит, что кто-то может написать еще один файл с таким кодом:

extern int x;

int func()
{
    x = 3;
}

Использование extern тут – это объявление переменной, но не ее описание. Тут говорится, что хранилищем для переменной находится где-то в другом месте. Технически, вы даже можете использовать такой код:

extern int x;
int func()
{
    x = 3;
}

int x;

Теперь у вас есть объявление переменной x в начале программы, и ее определение – внизу. Но как правило – extern используется тогда, когда вам требуется получить доступ к переменной, которая объявлена в другом файле, а затем, после компиляции – слинковать два объектных файла вместе.

Использование extern для объявления глобальной переменной весьма схоже с объявлением функций в заголовочных файлах (по факту – вам намного чаще придется использовать extern в заголовочных файлах, чем в исходном коде).

Кроме того – если вы разместите переменную в заголовочном файле и не будете использовать extern – вы столкнетесь с проблемой “неопределенного символа” (undefined symbol): у вас будет символ с несколькими определениями, что приведет к ошибке вида “redefinition of ‘foo’“, когда линкер начнет выполнять связывание нескльких объектных файлов.

Объявление (declaration) vs определение (definition): итог

Объявление предоставляет базовые атрибуты символа – его тип и имя. Определение включает в себя все остальные детали этого символа: если это функция – то описание ее действий, если класс – то его методы и атрибуты, если переменная – то место ее размещения. Зачастую компилятору требуется только объявление чего-либо для компиляции файла в объектный файл. В таких случаях компилятор ожидает, что линкер сможет сам найти определение в другом файле. Если не один файл не содержит описания символа, но в коде есть его объявление – вы получите сообщение об ошибке “undefined symbol” во время линковки.

Примеры использования

Если вы хотите использовать функцию в нескольких файлах исходного кода – вы должны объявить эту функцию в одном заголовочном файле (.h) а затем добавить ее описание в файле исходного кода (.c или .cpp). Весь код, который использует эту функцию должен включать в себе этот заголовочный файл.

Если вы хотите использовать некий класс в нескольких файлах – вам необходимо добавить его определение в заголовочный файл а его методы – определить в соответствующем файле исходного кода.

Если вы хотите использовать переменную в нескольких файлах – объявите ее с помощью ключевого слова extern в заголовочном файле, а затем подключите этот заголовочный файл в файлы исходного кода. Затем вам необходимо добавить определение этой переменной в файле исходного кода.

Оригинал и больше докментации – тут>>>.