Mảng để phân rã con trỏ là gì?


384

Mảng để phân rã con trỏ là gì? Có bất kỳ liên quan đến con trỏ mảng?


73
ít được biết đến: Toán tử cộng đơn có thể được sử dụng như một "toán tử phân rã": Được cho int a[10]; int b(void);, sau đó +alà một con trỏ int và +blà một con trỏ hàm. Hữu ích nếu bạn muốn chuyển nó đến một mẫu chấp nhận tham chiếu.
Johannes Schaub - litb

3
@litb - parens sẽ làm tương tự (ví dụ: (a) nên là một biểu thức ước lượng cho một con trỏ), phải không?.
Michael Burr

21
std::decaytừ C ++ 14 sẽ là một cách ít phân rã hơn một mảng trên unary +.
huyền thoại2k

21
@ JohannesSchaub-litb vì câu hỏi này được gắn thẻ cả C và C ++, tôi muốn làm rõ rằng mặc dù +a+blà hợp pháp trong C ++, nhưng đó là bất hợp pháp trong C (C11 6.5.3.3/1 "Toán hạng của đơn vị +hoặc -toán tử sẽ có loại số học ")
MM

5
@itic Đúng. Nhưng tôi cho rằng đó không phải là ít được biết đến như là mẹo với unary +. Lý do tôi đề cập đến nó không chỉ đơn thuần là vì nó phân rã mà vì nó là một thứ thú vị để chơi cùng;)
Johannes Schaub - litb

Câu trả lời:


283

Người ta nói rằng mảng "phân rã" thành con trỏ. Một mảng C ++ được khai báo là int numbers [5]không thể được trỏ lại, tức là bạn không thể nói numbers = 0x5a5aff23. Quan trọng hơn, thuật ngữ phân rã có nghĩa là mất loại và kích thước; numbersphân rã int*bằng cách mất thông tin kích thước (số 5) và loại không int [5]còn nữa. Nhìn vào đây để biết trường hợp sâu răng không xảy ra .

Nếu bạn truyền một mảng theo giá trị, những gì bạn thực sự đang làm là sao chép một con trỏ - một con trỏ tới phần tử đầu tiên của mảng được sao chép vào tham số (kiểu của nó cũng phải là con trỏ kiểu của phần tử mảng). Điều này hoạt động do tính chất phân rã của mảng; một khi đã phân rã, sizeofkhông còn cho kích thước của mảng hoàn chỉnh, vì về cơ bản nó trở thành một con trỏ. Đây là lý do tại sao nó được ưu tiên (trong số các lý do khác) để vượt qua bằng tham chiếu hoặc con trỏ.

Ba cách để vượt qua trong mảng 1 :

void by_value(const T* array)   // const T array[] means the same
void by_pointer(const T (*array)[U])
void by_reference(const T (&array)[U])

Hai cái cuối cùng sẽ cung cấp sizeofthông tin chính xác, trong khi cái đầu tiên sẽ không vì đối số mảng đã phân rã để được gán cho tham số.

1 Hằng số U nên được biết tại thời gian biên dịch.


8
Làm thế nào là lần đầu tiên đi qua giá trị?
rlbond

10
by_value đang chuyển một con trỏ đến phần tử đầu tiên của mảng; trong bối cảnh của các tham số chức năng, T a[]là giống hệt với T *a. by_pulum đang chuyển cùng một thứ, ngoại trừ giá trị con trỏ hiện đủ điều kiện const. Nếu bạn muốn truyền một con trỏ tới mảng (trái ngược với một con trỏ đến phần tử đầu tiên của mảng), cú pháp là T (*array)[U].
John Bode

4
"với một con trỏ rõ ràng đến mảng đó" - điều này không chính xác. Nếu alà một mảng của char, thì athuộc loại char[N]và sẽ phân rã thành char*; nhưng &alà loại char(*)[N], và sẽ không phân rã.
Pavel Minaev

5
@FredOverflow: Vì vậy, nếu Uthay đổi bạn không cần phải thay đổi nó ở hai nơi, hoặc có nguy cơ lỗi im lặng ... Tự chủ!
Các cuộc đua nhẹ nhàng trong quỹ đạo

4
"Nếu bạn chuyển một mảng theo giá trị, điều bạn thực sự đang làm là sao chép một con trỏ" Điều đó vô nghĩa, bởi vì các mảng không thể được truyền theo giá trị, thời gian.
juanchopanza

103

