2013-03-13 3 views
4

Итак, кто-то пришел ко мне с проектом, который не удалось связать с ошибкой LNK2005: символ, уже определенный в объекте (с использованием Visual Studio 2010). В этом случае я знаю , что является неправильным (и, следовательно, может указывать их на правильное решение), но я не знаю , почему это неправильно на уровне, чтобы дать хорошее объяснение этому (чтобы это не происходило еще раз).Как объяснить этот LNK2005?

// something.h 
#ifndef _SOMETHING_H 
#define _SOMETHING_H 
int myCoolFunction(); 

int myAwesomeFunction() // Note implementing function in header 
{ 
    return 3; 
} 
#endif 

-

// something.cpp 
#include "something.h" 
int myCoolFunction() 
{ 
    return 4; 
} 

-

// main.cpp 
#include <iostream> 
#include "something.h" 

int main() 
{ 
    std::cout << myAwesomeFunction() << std::endl; 
} 

Это не удается соединение, и фиксируется ввод myAwesomeFunction() в .cpp и оставив заявление в .h.

Мое понимание того, как работает компоновщик, в значительной степени зависит от here. Насколько я понимаю, мы предоставляем символ, который требуется в одном месте.

Я искал MSDN article on LNK2005, который соответствует тому, как я ожидаю, что линкеры будут вести себя (предоставить символ более одного раза -> линкер запутался), но, похоже, не охватывает этот случай (что означает, что я не понимаю что-то очевидно о связи).

вопросы доходности Google и StackOverflow с людьми, не включающими в себя #ifndef или #pragma once (что приводит к множественным декларациям, предусмотренные символов)

A related question I found on this site имеет ту же проблему, но ответ не объясняет, почему мы адекватно оценивая эту проблему с моим уровнем понимания.

У меня есть проблема, я знаю решение, но я не знаю, почему мое решение работает

+0

Возможно, было бы разумнее, если вы запустите все ваши .cpp-файлы через препроцессор, а затем подсчитайте, сколько экземпляров глобального пространства имен «myAwesomeFunction()» есть.Вы знаете, в чем проблема, решение для будущего избежания этой «проблемы» тривиально: «Доктор, мне больно, когда я это делаю». ** «Тогда не делай этого». **. – WhozCraig

+0

Мне нравится знать, почему я что-то делаю, чтобы кто-то сказал: «Эй, зачем ты это делаешь?» У меня лучший ответ, чем «О, кто-то сказал мне». Но да, в повседневной жизни, не злоупотребляя компоновщиком, как правило, я стараюсь избегать. – KidneyChris

+0

Ты и я оба. Компилятор - открытая игра, но я стараюсь уважать компоновщика = P. Это, в конце концов, ближе в девятом иннинге. – WhozCraig

ответ

3

В типичном проекте на C++ вы компилируете каждый из файлов реализации (или .cpp) отдельно - вы вообще никогда не передаете файл заголовка (или .h) в компилятор напрямую.После выполнения предварительной обработки и включений каждый из этих файлов становится единицей перевода . Таким образом, в этом примере вы дали, есть две единицы перевода, которые выглядят так:

  • main.cpp ЕП:

    // Contents of <iostream> header here 
    
    int myCoolFunction(); 
    
    int myAwesomeFunction() // Note implementing function in header 
    { 
        return 3; 
    } 
    
    int main() 
    { 
        std::cout << myAwesomeFunction() << std::endl; 
    } 
    
  • something.cpp ЕП:

    int myCoolFunction(); 
    
    int myAwesomeFunction() // Note implementing function in header 
    { 
        return 3; 
    } 
    
    int myCoolFunction() 
    { 
        return 4; 
    } 
    

Обратите внимание, что обе эти единицы перевода содержат дублирующее содержимое becau они оба включены something.h. Как вы можете видеть, только одна из перечисленных единиц перевода содержит определение myCoolFunction. Это хорошо! Тем не менее, они и содержат определение myAwesomeFunction. Плохо!

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

Каждая программа должна содержать ровно одно определение каждой не-встроенной функции или переменной, которая используется в этой программе как odr; не требуется диагностика.

У вас есть несколько определений myAwesomeFunction по вашей программе, и поэтому вы нарушаете правила. Вот почему ваш код неправильно связан.

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

Теперь давайте посмотрим, что единицы перевода выглядеть, если вы определяете myAwesomeFunction в something.cpp:

  • Fixed main.cpp ЕП:

    // Contents of <iostream> header here 
    
    int myCoolFunction(); 
    
    int myAwesomeFunction(); 
    
    int main() 
    { 
        std::cout << myAwesomeFunction() << std::endl; 
    } 
    
  • Fixed something.cpp ЕП:

    int myCoolFunction(); 
    
    int myAwesomeFunction(); 
    
    int myCoolFunction() 
    { 
        return 4; 
    } 
    
    int myAwesomeFunction() 
    { 
        return 3; 
    } 
    

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

+0

Итак, продолжая свою тенденцию глупых вопросов: что делают включенные охранники, если весь файл заголовка выгружается в каждую единицу перевода? Как они работают? – KidneyChris

+2

@ KidneyChris Включите защитные ограждения, чтобы заголовок не включался * в одну единицу перевода * дважды. Заголовки * предполагается * быть включенными в несколько единиц перевода. –

+0

@stfrabbit Heh. Я задал вопрос: «Я не хочу этого делать, не зная, почему он работает», и в качестве части его я считал само собой разумеющимся, что я нажимаю #ifndef в верхней части каждого заголовка. Моя ментальная модель сейчас идет вместе. Был бы я прав, полагая, что для этого примера включенные охранники совершенно не нужны, потому что ничто не включает файл, который содержит something.h? – KidneyChris

1

Компоновщик просто давая вам знать, что вы сломали одно правило определения. Это базовое, хорошо документированное правило C++ - оно не решается с помощью include guard или директив #pragma once, но в случае бесплатной функции помечает его inline или перемещает реализацию в исходный файл.

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

Перемещение реализации к cpp файла эффективно превращает первоначальное определение в декларации.

-2

myAwesomeFunction определяется в двух исходных файлах: something.cpp и main.cpp. Переместите его реализацию в один из исходных файлов или объявите эту функцию статичной.

+0

Он знает эту часть. –