簡體   English   中英

什么是數組到指針衰減?

[英]What is array to pointer decay?

什么是數組到指針衰減? 與數組指針有什么關系嗎?

據說數組“衰減”成指針。 聲明為int numbers [5]的 C++ 數組不能重新指向,即不能說numbers = 0x5a5aff23 更重要的是,衰減一詞表示類型和維度的損失; numbers通過丟失維度信息(計數 5)衰減為int* ,並且類型不再是int [5] 在這里尋找沒有發生衰變的情況

如果您按值傳遞數組,那么您真正要做的是復制指針 - 指向數組第一個元素的指針被復制到參數(其類型也應該是數組元素類型的指針)。 這是由於陣列的衰減性質而起作用的; 一旦衰減, sizeof不再給出完整數組的大小,因為它本質上變成了一個指針。 這就是為什么首選(以及其他原因)通過引用或指針傳遞的原因。

傳入數組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])

最后兩個將提供適當的sizeof信息,而第一個不會,因為數組參數已衰減以分配給參數。

1 常量 U 應該在編譯時知道。

數組與 C/C++ 中的指針基本相同,但不完全一樣。 轉換數組后:

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

放入一個指針(無需強制轉換即可工作,因此在某些情況下可能會意外發生):

const int* p = a;

您失去了sizeof運算符對數組中元素進行計數的能力:

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

這種失去的能力被稱為“衰減”。

有關更多詳細信息,請查看有關數組衰減的這篇文章

這是標准所說的(C99 6.3.2.1/3 - 其他操作數 - 左值、數組和函數指示符):

除非它是 sizeof 運算符或一元 & 運算符的操作數,或者是用於初始化數組的字符串文字,否則類型為 ''array of type'' 的表達式將轉換為類型為 ''pointer to type'' 指向數組對象的初始元素並且不是左值。

這意味着幾乎任何時候在表達式中使用數組名稱時,它都會自動轉換為指向數組中第一項的指針。

請注意,函數名稱以類似的方式起作用,但函數指針的使用要少得多,而且以更專業的方式使用,它不會像將數組名稱自動轉換為指針那樣引起混亂。

C++ 標准(4.2 數組到指針的轉換)將轉換要求放寬到(強調我的):

“NT數組”或“T的未知邊界數組”類型的左值或右值可以轉換為“指向T的指針”類型的右值。

所以轉換不必像在 C 中總是那樣發生(這讓函數重載或模板匹配數組類型)。

這也是為什么在 C 中你應該避免在函數原型/定義中使用數組參數(在我看來——我不確定是否有任何普遍的協議)。 它們會引起混淆並且無論如何都是虛構的 - 使用指針參數並且混淆可能不會完全消失,但至少參數聲明沒有撒謊。

“衰減”是指表達式從數組類型到指針類型的隱式轉換。 在大多數情況下,當編譯器看到一個數組表達式時,它會將表達式的類型從“T 的 N 元素數組”轉換為“指向 T 的指針”,並將表達式的值設置為數組第一個元素的地址. 此規則的例外情況是,當數組是sizeof&運算符的操作數,或者數組是在聲明中用作初始值設定項的字符串文字時。

假設以下代碼:

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

表達式a的類型是“80 元素的 char 數組”,而表達式“這是一個測試”的類型是“15 元素的 char 數組”(在 C 中;在 C++ 中,字符串文字是 const char 的數組)。 但是,在對strcpy()的調用中,兩個表達式都不是sizeof&的操作數,因此它們的類型被隱式轉換為“指向 char 的指針”,並且它們的值被設置為每個中第一個元素的地址。 strcpy()接收的不是數組,而是指針,如其原型所示:

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

這與數組指針不同。 例如:

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

ptr_to_first_elementptr_to_array都具有相同的 a的基地址。 但是,它們是不同的類型,被區別對待,如下圖:

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

請記住,表達式a[i]被解釋為*(a+i) (僅當數組類型轉換為指針類型時才有效),因此a[i]ptr_to_first_element[i]的工作方式相同。 表達式(*ptr_to_array)[i]被解釋為*(*a+i) 表達式*ptr_to_array[i]ptr_to_array[i]可能會導致編譯器警告或錯誤,具體取決於上下文; 如果您期望他們評估為a[i] ,他們肯定會做錯事。

sizeof a == sizeof *ptr_to_array == 80

同樣,當數組是sizeof的操作數時,它不會轉換為指針類型。

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是一個指向 char 的簡單指針。

在 C 中,數組沒有價值。

無論需要對象的值但該對象是數組,都將使用其第一個元素的地址,類型pointer to (type of array elements)

在函數中,所有參數都按值傳遞(數組也不例外)。 當您在函數中傳遞數組時,它“衰減為指針”(原文如此); 當您將數組與其他東西進行比較時,它再次“衰減為指針”(原文如此); ...

void foo(int arr[]);

函數 foo 需要一個數組的值。 但是,在 C 語言中,數組沒有價值! 所以foo獲取的是數組第一個元素的地址。

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

在上面的比較中, arr沒有值,所以它變成了一個指針。 它變成了一個指向 int 的指針。 該指針可以與變量ip進行比較。