Mảng về cơ bản giống như con trỏ trong C / C ++, nhưng không hoàn toàn. Khi bạn chuyển đổi một mảng:

const int a[] = { 2, 3, 5, 7, 11 };

vào một con trỏ (hoạt động mà không cần truyền, và do đó có thể xảy ra bất ngờ trong một số trường hợp):

const int* p = a;

bạn mất khả năng của sizeoftoán tử để đếm các phần tử trong mảng:

assert( sizeof(p) != sizeof(a) );  // sizes are not equal

Khả năng bị mất này được gọi là "phân rã".

Để biết thêm chi tiết, hãy xem bài viết này về phân rã mảng .


51
Mảng không cơ bản giống như con trỏ; chúng là những động vật hoàn toàn khác nhau. Trong hầu hết các bối cảnh, một mảng có thể được coi như là một con trỏ và một con trỏ có thể được xử lý như thể đó là một mảng, nhưng nó gần như chúng có được.
John Bode

20
@ John, xin hãy tha thứ cho ngôn ngữ thiếu chính xác của tôi. Tôi đã cố gắng để có được câu trả lời mà không bị sa lầy vào một câu chuyện dài dòng, và "về cơ bản ... nhưng không hoàn toàn" là một lời giải thích tốt như tôi từng học đại học. Tôi chắc rằng bất cứ ai quan tâm đều có thể có được một bức tranh chính xác hơn từ bình luận được đánh giá cao của bạn.
hệ thống PAUSE

"Hoạt động mà không cần truyền" có nghĩa giống như "xảy ra ngầm" khi nói về chuyển đổi loại
MM

47

Đây là những gì tiêu chuẩn nói (C99 6.3.2.1/3 - Các toán hạng khác - Giá trị, mảng và chỉ định hàm):

Ngoại trừ khi đó là toán hạng của toán tử sizeof hoặc toán tử unary & toán tử hoặc là một chuỗi ký tự được sử dụng để khởi tạo một mảng, một biểu thức có kiểu '' mảng kiểu '' được chuyển đổi thành một biểu thức có con trỏ kiểu '' thành gõ '' trỏ đến phần tử ban đầu của đối tượng mảng và không phải là giá trị.

Điều này có nghĩa là khá nhiều bất cứ lúc nào tên mảng được sử dụng trong một biểu thức, nó được tự động chuyển đổi thành một con trỏ đến mục thứ 1 trong mảng.

Lưu ý rằng tên hàm hoạt động theo cách tương tự, nhưng con trỏ hàm được sử dụng ít hơn nhiều và theo cách chuyên biệt hơn nhiều, nó không gây ra sự nhầm lẫn nhiều như việc chuyển đổi tự động tên mảng thành con trỏ.

Tiêu chuẩn C ++ (4.2 Chuyển đổi mảng sang con trỏ) nới lỏng yêu cầu chuyển đổi thành (nhấn mạnh của tôi):

Có thể chuyển đổi một giá trị hoặc giá trị của mảng kiểu dữ liệu của NT NT hoặc mảng có giới hạn không xác định của TÊ có thể được chuyển đổi thành một giá trị của con trỏ kiểu loại thành T.

Vì vậy, việc chuyển đổi không xảy ra như nó khá nhiều luôn luôn làm trong C (điều này cho phép các chức năng quá tải hoặc mẫu nào khớp vào loại mảng).

Đây cũng là lý do tại sao trong C, bạn nên tránh sử dụng các tham số mảng trong các nguyên mẫu / định nghĩa hàm (theo ý kiến ​​của tôi - tôi không chắc có bất kỳ thỏa thuận chung nào không). Chúng gây nhầm lẫn và dù sao cũng là hư cấu - sử dụng các tham số con trỏ và sự nhầm lẫn có thể không biến mất hoàn toàn, nhưng ít nhất khai báo tham số không nói dối.


2
Một dòng mã ví dụ trong đó "biểu thức có kiểu 'mảng kiểu'" là "một chuỗi ký tự được sử dụng để khởi tạo một mảng" là gì?
Garrett

4
@Garrett char x[] = "Hello";. Mảng 6 phần tử "Hello"không phân rã; thay vào đó xcó kích thước 6và các phần tử của nó được khởi tạo từ các phần tử của "Hello".
MM

30

