C: Declare vs Define в C и C++

Автор: | 02/08/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 в заголовочном файле, а затем подключите этот заголовочный файл в файлы исходного кода. Затем вам необходимо добавить определение этой переменной в файле исходного кода.

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