Lý do cho các chuỗi kết thúc null là gì?


281

Nhiều như tôi yêu C và C ++, tôi không thể không gãi đầu khi lựa chọn các chuỗi kết thúc null:

  • Các chuỗi có tiền tố (ví dụ Pascal) tồn tại trước C
  • Các chuỗi tiền tố có độ dài làm cho một số thuật toán nhanh hơn bằng cách cho phép tra cứu độ dài thời gian không đổi.
  • Các chuỗi tiền tố có độ dài làm cho việc gây ra lỗi tràn bộ đệm trở nên khó khăn hơn.
  • Ngay cả trên máy 32 bit, nếu bạn cho phép chuỗi có kích thước của bộ nhớ khả dụng, chuỗi có tiền tố dài chỉ rộng hơn ba byte so với chuỗi kết thúc null. Trên máy 16 bit, đây là một byte đơn. Trên các máy 64 bit, 4GB là giới hạn độ dài chuỗi hợp lý, nhưng ngay cả khi bạn muốn mở rộng nó thành kích thước của từ máy, máy 64 bit thường có bộ nhớ rộng làm cho thêm bảy byte đối số null. Tôi biết tiêu chuẩn C ban đầu được viết cho các máy cực kỳ kém (về bộ nhớ), nhưng đối số hiệu quả không bán cho tôi ở đây.
  • Khá nhiều ngôn ngữ khác (ví dụ Perl, Pascal, Python, Java, C #, v.v.) sử dụng các chuỗi có tiền tố dài. Các ngôn ngữ này thường đánh bại C trong các điểm chuẩn thao tác chuỗi vì chúng hiệu quả hơn với chuỗi.
  • C ++ đã sửa lỗi này một chút với std::basic_stringkhuôn mẫu, nhưng các mảng ký tự đơn giản mong đợi các chuỗi kết thúc null vẫn còn phổ biến. Điều này cũng không hoàn hảo bởi vì nó đòi hỏi phân bổ heap.
  • Các chuỗi kết thúc không phải dự trữ một ký tự (cụ thể là null), không thể tồn tại trong chuỗi, trong khi các chuỗi có tiền tố dài có thể chứa các null được nhúng.

Một vài trong số những điều này đã được đưa ra ánh sáng gần đây hơn C, vì vậy sẽ có ý nghĩa đối với C khi không biết về chúng. Tuy nhiên, một số đơn giản là tốt trước khi C đến. Tại sao các chuỗi kết thúc null được chọn thay vì tiền tố chiều dài rõ ràng vượt trội?

EDIT : Vì một số người hỏi về sự thật (và không giống như những gì tôi đã cung cấp) về điểm hiệu quả của tôi ở trên, chúng xuất phát từ một số điều:

  • Việc sử dụng chuỗi kết thúc null yêu cầu độ phức tạp thời gian O (n + m). Tiền tố độ dài thường chỉ yêu cầu O (m).
  • Độ dài sử dụng chuỗi kết thúc null yêu cầu độ phức tạp thời gian O (n). Tiền tố độ dài là O (1).
  • Chiều dài và concat là các hoạt động chuỗi phổ biến nhất. Có một số trường hợp chuỗi kết thúc null có thể hiệu quả hơn, nhưng những điều này xảy ra ít thường xuyên hơn.

Từ các câu trả lời dưới đây, đây là một số trường hợp chuỗi kết thúc null hiệu quả hơn:

  • Khi bạn cần cắt bỏ phần bắt đầu của một chuỗi và cần chuyển nó sang một phương thức nào đó. Bạn thực sự không thể làm điều này trong thời gian liên tục với tiền tố độ dài ngay cả khi bạn được phép hủy chuỗi gốc, bởi vì tiền tố độ dài có thể cần phải tuân theo quy tắc căn chỉnh.
  • Trong một số trường hợp khi bạn chỉ lặp qua ký tự chuỗi theo ký tự, bạn có thể lưu thanh ghi CPU. Lưu ý rằng điều này chỉ hoạt động trong trường hợp bạn chưa phân bổ động chuỗi (Vì sau đó bạn phải giải phóng nó, bắt buộc phải sử dụng thanh ghi CPU mà bạn đã lưu để giữ con trỏ ban đầu bạn có được từ malloc và bạn bè).

Không có cái nào ở trên gần như phổ biến như chiều dài và concat.

Có thêm một câu khẳng định trong các câu trả lời dưới đây:

  • Bạn cần cắt bỏ phần cuối của chuỗi

nhưng điều này là không chính xác - đó là cùng một khoảng thời gian cho các chuỗi tiền tố kết thúc và độ dài null. (Các chuỗi kết thúc không có chỉ là một null trong đó bạn muốn kết thúc mới, các tiền tố có độ dài chỉ trừ đi tiền tố.)


110
Tôi luôn nghĩ rằng đó là một nghi thức cho tất cả các lập trình viên C ++ viết thư viện chuỗi của riêng họ.
Juliet

31
Điều này là gì về mong đợi giải thích hợp lý bây giờ. Tôi cho rằng bạn sẽ muốn nghe một lý do cho x86 hoặc DOS tiếp theo? Theo tôi nghĩ, công nghệ tồi tệ nhất sẽ chiến thắng. Mỗi lần. Và đại diện chuỗi tồi tệ nhất.
jalf

4
Tại sao bạn yêu cầu chuỗi tiền tố dài là vượt trội? Rốt cuộc, C trở nên phổ biến vì nó sử dụng các chuỗi kết thúc null, khiến nó khác biệt với các ngôn ngữ khác.
Daniel C. Sobral

44
@Daniel: C trở nên phổ biến vì nó là một đại diện đơn giản, hiệu quả và di động của các chương trình có thể thực hiện được trên các máy Von Neumann và vì nó được sử dụng cho Unix. Nó chắc chắn không phải vì nó đã quyết định sử dụng các chuỗi kết thúc null. Nếu đó là một quyết định thiết kế tốt, mọi người sẽ sao chép nó và họ đã không làm thế. Họ chắc chắn đã sao chép khá nhiều thứ khác từ C.
Billy ONeal

4
Concat chỉ là O (m) với tiền tố độ dài nếu bạn phá hủy một trong các chuỗi. Nếu không, cùng tốc độ. Việc sử dụng phổ biến nhất chuỗi C (trong lịch sử) là in và quét. Trong cả hai điều này, việc chấm dứt null nhanh hơn vì nó lưu một thanh ghi.
Daniel C. Sobral

Câu trả lời:


195

Từ miệng ngựa

Không có BCPL, B hoặc C nào hỗ trợ dữ liệu ký tự mạnh mẽ bằng ngôn ngữ; mỗi chuỗi xử lý các chuỗi giống như các vectơ của số nguyên và bổ sung các quy tắc chung bằng một vài quy ước. Trong cả BCPL và B, một chuỗi ký tự biểu thị địa chỉ của một vùng tĩnh được khởi tạo với các ký tự của chuỗi, được đóng gói vào các ô. Trong BCPL, byte được đóng gói đầu tiên chứa số lượng ký tự trong chuỗi; trong B, không có số đếm và chuỗi được kết thúc bởi một ký tự đặc biệt, mà B đánh vần *e. Thay đổi này được thực hiện một phần để tránh giới hạn về độ dài của chuỗi gây ra bằng cách giữ số đếm trong khe 8- hoặc 9 bit, và một phần vì việc duy trì số đếm dường như, theo kinh nghiệm của chúng tôi, ít thuận tiện hơn so với sử dụng bộ kết thúc.

Dennis M Ritchie, Phát triển ngôn ngữ C


12
Một câu trích dẫn khác có liên quan: "... ngữ nghĩa của các chuỗi được hoàn toàn thay thế bởi các quy tắc chung hơn chi phối tất cả các mảng và kết quả là ngôn ngữ đơn giản hơn để mô tả ..."
AShelly

151

C không có một chuỗi như một phần của ngôn ngữ. Một "chuỗi" trong C chỉ là một con trỏ tới char. Vì vậy, có thể bạn đang hỏi sai câu hỏi.

"Lý do để loại bỏ một loại chuỗi" có thể phù hợp hơn. Tôi chỉ ra rằng C không phải là ngôn ngữ hướng đối tượng và chỉ có các loại giá trị cơ bản. Chuỗi là một khái niệm cấp cao hơn phải được thực hiện bằng một cách nào đó kết hợp các giá trị của các loại khác. C ở mức độ trừu tượng thấp hơn.

trong ánh sáng của cơn giận dữ bên dưới:

Tôi chỉ muốn chỉ ra rằng tôi không cố nói đây là một câu hỏi ngu ngốc hay xấu, hay cách biểu diễn chuỗi C là lựa chọn tốt nhất. Tôi đang cố gắng làm rõ rằng câu hỏi sẽ được đặt ngắn gọn hơn nếu bạn tính đến thực tế là C không có cơ chế để phân biệt một chuỗi như một kiểu dữ liệu từ một mảng byte. Đây có phải là sự lựa chọn tốt nhất trong khả năng xử lý và bộ nhớ của máy tính ngày nay? Chắc là không. Nhưng nhận thức muộn luôn là 20/20 và tất cả những thứ đó :)


29
char *temp = "foo bar";là một tuyên bố hợp lệ trong C ... hey! đó không phải là một chuỗi? nó không bị chấm dứt?
Yanick Rochon

56
@Yanick: đó chỉ là một cách thuận tiện để báo cho trình biên dịch tạo một mảng char có null ở cuối. đó không phải là một "chuỗi"
Robert S Ciaccio

28
@calavera: Nhưng nó có thể chỉ đơn giản có nghĩa là "Tạo bộ nhớ đệm với nội dung chuỗi này và tiền tố hai byte",
Billy ONeal