"Decay" dùng để chuyển đổi ngầm định một biểu thức từ kiểu mảng sang kiểu con trỏ. Trong hầu hết các bối cảnh, khi trình biên dịch nhìn thấy một biểu thức mảng, nó chuyển đổi loại biểu thức từ "mảng phần tử N của T" thành "con trỏ thành T" và đặt giá trị của biểu thức thành địa chỉ của phần tử đầu tiên của mảng . Các ngoại lệ cho quy tắc này là khi một mảng là toán hạng của toán tử sizeofhoặc &toán tử hoặc mảng là một chuỗi ký tự được sử dụng làm bộ khởi tạo trong khai báo.

Giả sử mã sau:

char a[80];
strcpy(a, "This is a test");

Biểu thức acó kiểu "mảng 80 phần tử của char" và biểu thức "Đây là một bài kiểm tra" thuộc loại "mảng 16 phần tử của char" (trong C; trong chuỗi C ++ là các mảng của const char). Tuy nhiên, trong lệnh gọi strcpy(), không biểu thức nào là toán hạng của sizeofhoặc &, do đó các kiểu của chúng được chuyển đổi hoàn toàn thành "con trỏ thành char" và các giá trị của chúng được đặt thành địa chỉ của phần tử đầu tiên trong mỗi. Những gì strcpy()nhận được không phải là mảng, mà là con trỏ, như đã thấy trong nguyên mẫu của nó:

char *strcpy(char *dest, const char *src);

Đây không phải là điều tương tự như một con trỏ mảng. Ví dụ:

char a[80];
char *ptr_to_first_element = a;
char (*ptr_to_array)[80] = &a;

Cả hai ptr_to_first_elementptr_to_arraycó cùng giá trị ; địa chỉ cơ sở của a. Tuy nhiên, chúng là các loại khác nhau và được xử lý khác nhau, như được hiển thị dưới đây:

a[i] == ptr_to_first_element[i] == (*ptr_to_array)[i] != *ptr_to_array[i] != ptr_to_array[i]

Hãy nhớ rằng biểu thức a[i]được hiểu là *(a+i)(chỉ hoạt động nếu kiểu mảng được chuyển đổi thành loại con trỏ), vì vậy cả hai a[i]ptr_to_first_element[i]hoạt động như nhau. Biểu thức (*ptr_to_array)[i]được hiểu là *(*a+i). Các biểu thức *ptr_to_array[i]ptr_to_array[i]có thể dẫn đến cảnh báo trình biên dịch hoặc lỗi tùy thuộc vào ngữ cảnh; họ chắc chắn sẽ làm điều sai trái nếu bạn mong đợi họ đánh giá a[i].

sizeof a == sizeof *ptr_to_array == 80

Một lần nữa, khi một mảng là toán hạng của sizeofnó, nó không được chuyển đổi thành kiểu con trỏ.

sizeof *ptr_to_first_element == sizeof (char) == 1
sizeof ptr_to_first_element == sizeof (char *) == whatever the pointer size
                                                  is on your platform

ptr_to_first_element là một con trỏ đơn giản để char.


1
Không phải là "This is a test" is of type "16-element array of char"một "15-element array of char"? (dài 14 + 1 cho \ 0)
chux - Tái lập lại

16

Mảng, trong C, không có giá trị.

Bất cứ nơi nào giá trị của một đối tượng được mong đợi nhưng đối tượng là một mảng, địa chỉ của phần tử đầu tiên của nó được sử dụng thay thế, với kiểu pointer to (type of array elements).

Trong một hàm, tất cả các tham số được truyền theo giá trị (mảng không phải là ngoại lệ). Khi bạn truyền một mảng trong hàm, nó "phân rã thành một con trỏ" (sic); khi bạn so sánh một mảng với một thứ khác, một lần nữa nó "phân rã thành một con trỏ" (sic); ...

void foo(int arr[]);

Hàm foo mong đợi giá trị của một mảng. Nhưng, trong C, mảng không có giá trị! Vì vậy, foothay vì địa chỉ của phần tử đầu tiên của mảng.

int arr[5];
int *ip = &(arr[1]);
if (arr == ip) { /* something; */ }

Trong so sánh ở trên, arrkhông có giá trị, vì vậy nó trở thành một con trỏ. Nó trở thành một con trỏ tới int. Con trỏ đó có thể được so sánh với biến ip.

Trong cú pháp lập chỉ mục mảng mà bạn thường thấy, một lần nữa, mảng bị 'phân rã thành một con trỏ'

arr[42];
/* same as *(arr + 42); */
/* same as *(&(arr[0]) + 42); */

