Apa alasan untuk string yang diakhiri null?

281

Seperti halnya saya suka C dan C ++, saya tidak bisa tidak menggaruk-garuk kepala saya pada pilihan string yang diakhiri null:

  • String awalan panjang (yaitu Pascal) ada sebelum C
  • String awalan panjang membuat beberapa algoritma lebih cepat dengan memungkinkan pencarian panjang waktu konstan.
  • String awalan panjang membuatnya lebih sulit untuk menyebabkan kesalahan buffer overrun.
  • Bahkan pada mesin 32 bit, jika Anda membiarkan string menjadi ukuran memori yang tersedia, string awalan panjang hanya tiga byte lebih lebar dari string yang diakhiri null. Pada mesin 16 bit ini adalah satu byte. Pada mesin 64 bit, 4GB adalah batas panjang string yang masuk akal, tetapi bahkan jika Anda ingin memperluasnya ke ukuran kata mesin, mesin 64 bit biasanya memiliki memori yang cukup membuat tambahan tujuh byte byte semacam argumen nol. Saya tahu standar C asli ditulis untuk mesin yang sangat buruk (dalam hal memori), tetapi argumen efisiensi tidak menjual saya di sini.
  • Hampir semua bahasa lainnya (yaitu Perl, Pascal, Python, Java, C #, dll) menggunakan string awalan panjang. Bahasa-bahasa ini biasanya mengalahkan C dalam benchmark manipulasi string karena mereka lebih efisien dengan string.
  • C ++ memperbaikinya sedikit dengan std::basic_stringtemplat, tetapi array karakter biasa yang mengharapkan string yang diakhiri null masih menyebar. Ini juga tidak sempurna karena membutuhkan alokasi tumpukan.
  • String yang diakhiri Null harus mencadangkan karakter (yaitu, null), yang tidak dapat ada dalam string, sementara string awalan panjang dapat berisi embedded nulls.

Beberapa dari hal-hal ini muncul lebih baru daripada C, jadi masuk akal bagi C untuk tidak mengetahuinya. Namun, beberapa jelas sebelum C terjadi. Mengapa string null yang diakhiri telah dipilih alih-alih awalan panjang yang jelas superior?

EDIT : Karena beberapa meminta fakta (dan tidak suka yang sudah saya berikan) pada poin efisiensi saya di atas, mereka berasal dari beberapa hal:

  • Concat menggunakan null terminated string membutuhkan O (n + m) kompleksitas waktu. Awalan panjang seringkali hanya membutuhkan O (m).
  • Panjang menggunakan string terminasi nol membutuhkan O (n) kompleksitas waktu. Panjang awalannya adalah O (1).
  • Panjang dan concat sejauh ini merupakan operasi string yang paling umum. Ada beberapa kasus di mana string null yang diakhiri dapat lebih efisien, tetapi ini terjadi jauh lebih jarang.

Dari jawaban di bawah, ini adalah beberapa kasus di mana string null yang diakhiri lebih efisien:

  • Ketika Anda harus memotong awal string dan harus meneruskannya ke beberapa metode. Anda tidak dapat benar-benar melakukan ini dalam waktu yang konstan dengan awalan panjang bahkan jika Anda diizinkan untuk menghancurkan string asli, karena awalan panjang mungkin perlu mengikuti aturan penyelarasan.
  • Dalam beberapa kasus di mana Anda hanya mengulang-ulang karakter string dengan karakter Anda mungkin dapat menyimpan register CPU. Perhatikan bahwa ini hanya berfungsi jika Anda belum mengalokasikan string secara dinamis (Karena itu Anda harus membebaskannya, mengharuskan menggunakan register CPU yang Anda simpan untuk memegang pointer yang Anda dapatkan dari malloc dan teman-teman).

Tak satu pun dari yang di atas hampir umum seperti panjang dan concat.

Ada satu lagi yang ditegaskan dalam jawaban di bawah ini:

  • Anda harus memotong ujung tali

tapi yang ini salah - jumlah waktu yang sama untuk string yang diakhiri nol dan panjang awalan. (String diakhiri Null hanya menempel nol di mana Anda ingin akhir baru menjadi, awalan panjang hanya mengurangi dari awalan.)

Billy ONeal
sumber
110
Saya selalu berpikir itu adalah ritual untuk semua programmer C ++ untuk menulis perpustakaan string mereka sendiri.
Juliet
31
Apa ini tentang mengharapkan penjelasan rasional sekarang. Saya kira Anda akan ingin mendengar alasan untuk x86 atau DOS selanjutnya? Sejauh yang saya ketahui, teknologi terburuk menang. Setiap saat. Dan representasi string terburuk.
Jalf
4
Mengapa Anda mengklaim string awalan panjang lebih unggul? Lagipula, C menjadi populer karena menggunakan string null-terminated, yang membedakannya dari bahasa lain.
Daniel C. Sobral
44
@Daniel: C menjadi populer karena itu adalah representasi sederhana, efisien, dan portabel dari program yang dapat dieksekusi pada mesin Von Neumann, dan karena itu digunakan untuk Unix. Jelas bukan karena itu memutuskan untuk menggunakan string yang diakhiri null. Jika itu adalah keputusan desain yang bagus, orang akan menyalinnya, dan mereka tidak melakukannya. Mereka tentu saja menyalin hampir semua yang lain dari C.
Billy ONeal
4
Concat hanya O (m) dengan awalan panjang jika Anda menghancurkan salah satu string. Kalau tidak, kecepatannya sama. Penggunaan C string yang paling umum (secara historis) adalah mencetak dan memindai. Dalam kedua hal ini, penghentian null lebih cepat karena menyimpan satu register.
Daniel C. Sobral

Jawaban:

195

Dari mulut kuda

Tidak ada BCPL, B, atau C yang mendukung data karakter dengan kuat dalam bahasa tersebut; masing-masing memperlakukan string seperti vektor bilangan bulat dan melengkapi aturan umum dengan beberapa konvensi. Dalam BCPL dan B, string literal menunjukkan alamat area statis yang diinisialisasi dengan karakter string, yang dikemas ke dalam sel. Dalam BCPL, byte yang dikemas pertama berisi jumlah karakter dalam string; di B, tidak ada hitungan dan string diakhiri oleh karakter khusus, yang B dieja *e. Perubahan ini dilakukan sebagian untuk menghindari pembatasan pada panjang string yang disebabkan oleh menahan hitungan dalam slot 8- atau 9-bit, dan sebagian karena mempertahankan jumlah tersebut, menurut pengalaman kami, kurang nyaman daripada menggunakan terminator.

Dennis M Ritchie, Pengembangan Bahasa C.

Hans Passant
sumber
12
Kutipan lain yang relevan: "... semantik string sepenuhnya dimasukkan oleh aturan yang lebih umum yang mengatur semua array, dan sebagai hasilnya bahasa lebih mudah untuk dijelaskan ..."
AShelly
151

C tidak memiliki string sebagai bagian dari bahasa. 'String' dalam C hanyalah sebuah pointer ke char. Jadi mungkin Anda mengajukan pertanyaan yang salah.

"Apa alasan untuk tidak menggunakan tipe string" mungkin lebih relevan. Untuk itu saya akan menunjukkan bahwa C bukan bahasa berorientasi objek dan hanya memiliki tipe nilai dasar. Sebuah string adalah konsep level yang lebih tinggi yang harus diimplementasikan dengan cara menggabungkan nilai dari tipe lain. C berada pada tingkat abstraksi yang lebih rendah.

mengingat badai mengamuk di bawah ini:

Saya hanya ingin menunjukkan bahwa saya tidak berusaha mengatakan ini adalah pertanyaan bodoh atau buruk, atau bahwa cara C mewakili string adalah pilihan terbaik. Saya mencoba mengklarifikasi bahwa pertanyaan akan lebih ringkas jika Anda memperhitungkan fakta bahwa C tidak memiliki mekanisme untuk membedakan string sebagai tipe data dari array byte. Apakah ini pilihan terbaik mengingat kekuatan pemrosesan dan memori komputer saat ini? Mungkin tidak. Tapi kalau dipikir-pikir selalu 20/20 dan semua itu :)

Robert S Ciaccio
sumber
29
char *temp = "foo bar";adalah pernyataan yang valid dalam bahasa C ... hei! bukankah itu sebuah string? bukankah itu nol diakhiri?
Yanick Rochon
56
@Yanick: itu hanya cara yang mudah untuk memberitahu kompiler untuk membuat array char dengan nol di akhir. itu bukan 'string'
Robert S Ciaccio
28
@calavera: Tapi itu bisa saja berarti "Buat buffer memori dengan konten string ini dan awalan panjang dua byte",
Billy ONeal
14
@ Billy: yah karena 'string' sebenarnya hanya pointer ke char, yang setara dengan pointer ke byte, bagaimana Anda tahu bahwa buffer yang Anda hadapi benar-benar dimaksudkan untuk menjadi 'string'? Anda membutuhkan tipe baru selain char / byte * untuk menunjukkan ini. mungkin struct?
Robert S Ciaccio
27
Saya pikir @calavera benar, C tidak memiliki tipe data untuk string. Ok, Anda dapat mempertimbangkan array karakter seperti string, tetapi ini tidak berarti itu selalu string (untuk string saya maksud urutan karakter dengan makna yang pasti). File biner adalah array karakter, tetapi karakter itu tidak berarti apa-apa bagi manusia.
BlackBear
106

