Ошибка на этапе линковки

Цитата
Сообщение от Kenny_Dalglish
Посмотреть сообщение

Как это?

линкеру не сообщили, что нужно линковаться с библиотекой.

Цитата
Сообщение от Kenny_Dalglish
Посмотреть сообщение

зве ошибок на этапе компиляции не должно было появится?

нет. ошибки линкера — это ошибки линковки, а не компиляции.

Цитата
Сообщение от Kenny_Dalglish
Посмотреть сообщение

Вроде бы все подключено.

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

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

Цитата
Сообщение от Kenny_Dalglish
Посмотреть сообщение

Если у Вас депрессия не обязательно это показывать в публичном месте.

а если у меня нет депрессии,
что тогда обязательно/не обязательно
мне делать в публичном/любом другом месте?

вы находитесь на форуме программистов, а не блондинок.
ваше условие — дырявое насквозь.
любой компилятор ткнул бы вас носом в:
«результат возвращается не на всех ветках if»

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

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

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

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

Содержание

  1. Причины ошибки на этапе линковки
  2. Последствия ошибки на этапе линковки
  3. Способы исправления ошибки на этапе линковки
  4. Важность исправления ошибки на этапе линковки
  5. Роль линковки в создании программного продукта

Причины ошибки на этапе линковки

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

  1. Отсутствие определения функции или переменной. Если компилятор не может найти нужную функцию или переменную, то будет генерирована ошибка линковки. Это может произойти, если вы забыли описать функцию или переменную, или если указали неправильное имя.
  2. Нестыковка аргументов функции. Если вы передаете аргументы функции, которые не соответствуют ее определению, компилятор может не быть в состоянии правильно связаться с функцией.
  3. Потерянная зависимость. Если один модуль программы зависит от другого модуля, и этот зависимый модуль был изменен или удален, то компилятор может не быть в состоянии правильно сгенерировать код для первого модуля.
  4. Проблемы с библиотеками или фреймворками. В случае использования внешних библиотек или фреймворков, возможны ошибки линковки из-за несовместимости версий, отсутствия нужных файлов или неправильной настройки среды разработки.

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

Последствия ошибки на этапе линковки

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

  • Неудачная компиляция: Наличие ошибок на этапе линковки может привести к тому, что программа не будет успешно скомпилирована. В результате, код программы не будет преобразован в исполняемый файл, и это может повлиять на возможность запуска программы.
  • Неработоспособность программы: Если процесс линковки содержит ошибки, это может привести к неработоспособности программы. Например, если при линковке не удается найти необходимые библиотеки или модули, программа может не иметь доступа к нужным функциям или ресурсам, что может привести к неожиданному завершению программы или ее некорректной работе.
  • Отказ в выполнении программы: Некоторые языки программирования могут отклонить запуск программы с ошибками на этапе линковки. Это может быть связано с обеспечением безопасности или контролем целостности системы. В таких случаях, разработчик программы должен исправить ошибки на этапе линковки, чтобы запустить программу на выполнение.

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

Способы исправления ошибки на этапе линковки

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

  1. Проверка наличия необходимых библиотек. Ошибка может возникнуть, если компилятор не может найти и подключить соответствующие библиотеки, необходимые для работы программы. В этом случае необходимо убедиться, что библиотеки установлены и доступны для компилятора. Если библиотеки отсутствуют, их можно установить либо из официального источника, либо использовать альтернативные библиотеки.
  2. Изменение опций компилятора. Ошибки на этапе линковки могут возникать из-за неправильных опций компилятора. Например, если программа использует сторонние библиотеки, необходимо указать компилятору путь к этим библиотекам при линковке. Также можно изменить опции компилятора, чтобы включить или исключить определенные функции или модули.
  3. Проверка и корректировка конфликтов имён. Ошибка на этапе линковки может возникнуть, если в программе используются одинаковые имена для разных объектов или функций. В этом случае необходимо переименовать объекты или функции так, чтобы они не конфликтовали между собой.
  4. Проверка зависимостей файлов. Ошибка может возникать, если не все необходимые файлы были включены в проект. Необходимо убедиться, что все файлы, необходимые для компиляции программы, присутствуют в проекте и правильно подключены.
  5. Проверка синтаксических ошибок и опечаток. Ошибки на этапе линковки также могут возникать из-за синтаксических ошибок или опечаток в коде программы. В данном случае необходимо внимательно просмотреть код и исправить все ошибки.
  6. Обновление версии компилятора или IDE. Некоторые ошибки на этапе линковки могут быть вызваны багами или неполадками в компиляторе или интегрированной среде разработки (IDE). В этом случае рекомендуется обновить версию компилятора или IDE до последней доступной версии.

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

Важность исправления ошибки на этапе линковки

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

Однако при линковке могут возникать ошибки, которые могут привести к неправильной работе программы или даже к его полному отказу. Это может быть вызвано неправильным использованием функций или переменных, отсутствием необходимых библиотек или неправильными настройками компилятора.

Исправление ошибки на этапе линковки имеет ряд важных преимуществ:

  1. Повышает стабильность программы. Если ошибка на этапе линковки не будет исправлена, то программа может работать неправильно или даже привести к аварийному завершению. Исправление ошибки позволяет обнаружить потенциальные проблемы и предотвратить сбои в работе программы.
  2. Упрощает отладку. Если ошибка на этапе линковки не исправлена, она может сделать процесс отладки более сложным. Исправление ошибки на этом этапе позволяет избежать необходимости искать причины неправильной работы программы во время отладки.
  3. Повышает переносимость программы. Если ошибка на этапе линковки не исправлена, программа может быть зависима от конкретного компилятора или операционной системы. Исправление ошибки позволяет убедиться, что программа будет работать правильно на разных системах и компиляторах.

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

Роль линковки в создании программного продукта

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

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

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

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

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

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

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

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

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

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

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

Содержание

  1. Ошибка на этапе линковки
  2. Причины ошибки на этапе линковки
  3. Способы исправления ошибки на этапе линковки

Ошибка на этапе линковки

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

Причины ошибок на этапе линковки:

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

Способы исправления ошибок на этапе линковки:

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

Выводы:

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

Причины ошибки на этапе линковки

