Các chức năng của thư viện C có luôn mong đợi độ dài của chuỗi không?


15

Tôi hiện đang làm việc trên một thư viện được viết bằng C. Nhiều chức năng của thư viện này mong đợi một chuỗi như char*hoặc const char*trong các đối số của chúng. Tôi đã bắt đầu với các hàm đó luôn mong đợi độ dài của chuỗi size_tvì không yêu cầu chấm dứt null. Tuy nhiên, khi viết bài kiểm tra, điều này dẫn đến việc sử dụng thường xuyên strlen(), như vậy:

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

Việc tin tưởng người dùng vượt qua các chuỗi kết thúc đúng sẽ dẫn đến mã kém an toàn hơn, nhưng ngắn gọn hơn và (theo ý kiến ​​của tôi):

libFunction("I hope there's a null-terminator there!");

Vì vậy, những gì thực hành hợp lý ở đây? Làm cho API phức tạp hơn để sử dụng, nhưng buộc người dùng phải nghĩ đến đầu vào của họ hoặc ghi lại yêu cầu cho chuỗi kết thúc null và tin tưởng người gọi?

Câu trả lời:


4

Chắc chắn nhất và hoàn toàn mang theo chiều dài xung quanh . Thư viện C tiêu chuẩn bị phá vỡ một cách khét tiếng theo cách này, điều này đã không gây ra kết thúc đau đớn trong việc xử lý tràn bộ đệm. Cách tiếp cận này là trọng tâm của rất nhiều thù hận và nỗi thống khổ mà các trình biên dịch hiện đại sẽ thực sự cảnh báo, than vãn và phàn nàn khi sử dụng các chức năng thư viện tiêu chuẩn loại này.

Thật tệ, đến nỗi nếu bạn từng gặp câu hỏi này trong một cuộc phỏng vấn - và người phỏng vấn kỹ thuật của bạn có vẻ như anh ta có một vài năm kinh nghiệm - nhiệt tâm thuần túy có thể hạ cánh công việc - bạn thực sự có thể tiến xa hơn nếu bạn có thể trích dẫn tiền lệ của việc bắn ai đó thực hiện API tìm kiếm bộ kết thúc chuỗi C.

Bỏ cảm xúc của nó sang một bên, có rất nhiều điều có thể sai với NULL ở cuối chuỗi của bạn, trong cả việc đọc và thao tác với nó - cộng với việc nó thực sự vi phạm trực tiếp các khái niệm thiết kế hiện đại như phòng thủ chuyên sâu (không nhất thiết phải áp dụng cho bảo mật, nhưng cho thiết kế API). Ví dụ về API C mang theo chiều dài rất nhiều - ví dụ. API Windows.

Trên thực tế, vấn đề này đã được giải quyết vào khoảng những năm 90, sự đồng thuận mới nổi ngày nay là bạn thậm chí không nên chạm vào chuỗi của mình .

Chỉnh sửa sau : đây là một cuộc tranh luận trực tiếp, vì vậy tôi sẽ thêm rằng tin tưởng mọi người bên dưới và bên trên bạn là tốt và sử dụng các chức năng str * của thư viện là OK, cho đến khi bạn thấy những thứ cổ điển như output = malloc(strlen(input)); strcpy(output, input);hoặc while(*src) { *dest=transform(*src); dest++; src++; }. Tôi gần như có thể nghe Lacrimosa của Mozart trong nền.


1
Tôi không hiểu ví dụ của bạn về API Windows yêu cầu người gọi cung cấp độ dài của chuỗi. Ví dụ: một hàm Win32 API điển hình như CreateFilelấy LPTCSTR lpFileNametham số làm đầu vào. Không có độ dài của chuỗi được mong đợi từ người gọi. Trong thực tế, việc sử dụng các chuỗi kết thúc NUL đã ăn sâu đến mức tài liệu thậm chí không đề cập đến việc tên tệp phải bị chấm dứt NUL (nhưng tất nhiên phải như vậy).
Greg Hewgill

1
Trên thực tế trong Win32, LPSTRkiểu này nói rằng các chuỗi có thể bị chấm dứt NUL và nếu không , điều đó sẽ được chỉ định trong thông số kỹ thuật liên quan. Vì vậy, trừ khi có chỉ định cụ thể khác, các chuỗi như vậy trong Win32 dự kiến ​​sẽ bị chấm dứt NUL.
Greg Hewgill

Điểm tuyệt vời, tôi đã không chính xác. Hãy xem xét rằng CreatFile và bó của nó có từ thời Windows NT 3.1 (đầu những năm 90); API hiện tại (tức là kể từ khi giới thiệu Strsafe.h trong XP SP2 - với lời xin lỗi công khai của Microsoft) đã phản đối rõ ràng tất cả những thứ bị chấm dứt NULL có thể. Lần đầu tiên Microsoft cảm thấy thực sự xin lỗi vì đã sử dụng các chuỗi kết thúc NULL thực sự sớm hơn nhiều, khi họ phải giới thiệu BSTR trong đặc tả OLE 2.0, để bằng cách nào đó đưa VB, COM và WINAPI cũ vào cùng một chiếc thuyền.
vski

1
Ngay cả trong StringCbCatví dụ, chỉ có đích có bộ đệm tối đa, có ý nghĩa. Các nguồn vẫn là một NUL-chấm dứt chuỗi C thông thường. Có lẽ bạn có thể cải thiện câu trả lời của mình bằng cách làm rõ sự khác biệt giữa tham số đầu vào và tham số đầu ra . Các tham số đầu ra phải luôn có chiều dài bộ đệm tối đa; tham số đầu vào thường là chấm dứt NUL (có những trường hợp ngoại lệ, nhưng hiếm khi theo kinh nghiệm của tôi).
Greg Hewgill

1
Đúng. Các chuỗi là bất biến trên cả JVM / Dalvik và .NET CLR ở cấp độ nền tảng, cũng như trong nhiều ngôn ngữ khác. Tôi sẽ đi xa hơn và suy đoán rằng thế giới bản địa chưa thể thực hiện được điều này (tiêu chuẩn C ++ 11) vì một) di sản (bạn không thực sự đạt được nhiều như vậy khi chỉ có một phần của chuỗi bất biến) và b ) bạn thực sự cần một bảng GC và bảng chuỗi để thực hiện công việc này, các bộ cấp phát có phạm vi trong C ++ 11 không thể cắt nó.
vski

16

Trong C, thành ngữ là các chuỗi ký tự bị chấm dứt NUL, do đó, việc tuân thủ thông lệ thông thường - thực sự không chắc là người dùng thư viện sẽ có các chuỗi không kết thúc NUL (vì chúng cần thêm công việc để in sử dụng printf và sử dụng trong bối cảnh khác). Sử dụng bất kỳ loại chuỗi nào khác là không tự nhiên và có lẽ tương đối hiếm.

Ngoài ra, trong các trường hợp, thử nghiệm của bạn có vẻ hơi kỳ lạ đối với tôi, vì để hoạt động chính xác (sử dụng strlen), bạn đang giả sử một chuỗi kết thúc NUL ở vị trí đầu tiên. Bạn nên kiểm tra trường hợp các chuỗi không kết thúc NUL nếu bạn dự định thư viện của bạn sẽ làm việc với chúng.


-1, tôi xin lỗi, điều này chỉ đơn giản là khuyên.
vski

Ngày xưa, điều này không phải lúc nào cũng đúng. Tôi đã làm việc rất nhiều với các giao thức nhị phân đặt dữ liệu chuỗi trong các trường có độ dài cố định mà NULL không bị chấm dứt. Trong những trường hợp như vậy, nó rất thuận tiện để làm việc với các chức năng mất nhiều thời gian. Tôi đã không làm C trong một thập kỷ, mặc dù.
Gort Robot

4
@vski, làm thế nào để buộc người dùng gọi 'strlen' trước khi gọi hàm mục tiêu làm bất cứ điều gì để tránh các vấn đề tràn bộ đệm? Ít nhất nếu bạn tự kiểm tra độ dài trong hàm mục tiêu, bạn có thể tự tin về cảm giác độ dài nào đang được sử dụng (bao gồm cả thiết bị đầu cuối null hay không).
Charles E. Grant

@Charles E. Grant: Xem bình luận ở trên về StringCbCat và StringCbCatN trong Strsafe.h. Nếu bạn chỉ có char * và không có độ dài, thì thực sự bạn không có lựa chọn thực sự nào ngoài việc sử dụng các hàm str *, nhưng vấn đề là phải mang theo chiều dài, do đó nó trở thành một tùy chọn giữa str * và strn * chức năng mà sau này được ưa thích.
vski

2
@vski Không cần phải vượt qua độ dài của chuỗi . Có một nhu cầu để vượt qua xung quanh một bộ đệm chiều dài 's. Không phải tất cả các bộ đệm đều là chuỗi và không phải tất cả các chuỗi đều là bộ đệm.
jamesdlin

10

Đối số "an toàn" của bạn không thực sự giữ. Nếu bạn không tin tưởng người dùng trao cho bạn một chuỗi kết thúc không có giá trị khi đó là những gì bạn đã ghi lại (và "chuẩn mực" cho đồng bằng C), bạn không thể thực sự tin tưởng vào độ dài mà họ cung cấp cho bạn (mà họ sẽ có thể nhận được bằng cách sử dụng strlengiống như bạn đang làm nếu họ không có nó và sẽ thất bại nếu "chuỗi" không phải là một chuỗi ở vị trí đầu tiên).

Có nhiều lý do hợp lệ để yêu cầu độ dài mặc dù: nếu bạn muốn các chức năng của mình hoạt động trên các chuỗi con, có thể dễ dàng hơn (và hiệu quả) để vượt qua một độ dài so với việc người dùng thực hiện một số phép thuật sao chép qua lại để có được byte null ở đúng nơi (và có nguy cơ xảy ra lỗi trên đường đi).
Có thể xử lý các mã hóa trong đó các byte null không phải là kết thúc hoặc có thể xử lý các chuỗi có null null (có mục đích) có thể hữu ích trong một số trường hợp (phụ thuộc vào chức năng của bạn làm gì).
Khả năng xử lý dữ liệu không kết thúc (mảng có độ dài cố định) cũng rất tiện lợi.
Tóm lại: phụ thuộc vào những gì bạn đang làm trong thư viện của bạn và loại dữ liệu bạn mong muốn người dùng của bạn sẽ xử lý.

Cũng có thể có một khía cạnh hiệu suất cho việc này. Nếu chức năng của bạn cần biết trước độ dài của chuỗi và bạn mong muốn người dùng của mình ít nhất thường biết thông tin đó, việc họ vượt qua nó (thay vì bạn tính toán nó) có thể cạo một vài chu kỳ.

Nhưng nếu thư viện của bạn mong đợi các chuỗi văn bản ASCII đơn giản thông thường và bạn không gặp phải các hạn chế về hiệu suất và hiểu rất rõ về cách người dùng của bạn sẽ tương tác với thư viện của bạn, việc thêm một tham số độ dài nghe có vẻ không phải là một ý tưởng hay. Nếu chuỗi không được kết thúc đúng cách, rất có thể tham số độ dài sẽ chỉ là không có thật. Tôi không nghĩ bạn sẽ đạt được nhiều với nó.


Rất không đồng ý với phương pháp này. Không bao giờ tin tưởng người gọi của bạn, đặc biệt là đằng sau API thư viện, hãy nỗ lực hết sức để đặt câu hỏi về những thứ họ đưa cho bạn và thất bại một cách duyên dáng. Mang theo chiều dài mờ nhạt, làm việc với các chuỗi kết thúc NULL không phải là "lỏng lẻo với người gọi của bạn và nghiêm ngặt với callees của bạn" nghĩa là gì.
vski

2
Tôi đồng ý chủ yếu với vị trí của bạn, nhưng bạn dường như đặt nhiều niềm tin vào cuộc tranh luận về chiều dài đó - không có lý do gì khiến nó đáng tin cậy hơn kẻ hủy diệt null. Vị trí của tôi là nó phụ thuộc vào những gì thư viện làm.
Mat

Có rất nhiều điều có thể sai với bộ kết thúc NULL trong chuỗi hơn là độ dài được truyền theo giá trị. Trong C, lý do duy nhất khiến người ta tin tưởng vào độ dài là vì nó không hợp lý và không thực tế nếu không mang theo chiều dài bộ đệm không phải là một câu trả lời hay, chỉ là cách tốt nhất để xem xét các phương án. Đó là một trong những lý do tại sao các chuỗi (và bộ đệm nói chung) được đóng gói gọn gàng và được gói gọn trong các ngôn ngữ RAD.
vski

2

Số chuỗi luôn được kết thúc bằng null theo định nghĩa, độ dài chuỗi là dự phòng.

Dữ liệu ký tự không kết thúc không bao giờ được gọi là "chuỗi". Việc xử lý nó (và ném độ dài xung quanh) thường phải được gói gọn trong thư viện và không phải là một phần của API. Yêu cầu độ dài làm tham số chỉ để tránh các lệnh gọi strlen () có khả năng Tối ưu hóa sớm.

Tin tưởng người gọi hàm API không an toàn ; hành vi không xác định là hoàn toàn ok nếu điều kiện tiên quyết không được đáp ứng.

Tất nhiên, API được thiết kế tốt không nên chứa những cạm bẫy và giúp dễ dàng sử dụng chính xác. Và điều này chỉ có nghĩa là nó phải đơn giản và dễ hiểu nhất có thể, tránh sự dư thừa và tuân theo các quy ước của ngôn ngữ.


không chỉ hoàn toàn ổn, mà thực sự không thể tránh khỏi trừ khi người ta chuyển sang ngôn ngữ đơn luồng, an toàn cho bộ nhớ. Có thể đã giảm một số hạn chế cần thiết hơn ...
Ded repeatator

1

Bạn nên luôn luôn giữ chiều dài của bạn xung quanh. Đối với một, người dùng của bạn có thể muốn chứa NULL trong đó. Và thứ hai, đừng quên đó strlenlà O (N) và yêu cầu chạm vào toàn bộ bộ đệm tạm biệt chuỗi. Và thứ ba, nó giúp việc vượt qua các tập hợp con dễ dàng hơn - ví dụ, chúng có thể cho ít hơn chiều dài thực tế.


4
Liệu hàm thư viện có xử lý các NULL nhúng trong chuỗi hay không cần phải được ghi lại rất tốt. Hầu hết các chức năng thư viện C dừng lại ở NULL hoặc độ dài, tùy theo cái nào đến trước. (Và nếu được viết thành thạo, những người không mất thời gian sẽ không bao giờ sử dụng strlentrong bài kiểm tra vòng lặp.)
Gort the Robot

1

Bạn nên phân biệt giữa việc truyền xung quanh một chuỗi và chuyển qua một bộ đệm .

Trong C, các chuỗi được kết thúc NUL theo truyền thống. Nó là hoàn toàn hợp lý để mong đợi điều này. Do đó, thường không cần phải vượt qua độ dài của chuỗi; nó có thể được tính toán strlennếu cần thiết.

Khi đi qua một bộ đệm , đặc biệt là một bộ đệm được ghi vào, thì bạn hoàn toàn nên chuyển dọc theo kích thước bộ đệm. Đối với bộ đệm đích, điều này cho phép callee đảm bảo rằng nó không tràn bộ đệm. Đối với bộ đệm đầu vào, nó cho phép callee tránh đọc quá cuối, đặc biệt nếu bộ đệm đầu vào chứa dữ liệu tùy ý có nguồn gốc từ một nguồn không đáng tin cậy.

Có lẽ có một số nhầm lẫn bởi vì cả chuỗi và bộ đệm đều có thể char*và bởi vì rất nhiều hàm chuỗi tạo ra chuỗi mới bằng cách ghi vào bộ đệm đích. Một số người sau đó kết luận rằng các hàm chuỗi nên có độ dài chuỗi. Tuy nhiên, đây là một kết luận không chính xác. Việc thực hành bao gồm một kích thước với một bộ đệm (cho dù bộ đệm đó được sử dụng cho chuỗi, mảng số nguyên, cấu trúc, bất cứ thứ gì) là một câu thần chú hữu ích và tổng quát hơn.

(Trong trường hợp đọc một chuỗi từ một nguồn không tin cậy (ví dụ: ổ cắm mạng), điều quan trọng là cung cấp độ dài vì đầu vào có thể không bị chấm dứt NUL. Tuy nhiên , bạn không nên coi đầu vào là một chuỗi. nên coi nó như một bộ đệm dữ liệu tùy ý có thể chứa một chuỗi (nhưng bạn không biết cho đến khi bạn thực sự xác nhận nó), vì vậy điều này vẫn tuân theo nguyên tắc là bộ đệm nên có kích thước liên quan và chuỗi đó không cần chúng.)


Đây chính xác là những gì câu hỏi và câu trả lời khác bỏ lỡ.
Blrfl

0

Nếu các chức năng chủ yếu được sử dụng với chuỗi ký tự, thì nỗi đau của việc xử lý độ dài rõ ràng có thể được giảm thiểu bằng cách xác định một số macro. Ví dụ: được cung cấp một hàm API:

void use_string(char *string, int length);

người ta có thể định nghĩa một macro:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

và sau đó gọi nó như thể hiện trong:

void test(void)
{
  use_strlit("Hello");
}

Mặc dù có thể đưa ra những thứ "sáng tạo" để vượt qua macro sẽ biên dịch nhưng thực tế sẽ không hoạt động, việc sử dụng ""một trong hai bên của chuỗi trong đánh giá "sizeof" sẽ bắt gặp những nỗ lực vô tình để sử dụng ký tự các con trỏ khác với các chuỗi ký tự phân tách [trong trường hợp không có các ký tự chuỗi "", một nỗ lực để vượt qua một con trỏ ký tự sẽ đưa ra độ dài là kích thước của một con trỏ, trừ đi một chiều.

Một cách tiếp cận khác trong C99 sẽ là xác định loại cấu trúc "con trỏ và chiều dài" và xác định một macro chuyển đổi một chuỗi ký tự thành một nghĩa đen của loại cấu trúc đó. Ví dụ:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

Lưu ý rằng nếu sử dụng một cách tiếp cận như vậy, người ta nên chuyển các cấu trúc như vậy theo giá trị thay vì chuyển qua địa chỉ của chúng. Nếu không thì đại loại như:

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

có thể thất bại vì thời gian tồn tại của chữ ghép sẽ kết thúc ở phần cuối của câu lệnh kèm theo.

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.