Pertanyaan diajukan sebagai hal Length Prefixed Strings (LPS)vs zero terminated strings (SZ), tetapi sebagian besar mengekspos manfaat string awalan panjang. Itu mungkin tampak luar biasa, tetapi jujur ​​saja kita juga harus mempertimbangkan kelemahan LPS dan kelebihan SZ.

Seperti yang saya pahami, pertanyaan itu bahkan dapat dipahami sebagai cara yang bias untuk bertanya "apa keuntungan dari Zero Terminated Strings?".

Keuntungan (saya melihat) dari Zero Terminated Strings:

  • sangat sederhana, tidak perlu memperkenalkan konsep baru dalam bahasa, array char / pointer dapat dilakukan.
  • bahasa inti hanya menyertakan gula sintaksis minimal untuk mengubah sesuatu antara tanda kutip ganda menjadi sekelompok karakter (benar-benar sekelompok byte). Dalam beberapa kasus dapat digunakan untuk menginisialisasi hal-hal yang sama sekali tidak berhubungan dengan teks. Misalnya format file gambar xpm adalah sumber C yang valid yang berisi data gambar yang disandikan sebagai string.
  • by the way, Anda dapat menempatkan nol dalam literal string compiler hanya akan juga menambah satu lagi di akhir literal: "this\0is\0valid\0C". Apakah ini sebuah string? atau empat senar? Atau banyak byte ...
  • implementasi datar, tidak ada tipuan tersembunyi, tidak ada bilangan bulat tersembunyi.
  • tidak ada alokasi memori tersembunyi yang terlibat (well, beberapa fungsi non-standar terkenal seperti strdup melakukan alokasi, tetapi itu sebagian besar merupakan sumber masalah).
  • tidak ada masalah khusus untuk perangkat keras kecil atau besar (bayangkan beban untuk mengelola panjang awalan 32 bit pada mikrokontroler 8 bit, atau pembatasan membatasi ukuran string menjadi kurang dari 256 byte, itu adalah masalah yang sebenarnya saya miliki dengan Turbo Pascal ribuan tahun yang lalu).
  • implementasi manipulasi string hanyalah beberapa fungsi perpustakaan yang sangat sederhana
  • efisien untuk penggunaan utama string: teks konstan dibaca berurutan dari awal yang diketahui (kebanyakan pesan ke pengguna).
  • nol penghentian bahkan tidak wajib, semua alat yang diperlukan untuk memanipulasi karakter seperti sekelompok byte tersedia. Saat melakukan inisialisasi array dalam C, Anda bahkan dapat menghindari terminator NUL. Cukup atur ukuran yang tepat. char a[3] = "foo";valid C (bukan C ++) dan tidak akan menempatkan nol akhir dalam a.
  • koheren dengan sudut pandang unix "semuanya adalah file", termasuk "file" yang tidak memiliki panjang intrinsik seperti stdin, stdout. Anda harus ingat bahwa primitif baca dan tulis terbuka diterapkan pada tingkat yang sangat rendah. Mereka bukan panggilan perpustakaan, tetapi panggilan sistem. Dan API yang sama digunakan untuk file biner atau teks. Primitif membaca file mendapatkan alamat penyangga dan ukuran dan mengembalikan ukuran baru. Dan Anda bisa menggunakan string sebagai buffer untuk menulis. Menggunakan jenis representasi string yang lain akan menyiratkan Anda tidak dapat dengan mudah menggunakan string literal sebagai buffer ke output, atau Anda harus membuatnya memiliki perilaku yang sangat aneh ketika melakukan casting char*. Yaitu untuk tidak mengembalikan alamat string, tetapi sebaliknya untuk mengembalikan data aktual.
  • sangat mudah untuk memanipulasi data teks yang dibaca dari file di tempat, tanpa salinan buffer yang tidak berguna, cukup masukkan angka nol di tempat yang tepat (well, tidak benar-benar dengan C modern karena string yang dikutip ganda adalah array array ar saat ini biasanya disimpan dalam data yang tidak dapat dimodifikasi segmen).
  • menambahkan beberapa nilai int dengan ukuran apa pun yang menyiratkan masalah pelurusan. Panjang awal harus disejajarkan, tetapi tidak ada alasan untuk melakukan itu untuk data karakter (dan sekali lagi, memaksa penyelarasan string akan menyiratkan masalah ketika memperlakukan mereka sebagai sekelompok byte).
  • panjang dikenal pada waktu kompilasi untuk string literal konstan (sizeof). Jadi mengapa ada orang yang ingin menyimpannya di memori dengan menambahkannya ke data aktual?
  • dengan cara yang dilakukan C seperti (hampir) orang lain, string dipandang sebagai array char. Karena panjang array tidak dikelola oleh C, maka panjang logis juga tidak dikelola untuk string. Satu-satunya hal yang mengejutkan adalah 0 item ditambahkan di akhir, tapi itu hanya pada tingkat bahasa inti saat mengetik string di antara tanda kutip ganda. Pengguna dapat dengan sempurna memanggil fungsi manipulasi string yang melewati panjang, atau bahkan menggunakan memo sederhana. SZ hanyalah sebuah fasilitas. Di sebagian besar bahasa lain, panjang larik dikelola, adalah logis bahwa sama untuk string.
  • di zaman modern ini, set karakter 1 byte tidak cukup dan Anda sering harus berurusan dengan string unicode yang dikodekan di mana jumlah karakter sangat berbeda dari jumlah byte. Ini menyiratkan bahwa pengguna mungkin akan menginginkan lebih dari "hanya ukuran", tetapi juga informasi lainnya. Menjaga panjang tidak menggunakan apa-apa (terutama tidak ada tempat alami untuk menyimpannya) mengenai informasi lain yang bermanfaat ini.

Yang mengatakan, tidak perlu mengeluh dalam kasus langka di mana string C standar memang tidak efisien. Lib tersedia. Jika saya mengikuti tren itu, saya harus mengeluh bahwa standar C tidak termasuk fungsi dukungan regex ... tapi benar-benar semua orang tahu itu bukan masalah karena ada perpustakaan yang tersedia untuk tujuan itu. Jadi ketika efisiensi manipulasi string diinginkan, mengapa tidak menggunakan perpustakaan seperti bstring ? Atau bahkan string C ++?

EDIT : Saya baru-baru melihat ke D string . Cukup menarik untuk melihat bahwa solusi yang dipilih bukanlah awalan ukuran, atau nol penghentian. Seperti dalam C, string literal yang dilampirkan dalam tanda kutip ganda hanya tulisan pendek untuk array char yang tidak dapat diubah, dan bahasa tersebut juga memiliki string kata kunci yang berarti (array char yang tidak dapat diubah).

Tapi array D jauh lebih kaya daripada array C. Dalam kasus panjang array statis diketahui pada saat run-time sehingga tidak perlu menyimpan panjangnya. Compiler memilikinya pada waktu kompilasi. Dalam kasus array dinamis, panjang tersedia tetapi dokumentasi D tidak menyatakan di mana disimpan. Sejauh yang kita ketahui, kompiler dapat memilih untuk menyimpannya dalam register, atau dalam variabel yang disimpan jauh dari data karakter.

Pada array char normal atau string non literal tidak ada nol akhir, maka programmer harus meletakkannya sendiri jika ia ingin memanggil beberapa fungsi C dari D. Dalam kasus string string literal tertentu, namun kompiler D masih menempatkan nol di akhir setiap string (untuk memungkinkan cast mudah ke string C untuk membuat lebih mudah memanggil fungsi C?), tetapi nol ini bukan bagian dari string (D tidak menghitungnya dalam ukuran string).

Satu-satunya hal yang agak mengecewakan saya adalah bahwa string seharusnya utf-8, tetapi panjang tampaknya masih mengembalikan sejumlah byte (setidaknya itu benar pada kompiler gdc saya) bahkan ketika menggunakan karakter multi-byte. Tidak jelas bagi saya apakah itu bug kompiler atau dengan sengaja. (OK, saya mungkin telah menemukan apa yang terjadi. Untuk mengatakan kepada D compiler sumber Anda menggunakan utf-8 Anda harus meletakkan beberapa tanda urutan byte bodoh di awal. Saya menulis bodoh karena saya tahu tidak editor melakukan itu, terutama untuk UTF- 8 yang seharusnya kompatibel dengan ASCII).