Ошибки на этапе линковки — распространенное явление при разработке программного обеспечения. Они возникают, когда компилятор не может найти или правильно сопоставить все необходимые символы в вашем коде. Вот несколько частых причин ошибок на этапе линковки:

  1. Отсутствие определения символа: это может произойти, если вы забыли определить функцию, переменную или другой символ, который используется в вашей программе. Компилятор не сможет найти этот символ во время линковки и выдаст ошибку.

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

  3. Некорректное использование библиотек: если вы используете внешние библиотеки, то должны убедиться, что они правильно подключены к вашему проекту. Если библиотека не подключена или подключена некорректно, компилятор может не найти необходимые символы и выдаст ошибку линковки.

  4. Отсутствие зависимостей: если ваш проект состоит из нескольких модулей или файлов, то компилятор должен знать, как они связаны друг с другом. Если вы не указали правильные зависимости между модулями, компилятор не сможет провести линковку правильно и выдаст ошибку.

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

Способы исправления ошибки на этапе линковки

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

Чтобы исправить ошибки на этапе линковки, можно применить следующие подходы:

  1. Убедиться в правильности написания кода. Ошибки могут возникать из-за опечаток, неправильного использования синтаксиса или неверных имен функций или переменных. Проверьте весь код на наличие опечаток и синтаксических ошибок.
  2. Проверить наличие и правильность заголовочных файлов. Некоторые ошибки на этапе линковки могут быть вызваны неправильным использованием заголовочных файлов. Убедитесь, что вы подключили все необходимые заголовки, и что они содержат правильные объявления функций и переменных.
  3. Проверить правильность использования библиотек. Если вы используете сторонние библиотеки, убедитесь, что вы правильно указали пути к файлам библиотек и что они доступны компилятору.
  4. Изменить параметры компиляции и линковки. В некоторых случаях, ошибки на этапе линковки могут возникать из-за неправильных параметров компиляции или линковки. Измените параметры компиляции и линковки, чтобы убедиться, что они соответствуют требованиям вашего проекта.
  5. Обратиться к документации и сообществу разработчиков. Если вы не можете найти и исправить ошибку самостоятельно, обратитесь к документации по вашему компилятору или к сообществу разработчиков для получения помощи и рекомендаций.

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

Что делать если возникают ошибки линковки?

Здравствуйте. У меня вопрос. Почему вылезает эта фигня, после плохой сборки и потом от нее не избавиться, даже если код абсолютно правильный. Ошибки:
1):-1: ошибка: cannot open output file debug\Project1.exe: Permission denied
2)collect2.exe:-1: ошибка: error: ld returned 1 exit status

Вроде бы это ошибка линковщика. Если перезапустить комп, то проблемы исчезнут. Как решить эту проблему.Очень напрягает. Большое спасибо.


  • Вопрос задан

  • 617 просмотров

Пригласить эксперта

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


  • Показать ещё
    Загружается…

22 сент. 2023, в 18:32

5000 руб./за проект

22 сент. 2023, в 18:03

60000 руб./за проект

22 сент. 2023, в 17:11

5000 руб./за проект

Минуточку внимания

Цитата
Сообщение от Kenny_Dalglish
Посмотреть сообщение

Как это?

линкеру не сообщили, что нужно линковаться с библиотекой.

Цитата
Сообщение от Kenny_Dalglish
Посмотреть сообщение

зве ошибок на этапе компиляции не должно было появится?

нет. ошибки линкера — это ошибки линковки, а не компиляции.

Цитата
Сообщение от Kenny_Dalglish
Посмотреть сообщение

Вроде бы все подключено.

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

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

Цитата
Сообщение от Kenny_Dalglish
Посмотреть сообщение

Если у Вас депрессия не обязательно это показывать в публичном месте.

а если у меня нет депрессии,
что тогда обязательно/не обязательно
мне делать в публичном/любом другом месте?

вы находитесь на форуме программистов, а не блондинок.
ваше условие — дырявое насквозь.
любой компилятор ткнул бы вас носом в:
«результат возвращается не на всех ветках if»

Добавлено 17 марта 2021 в 22:08

Продолжим обсуждение диаграммы из предыдущей статьи «0.4 – Введение в разработку на C++»:

Рисунок 1 Процесс разработки программного обеспечения

Рисунок 1 – Процесс разработки программного обеспечения

Обсудим шаги 4–7.

Шаг 4. Компиляция исходного кода

Чтобы скомпилировать программу на C++, мы используем компилятор C++, который последовательно просматривает каждый файл исходного кода (.cpp) в вашей программе и выполняет две важные задачи:

Сначала он проверяет ваш код, чтобы убедиться, что он соответствует правилам языка C++. В противном случае компилятор выдаст вам ошибку (и номер соответствующей строки), чтобы помочь точно определить, что нужно исправить. Процесс компиляции будет прерван, пока ошибка не будет исправлена.

Во-вторых, он переводит исходный код C++ в файл машинного кода, называемый объектным файлом. Объектные файлы обычно имеют имена name.o или name.obj, где name совпадает с именем файла .cpp, из которого он был создан.

Если бы в вашей программе было бы 3 файла .cpp, компилятор сгенерировал бы 3 объектных файла:

Рисунок 2 Процесс компиляции

Рисунок 2 – Процесс компиляции

Компиляторы C++ доступны для многих операционных систем. Мы скоро обсудим установку компилятора, поэтому сейчас нет необходимости останавливаться на этом.

Шаг 5. Компоновка (линковка) объектных файлов и библиотек

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

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

Рисунок 3 Процесс компоновки (линковки)

Рисунок 3 – Процесс компоновки (линковки)

Во-вторых, помимо возможности связывать объектные файлы, компоновщик (линкер) также может связывать файлы библиотек. Файл библиотеки – это набор предварительно скомпилированного кода, который был «упакован» для повторного использования в других программах.

Ядро языка C++ на самом деле довольно небольшое и лаконичное (и вы узнаете многое о нем в последующих статьях). Однако C++ также поставляется с обширной библиотекой, называемой стандартной библиотекой C++ (обычно сокращенно «стандартная библиотека», или STL), которая предоставляет дополнительные функции, которые вы можете использовать в своих программах. Одна из наиболее часто используемых частей стандартной библиотеки C++ – это библиотека iostream, которая содержит функции для печати текста на мониторе и получения от пользователя ввода с клавиатуры. Почти каждая написанная программа на C++ в той или иной форме использует стандартную библиотеку, поэтому она часто подключается к вашим программам. Большинство компоновщиков автоматически подключают стандартную библиотеку, как только вы используете какую-либо ее часть, так что, как правило, вам не о чем беспокоиться.

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

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