14
@Billy: vì một 'chuỗi' thực sự chỉ là một con trỏ tới char, tương đương với một con trỏ tới byte, làm sao bạn biết rằng bộ đệm mà bạn đang xử lý thực sự có ý định là một 'chuỗi'? bạn sẽ cần một loại mới khác với char / byte * để biểu thị điều này. có thể là một cấu trúc?
Robert S Ciaccio

27
Tôi nghĩ @calavera đã đúng, C không có kiểu dữ liệu cho chuỗi. Ok, bạn có thể xem xét một mảng các ký tự như một chuỗi, nhưng điều này không có nghĩa đó luôn là một chuỗi (đối với chuỗi tôi có nghĩa là một chuỗi các ký tự có ý nghĩa xác định). Tệp nhị phân là một mảng các ký tự, nhưng các ký tự đó không có ý nghĩa gì đối với con người.
BlackBear

106

Câu hỏi được hỏi như một điều Length Prefixed Strings (LPS)so với zero terminated strings (SZ)điều, nhưng chủ yếu là phơi bày lợi ích của các chuỗi tiền tố dài. Điều đó có vẻ quá sức, nhưng thành thật mà nói, chúng ta cũng nên xem xét những hạn chế của LPS và lợi thế của SZ.

Theo tôi hiểu, câu hỏi thậm chí có thể được hiểu là một cách thiên vị để hỏi "những lợi thế của Chuỗi không kết thúc là gì?".

Ưu điểm (tôi thấy) của Chuỗi không kết thúc:

  • Rất đơn giản, không cần phải giới thiệu các khái niệm mới trong ngôn ngữ, mảng char / con trỏ char có thể làm được.
  • ngôn ngữ cốt lõi chỉ bao gồm đường cú pháp tối thiểu để chuyển đổi một cái gì đó giữa dấu ngoặc kép thành một bó ký tự (thực sự là một bó byte). Trong một số trường hợp, nó có thể được sử dụng để khởi tạo những thứ hoàn toàn không liên quan đến văn bản. Ví dụ, định dạng tệp hình ảnh xpm là nguồn C hợp lệ chứa dữ liệu hình ảnh được mã hóa dưới dạng chuỗi.
  • Nhân tiện, bạn có thể đặt số 0 trong một chuỗi ký tự, trình biên dịch cũng sẽ thêm một số khác ở cuối chữ : "this\0is\0valid\0C". Có phải là một chuỗi? hoặc bốn chuỗi? Hoặc một loạt các byte ...
  • thực hiện bằng phẳng, không có ẩn số, không có số nguyên ẩn.
  • không có phân bổ bộ nhớ ẩn liên quan (tốt, một số chức năng phi tiêu chuẩn khét tiếng như strdup thực hiện phân bổ, nhưng đó chủ yếu là một vấn đề).
  • không có vấn đề cụ thể nào đối với phần cứng nhỏ hay lớn (hãy tưởng tượng gánh nặng quản lý độ dài tiền tố 32 bit trên bộ vi điều khiển 8 bit hoặc hạn chế kích thước chuỗi xuống dưới 256 byte, đó là vấn đề tôi thực sự gặp phải với Turbo Pascal trước đây).
  • thực hiện thao tác chuỗi chỉ là một số ít chức năng thư viện rất đơn giản
  • hiệu quả cho việc sử dụng chính của chuỗi: văn bản không đổi đọc tuần tự từ một khởi đầu đã biết (chủ yếu là tin nhắn cho người dùng).
  • số 0 kết thúc thậm chí không bắt buộc, tất cả các công cụ cần thiết để thao tác ký tự như một bó byte có sẵn. Khi thực hiện khởi tạo mảng trong C, bạn thậm chí có thể tránh được đầu cuối NUL. Chỉ cần đặt đúng kích thước. char a[3] = "foo";là C hợp lệ (không phải C ++) và sẽ không đặt số 0 cuối cùng vào a.
  • phù hợp với quan điểm unix "mọi thứ đều là tệp", bao gồm cả "tệp" không có chiều dài nội tại như stdin, stdout. Bạn nên nhớ rằng các nguyên thủy đọc và viết mở được thực hiện ở mức rất thấp. Chúng không phải là các cuộc gọi thư viện, mà là các cuộc gọi hệ thống. Và cùng một API được sử dụng cho các tệp nhị phân hoặc văn bản. Các tệp gốc đọc tệp có được một địa chỉ bộ đệm và kích thước và trả về kích thước mới. Và bạn có thể sử dụng chuỗi làm bộ đệm để viết. Sử dụng một kiểu biểu diễn chuỗi khác có nghĩa là bạn không thể dễ dàng sử dụng một chuỗi ký tự làm bộ đệm để xuất ra, hoặc bạn sẽ phải làm cho nó có một hành vi rất lạ khi truyền nó char*. Cụ thể là không trả về địa chỉ của chuỗi, mà thay vào đó là trả về dữ liệu thực tế.
  • rất dễ dàng để thao tác dữ liệu văn bản được đọc từ một tệp tại chỗ, không có bản sao bộ đệm vô dụng, chỉ cần chèn các số 0 ở đúng vị trí bộ phận).
  • chuẩn bị một số giá trị int ở bất kỳ kích thước nào sẽ bao hàm các vấn đề căn chỉnh. Độ dài ban đầu phải được căn chỉnh, nhưng không có lý do gì để làm điều đó cho các dữ liệu ký tự (và một lần nữa, việc buộc các chuỗi sẽ ngụ ý các vấn đề khi coi chúng là một bó byte).
  • độ dài được biết đến tại thời gian biên dịch cho chuỗi ký tự không đổi (sizeof). Vậy tại sao bất cứ ai cũng muốn lưu trữ nó trong bộ nhớ chuẩn bị nó vào dữ liệu thực tế?
  • theo cách mà C đang làm (gần như) mọi người khác, các chuỗi được xem như là mảng của char. Vì độ dài mảng không được quản lý bởi C, nên độ dài logic không được quản lý cho chuỗi. Điều đáng ngạc nhiên duy nhất là 0 mục được thêm vào cuối, nhưng đó chỉ ở cấp độ ngôn ngữ cốt lõi khi nhập một chuỗi giữa các dấu ngoặc kép. Người dùng hoàn toàn có thể gọi các hàm thao tác chuỗi đi qua chiều dài hoặc thậm chí sử dụng bản ghi nhớ đơn giản để thay thế. SZ chỉ là một cơ sở. Trong hầu hết các ngôn ngữ khác, độ dài mảng được quản lý, logic của nó giống với các chuỗi.
  • trong thời hiện đại, dù sao thì bộ ký tự 1 byte là không đủ và bạn thường phải xử lý các chuỗi unicode được mã hóa trong đó số lượng ký tự rất khác nhau về số lượng byte. Nó ngụ ý rằng người dùng có thể sẽ muốn nhiều hơn "chỉ kích thước", mà còn các thông tin khác. Giữ độ dài không sử dụng gì (đặc biệt là không có nơi tự nhiên để lưu trữ chúng) liên quan đến những thông tin hữu ích khác này.

Điều đó nói rằng, không cần phải phàn nàn trong trường hợp hiếm hoi khi chuỗi C tiêu chuẩn thực sự không hiệu quả. Libs có sẵn. Nếu tôi theo xu hướng đó, tôi nên phàn nàn rằng tiêu chuẩn C không bao gồm bất kỳ chức năng hỗ trợ regex nào ... nhưng thực sự mọi người đều biết đó không phải là vấn đề thực sự vì có thư viện cho mục đích đó. Vì vậy, khi muốn hiệu quả thao tác chuỗi, tại sao không sử dụng một thư viện như chuỗi ? Hoặc thậm chí chuỗi C ++?

EDIT : Gần đây tôi đã có một cái nhìn để D chuỗi . Thật thú vị khi thấy rằng giải pháp được chọn không phải là tiền tố kích thước, cũng không phải là chấm dứt. Như trong C, các chuỗi ký tự được đặt trong dấu ngoặc kép chỉ là viết tắt của các mảng char bất biến và ngôn ngữ cũng có một chuỗi từ khóa có nghĩa là (mảng char bất biến).

Nhưng mảng D phong phú hơn nhiều so với mảng C. Trong trường hợp độ dài mảng tĩnh được biết đến trong thời gian chạy nên không cần lưu trữ độ dài. Trình biên dịch có nó tại thời gian biên dịch. Trong trường hợp mảng động, chiều dài có sẵn nhưng tài liệu D không nêu rõ nơi lưu giữ. Đối với tất cả những gì chúng ta biết, trình biên dịch có thể chọn giữ nó trong một số thanh ghi hoặc trong một số biến được lưu trữ cách xa dữ liệu ký tự.

Trên các mảng char bình thường hoặc các chuỗi không có nghĩa đen, không có số 0 cuối cùng, do đó, lập trình viên phải tự đặt nó nếu anh ta muốn gọi một số hàm C từ D. Trong trường hợp cụ thể của các chuỗi ký tự, tuy nhiên trình biên dịch D vẫn đặt số 0 ở kết thúc mỗi chuỗi (để cho phép truyền dễ dàng tới chuỗi C để gọi hàm C dễ dàng hơn?), nhưng số 0 này không phải là một phần của chuỗi (D không tính nó theo kích thước chuỗi).

Điều duy nhất làm tôi thất vọng phần nào là các chuỗi được cho là utf-8, nhưng độ dài rõ ràng vẫn trả về một số byte (ít nhất là đúng trên trình biên dịch gdc của tôi) ngay cả khi sử dụng ký tự nhiều byte. Tôi không rõ là do lỗi biên dịch hay do mục đích. (OK, tôi có thể đã tìm ra điều gì đã xảy ra. Để nói với trình biên dịch D, nguồn của bạn sử dụng utf-8, bạn phải đặt một số thứ tự byte ngu ngốc ngay từ đầu. Tôi viết ngu ngốc vì tôi không biết trình soạn thảo làm điều đó, đặc biệt là cho UTF- 8 được cho là tương thích ASCII).


