Apa yang membuat penggunaan pointer ini tidak dapat diprediksi?

108

Saat ini saya sedang mempelajari petunjuk dan profesor saya memberikan potongan kode ini sebagai contoh:

//We cannot predict the behavior of this program!

#include <iostream>
using namespace std;

int main()
{
    char * s = "My String";
    char s2[] = {'a', 'b', 'c', '\0'};

    cout << s2 << endl;

    return 0;
}

Dia menulis di komentar bahwa kami tidak dapat memprediksi perilaku program. Apa sebenarnya yang membuatnya tidak dapat diprediksi? Saya tidak melihat ada yang salah dengan itu.

trungnt
sumber
2
Apakah Anda yakin telah mereproduksi kode profesor dengan benar? Meskipun secara formal mungkin untuk menyatakan bahwa program ini mungkin menghasilkan perilaku yang "tidak dapat diprediksi", tidak masuk akal untuk melakukannya. Dan saya ragu bahwa ada profesor yang akan menggunakan sesuatu yang begitu esoteris untuk menggambarkan "tak terduga" kepada siswa.
AnT
1
@Lightness Races di Orbit: Penyusun diizinkan untuk "menerima" kode berbentuk buruk setelah mengeluarkan pesan diagnostik yang diperlukan. Tetapi spesifikasi bahasa tidak menentukan perilaku kode. Yaitu karena kesalahan dalam inisialisasi s, program, jika diterima oleh beberapa kompilator, secara formal memiliki perilaku yang tidak dapat diprediksi.
AnT
2
@TheParamagneticCroissant: Tidak. Inisialisasi ini salah bentuk di zaman modern.
Balapan Ringan di Orbit
2
@ The Paramagnetic Croissant: Seperti yang saya katakan di atas, bahasa tidak memerlukan kode yang salah format untuk "gagal dikompilasi". Penyusun hanya perlu mengeluarkan diagnostik. Setelah itu mereka diizinkan untuk melanjutkan dan "berhasil" mengkompilasi kode. Namun, perilaku kode tersebut tidak ditentukan oleh spesifikasi bahasa.
AnT
2
Saya ingin tahu apa jawaban yang diberikan profesor Anda.
Daniël W. Crompton

Jawaban:

125

Perilaku program tidak ada, karena bentuknya buruk.

char* s = "My String";

Ini ilegal. Sebelum 2011, sudah tidak digunakan lagi selama 12 tahun.

Baris yang benar adalah:

const char* s = "My String";

Selain itu, programnya baik-baik saja. Profesor Anda harus minum lebih sedikit wiski!

Balapan Ringan dalam Orbit
sumber
10
with -pedantic yang dilakukannya: main.cpp: 6: 16: peringatan: ISO C ++ melarang pengubahan konstanta string menjadi 'char *' [-Wpedantic]
marcinj
17
@black: Tidak, fakta bahwa konversi ilegal membuat program menjadi tidak benar. Itu sudah usang di masa lalu . Kami tidak lagi di masa lalu.
Balapan Ringan di Orbit
17
(Yang konyol karena itulah tujuan penghentian 12 tahun)
Lightness Races in Orbit
17
@ hitam: Perilaku program yang bentuknya buruk tidak "didefinisikan dengan sempurna".
Balapan Ringan di Orbit
11
Terlepas dari itu, pertanyaannya adalah tentang C ++, bukan tentang beberapa versi GCC tertentu.
Balapan Ringan di Orbit
81

Jawabannya adalah: itu tergantung pada standar C ++ yang Anda kompilasi. Semua kode dibentuk dengan sempurna di semua standar ‡ dengan pengecualian baris ini:

char * s = "My String";

Sekarang, string literal memiliki tipe const char[10]dan kami mencoba untuk menginisialisasi pointer non-const untuk itu. Untuk semua jenis selain charkeluarga string literal, inisialisasi seperti itu selalu ilegal. Sebagai contoh:

const int arr[] = {1};
int *p = arr; // nope!

Namun, di pra-C ++ 11, untuk string literal, ada pengecualian di §4.2 / 2:

Sebuah string literal (2.13.4) yang bukan literal string lebar dapat diubah menjadi nilai r bertipe " pointer to char "; [...]. Dalam kedua kasus tersebut, hasilnya adalah penunjuk ke elemen pertama dari array. Konversi ini dianggap hanya jika terdapat tipe target penunjuk yang tepat secara eksplisit, dan bukan jika ada kebutuhan umum untuk mengubah nilai l ke nilai r. [Catatan: konversi ini tidak berlaku lagi . Lihat Lampiran D. ]

Jadi di C ++ 03, kodenya baik-baik saja (meskipun usang), dan memiliki perilaku yang jelas dan dapat diprediksi.

Di C ++ 11, blok itu tidak ada - tidak ada pengecualian untuk string literal yang diubah menjadi char*, dan kode sama buruknya dengan int*contoh yang baru saja saya berikan. Kompilator berkewajiban untuk mengeluarkan diagnostik, dan idealnya dalam kasus seperti ini yang merupakan pelanggaran yang jelas dari sistem tipe C ++, kita mengharapkan kompilator yang baik tidak hanya menyesuaikan dalam hal ini (misalnya dengan mengeluarkan peringatan) tetapi gagal sekaligus.

