Mengapa anggota data statis harus didefinisikan di luar kelas secara terpisah dalam C ++ (tidak seperti Java)?

41
class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

Saya tidak melihat perlunya A::xmendefinisikan secara terpisah dalam file .cpp (atau file yang sama untuk template). Mengapa tidak dapat A::xdideklarasikan dan didefinisikan pada saat yang sama?

Apakah itu dilarang karena alasan historis?

Pertanyaan utama saya adalah, apakah ini akan memengaruhi fungsionalitas apa pun jika staticanggota data dideklarasikan / didefinisikan pada waktu yang sama (sama dengan Java )?

iammilind
sumber
Sebagai praktik terbaik, umumnya lebih baik untuk membungkus variabel statis Anda dalam metode statis (mungkin sebagai statis lokal) untuk menghindari masalah urutan inisialisasi.
Tamás Szelei
2
Aturan ini sebenarnya sedikit santai di C ++ 11. anggota const static biasanya tidak harus didefinisikan lagi. Lihat: en.wikipedia.org/wiki/…
mirk
4
@afishwhoswimsaround: Menentukan aturan yang digeneralisasi untuk semua situasi bukanlah ide yang baik (praktik terbaik harus diterapkan dengan konteks). Di sini Anda mencoba menyelesaikan masalah yang tidak ada. Masalah urutan inisialisasi hanya memengaruhi objek yang memiliki konstruktor dan mengakses objek durasi penyimpanan statis lainnya. Karena 'x' adalah int, yang pertama tidak berlaku karena 'x' adalah pribadi, yang kedua tidak berlaku. Ketiga, ini tidak ada hubungannya dengan pertanyaan.
Martin York
1
Milik Stack Overflow?
Lightness Races dengan Monica
2
C ++ 17 memungkinkan inisialisasi inline anggota data statis (bahkan untuk jenis non-integer): inline static int x[] = {1, 2, 3};. Lihat en.cppreference.com/w/cpp/language/static#Static_data_members
Vladimir Reshetnikov

Jawaban:

15

Saya pikir batasan yang Anda pertimbangkan tidak terkait dengan semantik (mengapa sesuatu harus berubah jika inisialisasi didefinisikan dalam file yang sama?) Tetapi lebih kepada model kompilasi C ++ yang, karena alasan kompatibilitas mundur, tidak dapat dengan mudah diubah karena akan menjadi terlalu kompleks (mendukung model kompilasi baru dan yang ada pada saat yang sama) atau tidak akan mengizinkan untuk mengkompilasi kode yang ada (dengan memperkenalkan model kompilasi baru dan menjatuhkan yang ada).

Model kompilasi C ++ berasal dari C, di mana Anda mengimpor deklarasi ke file sumber dengan menyertakan file (header). Dengan cara ini, kompiler melihat persis satu file sumber besar, yang berisi semua file yang disertakan, dan semua file yang disertakan dari file-file itu, secara rekursif. Ini memiliki IMO satu keuntungan besar, yaitu membuat kompiler lebih mudah diimplementasikan. Tentu saja, Anda dapat menulis apa pun di file yang disertakan, yaitu deklarasi dan definisi. Ini hanya praktik yang baik untuk meletakkan deklarasi dalam file header dan definisi dalam file .c atau .cpp.

Di sisi lain, dimungkinkan untuk memiliki model kompilasi di mana kompiler tahu betul jika mengimpor deklarasi simbol global yang didefinisikan dalam modul lain , atau jika sedang menyusun definisi simbol global yang disediakan oleh modul saat ini . Hanya dalam kasus terakhir kompiler harus meletakkan simbol ini (misalnya variabel) di file objek saat ini.

Misalnya, dalam GNU Pascal Anda dapat menulis sebuah unit adalam file a.passeperti ini:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

di mana variabel global dideklarasikan dan diinisialisasi dalam file sumber yang sama.

Kemudian Anda dapat memiliki unit berbeda yang mengimpor dan menggunakan variabel global MyStaticVariable, misalnya unit b ( b.pas):

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

