Чем определение отличается от объявления

от admin

Разница между объявлением и определением в C и C++

В C и C++ есть маленькое, но важное различие между значением слов “объявление” и “определение”. Если вы не понимаете разницы, иногда вы будете получать странные ошибки, такие как:

Что означает “объявление” в C и C++

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

Итак, как же выглядит объявление?

Например, если вы пишете:

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

Что это значит, определить что-то в C и C++ ?

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

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

Так как компилятор знает возвращаемое значение функции, и количество аргументов, которые она принимает, он может скомпилировать вызов функции, даже если она еще не определена. В самом деле, определение функции может быть в другом файле!

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

Код, которому нужно знать детали того, что находится в MyClass не будет работать — вы не можете этого сделать:

Потому что компилятор должен знать размер переменной an_object, и он не может узнать его из объявления MyClass; ему нужно определение, которое появляется ниже.

Объявление и определение переменных с Extern

В большинстве случаев, когда вы объявляете переменную, вы также определяете ее. Так что же такое определение переменной? Это означает, что вы говорите компилятору, где выделить место для этой переменной. Например, если вы пишете:

Строка int х , как объявляет так и определяет переменную, она говорит: «создать переменную с именем х , типа int . Кроме того, место хранения переменной определяется тем, что это глобальная переменная, определенная в объектный файл, связанный с этим исходным файлом. Это немного странно, не правда ли? Что произойдет, если кто-то другой напишет второй исходный файл, который имеет такой код:

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

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

В самом деле, если вы поставите переменную в заголовке файла и не будете использовать extern , вы столкнетесь с обратной задачей неопределенного символа, у вас будет символ с несколькими определениями, с ошибкой типа:

Это произойдет, когда компоновщик будет связывать объектные файлы.

Разница между объявлением и определением: Вывод

Объявление предоставляет основные свойства символа: его тип и его название. Определение предоставляет все детали этого символа — если это функция, что она делает; если это класс, какие у него поля и методы; если это переменная, где эта переменная находится. Часто, компилятору нужно объявление, чтобы скомпилировать файл в объектный файл, так как компоновщик может найти определение из другого файла. Если исходный файл не определяет символ, но он объявлен, во время компоновки вы будете получать ошибки, сообщающие о неопределенных символах.

Общие случаи

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

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

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

2.6 – Предварительные объявления и определения

Взгляните на этот, казалось бы, правильный пример программы:

Вы ожидаете, что эта программа даст результат:

Но на самом деле она вообще не компилируется! Visual Studio выдает следующую ошибку компиляции:

Причина, по которой эта программа не компилируется, заключается в том, что компилятор последовательно компилирует содержимое исходных файлов. Когда компилятор достигает вызова функции add в строке 5 в функции main , он не знает, что такое add , потому что мы определили add только в строке 9! Это вызывает ошибку, « identifier not found » (идентификатор не найден).

Более старые версии Visual Studio выдали бы дополнительную ошибку:

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

Лучшая практика

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

Чтобы решить эту проблему, нам нужно разобраться с тем фактом, что компилятор не знает, что такое add . Есть два распространенных способа решения этой проблемы.

Вариант 1. Изменение порядка определений функций

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

Таким образом, к моменту вызовов функции add из main компилятор уже будет знать, что такое add . Поскольку это очень простая программа, сделать это изменение относительно легко. Однако в более большой программе может быть утомительно пытаться выяснить, какие функции вызывают какие другие функции (и в каком порядке), чтобы их можно было определять последовательно.

Кроме того, этот вариант не всегда возможен. Допустим, мы пишем программу, которая имеет две функции A и B . Если функция A вызывает функцию B , а функция B вызывает функцию A , то нет способа упорядочить функции таким образом, чтобы компилятор был доволен. Если вы сначала определите A , компилятор пожалуется, что не знает, что такое B . Если вы сначала определите B , компилятор пожалуется, что не знает, что такое A .

Вариант 2. Использование предварительного объявления

Мы также можем исправить это, используя предварительное объявление.