Kriss
sumber
7
... Lanjutan ... Beberapa poin Anda, saya pikir benar-benar salah, yaitu argumen "semuanya adalah file". File adalah akses berurutan, string C tidak. Awalan panjang juga bisa dilakukan dengan gula sintaksis minimal. Satu-satunya argumen yang masuk akal di sini adalah mencoba untuk mengelola awalan 32 bit pada perangkat keras kecil (yaitu 8 bit); Saya pikir itu bisa diselesaikan dengan mengatakan ukuran panjang ditentukan oleh implementasi. Bagaimanapun, itulah yang std::basic_stringdilakukannya.
Billy ONeal
3
@ Billy ONeal: benar-benar ada dua bagian berbeda dalam jawaban saya. Salah satunya adalah tentang apa yang merupakan bagian dari 'bahasa inti C', yang lain adalah tentang apa yang harus disampaikan oleh perpustakaan standar. Mengenai dukungan string, hanya ada satu item dari bahasa inti: arti dari kutipan ganda yang dilampirkan sekelompok byte. Saya tidak benar-benar lebih bahagia daripada Anda dengan perilaku C. Saya merasa ajaib menambahkan bahwa nol pada setiap akhir ganda tertutup banyak byte sudah cukup buruk. Saya lebih suka dan eksplisit \0di akhir ketika programmer menginginkannya daripada yang implisit. Panjang yang saling tergantung jauh lebih buruk.
Kriss
2
@ Billy ONeal: itu tidak benar, penggunaannya peduli tentang apa itu inti dan apa itu perpustakaan. Poin terbesarnya adalah ketika C digunakan untuk mengimplementasikan OS. Pada tingkat itu tidak ada perpustakaan yang tersedia. C juga sering digunakan dalam konteks tertanam atau untuk perangkat pemrograman di mana Anda sering memiliki jenis pembatasan yang sama. Dalam banyak kasus, Joes mungkin sebaiknya tidak menggunakan C sama sekali hari ini: "OK, Anda menginginkannya di konsol? Apakah Anda memiliki konsol? Tidak? Terlalu buruk ..."
kriss
5
@Illy "Yah, untuk 0,01% programmer C yang mengimplementasikan sistem operasi, oke." Pemrogram lain dapat melakukan kenaikan. C dibuat untuk menulis sistem operasi.
Daniel C. Sobral
5
Mengapa? Karena dikatakan itu adalah bahasa tujuan umum? Apakah itu mengatakan apa yang dilakukan orang-orang yang menulisnya ketika dibuat? Untuk apa tahun-tahun pertama kehidupannya? Jadi, apa yang dikatakan tidak setuju dengan saya? Ini adalah bahasa tujuan umum yang dibuat untuk menulis sistem operasi . Apakah itu membantahnya?
Daniel C. Sobral
61

Saya pikir, ini memiliki alasan historis dan menemukan ini di wikipedia :

Pada saat C (dan bahasa-bahasa itu berasal) dikembangkan, memori sangat terbatas, sehingga hanya menggunakan satu byte overhead untuk menyimpan panjang string itu menarik. Satu-satunya alternatif yang populer pada waktu itu, biasanya disebut "string Pascal" (meskipun juga digunakan oleh versi awal BASIC), menggunakan byte terkemuka untuk menyimpan panjang string. Ini memungkinkan string berisi NUL dan membuat mencari panjang hanya membutuhkan satu akses memori (O (1) (konstan) waktu). Tapi satu byte membatasi panjangnya menjadi 255. Batasan panjang ini jauh lebih ketat daripada masalah dengan string C, jadi string C pada umumnya menang.

khachik
sumber
2
@muntoo Hmm ... kompatibilitas?
khachik
19
@muntoo: Karena itu akan merusak jumlah monumental kode C dan C ++ yang ada.
Billy ONeal
10
@muntoo: Paradigma datang dan pergi, tetapi kode lama selamanya. Setiap versi C masa depan harus terus mendukung string yang diakhiri 0, jika tidak, kode warisan 30+ tahun harus ditulis ulang (yang tidak akan terjadi). Dan selama cara lama tersedia, itulah yang akan terus digunakan orang, karena itulah yang mereka kenal.
John Bode
8
@muntoo: Percayalah, kadang-kadang aku berharap bisa. Tapi saya masih lebih suka string 0-terminated daripada string Pascal.
John Bode
2
Bicara tentang warisan ... string C ++ sekarang diamanatkan untuk diakhiri NUL.
Jim Balter
32

Calavera adalah benar , tetapi sebagai orang tampaknya tidak mendapatkan titik, saya akan memberikan beberapa contoh kode.

Pertama, mari kita pertimbangkan apa itu C: bahasa sederhana, di mana semua kode memiliki terjemahan langsung ke dalam bahasa mesin. Semua tipe masuk ke register dan di stack, dan tidak memerlukan sistem operasi atau perpustakaan run-time yang besar untuk dijalankan, karena itu dimaksudkan untuk menulis hal-hal ini (tugas yang sangat cocok, mengingat ada bahkan tidak menjadi pesaing hingga hari ini).

Jika C memiliki stringtipe, suka intatau char, itu akan menjadi tipe yang tidak cocok dalam register atau di stack, dan akan membutuhkan alokasi memori (dengan semua infrastruktur pendukungnya) untuk ditangani dengan cara apa pun. Semua itu bertentangan dengan prinsip dasar C.

Jadi, string dalam C adalah:

char s*;

Jadi, mari kita asumsikan bahwa ini adalah awalan panjang. Mari kita menulis kode untuk menggabungkan dua string:

char* concat(char* s1, char* s2)
{
    /* What? What is the type of the length of the string? */
    int l1 = *(int*) s1;
    /* How much? How much must I skip? */
    char *s1s = s1 + sizeof(int);
    int l2 = *(int*) s2;
    char *s2s = s2 + sizeof(int);
    int l3 = l1 + l2;
    char *s3 = (char*) malloc(l3 + sizeof(int));
    char *s3s = s3 + sizeof(int);
    memcpy(s3s, s1s, l1);
    memcpy(s3s + l1, s2s, l2);
    *(int*) s3 = l3;
    return s3;
}

Alternatif lain akan menggunakan struct untuk mendefinisikan string:

struct {
  int len; /* cannot be left implementation-defined */
  char* buf;
}

Pada titik ini, semua manipulasi string akan membutuhkan dua alokasi yang harus dibuat, yang, dalam praktiknya, berarti Anda akan pergi melalui perpustakaan untuk melakukan penanganannya.

Lucunya ... struct seperti itu memang ada di C! Mereka hanya tidak digunakan untuk menampilkan pesan sehari-hari Anda ke penanganan pengguna.

Jadi, di sini adalah titik Calavera membuat: tidak ada tipe string di C . Untuk melakukan apa pun dengan itu, Anda harus mengambil pointer dan mendekode sebagai pointer ke dua jenis yang berbeda, dan kemudian menjadi sangat relevan dengan ukuran string, dan tidak bisa dibiarkan begitu saja sebagai "implementasi didefinisikan".

Sekarang, C dapat menangani memori dengan cara apapun, dan memfungsi - fungsi di perpustakaan (di <string.h>, bahkan!) Menyediakan semua alat yang Anda butuhkan untuk menangani memori sebagai sepasang penunjuk dan ukuran. Apa yang disebut "string" dalam C dibuat hanya untuk satu tujuan: menampilkan pesan dalam konteks penulisan sistem operasi yang ditujukan untuk terminal teks. Dan, untuk itu, penghentian nol sudah cukup.

Daniel C. Sobral
sumber
2
1. +1. 2. Jelas jika perilaku default bahasa akan dibuat menggunakan awalan panjang, akan ada hal-hal lain untuk membuatnya lebih mudah. Misalnya, semua pemain Anda di sana akan disembunyikan oleh panggilan ke strlendan teman-teman. Adapun masalah dengan "menyerahkannya ke implementasi", Anda bisa mengatakan bahwa awalan adalah apa pun yang shortada di kotak target. Maka semua casting Anda akan tetap bekerja. 3. Saya bisa membuat skenario yang dibuat sepanjang hari yang membuat satu atau sistem lainnya terlihat buruk.
Billy ONeal
5
@Illy Hal perpustakaan cukup benar, selain dari fakta bahwa C dirancang untuk penggunaan perpustakaan minimal atau tidak. Penggunaan prototipe, misalnya, tidak umum sejak awal. Mengatakan awalan secara shortefektif membatasi ukuran string, yang tampaknya merupakan satu hal yang tidak mereka sukai. Saya sendiri, setelah bekerja dengan string BASIC dan Pascal 8-bit, string COBOL ukuran tetap dan hal-hal serupa, menjadi penggemar berat string C ukuran tak terbatas dengan cepat. Saat ini, ukuran 32-bit akan menangani string praktis, tetapi menambahkan byte-byte tersebut sebelumnya bermasalah.
Daniel C. Sobral
1
@ Billy: Pertama, terima kasih Daniel ... Anda sepertinya mengerti apa yang saya maksud. Kedua, Billy, saya pikir Anda masih kehilangan poin yang sedang dibuat di sini. Saya sendiri tidak memperdebatkan pro dan kontra dari awalan tipe data string dengan panjangnya. Apa yang saya katakan, dan apa yang sangat ditekankan Daniel, adalah bahwa ada keputusan yang dibuat dalam implementasi C untuk tidak menangani argumen itu sama sekali . String tidak ada sejauh menyangkut bahasa dasar. Keputusan tentang bagaimana menangani string diserahkan kepada programmer ... dan null termination menjadi populer.
Robert S Ciaccio
1
+1 oleh saya. Satu hal lagi yang ingin saya tambahkan; struct ketika Anda mengusulkannya melewatkan langkah penting menuju stringtipe nyata : itu tidak menyadari karakter. Ini adalah array dari "char" ("char" dalam istilah mesin adalah karakter sebanyak "kata" adalah apa yang manusia sebut kata dalam sebuah kalimat). String karakter adalah konsep tingkat tinggi yang dapat diimplementasikan di atas array charjika Anda memperkenalkan gagasan pengkodean.
Frerich Raabe
2
@ DanielC.Sobral: Juga, struct yang Anda sebutkan tidak memerlukan dua alokasi. Baik menggunakannya seperti yang Anda miliki di stack (jadi hanya bufmembutuhkan alokasi), atau gunakan struct string {int len; char buf[]};dan alokasikan semuanya dengan satu alokasi sebagai anggota array yang fleksibel, dan bagikan sebagai: a string*. (Atau Diperdebatkan, struct string {int capacity; int len; char buf[]};untuk alasan kinerja yang jelas)
Mooing Duck
20

