Tautan internal dan eksternal dalam C ++

Hari baik untuk semua!

Kami hadir untuk Anda terjemahan dari artikel yang menarik yang telah disiapkan untuk Anda sebagai bagian dari kursus "Pengembang C ++" . Kami berharap ini akan bermanfaat dan menarik bagi Anda, juga bagi pendengar kami.

Ayo pergi.

Pernahkah Anda menjumpai istilah komunikasi internal dan eksternal? Ingin tahu untuk apa kata kunci eksternal digunakan, atau bagaimana pernyataan tentang sesuatu yang statis mempengaruhi ruang lingkup global? Maka artikel ini adalah untuk Anda.

Singkatnya

Unit terjemahan (.c / .cpp) dan semua file tajuknya (.h / .hpp) dimasukkan dalam unit terjemahan. Jika suatu objek atau fungsi memiliki ikatan internal di dalam unit terjemahan, maka simbol ini hanya dapat dilihat oleh penghubung di dalam unit terjemahan ini. Jika objek atau fungsi memiliki tautan eksternal, tautan tersebut akan dapat melihatnya saat memproses unit terjemahan lainnya. Menggunakan kata kunci statis di namespace global memberikan karakter yang mengikat internal. Kata kunci eksternal memberikan ikatan eksternal.
Kompiler default memberi karakter binding berikut:

  • Variabel global non-const - ikatan eksternal;
  • Variabel global konstan - pengikatan internal;
  • Fungsi - Menghubungkan Eksternal.



Dasar-dasarnya

Pertama, mari kita bicara tentang dua konsep sederhana yang diperlukan untuk membahas ikatan.

  • Perbedaan antara deklarasi dan definisi;
  • Unit Siaran.

Perhatikan juga nama-nama: kita akan menggunakan konsep "simbol" ketika datang ke "entitas kode" dengan mana linker bekerja, misalnya dengan variabel atau fungsi (atau dengan kelas / struktur, tetapi kita tidak akan fokus pada mereka).

Pengumuman VS. Definisi

Kami membahas secara singkat perbedaan antara deklarasi dan definisi simbol: pengumuman (atau deklarasi) memberi tahu kompiler tentang keberadaan simbol tertentu, dan memungkinkan akses ke simbol ini dalam kasus yang tidak memerlukan alamat memori atau penyimpanan simbol yang tepat. Definisi memberitahu kompiler apa yang terkandung dalam tubuh fungsi atau berapa banyak memori yang perlu dialokasikan variabel.

Dalam beberapa situasi, deklarasi tidak cukup untuk kompiler, misalnya, ketika elemen data kelas memiliki tautan atau tipe nilai (yaitu, bukan tautan, dan bukan pointer). Pada saat yang sama, sebuah penunjuk ke tipe yang dideklarasikan (tetapi tidak terdefinisi) diperbolehkan, karena ia membutuhkan jumlah memori yang tetap (misalnya, 8 byte dalam sistem 64-bit), terlepas dari tipe yang ditunjukkannya. Untuk mendapatkan nilai dengan pointer ini, diperlukan definisi. Juga, untuk mendeklarasikan suatu fungsi, Anda perlu mendeklarasikan (tetapi tidak mendefinisikan) semua parameter (apakah itu diambil oleh nilai, referensi atau pointer) dan tipe kembali. Menentukan jenis nilai balik dan parameter hanya diperlukan untuk mendefinisikan suatu fungsi.

Fungsi

Perbedaan antara mendefinisikan dan mendeklarasikan suatu fungsi sangat jelas.

int f(); //  int f() { return 42; } //  

Variabel

Dengan variabel, ini sedikit berbeda. Deklarasi dan definisi biasanya tidak dibagikan. Yang utama adalah:

 int x; 

Tidak hanya mendeklarasikan x , tetapi juga mendefinisikannya. Ini karena panggilan ke int konstruktor default. (Dalam C ++, tidak seperti Java, konstruktor tipe sederhana (seperti int) tidak menginisialisasi nilai ke 0 secara default. Pada contoh di atas, x akan sama dengan sampah apa pun yang terletak di alamat memori yang dialokasikan oleh kompiler).

Tetapi Anda dapat secara eksplisit memisahkan deklarasi variabel dan definisinya menggunakan kata kunci extern .

 extern int x; //  int x = 42; //  

Namun, ketika menginisialisasi dan menambahkan extern ke deklarasi, ekspresi berubah menjadi definisi dan kata kunci extern menjadi tidak berguna.

 extern int x = 5; //   ,   int x = 5; 

Pratinjau Iklan