Kode idealnya tidak dapat dikompilasi - tetapi dilakukan pada gcc dan clang (saya berasumsi karena mungkin ada banyak kode di luar sana yang akan rusak dengan sedikit keuntungan, meskipun lubang sistem jenis ini tidak digunakan lagi selama lebih dari satu dekade). Kode tersebut berbentuk buruk, dan oleh karena itu tidak masuk akal untuk mempertimbangkan seperti apa perilaku kode tersebut. Tetapi mengingat kasus khusus ini dan riwayatnya sebelumnya diizinkan, saya tidak percaya itu menjadi peregangan yang tidak masuk akal untuk menafsirkan kode yang dihasilkan seolah-olah itu implisit const_cast, sesuatu seperti:

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

Dengan itu, program lainnya baik-baik saja, karena Anda tidak pernah benar-benar menyentuhnya slagi. Membaca sebuah diciptakan- constobjek melalui non constpointer ini sangat OK. Menulis sebuah diciptakan- constobjek melalui pointer tersebut adalah perilaku undefined:

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

Karena tidak ada modifikasi melalui smana pun di kode Anda, program ini baik-baik saja di C ++ 03, seharusnya gagal dikompilasi di C ++ 11 tetapi tetap melakukannya - dan mengingat bahwa kompiler mengizinkannya, masih tidak ada perilaku yang tidak ditentukan di dalamnya † . Dengan kelonggaran bahwa kompiler masih [salah] menafsirkan aturan C ++ 03, saya tidak melihat apa pun yang akan menyebabkan perilaku "tidak dapat diprediksi". Tulislah s, dan semua taruhan dibatalkan. Di C ++ 03 dan C ++ 11.


† Meskipun, sekali lagi, menurut definisi kode yang tidak benar tidak menghasilkan harapan perilaku yang wajar
‡ Kecuali tidak, lihat jawaban Matt McNabb

Barry
sumber
Saya pikir di sini "tidak dapat diprediksi" dimaksudkan oleh profesor yang berarti bahwa seseorang tidak dapat menggunakan standar untuk memprediksi apa yang akan dilakukan kompilator dengan kode yang salah bentuk (selain mengeluarkan diagnostik). Ya, itu bisa memperlakukannya sebagai C ++ 03 mengatakan itu harus diperlakukan, dan (dengan risiko kekeliruan "No True Scotsman") akal sehat memungkinkan kita untuk memprediksi dengan keyakinan bahwa ini adalah satu-satunya hal yang masuk akal compiler-writer akan memilih apakah kode dapat dikompilasi sama sekali. Kemudian lagi, itu bisa memperlakukannya sebagai arti untuk membalikkan string literal sebelum mentransmisikannya ke non-const. Standar C ++ tidak peduli.
Steve Jessop
2
@SteveJessop Saya tidak percaya interpretasi itu. Ini bukanlah perilaku yang tidak terdefinisi atau kategori kode yang salah bentuk yang label standarnya tidak diperlukan diagnostik. Ini adalah jenis pelanggaran sistem sederhana yang seharusnya sangat dapat diprediksi (mengkompilasi dan melakukan hal-hal normal pada C ++ 03, gagal untuk dikompilasi pada C ++ 11). Anda tidak dapat benar-benar menggunakan bug compiler (atau lisensi artistik) untuk menunjukkan bahwa kode tidak dapat diprediksi - jika tidak, semua kode secara tautologis tidak dapat diprediksi.
Barry
Saya tidak berbicara tentang bug kompiler, saya berbicara tentang apakah standar mendefinisikan perilaku (jika ada) dari kode atau tidak. Saya menduga profesor melakukan hal yang sama, dan "tak terduga" hanyalah cara kasar untuk mengatakan bahwa standar saat ini tidak mendefinisikan perilaku. Bagaimanapun, itu tampaknya lebih mungkin bagi saya, daripada itu profesor secara keliru percaya bahwa ini adalah program yang dibentuk dengan baik dengan perilaku yang tidak ditentukan.
Steve Jessop
1
Tidak. Standar tidak menentukan perilaku program yang bentuknya buruk.
Steve Jessop
1
@supercat: itu poin yang adil, tapi saya tidak percaya itu alasan utamanya. Saya pikir alasan utama standar tidak menentukan perilaku program yang tidak benar, adalah agar kompiler dapat mendukung ekstensi ke bahasa dengan menambahkan sintaks yang tidak terbentuk dengan baik (seperti yang dilakukan oleh Objective C). Mengizinkan implementasi untuk membuat total horlicks keluar dari pembersihan setelah kompilasi yang gagal hanyalah bonus :-)
Steve Jessop
20

Jawaban lain telah menutupi bahwa program ini salah format dalam C ++ 11 karena penugasan const chararray ke a char *.

Namun program ini juga buruk bentuknya sebelum C ++ 11.

