ข้อผิดพลาดของการอ้างอิงที่ไม่ได้กำหนด / การอ้างอิงสัญลักษณ์ภายนอกที่ไม่ได้แก้ไขคืออะไร สาเหตุที่พบบ่อยคืออะไรและจะแก้ไข / ป้องกันได้อย่างไร
รู้สึกอิสระที่จะแก้ไข / เพิ่มของคุณเอง
ข้อผิดพลาดของการอ้างอิงที่ไม่ได้กำหนด / การอ้างอิงสัญลักษณ์ภายนอกที่ไม่ได้แก้ไขคืออะไร สาเหตุที่พบบ่อยคืออะไรและจะแก้ไข / ป้องกันได้อย่างไร
รู้สึกอิสระที่จะแก้ไข / เพิ่มของคุณเอง
คำตอบ:
การคอมไพล์โปรแกรม C ++ นั้นเกิดขึ้นในหลายขั้นตอนตามที่ระบุโดย2.2 (ให้เครดิต Keith Thompson สำหรับการอ้างอิง) :
ความสำคัญในหมู่กฎไวยากรณ์ของการแปลจะถูกระบุโดยขั้นตอนต่อไปนี้[ดูเชิงอรรถ]
- อักขระของแฟ้มต้นฉบับทางกายภาพถูกแมปในลักษณะที่กำหนดการนำไปใช้กับชุดอักขระของแหล่งข้อมูลพื้นฐาน (แนะนำอักขระบรรทัดใหม่สำหรับตัวบ่งชี้การสิ้นสุดของบรรทัด) หากจำเป็น [SNIP]
- แต่ละอินสแตนซ์ของอักขระแบ็กสแลช (\) ตามด้วยอักขระบรรทัดใหม่ทันทีจะถูกลบออกทำให้เกิดการเชื่อมต่อกับแหล่งที่มาทางกายภาพเพื่อสร้างบรรทัดของแหล่งที่มาทางตรรกะ [SNIP]
- ไฟล์ต้นฉบับจะถูกแยกออกเป็นโทเค็นการประมวลผลล่วงหน้า (2.5) และลำดับของอักขระช่องว่าง (รวมถึงความคิดเห็น) [SNIP]
- การประมวลผลคำสั่งล่วงหน้าจะถูกดำเนินการการเรียกใช้แมโครจะถูกขยายและนิพจน์โอเปอเรเตอร์ของ _Pragma unary จะถูกดำเนินการ [SNIP]
- อักขระต้นทางแต่ละชุดเป็นสมาชิกในตัวอักษรตัวอักษรหรือสตริงตัวอักษรเช่นเดียวกับแต่ละลำดับหนีและชื่อตัวละครสากลในตัวอักษรตัวอักษรหรือตัวอักษรสตริงที่ไม่ดิบจะถูกแปลงเป็นสมาชิกที่สอดคล้องกันของชุดอักขระการดำเนินการ; [SNIP]
- โทเค็นตัวอักษรของสตริงที่อยู่ติดกันจะถูกตัดแบ่ง
- อักขระช่องว่างที่แยกโทเค็นไม่สำคัญอีกต่อไป โทเค็นการประมวลผลล่วงหน้าแต่ละรายการจะถูกแปลงเป็นโทเค็น (2.7) โทเค็นที่ได้จะถูกวิเคราะห์และแปลและแปลเป็นหน่วยการแปล [SNIP]
- หน่วยการแปลที่แปลและหน่วยการสร้างอินสแตนซ์จะรวมกันดังนี้: [SNIP]
- การอ้างอิงเอนทิตีภายนอกทั้งหมดได้รับการแก้ไขแล้ว คอมโพเนนต์ไลบรารีถูกลิงก์เพื่อตอบสนองการอ้างอิงภายนอกไปยังเอนทิตีที่ไม่ได้กำหนดไว้ในการแปลปัจจุบัน เอาต์พุตตัวแปลทั้งหมดดังกล่าวจะถูกรวบรวมไว้ในอิมเมจโปรแกรมซึ่งมีข้อมูลที่จำเป็นสำหรับการดำเนินการในสภาพแวดล้อมการทำงาน (เน้นที่เหมือง)
[เชิงอรรถ]การติดตั้งใช้งานจะต้องทำตัวราวกับว่าขั้นตอนที่แยกต่างหากเหล่านี้เกิดขึ้นแม้ว่าในทางปฏิบัติขั้นตอนที่แตกต่างกันอาจถูกพับเข้าด้วยกัน
ข้อผิดพลาดที่ระบุเกิดขึ้นในระหว่างขั้นตอนสุดท้ายของการรวบรวมซึ่งส่วนใหญ่เรียกว่าการเชื่อมโยง นั่นหมายความว่าคุณรวบรวมไฟล์การใช้งานเป็นไฟล์หรือไลบรารีและตอนนี้คุณต้องการให้มันทำงานร่วมกัน
สมมติว่าคุณกำหนดสัญลักษณ์ในa
a.cpp
ตอนนี้b.cpp
ประกาศสัญลักษณ์นั้นและใช้มัน ก่อนที่จะทำการเชื่อมโยงเพียงแค่สมมติว่าสัญลักษณ์นั้นถูกกำหนดไว้ที่ไหนสักแห่งแต่มันยังไม่สนใจว่าจะอยู่ที่ไหน ขั้นตอนการเชื่อมโยงมีหน้าที่ในการค้นหาสัญลักษณ์และเชื่อมโยงอย่างถูกต้องกับb.cpp
(จริง ๆ แล้วกับวัตถุหรือไลบรารีที่ใช้)
หากคุณใช้ Microsoft Visual Studio คุณจะเห็นว่าโครงการสร้าง.lib
ไฟล์ สิ่งเหล่านี้มีตารางของสัญลักษณ์ที่ส่งออกและตารางของสัญลักษณ์ที่นำเข้า สัญลักษณ์ที่นำเข้าจะได้รับการแก้ไขกับไลบรารีที่คุณเชื่อมโยงและสัญลักษณ์ที่ส่งออกจะมีให้สำหรับห้องสมุดที่ใช้งานนั้น.lib
(ถ้ามี)
มีกลไกที่คล้ายกันสำหรับคอมไพเลอร์ / แพลตฟอร์มอื่น ๆ
ข้อความผิดพลาดที่พบบ่อยerror LNK2001
, error LNK1120
, error LNK2019
สำหรับMicrosoft Visual Studioและundefined reference to
symbolnameสำหรับGCC
รหัส:
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;
}
จะสร้างข้อผิดพลาดต่อไปนี้ด้วย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
และข้อผิดพลาดที่คล้ายกันกับ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
สาเหตุที่พบบ่อย ได้แก่ :
#pragma
(Microsoft Visual Studio)UNICODE
คำจำกัดความที่ไม่สอดคล้องกันvirtual
destructor ที่บริสุทธิ์ต้องมีการนำไปปฏิบัติการประกาศ destructor pure ยังคงต้องการให้คุณกำหนด (ต่างจากฟังก์ชั่นทั่วไป):
struct X
{
virtual ~X() = 0;
};
struct Y : X
{
~Y() {}
};
int main()
{
Y y;
}
//X::~X(){} //uncomment this line for successful definition
สิ่งนี้เกิดขึ้นเพราะ destructors คลาสฐานถูกเรียกเมื่อวัตถุถูกทำลายโดยปริยายดังนั้นจึงจำเป็นต้องมีคำจำกัดความ
virtual
เมธอดต้องถูกนำมาใช้หรือกำหนดเป็น pureสิ่งนี้คล้ายกับvirtual
วิธีที่ไม่มีคำนิยามโดยมีเหตุผลเพิ่มเติมว่าการประกาศล้วนสร้าง vummy vummy และคุณอาจได้รับข้อผิดพลาด linker โดยไม่ต้องใช้ฟังก์ชัน:
struct X
{
virtual void foo();
};
struct Y : X
{
void foo() {}
};
int main()
{
Y y; //linker error although there was no call to X::foo
}
เพื่อการทำงานให้ประกาศX::foo()
อย่างบริสุทธิ์
struct X
{
virtual void foo() = 0;
};
virtual
สมาชิกระดับสมาชิกบางคนจำเป็นต้องกำหนดแม้ว่าจะไม่ได้ใช้อย่างชัดเจน:
struct A
{
~A();
};
ต่อไปนี้จะทำให้เกิดข้อผิดพลาด:
A a; //destructor undefined
การใช้งานสามารถเป็นแบบอินไลน์ในการกำหนดชั้นเรียนของตัวเอง:
struct A
{
~A() {}
};
หรือนอก:
A::~A() {}
หากการใช้งานอยู่นอกข้อกำหนดคลาส แต่ในส่วนหัววิธีการจะต้องมีการทำเครื่องหมายเป็นinline
เพื่อป้องกันไม่ให้หลายคำนิยาม
วิธีสมาชิกทั้งหมดที่ใช้จะต้องมีการกำหนดถ้าใช้
struct A
{
void foo();
};
void foo() {}
int main()
{
A a;
a.foo();
}
ความหมายควรจะเป็น
void A::foo() {}
static
สมาชิกข้อมูลต้องถูกกำหนดนอกคลาสในหน่วยการแปลเดียว :struct X
{
static int x;
};
int main()
{
int x = X::x;
}
//int X::x; //uncomment this line to define X::x
initializer สามารถจัดเตรียมไว้สำหรับstatic
const
สมาชิกข้อมูลประเภท integral หรือ enumeration ภายในการกำหนดคลาส; อย่างไรก็ตามการใช้งานของสมาชิกรายนี้จะยังคงต้องการคำจำกัดความขอบเขตของเนมสเปซตามที่อธิบายไว้ข้างต้น C ++ 11 อนุญาตให้เริ่มต้นภายในชั้นเรียนสำหรับstatic const
สมาชิกข้อมูลทั้งหมด
โดยทั่วไปหน่วยการแปลแต่ละหน่วยจะสร้างไฟล์วัตถุที่มีคำจำกัดความของสัญลักษณ์ที่กำหนดไว้ในหน่วยการแปลนั้น ในการใช้สัญลักษณ์เหล่านั้นคุณต้องเชื่อมโยงกับไฟล์วัตถุเหล่านั้น
ภายใต้gccคุณจะต้องระบุไฟล์วัตถุทั้งหมดที่จะเชื่อมโยงเข้าด้วยกันในบรรทัดคำสั่งหรือรวบรวมไฟล์การนำไปใช้ร่วมกัน
g++ -o test objectFile1.o objectFile2.o -lLibraryName
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 จะใส่ข้อมูลลงในกล่องที่ด้านล่างของแต่ละฟังก์ชันในส่วนที่เรียกว่า "Library"
gcc main.c
แทนgcc main.c other.c
(ซึ่งผู้เริ่มต้นมักจะทำก่อนที่โครงการของพวกเขาจะมีขนาดใหญ่พอที่จะสร้างไฟล์. o)
การประกาศตัวแปรทั่วไปคือ
extern int x;
เนื่องจากนี่เป็นเพียงการประกาศจึงจำเป็นต้องมีคำนิยามเดียว คำนิยามที่สอดคล้องกันจะเป็น:
int x;
ตัวอย่างเช่นต่อไปนี้จะสร้างข้อผิดพลาด:
extern int x;
int main()
{
x = 0;
}
//int x; // uncomment this line for successful definition
หมายเหตุที่คล้ายกันใช้กับฟังก์ชั่น ประกาศฟังก์ชั่นโดยไม่กำหนดมันนำไปสู่ข้อผิดพลาด:
void foo(); // declaration only
int main()
{
foo();
}
//void foo() {} //uncomment this line for successful definition
ระวังว่าฟังก์ชั่นที่คุณใช้นั้นตรงกับฟังก์ชั่นที่คุณประกาศ ตัวอย่างเช่นคุณอาจมีคุณสมบัติ CV-qualifier ไม่ตรงกัน:
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)
ตัวอย่างอื่น ๆ ที่ไม่ตรงกัน ได้แก่
ข้อความแสดงข้อผิดพลาดจากคอมไพเลอร์มักจะให้การประกาศตัวแปรหรือฟังก์ชั่นเต็มรูปแบบที่คุณประกาศ แต่ไม่เคยกำหนดไว้ เปรียบเทียบอย่างละเอียดกับคำจำกัดความที่คุณให้ไว้ ตรวจสอบให้แน่ใจว่าทุกรายละเอียดตรงกัน
#includes
ไม่ได้ถูกเพิ่มลงในไดเรกทอรีต้นทางจะตกอยู่ภายใต้หมวดหมู่ของคำจำกัดความที่ขาดหายไป
ลำดับการเชื่อมโยงไลบรารีจะมีความสำคัญหากห้องสมุดขึ้นอยู่กับแต่ละอื่น ๆ โดยทั่วไปถ้าห้องสมุด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: creating libA.a
a - A.o
$ ar rvs libB.a B.o
ar: creating libB.a
a - B.o
รวบรวม:
$ 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
ดังนั้นจะทำซ้ำอีกครั้งสั่งซื้อไมว่า!
"การอ้างอิงที่ไม่ได้กำหนด / สัญลักษณ์ภายนอกที่ไม่ได้แก้ไข" คืออะไร
ฉันจะพยายามอธิบายว่า "การอ้างอิงที่ไม่ได้กำหนด / สัญลักษณ์ภายนอกที่ยังไม่ได้แก้ไข" คืออะไร
หมายเหตุ: ฉันใช้ g ++ และ Linux และตัวอย่างทั้งหมดเป็นของมัน
ตัวอย่างเช่นเรามีรหัส
// 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;
}
และ
// 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);
}
สร้างไฟล์วัตถุ
$ g++ -c src1.cpp -o src1.o
$ g++ -c src2.cpp -o src2.o
หลังจากเฟสแอสเซมเบลอร์เรามีไฟล์วัตถุซึ่งมีสัญลักษณ์ใด ๆ ที่จะส่งออก ดูสัญลักษณ์
$ 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]
ฉันได้ปฏิเสธบางบรรทัดจากผลลัพธ์เนื่องจากไม่สำคัญ
ดังนั้นเราจะเห็นสัญลักษณ์ติดตามเพื่อส่งออก
[1] - this is our static (local) variable (important - Bind has a type "LOCAL")
[2] - this is our global variable
src2.cpp ส่งออกอะไรและเราไม่เห็นสัญลักษณ์ใด ๆ
เชื่อมโยงไฟล์วัตถุของเรา
$ g++ src1.o src2.o -o prog
และเรียกใช้
$ ./prog
123
Linker เห็นสัญลักษณ์ที่ส่งออกและเชื่อมโยง ตอนนี้เราพยายาม uncomment บรรทัดใน src2.cpp เหมือนที่นี่
// 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);
}
และสร้างอ็อบเจ็กต์ไฟล์อีกครั้ง
$ g++ -c src2.cpp -o src2.o
ตกลง (ไม่มีข้อผิดพลาด) เนื่องจากเราสร้างไฟล์วัตถุเท่านั้นการเชื่อมโยงยังไม่เสร็จสิ้น ลองลิงค์
$ 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
มันเกิดขึ้นเพราะ local_var_name ของเราคงที่นั่นคือไม่สามารถมองเห็นได้สำหรับโมดูลอื่น ๆ ตอนนี้ลึกซึ้งยิ่งขึ้น รับเอาต์พุตเฟสการแปล
$ 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
ดังนั้นเราจึงเห็นว่าไม่มีป้ายกำกับสำหรับ local_var_name นั่นเป็นสาเหตุที่ตัวเชื่อมโยงไม่พบ แต่เราเป็นแฮ็กเกอร์ :) และเราสามารถแก้ไขได้ เปิด src1.s ในโปรแกรมแก้ไขข้อความและเปลี่ยนแปลง
.local _ZL14local_var_name
.comm _ZL14local_var_name,4,4
ถึง
.globl local_var_name
.data
.align 4
.type local_var_name, @object
.size local_var_name, 4
local_var_name:
.long 456789
เช่นคุณควรมีด้านล่าง
.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:
; ...
เราได้เปลี่ยนการมองเห็นของ local_var_name และตั้งค่าเป็น 456789 ลองสร้างไฟล์วัตถุจากมัน
$ g++ -c src1.s -o src2.o
ตกลงดูเอาต์พุต readelf (สัญลักษณ์)
$ readelf --symbols src1.o
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 local_var_name
ตอนนี้ local_var_name มีการผูก GLOBAL (เดิมคือ LOCAL)
ลิงค์
$ g++ src1.o src2.o -o prog
และเรียกใช้
$ ./prog
123456789
ตกลงเราแฮ็คมัน :)
ดังนั้นผลลัพธ์ - "การอ้างอิงที่ไม่ได้กำหนด / ข้อผิดพลาดของสัญลักษณ์ภายนอกที่ไม่ได้แก้ไข" เกิดขึ้นเมื่อตัวเชื่อมโยงไม่สามารถค้นหาสัญลักษณ์ทั่วโลกในไฟล์วัตถุ
มีการกำหนดฟังก์ชัน (หรือตัวแปร) void foo()
ในโปรแกรม C และคุณพยายามใช้ในโปรแกรม C ++:
void foo();
int main()
{
foo();
}
ตัวเชื่อมโยง C ++ ต้องการชื่อที่ถูกเรนจ์ดังนั้นคุณต้องประกาศฟังก์ชันเป็น:
extern "C" void foo();
int main()
{
foo();
}
เท่าเทียมกันแทนที่จะถูกกำหนดไว้ในโปรแกรม C ฟังก์ชั่น (หรือตัวแปร) void foo()
ถูกกำหนดใน C ++ แต่ด้วยการเชื่อมโยง C:
extern "C" void foo();
และคุณพยายามใช้มันในโปรแกรม C ++ พร้อมลิงก์ C ++
หากไลบรารีทั้งหมดรวมอยู่ในไฟล์ส่วนหัว (และรวบรวมเป็นรหัส C) รวมถึงจะต้องมีดังนี้
extern "C" {
#include "cheader.h"
}
#ifdef __cplusplus [\n] extern"C" { [\n] #endif
และ#ifdef __cplusplus [\n] } [\n] #endif
( [\n]
เป็นสายการบินจริงคืน แต่ฉันไม่สามารถเขียนได้อย่างถูกต้องในความคิดเห็น)
extern "C" { #include <myCppHeader.h> }
:
หากสิ่งอื่นล้มเหลวให้คอมไพล์ใหม่
ฉันเพิ่งสามารถกำจัดข้อผิดพลาดภายนอกที่ยังไม่ได้แก้ไขใน Visual Studio 2012 เพียงแค่คอมไพล์ไฟล์ที่ละเมิดใหม่อีกครั้ง เมื่อฉันสร้างใหม่ข้อผิดพลาดก็หายไป
เรื่องนี้มักจะเกิดขึ้นเมื่อสอง (หรือมากกว่า) ห้องสมุดมีวงจรพึ่งพา Library A พยายามใช้สัญลักษณ์ใน B.lib และ Library B พยายามใช้สัญลักษณ์จาก A.lib ไม่มีทั้งที่จะเริ่มต้นด้วย เมื่อคุณพยายามรวบรวม A ขั้นตอนการเชื่อมโยงจะล้มเหลวเนื่องจากไม่พบ B.lib A.lib จะถูกสร้างขึ้น แต่ไม่มี dll จากนั้นคุณรวบรวม B ซึ่งจะประสบความสำเร็จและสร้าง B.lib การคอมไพล์ใหม่อีกครั้งจะทำงานได้เนื่องจากพบ B.lib
MSVS กำหนดให้คุณต้องระบุสัญลักษณ์ที่จะส่งออกและนำเข้าโดยใช้__declspec(dllexport)
และ__declspec(dllimport)
และ
การทำงานแบบคู่นี้มักจะได้รับจากการใช้มาโคร:
#ifdef THIS_MODULE
#define DLLIMPEXP __declspec(dllexport)
#else
#define DLLIMPEXP __declspec(dllimport)
#endif
แมโครTHIS_MODULE
จะถูกกำหนดในโมดูลที่ส่งออกฟังก์ชันเท่านั้น ด้วยวิธีนี้การประกาศ:
DLLIMPEXP void foo();
ขยายเป็น
__declspec(dllexport) void foo();
และบอกคอมไพเลอร์เพื่อส่งออกฟังก์ชั่นเป็นโมดูลปัจจุบันมีความหมายของมัน เมื่อรวมถึงการประกาศในโมดูลอื่นมันจะขยายไป
__declspec(dllimport) void foo();
และบอกคอมไพเลอร์ว่าคำจำกัดความเป็นหนึ่งในไลบรารีที่คุณเชื่อมโยงกับ (ดูเพิ่มเติมที่1) )
คุณสามารถนำเข้า / ส่งออกคลาสที่คล้ายคลึงกัน:
class DLLIMPEXP X
{
};
visibility
และ Windows .def
เนื่องจากสิ่งเหล่านี้จะมีผลต่อชื่อสัญลักษณ์และสถานะ
.def
ไฟล์เป็นเวลานาน อย่าลังเลที่จะเพิ่มคำตอบหรือแก้ไขคำตอบนี้
นี่เป็นหนึ่งในข้อความแสดงข้อผิดพลาดที่สับสนที่สุดที่โปรแกรมเมอร์ VC ++ ทุกคนได้เห็นครั้งแล้วครั้งเล่า มาทำความกระจ่างก่อนกัน
A. สัญลักษณ์คืออะไร? ในระยะสั้นสัญลักษณ์คือชื่อ มันอาจเป็นชื่อตัวแปรชื่อฟังก์ชั่นชื่อชั้นชื่อ typedef หรืออะไรก็ได้ยกเว้นชื่อและเครื่องหมายที่เป็นของภาษา C ++ มันเป็นผู้ใช้ที่กำหนดหรือแนะนำโดยห้องสมุดอ้างอิง (อื่นที่ผู้ใช้กำหนด)
B. ภายนอกคืออะไร
ใน VC ++ ทุกไฟล์ต้นฉบับ (.cpp, .c, ฯลฯ ) ถือเป็นหน่วยการแปลคอมไพเลอร์รวบรวมหนึ่งหน่วยในแต่ละครั้งและสร้างหนึ่งวัตถุไฟล์ (.obj) สำหรับหน่วยการแปลปัจจุบัน (โปรดทราบว่าไฟล์ส่วนหัวทุกไฟล์ที่ไฟล์ต้นฉบับนี้รวมอยู่จะได้รับการประมวลผลล่วงหน้าและจะถือเป็นส่วนหนึ่งของหน่วยการแปลนี้) ทุกอย่างภายในหน่วยการแปลจะถูกพิจารณาว่าเป็นภายใน ใน C ++ คุณอาจอ้างอิงสัญลักษณ์ภายนอกโดยใช้คำหลักเช่นextern
,__declspec (dllimport)
และอื่น ๆ
C. “ แก้ไข” คืออะไร การแก้ไขเป็นคำเชื่อมโยงเวลา ในการลิงก์เวลาเวลาตัวเชื่อมโยงพยายามค้นหานิยามภายนอกสำหรับทุกสัญลักษณ์ในอ็อบเจ็กต์ไฟล์ที่ไม่สามารถหานิยามภายใน ขอบเขตของกระบวนการค้นหานี้ ได้แก่ :
กระบวนการค้นหานี้เรียกว่าการแก้ไข
D. ในที่สุดทำไมสัญลักษณ์ภายนอกที่ไม่ได้รับการแก้ไข หากตัวลิงก์ไม่สามารถค้นหาคำจำกัดความภายนอกสำหรับสัญลักษณ์ที่ไม่มีคำจำกัดความภายในก็จะรายงานข้อผิดพลาดสัญลักษณ์ภายนอกที่ไม่ได้รับการแก้ไข
E. สาเหตุที่เป็นไปได้ของ LNK2019 : ข้อผิดพลาดสัญลักษณ์ภายนอกที่ไม่ได้รับการแก้ไข เรารู้อยู่แล้วว่าข้อผิดพลาดนี้เกิดจากตัวเชื่อมโยงล้มเหลวในการค้นหาคำจำกัดความของสัญลักษณ์ภายนอกสาเหตุที่เป็นไปได้สามารถจัดเรียงเป็น:
ตัวอย่างเช่นถ้าเรามีฟังก์ชั่นชื่อ foo ที่กำหนดใน a.cpp:
int foo()
{
return 0;
}
ใน b.cpp เราต้องการเรียกฟังก์ชั่น foo ดังนั้นเราจึงเพิ่ม
void foo();
เพื่อประกาศฟังก์ชั่น foo () และเรียกมันในเนื้อหาฟังก์ชันอื่นพูดbar()
:
void bar()
{
foo();
}
ตอนนี้เมื่อคุณสร้างรหัสนี้คุณจะได้รับข้อผิดพลาด LNK2019 ที่บ่นว่า foo เป็นสัญลักษณ์ที่ไม่ได้รับการแก้ไข ในกรณีนี้เรารู้ว่า foo () มีคำจำกัดความใน a.cpp แต่แตกต่างจากที่เราเรียก (ค่าตอบแทนต่างกัน) นี่เป็นกรณีที่คำจำกัดความมีอยู่
หากเราต้องการเรียกบางฟังก์ชั่นในไลบรารี แต่ไลบรารีนำเข้าจะไม่ถูกเพิ่มเข้าไปในรายการการพึ่งพาเพิ่มเติม (ตั้งค่าจากProject | Properties | Configuration Properties | Linker | Input | Additional Dependency
:) ของการตั้งค่าโครงการของคุณ ตอนนี้ตัวเชื่อมโยงจะรายงาน LNK2019 เนื่องจากคำจำกัดความไม่มีอยู่ในขอบเขตการค้นหาปัจจุบัน
เทมเพลตที่ไม่ได้กำหนดจะต้องมีคำจำกัดความของหน่วยการแปลทั้งหมดที่ใช้ ซึ่งหมายความว่าคุณไม่สามารถแยกคำจำกัดความของเทมเพลตเป็นไฟล์การใช้งานได้ หากคุณต้องแยกการใช้งานวิธีแก้ปัญหาตามปกติคือมีimpl
ไฟล์ที่คุณรวมไว้ที่ส่วนท้ายของส่วนหัวที่ประกาศแม่แบบ สถานการณ์ทั่วไปคือ:
template<class T>
struct X
{
void foo();
};
int main()
{
X<int> x;
x.foo();
}
//differentImplementationFile.cpp
template<class T>
void X<T>::foo()
{
}
ในการแก้ไขปัญหานี้คุณต้องย้ายคำจำกัดความของ X::foo
ไปยังไฟล์ส่วนหัวหรือสถานที่ที่หน่วยการแปลที่ใช้งานได้ปรากฏให้เห็น
เทมเพลตเฉพาะทางสามารถนำไปใช้ในไฟล์การนำไปใช้และไม่ต้องมองเห็นการใช้งาน แต่ต้องประกาศความเชี่ยวชาญก่อนหน้านี้
สำหรับคำอธิบายเพิ่มเติมและวิธีแก้ปัญหาอื่นที่เป็นไปได้ (การสร้างอินสแตนซ์ชัดเจน) ให้ดูคำถามและคำตอบนี้
การอ้างอิงที่ไม่ได้กำหนดWinMain@16
หรือการอ้างอิงจุดเข้าที่ผิดปกติ main()
(โดยเฉพาะสำหรับภาพสตูดิโอ)
คุณอาจไม่ได้เลือกประเภทโครงการที่เหมาะสมกับ IDE ที่แท้จริงของคุณ IDE อาจต้องการผูกเช่นโครงการ Windows Application กับฟังก์ชั่นจุดเข้าใช้งานดังกล่าว (ตามที่ระบุในการอ้างอิงที่ขาดหายไปด้านบน) แทนint main(int argc, char** argv);
ลายเซ็นที่ใช้กันทั่วไป
หาก IDE ของคุณสนับสนุนโครงการคอนโซลธรรมดาคุณอาจต้องการเลือกประเภทโครงการนี้แทนโครงการแอปพลิเคชัน windows
ต่อไปนี้เป็นcase1และcase2 ที่จัดการในรายละเอียดเพิ่มเติมจากปัญหาโลกแห่งความจริง
นอกจากนี้หากคุณใช้ห้องสมุดบุคคลที่สามให้แน่ใจว่าคุณมีไบนารี 32/64 บิตที่ถูกต้อง
Microsoft เสนอ a #pragma
เพื่ออ้างอิงไลบรารีที่ถูกต้อง ณ เวลาลิงก์
#pragma comment(lib, "libname.lib")
นอกเหนือจากพา ธ ไลบรารีรวมถึงไดเร็กทอรีของไลบรารีนี่ควรเป็นชื่อเต็มของไลบรารี
แพคเกจ Visual Studio NuGet จำเป็นต้องได้รับการอัพเดตสำหรับชุดเครื่องมือรุ่นใหม่
ฉันเพิ่งมีปัญหานี้พยายามเชื่อมโยง libpng กับ Visual Studio 2013 ปัญหาคือไฟล์แพคเกจมีเฉพาะไลบรารีสำหรับ Visual Studio 2010 และ 2012
ทางออกที่ถูกต้องคือหวังว่านักพัฒนาซอฟต์แวร์จะปล่อยแพ็คเกจที่อัปเดตแล้วอัปเกรด แต่มันใช้งานได้โดยแฮ็คในการตั้งค่าพิเศษสำหรับ VS2013 โดยชี้ไปที่ไฟล์ไลบรารี VS2012
ฉันแก้ไขแพคเกจ (ในpackages
โฟลเดอร์ภายในไดเรกทอรีของโซลูชัน) โดยการค้นหาpackagename\build\native\packagename.targets
และภายในไฟล์นั้นคัดลอกv110
ส่วนทั้งหมด ผมเปลี่ยนv110
ไปv120
ในทุ่งนาสภาพเพียงv110
การระมัดระวังมากที่จะออกจากเส้นทางชื่อไฟล์ทั้งหมดเป็น สิ่งนี้ทำให้ Visual Studio 2013 สามารถเชื่อมโยงไปยังไลบรารีสำหรับปี 2012 และในกรณีนี้มันใช้งานได้
สมมติว่าคุณมีโปรเจ็กต์ใหญ่เขียนด้วย c ++ ซึ่งมีไฟล์. cpp หลายพันไฟล์และไฟล์. h ไฟล์หนึ่งพันและสมมุติว่าโปรเจ็กต์นั้นขึ้นอยู่กับไลบรารีแบบคงที่สิบตัว สมมติว่าเราอยู่บน Windows และเราสร้างโครงการของเราใน Visual Studio 20xx เมื่อคุณกด Ctrl + F7 Visual Studio เพื่อเริ่มรวบรวมโซลูชั่นทั้งหมด (สมมติว่าเรามีเพียงหนึ่งโครงการในโซลูชัน)
ความหมายของการรวบรวมคืออะไร?
ขั้นตอนที่สองของการรวบรวมทำโดย Linker.Linker ควรผสานวัตถุไฟล์ทั้งหมดและสร้างในที่สุดผลลัพธ์ (ซึ่งอาจจะปฏิบัติการหรือห้องสมุด)
ขั้นตอนในการเชื่อมโยงโครงการ
error LNK2001: unresolved external symbol "void __cdecl foo(void)" (?foo@@YAXXZ)
การสังเกต
วิธีแก้ไขข้อผิดพลาดประเภทนี้
ข้อผิดพลาดเกี่ยวกับเวลาของคอมไพเลอร์:
ข้อผิดพลาดเวลาของ Linker
#pragma once
สำหรับการอนุญาตให้คอมไพเลอร์ไม่รวมหนึ่งส่วนหัวถ้ามันรวมอยู่ใน. cpp ปัจจุบันซึ่งถูกคอมไพล์แล้วฉันเพิ่งมีปัญหานี้และจะเปิดออกมันเป็นข้อผิดพลาดใน Visual Studio 2013 ฉันต้องลบไฟล์ต้นฉบับออกจากโครงการและเพิ่มใหม่อีกครั้งเพื่อแก้ไขข้อผิดพลาด
ขั้นตอนในการลองหากคุณเชื่อว่าอาจเป็นข้อผิดพลาดในคอมไพเลอร์ / IDE:
ตัวเชื่อมโยงที่ทันสมัยส่วนใหญ่มีตัวเลือก verbose ที่พิมพ์ออกมาเป็นองศาที่แตกต่างกัน
สำหรับ gcc และ clang; โดยปกติแล้วคุณจะเพิ่ม-v -Wl,--verbose
หรือ-v -Wl,-v
ลงในบรรทัดคำสั่ง รายละเอียดเพิ่มเติมสามารถดูได้ที่นี่;
สำหรับ MSVC, /VERBOSE
(โดยเฉพาะอย่างยิ่งใน/VERBOSE:LIB
) จะถูกเพิ่มในคำสั่งลิงค์
/VERBOSE
ไฟล์. lib ที่เชื่อมโยงนั้นเชื่อมโยงกับ. dll
ฉันมีปัญหาเดียวกัน บอกว่าฉันมีโครงการ MyProject และ TestProject ฉันเชื่อมโยงไฟล์ lib สำหรับ MyProject กับ TestProject อย่างมีประสิทธิภาพ อย่างไรก็ตามไฟล์ lib นี้ถูกสร้างขึ้นเป็น DLL สำหรับ MyProject ถูกสร้างขึ้น นอกจากนี้ฉันไม่ได้มีซอร์สโค้ดสำหรับวิธีการทั้งหมดใน MyProject แต่เข้าถึงเฉพาะจุดเข้าใช้งานของ DLL เท่านั้น
เพื่อแก้ปัญหานี้ฉันสร้าง MyProject เป็น LIB และเชื่อมโยง TestProject กับไฟล์. lib นี้ (ฉันคัดลอกวางไฟล์. lib ที่สร้างขึ้นลงในโฟลเดอร์ TestProject) ฉันสามารถสร้าง MyProject อีกครั้งในฐานะ DLL มันจะรวบรวมตั้งแต่ lib ที่ TestProject เชื่อมโยงจะมีรหัสสำหรับวิธีการทั้งหมดในชั้นเรียนใน MyProject
เนื่องจากคนดูเหมือนจะถูกนำไปยังคำถามนี้เมื่อมันมาถึงข้อผิดพลาด linker ฉันจะเพิ่มที่นี่
เหตุผลหนึ่งที่เป็นไปได้สำหรับข้อผิดพลาด linker กับ GCC 5.2.0 คือไลบรารี libstdc ++ ใหม่ ABI ถูกเลือกโดยค่าเริ่มต้น
หากคุณได้รับข้อผิดพลาด linker เกี่ยวกับการอ้างอิงที่ไม่ได้กำหนดสัญลักษณ์ที่เกี่ยวข้องกับประเภทในเนมสเปซ std :: __ cxx11 หรือแท็ก [abi: cxx11] ก็อาจบ่งบอกว่าคุณกำลังพยายามเชื่อมโยงไฟล์วัตถุต่างๆที่คอมไพล์ด้วย แมโคร สิ่งนี้มักเกิดขึ้นเมื่อเชื่อมโยงไปยังห้องสมุดบุคคลที่สามที่รวบรวมกับ GCC รุ่นเก่ากว่า หากห้องสมุดของบุคคลที่สามไม่สามารถสร้างขึ้นมาใหม่ด้วย ABI ใหม่คุณจะต้องคอมไพล์รหัสของคุณด้วย ABI เก่า
ดังนั้นหากคุณได้รับข้อผิดพลาด linker ทันทีเมื่อเปลี่ยนเป็น GCC หลังจาก 5.1.0 สิ่งนี้จะเป็นสิ่งที่ควรตรวจสอบ
wrapper รอบ ๆ GNU ld ที่ไม่รองรับสคริปต์ linker
ไฟล์. so บางไฟล์เป็นGNU ld linker scriptเช่นไฟล์libtbb.soเป็นไฟล์ข้อความ ASCII ที่มีเนื้อหานี้:
INPUT (libtbb.so.2)
งานสร้างที่ซับซ้อนบางอย่างอาจไม่รองรับสิ่งนี้ ตัวอย่างเช่นหากคุณรวม -v ไว้ในตัวเลือกคอมไพเลอร์คุณจะเห็นว่าmainwin gcc wrapper mwdipละทิ้งไฟล์คำสั่งสคริปต์ของ linker ในรายการเอาต์พุต verbose ของไลบรารีที่จะเชื่อมโยงการทำงานง่าย ๆ คือการแทนที่คำสั่ง linker script input ไฟล์ที่มีสำเนาของไฟล์แทน (หรือ symlink) เช่น
cp libtbb.so.2 libtbb.so
หรือคุณสามารถแทนที่อาร์กิวเมนต์ -l ด้วยพา ธ แบบเต็มของ. so เช่นแทนที่จะ-ltbb
ทำ/home/foo/tbb-4.3/linux/lib/intel64/gcc4.4/libtbb.so.2
libfoo
ขึ้นอยู่กับlibbar
แล้วการเชื่อมโยงของคุณอย่างถูกต้องทำให้ก่อนlibfoo
libbar
undefined reference to
บางอย่าง#include
วันและในความเป็นจริงที่กำหนดไว้ในไลบรารีที่คุณมีการเชื่อมโยงตัวอย่างมีหน่วยเป็น C พวกเขาน่าจะเท่ากับ 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
eg1.c
#include <my_lib.h>
int main()
{
hw();
return 0;
}
คุณสร้างไลบรารีแบบคงที่ของคุณ:
$ gcc -c -o my_lib.o my_lib.c
$ ar rcs libmy_lib.a my_lib.o
คุณรวบรวมโปรแกรมของคุณ:
$ gcc -I. -c -o eg1.o eg1.c
คุณพยายามเชื่อมโยงกับlibmy_lib.a
และล้มเหลว:
$ 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
ผลลัพธ์เดียวกันหากคุณรวบรวมและเชื่อมโยงในขั้นตอนเดียวเช่น:
$ 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
eg2.c
#include <zlib.h>
#include <stdio.h>
int main()
{
printf("%s\n",zlibVersion());
return 0;
}
รวบรวมโปรแกรมของคุณ:
$ gcc -c -o eg2.o eg2.c
ลองลิงค์โปรแกรมของคุณด้วยlibz
และล้มเหลว:
$ 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
เช่นเดียวกันถ้าคุณรวบรวมและลิงก์ในครั้งเดียว:
$ 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
และรูปแบบที่เกี่ยวข้องกับตัวอย่างที่ 2 pkg-config
:
$ gcc -o eg2 $(pkg-config --libs zlib) eg2.o
eg2.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
ในลำดับของอ็อบเจ็กต์ไฟล์และไลบรารีที่คุณต้องการลิงก์เพื่อสร้างโปรแกรมของคุณคุณกำลังวางไลบรารีไว้ก่อนหน้าอ็อบเจ็กต์ไฟล์ที่อ้างถึง คุณต้องวางห้องสมุดหลังจากนั้นไฟล์อ็อบเจ็กต์ที่อ้างถึง
ลิงก์ตัวอย่าง 1 อย่างถูกต้อง:
$ gcc -o eg1 eg1.o -L. -lmy_lib
ความสำเร็จ:
$ ./eg1
Hello World
ลิงก์ตัวอย่าง 2 อย่างถูกต้อง:
$ gcc -o eg2 eg2.o -lz
ความสำเร็จ:
$ ./eg2
1.2.8
เชื่อมโยงตัวอย่างpkg-config
รูปแบบ2 อย่างถูกต้อง:
$ gcc -o eg2 eg2.o $(pkg-config --libs zlib)
$ ./eg2
1.2.8
การอ่านเป็นตัวเลือกจากที่นี่
ตามค่าเริ่มต้นคำสั่ง linkage ที่สร้างโดย GCC บน distro ของคุณจะใช้ไฟล์ในการลิงก์จากซ้ายไปขวาในลำดับ commandline เมื่อพบว่าไฟล์อ้างถึงบางสิ่งบางอย่าง และไม่มีคำนิยามสำหรับไฟล์นั้นเพื่อค้นหาคำจำกัดความในไฟล์ที่อยู่ทางด้านขวา หากในที่สุดพบคำจำกัดความการอ้างอิงจะได้รับการแก้ไข หากการอ้างอิงใด ๆ ยังคงไม่ได้รับการแก้ไขในตอนท้ายการเชื่อมโยงล้มเหลว: ตัวเชื่อมโยงจะไม่ค้นหาข้อมูลย้อนกลับ
ครั้งแรกตัวอย่างที่ 1พร้อมกับห้องสมุดแบบคงที่my_lib.a
ไลบรารีแบบสแตติกเป็นไฟล์เก็บถาวรที่จัดทำดัชนีของไฟล์วัตถุ เมื่อตัวเชื่อมโยงพบ-lmy_lib
ในลำดับการเชื่อมโยงและหาว่านี่หมายถึงไลบรารีแบบคงที่./libmy_lib.a
มันต้องการทราบว่าโปรแกรมของคุณต้องการไฟล์วัตถุใด ๆ หรือlibmy_lib.a
ไม่
มีไฟล์วัตถุเพียงอย่างเดียวคือในlibmy_lib.a
คือmy_lib.o
และมีเพียงสิ่งเดียวที่กำหนดไว้ในคือฟังก์ชั่นmy_lib.o
hw
ตัวเชื่อมโยงจะตัดสินใจว่าโปรแกรมของคุณต้องการmy_lib.o
หากว่ามันรู้อยู่แล้วว่าโปรแกรมของคุณอ้างถึงhw
ในไฟล์ออบเจ็กต์หนึ่งไฟล์หรือมากกว่านั้นที่เพิ่มเข้าไปในโปรแกรมแล้วและไม่มีไฟล์ออบเจ็กต์ที่เพิ่มเข้าไปแล้ว hw
คำนิยาม
หากเป็นจริงตัวลิงก์จะแยกสำเนาของmy_lib.o
จากไลบรารีและเพิ่มลงในโปรแกรมของคุณ จากนั้นโปรแกรมของคุณมีความหมายสำหรับhw
เพื่อการอ้างอิงในการhw
จะได้รับการแก้ไข
เมื่อคุณพยายามเชื่อมโยงโปรแกรมเช่น:
$ gcc -o eg1 -L. -lmy_lib eg1.o
ลิงเกอร์ยังไม่ได้เพิ่ม eg1.o
ไปยังโปรแกรม-lmy_lib
เมื่อเห็น
eg1.o
เพราะในจุดนั้นมันยังไม่เห็น โปรแกรมของคุณยังไม่ให้การอ้างอิงใด ๆhw
: มันยังไม่ให้การอ้างอิงใด ๆที่ทุกคนeg1.o
เพราะการอ้างอิงทั้งหมดมันทำให้อยู่ใน
ดังนั้นลิงเกอร์ไม่เพิ่มในการเขียนโปรแกรมและยังไม่มีการใช้งานต่อไปสำหรับmy_lib.o
libmy_lib.a
ถัดไปพบeg1.o
และเพิ่มเป็นโปรแกรม อ็อบเจ็กต์ไฟล์ในลำดับการลิงก์ถูกเพิ่มเข้ากับโปรแกรมเสมอ ตอนนี้โปรแกรมทำการอ้างอิงถึงhw
และไม่มีคำจำกัดความของhw
; แต่ไม่มีอะไรเหลืออยู่ในลำดับการเชื่อมโยงที่สามารถให้คำจำกัดความที่ขาดหายไป การอ้างอิงเพื่อhw
สิ้นสุดยังไม่ได้แก้ไขและการเชื่อมโยงล้มเหลว
ประการที่สองตัวอย่างที่ 2พร้อมห้องสมุดสาธารณะlibz
ไลบรารีที่ใช้ร่วมกันไม่ใช่ไฟล์เก็บถาวรของวัตถุหรืออะไรอย่างนั้น มันเป็นเหมือนโปรแกรมที่ไม่มีmain
ฟังก์ชั่นและจะแสดงสัญลักษณ์อื่น ๆ ที่กำหนดไว้เพื่อให้โปรแกรมอื่นสามารถใช้งานได้ในขณะทำงาน
หลาย Linux distros วันนี้กำหนดค่าของพวกเขา GCC toolchain เพื่อให้ไดรเวอร์ที่ภาษา ( gcc
, g++
, gfortran
ฯลฯ ) สั่งลิงเกอร์ระบบ ( ld
) เพื่อเชื่อมโยงร่วมกันห้องสมุดในความจำเป็นพื้นฐาน คุณมีหนึ่งในความโกลาหลเหล่านั้น
ซึ่งหมายความว่าเมื่อตัวเชื่อมโยงพบ-lz
ในลำดับการเชื่อมโยงและคิดว่าสิ่งนี้อ้างอิงถึงไลบรารีที่ใช้ร่วมกัน (พูด) /usr/lib/x86_64-linux-gnu/libz.so
ก็ต้องการที่จะรู้ว่าการอ้างอิงใด ๆ ที่เพิ่มไว้ในโปรแกรมของคุณที่ยังไม่ได้กำหนดมีคำจำกัดความที่ ส่งออกโดยlibz
หากเป็นเช่นนั้นตัวลิงก์จะไม่คัดลอกชิ้นส่วนใด ๆ ออกจากlibz
และเพิ่มลงในโปรแกรมของคุณ แต่จะเป็นหมอตรวจสอบรหัสของโปรแกรมของคุณเพื่อ: -
เมื่อรันไทม์ตัวโหลดโปรแกรมระบบจะโหลดสำเนาของlibz
กระบวนการเดียวกันกับโปรแกรมของคุณเมื่อใดก็ตามที่มันโหลดสำเนาของโปรแกรมของคุณเพื่อเรียกใช้
ณ รันไทม์เมื่อใดก็ตามที่โปรแกรมของคุณอ้างถึงสิ่งที่กำหนดไว้
libz
การอ้างอิงนั้นจะใช้คำจำกัดความที่ส่งออกโดยสำเนาของlibz
กระบวนการเดียวกัน
โปรแกรมของคุณต้องการที่จะหมายถึงเพียงสิ่งหนึ่งที่มีความหมายส่งออกโดยlibz
คือฟังก์ชั่นซึ่งจะเรียกเพียงครั้งเดียวในzlibVersion
eg2.c
หาก linker เพิ่มการอ้างอิงนั้นไปยังโปรแกรมของคุณแล้วค้นหาคำจำกัดความที่ส่งออกโดยlibz
การอ้างอิงนั้นจะได้รับการแก้ไข
แต่เมื่อคุณพยายามเชื่อมโยงโปรแกรมเช่น:
gcc -o eg2 -lz eg2.o
ลำดับของเหตุการณ์ที่เกิดขึ้นเป็นความผิดในลักษณะเช่นเดียวกับกับตัวอย่างเช่น 1. ที่จุดเมื่อพบลิงเกอร์-lz
มีไม่มีการอ้างอิงถึงสิ่งที่อยู่ในโปรแกรม: พวกเขาอยู่ในทั้งหมดeg2.o
ซึ่งยังไม่ได้รับการเห็น ดังนั้นลิงเกอร์ตัดสินใจว่ามันไม่มีประโยชน์libz
อะไร เมื่อมันมาถึงeg2.o
ให้เพิ่มมันเข้าไปในโปรแกรมแล้วมีการอ้างอิงที่ไม่ได้กำหนดzlibVersion
ลำดับการเชื่อมโยงจะเสร็จสิ้น การอ้างอิงนั้นไม่ได้รับการแก้ไขและการเชื่อมโยงล้มเหลว
สุดท้ายการpkg-config
เปลี่ยนแปลงของตัวอย่าง 2 มีคำอธิบายที่ชัดเจนในขณะนี้ หลังจากการขยายตัวของเชลล์:
gcc -o eg2 $(pkg-config --libs zlib) eg2.o
กลายเป็น:
gcc -o eg2 -lz eg2.o
ซึ่งเป็นเพียงตัวอย่าง 2 อีกครั้ง
การเชื่อมโยง:
gcc -o eg2 -lz eg2.o
ทำงานได้ดีสำหรับคุณ!
(หรือ: การเชื่อมโยงนั้นใช้งานได้ดีสำหรับคุณพูด Fedora 23 แต่ล้มเหลวบน Ubuntu 16.04)
นั่นเป็นเพราะ distro ที่เชื่อมโยงการทำงานเป็นหนึ่งในคนที่ไม่ได้กำหนดค่า toolchain GCC ในการเชื่อมโยงห้องสมุดสาธารณะความจำเป็น
ย้อนกลับไปในวันมันเป็นเรื่องปกติสำหรับระบบที่เหมือนยูนิกซ์ในการเชื่อมโยงไลบรารีแบบคงที่และที่ใช้ร่วมกันตามกฎต่างๆ ไลบรารีแบบสแตติกในลำดับการลิงก์ถูกลิงก์บนพื้นฐานตามที่ต้องการอธิบายไว้ในตัวอย่างที่ 1 แต่ไลบรารีแบบแบ่งใช้ถูกลิงก์แบบไม่มีเงื่อนไข
ลักษณะการทำงานนี้ประหยัดที่ linktime เนื่องจากตัวเชื่อมโยงไม่จำเป็นต้องไตร่ตรองว่าโปรแกรมที่ใช้ร่วมกับไลบรารีนั้นต้องการหรือไม่: ถ้าเป็นไลบรารีที่แชร์ให้ทำการเชื่อมโยง และห้องสมุดส่วนใหญ่ในการเชื่อมโยงส่วนใหญ่เป็นห้องสมุดสาธารณะ แต่ก็มีข้อเสียเช่นกัน: -
มันไม่ประหยัดที่รันไทม์เนื่องจากอาจทำให้ไลบรารีที่แชร์ถูกโหลดพร้อมกับโปรแกรมแม้ว่าจะไม่ต้องการก็ตาม
กฎการเชื่อมโยงที่แตกต่างกันสำหรับไลบรารีแบบคงที่และแบบแบ่งใช้อาจทำให้สับสนกับโปรแกรมเมอร์ที่ไม่มีประสบการณ์ซึ่งอาจไม่รู้ว่า-lfoo
ในการเชื่อมโยงของพวกเขากำลังจะแก้ไข/some/where/libfoo.a
หรือ/some/where/libfoo.so
ไม่และอาจไม่เข้าใจความแตกต่างระหว่างไลบรารีแบบแบ่งใช้และแบบคงที่
การแลกเปลี่ยนนี้นำไปสู่สถานการณ์ที่แตกแยกในทุกวันนี้ บาง distros ได้เปลี่ยนกฎการเชื่อมโยง GCC ของพวกเขาสำหรับไลบรารีที่ใช้ร่วมกันเพื่อที่ จะใช้หลักการที่จำเป็นสำหรับไลบรารีทั้งหมด บาง distros ติดอยู่กับวิธีเก่า
ถ้าฉันทำ:
$ gcc -o eg1 -I. -L. -lmy_lib eg1.c
แน่นอน GCC มีการรวบรวมแรกและจากนั้นเชื่อมโยงไฟล์วัตถุที่เกิดขึ้นกับeg1.c
libmy_lib.a
ดังนั้นจะไม่ทราบได้อย่างไรว่าไฟล์อ็อบเจ็กต์นั้นต้องการเมื่อทำการลิงก์?
เพราะการรวบรวมและเชื่อมโยงกับคำสั่งเดียวไม่ได้เปลี่ยนลำดับของการเชื่อมโยงลำดับ
เมื่อคุณเรียกใช้คำสั่งข้างต้นgcc
ตัวเลขที่คุณต้องการรวบรวม + เชื่อมโยง ดังนั้นเบื้องหลังมันสร้างคำสั่งการคอมไพล์และรันมันจากนั้นสร้างคำสั่งการเชื่อมโยงและรันมันราวกับว่าคุณได้รันทั้งสองคำสั่ง:
$ gcc -I. -c -o eg1.o eg1.c
$ gcc -o eg1 -L. -lmy_lib eg1.o
ดังนั้นการเชื่อมต่อล้มเหลวเช่นเดียวกับที่มันไม่ถ้าคุณไม่เรียกใช้ทั้งสองคำสั่ง แตกต่างเพียงคุณสังเกตเห็นในความล้มเหลวคือการที่ GCC ได้สร้างไฟล์วัตถุชั่วคราวในกรณีที่รวบรวม + eg1.o
การเชื่อมโยงเพราะคุณไม่ได้บอกให้ใช้ ที่เราเห็น:
/tmp/ccQk1tvs.o: In function `main'
แทน:
eg1.o: In function `main':
ลำดับการระบุไลบรารีที่เชื่อมโยงซึ่งกันและกันซึ่งระบุนั้นผิด
การวางไลบรารีที่พึ่งพาซึ่งกันและกันในลำดับที่ไม่ถูกต้องเป็นเพียงหนึ่งวิธีที่คุณสามารถรับไฟล์ที่ต้องการคำจำกัดความของสิ่งต่าง ๆ ที่จะเกิดขึ้นในภายหลังในการเชื่อมโยงมากกว่าไฟล์ที่ให้คำจำกัดความ การวางไลบรารีไว้ก่อนไฟล์อ็อบเจ็กต์ที่อ้างถึงเป็นอีกวิธีหนึ่งในการทำผิดพลาดแบบเดียวกัน
รับข้อมูลโค้ดของประเภทเทมเพลตที่มีโอเปอเรเตอร์เพื่อน (หรือฟังก์ชัน);
template <typename T>
class Foo {
friend std::ostream& operator<< (std::ostream& os, const Foo<T>& a);
};
operator<<
จะถูกประกาศเป็นฟังก์ชั่นที่ไม่ใช่แม่แบบ สำหรับทุกประเภทT
ใช้กับมีความต้องการที่จะเป็นไม่ใช่เทมเพลตFoo
operator<<
ตัวอย่างเช่นหากมีการFoo<int>
ประกาศประเภทแล้วจะต้องมีการดำเนินการดำเนินการดังนี้
std::ostream& operator<< (std::ostream& os, const Foo<int>& a) {/*...*/}
เนื่องจากไม่ได้ใช้งานตัวเชื่อมโยงจึงไม่สามารถค้นหาได้และทำให้เกิดข้อผิดพลาด
หากต้องการแก้ไขสิ่งนี้คุณสามารถประกาศโอเปอเรเตอร์เทมเพลตก่อนFoo
ประเภทจากนั้นประกาศว่าเป็นเพื่อนการสร้างอินสแตนซ์ที่เหมาะสม ไวยากรณ์ค่อนข้างอึดอัดเล็กน้อย แต่มีลักษณะดังนี้
// 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
}
รหัสข้างต้น จำกัด มิตรภาพของผู้ประกอบการที่จะ instantiation ที่สอดคล้องกันของFoo
คือoperator<< <int>
การเริ่มมีข้อ จำกัด ในการเข้าถึงสมาชิกส่วนตัวของ instantiation Foo<int>
ของ
ทางเลือก ได้แก่ ;
การอนุญาตให้มิตรภาพขยายไปสู่อินสแตนซ์ทั้งหมดของเทมเพลตดังต่อไปนี้
template <typename T>
class Foo {
template <typename T1>
friend std::ostream& operator<<(std::ostream& os, const Foo<T1>& a);
// ...
};
หรือการใช้งานสำหรับoperator<<
สามารถทำได้แบบอินไลน์ภายในคำจำกัดความของชั้นเรียน;
template <typename T>
class Foo {
friend std::ostream& operator<<(std::ostream& os, const Foo& a)
{ /*...*/ }
// ...
};
หมายเหตุเมื่อประกาศของผู้ประกอบการ (หรือฟังก์ชั่น) เท่านั้นที่ปรากฏในชั้นเรียนชื่อที่ไม่สามารถใช้ได้สำหรับ "ปกติ" ค้นหาเพียงเพื่อโต้แย้งการค้นหาขึ้นจากcppreference ;
ชื่อที่ประกาศครั้งแรกในการประกาศของเพื่อนภายในคลาสหรือเทมเพลตคลาส X กลายเป็นสมาชิกของเนมสเปซที่อยู่ด้านในสุดของ X แต่ไม่สามารถเข้าถึงได้สำหรับการค้นหา (ยกเว้นการค้นหาขึ้นอยู่กับอาร์กิวเมนต์ที่พิจารณา X) ยกเว้นการประกาศที่ตรงกันที่ขอบเขตเนมสเปซ ให้...
มีการอ่านเพิ่มเติมเกี่ยวกับเพื่อนของแม่แบบที่เป็นcppreferenceและC ++ คำถามที่พบบ่อย
รหัสรายชื่อการแสดงเทคนิคดังกล่าวข้างต้น
ในฐานะที่เป็นบันทึกด้านข้างเพื่อตัวอย่างรหัสความล้มเหลว; g ++ เตือนเกี่ยวกับสิ่งนี้ดังนี้
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)
ข้อผิดพลาดของ Linker สามารถเกิดขึ้นได้เมื่อไฟล์ส่วนหัวและไลบรารีที่แชร์ที่เกี่ยวข้อง (ไฟล์. lib) ขาดการซิงค์ ให้ฉันอธิบาย
linkers ทำงานอย่างไร ตัวเชื่อมโยงจับคู่การประกาศฟังก์ชัน (ประกาศในส่วนหัว) กับคำจำกัดความ (ในไลบรารีที่ใช้ร่วมกัน) โดยการเปรียบเทียบลายเซ็นของพวกเขา คุณจะได้รับข้อผิดพลาด linker ถ้า linker ไม่พบคำนิยามฟังก์ชันที่ตรงกับที่สมบูรณ์
เป็นไปได้ที่จะยังคงได้รับข้อผิดพลาด linker แม้ว่าการประกาศและคำจำกัดความดูเหมือนจะตรงกันหรือไม่ ใช่ พวกเขาอาจดูเหมือนกันในซอร์สโค้ด แต่มันขึ้นอยู่กับสิ่งที่คอมไพเลอร์เห็น เป็นหลักคุณสามารถท้ายด้วยสถานการณ์เช่นนี้:
// header1.h
typedef int Number;
void foo(Number);
// header2.h
typedef float Number;
void foo(Number); // this only looks the same lexically
โปรดสังเกตว่าแม้ว่าการประกาศฟังก์ชั่นทั้งสองจะมีลักษณะเหมือนกันในซอร์สโค้ด แต่มันก็แตกต่างกันไปตามคอมไพเลอร์
คุณอาจถามว่าจบลงในสถานการณ์เช่นนั้นได้อย่างไร รวมเส้นทางแน่นอน! หากเมื่อรวบรวมห้องสมุดสาธารณะเส้นทางรวมจะนำไปสู่header1.h
และคุณใช้header2.h
ในโปรแกรมของคุณเองคุณจะถูกเกาหัวส่วนหัวของคุณสงสัยว่าเกิดอะไรขึ้น (ปุนตั้งใจ)
ตัวอย่างของสิ่งที่สามารถเกิดขึ้นได้ในโลกแห่งความเป็นจริงได้อธิบายไว้ด้านล่าง
ฉันมีสองโครงการ: และgraphics.lib
ทั้งสองโครงการขึ้นอยู่กับmain.exe
common_math.h
สมมติว่าไลบรารีส่งออกฟังก์ชันต่อไปนี้:
// graphics.lib
#include "common_math.h"
void draw(vec3 p) { ... } // vec3 comes from common_math.h
จากนั้นคุณไปข้างหน้าและรวมไลบรารีไว้ในโครงการของคุณเอง
// main.exe
#include "other/common_math.h"
#include "graphics.h"
int main() {
draw(...);
}
บูม! คุณได้รับข้อผิดพลาด linker และคุณไม่รู้ว่าทำไมมันถึงล้มเหลว เหตุผลก็คือไลบรารีทั่วไปใช้รุ่นที่แตกต่างกันของการรวมcommon_math.h
(ฉันได้ทำให้มันชัดเจนในตัวอย่างโดยรวมถึงเส้นทางที่แตกต่างกัน แต่มันอาจจะไม่ชัดเจนเสมอไปบางทีเส้นทางรวมอาจแตกต่างกันในการตั้งค่าคอมไพเลอร์) .
หมายเหตุในตัวอย่างนี้ตัวเชื่อมโยงจะบอกคุณว่ามันหาไม่เจอdraw()
เมื่อในความเป็นจริงคุณรู้ว่ามันถูกส่งออกโดยห้องสมุด คุณอาจใช้เวลาหลายชั่วโมงเกาหัวของคุณสงสัยว่าเกิดอะไรขึ้น สิ่งคือลิงเกอร์เห็นลายเซ็นที่แตกต่างกันเพราะประเภทพารามิเตอร์แตกต่างกันเล็กน้อย ในตัวอย่างvec3
เป็นชนิดที่แตกต่างกันในทั้งสองโครงการเท่าที่คอมไพเลอร์เกี่ยวข้อง สิ่งนี้อาจเกิดขึ้นเพราะมาจากไฟล์รวมที่แตกต่างกันเล็กน้อยสองไฟล์ (อาจเป็นไฟล์รวมที่มาจากไลบรารีสองเวอร์ชันที่ต่างกัน)
DUMPBIN เป็นเพื่อนของคุณถ้าคุณใช้ Visual Studio ฉันแน่ใจว่าคอมไพเลอร์รายอื่นมีเครื่องมืออื่นที่คล้ายคลึงกัน
กระบวนการจะเป็นดังนี้:
[1] ตามโครงการฉันหมายถึงชุดของไฟล์ต้นฉบับที่เชื่อมโยงเข้าด้วยกันเพื่อสร้างไลบรารีหรือไฟล์ที่เรียกใช้งานได้
แก้ไข 1: เขียนซ้ำส่วนแรกเพื่อให้เข้าใจง่ายขึ้น โปรดแสดงความคิดเห็นด้านล่างเพื่อแจ้งให้เราทราบหากจำเป็นต้องแก้ไขสิ่งอื่น ขอบคุณ!
UNICODE
คำจำกัดความที่ไม่สอดคล้องกันที่ใช้ Windows UNICODE สร้างถูกสร้างขึ้นด้วยTCHAR
ฯลฯ ถูกกำหนดให้เป็นwchar_t
ฯลฯ เมื่อไม่ได้สร้างด้วยUNICODE
การกำหนดเป็นสร้างที่มีTCHAR
กำหนดเป็นchar
ฯลฯ เหล่านี้UNICODE
และ_UNICODE
กำหนดส่งผลกระทบต่อทุกคน" T
" ประเภทสตริง ; LPTSTR
, LPCTSTR
และกวางของพวกเขา
การสร้างหนึ่งไลบรารีที่มีการUNICODE
กำหนดและพยายามลิงก์ในโครงการที่UNICODE
ไม่ได้กำหนดไว้จะส่งผลให้เกิดข้อผิดพลาดของ linker เนื่องจากจะมีความไม่ตรงกันในคำจำกัดความของTCHAR
; char
กับwchar_t
.
ข้อผิดพลาดมักจะมีฟังก์ชั่นค่าที่มีchar
หรือwchar_t
ประเภทที่ได้รับเหล่านี้อาจรวมถึงstd::basic_string<>
ฯลฯ เช่นกัน เมื่อเรียกดูผ่านฟังก์ชั่นที่ได้รับผลกระทบในรหัสมักจะมีการอ้างอิงถึงTCHAR
หรือstd::basic_string<TCHAR>
อื่น ๆ นี่คือสัญญาณบอกเล่าว่ารหัสเดิมมีไว้สำหรับทั้ง UNICODE และอักขระแบบหลายไบต์ (หรือ "แคบ") .
หากต้องการแก้ไขสิ่งนี้ให้สร้างไลบรารีและโครงการที่จำเป็นทั้งหมดด้วยคำจำกัดความที่สอดคล้องกันของUNICODE
(และ_UNICODE
)
สามารถทำได้ด้วยเช่นกัน
#define UNICODE
#define _UNICODE
หรือในการตั้งค่าโครงการ
คุณสมบัติโครงการ> ทั่วไป> ค่าเริ่มต้นของโครงการ> ชุดอักขระ
หรือบนบรรทัดคำสั่ง
/DUNICODE /D_UNICODE
ทางเลือกสามารถใช้งานได้เช่นกันหากไม่ได้ใช้ UNICODE ตรวจสอบให้แน่ใจว่าไม่มีการตั้งค่าการกำหนดและ / หรือการตั้งค่าอักขระหลายตัวถูกใช้ในโครงการและนำไปใช้อย่างต่อเนื่อง
อย่าลืมว่าจะต้องสอดคล้องกันระหว่างบิลด์ "Release" และ "Debug" เช่นกัน
"clean" ของบิลด์สามารถลบ "dead wood" ที่อาจทิ้งไว้จากการสร้างก่อนหน้าบิวด์ที่ล้มเหลวบิวด์ที่ไม่สมบูรณ์บิวด์ที่ไม่สมบูรณ์และบิลด์อื่น ๆ ที่เกี่ยวข้องกับบิลด์
โดยทั่วไปแล้ว IDE หรือบิลด์จะมีฟังก์ชั่น "clean" บางรูปแบบ แต่อาจไม่ได้รับการกำหนดค่าอย่างถูกต้อง (เช่นใน makefile แบบกำหนดเอง) หรืออาจล้มเหลว (เช่นไบนารีกลางหรือผลลัพธ์เป็นแบบอ่านอย่างเดียว)
เมื่อ "clean" เสร็จสิ้นให้ตรวจสอบว่า "clean" สำเร็จแล้วและไฟล์กลางทั้งหมดที่สร้างขึ้น (เช่น makefile อัตโนมัติ) ถูกลบออกเรียบร้อยแล้ว
นี้กระบวนการสามารถมองเห็นได้เป็นที่พึ่งสุดท้าย แต่มักจะเป็นก้าวแรกที่ดี ; โดยเฉพาะอย่างยิ่งถ้ารหัสที่เกี่ยวข้องกับข้อผิดพลาดได้รับการเพิ่มเมื่อเร็ว ๆ นี้ (ทั้งในพื้นที่หรือจากแหล่งเก็บข้อมูล)
const
การประกาศตัวแปร / คำจำกัดความ (C ++ เท่านั้น)สำหรับผู้ที่มาจาก C อาจเป็นเรื่องน่าแปลกใจที่ในconst
ตัวแปรโกลบอล C ++ นั้นมีการเชื่อมโยงภายใน ใน C นี่ไม่ใช่กรณีเช่นเดียวกับตัวแปรโกลบอลทั้งหมดโดยปริยายextern
(เช่นเมื่อstatic
คำหลักหายไป)
ตัวอย่าง:
// 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
}
แก้ไขจะใช้ไฟล์ส่วนหัวและรวมไว้ใน file2.cpp และ file1.cpp
extern const int test;
extern int test2;
อีกทางเลือกหนึ่งสามารถประกาศconst
ตัวแปรใน file1.cpp อย่างชัดเจนextern
แม้ว่านี่จะเป็นคำถามเก่าที่มีคำตอบที่ยอมรับได้หลายข้อ แต่ฉันต้องการแบ่งปันวิธีแก้ไขข้อผิดพลาด "การอ้างอิงที่ไม่ได้กำหนดถึง" ที่ คลุมเครือ
ฉันใช้นามแฝงเพื่ออ้างถึงstd::filesystem::path
: ระบบไฟล์อยู่ในไลบรารีมาตรฐานตั้งแต่ C ++ 17 แต่โปรแกรมของฉันจำเป็นต้องคอมไพล์ใน C ++ 14 ด้วยดังนั้นฉันตัดสินใจใช้ตัวแปรนามแฝง:
#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
สมมติว่าฉันมีสามไฟล์: main.cpp, file.h, file.cpp:
หมายเหตุไลบรารีต่าง ๆ ที่ใช้ใน main.cpp และ file.h ตั้งแต่ main.cpp # include'd " file.h " หลังจาก < ระบบแฟ้ม > รุ่นของระบบแฟ้มที่ใช้มีc ++ 17 หนึ่ง ฉันเคยรวบรวมโปรแกรมด้วยคำสั่งต่อไปนี้:
$ g++ -g -std=c++17 -c main.cpp
-> รวบรวม main.cpp ไปยัง main.o
$ g++ -g -std=c++17 -c file.cpp
-> รวบรวม file.cpp และ file.h ไปยัง file.o
$ g++ -g -std=c++17 -o executable main.o file.o -lstdc++fs
-> ลิงก์ main.o และ file.o
วิธีนี้ฟังก์ชั่นใด ๆที่มีอยู่ใน file.o และใช้ใน main.o ที่ต้องpath_t
ให้ "การอ้างอิงที่ไม่ได้กำหนดข้อผิดพลาด" เพราะmain.oเรียกstd::filesystem::path
แต่file.ostd::experimental::filesystem::path
ไป
ในการแก้ไขปัญหานี้ผมเพียงต้องการที่จะเปลี่ยนแปลง <ทดลอง :: ระบบแฟ้ม> ใน file.h ที่ <ระบบแฟ้ม>
พฤติกรรมเริ่มต้นของ gcc คือสัญลักษณ์ทั้งหมดสามารถมองเห็นได้ อย่างไรก็ตามเมื่อหน่วยการแปลถูกสร้างขึ้นด้วยตัวเลือก-fvisibility=hidden
เฉพาะฟังก์ชั่น / สัญลักษณ์ที่ทำเครื่องหมายด้วย__attribute__ ((visibility ("default")))
เป็นภายนอกในวัตถุที่ใช้ร่วมกันที่เกิดขึ้น
คุณสามารถตรวจสอบว่าสัญลักษณ์ที่คุณกำลังมองหาอยู่ภายนอกหรือไม่โดยเรียกใช้:
# -D shows (global) dynamic symbols that can be used from the outside of XXX.so
nm -D XXX.so | grep MY_SYMBOL
สัญลักษณ์ซ่อน / โลคัลถูกแสดงnm
ด้วยชนิดสัญลักษณ์ตัวพิมพ์เล็กตัวอย่างเช่นt
แทนที่จะเป็น `T สำหรับส่วนของรหัส:
nm XXX.so
00000000000005a7 t HIDDEN_SYMBOL
00000000000005f8 T VISIBLE_SYMBOL
นอกจากนี้คุณยังสามารถใช้nm
กับตัวเลือก-C
เพื่อแยกชื่อ (ถ้าใช้ C ++)
คล้ายกับ Windows-dll ที่หนึ่งจะทำเครื่องหมายฟังก์ชั่นสาธารณะด้วยการกำหนดตัวอย่างเช่นDLL_PUBLIC
กำหนดเป็น:
#define DLL_PUBLIC __attribute__ ((visibility ("default")))
DLL_PUBLIC int my_public_function(){
...
}
ซึ่งสอดคล้องกับเวอร์ชั่น Windows / MSVC คร่าวๆ:
#ifdef BUILDING_DLL
#define DLL_PUBLIC __declspec(dllexport)
#else
#define DLL_PUBLIC __declspec(dllimport)
#endif
เพิ่มเติมข้อมูลเกี่ยวกับการมองเห็นสามารถพบได้ในวิกิพีเดียจีซี
เมื่อหน่วยการแปลถูกรวบรวมด้วย-fvisibility=hidden
สัญลักษณ์ผลลัพธ์ยังคงมีการเชื่อมโยงภายนอก (แสดงด้วยสัญลักษณ์ตัวพิมพ์ใหญ่โดยnm
) และสามารถใช้สำหรับการลิงก์ภายนอกโดยไม่มีปัญหาหากไฟล์วัตถุกลายเป็นส่วนหนึ่งของไลบรารีแบบคงที่ การเชื่อมโยงกลายเป็นโลคัลต่อเมื่อไฟล์อ็อบเจ็กต์ถูกลิงก์เข้ากับไลบรารีที่แบ่งใช้
ในการค้นหาสัญลักษณ์ใดในไฟล์วัตถุที่ซ่อนอยู่ให้เรียกใช้
>>> objdump -t XXXX.o | grep hidden
0000000000000000 g F .text 000000000000000b .hidden HIDDEN_SYMBOL1
000000000000000b g F .text 000000000000000b .hidden HIDDEN_SYMBOL2
สถาปัตยกรรมที่แตกต่าง
คุณอาจเห็นข้อความเช่น:
library machine type 'x64' conflicts with target machine type 'X86'
ในกรณีดังกล่าวหมายความว่าสัญลักษณ์ที่มีสำหรับสถาปัตยกรรมที่แตกต่างจากที่คุณรวบรวม
บน Visual Studio นี่เป็นเพราะ "แพลตฟอร์ม" ผิดและคุณต้องเลือกอย่างใดอย่างหนึ่งที่เหมาะสมหรือติดตั้งรุ่นที่เหมาะสมของห้องสมุด
บน Linux อาจเป็นเพราะโฟลเดอร์ไลบรารีผิด (ใช้lib
แทนlib64
ตัวอย่าง)
บน MacOS มีตัวเลือกในการจัดส่งสถาปัตยกรรมทั้งสองในไฟล์เดียวกัน อาจเป็นได้ว่าลิงก์คาดว่าทั้งสองเวอร์ชันจะอยู่ที่นั่น แต่มีเพียงเวอร์ชั่นเดียวเท่านั้น นอกจากนี้ยังสามารถเป็นปัญหากับการที่ไม่ถูกต้องlib
/ lib64
โฟลเดอร์ที่ห้องสมุดจะหยิบขึ้นมา