Dalam C ++, ada konsep pra-deklarasi karakter. Ini berarti bahwa kami menyatakan jenis dan nama simbol untuk digunakan dalam situasi yang tidak memerlukan definisi. Jadi kita tidak perlu memasukkan definisi penuh karakter (biasanya file header) tanpa kebutuhan yang jelas. Dengan demikian, kami mengurangi ketergantungan pada file yang berisi definisi. Keuntungan utama adalah bahwa ketika mengubah file dengan definisi, file tempat kita sebelumnya menyatakan simbol ini tidak memerlukan kompilasi ulang (yang berarti bahwa semua file lain termasuk itu).

Contoh

Misalkan kita memiliki deklarasi fungsi (disebut prototipe) untuk f yang mengambil objek bertipe Class berdasarkan nilai:

 // file.hpp void f(Class object); 

Segera sertakan definisi Class - naif. Tetapi karena kita baru saja mendeklarasikan f , itu sudah cukup untuk memberikan deklarasi Class kepada kompiler. Dengan demikian, kompiler dapat mengenali fungsi dengan prototipe-nya, dan kita bisa menghilangkan ketergantungan file.hpp pada file yang mengandung definisi Class , misalkan class.hpp:

 // file.hpp class Class; void f(Class object); 

Katakanlah file.hpp terkandung dalam 100 file lainnya. Dan katakanlah kita mengubah definisi Kelas di class.hpp. Jika Anda menambahkan class.hpp ke file.hpp, file.hpp dan ke-100 file yang mengandungnya harus dikompilasi ulang. Berkat deklarasi awal Kelas, satu-satunya file yang memerlukan kompilasi adalah class.hpp dan file.hpp (dengan asumsi f didefinisikan di sana).

Frekuensi penggunaan

Perbedaan penting antara deklarasi dan definisi adalah bahwa simbol dapat dideklarasikan berkali-kali, tetapi hanya didefinisikan sekali. Jadi Anda dapat mendeklarasikan fungsi atau kelas sebanyak yang Anda suka, tetapi hanya ada satu definisi. Ini disebut Aturan Satu Definisi . Di C ++, yang berikut ini berfungsi:

 int f(); int f(); int f(); int f(); int f(); int f(); int f() { return 5; } 

Dan ini tidak berhasil:

 int f() { return 6; } int f() { return 9; } 

Unit Siaran

Pemrogram biasanya bekerja dengan file header dan file implementasi. Tetapi bukan kompiler - mereka bekerja dengan unit terjemahan (unit terjemahan, singkatnya - TU), yang kadang-kadang disebut unit kompilasi. Definisi unit semacam itu cukup sederhana - file apa pun yang ditransfer ke kompiler setelah pemrosesan awal. Untuk lebih tepatnya, ini adalah file yang dihasilkan dari pekerjaan preprocessor makro ekstensi yang menyertakan kode sumber, yang bergantung pada ekspresi #ifndef dan #ifndef , dan salin-tempel semua file #include .

File-file berikut tersedia:

header.hpp:

 #ifndef HEADER_HPP #define HEADER_HPP #define VALUE 5 #ifndef VALUE struct Foo { private: int ryan; }; #endif int strlen(const char* string); #endif /* HEADER_HPP */ 

program.cpp:

 #include "header.hpp" int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + VALUE; } 

Preprocessor akan menghasilkan unit terjemahan berikut, yang kemudian diteruskan ke kompiler:

 int strlen(const char* string); int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + 5; } 

Komunikasi

Setelah membahas dasar-dasarnya, Anda dapat memulai hubungan. Secara umum, komunikasi adalah visibilitas karakter untuk tautan saat memproses file. Komunikasi dapat berupa eksternal atau internal.

Komunikasi eksternal

Ketika simbol (variabel atau fungsi) memiliki koneksi eksternal, itu menjadi terlihat oleh linker dari file lain, yaitu, "terlihat secara global", dapat diakses oleh semua unit terjemahan. Ini berarti bahwa Anda harus mendefinisikan simbol seperti itu di tempat tertentu dari satu unit terjemahan, biasanya dalam file implementasi (.c / .cpp), sehingga hanya memiliki satu definisi yang terlihat. Jika Anda mencoba untuk secara bersamaan mendefinisikan simbol pada saat yang sama dengan simbol tersebut dideklarasikan, atau jika Anda menempatkan definisi dalam file untuk deklarasi, Anda berisiko membuat marah penghubung. Mencoba menambahkan file ke lebih dari satu file implementasi mengarah pada penambahan definisi ke lebih dari satu unit terjemahan - linker Anda akan menangis.

Kata kunci eksternal dalam C dan C ++ (secara eksplisit) menyatakan bahwa suatu karakter memiliki koneksi eksternal.

 extern int x; extern void f(const std::string& argument); 

Kedua karakter memiliki koneksi eksternal. Tercatat di atas bahwa variabel global const memiliki ikatan internal secara default, variabel global non-const memiliki ikatan eksternal. Ini berarti int x; - sama seperti extern int x;, kan? Tidak juga. int x; sebenarnya analog dengan extern int x {}; (menggunakan sintaks inisialisasi universal / braket untuk menghindari penguraian yang paling tidak menyenangkan (penguraian paling menjengkelkan)), karena int x; tidak hanya mendeklarasikan, tetapi juga mendefinisikan x. Oleh karena itu, jangan tambahkan eksternal ke int x; global sama buruknya dengan mendefinisikan variabel saat mendeklarasikannya dari luar:

 int x; //   ,   extern int x{}; //      . extern int x; //      ,   

Contoh yang buruk

Mari kita mendeklarasikan fungsi f dengan tautan eksternal di file.hpp dan mendefinisikannya di sana:

 // file.hpp #ifndef FILE_HPP #define FILE_HPP extern int f(int x); /* ... */ int f(int) { return x + 1; } /* ... */ #endif /* FILE_HPP */ 

Harap dicatat bahwa Anda tidak perlu menambahkan extern di sini, karena semua fungsi secara eksternal extern. Pemisahan deklarasi dan definisi juga tidak diperlukan. Jadi mari kita tulis ulang seperti ini:

 // file.hpp #ifndef FILE_HPP #define FILE_HPP int f(int) { return x + 1; } #endif /* FILE_HPP */ 

Kode semacam itu dapat ditulis sebelum membaca artikel ini, atau setelah membacanya di bawah pengaruh alkohol atau zat-zat berat (misalnya, gulungan kayu manis).

Mari kita lihat mengapa ini tidak sepadan. Sekarang kita memiliki dua file implementasi: a.cpp dan b.cpp, keduanya termasuk dalam file.hpp:

 // a.cpp #include "file.hpp" /* ... */ 


 // b.cpp #include "file.hpp" /* ... */ 

Sekarang biarkan kompiler bekerja dan menghasilkan dua unit terjemahan untuk dua file implementasi di atas (ingat bahwa #include secara harfiah berarti salin / tempel):

 // TU A, from a.cpp int f(int) { return x + 1; } /* ... */ 

 // TU B, from b.cpp int f(int) { return x + 1; } /* ... */ 

Pada titik ini, linker melakukan intervensi (pengikatan terjadi setelah kompilasi). Linker mengambil karakter f dan mencari definisi. Hari ini dia beruntung, dia menemukan sebanyak dua! Satu di unit terjemahan A, yang lain di B. Linker membeku dengan bahagia dan memberi tahu Anda sesuatu seperti ini:

 duplicate symbol __Z1fv in: /path/to/ao /path/to/bo 

Linker menemukan dua definisi untuk satu karakter f . Karena f memiliki ikatan eksternal, itu terlihat oleh penghubung ketika memproses A dan B. Jelas, ini melanggar Aturan Satu Definisi dan menyebabkan kesalahan. Lebih tepatnya, ini menyebabkan kesalahan simbol duplikat, yang Anda akan menerima tidak kurang dari kesalahan simbol tidak terdefinisi yang terjadi ketika Anda mendeklarasikan simbol, tetapi lupa untuk mendefinisikannya.

Gunakan

Contoh standar untuk mendeklarasikan variabel eksternal adalah variabel global. Misalkan Anda sedang mengerjakan kue self-baking. Tentunya ada variabel global yang terkait dengan kue yang harus tersedia di berbagai bagian program Anda. Katakanlah frekuensi jam dari sirkuit yang dapat dimakan di dalam kue Anda. Nilai ini diperlukan secara alami di berbagai bagian untuk operasi sinkron dari semua elektronik cokelat. Cara (jahat) C untuk mendeklarasikan variabel global seperti ini adalah sebagai makro:

 #define CLK 1000000 

Seorang programmer C ++ yang merasa jijik dengan makro akan menulis kode nyata lebih baik. Sebagai contoh, ini:

 // global.hpp namespace Global { extern unsigned int clock_rate; } // global.cpp namespace Global { unsigned int clock_rate = 1000000; } 

(Seorang programmer C ++ modern akan ingin menggunakan literal pemisahan: unsigned int clock_rate = 1'000'000;)

Interkom

Jika simbol memiliki koneksi internal, maka simbol hanya akan terlihat di dalam unit terjemahan saat ini. Jangan mengacaukan visibilitas dengan hak akses, seperti pribadi. Visibilitas berarti bahwa penghubung akan dapat menggunakan simbol ini hanya ketika memproses unit terjemahan di mana simbol dinyatakan, dan tidak lebih lambat (seperti dalam kasus simbol dengan komunikasi eksternal). Dalam praktiknya, ini berarti bahwa ketika mendeklarasikan simbol dengan tautan internal di file header, setiap unit siaran yang menyertakan file ini akan menerima salinan unik dari simbol ini. Seolah-olah Anda telah menentukan masing-masing simbol tersebut di setiap unit terjemahan. Untuk objek, ini berarti bahwa kompiler secara harfiah akan mengalokasikan salinan yang sama sekali baru dan unik untuk setiap unit terjemahan, yang, jelas, dapat menyebabkan biaya memori tinggi.

Untuk mendeklarasikan simbol yang saling berhubungan, kata kunci statis ada di C dan C ++. Penggunaan ini berbeda dari penggunaan statis di kelas dan fungsi (atau, secara umum, di blok apa pun).

Contoh

Berikut ini sebuah contoh:

header.hpp:

 static int variable = 42; 

file1.hpp:

 void function1(); 

file2.hpp:

 void function2(); 

file1.cpp:

 #include "header.hpp" void function1() { variable = 10; } 


file2.cpp:

 #include "header.hpp" void function2() { variable = 123; } 

main.cpp:

 #include "header.hpp" #include "file1.hpp" #include "file2.hpp" #include <iostream> auto main() -> int { function1(); function2(); std::cout << variable << std::endl; } 

Setiap unit terjemahan, termasuk header.hpp, mendapat salinan variabel yang unik, karena koneksi internalnya. Ada tiga unit terjemahan:

  1. file1.cpp
  2. file2.cpp
  3. main.cpp

Ketika function1 dipanggil, salinan variabel file1.cpp mendapatkan nilai 10. Ketika function2 dipanggil, salinan variabel file2.cpp mendapatkan nilai 123. Namun, nilai yang dikembalikan di main.cpp tidak berubah dan tetap sama dengan 42.

Ruang nama anonim

Di C ++, ada cara lain untuk mendeklarasikan satu atau lebih karakter yang terhubung secara internal: ruang nama anonim. Ruang seperti itu memastikan bahwa karakter yang dinyatakan di dalamnya hanya dapat dilihat di unit terjemahan saat ini. Pada dasarnya, ini hanya cara untuk mendeklarasikan beberapa karakter statis. Untuk sementara, penggunaan kata kunci statis untuk mendeklarasikan karakter terkait-internal ditinggalkan demi ruang nama anonim. Namun, mereka kembali menggunakannya karena kemudahan mendeklarasikan satu variabel atau fungsi dengan komunikasi internal. Ada beberapa perbedaan kecil lainnya yang tidak akan saya pikirkan.

Bagaimanapun, ini adalah:

 namespace { int variable = 0; } 

Apakah (hampir) sama dengan:

 static int variable = 0; 

Gunakan

Jadi dalam hal apa menggunakan koneksi internal? Menggunakannya untuk objek adalah ide yang buruk. Konsumsi memori benda besar bisa sangat tinggi karena menyalin untuk setiap unit terjemahan. Tetapi pada dasarnya, itu hanya menyebabkan perilaku aneh dan tidak terduga. Bayangkan bahwa Anda memiliki singleton (kelas di mana Anda membuat instance hanya satu instance) dan tiba-tiba beberapa contoh "singleton" Anda muncul (satu untuk setiap unit terjemahan).

Namun, komunikasi internal dapat digunakan untuk menyembunyikan unit terjemahan dari area global fungsi pembantu lokal. Misalkan ada fungsi foo helper di file1.hpp yang Anda gunakan di file1.cpp. Pada saat yang sama, Anda memiliki fungsi foo di file2.hpp digunakan di file2.cpp. Foo pertama dan kedua berbeda satu sama lain, tetapi Anda tidak dapat menemukan nama lain. Karena itu, Anda dapat mendeklarasikannya statis. Jika Anda tidak menambahkan file1.hpp dan file2.hpp ke unit terjemahan yang sama, maka ini akan menyembunyikan foo satu sama lain. Jika ini tidak dilakukan, maka mereka secara implisit akan memiliki koneksi eksternal dan definisi foo pertama akan menemui definisi yang kedua, menyebabkan kesalahan linker tentang melanggar aturan satu definisi.

AKHIR

Anda selalu dapat meninggalkan komentar dan / atau pertanyaan Anda di sini atau kunjungi kami di hari terbuka.

Source: https://habr.com/ru/post/id432834/


All Articles