Ошибка "Неопределенная ссылка/нестираемая внешняя символ" — что это такое и как её исправить?
Что такое ошибки "неопределенная ссылка"/"нерешенный внешний символ"? Каковы их общие причины и как можно исправить и предотвратить эти ошибки?
4 ответ(ов)
Члены класса:
«Чистый» виртуальный деструктор требует реализации.
Объявление деструктора как чистого виртуального подразумевает, что вам нужно также его определить (в отличие от обычной функции):
struct X
{
virtual ~X() = 0;
};
struct Y : X
{
~Y() {}
};
int main()
{
Y y;
}
//X::~X(){} //раскомментируйте эту строку для успешного определения
Это происходит потому, что деструкторы базового класса вызываются при неявном разрушении объекта, поэтому определение обязательно.
Виртуальные методы должны быть либо реализованы, либо объявлены как чистые.
Это аналогично не-виртуальным методам без определения, с добавлением той идеи, что чистое объявление генерирует фиктивную vtable, и вы можете получить ошибку компоновщика даже без использования функции:
struct X
{
virtual void foo();
};
struct Y : X
{
void foo() {}
};
int main()
{
Y y; //ошибка компоновщика, хотя не было вызова X::foo
}
Для успешной компиляции объявите X::foo()
как чистый:
struct X
{
virtual void foo() = 0;
};
Невиртуальные члены класса
Некоторые члены должны быть определены, даже если не используются явно:
struct A
{
~A();
};
Следующее приведет к ошибке:
A a; //деструктор не определен
Реализация может быть встроенной, непосредственно в определение класса:
struct A
{
~A() {}
};
либо снаружи:
A::~A() {}
Если реализация находится за пределами определения класса, но в заголовочном файле, методы должны быть помечены как inline
, чтобы предотвратить множественное определение.
Все используемые методы-члены должны быть определены, если они используются.
Общая ошибка — забыть квалифицировать имя:
struct A
{
void foo();
};
void foo() {}
int main()
{
A a;
a.foo();
}
Определение должно быть следующим:
void A::foo() {}
Статические члены данных должны быть определены вне класса в едином объектном файле:
struct X
{
static int x;
};
int main()
{
int x = X::x;
}
//int X::x; //раскомментируйте эту строку, чтобы определить X::x
Инициализатор может быть предоставлен для статического константного члена данных целого или перечисляемого типа внутри определения класса; однако использование этого члена при ODR все равно потребует определения на уровне пространства имен, как описано выше. C++11 позволяет инициализацию внутри класса для всех статических константных членов данных.
Ошибка при линковке с соответствующими библиотеками/объектными файлами или при компиляции файлов реализации
Часто каждый единичный перевод (translation unit) генерирует объектный файл, содержащий определения символов, определенных в этом файле. Чтобы использовать эти символы, необходимо линковать с этими объектными файлами.
При использовании gcc необходимо указать все объектные файлы, которые должны быть связаны вместе, в командной строке, или компилировать файлы реализации вместе.
g++ -o test objectFile1.o objectFile2.o -lLibraryName
Опция -l...
должна находиться справа от любых файлов .o
/.c
/.cpp
.
libraryName
здесь — это просто название библиотеки без платформозависимых добавлений. Например, на Linux файлы библиотек обычно называются libfoo.so
, но вам нужно будет указать только -lfoo
. На Windows тот же файл может называться foo.lib
, но вы будете использовать тот же аргумент. Возможно, вам придется добавить каталог, где можно найти эти файлы, используя -L‹directory›
. Убедитесь, что вы не ставите пробел после -l
или -L
.
Для Xcode: добавьте пути поиска заголовков пользователя → добавьте путь поиска библиотек → перетащите и вставьте фактическую ссылку на библиотеку в папку проекта.
В MSVS файлы, добавленные в проект, автоматически имеют свои объектные файлы, связанные между собой, и в итоге будет сгенерирован lib
файл (в общем использовании). Чтобы использовать символы в отдельном проекте, необходимо включить lib
файлы в настройки проекта. Это делается в разделе «Компоновщик» (Linker) свойств проекта, в Input -> Additional Dependencies
. (путь к lib
файлу следует добавить в Linker -> General -> Additional Library Directories
). При использовании сторонней библиотеки, предоставленной с lib
файлом, отсутствие этого действия обычно приводит к ошибке.
Также может случиться так, что вы забудете добавить файл в компиляцию, в этом случае объектный файл не будет сгенерирован. В gcc необходимо добавить файлы в командную строку. В MSVS добавление файла в проект автоматически заставит его скомпилировать (хотя файлы могут быть вручную исключены из сборки).
В программировании на Windows явным признаком того, что вы не слинковали необходимую библиотеку, является то, что имя нерешенного символа начинается с __imp_
. Ознакомьтесь с названием функции в документации, и она должна указать, какую библиотеку необходимо использовать. Например, MSDN помещает эту информацию в блок внизу каждой функции в разделе под названием "Библиотека".
Объявлена, но не определена переменная или функция
Типичное объявление переменной выглядит следующим образом:
extern int x;
Так как это лишь объявление, необходима единственная определение. Соответствующее определение будет следующим:
int x;
Например, следующий код вызовет ошибку:
extern int x;
int main()
{
x = 0;
}
//int x; // раскомментируйте эту строку для успешного определения
Аналогичные замечания применимы и к функциям. Объявление функции без её определения приведёт к ошибке:
void foo(); // только объявление
int main()
{
foo();
}
//void foo() {} // раскомментируйте эту строку для успешного определения
Будьте внимательны, чтобы реализованная вами функция точно соответствовала объявленной. Например, вы можете столкнуться с несовпадающими квалификаторами:
void foo(int& x);
int main()
{
int x;
foo(x);
}
void foo(const int& x) {} // различная функция, не предоставляет определения
// для void foo(int& x)
Другие примеры несовпадений включают:
- Функция/переменная объявлена в одном пространстве имен, определена в другом.
- Функция/переменная объявлена как член класса, определена как глобальная (или наоборот).
- Возвратный тип функции, количество и типы параметров, а также соглашение о вызовах не совпадают.
Сообщение об ошибке от компилятора часто предоставит вам полное объявление переменной или функции, которая была объявлена, но никогда не определена. Сравните его внимательно с определением, которое вы предоставили. Убедитесь, что все детали совпадают.
Порядок, в котором указаны взаимозависимые библиотеки, неправильный.
Порядок, в котором библиотеки связываются, имеет значение, если библиотеки зависят друг от друга. В общем случае, если библиотека A
зависит от библиотеки B
, то libA
ДОЛЖНА появляться перед libB
в флагах компоновщика.
Например:
// B.h
#ifndef B_H
#define B_H
struct B {
B(int);
int x;
};
#endif
// B.cpp
#include "B.h"
B::B(int xx) : x(xx) {}
// A.h
#include "B.h"
struct A {
A(int x);
B b;
};
// A.cpp
#include "A.h"
A::A(int x) : b(x) {}
// main.cpp
#include "A.h"
int main() {
A a(5);
return 0;
}
Создайте библиотеки:
$ g++ -c A.cpp
$ g++ -c B.cpp
$ ar rvs libA.a A.o
ar: создается libA.a
a - A.o
$ ar rvs libB.a B.o
ar: создается libB.a
a - B.o
Скомпилируйте:
$ g++ main.cpp -L. -lB -lA
./libA.a(A.o): В функции `A::A(int)':
A.cpp:(.text+0x1c): неопределенная ссылка на `B::B(int)'
collect2: ошибка: ld завершил работу с кодом 1
$ g++ main.cpp -L. -lA -lB
$ ./a.out
Таким образом, повторяю, порядок ИМЕЕТ значение!
Что такое Правило трёх?
Разница между const int*, const int * const и int * const?
Новые возможности C++17: что стоит знать?
Имеют ли круглые скобки после имени типа значение при использовании new?
Что означает 'const' в конце объявления метода класса?