Tham chiếu không xác định / lỗi biểu tượng bên ngoài chưa được giải quyết là gì? Nguyên nhân phổ biến là gì và làm thế nào để khắc phục / ngăn chặn chúng?
Hãy chỉnh sửa / thêm của riêng bạn.
Tham chiếu không xác định / lỗi biểu tượng bên ngoài chưa được giải quyết là gì? Nguyên nhân phổ biến là gì và làm thế nào để khắc phục / ngăn chặn chúng?
Hãy chỉnh sửa / thêm của riêng bạn.
Câu trả lời:
Việc biên dịch chương trình C ++ diễn ra theo nhiều bước, như được chỉ định bởi 2.2 (tín dụng cho Keith Thompson để tham khảo) :
Ưu tiên trong số các quy tắc cú pháp của dịch thuật được chỉ định bởi các giai đoạn sau [xem chú thích] .
- Các ký tự tệp nguồn vật lý được ánh xạ, theo cách được xác định theo triển khai, đến bộ ký tự nguồn cơ bản (giới thiệu các ký tự dòng mới cho các chỉ báo cuối dòng) nếu cần. [SNIP]
- Mỗi phiên bản của một ký tự dấu gạch chéo ngược (\) ngay sau đó là một ký tự dòng mới sẽ bị xóa, nối các dòng nguồn vật lý để tạo thành các dòng nguồn logic. [SNIP]
- Tệp nguồn được phân tách thành các mã thông báo tiền xử lý (2.5) và các chuỗi ký tự khoảng trắng (bao gồm cả các nhận xét). [SNIP]
- Các chỉ thị tiền xử lý được thực thi, các lệnh macro được mở rộng và các biểu thức toán tử đơn nguyên _Pragma được thực thi. [SNIP]
- Mỗi ký tự nguồn được đặt thành viên theo một ký tự hoặc một chuỗi ký tự, cũng như từng chuỗi thoát và tên ký tự phổ quát trong một ký tự bằng chữ hoặc một chuỗi ký tự không thô, được chuyển đổi thành thành viên tương ứng của bộ ký tự thực thi; [SNIP]
- Chuỗi mã thông báo liền kề được nối liền nhau.
- Các ký tự khoảng trắng phân tách mã thông báo không còn đáng kể. Mỗi mã thông báo tiền xử lý được chuyển đổi thành mã thông báo. (2.7). Các mã thông báo kết quả được phân tích tổng hợp và ngữ nghĩa và được dịch là một đơn vị dịch thuật. [SNIP]
- Các đơn vị dịch thuật và các đơn vị dịch thuật được kết hợp như sau: [SNIP]
- Tất cả các tham chiếu thực thể bên ngoài được giải quyết. Các thành phần thư viện được liên kết để đáp ứng các tham chiếu bên ngoài đến các thực thể không được xác định trong bản dịch hiện tại. Tất cả đầu ra của trình dịch như vậy được thu thập vào một hình ảnh chương trình chứa thông tin cần thiết để thực thi trong môi trường thực thi của nó. (nhấn mạnh của tôi)
[chú thích] Việc triển khai phải hành xử như thể những giai đoạn riêng biệt này xảy ra, mặc dù trong thực tế, các giai đoạn khác nhau có thể được xếp lại với nhau.
Các lỗi được chỉ định xảy ra trong giai đoạn biên dịch cuối cùng này, thường được gọi là liên kết. Về cơ bản, điều đó có nghĩa là bạn đã biên dịch một loạt các tệp thực hiện thành các tệp đối tượng hoặc thư viện và bây giờ bạn muốn làm cho chúng hoạt động cùng nhau.
Nói rằng bạn xác định biểu tượng a
trong a.cpp
. Bây giờ, b.cpp
tuyên bố biểu tượng đó và sử dụng nó. Trước khi liên kết, nó chỉ đơn giản giả định rằng biểu tượng đó đã được xác định ở đâu đó , nhưng nó không quan tâm đến đâu. Pha liên kết chịu trách nhiệm tìm biểu tượng và liên kết chính xác với nó b.cpp
(tốt, thực sự với đối tượng hoặc thư viện sử dụng nó).
Nếu bạn đang sử dụng Microsoft Visual Studio, bạn sẽ thấy các dự án tạo .lib
tệp. Chúng bao gồm một bảng các ký hiệu được xuất và một bảng các ký hiệu được nhập. Các biểu tượng đã nhập được giải quyết đối với các thư viện mà bạn liên kết và các biểu tượng đã xuất được cung cấp cho các thư viện sử dụng .lib
(nếu có).
Cơ chế tương tự tồn tại cho các trình biên dịch / nền tảng khác.
Thông báo lỗi phổ biến là error LNK2001
, error LNK1120
, error LNK2019
cho Microsoft Visual Studio và undefined reference to
symbolName cho GCC .
Mật mã:
struct X
{
virtual void foo();
};
struct Y : X
{
void foo() {}
};
struct A
{
virtual ~A() = 0;
};
struct B: A
{
virtual ~B(){}
};
extern int x;
void foo();
int main()
{
x = 0;
foo();
Y y;
B b;
}
sẽ tạo ra các lỗi sau với GCC :
/home/AbiSfw/ccvvuHoX.o: In function `main':
prog.cpp:(.text+0x10): undefined reference to `x'
prog.cpp:(.text+0x19): undefined reference to `foo()'
prog.cpp:(.text+0x2d): undefined reference to `A::~A()'
/home/AbiSfw/ccvvuHoX.o: In function `B::~B()':
prog.cpp:(.text._ZN1BD1Ev[B::~B()]+0xb): undefined reference to `A::~A()'
/home/AbiSfw/ccvvuHoX.o: In function `B::~B()':
prog.cpp:(.text._ZN1BD0Ev[B::~B()]+0x12): undefined reference to `A::~A()'
/home/AbiSfw/ccvvuHoX.o:(.rodata._ZTI1Y[typeinfo for Y]+0x8): undefined reference to `typeinfo for X'
/home/AbiSfw/ccvvuHoX.o:(.rodata._ZTI1B[typeinfo for B]+0x8): undefined reference to `typeinfo for A'
collect2: ld returned 1 exit status
và các lỗi tương tự với Microsoft Visual Studio :
1>test2.obj : error LNK2001: unresolved external symbol "void __cdecl foo(void)" (?foo@@YAXXZ)
1>test2.obj : error LNK2001: unresolved external symbol "int x" (?x@@3HA)
1>test2.obj : error LNK2001: unresolved external symbol "public: virtual __thiscall A::~A(void)" (??1A@@UAE@XZ)
1>test2.obj : error LNK2001: unresolved external symbol "public: virtual void __thiscall X::foo(void)" (?foo@X@@UAEXXZ)
1>...\test2.exe : fatal error LNK1120: 4 unresolved externals
Nguyên nhân phổ biến bao gồm:
#pragma
(Microsoft Visual Studio)UNICODE
Định nghĩa không nhất quánvirtual
cần thực hiện.Khai báo một hàm hủy thuần túy vẫn yêu cầu bạn xác định nó (không giống như một hàm thông thường):
struct X
{
virtual ~X() = 0;
};
struct Y : X
{
~Y() {}
};
int main()
{
Y y;
}
//X::~X(){} //uncomment this line for successful definition
Điều này xảy ra bởi vì các hàm hủy của lớp cơ sở được gọi khi đối tượng bị hủy hoàn toàn, do đó cần có một định nghĩa.
virtual
phương pháp phải được thực hiện hoặc được định nghĩa là thuần túy.Điều này tương tự với các virtual
phương thức không có định nghĩa, với lý do được thêm vào rằng khai báo thuần tạo ra một vtable giả và bạn có thể gặp lỗi liên kết mà không sử dụng hàm:
struct X
{
virtual void foo();
};
struct Y : X
{
void foo() {}
};
int main()
{
Y y; //linker error although there was no call to X::foo
}
Để làm việc này, hãy khai báo X::foo()
là thuần túy:
struct X
{
virtual void foo() = 0;
};
virtual
Thành viên không thuộc lớpMột số thành viên cần được xác định ngay cả khi không được sử dụng rõ ràng:
struct A
{
~A();
};
Sau đây sẽ mang lại lỗi:
A a; //destructor undefined
Việc thực hiện có thể là nội tuyến, trong chính định nghĩa lớp:
struct A
{
~A() {}
};
hoặc bên ngoài:
A::~A() {}
Nếu việc triển khai nằm ngoài định nghĩa lớp, nhưng trong một tiêu đề, các phương thức phải được đánh dấu là inline
để ngăn chặn một định nghĩa nhiều.
Tất cả các phương thức thành viên được sử dụng cần phải được xác định nếu được sử dụng.
struct A
{
void foo();
};
void foo() {}
int main()
{
A a;
a.foo();
}
Định nghĩa nên là
void A::foo() {}
static
các thành viên dữ liệu phải được xác định bên ngoài lớp trong một đơn vị dịch thuật :struct X
{
static int x;
};
int main()
{
int x = X::x;
}
//int X::x; //uncomment this line to define X::x
Một bộ khởi tạo có thể được cung cấp cho một static
const
thành viên dữ liệu thuộc kiểu tích phân hoặc liệt kê trong định nghĩa lớp; tuy nhiên, việc sử dụng odr của thành viên này vẫn sẽ yêu cầu định nghĩa phạm vi không gian tên như được mô tả ở trên. C ++ 11 cho phép khởi tạo bên trong lớp cho tất cả các static const
thành viên dữ liệu.
Thông thường, mỗi đơn vị dịch thuật sẽ tạo ra một tệp đối tượng có chứa các định nghĩa của các ký hiệu được xác định trong đơn vị dịch thuật đó. Để sử dụng các ký hiệu đó, bạn phải liên kết với các tệp đối tượng đó.
Trong gcc, bạn sẽ chỉ định tất cả các tệp đối tượng sẽ được liên kết với nhau trong dòng lệnh hoặc biên dịch các tệp thực hiện với nhau.
g++ -o test objectFile1.o objectFile2.o -lLibraryName
Ở libraryName
đây chỉ là tên trần của thư viện, không có bổ sung dành riêng cho nền tảng. Vì vậy, ví dụ trên các tệp thư viện Linux thường được gọi libfoo.so
nhưng bạn chỉ viết -lfoo
. Trên Windows, cùng một tệp có thể được gọi foo.lib
, nhưng bạn sẽ sử dụng cùng một đối số. Bạn có thể phải thêm thư mục nơi các tệp đó có thể được tìm thấy bằng cách sử dụng -L‹directory›
. Đảm bảo không viết một khoảng trắng sau -l
hoặc -L
.
Đối với XCode : Thêm Đường dẫn tìm kiếm tiêu đề người dùng -> thêm Đường dẫn tìm kiếm thư viện -> kéo và thả tham chiếu thư viện thực tế vào thư mục dự án.
Trong MSVS , các tệp được thêm vào dự án sẽ tự động có các tệp đối tượng được liên kết với nhau và một lib
tệp sẽ được tạo (theo cách sử dụng chung). Để sử dụng các ký hiệu trong một dự án riêng biệt, bạn cần bao gồm các lib
tệp trong cài đặt dự án. Điều này được thực hiện trong phần Linker của các thuộc tính dự án, trong Input -> Additional Dependencies
. (đường dẫn đến lib
tệp nên được thêm vào Linker -> General -> Additional Library Directories
) Khi sử dụng thư viện của bên thứ ba được cung cấp cùng với lib
tệp, việc không làm như vậy thường dẫn đến lỗi.
Nó cũng có thể xảy ra khi bạn quên thêm tệp vào phần biên dịch, trong trường hợp đó tệp đối tượng sẽ không được tạo. Trong gcc, bạn thêm các tệp vào dòng lệnh. Trong MSVS, việc thêm tệp vào dự án sẽ khiến nó tự động biên dịch nó (mặc dù các tệp có thể, theo cách thủ công, được loại trừ riêng lẻ khỏi bản dựng).
Trong lập trình Windows, dấu hiệu nhận biết rằng bạn không liên kết một thư viện cần thiết là tên của biểu tượng chưa được giải quyết bắt đầu bằng __imp_
. Tra cứu tên của hàm trong tài liệu và nó sẽ cho biết thư viện nào bạn cần sử dụng. Ví dụ: MSDN đặt thông tin vào một hộp ở dưới cùng của mỗi chức năng trong một phần gọi là "Thư viện".
gcc main.c
thay vì gcc main.c other.c
(điều mà người mới bắt đầu thường làm trước khi các dự án của họ lớn đến mức xây dựng các tệp .o).
Một khai báo biến điển hình là
extern int x;
Vì đây chỉ là một tuyên bố, một định nghĩa duy nhất là cần thiết. Một định nghĩa tương ứng sẽ là:
int x;
Ví dụ: sau đây sẽ tạo ra một lỗi:
extern int x;
int main()
{
x = 0;
}
//int x; // uncomment this line for successful definition
Nhận xét tương tự áp dụng cho các chức năng. Khai báo một hàm mà không xác định nó dẫn đến lỗi:
void foo(); // declaration only
int main()
{
foo();
}
//void foo() {} //uncomment this line for successful definition
Hãy cẩn thận rằng chức năng bạn thực hiện chính xác khớp với chức năng bạn đã khai báo. Ví dụ: bạn có thể có vòng loại cv không khớp:
void foo(int& x);
int main()
{
int x;
foo(x);
}
void foo(const int& x) {} //different function, doesn't provide a definition
//for void foo(int& x)
Các ví dụ khác về sự không phù hợp bao gồm
Thông báo lỗi từ trình biên dịch thường sẽ cung cấp cho bạn khai báo đầy đủ về biến hoặc hàm được khai báo nhưng không bao giờ được xác định. So sánh nó chặt chẽ với định nghĩa bạn cung cấp. Hãy chắc chắn rằng mọi chi tiết phù hợp.
#includes
không được thêm vào thư mục nguồn cũng thuộc danh mục định nghĩa bị thiếu.
Thứ tự các thư viện được liên kết DOES có vấn đề nếu các thư viện phụ thuộc lẫn nhau. Nói chung, nếu thư viện A
phụ thuộc vào thư viện B
, thì libA
PHẢI xuất hiện trước libB
trong các cờ liên kết.
Ví dụ:
// 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;
};
Tạo các thư viện:
$ g++ -c A.cpp
$ g++ -c B.cpp
$ ar rvs libA.a A.o
ar: creating libA.a
a - A.o
$ ar rvs libB.a B.o
ar: creating libB.a
a - B.o
Biên dịch:
$ g++ main.cpp -L. -lB -lA
./libA.a(A.o): In function `A::A(int)':
A.cpp:(.text+0x1c): undefined reference to `B::B(int)'
collect2: error: ld returned 1 exit status
$ g++ main.cpp -L. -lA -lB
$ ./a.out
Vì vậy, để lặp lại một lần nữa, thứ tự DOES vấn đề!
"tham chiếu không xác định / biểu tượng bên ngoài chưa được giải quyết" là gì
Tôi sẽ cố gắng giải thích "biểu tượng bên ngoài không xác định / chưa được giải quyết" là gì.
lưu ý: tôi sử dụng g ++ và Linux và tất cả các ví dụ là dành cho nó
Ví dụ: chúng tôi có một số mã
// src1.cpp
void print();
static int local_var_name; // 'static' makes variable not visible for other modules
int global_var_name = 123;
int main()
{
print();
return 0;
}
và
// src2.cpp
extern "C" int printf (const char*, ...);
extern int global_var_name;
//extern int local_var_name;
void print ()
{
// printf("%d%d\n", global_var_name, local_var_name);
printf("%d\n", global_var_name);
}
Tạo các tệp đối tượng
$ g++ -c src1.cpp -o src1.o
$ g++ -c src2.cpp -o src2.o
Sau giai đoạn biên dịch chương trình, chúng ta có một tệp đối tượng, chứa bất kỳ ký hiệu nào để xuất. Nhìn vào các biểu tượng
$ readelf --symbols src1.o
Num: Value Size Type Bind Vis Ndx Name
5: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 _ZL14local_var_name # [1]
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_var_name # [2]
Tôi đã từ chối một số dòng từ đầu ra, vì chúng không quan trọng
Vì vậy, chúng tôi thấy theo biểu tượng để xuất khẩu.
[1] - this is our static (local) variable (important - Bind has a type "LOCAL")
[2] - this is our global variable
src2.cpp không xuất khẩu gì và chúng tôi không thấy biểu tượng nào của nó
Liên kết các tệp đối tượng của chúng tôi
$ g++ src1.o src2.o -o prog
và chạy nó
$ ./prog
123
Linker thấy các biểu tượng xuất khẩu và liên kết nó. Bây giờ chúng tôi cố gắng bỏ dòng trong src2.cpp như ở đây
// src2.cpp
extern "C" int printf (const char*, ...);
extern int global_var_name;
extern int local_var_name;
void print ()
{
printf("%d%d\n", global_var_name, local_var_name);
}
và xây dựng lại một tệp đối tượng
$ g++ -c src2.cpp -o src2.o
OK (không có lỗi), vì chúng tôi chỉ xây dựng tệp đối tượng, liên kết chưa được thực hiện. Cố gắng liên kết
$ g++ src1.o src2.o -o prog
src2.o: In function `print()':
src2.cpp:(.text+0x6): undefined reference to `local_var_name'
collect2: error: ld returned 1 exit status
Nó đã xảy ra vì local_var_name của chúng tôi là tĩnh, tức là nó không hiển thị cho các mô-đun khác. Bây giờ sâu sắc hơn. Nhận đầu ra giai đoạn dịch
$ g++ -S src1.cpp -o src1.s
// src1.s
look src1.s
.file "src1.cpp"
.local _ZL14local_var_name
.comm _ZL14local_var_name,4,4
.globl global_var_name
.data
.align 4
.type global_var_name, @object
.size global_var_name, 4
global_var_name:
.long 123
.text
.globl main
.type main, @function
main:
; assembler code, not interesting for us
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits
Vì vậy, chúng tôi đã thấy không có nhãn cho local_var_name, đó là lý do tại sao trình liên kết không tìm thấy nó. Nhưng chúng tôi là tin tặc :) và chúng tôi có thể sửa nó. Mở src1.s trong trình soạn thảo văn bản của bạn và thay đổi
.local _ZL14local_var_name
.comm _ZL14local_var_name,4,4
đến
.globl local_var_name
.data
.align 4
.type local_var_name, @object
.size local_var_name, 4
local_var_name:
.long 456789
tức là bạn nên có như dưới đây
.file "src1.cpp"
.globl local_var_name
.data
.align 4
.type local_var_name, @object
.size local_var_name, 4
local_var_name:
.long 456789
.globl global_var_name
.align 4
.type global_var_name, @object
.size global_var_name, 4
global_var_name:
.long 123
.text
.globl main
.type main, @function
main:
; ...
chúng tôi đã thay đổi mức độ hiển thị của local_var_name và đặt giá trị của nó thành 456789. Hãy thử xây dựng một tệp đối tượng từ nó
$ g++ -c src1.s -o src2.o
ok, xem đầu ra sẵn sàng (ký hiệu)
$ readelf --symbols src1.o
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 local_var_name
bây giờ local_var_name có Bind GLOBAL (là ĐỊA PHƯƠNG)
liên kết
$ g++ src1.o src2.o -o prog
và chạy nó
$ ./prog
123456789
ok, chúng tôi hack nó :)
Vì vậy, kết quả là - một "lỗi biểu tượng bên ngoài không được giải quyết / không được giải quyết" xảy ra khi trình liên kết không thể tìm thấy các ký hiệu toàn cục trong các tệp đối tượng.
Hàm (hoặc biến) void foo()
đã được xác định trong chương trình C và bạn cố gắng sử dụng nó trong chương trình C ++:
void foo();
int main()
{
foo();
}
Trình liên kết C ++ dự kiến các tên sẽ được đọc sai, vì vậy bạn phải khai báo hàm là:
extern "C" void foo();
int main()
{
foo();
}
Tương tự, thay vì được định nghĩa trong chương trình C, hàm (hoặc biến) void foo()
được định nghĩa trong C ++ nhưng với liên kết C:
extern "C" void foo();
và bạn cố gắng sử dụng nó trong chương trình C ++ với liên kết C ++.
Nếu toàn bộ thư viện được bao gồm trong tệp tiêu đề (và được biên dịch dưới dạng mã C); bao gồm sẽ cần phải được như sau;
extern "C" {
#include "cheader.h"
}
#ifdef __cplusplus [\n] extern"C" { [\n] #endif
và #ifdef __cplusplus [\n] } [\n] #endif
( [\n]
là trả lại vận chuyển thực sự nhưng tôi không thể viết điều này trong bình luận).
extern "C" { #include <myCppHeader.h> }
.
Nếu vẫn thất bại, biên dịch lại.
Gần đây tôi đã có thể thoát khỏi một lỗi bên ngoài chưa được giải quyết trong Visual Studio 2012 chỉ bằng cách biên dịch lại tệp vi phạm. Khi tôi xây dựng lại, lỗi đã biến mất.
Điều này thường xảy ra khi hai (hoặc nhiều) thư viện có sự phụ thuộc theo chu kỳ. Thư viện A cố gắng sử dụng các ký hiệu trong B.lib và thư viện B cố gắng sử dụng các ký hiệu từ A.lib. Không tồn tại để bắt đầu với. Khi bạn cố gắng biên dịch A, bước liên kết sẽ thất bại vì không thể tìm thấy B.lib. A.lib sẽ được tạo ra, nhưng không có dll. Sau đó, bạn biên dịch B, sẽ thành công và tạo B.lib. Biên dịch lại A bây giờ sẽ hoạt động vì B.lib đã được tìm thấy.
MSVS yêu cầu bạn chỉ định biểu tượng nào cần xuất và nhập bằng __declspec(dllexport)
và __declspec(dllimport)
.
Chức năng kép này thường có được thông qua việc sử dụng macro:
#ifdef THIS_MODULE
#define DLLIMPEXP __declspec(dllexport)
#else
#define DLLIMPEXP __declspec(dllimport)
#endif
Macro THIS_MODULE
sẽ chỉ được xác định trong mô-đun xuất hàm. Bằng cách đó, tuyên bố:
DLLIMPEXP void foo();
mở rộng đến
__declspec(dllexport) void foo();
và báo cho trình biên dịch xuất hàm, vì mô-đun hiện tại chứa định nghĩa của nó. Khi bao gồm khai báo trong một mô-đun khác, nó sẽ mở rộng sang
__declspec(dllimport) void foo();
và báo cho trình biên dịch rằng định nghĩa nằm trong một trong các thư viện mà bạn đã liên kết (cũng xem 1) ).
Bạn có thể nhập / xuất các lớp mô phỏng:
class DLLIMPEXP X
{
};
visibility
và Windows .def
, vì những điều này cũng ảnh hưởng đến tên và sự hiện diện của biểu tượng.
.def
tệp trong độ tuổi. Hãy thêm một câu trả lời hoặc chỉnh sửa câu trả lời này.
Đây là một trong những thông báo lỗi khó hiểu nhất mà mọi lập trình viên VC ++ đã gặp lại nhiều lần. Trước tiên hãy làm cho mọi thứ rõ ràng.
A. Biểu tượng là gì? Nói tóm lại, một biểu tượng là một cái tên. Nó có thể là tên biến, tên hàm, tên lớp, tên typedef hoặc bất cứ thứ gì ngoại trừ những tên và dấu hiệu đó thuộc về ngôn ngữ C ++. Nó được xác định hoặc giới thiệu bởi một thư viện phụ thuộc (do người dùng khác xác định).
B. Bên ngoài là gì?
Trong VC ++, mọi tệp nguồn (.cpp, .c, v.v.) được coi là một đơn vị dịch, trình biên dịch biên dịch một đơn vị tại một thời điểm và tạo một tệp đối tượng (.obj) cho đơn vị dịch hiện tại. (Lưu ý rằng mọi tệp tiêu đề mà tệp nguồn này bao gồm sẽ được xử lý trước và sẽ được coi là một phần của đơn vị dịch thuật này) Mọi thứ trong đơn vị dịch thuật được coi là nội bộ, mọi thứ khác được coi là bên ngoài. Trong C ++, bạn có thể tham chiếu một biểu tượng bên ngoài bằng cách sử dụng các từ khóa như extern
, __declspec (dllimport)
v.v.
C. Giải quyết vấn đề thế nào? Giải quyết là một thuật ngữ thời gian liên kết. Trong thời gian liên kết, trình liên kết cố gắng tìm định nghĩa bên ngoài cho mọi ký hiệu trong các tệp đối tượng không thể tìm thấy định nghĩa bên trong. Phạm vi của quá trình tìm kiếm này bao gồm:
Quá trình tìm kiếm này được gọi là giải quyết.
D. Cuối cùng, tại sao Biểu tượng bên ngoài chưa được giải quyết? Nếu trình liên kết không thể tìm thấy định nghĩa bên ngoài cho một biểu tượng không có định nghĩa bên trong, nó sẽ báo cáo lỗi Biểu tượng bên ngoài chưa được giải quyết.
E. Nguyên nhân có thể của LNK2019 : Lỗi Biểu tượng bên ngoài chưa được giải quyết. Chúng tôi đã biết rằng lỗi này là do trình liên kết không tìm thấy định nghĩa của các ký hiệu bên ngoài, các nguyên nhân có thể có thể được sắp xếp là:
Ví dụ: nếu chúng ta có một hàm gọi là foo được xác định trong a.cpp:
int foo()
{
return 0;
}
Trong b.cpp chúng tôi muốn gọi hàm foo, vì vậy chúng tôi thêm
void foo();
để khai báo hàm foo () và gọi nó trong phần thân hàm khác, giả sử bar()
:
void bar()
{
foo();
}
Bây giờ khi bạn xây dựng mã này, bạn sẽ gặp lỗi LNK2019 phàn nàn rằng foo là một biểu tượng chưa được giải quyết. Trong trường hợp này, chúng ta biết rằng foo () có định nghĩa của nó trong a.cpp, nhưng khác với định nghĩa mà chúng ta đang gọi (giá trị trả về khác nhau). Đây là trường hợp định nghĩa tồn tại.
Nếu chúng tôi muốn gọi một số chức năng trong thư viện, nhưng thư viện nhập không được thêm vào danh sách phụ thuộc bổ sung (được đặt từ Project | Properties | Configuration Properties | Linker | Input | Additional Dependency
:) của cài đặt dự án của bạn. Bây giờ trình liên kết sẽ báo cáo LNK2019 do định nghĩa không tồn tại trong phạm vi tìm kiếm hiện tại.
Các mẫu không chuyên biệt phải có định nghĩa hiển thị cho tất cả các đơn vị dịch sử dụng chúng. Điều đó có nghĩa là bạn không thể tách định nghĩa của mẫu thành tệp thực hiện. Nếu bạn phải tách riêng việc thực hiện, cách giải quyết thông thường là có một impl
tệp mà bạn đưa vào ở cuối tiêu đề khai báo mẫu. Một tình huống phổ biến là:
template<class T>
struct X
{
void foo();
};
int main()
{
X<int> x;
x.foo();
}
//differentImplementationFile.cpp
template<class T>
void X<T>::foo()
{
}
Để khắc phục điều này, bạn phải di chuyển định nghĩa của X::foo
tệp tiêu đề hoặc một số vị trí hiển thị cho đơn vị dịch sử dụng nó.
Các mẫu chuyên dụng có thể được thực hiện trong một tệp thực hiện và việc triển khai không phải hiển thị, nhưng chuyên môn phải được khai báo trước đó.
Để được giải thích thêm và một giải pháp khả thi khác (khởi tạo rõ ràng) hãy xem câu hỏi và câu trả lời này .
tham chiếu không xác định đến WinMain@16
hoặc tham chiếu điểm nhập cảnh 'bất thường' tương tựmain()
(đặc biệt đối vớiVisual Studio).
Bạn có thể đã bỏ lỡ để chọn loại dự án phù hợp với IDE thực tế của bạn. IDE có thể muốn liên kết, ví dụ các dự án Ứng dụng Windows với chức năng điểm vào như vậy (như được chỉ định trong tham chiếu bị thiếu ở trên), thay vì int main(int argc, char** argv);
chữ ký thường được sử dụng .
Nếu IDE của bạn hỗ trợ Dự án Bảng điều khiển Đồng bằng, bạn có thể muốn chọn loại dự án này, thay vì dự án ứng dụng Windows.
Dưới đây là case1 và case2 được xử lý chi tiết hơn từ một vấn đề trong thế giới thực .
WinMain
. Các chương trình C ++ hợp lệ cần a main
.
Gói Visual Studio NuGet cần được cập nhật cho phiên bản bộ công cụ mới
Tôi vừa gặp sự cố này khi cố gắng liên kết libpng với Visual Studio 2013. Vấn đề là tệp gói chỉ có thư viện cho Visual Studio 2010 và 2012.
Giải pháp chính xác là hy vọng nhà phát triển phát hành gói cập nhật và sau đó nâng cấp, nhưng nó hoạt động với tôi bằng cách hack trong một cài đặt bổ sung cho VS2013, chỉ vào các tệp thư viện VS2012.
Tôi đã chỉnh sửa gói (trong packages
thư mục bên trong thư mục của giải pháp) bằng cách tìm packagename\build\native\packagename.targets
và bên trong tệp đó, sao chép tất cả các v110
phần. Tôi đã thay đổi v110
thành v120
trong các trường điều kiện chỉ rất cẩn thận để lại tất cả các đường dẫn tên tệp là v110
. Điều này chỉ đơn giản là cho phép Visual Studio 2013 liên kết với các thư viện cho năm 2012 và trong trường hợp này, nó đã hoạt động.
Giả sử bạn có một dự án lớn được viết bằng c ++ có hàng ngàn tệp .cpp và một nghìn tệp .h. Hãy nói rằng dự án cũng phụ thuộc vào mười thư viện tĩnh. Hãy nói rằng chúng tôi đang ở trên Windows và chúng tôi xây dựng dự án của mình trong Visual Studio 20xx. Khi bạn nhấn Ctrl + F7 Visual Studio để bắt đầu biên dịch toàn bộ giải pháp (giả sử chúng tôi chỉ có một dự án trong giải pháp)
Ý nghĩa của việc biên soạn là gì?
Bước biên dịch thứ hai được thực hiện bởi Linker.Linker nên hợp nhất tất cả các tệp đối tượng và xây dựng cuối cùng là đầu ra (có thể là tệp thực thi hoặc thư viện)
Các bước trong liên kết một dự án
error LNK2001: unresolved external symbol "void __cdecl foo(void)" (?foo@@YAXXZ)
Quan sát
Làm thế nào để giải quyết loại lỗi này
Lỗi thời gian biên dịch:
Lỗi thời gian liên kết
#pragma once
để cho phép trình biên dịch không bao gồm một tiêu đề nếu nó đã được bao gồm trong .cpp hiện tại được biên dịchGần đây tôi đã gặp vấn đề này và hóa ra đó là một lỗi trong Visual Studio Express 2013 . Tôi đã phải xóa một tệp nguồn khỏi dự án và thêm lại nó để khắc phục lỗi.
Các bước để thử nếu bạn tin rằng nó có thể là một lỗi trong trình biên dịch / IDE:
Hầu hết các trình liên kết hiện đại bao gồm một tùy chọn dài dòng in ra các mức độ khác nhau;
Đối với gcc và tiếng kêu; bạn thường sẽ thêm -v -Wl,--verbose
hoặc -v -Wl,-v
vào dòng lệnh. Nhiều thông tin thêm có thế được tìm thấy ở đây;
Đối với MSVC, /VERBOSE
(đặc biệt /VERBOSE:LIB
) được thêm vào dòng lệnh liên kết.
/VERBOSE
tùy chọn liên kết .Tập tin .lib được liên kết có liên quan đến một.
Tôi gặp vấn đề tương tự. Nói rằng tôi có dự án MyProject và TestProject. Tôi đã liên kết hiệu quả tệp lib cho MyProject với TestProject. Tuy nhiên, tệp lib này được tạo ra khi DLL cho MyProject được xây dựng. Ngoài ra, tôi không chứa mã nguồn cho tất cả các phương thức trong MyProject, mà chỉ truy cập vào các điểm nhập của DLL.
Để giải quyết vấn đề, tôi đã xây dựng MyProject dưới dạng LIB và liên kết TestProject với tệp .lib này (tôi sao chép dán tệp .lib đã tạo vào thư mục TestProject). Sau đó tôi có thể xây dựng lại MyProject dưới dạng DLL. Nó được biên dịch vì lib mà TestProject được liên kết có chứa mã cho tất cả các phương thức trong các lớp trong MyProject.
Vì mọi người dường như được hướng đến câu hỏi này khi nói đến lỗi liên kết, tôi sẽ thêm nó vào đây.
Một lý do có thể gây ra lỗi liên kết với GCC 5.2.0 là mặc định thư viện libstdc ++ ABI mới được chọn theo mặc định.
Nếu bạn gặp lỗi liên kết về các tham chiếu không xác định đến các ký hiệu liên quan đến các loại trong không gian tên std :: __ cxx11 hoặc thẻ [abi: cxx11] thì có lẽ bạn đang cố gắng liên kết các tệp đối tượng được biên dịch với các giá trị khác nhau cho _GLIBCXX_USE_CXX11_ABI vĩ mô. Điều này thường xảy ra khi liên kết đến thư viện của bên thứ ba được biên dịch với phiên bản GCC cũ hơn. Nếu thư viện của bên thứ ba không thể được xây dựng lại bằng ABI mới thì bạn sẽ cần biên dịch lại mã của mình với ABI cũ.
Vì vậy, nếu bạn đột nhiên gặp lỗi liên kết khi chuyển sang GCC sau 5.1.0 thì đây sẽ là một điều cần kiểm tra.
Một trình bao bọc xung quanh GNU ld không hỗ trợ các tập lệnh liên kết
Một số tệp .so thực sự là các tập lệnh liên kết GNU ld , ví dụ tệp libtbb.so là tệp văn bản ASCII có nội dung này:
INPUT (libtbb.so.2)
Một số bản dựng phức tạp hơn có thể không hỗ trợ điều này. Ví dụ: nếu bạn bao gồm -v trong các tùy chọn trình biên dịch, bạn có thể thấy rằng trình bao bọc gcc mainwin mwdip loại bỏ các tệp lệnh tập lệnh liên kết trong danh sách đầu ra của các thư viện để liên kết. Một công việc đơn giản là thay thế lệnh nhập tập lệnh liên kết. thay vào đó là một bản sao của tệp (hoặc một liên kết tượng trưng), vd
cp libtbb.so.2 libtbb.so
Hoặc bạn có thể thay thế đối số -l bằng đường dẫn đầy đủ của .so, ví dụ thay vì -ltbb
làm/home/foo/tbb-4.3/linux/lib/intel64/gcc4.4/libtbb.so.2
libfoo
phụ thuộc vào libbar
, thì liên kết của bạn đặt chính xác libfoo
trước libbar
.undefined reference to
một cái gì đó lỗi.#include
d và trên thực tế được xác định trong các thư viện mà bạn đang liên kết.Ví dụ như trong C. Họ cũng có thể là C ++
my_lib.c
#include "my_lib.h"
#include <stdio.h>
void hw(void)
{
puts("Hello World");
}
my_lib.h
#ifndef MY_LIB_H
#define MT_LIB_H
extern void hw(void);
#endif
ví dụ1.c
#include <my_lib.h>
int main()
{
hw();
return 0;
}
Bạn xây dựng thư viện tĩnh của mình:
$ gcc -c -o my_lib.o my_lib.c
$ ar rcs libmy_lib.a my_lib.o
Bạn biên dịch chương trình của bạn:
$ gcc -I. -c -o eg1.o eg1.c
Bạn cố gắng liên kết nó với libmy_lib.a
và thất bại:
$ gcc -o eg1 -L. -lmy_lib eg1.o
eg1.o: In function `main':
eg1.c:(.text+0x5): undefined reference to `hw'
collect2: error: ld returned 1 exit status
Kết quả tương tự nếu bạn biên dịch và liên kết trong một bước, như:
$ gcc -o eg1 -I. -L. -lmy_lib eg1.c
/tmp/ccQk1tvs.o: In function `main':
eg1.c:(.text+0x5): undefined reference to `hw'
collect2: error: ld returned 1 exit status
libz
ví dụ2.c
#include <zlib.h>
#include <stdio.h>
int main()
{
printf("%s\n",zlibVersion());
return 0;
}
Biên dịch chương trình của bạn:
$ gcc -c -o eg2.o eg2.c
Cố gắng liên kết chương trình của bạn với libz
và thất bại:
$ gcc -o eg2 -lz eg2.o
eg2.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
collect2: error: ld returned 1 exit status
Tương tự nếu bạn biên dịch và liên kết trong một lần:
$ gcc -o eg2 -I. -lz eg2.c
/tmp/ccxCiGn7.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
collect2: error: ld returned 1 exit status
Và một biến thể trong ví dụ 2 liên quan đến pkg-config
:
$ gcc -o eg2 $(pkg-config --libs zlib) eg2.o
eg2.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
Trong chuỗi các tệp đối tượng và thư viện bạn muốn liên kết để tạo chương trình của mình, bạn đang đặt các thư viện trước các tệp đối tượng tham chiếu đến chúng. Bạn cần đặt các thư viện sau các tệp đối tượng tham chiếu đến chúng.
Liên kết ví dụ 1 chính xác:
$ gcc -o eg1 eg1.o -L. -lmy_lib
Sự thành công:
$ ./eg1
Hello World
Liên kết ví dụ 2 chính xác:
$ gcc -o eg2 eg2.o -lz
Sự thành công:
$ ./eg2
1.2.8
Liên kết pkg-config
biến thể của ví dụ 2 một cách chính xác:
$ gcc -o eg2 eg2.o $(pkg-config --libs zlib)
$ ./eg2
1.2.8
Đọc là tùy chọn từ đây về .
Theo mặc định, một lệnh liên kết được tạo bởi GCC, trên bản phân phối của bạn, tiêu thụ các tệp trong liên kết từ trái sang phải theo trình tự dòng lệnh. Khi nhận thấy rằng một tệp đề cập đến một cái gì đó và không chứa định nghĩa cho nó, sẽ tìm kiếm một định nghĩa trong các tệp ở bên phải. Nếu cuối cùng nó tìm thấy một định nghĩa, tham chiếu được giải quyết. Nếu bất kỳ tài liệu tham khảo nào vẫn chưa được giải quyết ở cuối, liên kết không thành công: trình liên kết không tìm kiếm ngược.
Đầu tiên, ví dụ 1 , với thư viện tĩnhmy_lib.a
Một thư viện tĩnh là một kho lưu trữ được lập chỉ mục của các tệp đối tượng. Khi trình liên kết tìm thấy -lmy_lib
trong chuỗi liên kết và chỉ ra rằng điều này đề cập đến thư viện tĩnh ./libmy_lib.a
, nó muốn biết liệu chương trình của bạn có cần bất kỳ tệp đối tượng nào không libmy_lib.a
.
Chỉ có tệp đối tượng libmy_lib.a
, cụ thể là my_lib.o
, và chỉ có một thứ được định nghĩa trong my_lib.o
đó là hàm hw
.
Trình liên kết sẽ quyết định rằng chương trình của bạn cần my_lib.o
nếu và chỉ khi nó biết rằng chương trình của bạn đề cập đến hw
, trong một hoặc nhiều tệp đối tượng mà nó đã thêm vào chương trình và không có tệp đối tượng nào được thêm vào có chứa định nghĩa cho hw
.
Nếu đó là sự thật, thì trình liên kết sẽ trích xuất một bản sao my_lib.o
từ thư viện và thêm nó vào chương trình của bạn. Sau đó, chương trình của bạn chứa một định nghĩa cho hw
, vì vậy các tham chiếu của nó hw
được giải quyết .
Khi bạn cố gắng liên kết chương trình như:
$ gcc -o eg1 -L. -lmy_lib eg1.o
Trình liên kết chưa được thêm vào eg1.o
chương trình khi nó nhìn thấy
-lmy_lib
. Bởi vì tại thời điểm đó, nó đã không nhìn thấy eg1.o
. Chương trình của bạn vẫn chưa thực hiện bất kỳ tài liệu tham khảo để hw
: nó chưa thực hiện bất kỳ tài liệu tham khảo ở tất cả , bởi vì tất cả các tài liệu tham khảo mà nó chỉ ra trong eg1.o
.
Vì vậy, trình liên kết không thêm my_lib.o
vào chương trình và không sử dụng thêm cho libmy_lib.a
.
Tiếp theo, nó tìm thấy eg1.o
và thêm nó vào chương trình. Một tệp đối tượng trong chuỗi liên kết luôn được thêm vào chương trình. Bây giờ, chương trình đưa ra một tham chiếu đến hw
, và không chứa định nghĩa về hw
; nhưng không còn gì trong chuỗi liên kết có thể cung cấp định nghĩa còn thiếu. Tham chiếu đến hw
kết thúc không được giải quyết , và liên kết không thành công.
Thứ hai, ví dụ 2 , với thư viện dùng chunglibz
Một thư viện chia sẻ không phải là một kho lưu trữ các tệp đối tượng hoặc bất cứ thứ gì giống như nó. Nó giống như một chương trình không có main
chức năng và thay vào đó hiển thị nhiều biểu tượng khác mà nó xác định, để các chương trình khác có thể sử dụng chúng khi chạy.
Nhiều Linux distro hiện nay cấu hình GCC họ toolchain để trình điều khiển ngôn ngữ của nó ( gcc
, g++
, gfortran
vv) chỉ thị cho mối liên kết hệ thống ( ld
) để liên kết các thư viện chia sẻ trên một khi cần cơ sở. Bạn đã có một trong những distro đó.
Điều này có nghĩa là khi trình liên kết tìm thấy -lz
trong chuỗi liên kết và chỉ ra rằng điều này đề cập đến thư viện dùng chung (giả sử) /usr/lib/x86_64-linux-gnu/libz.so
, nó muốn biết liệu có bất kỳ tài liệu tham khảo nào mà nó đã thêm vào chương trình của bạn chưa được xác định có định nghĩa không xuất khẩu bởilibz
Nếu đó là sự thật, thì trình liên kết sẽ không sao chép bất kỳ đoạn nào ra libz
và thêm chúng vào chương trình của bạn; thay vào đó, nó sẽ chỉ kiểm tra mã chương trình của bạn để: -
Khi chạy, trình tải chương trình hệ thống sẽ tải một bản sao libz
vào cùng tiến trình với chương trình của bạn bất cứ khi nào nó tải một bản sao của chương trình của bạn, để chạy nó.
Trong thời gian chạy, bất cứ khi nào chương trình của bạn đề cập đến một cái gì đó được định nghĩa
libz
, tham chiếu đó sử dụng định nghĩa được xuất bởi bản sao libz
trong cùng một quy trình.
Chương trình của bạn muốn chỉ một điều có định nghĩa được xuất bởi libz
, cụ thể là hàm zlibVersion
, được đề cập chỉ một lần, trong eg2.c
. Nếu trình liên kết thêm tham chiếu đó vào chương trình của bạn và sau đó tìm định nghĩa được xuất bởi libz
, tham chiếu được giải quyết
Nhưng khi bạn cố gắng liên kết chương trình như:
gcc -o eg2 -lz eg2.o
thứ tự của các sự kiện là sai theo cách tương tự như với ví dụ 1. Tại thời điểm khi trình liên kết tìm thấy -lz
, không có tài liệu tham khảo nào trong chương trình: tất cả chúng đều eg2.o
chưa được nhìn thấy. Vì vậy, trình liên kết quyết định nó không có sử dụng cho libz
. Khi nó đạt đến eg2.o
, thêm nó vào chương trình, và sau đó có tham chiếu không xác định zlibVersion
, chuỗi liên kết đã kết thúc; tham chiếu đó không được giải quyết và liên kết thất bại.
Cuối cùng, pkg-config
biến thể của ví dụ 2 có một lời giải thích rõ ràng. Sau khi mở rộng vỏ:
gcc -o eg2 $(pkg-config --libs zlib) eg2.o
trở thành:
gcc -o eg2 -lz eg2.o
đó chỉ là ví dụ 2 một lần nữa.
Liên kết:
gcc -o eg2 -lz eg2.o
chỉ hoạt động tốt cho bạn!
(Hoặc: Liên kết đó hoạt động tốt với bạn trên, giả sử, Fedora 23, nhưng không thành công trên Ubuntu 16.04)
Đó là bởi vì bản phân phối mà liên kết hoạt động là một trong những bản không cấu hình chuỗi công cụ GCC của nó để liên kết các thư viện dùng chung khi cần .
Trước đây, việc các hệ thống giống như unix liên kết các thư viện tĩnh và chia sẻ theo các quy tắc khác nhau là điều bình thường. Các thư viện tĩnh trong chuỗi liên kết được liên kết trên cơ sở khi cần được giải thích trong ví dụ 1, nhưng các thư viện dùng chung được liên kết vô điều kiện.
Hành vi này là kinh tế tại thời gian liên kết vì trình liên kết không phải suy ngẫm liệu chương trình chia sẻ có cần thiết cho chương trình hay không: nếu đó là thư viện dùng chung, hãy liên kết nó. Và hầu hết các thư viện trong hầu hết các liên kết là thư viện chia sẻ. Nhưng cũng có nhược điểm: -
Nó không kinh tế khi chạy , bởi vì nó có thể khiến các thư viện chia sẻ được tải cùng với một chương trình ngay cả khi không cần chúng.
Các quy tắc liên kết khác nhau cho các thư viện tĩnh và chia sẻ có thể gây nhầm lẫn cho các lập trình viên thiếu kinh nghiệm, những người có thể không biết liệu -lfoo
trong liên kết của họ sẽ giải quyết /some/where/libfoo.a
hay không /some/where/libfoo.so
và có thể không hiểu sự khác biệt giữa các thư viện chia sẻ và tĩnh.
Sự đánh đổi này đã dẫn đến tình trạng ly giáo ngày nay. Một số distro đã thay đổi quy tắc liên kết GCC của họ cho các thư viện dùng chung để nguyên tắc cần thiết áp dụng cho tất cả các thư viện. Một số distro đã bị mắc kẹt với cách cũ.
Nếu tôi chỉ làm:
$ gcc -o eg1 -I. -L. -lmy_lib eg1.c
chắc chắn gcc phải biên dịch eg1.c
trước, và sau đó liên kết tệp đối tượng kết quả với libmy_lib.a
. Vậy làm thế nào để nó không thể biết rằng tệp đối tượng là cần thiết khi nó thực hiện liên kết?
Bởi vì biên dịch và liên kết với một lệnh duy nhất không thay đổi thứ tự của chuỗi liên kết.
Khi bạn chạy lệnh ở trên, hãy chỉ gcc
ra rằng bạn muốn biên dịch + liên kết. Vì vậy, đằng sau hậu trường, nó tạo ra một lệnh biên dịch và chạy nó, sau đó tạo một lệnh liên kết và chạy nó, như thể bạn đã chạy hai lệnh:
$ gcc -I. -c -o eg1.o eg1.c
$ gcc -o eg1 -L. -lmy_lib eg1.o
Vì vậy, các liên kết không giống như nó nếu bạn làm chạy hai lệnh. Sự khác biệt duy nhất bạn nhận thấy trong thất bại là gcc đã tạo một tệp đối tượng tạm thời trong trường hợp biên dịch + liên kết, bởi vì bạn không bảo nó sử dụng eg1.o
. Chúng tôi thấy:
/tmp/ccQk1tvs.o: In function `main'
thay vì:
eg1.o: In function `main':
Thứ tự trong đó các thư viện liên kết phụ thuộc lẫn nhau được chỉ định là sai
Đặt các thư viện phụ thuộc lẫn nhau theo thứ tự sai chỉ là một cách mà bạn có thể nhận được các tệp cần định nghĩa về những thứ đến sau trong liên kết so với các tệp cung cấp định nghĩa. Đặt các thư viện trước các tệp đối tượng đề cập đến chúng là một cách khác để gây ra lỗi tương tự.
Đưa ra đoạn mã của một kiểu mẫu với toán tử bạn bè (hoặc hàm);
template <typename T>
class Foo {
friend std::ostream& operator<< (std::ostream& os, const Foo<T>& a);
};
Việc operator<<
này đang được khai báo là một hàm không phải mẫu. Đối với mỗi loại T
được sử dụng với Foo
, cần phải có một templated operator<<
. Ví dụ, nếu có một kiểu Foo<int>
khai báo, thì phải có một toán tử thực hiện như sau;
std::ostream& operator<< (std::ostream& os, const Foo<int>& a) {/*...*/}
Vì nó không được thực hiện, trình liên kết không tìm thấy nó và dẫn đến lỗi.
Để sửa lỗi này, bạn có thể khai báo một toán tử mẫu trước Foo
kiểu và sau đó khai báo là một người bạn, cách khởi tạo thích hợp. Cú pháp hơi khó xử, nhưng có vẻ như sau;
// forward declare the Foo
template <typename>
class Foo;
// forward declare the operator <<
template <typename T>
std::ostream& operator<<(std::ostream&, const Foo<T>&);
template <typename T>
class Foo {
friend std::ostream& operator<< <>(std::ostream& os, const Foo<T>& a);
// note the required <> ^^^^
// ...
};
template <typename T>
std::ostream& operator<<(std::ostream&, const Foo<T>&)
{
// ... implement the operator
}
Đoạn mã trên giới hạn tình bạn của toán tử đối với việc khởi tạo tương ứng của Foo
, tức là việc operator<< <int>
khởi tạo được giới hạn để truy cập vào các thành viên riêng của việc khởi tạo Foo<int>
.
Các lựa chọn thay thế bao gồm;
Cho phép tình bạn mở rộng đến tất cả các phần khởi tạo của các mẫu, như sau;
template <typename T>
class Foo {
template <typename T1>
friend std::ostream& operator<<(std::ostream& os, const Foo<T1>& a);
// ...
};
Hoặc, việc thực hiện operator<<
có thể được thực hiện nội tuyến bên trong định nghĩa lớp;
template <typename T>
class Foo {
friend std::ostream& operator<<(std::ostream& os, const Foo& a)
{ /*...*/ }
// ...
};
Lưu ý , khi khai báo của toán tử (hoặc hàm) chỉ xuất hiện trong lớp, tên không có sẵn cho tra cứu "bình thường", chỉ dành cho tra cứu phụ thuộc đối số, từ cppreference ;
Tên được khai báo đầu tiên trong khai báo bạn bè trong lớp hoặc mẫu lớp X trở thành thành viên của không gian tên kèm theo trong cùng của X, nhưng không thể truy cập để tra cứu (ngoại trừ tra cứu phụ thuộc đối số xem xét X) trừ khi khai báo khớp với phạm vi không gian tên là cung cấp ...
Có thêm đọc về bạn bè mẫu tại cppreference và C ++ FAQ .
Danh sách mã hiển thị các kỹ thuật trên .
Như một lưu ý phụ cho mẫu mã thất bại; g ++ cảnh báo về điều này như sau
warning: friend declaration 'std::ostream& operator<<(...)' declares a non-template function [-Wnon-template-friend]
note: (if this is not what you intended, make sure the function template has already been declared and add <> after the function name here)
Lỗi trình liên kết có thể xảy ra khi tệp tiêu đề và thư viện dùng chung được liên kết (tệp .lib) không đồng bộ. Hãy để tôi giải thích.
Làm thế nào để liên kết làm việc? Trình liên kết khớp với một khai báo hàm (được khai báo trong tiêu đề) với định nghĩa của nó (trong thư viện dùng chung) bằng cách so sánh chữ ký của chúng. Bạn có thể gặp lỗi liên kết nếu trình liên kết không tìm thấy định nghĩa hàm hoàn toàn khớp.
Có thể vẫn nhận được một lỗi liên kết mặc dù khai báo và định nghĩa dường như khớp? Đúng! Chúng có thể trông giống nhau trong mã nguồn, nhưng nó thực sự phụ thuộc vào những gì trình biên dịch nhìn thấy. Về cơ bản, bạn có thể kết thúc với một tình huống như thế này:
// header1.h
typedef int Number;
void foo(Number);
// header2.h
typedef float Number;
void foo(Number); // this only looks the same lexically
Lưu ý rằng mặc dù cả hai khai báo hàm trông giống hệt nhau trong mã nguồn, nhưng chúng thực sự khác nhau theo trình biên dịch.
Bạn có thể hỏi làm thế nào một người kết thúc trong một tình huống như vậy? Bao gồm các con đường tất nhiên! Nếu khi biên dịch thư viện dùng chung, đường dẫn bao gồm dẫn đến header1.h
và bạn kết thúc sử dụng header2.h
trong chương trình của riêng mình, bạn sẽ phải gãi đầu tiêu đề của bạn tự hỏi điều gì đã xảy ra (ý định chơi chữ).
Một ví dụ về cách điều này có thể xảy ra trong thế giới thực được giải thích dưới đây.
Tôi có hai dự án: graphics.lib
và main.exe
. Cả hai dự án đều phụ thuộc vào common_math.h
. Giả sử thư viện xuất hàm sau:
// graphics.lib
#include "common_math.h"
void draw(vec3 p) { ... } // vec3 comes from common_math.h
Và sau đó bạn tiếp tục và đưa thư viện vào dự án của riêng bạn.
// main.exe
#include "other/common_math.h"
#include "graphics.h"
int main() {
draw(...);
}
Bùng nổ! Bạn nhận được một lỗi liên kết và bạn không biết tại sao nó lại thất bại. Lý do là thư viện chung sử dụng các phiên bản khác nhau của cùng một bao gồm common_math.h
(tôi đã làm rõ ở đây trong ví dụ bằng cách bao gồm một đường dẫn khác, nhưng nó có thể không phải lúc nào cũng rõ ràng. Có thể đường dẫn bao gồm khác nhau trong cài đặt trình biên dịch) .
Lưu ý trong ví dụ này, trình liên kết sẽ cho bạn biết nó không thể tìm thấy draw()
, khi thực tế bạn biết nó rõ ràng đang được thư viện xuất khẩu. Bạn có thể dành hàng giờ gãi đầu tự hỏi điều gì đã xảy ra. Vấn đề là, trình liên kết nhìn thấy một chữ ký khác vì các loại tham số hơi khác nhau. Trong ví dụ này, vec3
là một loại khác nhau trong cả hai dự án liên quan đến trình biên dịch. Điều này có thể xảy ra vì chúng đến từ hai tệp bao gồm hơi khác nhau (có thể các tệp bao gồm đến từ hai phiên bản khác nhau của thư viện).
DUMPBIN là bạn của bạn, nếu bạn đang sử dụng Visual Studio. Tôi chắc chắn các trình biên dịch khác có các công cụ tương tự khác.
Quá trình diễn ra như sau:
[1] Theo dự án, ý tôi là một tập hợp các tệp nguồn được liên kết với nhau để tạo ra một thư viện hoặc một tệp thực thi.
EDIT 1: Viết lại phần đầu tiên để dễ hiểu hơn. Vui lòng bình luận dưới đây để cho tôi biết nếu một cái gì đó khác cần phải được sửa chữa. Cảm ơn!
UNICODE
Định nghĩa không nhất quánMột Windows UNICODE xây dựng được xây dựng với TCHAR
, vv được định nghĩa là wchar_t
vv Khi không xây dựng với UNICODE
định nghĩa là xây dựng với TCHAR
định nghĩa là char
vv Những UNICODE
và _UNICODE
định nghĩa ảnh hưởng đến tất cả các " T
" loại chuỗi ; LPTSTR
, LPCTSTR
Và nai sừng tấm của họ.
Xây dựng một thư viện với UNICODE
định nghĩa và cố gắng liên kết nó trong một dự án UNICODE
không được xác định sẽ dẫn đến lỗi liên kết do sẽ có sự không phù hợp trong định nghĩa TCHAR
; char
vs wchar_t
.
Lỗi thường bao gồm một hàm một giá trị với một loại char
hoặc wchar_t
dẫn xuất, chúng cũng có thể bao gồm std::basic_string<>
vv. Khi duyệt qua chức năng bị ảnh hưởng trong mã, thường sẽ có một tham chiếu đến TCHAR
hoặc std::basic_string<TCHAR>
v.v ... Đây là dấu hiệu cho biết mã ban đầu được dành cho cả bản dựng UNICODE và ký tự nhiều byte (hoặc "hẹp") .
Để sửa lỗi này, hãy xây dựng tất cả các thư viện và dự án cần thiết với định nghĩa nhất quán UNICODE
(và _UNICODE
).
Điều này có thể được thực hiện với một trong hai;
#define UNICODE
#define _UNICODE
Hoặc trong cài đặt dự án;
Thuộc tính dự án> Chung> Mặc định dự án> Bộ ký tự
Hoặc trên dòng lệnh;
/DUNICODE /D_UNICODE
Giải pháp thay thế cũng được áp dụng, nếu UNICODE không được sử dụng, hãy đảm bảo rằng các định nghĩa không được đặt và / hoặc cài đặt nhiều ký tự được sử dụng trong các dự án và được áp dụng nhất quán.
Đừng quên thống nhất giữa các bản dựng "Phát hành" và "Gỡ lỗi".
Việc "dọn dẹp" bản dựng có thể loại bỏ "gỗ chết" có thể bị bỏ lại xung quanh các bản dựng trước, bản dựng không thành công, bản dựng không hoàn chỉnh và các vấn đề xây dựng liên quan đến hệ thống xây dựng khác.
Nói chung, IDE hoặc bản dựng sẽ bao gồm một số dạng hàm "sạch", nhưng điều này có thể không được cấu hình chính xác (ví dụ: trong tệp tạo thủ công) hoặc có thể không thành công (ví dụ: các nhị phân trung gian hoặc kết quả là chỉ đọc).
Khi "sạch" đã hoàn thành, hãy xác minh rằng "sạch" đã thành công và tất cả các tệp trung gian được tạo (ví dụ: tệp tạo tự động) đã được xóa thành công.
Quá trình này có thể được xem như là một phương sách cuối cùng, nhưng thường là bước đầu tiên tốt ; đặc biệt là nếu mã liên quan đến lỗi gần đây đã được thêm vào (cục bộ hoặc từ kho lưu trữ nguồn).
const
khai báo / định nghĩa biến (chỉ C ++)Đối với những người đến từ C, có thể có một điều ngạc nhiên là trong C ++, các const
biến toàn cục có liên kết bên trong (hoặc tĩnh). Trong C, đây không phải là trường hợp, vì tất cả các biến toàn cục đều ngầm định extern
(nghĩa là khi static
thiếu từ khóa).
Thí dụ:
// file1.cpp
const int test = 5; // in C++ same as "static const int test = 5"
int test2 = 5;
// file2.cpp
extern const int test;
extern int test2;
void foo()
{
int x = test; // linker error in C++ , no error in C
int y = test2; // no problem
}
đúng sẽ là sử dụng tệp tiêu đề và đưa nó vào file2.cpp và file1.cpp
extern const int test;
extern int test2;
Ngoài ra, người ta có thể khai báo const
biến trong file1.cpp với tường minhextern
Mặc dù đây là một câu hỏi khá cũ với nhiều câu trả lời được chấp nhận, tôi muốn chia sẻ cách khắc phục lỗi "tham chiếu không xác định" tối nghĩa .
Tôi đã sử dụng một bí danh để tham khảo std::filesystem::path
: hệ thống tập tin nằm trong thư viện tiêu chuẩn kể từ C ++ 17 nhưng chương trình của tôi cũng cần phải biên dịch trong C ++ 14 vì vậy tôi quyết định sử dụng một bí danh biến:
#if (defined _GLIBCXX_EXPERIMENTAL_FILESYSTEM) //is the included filesystem library experimental? (C++14 and newer: <experimental/filesystem>)
using path_t = std::experimental::filesystem::path;
#elif (defined _GLIBCXX_FILESYSTEM) //not experimental (C++17 and newer: <filesystem>)
using path_t = std::filesystem::path;
#endif
Giả sử tôi có ba tệp: main.cpp, file.h, file.cpp:
Lưu ý các thư viện khác nhau được sử dụng trong main.cpp và file.h. Vì main.cpp # bao gồm " file.h " sau < filesystem >, phiên bản của hệ thống tập tin được sử dụng là C ++ 17 . Tôi đã sử dụng để biên dịch chương trình với các lệnh sau:
$ g++ -g -std=c++17 -c main.cpp
-> biên dịch main.cpp thành main.o
$g++ -g -std=c++17 -c file.cpp
-> biên dịch file.cpp và file.h thành file.o
$ g++ -g -std=c++17 -o executable main.o file.o -lstdc++fs
-> liên kết main.o và file.o
Bằng cách này, bất kỳ hàm nào có trong file.o và được sử dụng trong main.o yêu cầupath_t
đã đưa ra các lỗi "tham chiếu không xác định" vì main.o đã đề cập đến std::filesystem::path
nhưng file.o đến std::experimental::filesystem::path
.
Để sửa lỗi này, tôi chỉ cần thay đổi <thử nghiệm :: hệ thống tập tin> trong tệp.h thành <hệ thống tập tin> .
Hành vi mặc định của gcc là tất cả các biểu tượng đều có thể nhìn thấy. Tuy nhiên, khi các đơn vị dịch được xây dựng với tùy chọn -fvisibility=hidden
, chỉ các chức năng / ký hiệu được đánh dấu bằng__attribute__ ((visibility ("default")))
là bên ngoài trong đối tượng chia sẻ kết quả.
Bạn có thể kiểm tra xem các biểu tượng bạn đang tìm kiếm có bên ngoài hay không bằng cách gọi:
# -D shows (global) dynamic symbols that can be used from the outside of XXX.so
nm -D XXX.so | grep MY_SYMBOL
các ký hiệu ẩn / cục bộ được hiển thị bằng nm
loại ký hiệu chữ thường, ví dụ t
thay vì `T cho phần mã:
nm XXX.so
00000000000005a7 t HIDDEN_SYMBOL
00000000000005f8 T VISIBLE_SYMBOL
Bạn cũng có thể sử dụng nm
với tùy chọn -C
để khử tên (nếu C ++ đã được sử dụng).
Tương tự như Windows-dlls, người ta sẽ đánh dấu các hàm công khai bằng một định nghĩa, ví dụ DLL_PUBLIC
được xác định là:
#define DLL_PUBLIC __attribute__ ((visibility ("default")))
DLL_PUBLIC int my_public_function(){
...
}
Tương đương với phiên bản Windows / MSVC:
#ifdef BUILDING_DLL
#define DLL_PUBLIC __declspec(dllexport)
#else
#define DLL_PUBLIC __declspec(dllimport)
#endif
Thông tin thêm về khả năng hiển thị có thể được tìm thấy trên wiki gcc.
Khi một đơn vị dịch được biên dịch với -fvisibility=hidden
các ký hiệu kết quả vẫn có liên kết ngoài (được hiển thị với loại ký hiệu chữ hoanm
) và có thể được sử dụng cho liên kết ngoài mà không gặp vấn đề gì nếu các tệp đối tượng trở thành một phần của thư viện tĩnh. Liên kết chỉ trở thành cục bộ khi các tệp đối tượng được liên kết vào thư viện dùng chung.
Để tìm các ký hiệu trong tệp đối tượng được ẩn chạy:
>>> objdump -t XXXX.o | grep hidden
0000000000000000 g F .text 000000000000000b .hidden HIDDEN_SYMBOL1
000000000000000b g F .text 000000000000000b .hidden HIDDEN_SYMBOL2
Kiến trúc khác nhau
Bạn có thể thấy một thông báo như:
library machine type 'x64' conflicts with target machine type 'X86'
Trong trường hợp đó, điều đó có nghĩa là các biểu tượng có sẵn dành cho một kiến trúc khác với kiến trúc bạn đang biên dịch.
Trên Visual Studio, điều này là do "Nền tảng" sai và bạn cần chọn đúng hoặc cài đặt phiên bản thích hợp của thư viện.
Trên Linux, có thể do thư mục thư viện sai (sử dụng lib
thay vì lib64
ví dụ).
Trên MacOS, có tùy chọn vận chuyển cả hai kiến trúc trong cùng một tệp. Có thể là liên kết hy vọng cả hai phiên bản sẽ ở đó, nhưng chỉ có một phiên bản. Nó cũng có thể là một vấn đề với thư mục lib
/ sai lib64
trong đó thư viện được chọn.