2009-11-27 43 views
96

Câu hỏi này đi ra ngoài để các bậc thầy C ra có:C con trỏ: trỏ đến một mảng có kích thước cố định

Trong C, chúng ta có thể khai báo một con trỏ như sau:

char (* p)[10]; 

.. mà về cơ bản nói rằng con trỏ này trỏ đến một mảng gồm 10 ký tự. Điều gọn gàng về việc khai báo một con trỏ như thế này là bạn sẽ nhận được một lỗi thời gian biên dịch nếu bạn cố gắng gán một con trỏ của một mảng có kích thước khác với p. Nó cũng sẽ cung cấp cho bạn một lỗi thời gian biên dịch nếu bạn cố gắng gán giá trị của một con trỏ char đơn giản đến p. Tôi đã thử điều này với gcc và nó có vẻ làm việc với ANSI, C89 và C99.

Dường như với tôi, việc khai báo một con trỏ như thế này sẽ rất hữu ích - đặc biệt là khi chuyển con trỏ tới hàm. Thông thường, mọi người sẽ viết mẫu thử nghiệm của một chức năng như thế này:

void foo(char * p, int plen); 

Nếu bạn mong đợi một bộ đệm có kích thước cụ thể, bạn chỉ cần kiểm tra giá trị của plen. Tuy nhiên, bạn không thể được đảm bảo rằng người chuyển p cho bạn sẽ thực sự cung cấp cho bạn các vị trí bộ nhớ hợp lệ trên bộ đệm đó. Bạn phải tin tưởng rằng người gọi chức năng này đang làm điều đúng. Mặt khác:

void foo(char (*p)[10]); 

.. buộc người gọi phải cung cấp cho bạn bộ đệm có kích thước được chỉ định.

Điều này có vẻ rất hữu ích nhưng tôi chưa bao giờ thấy một con trỏ được khai báo như thế này trong bất kỳ mã nào tôi đã từng chạy qua.

Câu hỏi của tôi là: Có lý do nào khiến mọi người không khai báo con trỏ như thế này không? Tôi không thấy một số lỗ hổng rõ ràng?

+1

lưu ý: Kể từ C99 mảng không nhất thiết phải có kích cỡ cố định theo đề nghị của danh hiệu, '10' có thể được thay thế bằng bất kỳ biến trong phạm vi –

Trả lời

7

Tôi muốn để thêm vào câu trả lời AndreyT của (trong trường hợp bất cứ ai tình cờ gặp trang này tìm kiếm để biết thêm về chủ đề này):

Như Tôi bắt đầu chơi nhiều hơn với những tuyên bố này, tôi nhận ra rằng có điểm chấp lớn liên quan đến chúng trong C (dường như không phải trong C++). Nó là khá phổ biến để có một tình huống mà bạn muốn cung cấp cho một người gọi một con trỏ const đến một bộ đệm bạn đã viết vào. Thật không may, điều này là không thể khi tuyên bố một con trỏ như thế này trong C.Nói cách khác, tiêu chuẩn C (6.7.3 - Đoạn 8) là mâu thuẫn với một cái gì đó như thế này:


    int array[9]; 

    const int (* p2)[9] = &array; /* Not legal unless array is const as well */ 

chế này dường như không có mặt trong C++, làm cho những loại tờ khai xa hữu ích hơn. Nhưng trong trường hợp của C, nó là cần thiết để rơi trở lại một khai báo con trỏ thường xuyên bất cứ khi nào bạn muốn một con trỏ const đến bộ đệm kích thước cố định (trừ khi bản thân bộ đệm đã được khai báo const để bắt đầu với). Bạn có thể tìm thêm thông tin trong chuỗi thư này: link text

Đây là một hạn chế nghiêm trọng trong quan điểm của tôi và có thể là một trong những lý do chính khiến mọi người thường không khai báo con trỏ như thế này trong C. hầu hết mọi người thậm chí không biết rằng bạn có thể khai báo một con trỏ như thế này như AndreyT đã chỉ ra.

+1

Điều đó dường như là một vấn đề cụ thể của trình biên dịch. Tôi đã có thể sao chép bằng cách sử dụng gcc 4.9.1, nhưng clang 3.4.2 đã có thể đi từ một không const để const phiên bản không có vấn đề. Tôi đã đọc thông số C11 (p 9 trong phiên bản của tôi ... phần nói về hai loại đủ điều kiện tương thích) và đồng ý rằng dường như các chuyển đổi này là bất hợp pháp. Tuy nhiên, chúng tôi biết trong thực tế rằng bạn luôn có thể tự động chuyển đổi từ char * sang char const * mà không cần cảnh báo. IMO, clang nhất quán hơn trong việc cho phép điều này hơn gcc, mặc dù tôi đồng ý với bạn rằng thông số kỹ thuật dường như cấm bất kỳ chuyển đổi tự động nào trong số này. –

4

Lý do rõ ràng là mã này không biên dịch:

extern void foo(char (*p)[10]); 
void bar() { 
    char p[10]; 
    foo(p); 
} 

Chương trình khuyến mãi mặc định của một mảng là một con trỏ không đủ tiêu chuẩn.

Đồng thời xem this question, sử dụng foo(&p) sẽ hoạt động.

+0

Tất nhiên foo (p) sẽ không làm việc, foo đang yêu cầu một con trỏ tới một mảng gồm 10 phần tử, do đó bạn cần phải chuyển địa chỉ của mảng ... –

+7

Làm thế nào mà "lý do hiển nhiên"? Đó là, rõ ràng, hiểu rằng cách thích hợp để gọi hàm là 'foo (& p)'. – AnT

+2

Tôi đoán "hiển nhiên" là từ sai. Tôi có nghĩa là "đơn giản nhất". Sự khác biệt giữa p và & p trong trường hợp này khá mơ hồ đối với lập trình viên trung bình C. Một người nào đó đang cố gắng làm những gì mà người viết đề xuất sẽ viết những gì tôi viết, nhận được một lỗi biên dịch thời gian, và bỏ cuộc. –

0

Có lẽ tôi đang thiếu một cái gì đó, nhưng ... vì mảng là con trỏ liên tục, về cơ bản điều đó có nghĩa là không có điểm nào trong việc truyền xung quanh con trỏ tới chúng.

Bạn không thể sử dụng void foo(char p[10], int plen);?

+3

Mảng không phải là con trỏ liên tục. Đọc một số câu hỏi thường gặp về mảng. – AnT

+2

Đối với những gì quan trọng ở đây (mảng đơn hướng như các tham số), thực tế là chúng phân rã thành các con trỏ liên tục. Đọc Câu hỏi thường gặp về cách ít gây hiểu lầm hơn. – fortran

1

Vâng, chỉ cần đặt, C không làm những việc theo cách đó. Một mảng kiểu T được chuyển xung quanh dưới dạng con trỏ tới số T đầu tiên trong mảng và đó là tất cả những gì bạn nhận được.

Điều này cho phép một số thuật toán mát mẻ và thanh lịch, chẳng hạn như vòng lặp qua mảng với các biểu thức như

*dst++ = *src++ 

Nhược điểm là quản lý của kích thước là tùy thuộc vào bạn. Thật không may, việc không thực hiện điều này một cách tận tâm cũng đã dẫn đến hàng triệu lỗi trong mã hóa C và/hoặc cơ hội cho việc khai thác ác ý.

Điều gì đến gần với những gì bạn yêu cầu trong C là để vượt qua một struct (theo giá trị) hoặc một con trỏ đến một (theo tham chiếu).Miễn là cùng loại cấu trúc được sử dụng trên cả hai mặt của hoạt động này, cả hai mã phân phát tham chiếu và mã sử dụng tham chiếu đều thỏa thuận về kích thước của dữ liệu đang được xử lý.

Cấu trúc của bạn có thể chứa bất kỳ dữ liệu nào bạn muốn; nó có thể chứa mảng của bạn có kích thước được xác định rõ.

Tuy nhiên, không có gì ngăn cản bạn hoặc một coder không đủ năng lực hoặc ác ý sử dụng phôi để đánh lừa trình biên dịch xử lý cấu trúc của bạn như một kích thước khác. Khả năng gần như không giải quyết được để làm điều này là một phần của thiết kế của C.

1

Bạn có thể khai báo một mảng các ký tự một số cách sau:

char p[10]; 
char* p = (char*)malloc(10 * sizeof(char)); 

Nguyên mẫu để một chức năng mà phải mất một mảng theo giá trị là:

void foo(char* p); //cannot modify p 

hoặc bằng cách tham khảo:

void foo(char** p); //can modify p, derefernce by *p[0] = 'f'; 

hoặc theo cú pháp mảng:

void foo(char p[]); //same as char* 
+1

Đừng quên rằng một mảng có kích thước cố định cũng có thể được phân bổ động dưới dạng 'char (* p) [10] = malloc (sizeof * p)'. – AnT

+0

Xem ở đây để có một cuộc thảo luận chi tiết hơn về sự khác biệt của mảng char [] và char * ptr tại đây. http://stackoverflow.com/questions/1807530/difference-between-using-character-pointers-and-character-arrays/1807766#1807766 – t0mm13b

138

Điều bạn đang nói trong bài đăng của mình là hoàn toàn chính xác. Tôi muốn nói rằng mọi nhà phát triển C đều có cùng một khám phá và kết luận chính xác khi (nếu) họ đạt được trình độ thông thạo nhất định với ngôn ngữ C.

Khi các chi tiết cụ thể trong vùng ứng dụng của bạn gọi một mảng có kích thước cố định cụ thể (kích thước mảng là hằng số biên dịch), cách duy nhất để truyền một mảng như vậy đến hàm là bằng cách sử dụng con trỏ tới mảng tham số

void foo(char (*p)[10]); 

(bằng ngôn ngữ C++ này cũng được thực hiện với sự tham khảo

void foo(char (&p)[10]); 

).

Điều này sẽ bật kiểm tra loại cấp độ ngôn ngữ, sẽ đảm bảo rằng mảng chính xác kích thước được cung cấp dưới dạng đối số. Trong thực tế, trong nhiều trường hợp người sử dụng kỹ thuật này mặc nhiên, mà không hề nhận ra nó, ẩn kiểu mảng đằng sau một tên typedef

typedef int Vector3d[3]; 

void transform(Vector3d *vector); 
/* equivalent to `void transform(int (*vector)[3])` */ 
... 
Vector3d vec; 
... 
transform(&vec); 

Lưu ý thêm rằng mã trên là bất biến với liên quan đến Vector3d loại là một mảng hoặc một struct. Bạn có thể chuyển định nghĩa của Vector3d bất kỳ lúc nào từ một mảng thành struct và ngược lại, và bạn sẽ không phải thay đổi khai báo hàm. Trong cả hai trường hợp, các hàm sẽ nhận được một đối tượng tổng hợp "bằng cách tham chiếu" (có những ngoại lệ cho điều này, nhưng trong bối cảnh của cuộc thảo luận này điều này là đúng). Tuy nhiên, bạn sẽ không thấy phương pháp này được sử dụng một cách rõ ràng quá thường xuyên, đơn giản vì quá nhiều người bị lẫn lộn bởi một cú pháp khá phức tạp và đơn giản là không đủ thoải mái với các tính năng của ngôn ngữ C để sử dụng chúng đúng cách. Vì lý do này, trong thực tế trung bình cuộc sống, đi qua một mảng như một con trỏ đến yếu tố đầu tiên của nó là một cách tiếp cận phổ biến hơn. Nó chỉ trông "đơn giản hơn". Nhưng trên thực tế, việc sử dụng con trỏ đến phần tử đầu tiên để chuyển mảng là một kỹ thuật rất thích hợp, một mẹo, phục vụ một mục đích rất cụ thể: mục đích duy nhất của nó là tạo điều kiện cho mảng đi qua của kích thước khác nhau (I Ekích thước thời gian chạy). Nếu bạn thực sự cần phải có khả năng xử lý mảng kích thước thời gian chạy, sau đó một cách thích hợp để vượt qua một mảng như vậy là bởi một con trỏ đến phần tử đầu tiên của mình với kích thước bê tông được cung cấp bởi một tham số bổ sung

void foo(char p[], unsigned plen); 

Trên thực tế , trong nhiều trường hợp, nó rất hữu ích để có thể xử lý các mảng kích thước thời gian chạy, cũng góp phần vào sự phổ biến của phương thức. Nhiều nhà phát triển C chỉ đơn giản là không bao giờ gặp phải (hoặc không bao giờ nhận ra) sự cần thiết phải xử lý một mảng có kích thước cố định, do đó vẫn không biết đến kỹ thuật kích thước cố định thích hợp.

Tuy nhiên, nếu kích thước mảng là cố định, đi qua nó như một con trỏ đến một yếu tố

void foo(char p[]) 

là một lỗi kỹ thuật cấp lớn, mà tiếc là khá phổ biến những ngày này. Một kỹ thuật con trỏ tới mảng là một cách tiếp cận tốt hơn nhiều trong các trường hợp như vậy.

Một lý do khác có thể cản trở việc áp dụng kỹ thuật truyền mảng cố định là sự thống trị của cách tiếp cận ngây thơ để nhập mảng được phân bổ động. Ví dụ, nếu chương trình đòi hỏi mảng cố định kiểu char[10] (như trong ví dụ của bạn), một nhà phát triển trung bình sẽ malloc mảng như

char *p = malloc(10 * sizeof *p); 

mảng này không thể được truyền cho một chức năng khai báo là

void foo(char (*p)[10]); 

gây nhầm lẫn cho nhà phát triển trung bình và khiến họ từ bỏ tuyên bố tham số kích thước cố định mà không phải suy nghĩ thêm. Trong thực tế, gốc rễ của vấn đề nằm trong cách tiếp cận ngây thơ malloc. Định dạng malloc được hiển thị ở trên phải được dành riêng cho các mảng có kích thước thời gian chạy. Nếu kiểu mảng có thời gian biên dịch kích thước, một cách tốt hơn để malloc nó sẽ trông như sau

char (*p)[10] = malloc(sizeof *p); 

này, tất nhiên, có thể dễ dàng truyền cho trên tuyên bố foo

foo(p); 

và trình biên dịch sẽ thực hiện kiểm tra kiểu thích hợp. Nhưng một lần nữa, điều này là quá khó hiểu với một nhà phát triển C không chuẩn bị, đó là lý do tại sao bạn sẽ không thấy nó quá thường xuyên trong mã trung bình hàng ngày "điển hình".

+4

Câu trả lời rất hay và phức tạp. –

+1

@Johannes: Bạn đang châm biếm, phải không? Có nhiều lời lăng mạ che giấu trong câu trả lời đó để gọi nó là "tốt đẹp" :))) – AnT

+0

@AndreyT: Câu trả lời rất hay. Cảm ơn bạn đã dành thời gian để xây dựng trên đó. – figurassa

1

tôi sẽ không khuyên bạn nên giải pháp này

typedef int Vector3d[3]; 

vì nó che khuất một thực tế rằng Vector3D có một kiểu mà bạn phải biết. Các lập trình viên thường không mong đợi các biến số của cùng loại để có các kích thước khác nhau. Xem xét:!

void foo(Vector3d a) { 
    Vector3D b; 
} 

nơi sizeof a = sizeof b

+0

Ông không đề xuất điều này như một giải pháp. Ông chỉ đơn giản là sử dụng điều này như là một ví dụ. – figurassa

-1

Trên trình biên dịch (vs2008), nó xử lý char (*p)[10] dưới dạng một chuỗi ký tự con trỏ, như thể không có dấu ngoặc đơn, ngay cả khi tôi biên dịch dưới dạng tệp C. Trình biên dịch có hỗ trợ cho "biến" này không? Nếu vậy đó là một lý do chính để không sử dụng nó.

+0

-1 Sai. Nó hoạt động tốt trên vs2008, vs2010, gcc.Đặc biệt ví dụ này hoạt động tốt: http://stackoverflow.com/a/19208364/2333290 – kotlomoy

1

Tôi cũng muốn sử dụng cú pháp này để cho phép kiểm tra loại hơn.

Nhưng tôi cũng đồng ý rằng cú pháp và mô hình tinh thần của việc sử dụng con trỏ đơn giản hơn và dễ nhớ hơn.

Dưới đây là một số trở ngại mà tôi đã gặp phải.

  • Tiếp cận mảng đòi hỏi sử dụng (*p)[]:

    void foo(char (*p)[10]) 
    { 
        char c = (*p)[3]; 
        (*p)[0] = 1; 
    } 
    

    Nó được cám dỗ sử dụng một con trỏ-to-char địa phương thay vì:

    void foo(char (*p)[10]) 
    { 
        char *cp = (char *)p; 
        char c = cp[3]; 
        cp[0] = 1; 
    } 
    

    Nhưng điều này một phần sẽ đánh bại mục đích của sử dụng đúng loại.

  • Người ta phải nhớ để sử dụng địa chỉ-của nhà điều hành khi gán địa chỉ của một mảng để một con trỏ-to-mảng:

    char a[10]; 
    char (*p)[10] = &a; 
    

    Địa chỉ-của nhà điều hành được địa chỉ của toàn bộ mảng trong &a , với loại chính xác để gán nó cho p. Không có toán tử, a được tự động chuyển đổi thành địa chỉ của phần tử đầu tiên của mảng, giống như trong &a[0], có loại khác.

    Vì chuyển đổi tự động này đã diễn ra, tôi luôn luôn bối rối rằng cần &. Nó phù hợp với việc sử dụng & trên các biến khác, nhưng tôi phải nhớ rằng một mảng là đặc biệt và tôi cần & để có được loại địa chỉ chính xác, mặc dù giá trị địa chỉ giống nhau.

    Một lý do cho vấn đề của tôi có thể là tôi đã học được K & R C trong thập niên 80, không cho phép sử dụng toán tử & trên toàn bộ mảng (mặc dù một số trình biên dịch đã bỏ qua hoặc chấp nhận cú pháp). Mà, bằng cách này, có thể là một lý do khác tại sao con trỏ tới mảng có thời gian khó nhận: chúng chỉ hoạt động bình thường kể từ ANSI C và giới hạn của toán tử & có thể là một lý do khác để cho rằng chúng quá khó xử.

  • Khi typedefkhông sử dụng để tạo ra một kiểu cho mảng con trỏ-to-(trong một tập tin tiêu đề phổ biến), sau đó một mảng con trỏ-to-toàn cầu cần có một extern khai phức tạp hơn để chia sẻ nó trên các tập tin :

    fileA: 
    char (*p)[10]; 
    
    fileB: 
    extern char (*p)[10]; 
    
Các vấn đề liên quan