7
... Tiếp tục ... Một số điểm của bạn tôi nghĩ là hoàn toàn sai, tức là đối số "mọi thứ là một tệp". Tập tin được truy cập tuần tự, chuỗi C thì không. Tiền tố chiều dài cũng có thể được thực hiện với đường cú pháp tối thiểu. Đối số hợp lý duy nhất ở đây là cố gắng quản lý tiền tố 32 bit trên phần cứng nhỏ (tức là 8 bit); Tôi nghĩ rằng điều đó có thể được giải quyết đơn giản bằng cách nói kích thước của chiều dài được xác định bởi việc thực hiện. Rốt cuộc, đó là những gì std::basic_stringlàm.
Billy ONeal

3
@Billy ONeal: thực sự có hai phần khác nhau trong câu trả lời của tôi. Một là về những gì là một phần của 'ngôn ngữ C cốt lõi', một là về những thư viện tiêu chuẩn sẽ cung cấp. Liên quan đến hỗ trợ chuỗi, chỉ có một mục từ ngôn ngữ cốt lõi: ý nghĩa của một trích dẫn kép kèm theo bó byte. Tôi không thực sự hạnh phúc hơn bạn với hành vi C. Tôi cảm thấy kỳ diệu khi thêm số 0 ở cuối mỗi lần đóng hai byte kèm theo là đủ tệ. Tôi muốn và rõ ràng \0vào cuối khi các lập trình viên muốn điều đó thay vì ngầm định. Chuẩn bị chiều dài là tồi tệ hơn nhiều.
kriss

2
@Billy ONeal: điều đó không đúng, việc sử dụng quan tâm đến cốt lõi và thư viện là gì. Điểm lớn nhất là khi C được sử dụng để thực hiện HĐH. Ở cấp độ đó không có thư viện có sẵn. C cũng thường được sử dụng trong các bối cảnh nhúng hoặc cho các thiết bị lập trình nơi bạn thường có cùng loại hạn chế. Trong nhiều trường hợp Joes của lẽ không nên sử dụng C ở tất cả ngày nay: "OK, bạn muốn nó trên bàn điều khiển Bạn có một giao diện điều khiển Không Quá xấu ...??"
Kriss

5
@Billy "Chà, đối với 0,01% lập trình viên C đang triển khai hệ điều hành, tốt thôi." Các lập trình viên khác có thể đi bộ đường dài. C được tạo ra để viết một hệ điều hành.
Daniel C. Sobral

5
Tại sao? Bởi vì nó nói nó là một ngôn ngữ mục đích chung? Nó có nói những gì những người đã viết nó đang làm khi nó được tạo ra không? Nó được sử dụng trong vài năm đầu tiên của cuộc đời? Vì vậy, những gì nó nói rằng không đồng ý với tôi? Nó là một ngôn ngữ có mục đích chung được tạo ra để viết một hệ điều hành . Liệu nó có phủ nhận nó?
Daniel C. Sobral

61

Tôi nghĩ rằng, nó có lý do lịch sử và tìm thấy điều này trong wikipedia :

Tại thời điểm C (và các ngôn ngữ mà nó được tạo ra) được phát triển, bộ nhớ cực kỳ hạn chế, vì vậy chỉ sử dụng một byte phí để lưu trữ độ dài của chuỗi là hấp dẫn. Sự thay thế phổ biến duy nhất tại thời điểm đó, thường được gọi là "chuỗi Pascal" (mặc dù cũng được sử dụng bởi các phiên bản đầu tiên của BASIC), đã sử dụng một byte hàng đầu để lưu trữ độ dài của chuỗi. Điều này cho phép chuỗi chứa NUL và thực hiện tìm độ dài chỉ cần một lần truy cập bộ nhớ (O (1) (không đổi) thời gian). Nhưng một byte giới hạn độ dài là 255. Giới hạn độ dài này hạn chế hơn nhiều so với các vấn đề với chuỗi C, do đó, chuỗi C nói chung đã thắng.


2
@muntoo Hmm ... tương thích?
khachik

19
@muntoo: Bởi vì điều đó sẽ phá vỡ số lượng lớn mã C và C ++ hiện có.
Billy ONeal

10
@muntoo: Nghịch lý đến rồi đi, nhưng mã kế thừa là mãi mãi. Bất kỳ phiên bản nào trong tương lai của C sẽ phải tiếp tục hỗ trợ các chuỗi kết thúc 0, nếu không, mã kế thừa có giá trị hơn 30 năm sẽ phải được viết lại (điều này sẽ không xảy ra). Và miễn là cách cũ có sẵn, đó là những gì mọi người sẽ tiếp tục sử dụng, vì đó là những gì họ quen thuộc.
John Bode

8
@muntoo: Tin tôi đi, đôi khi tôi ước mình có thể. Nhưng tôi vẫn thích các chuỗi kết thúc 0 hơn các chuỗi Pascal.
John Bode

2
Nói về di sản ... Các chuỗi C ++ hiện bắt buộc phải chấm dứt NUL.
Jim Balter

32

Calaverađúng , nhưng khi mọi người dường như không có được quan điểm của mình, tôi sẽ cung cấp một số ví dụ mã.

Trước tiên, hãy xem xét C là gì: một ngôn ngữ đơn giản, trong đó tất cả các mã có một bản dịch trực tiếp khá thành ngôn ngữ máy. Tất cả các loại phù hợp với các thanh ghi và trên ngăn xếp, và nó không yêu cầu một hệ điều hành hoặc một thư viện thời gian lớn để chạy, vì nó có nghĩa là viết những điều này (một nhiệm vụ rất phù hợp, xem xét ở đó thậm chí không phải là một đối thủ cạnh tranh cho đến ngày nay).

Nếu C có một stringloại, như inthoặc char, thì đó sẽ là loại không phù hợp với thanh ghi hoặc trong ngăn xếp và sẽ yêu cầu cấp phát bộ nhớ (với tất cả cơ sở hạ tầng hỗ trợ) theo bất kỳ cách nào. Tất cả đều đi ngược lại các nguyên lý cơ bản của C.

Vì vậy, một chuỗi trong C là:

char s*;

Vì vậy, hãy giả sử rằng đây là tiền tố dài. Hãy viết mã để nối hai chuỗi:

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;
}

Một cách khác là sử dụng struct để xác định chuỗi:

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

Tại thời điểm này, tất cả các thao tác chuỗi sẽ yêu cầu hai phân bổ được thực hiện, trong thực tế, điều đó có nghĩa là bạn sẽ đi qua một thư viện để thực hiện bất kỳ xử lý nào.

Điều buồn cười là ... những cấu trúc như thế tồn tại trong C! Chúng chỉ không được sử dụng để hiển thị tin nhắn hàng ngày của bạn cho người dùng xử lý.

Vì vậy, đây là điểm Calavera đang thực hiện: không có loại chuỗi trong C . Để làm bất cứ điều gì với nó, bạn phải lấy một con trỏ và giải mã nó thành một con trỏ thành hai loại khác nhau, và sau đó nó trở nên rất phù hợp với kích thước của một chuỗi và không thể chỉ là "xác định thực hiện".

Bây giờ, C có thể xử lý bộ nhớ bằng mọi cách và các memhàm trong thư viện (trong <string.h>, thậm chí!) Cung cấp tất cả các công cụ bạn cần để xử lý bộ nhớ dưới dạng một cặp con trỏ và kích thước. Cái gọi là "chuỗi" trong C được tạo ra chỉ với một mục đích: hiển thị các thông báo trong bối cảnh viết một hệ điều hành dành cho các thiết bị đầu cuối văn bản. Và, cho điều đó, chấm dứt null là đủ.


2
1. +1. 2. Rõ ràng nếu hành vi mặc định của ngôn ngữ sẽ được thực hiện bằng cách sử dụng tiền tố độ dài, sẽ có những thứ khác để làm cho điều đó dễ dàng hơn. Ví dụ, tất cả các diễn viên của bạn ở đó sẽ bị ẩn bởi các cuộc gọi đến strlenvà bạn bè. Đối với vấn đề "để nó thực hiện", bạn có thể nói rằng tiền tố là bất cứ thứ gì shortcó trên hộp mục tiêu. Sau đó tất cả đúc của bạn vẫn sẽ làm việc. 3. Tôi có thể đưa ra các kịch bản giả định suốt cả ngày khiến cho một hoặc hệ thống khác trông tệ.
Billy ONeal

5
@Billy Điều thư viện là đủ đúng, ngoài thực tế là C được thiết kế để sử dụng tối thiểu hoặc không sử dụng thư viện. Việc sử dụng các nguyên mẫu, ví dụ, không phổ biến sớm. Nói tiền tố có shorthiệu quả giới hạn kích thước của chuỗi, dường như là một điều họ không quan tâm. Bản thân tôi, đã làm việc với các chuỗi BASIC và Pascal 8 bit, các chuỗi COBOL có kích thước cố định và những thứ tương tự, đã nhanh chóng trở thành một fan hâm mộ của các chuỗi C có kích thước không giới hạn. Ngày nay, kích thước 32 bit sẽ xử lý bất kỳ chuỗi thực tế nào, nhưng việc thêm các byte đó sớm là vấn đề.
Daniel C. Sobral

1
@Billy: Đầu tiên, cảm ơn bạn Daniel ... bạn dường như hiểu những gì tôi đang làm. Thứ hai, Billy, tôi nghĩ bạn vẫn còn thiếu điểm đang được thực hiện ở đây. Tôi cho một người không tranh luận về ưu và nhược điểm của các kiểu dữ liệu chuỗi tiền tố với độ dài của chúng. Những gì tôi đang nói, và những gì Daniel nhấn mạnh rất rõ ràng, đó là đã có một quyết định trong việc thực hiện C đến không xử lý mà tranh luận ở tất cả . Các chuỗi không tồn tại khi có liên quan đến ngôn ngữ cơ bản. Quyết định về cách xử lý các chuỗi được để lại cho lập trình viên ... và việc chấm dứt null trở nên phổ biến.
Robert S Ciaccio

1
+1 bởi tôi. Một điều nữa tôi muốn thêm vào; một cấu trúc như bạn đề xuất, nó bỏ lỡ một bước quan trọng đối với một stringkiểu thực : nó không nhận thức được các ký tự. Đó là một mảng của "char" (một "char" trong biệt ngữ máy cũng giống như một "từ" là những gì con người sẽ gọi một từ trong câu). Một chuỗi các ký tự là một khái niệm cấp cao hơn có thể được triển khai trên đầu một mảng charnếu bạn đưa ra khái niệm mã hóa.
Frerich Raabe

2
@ DanielC.Sobral: Ngoài ra, cấu trúc bạn đề cập sẽ không yêu cầu hai phân bổ. Hoặc sử dụng nó khi bạn có nó trên ngăn xếp (vì vậy chỉ bufyêu cầu phân bổ) hoặc sử dụng struct string {int len; char buf[]};và phân bổ toàn bộ với một phân bổ là một thành viên mảng linh hoạt và chuyển nó thành một string*. (Hoặc có thể cho rằng, struct string {int capacity; int len; char buf[]};vì lý do hiệu suất rõ ràng)
Vịt Mooing

20

Rõ ràng về hiệu suất và an toàn, bạn sẽ muốn giữ độ dài của chuỗi trong khi bạn làm việc với chuỗi đó thay vì liên tục thực hiện strlenhoặc tương đương với chuỗi đó. Tuy nhiên, lưu trữ độ dài ở một vị trí cố định ngay trước nội dung chuỗi là một thiết kế cực kỳ tệ. Như Jorgen đã chỉ ra trong các nhận xét về câu trả lời của Sanjit, nó loại trừ việc xử lý phần đuôi của chuỗi như một chuỗi, ví dụ, thực hiện rất nhiều thao tác phổ biến như path_to_filenamehoặc filename_to_extensionkhông thể mà không phân bổ bộ nhớ mới (và phát sinh khả năng thất bại và xử lý lỗi) . Và dĩ nhiên, có một vấn đề là không ai có thể đồng ý trường có độ dài chuỗi nên chiếm bao nhiêu byte (rất nhiều "chuỗi Pascal" xấu

Thiết kế của C cho phép lập trình viên chọn nếu / ở đâu / cách lưu trữ độ dài linh hoạt và mạnh mẽ hơn nhiều. Nhưng tất nhiên các lập trình viên phải thông minh. C trừng phạt sự ngu ngốc với các chương trình bị sập, ngừng hoạt động hoặc cho kẻ thù của bạn root.


+1. Thật tuyệt khi có một nơi tiêu chuẩn để lưu trữ độ dài mặc dù vậy, những người trong chúng ta, những người muốn một cái gì đó như tiền tố chiều dài không phải viết hàng tấn "mã keo" ở khắp mọi nơi.
Billy ONeal

2
Không có vị trí tiêu chuẩn nào có thể liên quan đến dữ liệu chuỗi, nhưng tất nhiên bạn có thể sử dụng một biến cục bộ riêng (tính toán lại thay vì chuyển nó khi cái sau không thuận tiện và cái trước không quá lãng phí) hoặc cấu trúc có con trỏ đến chuỗi (và thậm chí tốt hơn, một cờ cho biết cấu trúc "sở hữu" con trỏ cho mục đích phân bổ hay liệu nó có tham chiếu đến chuỗi sở hữu ở nơi khác không. Và tất nhiên bạn có thể bao gồm một thành viên mảng linh hoạt trong cấu trúc để linh hoạt phân bổ chuỗi có cấu trúc khi nó phù hợp với bạn.
R .. GitHub DỪNG GIÚP ICE

13

Sự lười biếng, đăng ký tính tiết kiệm và tính di động khi xem xét việc lắp ráp của bất kỳ ngôn ngữ nào, đặc biệt là C, một bước so với lắp ráp (do đó thừa hưởng rất nhiều mã kế thừa lắp ráp). Bạn sẽ đồng ý vì một char null sẽ vô dụng trong những ngày ASCII đó, nó (và có lẽ tốt như char char kiểm soát EOF).

Hãy xem mã giả

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

tổng số 1 đăng ký sử dụng

trường hợp 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

tổng số 2 thanh ghi được sử dụng

Điều đó có vẻ thiển cận vào thời điểm đó, nhưng xem xét tính tiết kiệm trong mã và đăng ký (đó là PREMIUM tại thời điểm đó, thời điểm bạn biết, họ sử dụng thẻ đục lỗ). Do đó nhanh hơn (khi tốc độ bộ xử lý có thể được tính bằng kHz), "Hack" này khá tốt và dễ mang theo bộ xử lý không đăng ký một cách dễ dàng.

Để tranh luận, tôi sẽ thực hiện 2 thao tác chuỗi chung

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

độ phức tạp O (n) trong hầu hết các trường hợp chuỗi PASCAL là O (1) vì độ dài của chuỗi được đặt trước vào cấu trúc chuỗi (điều đó cũng có nghĩa là thao tác này sẽ phải được thực hiện trong giai đoạn trước).

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

độ phức tạp O (n) và chuẩn bị độ dài chuỗi sẽ không thay đổi độ phức tạp của thao tác, trong khi tôi thừa nhận sẽ mất ít thời gian hơn 3 lần.

Mặt khác, nếu bạn sử dụng chuỗi PASCAL, bạn sẽ phải thiết kế lại API của mình để lấy chiều dài đăng ký tài khoản và độ bền bit, chuỗi PASCAL có giới hạn nổi tiếng là 255 char (0xFF) vì độ dài được lưu trữ trong 1 byte (8 bit) ) và bạn muốn có một chuỗi dài hơn (16 bit-> bất cứ thứ gì) bạn sẽ phải tính đến kiến ​​trúc trong một lớp mã của mình, điều đó có nghĩa là trong hầu hết các API chuỗi không tương thích trong trường hợp nếu bạn muốn chuỗi dài hơn.

Thí dụ:

Một tệp được viết bằng chuỗi api được chuẩn bị trước của bạn trên máy tính 8 bit và sau đó sẽ phải đọc trên máy tính 32 bit, chương trình lười biếng sẽ xem xét rằng 4byte của bạn là độ dài của chuỗi sau đó phân bổ số lượng bộ nhớ đó như thế nào sau đó cố gắng đọc nhiều byte đó. Một trường hợp khác sẽ là chuỗi PPC 32 byte đọc (endian nhỏ) trên x86 (endian lớn), tất nhiên nếu bạn không biết rằng cái này được viết bởi cái kia thì sẽ có rắc rối. Độ dài 1 byte (0x00000001) sẽ trở thành 16777216 (0x0100000) là 16 MB để đọc chuỗi 1 byte. Tất nhiên bạn sẽ nói rằng mọi người nên đồng ý về một tiêu chuẩn nhưng ngay cả unicode 16 bit cũng có độ bền nhỏ và lớn.

Tất nhiên C cũng có vấn đề của nó, nhưng, sẽ rất ít bị ảnh hưởng bởi các vấn đề được nêu ra ở đây.


2
@deemoowoor: Concat: O(m+n)với chuỗi nullterm, O(n)điển hình ở mọi nơi khác. Độ dài O(n)với chuỗi nullterm, O(1)ở mọi nơi khác. Tham gia: O(n^2)với chuỗi nullterm, O(n)ở mọi nơi khác. Có một số trường hợp chuỗi kết thúc null hiệu quả hơn (nghĩa là chỉ thêm một vào trường hợp con trỏ), nhưng concat và length là các hoạt động phổ biến nhất (ít nhất là độ dài được yêu cầu để định dạng, đầu ra tệp, hiển thị bảng điều khiển, v.v.) . Nếu bạn lưu trữ độ dài để khấu hao thì O(n)bạn chỉ đưa ra quan điểm rằng độ dài sẽ được lưu trữ bằng chuỗi.
Billy ONeal

1
Tôi đồng ý rằng trong mã ngày nay loại chuỗi này không hiệu quả và dễ bị lỗi, nhưng ví dụ hiển thị Bảng điều khiển không thực sự phải biết độ dài của chuỗi để hiển thị chuỗi hiệu quả, đầu ra tệp không thực sự cần biết về chuỗi. độ dài (chỉ phân bổ cụm khi đang di chuyển) và định dạng chuỗi tại thời điểm này được thực hiện trên độ dài chuỗi cố định trong hầu hết các trường hợp. Dù sao bạn cũng phải viết mã xấu nếu bạn ghép trong C có độ phức tạp O (n ^ 2), tôi khá chắc chắn rằng tôi có thể viết một mã trong độ phức tạp O (n)
dvhh

1
@dvhh: Tôi không nói n ^ 2 - Tôi đã nói m + n - nó vẫn tuyến tính, nhưng bạn cần tìm đến cuối chuỗi gốc để thực hiện nối, trong khi với tiền tố dài không tìm kiếm bắt buộc. (Đây thực sự chỉ là một hệ quả khác của độ dài cần có thời gian tuyến tính)
Billy ONeal

1
@Billy ONeal: từ sự tò mò đơn thuần, tôi đã thực hiện một grep trên dự án C hiện tại của tôi (khoảng 50000 dòng mã) cho các lệnh gọi hàm thao tác chuỗi. strlen 101, strcpy và các biến thể (strncpy, strlcpy): 85 (Tôi cũng có vài trăm chuỗi ký tự được sử dụng cho tin nhắn, các bản sao ngụ ý), strcmp: 56, strcat: 13 (và 6 là nối chuỗi với chuỗi có độ dài bằng 0 để gọi strncat) . Tôi đồng ý độ dài tiền tố sẽ tăng tốc các cuộc gọi đến strlen, nhưng không phải là strcpy hoặc strcmp (có thể nếu API strcmp không sử dụng tiền tố chung). Điều thú vị nhất liên quan đến các ý kiến ​​trên là strcat rất hiếm.
kriss

1
@supercat: không thực sự, nhìn vào một số triển khai. Các chuỗi ngắn đang sử dụng bộ đệm dựa trên ngăn xếp ngắn (không phân bổ heap) chỉ sử dụng heap khi chúng lớn hơn. Nhưng hãy thoải mái cung cấp một triển khai thực tế ý tưởng của bạn như một thư viện. Thông thường những rắc rối chỉ xuất hiện khi chúng ta đi vào chi tiết, không phải trong thiết kế tổng thể.
kriss

9

Theo nhiều cách, C là nguyên thủy. Và tôi yêu nó.

Đó là một bước trên ngôn ngữ lắp ráp, mang lại cho bạn hiệu suất gần như tương tự với ngôn ngữ dễ viết và duy trì hơn nhiều.

Bộ kết thúc null đơn giản và không yêu cầu sự hỗ trợ đặc biệt của ngôn ngữ.

Nhìn lại, có vẻ không thuận tiện. Nhưng tôi đã sử dụng ngôn ngữ lắp ráp từ những năm 80 và nó có vẻ rất thuận tiện vào thời điểm đó. Tôi chỉ nghĩ rằng phần mềm liên tục phát triển, và các nền tảng và công cụ liên tục ngày càng tinh vi hơn.


Tôi không thấy những gì còn nguyên thủy về chuỗi kết thúc null hơn bất kỳ thứ gì khác. Pascal có trước C và nó sử dụng tiền tố chiều dài. Chắc chắn, nó được giới hạn ở 256 ký tự trên mỗi chuỗi, nhưng chỉ cần sử dụng trường 16 bit sẽ giải quyết được vấn đề trong phần lớn các trường hợp.
Billy ONeal

Thực tế là nó giới hạn số lượng nhân vật chính xác là loại vấn đề bạn cần suy nghĩ khi làm điều gì đó như thế. Vâng, bạn có thể làm cho nó dài hơn, nhưng sau đó byte quan trọng. Và một trường 16 bit sẽ đủ dài cho tất cả các trường hợp? Thôi nào, bạn phải thừa nhận rằng việc chấm dứt null là nguyên thủy về mặt khái niệm.
Jonathan Wood

10
Hoặc bạn giới hạn độ dài của chuỗi hoặc bạn giới hạn nội dung (không có ký tự null) hoặc bạn chấp nhận chi phí phụ của số lượng 4 đến 8 byte. Không có bữa trưa miễn phí. Tại thời điểm bắt đầu, chuỗi kết thúc null có ý nghĩa hoàn hảo. Trong phần lắp ráp, đôi khi tôi đã sử dụng bit trên cùng của một ký tự để đánh dấu phần cuối của chuỗi, tiết kiệm thêm một byte!
Đánh dấu tiền chuộc

Chính xác, Mark: Không có bữa trưa miễn phí. Nó luôn luôn là một sự thỏa hiệp. Ngày nay, chúng ta không cần phải thực hiện cùng một loại thỏa hiệp. Nhưng hồi đó, cách tiếp cận này có vẻ tốt như mọi cách khác.
Jonathan Wood

8

Giả sử trong giây lát C đã triển khai chuỗi theo cách Pascal, bằng cách thêm tiền tố vào chúng theo chiều dài: chuỗi dài 7 char có phải là DATA TYPE giống như chuỗi 3 char không? Nếu câu trả lời là có, thì trình biên dịch sẽ tạo ra loại mã nào khi tôi gán cái trước cho cái sau? Chuỗi nên được cắt ngắn, hoặc tự động thay đổi kích thước? Nếu thay đổi kích thước, thao tác đó có nên được bảo vệ bằng khóa để làm cho nó an toàn không? Phương pháp tiếp cận C đã đẩy tất cả những vấn đề này, dù muốn hay không :)


2
Ơ .. không, không. Cách tiếp cận C không cho phép gán chuỗi dài 7 char cho chuỗi dài 3 char.
Billy ONeal

@Billy ONeal: tại sao không? Theo như tôi hiểu trong trường hợp này, tất cả các chuỗi đều có cùng kiểu dữ liệu (char *), vì vậy độ dài không thành vấn đề. Không giống như Pascal. Nhưng đó là một hạn chế của Pascal, chứ không phải là vấn đề với các chuỗi có tiền tố dài.
Oliver Mason

4
@Billy: Tôi nghĩ bạn vừa nói lại quan điểm của Cristian. C giải quyết những vấn đề này bằng cách không giải quyết chúng. Bạn vẫn đang suy nghĩ về C thực sự có chứa một khái niệm về một chuỗi. Nó chỉ là một con trỏ, vì vậy bạn có thể gán nó cho bất cứ điều gì bạn muốn.
Robert S Ciaccio

2
Nó giống như ** ma trận: "không có chuỗi".
Robert S Ciaccio

1
@calavera: Tôi không thấy điều đó chứng minh điều gì. Bạn có thể giải quyết nó theo cùng một cách với tiền tố độ dài ... tức là không cho phép chuyển nhượng.
Billy ONeal

8

Bằng cách nào đó tôi đã hiểu câu hỏi để ngụ ý rằng không có trình biên dịch hỗ trợ cho các chuỗi có tiền tố dài trong C. Ví dụ sau đây cho thấy, ít nhất bạn có thể bắt đầu thư viện chuỗi C của riêng mình, trong đó độ dài chuỗi được tính vào thời gian biên dịch, với cấu trúc như sau:

#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;
}

Tuy nhiên, điều này sẽ không có vấn đề gì vì bạn cần cẩn thận khi giải phóng cụ thể con trỏ chuỗi đó và khi nào nó được phân bổ tĩnh ( charmảng bằng chữ ).

Chỉnh sửa: Là câu trả lời trực tiếp hơn cho câu hỏi, quan điểm của tôi là đây là cách C có thể hỗ trợ cả việc có sẵn độ dài chuỗi (dưới dạng hằng số thời gian biên dịch), nếu bạn cần, nhưng vẫn không có phí bộ nhớ nếu bạn muốn sử dụng con trỏ chỉ và chấm dứt bằng không.

Tất nhiên, có vẻ như làm việc với các chuỗi kết thúc bằng 0 là cách làm được khuyến nghị, vì nói chung thư viện tiêu chuẩn không lấy độ dài chuỗi làm đối số và vì việc trích xuất độ dài không phải là mã đơn giản char * s = "abc"như ví dụ của tôi cho thấy.


Vấn đề là các thư viện không biết đến sự tồn tại của cấu trúc của bạn và vẫn xử lý những thứ như null null được nhúng không chính xác. Ngoài ra, điều này không thực sự trả lời câu hỏi tôi đã hỏi.
Billy ONeal

1
Đúng. Vì vậy, vấn đề lớn hơn là không có cách tiêu chuẩn nào tốt hơn để cung cấp các giao diện với các tham số chuỗi hơn các chuỗi không kết thúc cũ đơn giản. Tôi vẫn khẳng định, có những thư viện hỗ trợ cho ăn theo cặp chiều dài con trỏ (tốt, ít nhất bạn có thể xây dựng chuỗi C ++ std :: với chúng).
Pyry Jahkola

2
Ngay cả khi bạn lưu trữ độ dài, bạn không bao giờ nên cho phép các chuỗi có null được nhúng. Đây là ý nghĩa phổ biến cơ bản. Nếu dữ liệu của bạn có thể có null trong đó, bạn không bao giờ nên sử dụng nó với các hàm mong đợi chuỗi.
R .. GitHub DỪNG GIÚP ICE

1
@supercat: Từ góc độ bảo mật, tôi hoan nghênh sự dư thừa đó. Mặt khác, các lập trình viên không biết gì (hoặc bị mất ngủ) kết thúc việc kết hợp dữ liệu nhị phân và chuỗi và chuyển chúng vào những thứ mong đợi chuỗi [kết thúc null] ...
R .. GitHub DỪNG GIÚP ICE