Tentunya untuk kinerja dan keamanan, Anda harus menjaga panjang string saat Anda bekerja dengannya daripada berulang kali melakukan strlenatau setara di atasnya. Namun, menyimpan panjang di lokasi tetap sebelum konten string adalah desain yang sangat buruk. Seperti yang Jörgen tunjukkan dalam komentar pada jawaban Sanjit, itu menghalangi memperlakukan ekor string sebagai string, yang misalnya membuat banyak operasi umum suka path_to_filenameatau filename_to_extensiontidak mungkin tanpa mengalokasikan memori baru (dan menimbulkan kemungkinan kegagalan dan penanganan kesalahan) . Dan tentu saja ada masalah yang tak seorang pun dapat menyetujui berapa byte bidang panjang string yang harus ditempati (banyak "string Pascal" yang buruk

Desain C membiarkan programmer memilih jika / di mana / bagaimana menyimpannya jauh lebih fleksibel dan kuat. Tetapi tentu saja programmer harus pintar. C menghukum kebodohan dengan program yang macet, berhenti, atau memberi root musuh Anda.

R .. GitHub BERHENTI MEMBANTU ICE
sumber
+1. Akan lebih baik untuk memiliki tempat standar untuk menyimpan panjang sehingga agar kita yang menginginkan sesuatu seperti awalan panjang tidak harus menulis banyak "kode lem" di mana-mana.
Billy ONeal
2
Tidak ada tempat standar yang mungkin relatif terhadap data string, tetapi Anda tentu saja dapat menggunakan variabel lokal yang terpisah (mengkomputasi ulang alih-alih meneruskannya ketika yang terakhir tidak nyaman dan yang pertama tidak terlalu boros) atau struktur dengan pointer ke string (dan bahkan lebih baik, bendera yang menunjukkan apakah struktur "memiliki" pointer untuk tujuan alokasi atau apakah itu referensi ke string yang dimiliki di tempat lain. Dan tentu saja Anda dapat menyertakan anggota array yang fleksibel dalam struktur untuk fleksibilitas untuk mengalokasikan tali dengan struktur ketika itu cocok untuk Anda
.. GitHub BERHENTI MEMBANTU ICE
13

Malas, mendaftar berhemat dan mudah dibawa mengingat nyali perakitan bahasa apa pun, terutama C yang merupakan satu langkah di atas perakitan (sehingga mewarisi banyak kode warisan perakitan). Anda akan setuju sebagai null char akan sia-sia di hari-hari ASCII, itu (dan mungkin sebaik char control EOF).

mari kita lihat dalam kode semu

function readString(string) // 1 parameter: 1 register or 1 stact entries
    pointer=addressOf(string) 
    while(string[pointer]!=CONTROL_CHAR) do
        read(string[pointer])
        increment pointer

total 1 penggunaan register

kasus 2

 function readString(length,string) // 2 parameters: 2 register used or 2 stack entries
     pointer=addressOf(string) 
     while(length>0) do 
         read(string[pointer])
         increment pointer
         decrement length

total 2 register digunakan

Itu mungkin tampak picik pada waktu itu, tetapi mengingat berhemat dalam kode dan register (yang PREMIUM pada waktu itu, waktu ketika Anda tahu, mereka menggunakan kartu punch). Dengan demikian menjadi lebih cepat (ketika kecepatan prosesor dapat dihitung dalam kHz), "Retasan" ini sangat bagus dan mudah dibawa ke prosesor yang tidak memiliki register dengan mudah.

Demi argumen saya akan menerapkan 2 operasi string umum

stringLength(string)
     pointer=addressOf(string)
     while(string[pointer]!=CONTROL_CHAR) do
         increment pointer
     return pointer-addressOf(string)

kompleksitas O (n) di mana dalam banyak kasus string PASCAL adalah O (1) karena panjang string dipra-pended ke struktur string (itu juga berarti bahwa operasi ini harus dilakukan pada tahap sebelumnya).

concatString(string1,string2)
     length1=stringLength(string1)
     length2=stringLength(string2)
     string3=allocate(string1+string2)
     pointer1=addressOf(string1)
     pointer3=addressOf(string3)
     while(string1[pointer1]!=CONTROL_CHAR) do
         string3[pointer3]=string1[pointer1]
         increment pointer3
         increment pointer1
     pointer2=addressOf(string2)
     while(string2[pointer2]!=CONTROL_CHAR) do
         string3[pointer3]=string2[pointer2]
         increment pointer3
         increment pointer1
     return string3

kompleksitas O (n) dan menambahkan panjang string tidak akan mengubah kompleksitas operasi, sementara saya akui itu akan memakan waktu 3 kali lebih sedikit.

Di sisi lain, jika Anda menggunakan string PASCAL Anda harus mendesain ulang API Anda untuk memperhitungkan panjang register dan bit-endianness, string PASCAL mendapatkan batasan 255 char (0xFF) yang terkenal karena panjangnya disimpan dalam 1 byte (8bits) ), dan jika Anda menginginkan string yang lebih panjang (16bits-> apa pun), Anda harus memperhitungkan arsitektur dalam satu lapisan kode Anda, yang pada umumnya berarti API string yang tidak kompatibel jika Anda menginginkan string yang lebih panjang.

Contoh:

Satu file ditulis dengan string api prepended Anda pada komputer 8 bit dan kemudian harus dibaca pada katakanlah komputer 32 bit, apa yang program malas menganggap bahwa 4bytes Anda adalah panjang string kemudian mengalokasikan banyak memori kemudian mencoba membaca banyak byte. Kasus lain adalah PPC 32 byte string membaca (little endian) ke x86 (big endian), tentu saja jika Anda tidak tahu bahwa satu ditulis oleh yang lain akan ada masalah. Panjang 1 byte (0x00000001) akan menjadi 16777216 (0x0100000) yaitu 16 MB untuk membaca string 1 byte. Tentu saja Anda akan mengatakan bahwa orang harus menyetujui satu standar tetapi bahkan 16bit unicode mendapat sedikit dan endianness besar.

Tentu saja C akan memiliki masalah juga tetapi, akan sangat sedikit dipengaruhi oleh masalah yang diangkat di sini.

dvhh
sumber
2
@deemoowoor: Concat: O(m+n)dengan string nullterm, O(n)khas di tempat lain. Panjang O(n)dengan string nullterm, di O(1)mana saja. Bergabunglah: O(n^2)dengan string nullterm, di O(n)mana pun. Ada beberapa kasus di mana string null yang diakhiri lebih efisien (yaitu hanya menambahkan satu ke case pointer), tetapi concat dan panjangnya adalah operasi yang paling umum (panjang setidaknya diperlukan untuk memformat, output file, tampilan konsol, dll) . Jika Anda men-cache panjang untuk mengamortisasi O(n)Anda , Anda hanya membuat poin saya bahwa panjang harus disimpan dengan string.
Billy ONeal
1
Saya setuju bahwa dalam kode hari ini jenis string ini tidak efisien dan rentan terhadap kesalahan, tetapi misalnya tampilan Konsol tidak benar-benar harus mengetahui panjang string untuk menampilkannya secara efisien, output file tidak benar-benar perlu tahu tentang string panjang (hanya mengalokasikan cluster saat bepergian), Dan pemformatan string saat ini dilakukan pada panjang string tetap di sebagian besar kasus. Pokoknya Anda harus menulis kode buruk jika Anda concat di C memiliki kompleksitas O (n ^ 2), saya cukup yakin saya dapat menulis satu dalam kompleksitas O (n)
dvhh
1
@dvhh: Saya tidak mengatakan n ^ 2 - saya katakan m + n - masih linear, tetapi Anda perlu mencari sampai akhir string asli untuk melakukan penggabungan, sedangkan dengan awalan panjang tidak ada pencarian diperlukan. (Ini benar-benar hanya konsekuensi lain dari panjang yang membutuhkan waktu linier)
Billy ONeal
1
@ Billy ONeal: dari rasa ingin tahu belaka saya melakukan grep pada proyek C saya saat ini (sekitar 50000 baris kode) untuk panggilan fungsi manipulasi string. strlen 101, strcpy dan varian (strncpy, strlcpy): 85 (Saya juga punya beberapa ratusan string literal yang digunakan untuk pesan, salinan tersirat), strcmp: 56, strcat: 13 (dan 6 adalah gabungan string dengan panjang nol untuk memanggil strncat) . Saya setuju awalan panjang akan mempercepat panggilan ke strlen, tetapi tidak untuk strcpy atau strcmp (mungkin jika strcmp API tidak menggunakan awalan umum). Hal yang paling menarik mengenai komentar di atas adalah strcat sangat jarang.
Kriss
1
@supercat: tidak juga, lihat beberapa implementasi. String pendek menggunakan buffer berbasis stack pendek (tanpa alokasi heap) dan hanya menggunakan heap ketika mereka menjadi lebih besar. Tapi jangan ragu untuk memberikan implementasi aktual dari ide Anda sebagai perpustakaan. Biasanya masalah muncul hanya ketika kita sampai ke detail, bukan dalam desain keseluruhan.
Kriss
9

Dalam banyak hal, C adalah primitif. Dan saya menyukainya.

Itu adalah langkah di atas bahasa rakitan, memberi Anda kinerja yang hampir sama dengan bahasa yang jauh lebih mudah untuk ditulis dan dipelihara.

Terminator nol sederhana dan tidak memerlukan dukungan khusus oleh bahasa.

Melihat ke belakang, sepertinya tidak nyaman. Tapi saya menggunakan bahasa assembly di tahun 80-an dan sepertinya sangat nyaman saat itu. Saya hanya berpikir perangkat lunak terus berkembang, dan platform dan alat terus-menerus semakin canggih.

Jonathan Wood
sumber
Saya tidak melihat apa yang lebih primitif tentang string nol yang diakhiri daripada yang lainnya. Pascal mendahului C dan menggunakan awalan panjang. Tentu saja, itu dibatasi hingga 256 karakter per string, tetapi hanya menggunakan bidang 16 bit akan memecahkan masalah di sebagian besar kasus.
Billy ONeal
Fakta bahwa itu membatasi jumlah karakter persis jenis masalah yang perlu Anda pikirkan ketika melakukan sesuatu seperti itu. Ya, Anda bisa membuatnya lebih lama, tetapi saat itu byte penting. Dan apakah bidang 16-bit akan cukup panjang untuk semua kasus? Ayo, Anda harus mengakui bahwa null-terminate secara konseptual primitif.
Jonathan Wood
10
Entah Anda membatasi panjang string atau Anda membatasi konten (tidak ada karakter nol), atau Anda menerima overhead tambahan dari hitungan 4 hingga 8 byte. Tidak ada makan siang gratis. Pada saat dimulainya, null string yang diakhiri masuk akal. Dalam assembly, saya terkadang menggunakan bit karakter paling atas untuk menandai akhir sebuah string, bahkan menghemat satu byte lagi!
Mark Ransom
Tepat, Mark: Tidak ada makan siang gratis. Itu selalu kompromi. Saat ini, kita tidak perlu melakukan kompromi yang sama. Tetapi saat itu, pendekatan ini tampak sama baiknya dengan yang lain.
Jonathan Wood
8

Dengan asumsi sejenak bahwa C mengimplementasikan string dengan cara Pascal, dengan mengawali panjangnya: apakah string 7 char adalah DATA TYPE yang sama dengan string 3-char? Jika jawabannya adalah ya, lalu kode seperti apa yang harus dihasilkan oleh kompiler ketika saya menetapkan yang pertama ke yang terakhir? Haruskah string dipotong, atau secara otomatis diubah ukurannya? Jika diubah ukurannya, haruskah operasi itu dilindungi oleh kunci untuk membuatnya aman? Sisi pendekatan C melangkah semua masalah ini, suka atau tidak :)

Cristian
sumber
2
Err .. tidak, tidak. Pendekatan C tidak memungkinkan menetapkan string 7 char panjang ke string 3 char panjang sama sekali.
Billy ONeal
@ Billy ONeal: mengapa tidak? Sejauh yang saya mengerti dalam hal ini, semua string adalah tipe data yang sama (char *), jadi panjangnya tidak masalah. Berbeda dengan Pascal. Tapi itu adalah keterbatasan Pascal, bukan masalah dengan string yang panjangnya diawali.
Oliver Mason
4
@ Billy: Saya pikir Anda baru saja menyatakan kembali poin Cristian. C menangani masalah-masalah ini dengan tidak menanganinya sama sekali. Anda masih berpikir dalam istilah C yang sebenarnya mengandung gagasan tentang string. Ini hanya sebuah pointer, sehingga Anda dapat menetapkannya untuk apa pun yang Anda inginkan.
Robert S Ciaccio
2
Ini seperti ** matriks: "tidak ada string".
Robert S Ciaccio
1
@ calvera: Saya tidak melihat bagaimana itu membuktikan apa pun. Anda dapat menyelesaikannya dengan cara yang sama dengan awalan panjang ... yaitu tidak mengizinkan tugas sama sekali.
Billy ONeal
8

Entah bagaimana saya memahami pertanyaan untuk menyiratkan tidak ada dukungan kompiler untuk string awalan panjang di C. Contoh berikut menunjukkan, setidaknya Anda dapat memulai perpustakaan string C Anda sendiri, di mana panjang string dihitung pada waktu kompilasi, dengan konstruksi seperti ini:

#define PREFIX_STR(s) ((prefix_str_t){ sizeof(s)-1, (s) })

typedef struct { int n; char * p; } prefix_str_t;

int main() {
    prefix_str_t string1, string2;

    string1 = PREFIX_STR("Hello!");
    string2 = PREFIX_STR("Allows \0 chars (even if printf directly doesn't)");

    printf("%d %s\n", string1.n, string1.p); /* prints: "6 Hello!" */
    printf("%d %s\n", string2.n, string2.p); /* prints: "48 Allows " */

    return 0;
}

Ini tidak akan, bagaimanapun, datang tanpa masalah karena Anda harus berhati-hati ketika secara khusus membebaskan pointer string itu dan ketika itu dialokasikan secara statis ( chararray literal ).

Sunting: Sebagai jawaban yang lebih langsung untuk pertanyaan, pandangan saya adalah ini adalah cara C dapat mendukung keduanya memiliki panjang string yang tersedia (sebagai konstanta waktu kompilasi), jika Anda memerlukannya, tetapi masih tanpa overhead memori jika Anda ingin menggunakan hanya pointer dan terminasi nol.

Tentu saja sepertinya bekerja dengan string tanpa-penghentian nol adalah praktik yang disarankan, karena pustaka standar secara umum tidak menggunakan panjang string sebagai argumen, dan karena mengekstraksi panjangnya tidak semudah kode sederhana char * s = "abc", seperti yang ditunjukkan oleh contoh saya.