Ada operator<<kelebihan beban <ostream>. Persyaratan untuk iostreamdisertakan ostreamtelah ditambahkan dalam C ++ 11.

Secara historis, sebagian besar implementasi telah iostreammenyertakan ostream, mungkin untuk kemudahan implementasi atau mungkin untuk memberikan QoI yang lebih baik.

Tapi itu akan sesuai untuk iostreamhanya mendefinisikan ostreamkelas tanpa mendefinisikan operator<<kelebihan beban.

MM
sumber
13

Satu-satunya hal yang sedikit salah yang saya lihat dengan program ini adalah Anda tidak seharusnya menetapkan literal string ke charpenunjuk yang bisa berubah , meskipun ini sering diterima sebagai ekstensi kompiler.

Jika tidak, program ini tampak terdefinisi dengan baik bagi saya:

  • Aturan yang menentukan bagaimana larik karakter menjadi penunjuk karakter ketika diteruskan sebagai parameter (seperti dengan cout << s2) didefinisikan dengan baik.
  • Array diakhiri null, yang merupakan kondisi operator<<dengan a char*(atau a const char*).
  • #include <iostream>termasuk <ostream>, yang pada gilirannya menentukan operator<<(ostream&, const char*), sehingga semuanya tampak pada tempatnya.
zneak
sumber
12

Anda tidak dapat memprediksi perilaku kompilator, karena alasan yang disebutkan di atas. ( Seharusnya gagal untuk dikompilasi, tetapi mungkin tidak.)

Jika kompilasi berhasil, maka perilakunya didefinisikan dengan baik. Anda pasti bisa memprediksi perilaku program.

Jika gagal dikompilasi, tidak ada program. Dalam bahasa yang dikompilasi, program ini dapat dieksekusi, bukan kode sumbernya. Jika Anda tidak memiliki file yang dapat dieksekusi, Anda tidak memiliki program, dan Anda tidak dapat berbicara tentang perilaku sesuatu yang tidak ada.

Jadi menurut saya pernyataan prof Anda salah. Anda tidak dapat memprediksi perilaku kompilator ketika dihadapkan dengan kode ini, tetapi itu berbeda dari perilaku program . Jadi jika dia akan memilih telur kutu, dia sebaiknya memastikan dia benar. Atau, tentu saja, Anda mungkin salah mengutipnya dan kesalahan itu terletak pada terjemahan Anda atas apa yang dia katakan.

Graham
sumber
10

Seperti yang telah dicatat orang lain, kode tersebut tidak sah di bawah C ++ 11, meskipun itu valid di bawah versi sebelumnya. Akibatnya, compiler untuk C ++ 11 diperlukan untuk mengeluarkan setidaknya satu diagnostik, tetapi perilaku compiler atau sistem build lainnya tidak ditentukan di luar itu. Tidak ada dalam Standar yang akan melarang kompilator untuk keluar secara tiba-tiba sebagai tanggapan atas kesalahan, meninggalkan file objek yang ditulis sebagian yang mungkin dianggap valid oleh linker, menghasilkan file eksekusi yang rusak.

Meskipun kompilator yang baik harus selalu memastikan sebelum keluar bahwa file objek apa pun yang diharapkan dihasilkan akan valid, tidak ada, atau dikenali sebagai tidak valid, masalah seperti itu berada di luar yurisdiksi Standar. Meskipun secara historis ada (dan mungkin masih) beberapa platform di mana kompilasi yang gagal dapat menghasilkan file yang dapat dieksekusi yang tampak sah yang macet secara sewenang-wenang ketika dimuat (dan saya harus bekerja dengan sistem di mana kesalahan tautan sering memiliki perilaku seperti itu) , Saya tidak akan mengatakan bahwa konsekuensi kesalahan sintaks pada umumnya tidak dapat diprediksi. Pada sistem yang baik, upaya membangun umumnya akan menghasilkan executable dengan upaya terbaik kompiler pada pembuatan kode, atau tidak akan menghasilkan eksekusi sama sekali. Beberapa sistem akan meninggalkan eksekusi lama setelah gagal dibangun,

Preferensi pribadi saya adalah untuk sistem berbasis disk untuk mengganti nama file output, untuk memungkinkan kesempatan langka ketika eksekusi itu akan berguna sambil menghindari kebingungan yang dapat diakibatkan oleh secara keliru percaya bahwa seseorang menjalankan kode baru, dan untuk pemrograman tertanam sistem untuk memungkinkan programmer untuk menentukan untuk setiap proyek program yang harus dimuat jika executable yang valid tidak tersedia dengan nama normal [idealnya sesuatu yang dengan aman menunjukkan kurangnya program yang bisa digunakan]. Perangkat sistem tertanam umumnya tidak memiliki cara untuk mengetahui apa yang harus dilakukan oleh program semacam itu, tetapi dalam banyak kasus seseorang yang menulis kode "nyata" untuk suatu sistem akan memiliki akses ke beberapa kode pengujian perangkat keras yang dapat dengan mudah diadaptasi ke tujuan. Saya tidak tahu bahwa saya telah melihat perilaku penggantian nama, namun,

supercat
sumber