Lần duy nhất một mảng không phân rã thành một con trỏ là khi nó là toán hạng của toán tử sizeof hoặc toán tử & (toán tử 'địa chỉ của toán tử) hoặc như một chuỗi ký tự được sử dụng để khởi tạo một mảng ký tự.


5
"Mảng không có giá trị" - nghĩa là gì? Tất nhiên các mảng có giá trị ... chúng là các đối tượng, bạn có thể có con trỏ và trong C ++, các tham chiếu đến chúng, v.v.
Pavel Minaev

2
Tôi tin rằng, đúng, "Giá trị" được định nghĩa trong C là cách giải thích các bit của đối tượng theo một loại. Tôi có một thời gian khó khăn để tìm ra một ý nghĩa hữu ích của điều đó với một kiểu mảng. Thay vào đó, bạn có thể nói rằng bạn chuyển đổi thành một con trỏ, nhưng điều đó không diễn giải nội dung của mảng, nó chỉ lấy vị trí của nó. Những gì bạn nhận được là giá trị của một con trỏ (và đó là một địa chỉ), không phải giá trị của một mảng (đây sẽ là "chuỗi các giá trị của các mục được chứa", như được sử dụng trong định nghĩa của "chuỗi"). Điều đó nói rằng, tôi nghĩ thật công bằng khi nói "giá trị của mảng" khi một nghĩa là con trỏ được nhận.
Julian Schaub - litb

dù sao, tôi nghĩ có một sự mơ hồ nhỏ: Giá trị của một đối tượng và giá trị của một biểu thức (như trong "rvalue"). Nếu diễn giải theo cách thứ hai, thì một biểu thức mảng chắc chắn có một giá trị: Đó là một kết quả từ việc phân rã nó thành một giá trị và là biểu thức con trỏ. Nhưng nếu diễn giải theo cách cũ, thì tất nhiên không có ý nghĩa hữu ích cho một đối tượng mảng.
Julian Schaub - litb

1
+1 cho cụm từ với một sửa chữa nhỏ; đối với mảng, nó thậm chí không phải là một bộ ba chỉ là một khớp nối [vị trí, loại]. Bạn đã có ý tưởng nào khác cho vị trí thứ ba trong trường hợp của mảng chưa? Tôi không thể nghĩ về bất kỳ.
huyền thoại2k

1
@ legends2k: Tôi nghĩ rằng tôi đã sử dụng vị trí thứ ba trong mảng để tránh biến chúng thành trường hợp đặc biệt chỉ có khớp nối. Có lẽ [vị trí, loại, khoảng trống ] sẽ tốt hơn.
pmg

8

Đó là khi mảng thối và đang được chỉ vào ;-)

Trên thực tế, chỉ là nếu bạn muốn truyền một mảng ở đâu đó, nhưng con trỏ được truyền thay thế (bởi vì ai sẽ truyền toàn bộ mảng cho bạn), mọi người nói rằng mảng kém đó đã phân rã thành con trỏ.


Lời nói thân thiện. Điều gì sẽ là một mảng đẹp mà không phân rã thành một con trỏ hoặc một mảng bị ngăn không bị phân rã? Bạn có thể trích dẫn một ví dụ trong C? Cảm ơn.
Unheilig

@Unheilig, chắc chắn, người ta có thể đóng gói một mảng thành struct và truyền cấu trúc.
Michael Krelin - hacker

Tôi không chắc ý của bạn là "công việc". Nó không được phép truy cập qua mảng, mặc dù nó hoạt động như mong đợi nếu bạn mong đợi những gì thực sự sẽ xảy ra. Hành vi đó (mặc dù, một lần nữa, chính thức không xác định) được bảo tồn.
Michael Krelin - hacker

Phân rã cũng xảy ra trong nhiều tình huống không đi qua mảng ở bất cứ đâu (như được mô tả bởi các câu trả lời khác). Ví dụ , a + 1.
MM

3

Phân rã mảng có nghĩa là, khi một mảng được truyền dưới dạng tham số cho hàm, nó được xử lý giống hệt ("phân rã thành") một con trỏ.

void do_something(int *array) {
  // We don't know how big array is here, because it's decayed to a pointer.
  printf("%i\n", sizeof(array));  // always prints 4 on a 32-bit machine
}