Pyry Jahkola
sumber
Masalahnya adalah bahwa perpustakaan tidak mengetahui keberadaan struct Anda, dan masih menangani hal-hal seperti nulls yang disematkan secara salah. Juga, ini tidak benar-benar menjawab pertanyaan yang saya tanyakan.
Billy ONeal
1
Itu benar. Jadi masalah yang lebih besar adalah tidak ada cara standar yang lebih baik untuk menyediakan antarmuka dengan parameter string daripada string putus-putus biasa. Saya masih akan mengklaim, ada perpustakaan yang mendukung pengumpanan dalam pasangan panjang-pointer (well, setidaknya Anda dapat membangun string C ++ std :: dengan mereka).
Pyry Jahkola
2
Bahkan jika Anda menyimpan panjang, Anda tidak boleh membiarkan string dengan null tertanam. Ini adalah akal sehat dasar. Jika data Anda mungkin memiliki null di dalamnya, Anda tidak boleh menggunakannya dengan fungsi yang mengharapkan string.
R .. GitHub BERHENTI MEMBANTU ICE
1
@ supercat: Dari sudut pandang keamanan saya akan menyambut redundansi itu. Jika tidak bodoh (atau kurang tidur) programmer berakhir concatenating data biner dan string dan melewati mereka ke hal-hal yang mengharapkan [diakhiri null-] string ...
R .. GitHub BERHENTI MEMBANTU ICE
1
@ R ..: Walaupun metode yang mengharapkan string diakhiri-nol umumnya mengharapkan a char*, banyak metode yang tidak mengharapkan pengakhiran null juga mengharapkan a char*. Manfaat yang lebih signifikan dari pemisahan jenis akan berhubungan dengan perilaku Unicode. Mungkin bermanfaat bagi implementasi string untuk memelihara flag-flag untuk apakah string diketahui mengandung jenis karakter tertentu, atau diketahui tidak mengandung mereka [misalnya menemukan titik kode 999.990 dalam string jutaan karakter yang diketahui tidak mengandung setiap karakter di luar bidang multibahasa dasar akan menjadi perintah yang lebih cepat ...
supercat
6

"Bahkan pada mesin 32 bit, jika Anda membiarkan string menjadi ukuran memori yang tersedia, string awalan panjang hanya tiga byte lebih lebar dari string yang diakhiri null."

Pertama, tambahan 3 byte mungkin merupakan overhead yang cukup untuk string pendek. Secara khusus, string dengan panjang nol sekarang membutuhkan 4 kali lebih banyak memori. Beberapa dari kita menggunakan mesin 64-bit, jadi kita perlu 8 byte untuk menyimpan string panjang nol, atau format string tidak dapat mengatasi string terpanjang yang didukung platform.

Mungkin juga ada masalah keberpihakan yang harus dihadapi. Misalkan saya memiliki blok memori yang berisi 7 string, seperti "solo \ 0second \ 0 \ 0four \ 0five \ 0five \ 0 \ 0seventh". String kedua dimulai pada offset 5. Perangkat keras mungkin mengharuskan bilangan bulat 32-bit diluruskan pada alamat yang merupakan kelipatan dari 4, jadi Anda harus menambahkan bantalan, menambah biaya overhead lebih jauh. Representasi C sangat hemat memori dibandingkan. (Efisiensi memori baik; itu membantu kinerja cache, misalnya.)

Brangdon
sumber
Saya yakin saya membahas semua ini dalam pertanyaan. Ya, pada platform x64 awalan 32 bit tidak dapat memuat semua string yang mungkin. Di sisi lain, Anda tidak pernah menginginkan string sebesar string diakhiri null, karena untuk melakukan apa pun Anda harus memeriksa semua 4 miliar byte untuk menemukan akhir untuk hampir setiap operasi yang ingin Anda lakukan untuk itu. Juga, saya tidak mengatakan bahwa string nol yang diakhiri selalu jahat - jika Anda membangun salah satu dari struktur blok ini dan aplikasi spesifik Anda dipercepat oleh konstruksi semacam itu, lakukanlah. Saya hanya berharap perilaku default bahasa tidak melakukan itu.
Billy ONeal
2
Saya mengutip bagian dari pertanyaan Anda karena menurut saya itu meremehkan masalah efisiensi. Penggandaan atau kebutuhan memori empat kali lipat (masing-masing pada 16-bit dan 32-bit) dapat menjadi biaya kinerja yang besar. Tali panjang mungkin lambat, tetapi setidaknya mereka didukung dan masih berfungsi. Poin saya yang lain, tentang perataan, Anda tidak menyebutkan sama sekali.
Brangdon
Penyelarasan dapat ditangani dengan menetapkan bahwa nilai di luar UCHAR_MAX harus berperilaku seolah-olah dikemas dan dibongkar menggunakan akses byte dan bit-shifting. Tipe string yang dirancang dengan tepat dapat menawarkan efisiensi penyimpanan yang pada dasarnya sebanding dengan string tanpa-penghentian, sementara juga memungkinkan pemeriksaan batas pada buffer tanpa overhead memori tambahan (gunakan satu bit pada awalan untuk mengatakan apakah buffer "penuh"; jika itu tidak dan byte terakhir adalah bukan nol, byte itu akan mewakili ruang yang tersisa. Jika buffer tidak penuh dan byte terakhir adalah nol, maka 256 byte terakhir akan tidak digunakan, jadi ...
supercat
... seseorang dapat menyimpan dalam ruang itu jumlah persis byte yang tidak digunakan, dengan nol biaya memori tambahan). Biaya bekerja dengan awalan akan diimbangi dengan kemampuan untuk menggunakan metode seperti fgets () tanpa harus melewati panjang string (karena buffer akan tahu seberapa besar mereka).
supercat
4

Pengakhiran nol memungkinkan untuk operasi berbasis penunjuk cepat.

Sanjit Saluja
sumber
5
Hah? "Operasi penunjuk cepat" apa yang tidak berfungsi dengan awalan panjang? Lebih penting lagi, bahasa lain yang menggunakan awalan panjang lebih cepat dari manipulasi string Ctrt.
Billy ONeal
12
@ billy: Dengan panjang awalan string, Anda tidak bisa hanya mengambil penunjuk string dan menambahkan 4 untuk itu, dan berharap itu masih menjadi string yang valid, karena tidak memiliki awalan panjang (bukan pula yang valid).
Jörgen Sigvardsson
3
@j_random_hacker: Penggabungan jauh lebih buruk untuk string asciiz (O (m + n) daripada berpotensi O (n)), dan concat jauh lebih umum daripada operasi lain yang tercantum di sini.
Billy ONeal
3
ada satu operasi kecil tiiny yang menjadi lebih mahal dengan null-string dihentikan: strlen. Saya akan mengatakan itu sedikit kelemahan.
jalf
10
@Billy ONeal: semua orang juga mendukung regex. Terus ? Gunakan perpustakaan untuk itulah mereka dibuat. C adalah tentang efisiensi maksimal dan minimalis, tidak termasuk baterai. Alat C juga memungkinkan Anda untuk mengimplementasikan string Prefixed Panjang menggunakan struct dengan sangat mudah. Dan tidak ada yang melarang Anda untuk mengimplementasikan program manipulasi string dengan mengelola panjang dan karakter Anda sendiri. Itu biasanya yang saya lakukan ketika saya ingin efisiensi dan menggunakan C, tidak memanggil beberapa fungsi yang mengharapkan nol pada akhir buffer char bukan masalah.
kriss
4

Satu hal yang belum disebutkan: ketika C dirancang, ada banyak mesin di mana 'char' tidak delapan bit (bahkan saat ini ada platform DSP di tempat yang tidak). Jika seseorang memutuskan bahwa string harus awalan panjang, berapa awalan panjang nilai char 'harus digunakan? Menggunakan dua akan memaksakan batas buatan pada panjang string untuk mesin dengan 8-bit char dan 32-bit addressing space, sementara membuang ruang pada mesin dengan 16-bit char dan 16-bit addressing space.

Jika seseorang ingin membiarkan string panjang sewenang-wenang disimpan secara efisien, dan jika 'char' selalu 8-bit, seseorang dapat - untuk beberapa biaya dalam kecepatan dan ukuran kode - mendefinisikan skema adalah string yang diawali oleh angka genap N akan menjadi N / 2 byte panjang, sebuah string yang diawali dengan nilai ganjil N dan nilai genap M (membaca mundur) bisa menjadi ((N-1) + M * char_max) / 2, dll. Dan mensyaratkan bahwa setiap buffer yang klaim untuk menawarkan sejumlah ruang tertentu untuk menampung string harus memungkinkan byte yang cukup sebelum ruang itu untuk menangani panjang maksimum. Fakta bahwa 'char' tidak selalu 8 bit, bagaimanapun, akan menyulitkan skema seperti itu, karena jumlah 'char' yang dibutuhkan untuk memegang panjang string akan bervariasi tergantung pada arsitektur CPU.

supercat
sumber
Awalan bisa dengan mudah dari ukuran yang ditentukan implementasi, seperti apa adanya sizeof(char).
Billy ONeal
@Illyillyeal: sizeof(char)adalah satu. Selalu. Satu bisa memiliki awalan menjadi ukuran yang ditentukan implementasi, tetapi akan canggung. Lebih jauh lagi, tidak ada cara nyata untuk mengetahui ukuran "tepat" seharusnya. Jika seseorang memegang banyak string 4-karakter, zero-padding akan membebankan 25% overhead, sedangkan awalan panjang empat byte akan memberlakukan 100% overhead. Lebih lanjut, waktu yang dihabiskan untuk mengemas dan membongkar prefiks panjang empat byte dapat melebihi biaya pemindaian string 4-byte untuk byte nol.
supercat
1
Ah iya. Kamu benar. Awalan bisa dengan mudah menjadi sesuatu selain char. Apa pun yang akan membuat persyaratan penyelarasan pada platform target berhasil akan baik-baik saja. Saya tidak akan pergi ke sana - saya sudah berpendapat ini mati.
Billy ONeal
Dengan asumsi string adalah awalan panjang, mungkin hal yang paling baik untuk dilakukan adalah size_tawalan (pemborosan memori terkutuk, itu akan menjadi sanest --- memungkinkan string dengan panjang berapa pun panjang yang mungkin bisa masuk ke dalam memori). Bahkan, itu semacam apa D tidak; array adalah struct { size_t length; T* ptr; }, dan string hanyalah array immutable(char).
Tim Čas
@ TimČas: Kecuali string harus disejajarkan dengan kata, biaya bekerja dengan string pendek akan di banyak platform didominasi oleh persyaratan untuk mengemas dan membongkar panjang; Saya benar-benar tidak menganggapnya praktis. Jika seseorang ingin string menjadi content-agnostic byte array berukuran sewenang-wenang, saya pikir akan lebih baik untuk menjaga panjangnya terpisah dari pointer ke data karakter, dan memiliki bahasa yang memungkinkan kedua bagian informasi diperoleh untuk string literal .
supercat
2

Banyak keputusan desain seputar C berasal dari fakta bahwa ketika awalnya diimplementasikan, melewati parameter agak mahal. Diberi pilihan antara misalnya

void add_element_to_next(arr, offset)
  char[] arr;
  int offset;
{
  arr[offset] += arr[offset+1];
}

char array[40];

void test()
{
  for (i=0; i<39; i++)
    add_element_to_next(array, i);
}

melawan

void add_element_to_next(ptr)
  char *p;
{
  p[0]+=p[1];
}

char array[40];

void test()
{
  int i;
  for (i=0; i<39; i++)
    add_element_to_next(arr+i);
}

yang terakhir akan sedikit lebih murah (dan karena itu lebih disukai) karena hanya diperlukan melewati satu parameter daripada dua. Jika metode yang dipanggil tidak perlu mengetahui alamat basis dari array atau indeks di dalamnya, melewati satu pointer yang menggabungkan keduanya akan lebih murah daripada melewati nilai-nilai secara terpisah.

Meskipun ada banyak cara yang masuk akal di mana C dapat menyandikan panjang string, pendekatan yang telah ditemukan hingga saat itu akan memiliki semua fungsi yang diperlukan yang harus dapat bekerja dengan bagian dari string untuk menerima alamat basis string dan indeks yang diinginkan sebagai dua parameter terpisah. Menggunakan terminasi nol byte memungkinkan untuk menghindari persyaratan itu. Meskipun pendekatan lain akan lebih baik dengan mesin saat ini (kompiler modern sering melewati parameter dalam register, dan memcpy dapat dioptimalkan dengan cara strcpy () - yang setara tidak dapat) kode produksi yang cukup menggunakan string terminasi nol-byte sehingga sulit untuk mengubah ke yang lain.

PS - Sebagai imbalan atas penalti kecepatan sedikit pada beberapa operasi, dan sedikit overhead tambahan pada string yang lebih panjang, akan mungkin untuk memiliki metode yang bekerja dengan string menerima pointer langsung ke string, buffer string yang diperiksa batas , atau struktur data yang mengidentifikasi substring dari string lain. Fungsi seperti "strcat" akan terlihat seperti [sintaks modern]

void strcat(unsigned char *dest, unsigned char *src)
{
  struct STRING_INFO d,s;
  str_size_t copy_length;

  get_string_info(&d, dest);
  get_string_info(&s, src);
  if (d.si_buff_size > d.si_length) // Destination is resizable buffer
  {
    copy_length = d.si_buff_size - d.si_length;
    if (s.src_length < copy_length)
      copy_length = s.src_length;
    memcpy(d.buff + d.si_length, s.buff, copy_length);
    d.si_length += copy_length;
    update_string_length(&d);
  }
}

Sedikit lebih besar dari metode strcat K&R, tetapi ini akan mendukung pengecekan batas, yang mana metode K&R tidak. Lebih jauh, tidak seperti metode saat ini, akan mungkin untuk dengan mudah menggabungkan substring sewenang-wenang, misalnya

/* Concatenate 10th through 24th characters from src to dest */

void catpart(unsigned char *dest, unsigned char *src)
{
  struct SUBSTRING_INFO *inf;
  src = temp_substring(&inf, src, 10, 24);
  strcat(dest, src);
}

Perhatikan bahwa masa pakai string yang dikembalikan oleh temp_substring akan dibatasi oleh orang-orang dari sdan src, yang pernah lebih pendek (itulah sebabnya metode ini infharus diteruskan - jika itu lokal, itu akan mati ketika metode kembali).

Dalam hal biaya memori, string dan buffer hingga 64 byte akan memiliki satu byte overhead (sama dengan string yang diakhiri nol); string yang lebih panjang akan memiliki sedikit lebih banyak (apakah satu diperbolehkan jumlah overhead antara dua byte dan maksimum yang diperlukan akan menjadi tradeoff waktu / ruang). Nilai khusus dari byte panjang / mode akan digunakan untuk menunjukkan bahwa fungsi string diberi struktur yang mengandung byte bendera, pointer, dan panjang buffer (yang kemudian dapat mengindeks secara sewenang-wenang ke string lain).

Tentu saja, K&R tidak menerapkan hal seperti itu, tetapi itu kemungkinan besar karena mereka tidak ingin menghabiskan banyak upaya untuk penanganan string - suatu daerah di mana bahkan hari ini banyak bahasa tampak agak anemia.

supercat
sumber
Tidak ada yang mencegah char* arrdari menunjuk ke struktur formulir struct { int length; char characters[ANYSIZE_ARRAY] };atau serupa yang masih bisa dilewati sebagai parameter tunggal.
Billy ONeal
@ BillyONeal: Dua masalah dengan pendekatan itu: (1) Itu hanya akan memungkinkan melewati string secara keseluruhan, sedangkan pendekatan yang sekarang juga memungkinkan melewati ekor string; (2) itu akan membuang-buang ruang yang signifikan ketika digunakan dengan string kecil. Jika K&R ingin meluangkan waktu untuk membuat string, mereka bisa membuat banyak hal menjadi lebih kuat, tetapi saya tidak berpikir mereka bermaksud bahwa bahasa baru mereka akan digunakan sepuluh tahun kemudian, apalagi empat puluh.
supercat
1
Ini sedikit tentang konvensi pemanggilan adalah cerita biasa-biasa saja tanpa ada kaitannya dengan kenyataan ... itu bukan pertimbangan dalam desain. Dan konvensi panggilan berbasis register sudah "diciptakan". Juga, pendekatan seperti dua pointer bukan pilihan karena struct bukan kelas pertama ... hanya primitif yang ditugaskan atau dapat dilewati; penyalinan struct tidak tiba sampai UNIX V7. Membutuhkan memcpy (yang juga tidak ada) hanya untuk menyalin pointer string adalah lelucon. Cobalah menulis program lengkap, bukan hanya fungsi terisolasi, jika Anda membuat kepura-puraan desain bahasa.
Jim Balter
1
"Itu kemungkinan besar karena mereka tidak ingin menghabiskan banyak usaha pada penanganan string" - omong kosong; seluruh domain aplikasi UNIX awal adalah penanganan string. Jika bukan karena itu, kita tidak akan pernah mendengarnya.
Jim Balter
1
'Saya tidak berpikir "buffer arang dimulai dengan sebuah int yang berisi panjangnya' lebih ajaib '- itu jika Anda akan str[n]merujuk pada char yang tepat. Ini adalah hal-hal yang tidak dipikirkan orang-orang yang mendiskusikan hal ini .
Jim Balter
2

Menurut Joel Spolsky dalam posting blog ini ,

Itu karena mikroprosesor PDP-7, di mana UNIX dan bahasa pemrograman C diciptakan, memiliki tipe string ASCIZ. ASCIZ berarti "ASCII dengan Z (nol) di akhir."

Setelah melihat semua jawaban lain di sini, saya yakin bahwa bahkan jika ini benar, itu hanya bagian dari alasan C memiliki "string" yang diakhiri dengan null. Posting itu cukup menjelaskan bagaimana hal-hal sederhana seperti string sebenarnya bisa sangat sulit.

Benk
sumber
2
Dengar, aku menghormati Joel untuk banyak hal; tapi ini adalah sesuatu yang dia berspekulasi. Jawaban Hans Passant datang langsung dari para penemu C.
Billy ONeal
1
Ya, tetapi jika apa yang Spolsky katakan itu benar, maka itu akan menjadi bagian dari "kenyamanan" yang mereka maksudkan. Itu sebabnya saya memasukkan jawaban ini.
BenK
AFAIK .ASCIZhanyalah pernyataan assembler untuk membangun urutan byte, diikuti oleh 0. Ini hanya berarti bahwa nol string yang dihentikan adalah konsep mapan pada waktu itu. Ini tidak berarti bahwa string yang diakhiri nol adalah sesuatu yang terkait dengan arsitektur PDP- *, kecuali bahwa Anda dapat menulis loop ketat yang terdiri dari MOVB(salin satu byte) dan BNE(cabang jika byte terakhir yang disalin bukan nol).
Adrian W
Seharusnya menunjukkan bahwa C adalah bahasa tua, lembek, jompo.
purec
2

Bukan Rasional tentu tapi tandingan panjang-disandikan

  1. Bentuk-bentuk tertentu dari pengkodean panjang dinamis lebih unggul daripada pengkodean panjang statis sejauh menyangkut memori, semuanya tergantung pada penggunaan. Lihat saja UTF-8 sebagai bukti. Ini pada dasarnya adalah array karakter yang dapat diperluas untuk mengkodekan satu karakter. Ini menggunakan bit tunggal untuk setiap byte yang diperluas. Pengakhiran NUL menggunakan 8 bit. Panjang-awalan Saya pikir bisa disebut panjang tak terbatas juga dengan menggunakan 64 bit. Seberapa sering Anda menekan kasus bit ekstra Anda adalah faktor penentu. Hanya 1 string yang sangat besar? Siapa yang peduli jika Anda menggunakan 8 atau 64 bit? Banyak string kecil (Yaitu String kata-kata bahasa Inggris)? Maka biaya awalan Anda adalah persentase yang besar.

  2. String dengan awalan panjang yang memungkinkan penghematan waktu bukanlah hal yang nyata . Apakah data Anda yang disediakan harus memiliki panjang yang disediakan, Anda menghitung pada waktu kompilasi, atau Anda benar-benar diberikan data dinamis yang harus Anda encode sebagai string. Ukuran ini dihitung pada beberapa titik dalam algoritma. Variabel terpisah untuk menyimpan ukuran string yang dihentikan nol dapat disediakan. Yang membuat perbandingan pada penghematan waktu diperdebatkan. Satu hanya memiliki NUL ekstra di akhir ... tetapi jika panjang encode tidak termasuk NUL itu maka secara harfiah tidak ada perbedaan antara keduanya. Tidak ada perubahan algoritmik yang diperlukan sama sekali. Hanya sebuah pre-pass Anda harus mendesain sendiri secara manual alih-alih membuat kompiler / runtime melakukannya untuk Anda. C sebagian besar tentang melakukan sesuatu secara manual.

  3. Panjang-awalan menjadi opsional adalah nilai jual. Saya tidak selalu membutuhkan info tambahan untuk suatu algoritma sehingga diminta untuk melakukannya untuk setiap string membuat waktu komputasi + komputasi saya tidak pernah bisa turun di bawah O (n). (Yaitu hardware nomor acak generator 1-128. Saya dapat menarik dari "string tak terbatas". Katakan saja hanya menghasilkan karakter begitu cepat. Jadi panjang string kami berubah sepanjang waktu. Tetapi penggunaan data saya mungkin tidak peduli seberapa banyak byte acak yang saya miliki. Itu hanya ingin byte yang tidak terpakai berikutnya tersedia segera setelah itu bisa mendapatkannya setelah permintaan. Saya bisa menunggu di perangkat. Tapi saya juga bisa memiliki buffer karakter pra-baca. Perbandingan panjang adalah pemborosan perhitungan yang tidak perlu. Pemeriksaan nol lebih efisien.)

  4. Panjang-awalan adalah pelindung yang baik terhadap buffer overflow? Begitu juga penggunaan fungsi dan implementasi perpustakaan secara waras. Bagaimana jika saya meneruskan data yang cacat? Buffer saya panjangnya 2 byte tapi saya bilang fungsinya 7! Mis: Jika get () dimaksudkan untuk digunakan pada data yang diketahui itu bisa saja memiliki pemeriksaan buffer internal yang menguji buffer yang terkompilasi dan malloc ()panggilan dan masih mengikuti spesifikasi. Jika itu dimaksudkan untuk digunakan sebagai pipa untuk STDIN yang tidak diketahui untuk sampai pada buffer yang tidak diketahui maka jelas seseorang tidak dapat mengetahui tentang ukuran buffer yang berarti panjang arg tidak ada gunanya, Anda perlu sesuatu yang lain di sini seperti cek kenari. Dalam hal ini, Anda tidak dapat awalan panjang beberapa aliran dan input, Anda hanya tidak bisa. Yang berarti pemeriksaan panjang harus dibangun ke dalam algoritma dan bukan bagian ajaib dari sistem pengetikan. TL; DR NUL yang diputus tidak pernah harus tidak aman, itu hanya berakhir seperti itu melalui penyalahgunaan.

  5. counter-counter point: NUL-termination menjengkelkan pada biner. Anda juga perlu melakukan awalan panjang di sini atau mengubah byte NUL dengan beberapa cara: kode-lepas, range remapping, dll ... yang tentu saja berarti lebih banyak penggunaan memori / pengurangan-informasi / lebih banyak operasi-per-byte. Panjang-awalan sebagian besar memenangkan perang di sini. Satu-satunya terbalik untuk transformasi adalah bahwa tidak ada fungsi tambahan harus ditulis untuk menutupi string awalan panjang. Yang berarti pada rutinitas sub-O (n) yang lebih dioptimalkan, Anda dapat membuatnya secara otomatis bertindak sebagai padanan O (n) tanpa menambahkan kode lebih banyak. Kelemahannya, tentu saja, waktu / memori / limbah kompresi bila digunakan pada string NUL yang berat.Bergantung pada seberapa banyak perpustakaan Anda yang akhirnya Anda duplikasi untuk beroperasi pada data biner, mungkin masuk akal untuk bekerja hanya dengan string awalan panjang. Yang mengatakan orang juga bisa melakukan hal yang sama dengan string awalan panjang ... -1 panjang bisa berarti NUL-dihentikan dan Anda dapat menggunakan string NUL-dihentikan di dalam panjang-dihentikan.

  6. Concat: "O (n + m) vs O (m)" Saya menganggap Anda merujuk ke m sebagai total panjang string setelah digabungkan karena mereka berdua harus memiliki jumlah operasi minimum (Anda tidak bisa hanya menangani -pada string 1, bagaimana jika Anda harus realokasi?). Dan saya berasumsi n adalah jumlah operasi mitos yang tidak perlu Anda lakukan lagi karena pre-compute. Jika demikian, maka jawabannya sederhana: pra-hitung. JikaAnda bersikeras Anda akan selalu memiliki cukup memori untuk tidak perlu realokasi dan itulah dasar dari notasi O-besar maka jawabannya bahkan lebih sederhana: melakukan pencarian biner pada memori yang dialokasikan untuk akhir string 1, jelas ada yang besar carikan nol tanpa batas setelah string 1 agar kita tidak khawatir tentang realokasi. Di sana, dengan mudah mendapat n untuk log (n) dan saya nyaris tidak mencoba. Yang jika Anda ingat log (n) pada dasarnya hanya sebesar 64 pada komputer nyata, yang pada dasarnya seperti mengatakan O (64 + m), yang pada dasarnya adalah O (m). (Dan ya, logika itu telah digunakan dalam analisis run-time dari struktur data nyata yang digunakan hari ini. Ini bukan omong kosong dari atas kepala saya.)

  7. Concat () / Len () lagi : Memoize results. Mudah. Mengubah semua perhitungan menjadi pra-perhitungan jika memungkinkan / perlu. Ini adalah keputusan algoritmik. Ini bukan kendala bahasa yang dipaksakan.

  8. Pengambilan string suffix lebih mudah / mungkin dengan terminasi NUL. Tergantung pada bagaimana awalan panjang diimplementasikan itu dapat merusak pada string asli dan kadang-kadang bahkan tidak mungkin Membutuhkan salinan dan lulus O (n) bukan O (1).

  9. Argumen-passing / de-referencing kurang untuk awalan NUL versus panjang-awalan. Jelas karena Anda memberikan informasi yang lebih sedikit. Jika Anda tidak membutuhkan panjang, maka ini menghemat banyak jejak dan memungkinkan pengoptimalan.

  10. Anda bisa curang. Benar-benar hanya sebuah pointer. Siapa bilang Anda harus membacanya sebagai string? Bagaimana jika Anda ingin membacanya sebagai karakter tunggal atau float? Bagaimana jika Anda ingin melakukan yang sebaliknya dan membaca pelampung sebagai string? Jika Anda berhati-hati, Anda dapat melakukan ini dengan penghentian NUL. Anda tidak dapat melakukan ini dengan awalan panjang, ini adalah tipe data yang berbeda dari pointer biasanya. Anda kemungkinan besar harus membangun string byte-by-byte dan mendapatkan panjangnya. Tentu saja jika Anda menginginkan sesuatu seperti seluruh float (mungkin memiliki NUL di dalamnya) Anda harus membaca byte-by-byte, tetapi rinciannya diserahkan kepada Anda untuk memutuskan.

TL; DR Apakah Anda menggunakan data biner? Jika tidak, maka pemutusan NUL memungkinkan lebih banyak kebebasan algoritmik. Jika ya, maka kuantitas kode vs kecepatan / memori / kompresi adalah perhatian utama Anda. Perpaduan dari dua pendekatan atau memoisasi mungkin yang terbaik.

Hitam
sumber
9 agak off-base / salah representasi. Pre-fix panjang tidak memiliki masalah ini. Lenth passing sebagai variabel terpisah. Kami berbicara tentang pre-fiix tapi saya terbawa suasana. Masih hal yang baik untuk dipikirkan jadi saya akan meninggalkannya di sana. : d
Black
1

Saya tidak membeli jawaban "C tidak punya string". Benar, C tidak mendukung tipe tingkat tinggi bawaan tetapi Anda masih bisa mewakili struktur data di C dan itulah string. Fakta bahwa sebuah string hanyalah sebuah penunjuk dalam C tidak berarti bahwa N byte pertama tidak dapat memiliki arti khusus sebagai panjangnya.

Pengembang Windows / COM akan sangat terbiasa dengan BSTRtipe yang persis seperti ini - string C yang diawali dengan panjang di mana data karakter sebenarnya dimulai bukan pada byte 0.

Jadi sepertinya keputusan untuk menggunakan penghentian nol hanyalah apa yang disukai orang, bukan keharusan bahasa.

Pak Boy
sumber
-3

gcc menerima kode di bawah ini:

char s [4] = "abcd";

dan tidak masalah jika kita memperlakukannya sebagai array karakter tetapi bukan string. Yaitu, kita dapat mengaksesnya dengan s [0], s [1], s [2], dan s [3], atau bahkan dengan memcpy (dest, s, 4). Tapi kita akan mendapatkan karakter berantakan ketika kita mencoba dengan menempatkan (s), atau lebih buruk dengan strcpy (dest, s).

kkaaii
sumber
@Adrian W. Ini valid C. String panjang yang tepat adalah casing khusus dan NUL dihilangkan untuk mereka. Ini umumnya merupakan praktik yang tidak bijaksana tetapi dapat berguna dalam kasus-kasus seperti mengisi struct header yang menggunakan "string" FourCC.
Kevin Thibedeau
Kamu benar. Ini adalah C yang valid, akan dikompilasi dan berlaku seperti yang dijelaskan kkaaii. Alasan untuk downvotes (bukan milikku ...) mungkin karena jawaban ini tidak menjawab pertanyaan OP dengan cara apa pun.
Adrian W