Tại sao chữ C chuỗi chỉ đọc?


29

Những lợi thế nào của chuỗi ký tự chuỗi chỉ đọc (-ies / -ied) là:

  1. Một cách khác để tự bắn vào chân mình

    char *foo = "bar";
    foo[0] = 'd'; /* SEGFAULT */
    
  2. Không có khả năng khởi tạo một cách tao nhã một mảng các từ đọc-ghi trong một dòng:

    char *foo[] = { "bar", "baz", "running out of traditional placeholder names" };
    foo[1][2] = 'n'; /* SEGFAULT */ 
    
  3. Phức tạp ngôn ngữ chính nó.

    char *foo = "bar";
    char var[] = "baz";
    some_func(foo); /* VERY DANGEROUS! */
    some_func(var); /* LESS DANGEROUS! */
    

Tiết kiệm bộ nhớ? Tôi đã đọc ở đâu đó (không thể tìm thấy nguồn bây giờ) từ lâu, khi RAM khan hiếm, các trình biên dịch đã cố gắng tối ưu hóa việc sử dụng bộ nhớ bằng cách hợp nhất các chuỗi tương tự.

Ví dụ: "more" và "regex" sẽ trở thành "moregex". Điều này có còn đúng cho đến ngày nay, trong thời đại của những bộ phim chất lượng blu-ray kỹ thuật số? Tôi hiểu rằng các hệ thống nhúng vẫn hoạt động trong môi trường tài nguyên bị hạn chế, tuy nhiên, số lượng bộ nhớ khả dụng đã tăng lên đáng kể.

Những vấn đề tương thích? Tôi giả định rằng một chương trình cũ sẽ cố gắng truy cập bộ nhớ chỉ đọc sẽ bị sập hoặc tiếp tục với lỗi chưa được phát hiện. Do đó, không có chương trình kế thừa nào nên cố gắng truy cập chuỗi bằng chữ và vì vậy, cho phép ghi vào chuỗi bằng chữ sẽ không gây hại cho các chương trình di sản hợp lệ, không hack, di động .

Có bất kỳ lý do khác? Là lý luận của tôi không chính xác? Sẽ là hợp lý khi xem xét một sự thay đổi để đọc các chuỗi ký tự trong các tiêu chuẩn C mới hoặc ít nhất là thêm một tùy chọn cho trình biên dịch? Điều này đã được xem xét trước đây hay "những vấn đề" của tôi quá nhỏ và không đáng kể để làm phiền bất cứ ai?


12
Tôi giả sử bạn đã xem cách các chuỗi ký tự tìm kiếm trong mã được biên dịch ?

2
Nhìn vào hội đồng mà liên kết tôi cung cấp có chứa. Nó ở ngay đó.

8
Ví dụ "moregex" của bạn sẽ không hoạt động vì chấm dứt null.
dan04

4
Bạn không muốn viết lên các hằng số vì điều đó sẽ thay đổi giá trị của chúng. Lần tiếp theo bạn muốn sử dụng cùng một hằng số, nó sẽ khác. Trình biên dịch / thời gian chạy phải lấy nguồn hằng từ đâu đó và bất cứ nơi nào bạn không được phép sửa đổi.
Erik Eidt

1
'Vì vậy, chuỗi ký tự được lưu trữ trong bộ nhớ chương trình, không phải RAM và tràn bộ đệm sẽ dẫn đến tham nhũng của chính chương trình?' Hình ảnh chương trình cũng nằm trong RAM. Nói chính xác, chuỗi ký tự được lưu trữ trong cùng một phân đoạn RAM được sử dụng để lưu trữ hình ảnh chương trình. Và vâng, ghi đè chuỗi có thể làm hỏng chương trình. Quay trở lại thời MS-DOS và CP / M không có bảo vệ bộ nhớ, bạn có thể làm những việc như thế này và nó thường gây ra những vấn đề khủng khiếp. Các virus PC đầu tiên sẽ sử dụng các thủ thuật như thế để sửa đổi chương trình của bạn để nó định dạng ổ cứng của bạn khi bạn cố chạy nó.
Charles E. Grant

Câu trả lời:


40

Trong lịch sử (có lẽ bằng cách viết lại các phần của nó), nó đã trái ngược. Trên các máy tính đầu tiên của đầu những năm 1970 (có lẽ PDP-11 ) chạy phôi nguyên mẫu C (có lẽ BCPL ) không có MMU và không có bảo vệ bộ nhớ (tồn tại trên hầu hết các máy tính lớn của IBM / 360 ). Vì vậy, mỗi byte của bộ nhớ (kể cả xử lý các chuỗi chữ hoặc mã máy) có thể bị ghi đè bởi một chương trình có sai sót (tưởng tượng một chương trình thay đổi một số %để /trong một printf (3) định dạng chuỗi). Do đó, chuỗi và hằng số theo nghĩa đen là có thể ghi.

Khi còn là một thiếu niên vào năm 1975, tôi đã mã hóa trong bảo tàng Palais de la Découverte ở Paris trên các máy tính cũ của những năm 1960 mà không có bộ nhớ bảo vệ: IBM / 1620 chỉ có một bộ nhớ cốt lõi - bạn có thể khởi tạo bàn phím thông thường, vì vậy bạn phải gõ vài chục các chữ số để đọc chương trình ban đầu trên băng đục lỗ; CAB / 500 có bộ nhớ trống từ tính; bạn có thể vô hiệu hóa việc viết một số bản nhạc thông qua các công tắc cơ học gần trống.

Sau đó, máy tính có một số dạng đơn vị quản lý bộ nhớ (MMU) với một số bảo vệ bộ nhớ. Có một thiết bị cấm CPU ghi đè lên một số loại bộ nhớ. Vì vậy, một số phân đoạn bộ nhớ, đáng chú ý là phân đoạn mã (còn gọi là .textphân đoạn) trở thành chỉ đọc (ngoại trừ bởi hệ điều hành đã tải chúng từ đĩa). Việc trình biên dịch và trình liên kết đặt các chuỗi ký tự trong phân đoạn mã đó là điều tự nhiên và các chuỗi ký tự chỉ trở thành đọc. Khi chương trình của bạn cố ghi đè lên chúng, đó là một hành vi không xác định . Và việc có một đoạn mã chỉ đọc trong bộ nhớ ảo mang lại một lợi thế đáng kể: một số quy trình chạy cùng một chương trình có chung RAM ( bộ nhớ vật lýtrang) cho đoạn mã đó (xem MAP_SHAREDcờ cho mmap (2) trên Linux).

Ngày nay, các bộ vi điều khiển giá rẻ có một số bộ nhớ chỉ đọc (ví dụ Flash hoặc ROM) và giữ mã của chúng (và các chuỗi ký tự và các hằng số khác) ở đó. Và các bộ vi xử lý thực (như cái trong máy tính bảng, máy tính xách tay hoặc máy tính để bàn của bạn) có một bộ quản lý bộ nhớ tinh vi và bộ máy bộ đệm được sử dụng cho bộ nhớ ảo & phân trang . Vì vậy, đoạn mã của chương trình thực thi (ví dụ trong ELF ) là bộ nhớ được ánh xạ dưới dạng phân đoạn chỉ đọc, có thể chia sẻ và có thể thực thi (bằng mmap (2) hoặc thực thi (2) trên Linux; BTW bạn có thể đưa ra chỉ thị cho ldđể có được một đoạn mã có thể ghi nếu bạn thực sự muốn). Viết hoặc lạm dụng nó thường là một lỗi phân khúc .

Vì vậy, tiêu chuẩn C là baroque: về mặt pháp lý (chỉ vì lý do lịch sử), chuỗi ký tự không phải là const char[]mảng, mà chỉ là char[]các mảng bị cấm ghi đè.

BTW, một số ngôn ngữ hiện tại cho phép ghi đè chuỗi ký tự (ngay cả Ocaml, trong lịch sử - và rất tệ - có chuỗi ký tự có thể ghi đã thay đổi hành vi gần đây trong 4.02 và hiện có chuỗi chỉ đọc).

Trình biên dịch C hiện tại có thể tối ưu hóa và có "ions""expressions"chia sẻ 5 byte cuối cùng của chúng (bao gồm cả byte null kết thúc).

Cố gắng biên dịch mã C của bạn trong tập tin foo.cvới gcc -O -fverbose-asm -S foo.cvà cái nhìn bên trong file lắp ráp tạo ra foo.sbởi GCC

Cuối cùng, ngữ nghĩa của C đủ phức tạp (đọc thêm về CompCert & Frama-C đang cố gắng nắm bắt nó) và thêm các chuỗi ký tự không đổi có thể ghi sẽ làm cho nó thậm chí còn phức tạp hơn trong khi làm cho các chương trình yếu hơn và thậm chí kém an toàn hơn (và ít hơn hành vi được xác định), do đó, rất khó có khả năng các tiêu chuẩn C trong tương lai sẽ chấp nhận các chuỗi chữ có thể ghi. Có lẽ trái lại họ sẽ làm cho họ const char[]mảng như họ nên về mặt đạo đức.