Как только компоновщик завершит линковку всех объектных файлов и библиотек (при условии, что всё идет хорошо), вы получите исполняемый файл, который затем можно будет запустить!

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


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

Шаги 6 и 7. Тестирование и отладка

Это самое интересное (надеюсь)! Вы можете запустить исполняемый файл и посмотреть, выдаст ли он ожидаемый результат!

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

Интегрированные среды разработки (IDE)

Обратите внимание, что шаги 3, 4, 5 и 7 включают в себя использование программного обеспечения (редактор, компилятор, компоновщик, отладчик). Хотя для каждого из этих действий вы можете использовать отдельные программы, программный пакет, известный как интегрированная среда разработки (IDE), объединяет все эти функции вместе. Мы обсудим IDE и установим одну из них в следующем разделе.

Теги

C++ / CppLearnCppДля начинающихКомпиляторЛинкерОбучениеПрограммирование

Что такое компиляция?

Компиляция — преобразование одностороннее, нельзя восстановить исходный код.

Для того, чтобы скомпилировать программу на C++ для некоторой архитектуры X, необязательно устанавливать компилятор С++ на компьютер с архитектурой X.

Не каждая программа, написанная на компилируемом языке, переносима. Т.е. не любая программа, написанная на компилируемом языке, будет работать везде одинаково.

Плюсы и минусы компилируемости в машинный код

Плюсы:

  • эффективность: программа компилируется и оптимизируется для конкретного процессора;
  • нет необходимости устанавливать сторонние приложения, такие как интерпретатор или виртуальная машина (т.е. для запуска программы, написаной на компилируемом языке, не требуется установка компилятора).

Минусы:

  • нужно компилировать для каждой платформы (т.е. программу, написанную на языке, который компилируется в машинный код, недостаточно скомпилировать однажды чтобы её можно было запускать на любой платформе);
  • сложность внесения изменения в программу — нужно перекомпилировать заново.

Общая схема

g++ — это такая обёртка над несколькими программами:

  1. над препроцессором;
  2. над непосредственно компилятором;
  3. и над линковщиком.

g++ может сама решать, что вызывать, если не просить что-то делать специально, а просто изначально дать ей файлы, то g++ сама догадается что с ними нужно сделать.
g++ main.cpp square.cpp -o program

Этап 1: препроцессор

Язык препроцессора – это специальный язык программирования, встроенный в C++. Препроцессор работает с кодом на C++ как с текстом.

Команды языка препроцессора называют директивами, все директивы начинаются со знака #. Директива #include позволяет подключать заголовочные файлы к файлам кода.

  • #include <foo.h> — библиотечный заголовочный файл,
  • #include «bar.h» — локальный заголовочный файл.

Препроцессор заменяет директиву #include «bar.h» на содержимое файла bar.h.

Можно попросить компилятор вызвать только препроцессор и посмотреть что получится. Для компилятора g++ можно использовать ключ -E.

  • g++ -E square.cpp -o square_preprocessed.cpp
  • g++ -E main.cpp -o main_preprocessed.cpp

Этап 2: компиляция

На вход компилятору поступает код на C++ после обработки препроцессором.

Каждый файл с кодом компилируется отдельно и независимо от других файлов с кодом. Компилируется только файлы с кодом (т.е. *.cpp).

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

На выходе компилятора из каждого файла с кодом получается “объектный файл” — бинарный файл со скомпилированным кодом (с расширением .o или .obj).

Если в коде C++ вы вызывает не объявленную функцию, то это ошибка этапа компиляции.

Можно «скормить» файлы с кодом непосредственно компилятору. Для компилятора g++ можно использовать ключ -c.

  • g++ -c main.cpp 
  • g++ -c square.cpp 

На выходе получается файлы с расширением .o — это объектные файлы.

  • main.o
  • square.o

Можно попросить компилятор g++ показать некоторое содержимое объектных файлов, но сам по себе смотреть объектный файл не интересно (там бинарная информация), но можно посмотреть его ассемблированный вид.
g++ -S square.cpp
Получится файл square.s.

Этап 3: линковка (компоновка)

На этом этапе все объектные файлы объединяются в один исполняемый (или библиотечный) файл. При этом происходит подстановка адресов функций в места их вызова.

По каждому объектному файлу строится таблица всех функций, которые в нём определены.

На этапе компоновки важно, что каждая функция имеет уникальное имя. В C++ может быть две функции с одним именем, но разными параметрами. Имена функций искажаются (mangle) таким образом, что в их имени кодируются их параметры.

Например, компилятор GCC превратит имя функции foo

в _Z3fooid. Компилятор g++ также предоставляет возможность обратного преобразования.
c++filt -n _Z3fooid
foo(int, double)

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

Аналогично функциям в линковке нуждаются глобальные переменные.

Точка входа — функция, вызываемая при запуске программы. По умолчанию — это функция main:

или

int main(int argc, char ** argv)
{
 return 0;
}

Даже для программы, состоящей всего из одного файла и из одной пустой функции int main() { return 0; } все равно требуется ликовка.

Если в коде C++ вы вызываете функцию, которая была объявлена, но не была определена, то это ошибка этапа линковки.

Для того чтобы собрать объектные файлы в один файл их нужно «скормить» компилятору и указать имя исполняемого файла:
g++ square.o main.o -o program

Лекция 3

Многофайловая компоновка

Говорят, что переменная описана (определена), если она объявлена и под неё
выделена память. Рассмотрим следующий пример, пусть в проекте есть два файла:

/* a.cpp */
extern int n;    // "Где-то дальше определено"
void f(int);     // Предварительное объявление

void main() {
    n = 5; f(n);
}


/* b.cpp */
#include <iostream>
int i;           // Описание переменной

// Определение функции
void f(int i) {
    std::cout << i;
}

В C++ имеет место независимая компиляция: все файлы проекта компилируются
независимо один от другого. Компиляция состоит из этапа собственно компиляции
и этапа линковки.

Описание процесса сборки программы

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

Содержат заголовки всех функций и объявления переменных, обычно имеют
расширение *.h (header). Теперь можно вынести объявления функций из всех
файлов в один (заголовочный):

/* myheader.h */
extern int n;
void f(int);

/* myheader.cpp */
#include <iostream>
int n;

void f(int i) {
    std::cout << i;
}


/* a.cpp */
#include "myheader.h"

void main() {
    n = 5; f(n);
}

/* b.cpp */
#include "myheader.h"

void makeZero() {
    n = 0;
}

Имена пользовательских заголовочных файлов в директиве include
заключаются в двойные кавычки, а имена стандартных заголовочных файлов —
в угловые скобки. Стандартные заголовочные файлы расположены в /INCLUDE.
Поиск пользовательских файлов производится в текущем каталоге.

Замечание: inline-функции не сохраняются в исходном коде, так как больше не
используются (а сразу встраиваются на место вызова). Чтобы воспользоваться
такими функциями в другой единице компиляции, их нужно поместить в
заголовочный файл.

Содержимое заголовочных файлов

Что может содержать заголовочный файл:

Пример
Определения типов struct point { int x, y; };
Шаблоны типов template<class T> class V { /* ... */ }
Описания функций extern int strlen(const char*);
Определения встраиваемых функций inline char get() { return *p++; }
Описания данных extern int a;
Определения констант const float pi = 3.141593;
Перечисления enum bool { false, true };
Описания имен class Matrix;
Команды включения файлов #include <signal.h>
Макроопределения #define Case break;case
Комментарии /* проверка на конец файла */

В заголовочном файле никогда не должно быть:

Пример
Определений обычных функций char get() { return *p++; }
Определений данных int a;
Определений составных констант const tb[i] = { /* ... */ };

Глобальные описания в C++ и необходимость пространств имен

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

namespace MyNamespace{
    ... // содержимое пространства имён: типы, функции, что-угодно…
}

Пространства имен и заголовочные файлы

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

/* header.h */
namespace MyNamespace{
    extern int n;
    void f();
}

Основные этапы сборки проекта

  1. Препроцессирование.
  2. Компиляция каждого *.cpp-файла в объектный код (файлы *.obj или *.o).
  3. Линковка — сборка всех объектных файлов в один исполняемый
    (*.exe или ELF).

Ошибки во время линковки

  • Одинаковые объявления в одном пространстве имен.
  • Ошибки при использовании #include "*.cpp" (грубая ошибка).
  • Отсутствие описания функции.
  • Отсутствие main() во всех файлах проектах.
  • Несколько объявлений main().

Особенности линковки

  • Константы имеют внутреннюю линковку.
  • inline-функции «погибают» при компиляции.

При сборке программы с включенными опциями для GCOV можно получить ошибку на этапе линковки следующего вида:

/usr/bin/ld: ./a.out: hidden symbol `__gcov_merge_add’ in /usr/lib/gcc/i486-linux-gnu/4.1.3/libgcov.a(_gcov_merge_add.o) is referenced by DSO
/usr/bin/ld: final link failed: Nonrepresentable section on output

Скорее всего причина заключается в том, что к исполняемому модулю линкуются библиотеки, которые тоже были скомпилированы с опциями -fprofile-arcs -ftest-coverage.

Простой пример, иллюстрирующий проблему.

Даны два файла:

=== a.cpp
#include <stdio.h>

extern unsigned long myfunc ();

int main (int argc, char **argv)
{
  unsigned long z = myfunc();
  printf("%08xn", z);
  return 0;
}

=== b.cpp
#include <time.h>

unsigned long myfunc ()
{
  return time(0);
}

Из файла b.cpp делаем статическую библиотеку libmy.a, а из файла a.cpp — исполняемый модуль a.out:

g++ -c -fprofile-arcs -ftest-coverage b.cpp
g++ -shared -o libmy.a ./b.o
g++ -c -fprofile-arcs -ftest-coverage a.cpp
g++ -o a.out a.o -L. -lmy

Получаем ошибку вида «undefined reference to `__gcov_init’» — забыли подключить библиотеку libgcov.a.

Подключаем к сборке требуемую библиотеку как обычно:

g++ -c -fprofile-arcs -ftest-coverage b.cpp
g++ -shared -o libmy.a ./b.o
g++ -c -fprofile-arcs -ftest-coverage a.cpp
g++ -o a.out a.o -L. -lmy -lgcov

Вот здесь-то и получаем странную ошибку:

/usr/bin/ld: ./a.out: hidden symbol `__gcov_merge_add' in /usr/lib/gcc/i486-linux-gnu/4.1.3/libgcov.a(_gcov_merge_add.o) is referenced by DSO
/usr/bin/ld: final link failed: Nonrepresentable section on output

Исправить это просто. Нужно подключить библиотеку libgcov.a не только к сборке исполняемого модуля, но и к сборке статической библиотеки:

g++ -c -fprofile-arcs -ftest-coverage b.cpp
g++ -shared -o libmy.a ./b.o -lgcov
g++ -c -fprofile-arcs -ftest-coverage a.cpp
g++ -o a.out a.o -L. -lmy -lgcov

Вот и всё. Так нужно поступить с каждой библиотекой, исходный код которой скомпилирован с опциями -fprofile-arcs -ftest-coverage.

Процесс компиляции программ

  • Запись лекции №1
  • Запись лекции №2
  • Запись лекции №3
  • Практика

Зачем нам нужно это изучать?

  • У студентов часто возникают с этим проблемы — когда компилятор пишет ошибку, а человек не понимает, что ему говорят.
  • Если вы делаете ошибку в организации программы, причём такую ошибку, которая сразу к проблеме не приводит, то бывает такое, что при компиляции чуть-чуть по-другому всё сломается. Причём даже в крупных компаниях
    такое случается.

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

Базовые знания об этапах компиляции.

Обычно мы компилируем программу как g++ program.cpp. А вот чего мы пока не знаем, так это того, что g++ не делает всю работу самостоятельно, а вызывает другие команды, которые выполняют компиляцию по частям. И если посмотреть, что там, то происходит cc1plus, потом as, в конце collect2, который вызывает ld. Давайте попытаемся это повторить.

Дальше будет перечисление стадий с указанием двух моментов: как их можно выполнить руками и какое расширение обычно имеет результат этой стадии.

  1. Препроцессирование. Выполняется при помощи g++ -E (если дополнительно передать ключ -P, то вывод будет чуть короче), выходной файл обычно имеет расширение .i. На файл с расширением .i можно и глазами посмотреть — в нём будет куча текста вместо #include, а потом наш код. Собственно, #include — директива препроцессора, которая тупо вставляет указанный файл в то место, где написана. Также препроцессор занимается макросами (#define). О них позже.
  2. Трансляция. Выполняется при помощи g++ -S, выходной файл обычно имеет расширение .s. «Трансляция» — это (с английского) «перевод». Кого и куда переводим? Наш язык в ассемблер. Если передать параметр -masm=intel, можно уточнить, в какой именно ассемблер переводить (как было сказано в 01_asm, ассемблеры отличаются в зависимости от инструмента).
  3. Ассемблирование. Выполняется специальной утилитой as, выходной файл обычно имеет расширение .o (и называется объектным файлом). На данном этапе не происходит ничего интересного — просто инструкции, которые были в ассемблере, перегоняются в машинный код. Поэтому файлы .o бесполезно смотреть глазами, они бинарные, для этого есть специальные утилиты, например, objdump. Про него будет рассказано чуть позже.
  4. Линковка. Выполняется простым вызовом g++ от объектного файла. На выходе даёт исполняемый файл. Нужна, если файлов несколько: мы запускаем препроцессор, трансляцию и ассемблирование независимо для каждого файла, а объединяются они только на этапе линковки. Независимые .cpp файлы называют единицами трансляции. Разумеется, только в одной единице должен быть main. В этом main‘е, кстати, можно не делать return 0, его туда вставит компилятор.
    Сто́ит сказать, что информация о линковке верна до появления модулей в C++20, где можно доставать данные одного файла для другого. Там появляется зависимость файлов друг от друга, а значит компилировать их надо в определённом порядке.

Классическая схема этапов компиляции выглядит так:

Compilation graph

Есть похожая статья на хабре по теме.

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

Очень хочется слинковать вот это:

// a.cpp:
int main() {
	f();
}
// b.cpp:
#include <cstdio>

void f() {
	printf("Hello, world!n");
}

Это не компилируется, а точнее ошибка происходит на этапе трансляции a.cpp. В тексте ошибки написано, что f не определена в области видимости. Всё потому, что для того, чтобы вызвать функцию, надо что-то про неё знать. Например, если мы передаём в функцию int — это один ассемблерный код, а если double — то совершенно другой (потому что разные calling convention’ы могут быть). Поэтому на этапе трансляции нужно знать сигнатуру функции. Чтобы указать эту сигнатуру, в C++ есть объявления:

// a.cpp:			
void f(); // Вот это объявление.

int main() {
	f();
}
// b.cpp:
#include <cstdio>

void f() {
	printf("Hello world");
}

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

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

#include <cstdio>

int main() {
	f();
}

void f() {
	printf("Hello, worldn");
}

Это не компилируется, и дело в том, что компилятор смотрит файл сверху вниз, и когда доходит до вызова функции f внутри main, он ещё не дошёл до её определения. Тут можно переставить функции местами, да, но если у нас есть взаиморекурсивные функции, то там переставить их не получится — только написать декларацию.

Ошибки линковки. Инструменты nm и objdump. Ключевое слово static.

Рассмотрим такой пример:

// a.cpp
#include <cstdio>

void f()
{
	printf("Hello, a.cpp!n");
}
// b.cpp
#include <cstdio>

void f()
{
	printf("Hello, b.cpp!n");
}
// main.cpp
void f();

int main()
{
	f();
}

Тут вам на этапе линковки напишут, что функция f() определяется дважды. Чтобы красиво посмотреть, как это работает, можно использовать утилиту nm. Когда вы сгенерируете a.o и вызовете nm -C a.o, то увидите что-то такое:

                 U puts
0000000000000000 T f()

Что делает ключ -C, оставим на потом. На то что тут находится puts вместо printf, тоже обращать внимание не надо, это просто такая оптимизация компилятора — когда можно заменить printf на puts, заменяем.
А обратить внимание надо на то, что puts не определена (об этом нам говорит буква U), а функция f() — определена в секции .text (буква T). У main.cpp, понятно, будет неопределённая функция f() и определённая main. Поэтому, имея эти объектные файлы, можно слинковать main.cpp и a.cpp. Или main.cpp и b.cpp. Без перекомпиляции. Но нельзя все три вместе, ведь f() будет определена дважды.

Если мы хотим посмотреть на объектные файлы поподробнее, нам понадобится утилита objdump. У неё есть бесчисленное много ключей, которые говорят, что мы хотим увидеть. Например -x — выдать вообще всё. Нам сейчас нужно -d — дизассемблирование и -r — релокации. Когда мы вызовем objdump -dr -Mintel -C main.o, мы увидим, что на месте вызова функции f находится call и нули. Потому что неизвестно, где эта функция, надо на этапе линковки подставить её адрес. А чтобы узнать, что именно подставить, есть релокации, которые информацию об этом и содержат. В общем случае релокация — информация о том, какие изменения нужно сделать с программой, чтобы файл можно было запустить.

Давайте теперь вот на что посмотрим. Пусть в нашем файле определена функция f(). И где-то по случайному совпадению далеко-далеко также определена функция f(). Понятно, что оно так не слинкуется. Но мы можем иметь в виду, что наша функция f нужна только нам и никак наружу не торчит. Для этого имеется специальный модификатор: static. Если сделать на такие функции nm, то можно увидеть символ t вместо T, который как раз обозначает локальность для единицы трансляции. Вообще функции, локальные для одного файла сто́ит помечать как static в любом случае, потому что это ещё помогает компилятору сделать оптимизации.

Глобальные переменные.

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

extern int x; // Объявление.

int x;        // Определение.

И точно также в глобальных переменных можно писать static. А теперь пример:

// a.cpp
extern int a;

void f();

int main()
{
	f();
	a = 5;
	f();
}
// b.cpp
#include <cstdio>

int a;

void f()
{
	printf("%dn", a);
}

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

Декорирование имён. extern "C".

Обсуждённая нами модель компиляции позволяет использовать несколько разных языков программирования. Пока ЯП умеет транслироваться в объектные файлы, проблемы могут возникнуть только на этапе линковки. Например, никто не мешает вам взять уже готовый ассемблерник и скомпилировать его с .cpp файлом. Но в вызове ассемблера есть одна проблема. Тут надо поговорить о такой вещи как extern "C". В языке C всё было так: имя функции и имя символа для линковщика — это одно и то же. Если мы скомпилируем файл

// a.c <-- C, не C++.
void foo(int)
{
	// ...
}

То имя символа, которое мы увидим в nm будет foo. А в C++ появилась перегрузка функций, то есть void foo(int) и void foo(double) — это две разные функции, обе из которых можно вызывать. Поэтому одно имя символа присвоить им нельзя. Так что компилятор mangle’ит/декорирует имена, то есть изменяет их так, чтобы символы получились уникальными. nm даже может выдать вам эти имена (в данном случае получится _Z3fooi и _Z3food). Но у вас есть и возможность увидеть их по-человечески: для этого существует уже упомянутый ключ -C, который если передать программе nm, то она раздекорирует всё обратно и выдаст вам имена человекочитаемо. objdump‘у этот ключ дать тоже можно. А ещё есть утилита
c++filt, которая по имени символа даёт сигнатуру функции.

Так вот, extern "C" говорит, что при линковке нам не нужно проводить декорацию. И если у нас в ассемблерном файле написано fibonacci:, то вам и нужно оставить имя символа как есть:

extern "C" uint32_t fibonacci(uint32_t n);

У функций, имеющих разные сигнатуры, но помеченных как extern "C", после компиляции не будет информации о типах их аргументов, поэтому это слинкуется, но работать не будет (ну либо будет, но тут UB, так как, например, типы аргументов ожидаются разные).

Линковка со стандартной библиотекой.

Возьмём теперь объявление printf из cstdio и вставим его объявление вручную:

extern "C" int printf(const char*, ...);

int main() {
	printf("Hello, world!");
}

Такая программа тоже работает. А где определение printf, возникает вопрос? А вот смотрите. На этапе связывания
связываются не только ваши файлы. Помимо этого в параметры связывания добавляются несколько ещё объектных файлов и несколько библиотек. В нашей модели мира хватит информации о том, что библиотека — просто набор объектных файлов. И вот при линковке вам дают стандартную библиотеку C++ (-lstdc++), математическую библиотеку (-lm), библиотеку -libgcc, чтобы если вы делаете арифметику в 128-битных числах, то компилятор мог вызвать функцию __udivti3 (деление), и кучу всего ещё. В нашем случае нужна одна — -lc, в которой и лежит printf. А ещё один из объектных файлов, с которыми вы линкуетесь, содержит функцию _start (это может быть файл crt1.o), которая вызывает main.

Headers (заголовочные файлы). Директива #include.

Если мы используем одну функцию во многих файлах, то нам надо писать её сигнатуру везде. А если мы её меняем, то вообще повеситься можно. Поэтому так не делают. А как делают? А так: декларация выделяется в отдельный файл. Этот файл имеет расширение .h и называется заголовочным. По сути это же происходит в стандартной библиотеке. Подключаются заголовочные файлы директивой #include <filename>, если они из какой-то библиотеки, или #include "filename", если они ваши. В чём разница? Стандартное объяснение — тем, что треугольные скобки сначала ищут в библиотеках, а потом в вашей программе, а кавычки — наоборот. На самом деле у обоих вариантов просто есть список путей, где искать файл, и эти списки разные.

Но с заголовками нужно правильно работать. Например, нельзя делать #include "a.cpp". Почему? Потому что все определённые в a.cpp функции и переменные просочатся туда, куда вы его подключили. И если файл у вас один, то ещё ничего, а если больше, то в каждом, где написано #include "a.cpp", будет определение, а значит определение одного и того же объекта будет написано несколько раз.
Аналогичный эффект будет, если писать определение сразу в заголовочном файле, не надо так.

К сожалению, у директивы #include есть несколько нюансов.

Предотвращение повторного включения.

Давайте поговорим про структуры. Что будет, если мы в заголовочном файле создадим struct, и подключим этот файл? Да ничего. Абсолютно ничего. Сгенерированный ассемблерный код будет одинаковым. У структур нет определения по сути, потому что они не генерируют код. Поэтому их пишут в заголовках. При этом их методы можно (но не нужно) определять там же, потому что они воспринимаются компилятором как inline. А кто такой этот inline и как он работает — смотри дальше. Но со структурами есть один нюанс. Рассмотрим вот что:

// a.cpp:
#include "y.h" //    -->    `struct x{};`.
#include "z.h" //    -->    `struct x{};` ошибка компиляции, повторное определение.

Стандартный способ это поправить выглядит так:

// x.h:
#ifndef X_H // Если мы уже определили макрос, то заголовок целиком игнорируется.
#define X_H	// Если не игнорируется, то помечаем, что файл мы подключили.

struct x {};

#endif // В блок #ifndef...#endif заключается весь файл целиком.

Это называется include guard. Ещё все возможные компиляторы поддерживают #pragma once (эффект как у include guard, но проще). И на самом деле #pragma once работает лучше, потому что не опирается на имя файла, например. Но его нет в стандарте, что грустно.

Есть один нюанс с #pragma once‘ом. Если у вас есть две жёстких ссылки на один файл, то у него проблемы. Если у вас include guard, то интуитивно понятно, что такое разные файлы — когда макросы у них разные. А вот считать ли разными файлами две жёстких ссылки на одно и то же — вопрос сложный. Другое дело, что делать так, чтобы источники содержали жёсткие
или символические ссылки, уже довольно странно.

Forward-декларации.

// a.h
#ifndef A_H
#define A_H

#include "b.h" // Nothing, then `struct b { ... };`

struct a {
	b* bb;
};
#endif
// b.h
#ifndef B_H
#define B_H

#include "a.h" // Nothing, then `struct a { ... };`

struct b {
	a* aa;
};
#endif
// main.cpp
#include "a.h" // `struct b { ... }; struct a { ... };`
#include "b.h" // Nothing.

Понятно, в чём проблема заключается. Мы подключаем a.h, в нём — b.h, а в нём, поскольку мы уже зашли в a.h, include guard нам его блокирует. И мы сначала определяем структуру b, а потом — a. И при просмотре структуры b, мы не будем знать, что такое a.

Для этого есть конструкция, называемая forward-декларацией. Она выглядит так:

// a.h
#ifndef A_H
#define A_H

struct b;

struct a {
	b* bb;
};
#endif
// b.h
#ifndef B_H
#define B_H

struct a;

struct b {
	a* aa;
};
#endif

Чтобы завести указатель, нам не надо знать содержимое структуры. Поэтому мы просто говорим, что b — это некоторая структура, которую мы дальше определим.

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

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

Пока структуру не определили, структура — это incomplete type. Например, на момент объявления struct b; в коде выше, b — incomplete. Кстати, в тот момент, когда вы находитесь в середине определения класса, он всё ещё incomplete.
Все, что можно с incomplete типами делать — это объявлять функции с их использованием и создавать указатель. Incomplete тип становится полным типом после определения.
Пока что информация об incomplete-типах нам ни к чему, но она выстрелит позже.

Правило единственного определения.

А теперь такой пример:

// a.cpp
#include <iostream>

struct x {
	int a;
	// padding
	double b;
	int c;
	int d;
};

x f();

int main() {
	x xx = f();
	std::cout << xx.a << " "
	          << xx.b << " "
	          << xx.c << " "
	          << xx.d << std::endl;
}
// b.cpp
struct x {
	int a;
	int b;
	int c;
	int d;
	int e;
};

x f() {
	x result;
	result.a = 1;
	result.b = 2;
	result.c = 3;
	result.d = 4;
	result.e = 5;
	return result;
};

Тут стоит вспомнить, что структуры при линковке не играют никакой роли, то есть линковщику всё равно, что у нас структура x определена в двух местах. Поэтому такая программа отлично скомпилируется и запустится, но тем не менее она является некорректной. По стандарту такая программа будет работать неизвестно как, а по жизни данные поедут. А именно 2 пропадёт из-за выравнивания double, 3 и 4 превратятся в одно число (double), а 5 будет на своём месте, а x::e из файла a.cpp будет просто не проинициализирован. Правило, согласно которому так нельзя, называется one-definition rule/правило единственного определения. Кстати, нарушением ODR является даже тасовка полей.

Inlining.

int foo(int a, int b) {
	return a + b;
}

int bar(int a, int b) {
	return foo(a, b) - a;
}

Если посмотреть на ассемблерный код для bar, то там не будет вызова функции foo, а будет return b;. Это называется inlining — когда мы берём тело одной функции и вставляем внутрь другой как оно есть. Это связано, например, со стилем программирования в текущем мире (много маленьких функций, которые делают маленькие вещи) — мы убираем все эти абстракции, сливаем функции в одну и потом оптимизируем что там есть.

Но есть один нюанс…

Модификатор inline.

// a.c
void say_hello();

int main() {
	say_hello();
}
// b.c
#include <cstdio>

void say_hello() {
	printf("Hello, world!n");
}

Тут не произойдёт inlining, а почему? А потому что компилятор умеет подставлять тело функций только внутри одной единицы трансляции (так как inlining происходит на момент трансляции, а тогда у компилятора нет функций из других единиц).

Тут умеренно умалчивается, что модель компиляции, которую мы обсуждаем — древняя и бородатая. Мы можем передать ключ -flto в компилятор, тогда всё будет за’inline’ено. Дело в том, что при включенном режиме linking time optimization, мы откладываем на потом генерацию кода и генерируем его на этапе линковки. В таком случае линковка может занимать много времени, поэтому применяется при сборке с оптимизациями. Подробнее о режиме LTO — сильно позже.

Но тем не менее давайте рассмотрим, как без LTO исправить проблему с отсутствием inlining’а. Мы можем написать в заголовочном файле тело, это поможет, но это, как мы знаем, ошибка компиляции. Хм-м, ну, можно не только написать функцию в заголовочном файле, но и пометить её как static, но это даёт вам свою функцию на каждую единицу трансляции, что, во-первых, бывает просто не тем, что вы хотите, а во-вторых, кратно увеличивает размер выходного файла.

Поэтому есть модификатор inline. Он нужен для того, чтобы линковщик не дал ошибку нарушения ODR. Модификатор inline напрямую никак не влияет на то, что функции встраиваются. Если посмотреть на inline через nm, то там увидим W (weak) — из нескольких функций можно выбрать любую (предполагается, что все они одинаковые).

По сути inline — указание компилятору, что теперь за соблюдением ODR следите вы, а не он. И если ODR вы нарушаете, то это неопределённое поведение (ill-formed, no diagnostic required). ill-formed, no diagnostic required — это ситуация, когда программа некорректна, но никто не заставляет компилятор вам об этом говорить. Он может (у GCC есть такая возможность: если дать g++ ключи -flto -Wodr, он вам об этом скажет), но не обязан. А по жизни линковщик выберет произвольную из имеющихся функций (например, из первой единицы трансляции или вообще разные в разных местах):

// a.cpp
#include <cstdio>

inline void f() {
	printf("Hello, a.cpp!n");
}

void g();

int main() {
	f();
	g();
}
// b.cpp
inline void f() {
	printf("Hello, b.cpp!n");
}

void g() {
	f();
}

Если скомпилировать этот код с оптимизацией, обе функции f будут за’inline’ены, и всё будет хорошо. Если без, то зависит от порядка файлов: g++ a.cpp b.cpp может вполне выдавать Hello, a.cpp! два раза, а g++ b.cpp a.cppHello, b.cpp! два раза.

Если нужно именно за’inline’ить функцию, то есть нестандартизированные модификаторы типа __forceinline, однако даже они могут игнорироваться компилятором. Inlining функции может снизить производительность: на эту тему можно послушать доклад Антона Полухина на C++ Russia 2017.

Остальные команды препроцессора.

#include обсудили уже вдоль и поперёк. Ещё есть директивы #if, #ifdef, #ifndef, #else, #elif, #endif, которые дают условную компиляцию. То есть если выполнено какое-то условие, можно выполнить один код, а иначе — другой.

Определение макроса.

И ещё есть макросы: определить макрос (#define) и разопределить макрос (#undef):

#define PI 3.14159265
double circumference(double r) {
    return 2 * PI * r;
}

Текст, который идет после имени макроса, называется replacement. Replacement отделяется от имени макроса пробелом и распространяется до конца строки. Все вхождения идентификатора PI ниже этой директивы будут заменены на replacement. Самый простой макрос — object-like, его вы видите выше, чуть более сложный — function-like:

#define MIN(x, y) x < y ? x : y

printf("%d", MIN(4, 5));

Что нам нужно про это знать — макросы работают с токенами. Они не знают вообще ничего о том, что вы делаете. Вы можете написать

#include <cerrno>

int main() {
	int errno = 42;
}

И получить отрешённое от реальности сообщение об ошибке. А дело всё в том, что это на этапе препроцессинга раскрывается, например, так:

int main() {
	int (*__errno_location()) = 42;
}

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

Что ещё не видит препроцессор, так это синтаксическую структуру и приоритет операций. Более страшные вещи получаются, когда пишется что-то такое:

#define MUL(x, y) x * y

int main() {
	int z = MUL(2, 1 + 1);
}

Потому что раскрывается это в

int main() {
	int z = 2 * 1 + 1;
}

Это не то, что вы хотите. Поэтому когда вы такое пишите, нужно, во-первых, все аргументы запихивать в скобки, во-вторых — само выражение тоже, а в-третьих, это вас никак не спасёт от чего-то такого:

#define max(a, b) ((a) < (b) ? (a) : (b))

int main() {
	int x = 1;
	int y = 2;
	int z = max(x++, ++y);
}

Поэтому перед написанием макросов три раза подумайте, нужно ли оно, а если нужно, будьте очень аккуратны. А ещё, если вы используете отладчик, то он ничего не знает про макросы, зачем ему знать. Поэтому в отладчике написать «вызов макроса» Вы обычно не можете. Cм. также FAQ Бьярна Страуструпа о том, почему макросы — это плохо.

Ещё #define позволяет переопределять макросы.

#define STR "abc"
const char* first = STR; // "abc".
#define STR "def"
const char* second = STR; // "def".

Replacement макроса не препроцессируется при определении макроса, но результат раскрытия макроса препроцессируется повторно:

#define Y foo
#define X Y   // Это не `#define X foo`.
#define Y bar // Это не `#define foo bar`.
X             // Раскрывается `X` -> `Y` -> `bar`.

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

#define M { M }
M   // Раскрывается в { M }.

Ещё пример:

#define A a{ B }
#define B b{ C }
#define C c{ A }
A // a{ b{ c{ A } } }
B // b{ c{ a{ B } } }
C // c{ a{ b{ C } } }

Условная компиляция. Проверка макроса.

Директивы #ifdef, #ifndef, #if, #else, #elif, #endif позволяют отпрепроцессировать часть файла, лишь при определенном условии. Директивы #ifdef, #ifndef проверяют определен ли указанный макрос. Например, они полезны для разной компиляции:

#ifdef __x86_64__
typedef unsigned long uint64_t;
#else
typedef unsigned long long uint64_t;
#endif

Директива #if позволяет проверить произвольное арифметическое выражение.

#define TWO 2
#if TWO + TWO == 4
// ...
#endif

Директива #if препроцессирует свой аргумент, а затем парсит то, что получилось как арифметическое выражение. Если после препроцессирования в аргументе #if остаются идентификаторы, то они заменяются на 0, кроме идентификатора true, который заменяется на 1.

Одно из применений #ifndef — это include guard, которые уже обсуждались ранее.

Константы.

Понадобилась нам, например, $pi$. Традиционно в C это делалось через #define. Но у препроцессора, как мы знаем, есть куча проблем. В случае с константой PI ничего не случится, вряд ли кто-то будет называть переменную так, особенно большими буквами, но всё же.

А в C++ (а позже и в C) появился const. Но всё же, зачем он нужен, почему нельзя просто написать глобальную переменную double PI = 3.141592;?

  1. Во-первых, константы могут быть оптимизированы компилятором. Если вы делаете обычную переменную, компилятор обязан её взять из памяти (или регистров), ведь в другом файле кто-то мог её поменять. А если вы напишете const, то у вас не будет проблем ни с оптимизацией (ассемблер будет как при #define), ни с адекватностью сообщений об ошибках.
  2. Во-вторых, она несёт документирующую функцию, когда вы пишете const с указателями. Если в заголовке функции написано const char*, то вы точно знаете, что вы передаёте в неё строку, которая не меняется, а если char*, то, скорее всего, меняется (то есть функция создана для того, чтобы менять).
  3. В-третьих, имея const, компилятор может вообще не создавать переменную: если мы напишем return PI * 2, то там будет возвращаться константа, и никакого умножения на этапе исполнения.

Кстати, как вообще взаимодействует const с указателями? Посмотрим на такой пример:

int main() {
	const int MAGIC = 42;
	int* p = &MAGIC;
}

Так нельзя, это имеет фундаментальную проблему: вы можете потом записать *p = 3, и это всё порушит. Поэтому вторая строка не компилируется, и её надо заменить на

Но тут нужно вот на что посмотреть. У указателя в некотором смысле два понятия неизменяемости. Мы же можем сделать так:

int main() {
	const int MAGIC = 42;
	const int* p = &MAGIC;
	// ...
	p = nullptr;
}

Кто нам мешает так сделать? Да никто, нам нельзя менять содержимое p, а не его самого. А если вы хотите написать, что нельзя менять именно сам указатель, то это не const int*/int const*, а int* const. Если вам нужно запретить оба варианта использования, то, что логично, const int* const или int const* const. То есть

int main() {
	int* a;
	*a = 1;      // ok.
	a = nullptr; // ok.

	const int* b;       // Синоним `int const* b;`
	*b = 1;      // Error.
	b = nullptr; // ok.

	int* const c;
	*c = 1;      // ok.
	c = nullptr; // Error.

	const int* const d; // Синоним `int const* const d;`
	*d = 1;      // Error.
	d = nullptr; // Error.
}

Теперь вот на что посмотрим:

int main() {
	int a = 3;
	const int b = 42;

	int* pa = &a;        // 1.
	const int* pca = &a; // 2.
	int* pb = &b;        // 3.
	const int* pcb = &b; // 4.
}

Что из этого содержит ошибку? Ну, в третьем точно ошибка, это мы уже обсудили. Также первое и четвёртое точно корректно. А что со вторым? Ну, нарушает ли второе чьи-то права? Ну, нет. Или как бы сказали на парадигмах программирования, никто не нарушает контракт, мы только его расширяем (дополнительно обещая неизменяемость), а значит всё должно быть хорошо. Ну, так и работают неявные преобразования в C++, вы можете навешивать const везде, куда хотите, но не можете его убирать.

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

Понравилась статья? Поделить с друзьями:
  • Ошибка молодости атоса 6 букв
  • Ошибка на этапе компиляции это
  • Ошибка на швейной машинке brother e1
  • Ошибка на электрической плите electrolux e6
  • Ошибка на эмуляторе psp