1
@R ..: Mặc dù các phương thức mong đợi chuỗi kết thúc null thường mong đợi a char*, nhưng nhiều phương thức không mong đợi kết thúc null cũng mong đợi a char*. Một lợi ích đáng kể hơn của việc tách các loại sẽ liên quan đến hành vi Unicode. Có thể đáng để triển khai chuỗi để duy trì các cờ cho dù các chuỗi được biết có chứa một số loại ký tự nhất định hay được biết là không chứa chúng [ví dụ: tìm điểm mã 999.990 trong chuỗi triệu ký tự được biết là không chứa bất kỳ ký tự nào ngoài mặt phẳng đa ngôn ngữ cơ bản sẽ có thứ tự cường độ nhanh hơn ...
supercat

6

"Ngay cả trên máy 32 bit, nếu bạn cho phép chuỗi có kích thước của bộ nhớ khả dụng, chuỗi có tiền tố dài chỉ rộng hơn ba byte so với chuỗi kết thúc null."

Đầu tiên, thêm 3 byte có thể là chi phí đáng kể cho các chuỗi ngắn. Đặc biệt, một chuỗi có độ dài bằng không bây giờ chiếm gấp 4 lần bộ nhớ. Một số người trong chúng ta đang sử dụng máy 64 bit, vì vậy chúng tôi cần 8 byte để lưu trữ chuỗi có độ dài bằng 0 hoặc định dạng chuỗi không thể đối phó với chuỗi dài nhất mà nền tảng hỗ trợ.

Cũng có thể có vấn đề liên kết để giải quyết. Giả sử tôi có một khối bộ nhớ chứa 7 chuỗi, như "solo \ 0second \ 0 \ 0four \ 0five \ 0 \ 0seventh". Chuỗi thứ hai bắt đầu ở offset 5. Phần cứng có thể yêu cầu các số nguyên 32 bit được căn chỉnh tại một địa chỉ là bội số của 4, vì vậy bạn phải thêm phần đệm, tăng thêm chi phí. Các đại diện C là rất hiệu quả bộ nhớ trong so sánh. (Hiệu quả bộ nhớ là tốt; chẳng hạn, nó giúp hiệu năng bộ nhớ cache.)


Tôi tin rằng tôi đã giải quyết tất cả những điều này trong câu hỏi. Có, trên nền tảng x64, tiền tố 32 bit không thể phù hợp với tất cả các chuỗi có thể. Mặt khác, bạn không bao giờ muốn một chuỗi lớn như một chuỗi kết thúc null, bởi vì để làm bất cứ điều gì bạn phải kiểm tra tất cả 4 tỷ byte để tìm ra kết thúc cho hầu hết mọi hoạt động bạn có thể muốn thực hiện. Ngoài ra, tôi không nói rằng các chuỗi kết thúc null luôn luôn xấu - nếu bạn đang xây dựng một trong những cấu trúc khối này và ứng dụng cụ thể của bạn được thúc đẩy bởi kiểu xây dựng đó, hãy thực hiện nó. Tôi chỉ muốn hành vi mặc định của ngôn ngữ đã không làm điều đó.
Billy ONeal

2
Tôi đã trích dẫn một phần câu hỏi của bạn bởi vì theo quan điểm của tôi, nó đánh giá thấp vấn đề hiệu quả. Nhân đôi hoặc tăng gấp bốn lần yêu cầu bộ nhớ (lần lượt là 16 bit và 32 bit) có thể là một chi phí hiệu năng lớn. Chuỗi dài có thể chậm, nhưng ít nhất chúng được hỗ trợ và vẫn hoạt động. Điểm khác của tôi, về sự liên kết, bạn không đề cập gì cả.
Brangdon

Sắp xếp có thể được xử lý bằng cách chỉ định rằng các giá trị ngoài UCHAR_MAX sẽ hoạt động như thể được đóng gói và giải nén bằng cách truy cập byte và dịch chuyển bit. Một loại chuỗi được thiết kế phù hợp có thể mang lại hiệu quả lưu trữ về cơ bản tương đương với các chuỗi kết thúc bằng 0, đồng thời cho phép kiểm tra giới hạn trên bộ đệm để không có chi phí bộ nhớ bổ sung (sử dụng một bit trong tiền tố để nói liệu bộ đệm có "đầy" hay không không phải và byte cuối cùng khác không, byte đó sẽ đại diện cho không gian còn lại. Nếu bộ đệm không đầy và byte cuối cùng bằng 0, thì 256 byte cuối cùng sẽ không được sử dụng, vì vậy ...
supercat

... người ta có thể lưu trữ trong không gian đó số byte không sử dụng chính xác, với chi phí bộ nhớ bổ sung bằng 0). Chi phí làm việc với các tiền tố sẽ được bù đắp bằng khả năng sử dụng các phương thức như fgets () mà không phải vượt qua độ dài chuỗi (vì bộ đệm sẽ biết chúng lớn như thế nào).
supercat

4

Việc chấm dứt null cho phép các hoạt động dựa trên con trỏ nhanh.


5
Huh? "Hoạt động con trỏ nhanh" nào không hoạt động với tiền tố chiều dài? Quan trọng hơn, các ngôn ngữ khác sử dụng tiền tố độ dài nhanh hơn thao tác chuỗi C wrt.
Billy ONeal

12
@billy: Với các chuỗi có tiền tố dài, bạn không thể lấy một con trỏ chuỗi và thêm 4 vào chuỗi đó và hy vọng nó vẫn là một chuỗi hợp lệ, vì dù sao nó không có tiền tố độ dài (dù sao không phải là tiền tố hợp lệ).
Jörgen Sigvardsson

3
@j_random_hacker: Ghép nối kém hơn nhiều đối với chuỗi asciiz (O (m + n) thay vì có khả năng O (n)) và concat phổ biến hơn nhiều so với bất kỳ thao tác nào khác được liệt kê ở đây.
Billy ONeal

3
có một thao tác nhỏ mà trở nên đắt hơn với các chuỗi kết thúc null : strlen. Tôi muốn nói rằng đó là một chút hạn chế.
jalf

10
@Billy ONeal: mọi người khác cũng hỗ trợ regex. Vậy thì sao ? Sử dụng các thư viện đó là những gì họ được tạo ra cho. C là về hiệu quả tối đa và tối giản, không bao gồm pin. Các công cụ C cũng cho phép bạn thực hiện chuỗi Tiền tố dài bằng cách sử dụng các cấu trúc rất dễ dàng. Và không có gì cấm bạn thực hiện các chương trình thao tác chuỗi thông qua việc quản lý bộ đệm char và độ dài của riêng bạn. Đó thường là những gì tôi làm khi tôi muốn hiệu quả và sử dụng C, không gọi một số hàm có số 0 ở cuối bộ đệm char không phải là vấn đề.
kriss

4

Một điểm chưa được đề cập: khi C được thiết kế, có nhiều máy mà 'char' không phải là tám bit (thậm chí ngày nay còn có các nền tảng DSP không có). Nếu một người quyết định rằng các chuỗi phải có tiền tố độ dài, thì nên sử dụng bao nhiêu tiền tố có độ dài char? Sử dụng hai sẽ áp đặt giới hạn nhân tạo cho độ dài chuỗi cho các máy có không gian địa chỉ char 8 bit và 32 bit, trong khi lãng phí không gian trên các máy có char 16 bit và không gian địa chỉ 16 bit.

Nếu người ta muốn cho phép các chuỗi có độ dài tùy ý được lưu trữ hiệu quả và nếu 'char' luôn là 8 bit, thì người ta có thể - với một số chi phí về tốc độ và kích thước mã - xác định một sơ đồ là một chuỗi có tiền tố N sẽ dài N / 2 byte, một chuỗi có tiền tố N và giá trị chẵn M (đọc ngược) có thể là ((N-1) + M * char_max) / 2, v.v. và yêu cầu bất kỳ bộ đệm nào yêu cầu cung cấp một lượng không gian nhất định để giữ một chuỗi phải cho phép đủ byte trước không gian đó để xử lý độ dài tối đa. Tuy nhiên, thực tế là 'char' không phải lúc nào cũng 8 bit, sẽ làm phức tạp một sơ đồ như vậy, vì số lượng 'char' được yêu cầu để giữ độ dài của chuỗi sẽ thay đổi tùy theo kiến ​​trúc CPU.


Tiền tố có thể dễ dàng có kích thước do xác định thực hiện, giống như vậy sizeof(char).
Billy ONeal

@BillyONeal: sizeof(char)là một. Luôn luôn. Người ta có thể có tiền tố là một kích thước được xác định thực hiện, nhưng nó sẽ rất khó xử. Hơn nữa, không có cách nào thực sự để biết kích thước "đúng" nên là gì. Nếu một người đang giữ nhiều chuỗi 4 ký tự, phần đệm bằng 0 sẽ áp đặt 25% phí, trong khi tiền tố có độ dài bốn byte sẽ áp đặt 100% phí. Hơn nữa, thời gian dành cho việc đóng gói và giải nén các tiền tố có độ dài bốn byte có thể vượt quá chi phí quét các chuỗi 4 byte cho byte không.
supercat

1
À, vâng. Bạn đúng. Tiền tố có thể dễ dàng là một cái gì đó khác hơn char. Bất cứ điều gì có thể làm cho các yêu cầu căn chỉnh trên nền tảng đích đều ổn. Tôi sẽ không đến đó mặc dù - tôi đã tranh cãi điều này cho đến chết.
Billy ONeal

Giả sử các chuỗi có tiền tố dài, có lẽ điều chắc chắn nhất sẽ là size_ttiền tố (lãng phí bộ nhớ bị nguyền rủa, nó sẽ là sanest --- cho phép các chuỗi có độ dài bất kỳ có thể phù hợp với bộ nhớ). Trong thực tế, đó là loại gì D làm; mảng là struct { size_t length; T* ptr; }và chuỗi chỉ là mảng của immutable(char).
Tim Čas

@ TimČas: Trừ khi các chuỗi được yêu cầu phải được căn chỉnh từ, chi phí làm việc với các chuỗi ngắn sẽ trên nhiều nền tảng bị chi phối bởi yêu cầu đóng gói và giải nén độ dài; Tôi thực sự không thấy điều đó là thực tế. Nếu người ta muốn các chuỗi là các mảng byte có kích thước tùy ý nội dung, tôi nghĩ sẽ tốt hơn nếu giữ độ dài tách biệt khỏi con trỏ đến dữ liệu ký tự và có một ngôn ngữ cho phép thu được cả hai phần thông tin cho một chuỗi ký tự .
supercat

2

Nhiều quyết định thiết kế xung quanh C xuất phát từ thực tế là khi nó được thực hiện ban đầu, việc truyền tham số có phần tốn kém. Đưa ra lựa chọn giữa ví dụ

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);
}

đấu với

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);
}

cái sau sẽ rẻ hơn một chút (và do đó được ưa thích) vì nó chỉ yêu cầu truyền một tham số chứ không phải hai. Nếu phương thức được gọi không cần biết địa chỉ cơ sở của mảng cũng như chỉ mục bên trong nó, việc truyền một con trỏ kết hợp cả hai sẽ rẻ hơn so với việc chuyển các giá trị riêng rẽ.

Mặc dù có nhiều cách hợp lý để C có thể mã hóa độ dài chuỗi, các cách tiếp cận đã được phát minh cho đến thời điểm đó sẽ có tất cả các hàm bắt buộc có thể hoạt động với một phần của chuỗi để chấp nhận địa chỉ cơ sở của chuỗi và chỉ số mong muốn là hai tham số riêng biệt. Sử dụng kết thúc bằng 0 byte giúp tránh yêu cầu đó. Mặc dù các cách tiếp cận khác sẽ tốt hơn với các máy hiện nay (trình biên dịch hiện đại thường truyền tham số trong các thanh ghi và memcpy có thể được tối ưu hóa theo cách strcpy () - tương đương không thể) đủ mã sản xuất sử dụng các chuỗi kết thúc bằng 0 byte mà khó có thể thay đổi thành bất kỳ thứ gì khác.

PS - Để đổi lấy một hình phạt tốc độ nhẹ đối với một số thao tác và một chút chi phí phụ trên chuỗi dài hơn, có thể có các phương thức hoạt động với chuỗi chấp nhận con trỏ trực tiếp đến chuỗi, bộ đệm chuỗi được kiểm tra giới hạn hoặc cấu trúc dữ liệu xác định các chuỗi con của một chuỗi khác. Một hàm như "strcat" sẽ trông giống như [cú pháp hiện đại]

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);
  }
}

Lớn hơn một chút so với phương pháp strcat K & R, nhưng nó sẽ hỗ trợ kiểm tra giới hạn, điều mà phương pháp K & R không có. Hơn nữa, không giống như phương thức hiện tại, có thể dễ dàng nối một chuỗi con tùy ý, ví dụ:

/* 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);
}

Lưu ý rằng thời gian tồn tại của chuỗi được trả về bởi temp_sub chuỗi sẽ bị giới hạn bởi các chuỗi ssrc, bao giờ ngắn hơn (đó là lý do tại sao phương thức yêu cầu infđược truyền vào - nếu là cục bộ, nó sẽ chết khi phương thức được trả về).

Về chi phí bộ nhớ, các chuỗi và bộ đệm lên đến 64 byte sẽ có một byte trên không (giống như các chuỗi kết thúc bằng 0); các chuỗi dài hơn sẽ có nhiều hơn một chút (cho dù một lượng cho phép giữa hai byte và mức tối đa được yêu cầu sẽ là sự đánh đổi thời gian / không gian). Một giá trị đặc biệt của byte độ dài / chế độ sẽ được sử dụng để chỉ ra rằng hàm chuỗi đã được cung cấp một cấu trúc có chứa byte cờ, con trỏ và độ dài bộ đệm (sau đó có thể lập chỉ mục tùy ý vào bất kỳ chuỗi nào khác).

Tất nhiên, K & R đã không thực hiện bất kỳ điều gì như vậy, nhưng rất có thể là vì họ không muốn dành nhiều nỗ lực để xử lý chuỗi - một lĩnh vực mà ngay cả ngày nay nhiều ngôn ngữ có vẻ khá thiếu máu.


Không có gì có thể ngăn cản việc char* arrchỉ đến một cấu trúc của biểu mẫu struct { int length; char characters[ANYSIZE_ARRAY] };hoặc tương tự mà vẫn có thể vượt qua như một tham số duy nhất.
Billy ONeal

@BillyONeal: Hai vấn đề với cách tiếp cận đó: (1) Nó chỉ cho phép truyền toàn bộ chuỗi, trong khi cách tiếp cận hiện tại cũng cho phép chuyển qua đuôi của chuỗi; (2) nó sẽ lãng phí không gian đáng kể khi được sử dụng với các chuỗi nhỏ. Nếu K & R muốn dành một chút thời gian cho chuỗi, họ có thể đã khiến mọi thứ trở nên mạnh mẽ hơn nhiều, nhưng tôi không nghĩ họ dự định rằng ngôn ngữ mới của họ sẽ được sử dụng mười năm sau, ít hơn bốn mươi.
supercat

1
Một chút về quy ước gọi điện này là một câu chuyện đơn giản không liên quan đến thực tế ... nó không phải là một sự cân nhắc trong thiết kế. Và các quy ước gọi dựa trên đăng ký đã được "phát minh". Ngoài ra, các cách tiếp cận như hai con trỏ không phải là một lựa chọn vì các cấu trúc không phải là lớp đầu tiên ... chỉ các nguyên thủy mới có thể được gán hoặc có thể qua được; sao chép cấu trúc đã không đến cho đến UNIX V7. Cần memcpy (cũng không tồn tại) chỉ để sao chép một con trỏ chuỗi là một trò đùa. Hãy thử viết một chương trình đầy đủ, không chỉ các chức năng riêng biệt, nếu bạn đang giả vờ thiết kế ngôn ngữ.
Jim Balter

1
"Điều đó rất có thể bởi vì họ không muốn dành nhiều nỗ lực cho việc xử lý chuỗi" - vô nghĩa; toàn bộ miền ứng dụng của UNIX ban đầu là xử lý chuỗi. Nếu không phải như vậy, chúng ta sẽ không bao giờ nghe về nó.
Jim Balter

1
'Tôi không nghĩ rằng "bộ đệm char bắt đầu bằng một int chứa chiều dài" sẽ kỳ diệu hơn "- đó là nếu bạn sẽ thực hiện str[n]tham chiếu đến char đúng. Đây là những điều mà những người thảo luận về điều này không nghĩ tới.
Jim Balter

2

Theo Joel Spolsky trong bài đăng trên blog này ,

Đó là bởi vì bộ vi xử lý PDP-7, trên đó ngôn ngữ lập trình UNIX và C được phát minh, có kiểu chuỗi ASCIZ. ASCIZ có nghĩa là "ASCII có chữ Z (không) ở cuối."

Sau khi xem tất cả các câu trả lời khác ở đây, tôi tin chắc rằng ngay cả khi điều này là đúng, đó chỉ là một phần lý do khiến C có "chuỗi" kết thúc null. Bài đăng đó khá rõ ràng về việc những thứ đơn giản như chuỗi thực sự có thể khá khó khăn.


2
Hãy nhìn xem, tôi tôn trọng Joel vì rất nhiều thứ; nhưng đây là thứ mà anh ấy đang suy đoán. Câu trả lời của Hans Passant đến trực tiếp từ các nhà phát minh của C.
Billy ONeal

1
Đúng, nhưng nếu những gì Spolsky nói hoàn toàn đúng, thì đó sẽ là một phần của "sự tiện lợi" mà họ đang đề cập đến. Đó là một phần lý do tại sao tôi bao gồm câu trả lời này.
BenK

AFAIK .ASCIZchỉ là một câu lệnh biên dịch để xây dựng một chuỗi các byte, theo sau là 0. Nó chỉ có nghĩa là chuỗi không kết thúc là một khái niệm được thiết lập tốt tại thời điểm đó. Điều đó không có nghĩa là các chuỗi kết thúc bằng 0 là thứ gì đó liên quan đến kiến ​​trúc của PDP- *, ngoại trừ việc bạn có thể viết các vòng lặp chặt chẽ bao gồm MOVB(sao chép một byte) và BNE(nhánh nếu byte cuối cùng được sao chép không bằng 0).
Adrian W

Nó giả sử cho thấy C là ngôn ngữ cũ, chập chờn, suy đồi.
purec

2

Không nhất thiết phải là Cơ sở lý luận mà là đối trọng với mã hóa chiều dài

  1. Một số hình thức mã hóa chiều dài động là vượt trội so với mã hóa chiều dài tĩnh khi có liên quan đến bộ nhớ, tất cả phụ thuộc vào cách sử dụng. Chỉ cần nhìn vào UTF-8 để chứng minh. Về cơ bản, nó là một mảng ký tự mở rộng để mã hóa một ký tự đơn. Điều này sử dụng một bit cho mỗi byte mở rộng. Chấm dứt NUL sử dụng 8 bit. Tiền tố độ dài Tôi nghĩ có thể được gọi là độ dài vô hạn một cách hợp lý bằng cách sử dụng 64 bit. Tần suất bạn nhấn trường hợp các bit thừa của bạn là yếu tố quyết định. Chỉ có 1 chuỗi cực lớn? Ai quan tâm nếu bạn đang sử dụng 8 hoặc 64 bit? Nhiều chuỗi nhỏ (Ie Chuỗi các từ tiếng Anh)? Sau đó, chi phí tiền tố của bạn là một tỷ lệ lớn.

  2. Chuỗi tiền tố dài cho phép tiết kiệm thời gian không phải là một điều thực sự . Cho dù dữ liệu được cung cấp của bạn được yêu cầu có độ dài được cung cấp, bạn đang tính vào thời gian biên dịch hoặc bạn thực sự được cung cấp dữ liệu động mà bạn phải mã hóa dưới dạng chuỗi. Các kích thước này được tính toán tại một số điểm trong thuật toán. Một biến riêng biệt để lưu trữ kích thước của chuỗi kết thúc null có thể được cung cấp. Mà làm cho sự so sánh về moot tiết kiệm thời gian. Một người chỉ có thêm một NUL ở cuối ... nhưng nếu mã hóa độ dài không bao gồm NUL đó thì thực sự không có sự khác biệt giữa hai. Không có thay đổi thuật toán nào cả. Chỉ cần vượt qua trước, bạn phải tự thiết kế thay vì có trình biên dịch / thời gian chạy làm việc đó cho bạn. C chủ yếu là về làm việc thủ công.

  3. Tiền tố chiều dài là tùy chọn là một điểm bán hàng. Tôi không luôn cần thông tin bổ sung đó cho một thuật toán vì vậy việc được yêu cầu thực hiện nó cho mọi chuỗi làm cho thời gian tính toán trước + tính toán của tôi không bao giờ có thể giảm xuống dưới O (n). (Tức là trình tạo số ngẫu nhiên phần cứng 1-128. Tôi có thể lấy từ một "chuỗi vô hạn". Giả sử nó chỉ tạo ra các ký tự quá nhanh. Vì vậy, độ dài chuỗi của chúng tôi thay đổi mọi lúc. Nhưng cách sử dụng dữ liệu của tôi có thể không quan tâm làm thế nào Tôi có rất nhiều byte ngẫu nhiên. Nó chỉ muốn byte không được sử dụng tiếp theo ngay khi có thể nhận được nó sau một yêu cầu. Tôi có thể đợi trên thiết bị. Nhưng tôi cũng có thể có một bộ đệm các ký tự được đọc trước. một sự lãng phí không cần thiết của tính toán. Kiểm tra null hiệu quả hơn.)

  4. Tiền tố dài là một bảo vệ tốt chống tràn bộ đệm? Vậy là sử dụng lành mạnh các chức năng thư viện và thực hiện. Nếu tôi chuyển dữ liệu không đúng định dạng thì sao? Bộ đệm của tôi dài 2 byte nhưng tôi nói với chức năng đó là 7! Ví dụ: Nếu get () được dự định sẽ được sử dụng trên dữ liệu đã biết, nó có thể đã có kiểm tra bộ đệm nội bộ đã kiểm tra bộ đệm đã biên dịch và malloc ()gọi và vẫn theo spec. Nếu nó được sử dụng như một đường ống để STDIN không xác định đến được vùng đệm không xác định thì rõ ràng người ta không thể biết kích thước bộ đệm có nghĩa là độ dài arg là vô nghĩa, bạn cần một thứ khác ở đây như kiểm tra chim hoàng yến. Đối với vấn đề đó, bạn không thể tiền tố dài một số luồng và đầu vào, bạn không thể. Điều đó có nghĩa là kiểm tra độ dài phải được tích hợp vào thuật toán và không phải là một phần ma thuật của hệ thống gõ. TL; DR NUL chấm dứt không bao giờ phải không an toàn, nó chỉ kết thúc theo cách đó thông qua việc sử dụng sai.

  5. điểm truy cập: chấm dứt NUL gây khó chịu trên hệ nhị phân. Bạn cần phải thực hiện tiền tố độ dài ở đây hoặc chuyển đổi các byte NUL theo một cách nào đó: mã thoát, ánh xạ lại phạm vi, v.v ... tất nhiên có nghĩa là sử dụng nhiều bộ nhớ / giảm thông tin / nhiều thao tác hơn trên mỗi byte. Tiền tố dài chủ yếu chiến thắng cuộc chiến ở đây. Ưu điểm duy nhất của một phép biến đổi là không có hàm bổ sung nào phải được viết để bao trùm các chuỗi tiền tố có độ dài. Điều đó có nghĩa là trên các thói quen phụ O (n) được tối ưu hóa hơn của bạn, bạn có thể để chúng tự động hoạt động như các tương đương O (n) của chúng mà không cần thêm mã. Nhược điểm là, tất nhiên, lãng phí thời gian / bộ nhớ / nén khi được sử dụng trên các chuỗi nặng NUL.Tùy thuộc vào số lượng thư viện mà bạn kết thúc sao chép để hoạt động trên dữ liệu nhị phân, có thể có ý nghĩa khi chỉ hoạt động với các chuỗi tiền tố dài. Điều đó nói rằng người ta cũng có thể làm tương tự với các chuỗi tiền tố dài ... -1 độ dài có thể có nghĩa là chấm dứt NUL và bạn có thể sử dụng các chuỗi kết thúc NUL bên trong chấm dứt độ dài.

  6. Concat: "O (n + m) vs O (m)" Tôi giả sử bạn coi m là tổng chiều dài của chuỗi sau khi nối bởi vì cả hai đều phải có số lượng hoạt động tối thiểu (bạn không thể giải quyết -trên chuỗi 1, nếu bạn phải realloc thì sao?). Và tôi cho rằng n là số lượng hoạt động thần thoại mà bạn không còn phải thực hiện vì tính toán trước. Nếu vậy, thì câu trả lời rất đơn giản: tiền điện toán. Nếubạn đang khẳng định rằng bạn sẽ luôn có đủ bộ nhớ để không cần phải phân bổ lại và đó là cơ sở của ký hiệu big-O thì câu trả lời thậm chí còn đơn giản hơn: thực hiện tìm kiếm nhị phân trên bộ nhớ được phân bổ cho cuối chuỗi 1, rõ ràng có rất nhiều swatch các số 0 vô hạn sau chuỗi 1 để chúng ta không phải lo lắng về realloc. Ở đó, dễ dàng có n để đăng nhập (n) và tôi hầu như không thử. Mà nếu bạn nhớ lại log (n) về cơ bản chỉ lớn bằng 64 trên một máy tính thực, về cơ bản giống như nói O (64 + m), về cơ bản là O (m). (Và vâng, logic đó đã được sử dụng trong phân tích thời gian thực của các cấu trúc dữ liệu thực tế đang sử dụng ngày nay. Nó không nhảm nhí trên đỉnh đầu của tôi.)

  7. Concat () / Len () lần nữa : Ghi nhớ kết quả. Dễ dàng. Biến tất cả các tính toán thành tiền tính toán nếu có thể / cần thiết. Đây là một quyết định thuật toán. Đó không phải là một ràng buộc bắt buộc của ngôn ngữ.

  8. Chuỗi hậu tố đi qua dễ dàng hơn / có thể với chấm dứt NUL. Tùy thuộc vào cách triển khai tiền tố dài, nó có thể bị phá hủy trên chuỗi gốc và đôi khi thậm chí không thể thực hiện được. Yêu cầu một bản sao và vượt qua O (n) thay vì O (1).

  9. Đối số chuyển qua / bỏ tham chiếu ít hơn đối với tiền tố NUL kết thúc so với tiền tố dài. Rõ ràng bởi vì bạn đang truyền ít thông tin hơn. Nếu bạn không cần chiều dài, thì điều này sẽ tiết kiệm rất nhiều dấu chân và cho phép tối ưu hóa.

  10. Bạn có thể gian lận. Nó thực sự chỉ là một con trỏ. Ai nói bạn phải đọc nó như một chuỗi? Điều gì nếu bạn muốn đọc nó như là một nhân vật duy nhất hoặc nổi? Điều gì nếu bạn muốn làm ngược lại và đọc một float như một chuỗi? Nếu bạn cẩn thận, bạn có thể làm điều này với chấm dứt NUL. Bạn không thể làm điều này với tiền tố độ dài, đó là loại dữ liệu khác biệt với con trỏ thông thường. Bạn rất có thể phải xây dựng một chuỗi byte theo byte và có được độ dài. Tất nhiên nếu bạn muốn một cái gì đó giống như toàn bộ float (có thể có NUL bên trong nó) thì bạn phải đọc từng byte một, nhưng các chi tiết còn lại để bạn quyết định.

TL; DR Bạn đang sử dụng dữ liệu nhị phân? Nếu không, thì chấm dứt NUL cho phép tự do thuật toán hơn. Nếu có, thì số lượng mã so với tốc độ / bộ nhớ / nén là mối quan tâm chính của bạn. Một sự pha trộn của hai cách tiếp cận hoặc ghi nhớ có thể là tốt nhất.


9 là loại không căn cứ / đại diện sai. Chiều dài sửa trước không có vấn đề này. Lenth đi như một biến riêng biệt nào. Chúng tôi đã nói về pre-fiix nhưng tôi đã mang đi. Vẫn còn một điều tốt để suy nghĩ vì vậy tôi sẽ để nó ở đó. : d
Đen

1

Tôi không mua câu trả lời "C không có chuỗi". Đúng, C không hỗ trợ các loại mức cao hơn tích hợp nhưng bạn vẫn có thể biểu diễn các cấu trúc dữ liệu trong C và đó là chuỗi. Thực tế một chuỗi chỉ là một con trỏ trong C không có nghĩa là N byte đầu tiên không thể có ý nghĩa đặc biệt như độ dài.

Các nhà phát triển Windows / COM sẽ rất quen thuộc với BSTRkiểu giống hệt như thế này - một chuỗi C có tiền tố dài, trong đó dữ liệu ký tự thực tế bắt đầu không ở byte 0.

Vì vậy, dường như quyết định sử dụng chấm dứt null chỉ đơn giản là những gì mọi người ưa thích, không phải là một sự cần thiết của ngôn ngữ.


-3

gcc chấp nhận các mã dưới đây:

char s [4] = "abcd";

và nó ổn nếu chúng ta coi là một mảng ký tự nhưng không phải là chuỗi. Đó là, chúng ta có thể truy cập nó với s [0], s [1], s [2] và s [3] hoặc thậm chí với memcpy (mệnh, s, 4). Nhưng chúng ta sẽ nhận được các ký tự lộn xộn khi chúng ta cố gắng đặt (s) hoặc tệ hơn với strcpy (mệnh, s).


@Adrian W. Đây là hợp lệ C. Chuỗi độ dài chính xác được đặt trong trường hợp đặc biệt và NUL được bỏ qua cho chúng. Đây thường là một cách thực hành không khôn ngoan nhưng có thể hữu ích trong các trường hợp như cấu trúc tiêu đề dân cư sử dụng "chuỗi" FourCC.
Kevin Thibedeau

Bạn đúng rồi. Đây là C hợp lệ, sẽ biên dịch và hành xử như kkaaii mô tả. Lý do cho các downvote (không phải của tôi ...) có lẽ là câu trả lời này không trả lời câu hỏi của OP theo bất kỳ cách nào.
Adrian W
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.