int main (int argc, char **argv) {
    int a[10];
    int b[20];
    int *c;
    printf("%zu\n", sizeof(a)); //prints 40 on a 32-bit machine
    printf("%zu\n", sizeof(b)); //prints 80 on a 32-bit machine
    printf("%zu\n", sizeof(c)); //prints 4 on a 32-bit machine
    do_something(a);
    do_something(b);
    do_something(c);
}

Có hai biến chứng hoặc ngoại lệ ở trên.

Đầu tiên, khi xử lý các mảng đa chiều trong C và C ++, chỉ có chiều thứ nhất bị mất. Điều này là do các mảng được đặt liền kề trong bộ nhớ, do đó trình biên dịch phải biết tất cả ngoại trừ kích thước đầu tiên để có thể tính toán độ lệch vào khối bộ nhớ đó.

void do_something(int array[][10])
{
    // We don't know how big the first dimension is.
}

int main(int argc, char *argv[]) {
    int a[5][10];
    int b[20][10];
    do_something(a);
    do_something(b);
    return 0;
}

Thứ hai, trong C ++, bạn có thể sử dụng các mẫu để suy ra kích thước của mảng. Microsoft sử dụng điều này cho các phiên bản C ++ của các hàm CRT bảo mật như strcpy_s và bạn có thể sử dụng một mẹo tương tự để lấy số lượng phần tử trong một mảng một cách đáng tin cậy .


1
phân rã xảy ra trong nhiều tình huống khác, không chỉ truyền một mảng cho hàm.
MM

0

tl; dr: Khi bạn sử dụng một mảng bạn đã xác định, bạn thực sự sẽ sử dụng một con trỏ đến phần tử đầu tiên của nó.

Như vậy:

  • Khi bạn viết arr[idx]bạn thực sự chỉ cần nói *(arr + idx).
  • các hàm không bao giờ thực sự lấy mảng làm tham số, chỉ con trỏ, ngay cả khi bạn chỉ định tham số mảng.

Sắp xếp các ngoại lệ cho quy tắc này:

  • Bạn có thể chuyển các mảng có độ dài cố định cho các hàm trong a struct.
  • sizeof() đưa ra kích thước được lấy bởi mảng, không phải kích thước của một con trỏ.

0

Tôi có thể rất táo bạo khi nghĩ rằng có bốn (4) cách để truyền một mảng làm đối số hàm. Ngoài ra đây là mã ngắn nhưng làm việc cho sự nhìn chăm chú của bạn.

#include <iostream>
#include <string>
#include <vector>
#include <cassert>

using namespace std;

// test data
// notice native array init with no copy aka "="
// not possible in C
 const char* specimen[]{ __TIME__, __DATE__, __TIMESTAMP__ };

// ONE
// simple, dangerous and useless
template<typename T>
void as_pointer(const T* array) { 
    // a pointer
    assert(array != nullptr); 
} ;

// TWO
// for above const T array[] means the same
// but and also , minimum array size indication might be given too
// this also does not stop the array decay into T *
// thus size information is lost
template<typename T>
void by_value_no_size(const T array[0xFF]) { 
    // decayed to a pointer
    assert( array != nullptr ); 
}

// THREE
// size information is preserved
// but pointer is asked for
template<typename T, size_t N>
void pointer_to_array(const T (*array)[N])
{
   // dealing with native pointer 
    assert( array != nullptr ); 
}

// FOUR
// no C equivalent
// array by reference
// size is preserved
template<typename T, size_t N>
void reference_to_array(const T (&array)[N])
{
    // array is not a pointer here
    // it is (almost) a container
    // most of the std:: lib algorithms 
    // do work on array reference, for example
    // range for requires std::begin() and std::end()
    // on the type passed as range to iterate over
    for (auto && elem : array )
    {
        cout << endl << elem ;
    }
}

int main()
{
     // ONE
     as_pointer(specimen);
     // TWO
     by_value_no_size(specimen);
     // THREE
     pointer_to_array(&specimen);
     // FOUR
     reference_to_array( specimen ) ;
}

Tôi cũng có thể nghĩ rằng điều này cho thấy sự vượt trội của C ++ so với C. Ít nhất là trong tham chiếu (ý định chơi chữ) của việc truyền một mảng bằng cách tham chiếu.

Tất nhiên có những dự án cực kỳ nghiêm ngặt không có phân bổ heap, không có ngoại lệ và không có std :: lib. Xử lý mảng gốc C ++ là tính năng ngôn ngữ quan trọng, người ta có thể nói.

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.