Предварительное объявление позволяет нам сообщить компилятору о существовании идентификатора до его фактического определения.

В случае функций это позволяет нам сообщить компилятору о существовании функции до того, как мы определим тело функции. Таким образом, когда компилятор встретит вызов функции, он поймет, что мы выполняем вызов функции, и сможет проверить, правильно ли мы вызываем функцию, даже если он еще не знает, как и где эта функция определена.

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

Вот прототип функции для функции add :

Итак, вот наша исходная программа, которая не компилировалась, использующая прототип функции в качестве предварительного объявления для функции add :

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

Стоит отметить, что в прототипах функций не нужно указывать имена параметров. В приведенном выше коде вы также можете предварительно объявить эту функцию следующим образом:

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

Лучшая практика

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

Забываем о теле функции

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

Ответ: это зависит от обстоятельств. Если предварительное объявление сделано, но функция никогда не вызывается, программа будет компилироваться и работать нормально. Однако если предварительное объявление сделано, и функция вызывается, но программа никогда не определяет эту функцию, программа будет компилироваться нормально, но компоновщик (линкер) будет жаловаться, что не может разрешить вызов функции.

Рассмотрим следующую программу:

В этой программе мы даем предварительное объявление add и вызываем add , но нигде не определяем add . Когда мы пытаемся скомпилировать эту программу, Visual Studio выдает следующее сообщение:

Как видите, программа скомпилировалась нормально, но проблема возникла на этапе компоновки, потому что int add(int, int) никогда не определялась.

Другие типы предварительных объявлений

Предварительные объявления чаще всего используются с функциями. Однако предварительные объявления в C++ также могут использоваться и с другими идентификаторами, такими как переменные и пользовательские типы. Переменные и пользовательские типы имеют отличающийся синтаксис для предварительного объявления, поэтому мы рассмотрим их в будущих уроках.

Читать:
Сколько градусов в угле шестиугольника объемного

Объявления против определений

В C++ вы часто будете слышать слова «объявление» и «определение», часто взаимозаменяемые. Что они имеют в виду? Теперь у вас достаточно знаний, чтобы понять разницу между ними.

Определение фактически реализует (для функций или типов) или создает экземпляр (для переменных) идентификатора. Вот несколько примеров определений:

Определение требуется, чтобы удовлетворить компоновщик (линкер). Если вы используете идентификатор без определения, компоновщик выдаст ошибку.

Правило одного определения (или сокращенно ODR, one definition rule) – это хорошо известное правило в C++. ODR состоит из трех частей:

  1. В заданном файле функция, объект, тип или шаблон могут иметь только одно определение.
  2. В заданной программе объект или обычная функция может иметь только одно определение. Это выделено потому, что программы могут иметь более одного файла (мы рассмотрим это в следующем уроке).
  3. Типы, шаблоны, встроенные функции и переменные могут иметь идентичные определения в разных файлах. Мы еще не рассмотрели большинство из этих вещей, поэтому не беспокойтесь об этом сейчас – мы вернемся к ним, когда это будет уместно.

Нарушение пункта 1 правила одного определения приведет к тому, что компилятор выдаст ошибку переопределения. Нарушение пункта 2 правила одного определения может привести к тому, что компоновщик выдаст ошибку переопределения. Нарушение пункта 3 правила одного определения приведет к неопределенному поведению.

Вот пример нарушения пункта 1:

Поскольку указанная выше программа нарушает пункт 1 правила одного определения, компилятор Visual Studio выдает следующие ошибки компиляции:

Для продвинутых читателей

Функции, которые имеют общий идентификатор, но имеют разные параметры, считаются отдельными функциями. Мы обсудим это далее в уроке «8.9 – Перегрузка функций».

Объявление – это инструкция, которая сообщает компилятору о существовании идентификатора и информацию о его типе. Вот несколько примеров объявлений:

Объявление – это всё, что нужно компилятору. Вот почему мы можем использовать предварительное объявление, чтобы сообщить компилятору об идентификаторе, который на самом деле не будет определен позже.

В C++ все определения также служат объявлениями. Вот почему int x появляется в наших примерах как для определений, так и для объявлений. Поскольку int x – это определение, оно же и объявление. В большинстве случаев определение служит нашим целям, поскольку оно удовлетворяет и компилятор, и компоновщик. Явное объявление нам нужно предоставить только тогда, когда мы хотим использовать идентификатор до того, как он будет определен.

Хотя верно, что все определения являются объявлениями, обратное неверно: все объявления не являются определениями. Примером этого является прототип функции – он удовлетворяет компилятор, но не компоновщик. Объявления, которые не являются определениями, называются чистыми объявлениями. Другие типы чистых объявлений включают в себя предварительные объявления для переменных и объявления типов (вы столкнетесь с ними в будущих уроках, сейчас о них не нужно беспокоиться).

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

Примечание автора

В обычном языке термин «объявление» обычно используется для обозначения «чистого объявления», а «определение» используется для обозначения «определения, которое также служит объявлением». Таким образом, мы обычно называем int x; определением, хотя это и определение, и объявление.

Небольшой тест

Вопрос 1

Что такое прототип функции?

Прототип функции – это инструкция объявления, которая включает в себя имя функции, тип возвращаемого значения и параметры. Он не включает в себя тело функции.

Вопрос 2

Что такое предварительное объявление?

Предварительное объявление сообщает компилятору, что идентификатор существует до того, как он будет фактически определен.

Вопрос 3

Как мы даем предварительное объявление для функций?

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

Вопрос 4

Напишите прототип для этой функции (используйте предпочтительную форму с именами):

Вопрос 5

Для каждой из следующих программ укажите, завершится ли их компиляция неудачей, завершится ли их линковка неудачей, или будут успешными и компиляция, и линковка. Если не уверены, попробуйте их скомпилировать!

Не компилируется. Компилятор будет жаловаться, что add() , вызываемая в main() , не имеет того же количества параметров, что и та, что была предварительно объявлена.

Не компилируется. Компилятор будет жаловаться, что не может найти подходящую функцию add() , которая принимает 3 аргумента, потому что функция add() , которая была предварительно объявлена, принимает только 2 аргумента.

Не линкуется. Компилятор сопоставит предварительно объявленный прототип add с вызовом функции add() в main() . Однако функция add() , которая принимает два параметра, никогда не была реализована (мы реализовали только ту, которая принимает 3 параметра), поэтому компоновщик будет жаловаться.

Компилируется и линкуется. Вызов функции add() соответствует прототипу, который был предварительно объявлен, реализованная функция также совпадает.

В чём разница между определением, объявлением, реализацией и инициализацией?

В чём разница между определением, объявлением, реализацией и инициализацией?

Если совсем кратко:

Явная и неявная инициализация переменных

Если при определении переменной не происходит явного присвоения ей какого-либо значения (т.е. не происходит явной инициализации), то компилятор может проинициализировать её сам в соответствии со следующими правилами:

  1. переменные со статическим временем существования (глобальные, в пространствах имен и статические) инициализируются нулем;
  2. автоматические переменные не инициализируются (имеют случайное значение той области, которая была выделена под переменную на стеке);
  3. динаммические переменные не инициализируются (имеют случайное значение той области памяти, которая была выделена под объект в куче).

Стоит заметить, что в большинстве реализаций в debug-версии под автоматические переменные область стека (stack) заполняется значениями 0xCCCCCCCC , а область кучи (heap) заполняется 0xCDCDCDCD , что может позволить при отладке выявлять использование неинициализированных переменных.

Объявление и определение

Стоит заметить, что объявлений может быть сколь угодно много, а определение лишь одно (за исключением inline функций, которые при этом не нарушают ODR).

StateItPrimitive's user avatar

Хоть вопрос и старый, но я вставлю свои 5 копеек. @StateItPrimitive утверждает, что объявлений может быть сколь угодно много, а определение лишь одно. Но ведь существуют weak -функции, которые могут иметь сколь угодно определений. Например, в этом случае:

скомпилируются обе функции, но линковщик выбросит «слабую». В этом случае:

линковщик не сможет выбрать правильную функцию и выдаст ошибку. В этом случае:

всё в порядке, ошибок нет. Слабое связывание применяется, в основном, в низкоуровневом программировании при создании обработчиков прерываний. Обычно в библиотеках, работающих с ядром, создаются обработчики прерываний по умолчанию, и они определяются как weak . При этом программист может написать свой обработчик, который никак не будет конфликтовать с обработчиком по умолчанию.

UPD. Насчет стандарта не знаю, не читал (всё-таки денег стоит), но в документации по gcc это есть.

2.3. Объявления, определения и описания в языке Си

Существуют различные термины в языке Си, такие как, объявления, определения и описания. В литературе присутствует некоторая терминологическая путаница между этими понятиями, связанная, в том числе, и с переводом английских терминов, частично являющимися синонимами, на русский язык.

Для однозначной интерпретации данных терминов будем понимать их следующим образов.

Объявление объекта (переменной) — это оператор, позволяющий устанавливать атрибуты объекта, резервировать для него память, связывать объект с идентификатором (именем) или делать известным компилятору идентификатор объекта.

Прежде чем использовать объект в программе, он должен быть объявлен. Объявления делятся на два класса: определения и описания.

Определение – это разновидность объявления, которое устанавливает атрибуты объекта (переменной), резервирует для него память и связывает объект с идентификатором (именем).

Описание – это разновидность объявления, которое делает известным компилятору идентификатор объекта.

Определять объект можно только один раз, описывать множество раз. Рассмотрим отличительные признаки определений от описания применительно для переменных.

Признаки определения переменных:

объявлена переменная без модификатора extern;

объявлена переменная с инициализатором.

Признаки описания переменных:

наличие модификатора extern (инициализация запрещена).

2.4. Объявления переменных стандартных типов

Общий формат объявления переменных:

[<спецификатор_класса_памяти>] [<модификатор>] <тип> <имя1>[=<инициализатор1>], <имя2>[=<инициализатор2>],…, <имяN>[=<инициализаторN>];

Необязательный элемент <спецификатор_класса_памяти> (необязательный элемент далее в записи форматов будет помещаться в квадратные скобки) одно из ключевых слов: auto, register, static, extern.

Необязательные элемент <модификатор> — могут быть ключевые слова: const, volatile и др.

Модификатор const означает, что инициализация (присвоение начального значения) переменной обязательна и далее значение переменной изменять нельзя. Модификатор volatile означает, что данная переменная может быть изменена где- то в другом месте, а не обязательно в этой программе, например, программой обработки прерываний (модификатор влияет на режим оптимизацию работы компилятора).

const float pi=3.14; // Далее запрещено изменять значение pi

extern double x; // Это есть описание, определение переменной в другом месте.

unsigned char C1=‘A’, C2=10, C3;

2.5. Преобразования типов

Преобразование типа выполняется в случае, когда некоторое значение одного типа (значение переменной или результат выражения) присваивается переменной другого типа.

Преобразования типов могут выполняться неявно (по умолчанию) или существует операция явного преобразования типа.

Следует отметить, что язык Си – это язык со слабой типизацией, допустимы неявные преобразования любого стандартного типа в другой любой стандартный тип.

Неявные преобразования типов

char Ch=x; // Допустимо

unsigned char C1=L;

Сформулируем следующие правила неявного преобразования стандартных типов.

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

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

При преобразовании целых типов одинаковой длины, содержимое не меняется, может меняться интерпретация знакового бита

Преобразование менее длинного вещественного в более длинное: мантисса дополняется нулями справа.

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

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

Преобразование целых к вещественным: преобразование к long, далее к вещественному, возможна потеря точности.

Явное преобразование типов

Существует операция явного преобразования типа, ее формат:

Вторая форма может использоваться только, если имя типа задается одним словом, например, вторая форма не подходит для типа unsigned short и других подобных.

Похожие статьи