dan unit c ( c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

Akhirnya Anda dapat menggunakan unit b dan c dalam program utama m.pas:

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

Anda dapat mengkompilasi file-file ini secara terpisah:

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

dan kemudian menghasilkan executable dengan:

$ gpc -o m m.o a.o b.o c.o

dan jalankan:

$ ./m
1
2
3

Kuncinya di sini adalah bahwa ketika kompilator melihat arahan kegunaan dalam modul program (mis. Menggunakan a di b.pas), kompiler tidak menyertakan file .pas yang sesuai, tetapi mencari file .gpi, yaitu untuk pra-kompilasi file antarmuka (lihat dokumentasi ). .gpiFile - file ini dihasilkan oleh kompilator bersama dengan .ofile ketika setiap modul dikompilasi. Jadi simbol global MyStaticVariablehanya didefinisikan sekali dalam file objek a.o.

Java bekerja dengan cara yang sama: ketika kompiler mengimpor kelas A ke kelas B, ia melihat file kelas untuk A dan tidak memerlukan file A.java. Jadi semua definisi dan inisialisasi untuk kelas A dapat dimasukkan ke dalam satu file sumber.

Kembali ke C ++, alasan mengapa dalam C ++ Anda harus mendefinisikan anggota data statis dalam file terpisah lebih terkait dengan model kompilasi C ++ daripada keterbatasan yang dikenakan oleh linker atau alat lain yang digunakan oleh kompiler. Di C ++, mengimpor beberapa simbol berarti membangun deklarasi mereka sebagai bagian dari unit kompilasi saat ini. Ini sangat penting, antara lain, karena cara templat dikompilasi. Tetapi ini menyiratkan bahwa Anda tidak dapat / tidak boleh mendefinisikan simbol global (fungsi, variabel, metode, anggota data statis) dalam file yang disertakan, jika tidak, simbol-simbol ini dapat didefinisikan berlipat ganda dalam file objek yang dikompilasi.

Giorgio
sumber
42

Karena anggota statis dibagikan di antara SEMUA instance kelas, mereka harus didefinisikan dalam satu dan hanya satu tempat. Sungguh, mereka variabel global dengan beberapa batasan akses.

Jika Anda mencoba mendefinisikannya di header, mereka akan didefinisikan di setiap modul yang menyertakan header itu, dan Anda akan mendapatkan kesalahan saat menautkan karena menemukan semua definisi duplikat.

Ya, ini setidaknya sebagian merupakan masalah historis yang berasal dari cfront; kompiler dapat ditulis yang akan membuat semacam "static_members_of_everything.cpp" yang tersembunyi dan tautan ke sana. Namun, itu akan merusak kompatibilitas, dan tidak akan ada manfaat nyata untuk melakukannya.

mjfgates
sumber
2
Pertanyaan saya bukan alasan untuk perilaku saat ini, melainkan pembenaran untuk tata bahasa seperti itu. Dengan kata lain, anggaplah jika staticvariabel dideklarasikan / didefinisikan di tempat yang sama (seperti Java) lalu apa yang bisa salah?
iammilind
8
@ iammilind Saya pikir Anda tidak mengerti bahwa tata bahasa itu perlu karena penjelasan dari jawaban ini. Sekarang kenapa? Karena model kompilasi C (dan C ++): file c dan cpp adalah file kode nyata yang dikompilasi secara terpisah seperti program yang terpisah, maka mereka dihubungkan bersama untuk membuat eksekusi penuh. Header tidak benar-benar kode untuk kompiler, mereka hanya teks untuk menyalin dan menempelkan di dalam file c dan cpp. Sekarang jika sesuatu didefinisikan beberapa kali, itu tidak dapat dikompilasi, cara yang sama tidak akan dikompilasi jika Anda memiliki beberapa variabel lokal dengan nama yang sama.
Klaim
1
@Klaim, bagaimana dengan staticanggota template? Mereka diizinkan di semua file header karena mereka harus terlihat. Saya tidak membantah jawaban ini, tetapi tidak cocok dengan pertanyaan saya juga.
iammilind
Templat @ iammilind bukan kode asli, melainkan kode yang menghasilkan kode. Setiap instance dari template memiliki satu dan hanya satu instance statis dari setiap deklarasi statis yang disediakan oleh kompiler. Anda masih harus mendefinisikan instance tetapi ketika Anda mendefinisikan template dari instance, itu bukan kode nyata, seperti dikatakan di atas. Templat, secara harfiah, templat kode bagi kompiler untuk menghasilkan kode.
Klaim
2
@ iammilind: Template biasanya dipakai di setiap file objek, termasuk variabel statis mereka. Di Linux dengan file objek ELF, kompiler menandai instantiations sebagai simbol yang lemah , yang berarti bahwa linker menggabungkan banyak salinan dari instantiasi yang sama. Teknologi yang sama dapat digunakan untuk memungkinkan mendefinisikan variabel statis dalam file header, sehingga alasan itu tidak dilakukan mungkin merupakan kombinasi dari alasan historis dan kompilasi pertimbangan kinerja. Seluruh model kompilasi diharapkan akan diperbaiki setelah standar C ++ berikutnya memasukkan modul .
Han
6

Alasan yang mungkin untuk ini adalah bahwa ini membuat bahasa C ++ dapat diimplementasikan dalam lingkungan di mana file objek dan model tautan tidak mendukung penggabungan beberapa definisi dari beberapa file objek.

Deklarasi kelas (disebut deklarasi karena alasan yang baik) ditarik ke dalam beberapa unit terjemahan. Jika deklarasi berisi definisi untuk variabel statis, maka Anda akan berakhir dengan beberapa definisi dalam beberapa unit terjemahan (Dan ingat, nama-nama ini memiliki tautan eksternal.)

Situasi itu mungkin, tetapi mengharuskan penghubung untuk menangani banyak definisi tanpa mengeluh.

(Dan perhatikan bahwa ini bertentangan dengan Aturan Satu Definisi, kecuali jika hal itu dapat dilakukan sesuai dengan jenis simbol atau bagian seperti apa itu ditempatkan.)

Kaz
sumber
6

Ada perbedaan besar antara C ++ dan Java.

Java beroperasi pada mesin virtualnya sendiri yang menciptakan semuanya menjadi lingkungan run-time-nya sendiri. Jika suatu definisi terlihat lebih dari sekali, hanya akan bertindak pada objek yang sama dengan yang diketahui oleh lingkungan runtime.

Dalam C ++ tidak ada "pemilik pengetahuan pamungkas": C ++, C, Fortran Pascal dll. Semuanya adalah "penerjemah" dari kode sumber (file CPP) ke dalam format perantara (file OBJ, atau file ".o", tergantung pada OS) di mana pernyataan diterjemahkan ke dalam instruksi mesin dan nama menjadi alamat tidak langsung yang dimediasi oleh tabel simbol.

Suatu program tidak dibuat oleh kompiler, tetapi oleh program lain ("penghubung"), yang menggabungkan semua OBJ-s bersama-sama (tidak peduli bahasa asalnya) dengan menunjuk kembali semua alamat yang mengarah ke simbol, ke arah mereka. definisi yang efektif.

By the way linker bekerja, definisi (apa yang menciptakan ruang fisik untuk suatu variabel) harus unik.

Perhatikan bahwa C ++ tidak dengan sendirinya tautan, dan bahwa penghubung tidak dikeluarkan oleh spesifikasi C ++: penghubung ada karena cara modul OS dibangun (biasanya dalam C dan ASM). C ++ harus menggunakannya sebagaimana mestinya.

Sekarang: file header adalah sesuatu yang harus "disisipkan ke" beberapa file CPP. Setiap file CPP diterjemahkan secara independen dari yang lain. Kompiler yang menerjemahkan file CPP yang berbeda, semua yang menerima-dalam definisi yang sama akan menempatkan " kode pembuatan " untuk objek yang ditentukan di semua OBJ yang dihasilkan.

Kompiler tidak tahu (dan tidak akan pernah tahu) apakah semua OBJ tersebut akan digunakan bersama untuk membentuk satu program atau secara terpisah untuk membentuk program independen yang berbeda.

Tautan tidak tahu bagaimana dan mengapa definisi ada dan dari mana asalnya (bahkan tidak tahu tentang C ++: setiap "bahasa statis" dapat menghasilkan definisi dan referensi untuk dihubungkan). Ia hanya tahu ada referensi ke "simbol" yang diberikan yang "didefinisikan" pada alamat yang diberikan.

Jika ada beberapa definisi (jangan bingung definisi dengan referensi) untuk simbol yang diberikan, linker tidak memiliki pengetahuan (menjadi bahasa agnostik) tentang apa yang harus dilakukan dengan mereka.

Ini seperti menggabungkan sejumlah kota untuk membentuk kota besar: jika Anda ditemukan memiliki dua " Time Square " dan sejumlah orang yang datang dari luar meminta untuk pergi ke " Time square ", Anda tidak dapat memutuskan berdasarkan teknis murni (tanpa pengetahuan tentang politik yang menetapkan nama-nama itu dan akan bertanggung jawab untuk mengelolanya) di tempat yang tepat untuk mengirimkannya.

Emilio Garavaglia
sumber
3
Perbedaan antara Java dan C ++ sehubungan dengan simbol global tidak terhubung dengan Java yang memiliki mesin virtual, melainkan dengan model kompilasi C ++. Dalam hal ini, saya tidak akan menempatkan Pascal dan C ++ dalam kategori yang sama. Sebaliknya saya akan mengelompokkan C dan C ++ bersama sebagai "bahasa di mana deklarasi impor dimasukkan dan dikompilasi bersama dengan file sumber utama" sebagai lawan dari Java dan Pascal (dan mungkin OCaml, Scala, Ada, dll) sebagai "bahasa di mana deklarasi yang diimpor dilihat oleh kompiler dalam file yang dikompilasi sebelumnya yang berisi informasi tentang simbol yang diekspor ".
Giorgio
1
@Iorgio: referensi ke Java mungkin tidak diterima, tapi saya pikir jawaban Emilio sebagian besar benar dengan mendapatkan inti masalah, yaitu fase file objek / linker setelah kompilasi terpisah.
ixache
5

Ini diperlukan karena jika tidak kompiler tidak tahu di mana harus meletakkan variabel. Setiap file cpp dikompilasi secara individual dan tidak tahu tentang yang lain. Tautan menyelesaikan variabel, fungsi, dll. Saya pribadi tidak melihat perbedaan antara anggota vtable dan statis (kita tidak harus memilih file apa yang didefinisikan oleh vtable).

Saya sebagian besar menganggap lebih mudah bagi penulis kompiler untuk mengimplementasikannya seperti itu. Vars statis di luar kelas / struct ada dan mungkin baik untuk alasan konsistensi atau karena akan lebih mudah untuk diterapkan bagi penulis kompiler mereka mendefinisikan pembatasan dalam standar.


sumber
2

Saya rasa saya menemukan alasannya. Mendefinisikan staticvariabel dalam ruang terpisah memungkinkan untuk menginisialisasi nilai apa pun. Jika tidak diinisialisasi maka akan default ke 0.

Sebelum C ++ 11 inisialisasi kelas tidak diizinkan di C ++. Jadi seseorang tidak bisa menulis seperti:

struct X
{
  static int i = 4;
};

Jadi sekarang untuk menginisialisasi variabel kita harus menulisnya di luar kelas sebagai:

struct X
{
  static int i;
};
int X::i = 4;

Sebagaimana dibahas dalam jawaban lain juga, int X::isekarang menjadi global dan menyatakan global dalam banyak file menyebabkan kesalahan tautan simbol banyak.

Jadi kita harus mendeklarasikan staticvariabel kelas di dalam unit terjemahan yang terpisah. Namun, masih dapat diperdebatkan bahwa cara berikut harus menginstruksikan kompiler untuk tidak membuat banyak simbol

static int X::i = 4;
^^^^^^
iammilind
sumber
0

A :: x hanyalah variabel global tetapi namespace'd ke A, dan dengan pembatasan akses.

Seseorang masih harus mendeklarasikannya, seperti variabel global lainnya, dan itu bahkan dapat dilakukan dalam proyek yang terhubung secara statis dengan proyek yang berisi sisa kode A.

Saya akan menyebut ini semua desain yang buruk, tetapi ada beberapa fitur yang dapat Anda manfaatkan dengan cara ini:

  1. pesanan panggilan konstruktor ... Tidak penting untuk int, tetapi untuk anggota yang lebih kompleks yang mungkin mengakses variabel statis atau global lainnya, ini bisa menjadi sangat penting.

  2. initializer statis - Anda dapat membiarkan klien memutuskan apa yang harus diinisialisasi oleh A :: x.

  3. di c ++ dan c, karena Anda memiliki akses penuh ke memori melalui pointer, lokasi fisik variabel signifikan. Ada hal-hal yang sangat nakal yang dapat Anda manfaatkan berdasarkan di mana variabel berada di objek tautan.

Saya ragu ini "mengapa" situasi ini telah muncul. Ini mungkin hanya evolusi C berubah menjadi C ++, dan masalah kompatibilitas mundur yang menghentikan Anda dari mengubah bahasa sekarang.

James Podesta
sumber
2
ini tampaknya tidak menawarkan sesuatu yang substansial atas poin yang dibuat dan dijelaskan dalam 6 jawaban sebelumnya
nyamuk