Cũng lưu ý rằng vì nhiều lý do, dữ liệu có thể thay đổi khó xử lý hơn bởi máy tính (tính liên kết bộ đệm), để mã cho, để nhà phát triển hiểu, hơn là dữ liệu không đổi. Vì vậy, tốt nhất là có hầu hết dữ liệu của bạn (và đáng chú ý là chuỗi ký tự) không thay đổi . Tìm hiểu thêm về mô hình lập trình chức năng .

Trong những ngày Fortran77 cũ trên IBM / 7094, một lỗi thậm chí có thể thay đổi hằng số: nếu bạn CALL FOO(1)và nếu FOOtình cờ sửa đổi đối số của nó được chuyển qua tham chiếu thành 2, thì việc triển khai có thể đã thay đổi các lần xuất hiện khác thành 1 thành 2 và đó thực sự là một sự cố lỗi nghịch ngợm, khá khó tìm.


Đây có phải là để bảo vệ chuỗi như hằng số? Mặc dù chúng không được định nghĩa như consttrong tiêu chuẩn ( stackoverflow.com/questions/2245664/ cấp )?
Marius Macijauskas

Bạn có chắc những máy tính đầu tiên không có bộ nhớ chỉ đọc? Không phải là rẻ hơn đáng kể so với ram? Ngoài ra, việc đưa chúng vào bộ nhớ RO không khiến cho UB cố gắng sửa đổi chúng một cách sai lầm, nhưng dựa vào OP không làm điều đó và anh ta đã vi phạm sự tin tưởng đó. Xem ví dụ các chương trình Fortran nơi tất cả các chữ nghĩa 1đột nhiên hoạt động như 2s và vui như vậy ...
Ded

1
Khi còn là một thiếu niên trong một bảo tàng, tôi đã mã hóa vào năm 1975 trên các máy tính IBM / 1620 và CAB500 cũ. Không có ROM nào: IBM / 1620 có bộ nhớ lõi và CAB500 có trống từ tính (một số bản nhạc có thể bị vô hiệu hóa để có thể ghi được bằng công tắc cơ học)
Basile Starynkevitch

2
Cũng đáng chỉ ra: Đặt các chữ trong đoạn mã có nghĩa là chúng có thể được chia sẻ giữa nhiều bản sao của chương trình vì việc khởi tạo xảy ra vào thời gian biên dịch thay vì thời gian chạy.
Blrfl

@Ded repeatator Vâng, tôi đã thấy một máy chạy biến thể BASIC cho phép bạn thay đổi các hằng số nguyên (Tôi không chắc bạn có cần phải lừa nó để làm như vậy không, ví dụ: truyền các đối số "byref" hoặc nếu một thao let 2 = 3tác đơn giản ). Điều này dẫn đến rất nhiều FUN (theo định nghĩa của Pháo đài Lùn), tất nhiên. Tôi không biết làm thế nào trình thông dịch được thiết kế mà nó cho phép điều này, nhưng nó đã được.
Luaan

2

Trình biên dịch không thể kết hợp "more""regex", vì cái trước có byte rỗng sau ekhi cái sau có x, nhưng nhiều trình biên dịch sẽ kết hợp chuỗi ký tự chuỗi khớp hoàn hảo, và một số cũng sẽ khớp với chuỗi ký tự chung có đuôi. Do đó, mã thay đổi một chuỗi ký tự có thể thay đổi một chuỗi ký tự khác được sử dụng cho một số mục đích hoàn toàn khác nhau nhưng lại chứa các ký tự giống nhau.

Một vấn đề tương tự sẽ phát sinh trong FORTRAN trước khi phát minh ra C. Đối số luôn được truyền qua địa chỉ thay vì theo giá trị. Do đó, một thói quen để thêm hai số sẽ tương đương với:

float sum(float *f1, float *f2) { return *f1 + *f2; }

Trong trường hợp người ta muốn truyền một giá trị không đổi (ví dụ 4.0) sum, trình biên dịch sẽ tạo một biến ẩn danh và khởi tạo nó 4.0. Nếu cùng một giá trị được truyền cho nhiều hàm, trình biên dịch sẽ chuyển cùng một địa chỉ cho tất cả chúng. Kết quả là, nếu một hàm đã sửa đổi một trong các tham số của nó được truyền hằng số dấu phẩy động, thì giá trị của hằng số đó ở nơi khác trong chương trình có thể bị thay đổi, do đó dẫn đến câu nói "Biến sẽ không; 'T ".

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.