Con trỏ C: trỏ đến một mảng có kích thước cố định


118

Câu hỏi này được đưa ra cho các bậc thầy về C:

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

char (* p)[10];

.. 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 khi khai báo một con trỏ như thế này là bạn sẽ gặp lỗi thời gian biên dịch nếu bạn cố gán một con trỏ của một mảng có kích thước khác nhau cho 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án giá trị của một con trỏ char đơn giản cho p. Tôi đã thử điều này với gcc và nó dường như hoạt động với ANSI, C89 và C99.

Đối 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, khi chuyển một con trỏ tới một hàm. Thông thường, mọi người sẽ viết nguyên mẫu của một chức năng như thế này:

void foo(char * p, int plen);

Nếu bạn đang 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ệ trong bộ đệm đó. Bạn phải tin tưởng rằng người gọi chức năng này đang làm đúng. Mặt khác:

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

..có thể buộc người gọi cung cấp cho bạn một 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ó bất kỳ lý do tại sao mọi người không tuyên bố con trỏ như thế này? Tôi không thấy một số cạm bẫy rõ ràng?


3
lưu ý: Vì C99, mảng không phải có kích thước cố định như đề xuất của tiêu đề, 10có thể được thay thế bằng bất kỳ biến nào trong phạm vi
MM

Câu trả lời:


172

Những gì bạn đang nói trong bài viết của bạn 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à đưa ra kết luận chính xác như nhau khi (nếu) họ đạt đến mức độ thành thạo nhất định với ngôn ngữ C.

Khi các chi tiết cụ thể của khu vực ứ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ố thời gian biên dịch), cách duy nhất thích hợp để truyền một mảng như vậy cho một hàm là sử dụng tham số con trỏ đến mảng

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

(trong ngôn ngữ C ++, điều này cũng được thực hiện với các tài liệu tham khảo

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

).

Điều này sẽ cho phép kiểm tra loại cấp độ ngôn ngữ, điều này sẽ đảm bảo rằng mảng có kích thước chính xác được cung cấp dưới dạng đối số. Trong thực tế, trong nhiều trường hợp, mọi người sử dụng kỹ thuật này một cách ngầm định, thậm chí không nhận ra nó, ẩn kiểu mảng phía 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);

Ngoài ra, lưu ý rằng đoạn mã trên là bất biến liên quan đến Vector3dkiểu là một mảng hoặc a struct. Bạn có thể chuyển định nghĩa Vector3dbất cứ lúc nào từ một mảng sang một structtrở 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ó trường hợp 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 thức truyền mảng này được sử dụng quá thường xuyên, đơn giản vì quá nhiều người bị nhầm 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 như vậy của ngôn ngữ C để sử dụng chúng đúng cách. Vì lý do này, trong cuộc sống thực trung bình, việc truyền một mảng như một con trỏ đến phần 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 trong thực tế, sử dụng con trỏ đến phần tử đầu tiên để truyề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 các mảng có kích thước khác nhau (tức là kích thước thời gian chạy) . Nếu bạn thực sự cần có khả năng xử lý các mảng có kích thước thời gian chạy, thì cách thích hợp để vượt qua một mảng như vậy là bằng một con trỏ đến phần tử đầu tiên của nó với kích thước cụ thể đượ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, rất hữu ích để có thể xử lý các mảng có kích thước thời gian chạy, điều nà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) nhu cầu xử lý một mảng có kích thước cố định, do đó vẫn không biết gì về 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, chuyển nó dưới dạng con trỏ đến một phần tử

void foo(char p[])

là một lỗi cấp độ kỹ thuật lớn, không may là khá phổ biến ngày nay. Một kỹ thuật con trỏ đến mảng là một cách tiếp cận tốt hơn nhiều trong những 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 chuyển mảng có kích thước cố định là sự thống trị của cách tiếp cận ngây thơ đối với việc gõ các mảng được phân bổ động. Ví dụ: nếu chương trình gọi các mảng cố định loại char[10](như trong ví dụ của bạn), một nhà phát triển trung bình sẽ có malloccác mảng như

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

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

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

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

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

Điều này, tất nhiên, có thể dễ dàng được chuyển đến tuyên bố ở trên foo

foo(p);

và trình biên dịch sẽ thực hiện kiểm tra loại thích hợp. Nhưng một lần nữa, điều này quá khó hiểu với một nhà phát triển C chưa 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".


2
Câu trả lời cung cấp một mô tả rất súc tích và đầy thông tin về cách sizeof () thành công, mức độ thường xuyên thất bại và cách nó luôn thất bại. quan sát của bạn về hầu hết các kỹ sư C / C ++ không hiểu và do đó, làm điều gì đó mà họ nghĩ họ hiểu thay vào đó là một trong những điều tiên tri hơn mà tôi đã thấy trong một lúc, và tấm màn che không là gì so với độ chính xác mà nó mô tả. Nghiêm túc đấy, thưa ngài. câu trả lời chính xác.
WhozCraig

Tôi chỉ tái cấu trúc một số mã dựa trên câu trả lời này, bravo và cảm ơn cho cả Q và A.
Perry

1
Tôi tò mò muốn biết làm thế nào bạn xử lý consttài sản với kỹ thuật này. Một const char (*p)[N]đối số dường như không tương thích với một con trỏ đến char table[N];Ngược lại, một char*ptr đơn giản vẫn tương thích với một const char*đối số.
Cyan

4
Có thể hữu ích để lưu ý rằng để truy cập một phần tử của mảng của bạn, bạn cần phải làm (*p)[i]và không *p[i]. Cái sau sẽ nhảy theo kích thước của mảng, gần như chắc chắn không phải là thứ bạn muốn. Ít nhất là đối với tôi, việc học cú pháp này gây ra, thay vì ngăn chặn, là một sai lầm; Tôi sẽ nhận được mã chính xác nhanh hơn chỉ cần vượt qua một dấu phẩy *.
Andrew Wagner

1
Có @mickey, những gì bạn đã đề xuất là một constcon trỏ tới một mảng các phần tử có thể thay đổi. Và vâng, điều này hoàn toàn khác nhau từ một con trỏ đến một mảng các yếu tố bất biến.
Cyan

11

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

Khi tôi bắt đầu chơi nhiều hơn với các tuyên bố này, tôi nhận ra rằng có một sự bất lợi lớn liên quan đến chúng trong C (dường như không có trong C ++). Điều khá phổ biến là có một tình huống mà bạn muốn cung cấp cho người gọi một con trỏ const tới bộ đệm mà bạn đã ghi vào. Thật không may, điều này là không thể khi khai báo 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 */

Ràng buộc này dường như không có trong C ++, làm cho các kiểu khai báo này hữu ích hơn nhiều. Nhưng trong trường hợp của C, cần quay lại khai báo con trỏ thông thường bất cứ khi nào bạn muốn một con trỏ const tới bộ đệm có kích thước cố định (trừ khi chính bộ đệm được khai báo const bắt đầu). Bạn có thể tìm thêm thông tin trong chuỗi thư này: liên kết văn bản

Đây là một hạn chế nghiêm trọng theo quan điểm của tôi và nó có thể là một trong những lý do chính tại sao mọi người thường không khai báo các con trỏ như thế này trong C. Điều khác là 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.


2
Đó 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 gcc 4.9.1, nhưng clang 3.4.2 có thể chuyển từ phiên bản không thành const thành không có vấn đề. Tôi đã đọc thông số kỹ thuật 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 có vẻ như các chuyển đổi này là bất hợp pháp. Tuy nhiên, trong thực tế, chúng tôi biế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 phù hợp 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.
Doug Richardson

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

Quảng cáo mặc định của một mảng là một con trỏ không đủ tiêu chuẩn.

Cũng thấy câu hỏi này , sử dụng foo(&p)nên làm việc.


3
Tất nhiên foo (p) sẽ không hoạt động, 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 của bạn ...
Brian R. Bondy

8
Làm thế nào mà "lý do rõ ràng"? Rõ ràng, đó là cách hiểu đúng để gọi hàm là foo(&p).
AnT

3
Tôi đoán "rõ ràng" 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 C trung bình. Ai đó đang cố gắng làm những gì người đăng đề nghị sẽ viết những gì tôi đã viết, nhận được lỗi biên dịch và bỏ cuộc.
Keith Randall

1

Chà, đơn giản thôi, C không làm mọi thứ theo cách đó. Một mảng kiểu Tđược truyền xung quanh dưới dạng con trỏ đến đầu tiên Ttrong 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 thú vị và thanh lịch, chẳng hạn như lặp qua mảng với các biểu thức như

*dst++ = *src++

Nhược điểm là quản lý kích thước là tùy thuộc vào bạn. Thật không may, việc không làm đ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ác cơ hội khai thác xấu.

Những gì gần với những gì bạn yêu cầu trong C là chuyển 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 một kiểu cấu trúc được sử dụng cho cả hai mặt của thao tác này, cả mã phát ra tham chiếu và mã sử dụng nó đều phù hợp với kích thước của dữ liệu được xử lý.

Cấu trúc của bạn có thể chứa bất cứ 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 lập trình viên không đủ năng lực hoặc ác ý sử dụng các diễn viên để đánh lừa trình biên dịch coi cấu trúc của bạn là một trong các kích thước khác nhau. Khả năng gần như không bị xáo trộn để làm điều này là một phần trong thiết kế của C.


1

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

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

Nguyên mẫu cho một hàm lấy 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*

2
Đừ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 như char (*p)[10] = malloc(sizeof *p).
AnT

Xem ở đây để 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. stackoverflow.com/questions/1807530/
Mạnh

1

Tôi sẽ không đề xuất giải pháp này

typedef int Vector3d[3];

vì nó che khuất thực tế rằng Vector3D có một loại 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 cùng loại có kích thước khác nhau. Xem xét :

void foo(Vector3d a) {
   Vector3D b;
}

trong đó sizeof a! = sizeof b


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

Hừm. Tại sao sizeof(a)không giống như sizeof(b)?
sherrellbc

1

Tôi cũng muốn sử dụng cú pháp này để cho phép kiểm tra nhiều 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 hơn tôi đã đi qua.

  • Truy cập mảng yêu cầu sử dụng (*p)[]:

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

    Thay vào đó, thật hấp dẫn khi sử dụng một con trỏ cục bộ để thay thế:

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

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

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

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

    Địa chỉ của toán tử lấy địa chỉ của toàn bộ mảng &a, với loại chính xác để gán cho nó p. Không có toán tử, asẽ tự động được 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ó một kiểu khác.

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

    Một lý do cho vấn đề của tôi có thể là tôi đã học K & R C từ những năm 80, điều này chưa 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 điều đó hoặc chấp nhận cú pháp). Nhân tiện, đây có thể là một lý do khác khiến các con trỏ trở nên khó chấp nhận: chúng chỉ hoạt động chính xác kể từ ANSI C, và &giới hạn toán tử có thể là một lý do khác để cho rằng chúng quá vụng về.

  • Khi typedefđược khô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 phức tạp hơn externkhai để chia sẻ nó trên các tập tin:

    fileA:
    char (*p)[10];
    
    fileB:
    extern char (*p)[10];

0

Có thể tôi đang thiếu một cái gì đó, nhưng ... vì các 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 để chuyển xung quanh con trỏ tới chúng.

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


4
Mảng KHÔNG phải là con trỏ không đổi. Đọc một số câu hỏi thường gặp về mảng, xin vui lòng.
AnT

2
Đối với những gì quan trọng ở đây (mảng một chiều là tham số), thực tế là chúng phân rã thành con trỏ không đổi. Xin vui lòng đọc một câu hỏi thường gặp về làm thế nào để ít pedantic.
fortran

-2

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


1
-1 Sai. Nó hoạt động tốt trên vs2008, vs2010, gcc. Cụ thể, ví dụ này hoạt động tốt: stackoverflow.com/a/19208364/2333290
kotlomoy
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.