在您習慣於看到的數組索引語法中,arr 再次“衰減為指針”

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

數組不衰減為指針的唯一情況是它是 sizeof 運算符或 & 運算符(運算符的“地址”)的操作數,或作為用於初始化字符數組的字符串文字。

這是當數組腐爛並被指向時;-)

實際上,只是如果你想在某個地方傳遞一個數組,但傳遞的是指針(因為誰會為你傳遞整個數組),人們說糟糕的數組衰減為指針。

數組衰減意味着,當數組作為參數傳遞給函數時,它被視為(“衰減”)指針。

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 和 C++ 中處理多維數組時,只會丟失第一個維度。 這是因為數組在內存中是連續布局的,因此編譯器必須知道除第一個維度之外的所有維度,才能計算到該內存塊的偏移量。

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

其次,在 C++ 中,您可以使用模板來推斷數組的大小。 Microsoft 將此用於 C++ 版本的 Secure CRT 函數(如strcpy_s ),您可以使用類似的技巧可靠地獲取數組中的元素數

tl; dr:當您使用已定義的數組時,您實際上將使用指向其第一個元素的指針。

因此:

  • 當您編寫arr[idx]時,您實際上只是在說*(arr + idx)
  • 函數從不真正將數組作為參數,只有指針——直接地,當你指定一個數組參數時,或者間接地,如果你傳遞一個數組的引用。

此規則的一些例外情況:

  • 您可以將固定長度的數組傳遞給struct中的函數。
  • sizeof()給出數組占用的大小,而不是指針的大小。

數組在 C 中通過指針自動傳遞。其背后的基本原理只能推測

int a[5]int *aint (*a)[5]都是美化的地址,這意味着編譯器根據類型對它們的算術和遵從運算符進行不同的處理,因此當它們引用相同的地址時,它們不是編譯器同樣處理。 int a[5]與其他 2 的不同之處在於地址是隱式的,並且不會作為數組本身的一部分顯示在堆棧或可執行文件上,它僅由編譯器用於解析某些算術運算,例如將其地址或指針算術。 因此int a[5]既是一個數組也是一個隱式地址,但是一旦你談到地址本身並將其放入堆棧中,地址本身就不再是一個數組,而只能是一個指向數組或衰減數組,即指向數組第一個成員的指針。

例如,在int (*a)[5]上,第一次取消引用a將產生一個int * (所以相同的地址,只是不同的類型,注意不是int a[5] ),以及 a 上a指針運算,即a+1*(a+1)將根據 5 個 int 數組的大小(這是它指向的數據類型),第二次取消引用將產生int 然而,在int a[5]上,第一次取消引用將產生int並且指針算術將根據int的大小。

對於函數,您只能傳遞int *int (*)[5] ,並且該函數將其轉換為任何參數類型,因此在函數中您可以選擇是否將傳遞的地址視為衰減數組或指向數組的指針(函數必須指定要傳遞的數組的大小)。 如果將a傳遞給函數並且a定義為int a[5] ,那么當a解析為地址時,您傳遞的是地址,並且地址只能是指針類型。 在函數中,它訪問的參數是堆棧或寄存器中的地址,它只能是指針類型而不是數組類型 - 這是因為它是堆棧上的實際地址,因此顯然不是數組本身。

您會丟失數組的大小,因為參數的類型是地址,是指針而不是數組,它沒有數組大小,正如使用sizeof時可以看到的那樣,它適用於值的類型被傳遞給它。 參數類型int a[5]而不是int *a是允許的,但被視為int *而不是徹底禁止它,盡管它應該被禁止,因為它具有誤導性,因為它使您認為可以使用大小信息,但你只能通過將其轉換為int (*a)[5]來做到這一點,當然,函數必須指定數組的大小,因為無法傳遞數組的大小,因為該數組需要是編譯時常量。

我可能非常大膽地認為有四 (4) 種方法可以將數組作為函數參數傳遞。 這里還有簡短但有效的代碼供您閱讀。

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

我也可能認為這顯示了 C++ 與 C 的優勢。至少在通過引用傳遞數組的引用(雙關語)方面。

當然也有非常嚴格的項目,沒有堆分配,沒有異常,也沒有 std::lib。 有人可能會說,C++ 原生數組處理是任務關鍵型語言功能。

試試這個代碼


void f(double a[10]) {
    printf("in function: %d", sizeof(a));
    printf("pointer size: %d\n", sizeof(double *));
}

int main() {
    double a[10];
    printf("in main: %d", sizeof(a));
    f(a);
}

你會看到函數內部數組的大小不等於main中數組的大小,而是等於指針的大小。

您可能聽說過“數組是指針”,但是,這並不完全正確( main中的sizeof打印出正確的大小)。 但是,當通過時,數組衰減為指針。 也就是說,無論語法顯示什么,您實際上都傳遞了一個指針,而函數實際上接收了一個指針。

在這種情況下,定義void f(double a[10]被編譯器隱式轉換為void f(double *a) 。您可以等效地將函數參數直接聲明為*a 。您甚至可以編寫a[100]a[1] ,而不是a[10] ,因為它實際上從未以這種方式編譯(但是,您顯然不應該這樣做,這會使讀者感到困惑)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM