ไม่เข้าใจวิธีคำนวณกำลังสองของตัวเลขนี้


135

ฉันพบฟังก์ชันที่คำนวณกำลังสองของตัวเลข:

int p(int n) {
    int a[n]; //works on C99 and above
    return (&a)[n] - a;
}

ก็จะส่งกลับค่า n 2 คำถามคือมันทำได้อย่างไร? หลังจากการทดสอบเล็ก ๆ น้อย ๆ ผมพบว่าระหว่าง(&a)[k]และ(&a)[k+1]เป็น/sizeof(a) sizeof(int)ทำไมถึงเป็นเช่นนั้น?


6
คุณมีลิงค์ไปยังที่ที่คุณพบข้อมูลนี้หรือไม่?
ราหู

4
int p(n)เหรอ? มันยังรวบรวม?
barak manos

78
มันยอดเยี่ยมมากตอนนี้ไม่เคยใช้อีกแล้วใช้ n * n แทน ...

26
หรือดีกว่า:int q(int n) { return sizeof (char [n][n]); }
ouah

17
@ouah สมมติว่าคำถามนี้อ้างถึงcodegolf.stackexchange.com/a/43262/967เหตุผลที่ฉันไม่ได้ใช้sizeofคือการบันทึกอักขระ คนอื่น ๆ : นี่เป็นรหัสที่คลุมเครือโดยเจตนาเป็นพฤติกรรมที่ไม่ได้กำหนดคำตอบของ @ ouah นั้นถูกต้อง
ecatmur

คำตอบ:


117

เห็นได้ชัดว่าเป็นการแฮ็ก ... แต่เป็นวิธีการยกกำลังสองจำนวนโดยไม่ใช้ตัว*ดำเนินการ (นี่เป็นข้อกำหนดในการแข่งขันการเข้ารหัส)

(&a)[n] 

เทียบเท่ากับตัวชี้ไปintที่ตำแหน่ง

(a + sizeof(a[n])*n)

ดังนั้นนิพจน์ทั้งหมดคือ

  (&a)[n] -a 

= (a + sizeof(a[n])*n -a) /sizeof(int)

= sizeof(a[n])*n / sizeof(int)
= sizeof(int) * n * n / sizeof(int)
= n * n

11
และอย่างที่คุณบอกเป็นนัยอย่างชัดเจน แต่ฉันรู้สึกว่าจำเป็นต้องทำให้ชัดเจนมันเป็นการแฮ็กไวยากรณ์ที่ดีที่สุด การดำเนินการทวีคูณจะยังคงอยู่ที่นั่น เป็นเพียงตัวดำเนินการที่ถูกหลีกเลี่ยง
Tommy

ฉันรู้ว่ามันเกิดอะไรขึ้นเบื้องหลัง แต่คำถามที่แท้จริงของฉันคือทำไม (& a) [k] อยู่ที่เดียวกับ a + k * sizeof (a) / sizeof (int)
Emanuel

33
ในฐานะนักเขียนโค้ดรุ่นเก่าฉันรู้สึกแปลกใจที่คอมไพเลอร์สามารถใช้(&a)เป็นตัวชี้ไปยังวัตถุn*sizeof(int)เมื่อnใดที่ไม่รู้เวลาคอมไพล์ C เคยเป็นภาษาง่ายๆ ...
Floris

นั่นเป็นการแฮ็คที่ค่อนข้างฉลาด แต่มีบางอย่างที่คุณจะไม่เห็นในรหัสการผลิต (หวังว่า)
John Odom

14
นอกจากนี้ยังเป็น UB อีกด้วยเนื่องจากจะเพิ่มตัวชี้เพื่อไม่ให้ชี้ไปที่องค์ประกอบของอาร์เรย์ที่อยู่ข้างใต้หรือเพียงแค่ผ่านมา
Deduplicator

86

เพื่อทำความเข้าใจกับแฮ็คนี้ก่อนอื่นคุณต้องเข้าใจความแตกต่างของตัวชี้กล่าวคือจะเกิดอะไรขึ้นเมื่อตัวชี้สองตัวที่ชี้ไปยังองค์ประกอบของอาร์เรย์เดียวกันถูกลบออก

เมื่อตัวชี้ตัวหนึ่งถูกลบออกจากอีกตัวหนึ่งผลลัพธ์คือระยะทาง (วัดในองค์ประกอบอาร์เรย์) ระหว่างตัวชี้ ดังนั้นถ้าpจุดไปa[i]และqจุดที่จะต้องa[j]แล้วจะมีค่าเท่ากับ p - qi - j

C11: 6.5.6 ตัวดำเนินการเพิ่มเติม (p9):

เมื่อมีการลบพอยน์เตอร์สองตัวทั้งสองจะชี้ไปที่องค์ประกอบของอ็อบเจ็กต์อาร์เรย์เดียวกันหรืออย่างใดอย่างหนึ่งผ่านองค์ประกอบสุดท้ายของอ็อบเจ็กต์อาร์เรย์ ผลที่ได้คือความแตกต่างของตัวห้อยของทั้งสององค์ประกอบอาร์เรย์ [... ].
ในคำอื่น ๆ ถ้าการแสดงออกPและQชี้ไปตามลำดับi-th และjองค์ประกอบ -th ของวัตถุอาร์เรย์แสดงออก(P)-(Q)ได้ค่าi−jptrdiff_tบริการที่มีให้พอดีกับค่าในวัตถุของการพิมพ์

ตอนนี้ผมหวังว่าคุณมีความตระหนักของการแปลงชื่ออาร์เรย์ชี้แปรรูปชี้ไปยังองค์ประกอบแรกของอาร์เรย์a เป็นที่อยู่ของบล็อกหน่วยความจำทั้งหมดคือมันเป็นอยู่ของอาร์เรย์ รูปด้านล่างจะช่วยให้คุณเข้าใจ ( อ่านคำตอบนี้สำหรับคำอธิบายโดยละเอียด ): a&aa

ป้อนคำอธิบายภาพที่นี่

นี้จะช่วยให้คุณเข้าใจว่าทำไมaและ&aมีที่อยู่เดียวกันและวิธีการที่(&a)[i]เป็นที่อยู่ของฉันTHอาร์เรย์ (ที่มีขนาดเดียวกันกับที่a)

ดังนั้นคำสั่ง

return (&a)[n] - a; 

เทียบเท่ากับ

return (&a)[n] - (&a)[0];  

และความแตกต่างนี้จะให้จำนวนองค์ประกอบระหว่างพอยน์เตอร์(&a)[n]และ(&a)[0]ซึ่งคือnอาร์เรย์แต่ละn intองค์ประกอบ ดังนั้นองค์ประกอบมากมายรวมn*n= 2 n


บันทึก:

C11: 6.5.6 ตัวดำเนินการเพิ่มเติม (p9):

เมื่อทั้งสองตัวชี้จะถูกหัก, ทั้งสองจะชี้ไปที่องค์ประกอบของวัตถุอาร์เรย์เดียวกันหรือหนึ่งในอดีตที่ผ่านมาองค์ประกอบสุดท้ายของวัตถุอาร์เรย์ ; ผลลัพธ์คือความแตกต่างของตัวห้อยขององค์ประกอบอาร์เรย์ทั้งสอง ขนาดของผลลัพธ์ถูกกำหนดโดยการนำไปใช้งานและประเภทของผลลัพธ์ (ชนิดจำนวนเต็มที่ลงนาม) ถูกptrdiff_tกำหนดไว้ใน<stddef.h>ส่วนหัว หากไม่สามารถแสดงผลลัพธ์ในออบเจ็กต์ประเภทนั้นได้แสดงว่าพฤติกรรมนั้นไม่ได้กำหนดไว้

ตั้งแต่(&a)[n]จุดองค์ประกอบของวัตถุอาร์เรย์เดียวกันมิได้หนึ่งที่ผ่านมาองค์ประกอบสุดท้ายของวัตถุอาร์เรย์ไม่และ(&a)[n] - aจะเรียกไม่ได้กำหนดพฤติกรรม

นอกจากนี้ทราบว่าดีกว่าที่จะเปลี่ยนประเภทการกลับมาของฟังก์ชั่นการ pptrdiff_t


"ทั้งสองจะชี้ไปที่องค์ประกอบของวัตถุอาร์เรย์เดียวกัน" ซึ่งทำให้เกิดคำถามสำหรับฉันว่า "แฮ็ก" นี้ไม่ใช่ UB หรือไม่ นิพจน์เลขคณิตของตัวชี้หมายถึงจุดสิ้นสุดของสมมุติฐานของวัตถุที่ไม่มีอยู่จริง: สิ่งนี้ได้รับอนุญาตหรือไม่
Martin Ba

สรุปได้ว่า a คือแอดเดรสของอาร์เรย์ของ n องค์ประกอบดังนั้น & a [0] คือแอดเดรสขององค์ประกอบแรกในอาร์เรย์นี้ซึ่งเหมือนกับ a; นอกจากนี้ & a [k] จะถือว่าเป็นแอดเดรสของอาร์เรย์ของ n องค์ประกอบเสมอโดยไม่คำนึงถึง k และเนื่องจาก & a [1..n] เป็นเวกเตอร์ด้วยเช่นกัน "ตำแหน่ง" ขององค์ประกอบจึงเรียงต่อเนื่องกันหมายความว่า องค์ประกอบแรกอยู่ที่ตำแหน่ง x วินาทีอยู่ที่ตำแหน่ง x + (จำนวนองค์ประกอบของเวกเตอร์ a ซึ่งเป็น n) เป็นต้น ฉันถูกไหม? นอกจากนี้นี่คือพื้นที่ฮีปดังนั้นหมายความว่าถ้าฉันจัดสรรเวกเตอร์ใหม่ขององค์ประกอบ n เดียวกันที่อยู่ของมันจะเหมือนกับ (& a) [1] หรือไม่
Emanuel

1
@ เอมานูเอล; &a[k]คือที่อยู่ขององค์ประกอบของอาร์เรย์k TH aมันเป็น(&a)[k]ที่มักจะได้รับการพิจารณาเป็นที่อยู่ของอาร์เรย์ของkองค์ประกอบ ดังนั้นองค์ประกอบแรกอยู่ที่ตำแหน่งa(หรือ&a) ที่สองอยู่ที่ตำแหน่งa+ (จำนวนองค์ประกอบของอาร์เรย์aซึ่งเป็นn) * (ขนาดขององค์ประกอบอาร์เรย์) และอื่น ๆ และโปรดทราบว่าหน่วยความจำสำหรับอาร์เรย์ที่มีความยาวตัวแปรจะถูกจัดสรรบนสแต็กไม่ใช่บนฮีป
haccks

@MartinBa; สิ่งนี้ได้รับอนุญาตหรือไม่? ไม่ไม่อนุญาต UB ของมัน ดูการแก้ไข
haccks

1
@haccks เรื่องบังเอิญที่ดีระหว่างธรรมชาติกับคำถามและชื่อเล่นของคุณ
Dimitar Tsonev

35

aคืออาร์เรย์ (ตัวแปร) ของn int.

&aเป็นตัวชี้ไปยังอาร์เรย์ (ตัวแปร) ของn int.

(&a)[1]เป็นตัวชี้ของintหนึ่งในintอดีตที่ผ่านมาองค์ประกอบอาร์เรย์ที่ผ่านมา ชี้นี่คือองค์ประกอบหลังจากn int&a[0]

(&a)[2]เป็นตัวชี้ของintหนึ่งในintอดีตที่ผ่านมาองค์ประกอบแถวสุดท้ายของสองอาร์เรย์ ชี้นี่คือองค์ประกอบหลังจาก2 * n int&a[0]

(&a)[n]เป็นตัวชี้ของintหนึ่งในintอดีตที่ผ่านมาองค์ประกอบแถวสุดท้ายของnอาร์เรย์ ชี้นี่คือองค์ประกอบหลังจากn * n int &a[0]เพียงแค่ลบ&a[0]หรือและคุณมีan

แน่นอนว่านี่เป็นพฤติกรรมที่ไม่ได้กำหนดไว้ในทางเทคนิคแม้ว่ามันจะทำงานบนเครื่องของคุณโดยที่(&a)[n]ไม่ได้ชี้เข้าไปในอาร์เรย์หรืออย่างใดอย่างหนึ่งเลยองค์ประกอบอาร์เรย์สุดท้าย (ตามที่กำหนดโดยกฎ C ของเลขคณิตตัวชี้)


ฉันเข้าใจแล้ว แต่ทำไมสิ่งนี้ถึงเกิดขึ้นใน C? ตรรกะเบื้องหลังสิ่งนี้คืออะไร?
Emanuel

@Emanuel ไม่มีคำตอบที่เข้มงวดไปกว่านั้นจริงๆแล้วการคำนวณทางคณิตศาสตร์ของตัวชี้นั้นมีประโยชน์สำหรับการวัดระยะทาง (โดยปกติจะอยู่ในอาร์เรย์) [n]ไวยากรณ์จะประกาศอาร์เรย์และอาร์เรย์ที่สลายตัวเป็นพอยน์เตอร์ สามสิ่งที่มีประโยชน์แยกกันจากผลลัพธ์นี้
Tommy

1
@Emanuel หากคุณถามว่าทำไมใครบางคนถึงทำเช่นนี้มีเหตุผลเพียงเล็กน้อยและทุกเหตุผลที่ไม่เกิดจากลักษณะการกระทำของ UB และเป็นที่น่าสังเกตว่า(&a)[n]เป็นประเภทint[n]และนั่นเป็นการแสดงออกint*เนื่องจากอาร์เรย์ที่แสดงเป็นที่อยู่ขององค์ประกอบแรกในกรณีที่คำอธิบายไม่ชัดเจน
WhozCraig

ไม่ฉันไม่ได้หมายความว่าทำไมใครบางคนถึงทำเช่นนี้ฉันหมายความว่าทำไมมาตรฐาน C จึงมีพฤติกรรมเช่นนี้ในสถานการณ์นี้
Emanuel

1
@Emanuel Pointer เลขคณิต (และในกรณีนี้บทย่อยของหัวข้อนั้น: ความแตกต่างของตัวชี้ ) ควรใช้ Googling รวมทั้งอ่านคำถามและคำตอบในไซต์นี้ มีประโยชน์มากมายและมีการกำหนดไว้อย่างเป็นรูปธรรมในมาตรฐานเมื่อใช้อย่างเหมาะสม เพื่อที่จะเข้าใจอย่างถ่องแท้คุณต้องเข้าใจว่าประเภทในรหัสที่คุณระบุนั้นมีการจัดทำขึ้นอย่างไร
WhozCraig

12

หากคุณมีพอยน์เตอร์สองตัวที่ชี้ไปยังสององค์ประกอบของอาร์เรย์เดียวกันความแตกต่างของมันจะให้จำนวนองค์ประกอบระหว่างพอยน์เตอร์เหล่านี้ ตัวอย่างเช่นข้อมูลโค้ดนี้จะแสดงผล 2

int a[10];

int *p1 = &a[1];
int *p2 = &a[3];

printf( "%d\n", p2 - p1 ); 

ตอนนี้ให้พิจารณาการแสดงออก

(&a)[n] - a;

ในนิพจน์นี้aมีประเภทint *และชี้ไปที่องค์ประกอบแรก

นิพจน์&aมีประเภทint ( * )[n]และชี้ไปที่แถวแรกของอาร์เรย์สองมิติที่สร้างภาพ ค่าตรงกับค่าaแม้ว่าประเภทจะแตกต่างกัน

( &a )[n]

เป็นองค์ประกอบที่ n ของอาร์เรย์สองมิติที่สร้างภาพนี้และมีชนิดint[n]นั่นคือมันเป็นแถวที่ n ของอาร์เรย์ภาพ ในการแสดงออกที่(&a)[n] - aจะถูกแปลงเป็นอยู่ขององค์ประกอบแรกของตนและมีประเภท `int *

ระหว่าง (&a)[n]และaมี n แถวของ n องค์ประกอบ n * nดังนั้นความแตกต่างจะเท่ากับ


ดังนั้นข้างหลังทุกอาร์เรย์จึงมีเมทริกซ์ขนาด n * n?
Emanuel

@ เอมานูเอลระหว่างพอยน์เตอร์ทั้งสองนี้มีเมทริกซ์ขององค์ประกอบ nxn และความแตกต่างของพอยน์เตอร์จะให้ค่าเท่ากับ n * n นั่นคือจำนวนองค์ประกอบที่อยู่ระหว่างพอยน์เตอร์
วลาดจากมอสโก

แต่ทำไมเมทริกซ์ที่มีขนาด n * n อยู่ข้างหลัง? มีการใช้งานใน C หรือไม่? ฉันหมายความว่ามันเหมือนกับ C "จัดสรร" อาร์เรย์ขนาด n มากขึ้นโดยที่ฉันไม่รู้ตัว? ถ้าเป็นเช่นนั้นฉันสามารถใช้มันได้หรือไม่? มิฉะนั้นทำไมเมทริกซ์นี้จึงถูกสร้างขึ้น (ฉันหมายถึงมันต้องมีจุดประสงค์เพื่อให้มันอยู่ที่นั่น)
Emanuel

2
@Emanuel - เมทริกซ์นี้เป็นเพียงคำอธิบายว่าการคำนวณทางคณิตศาสตร์ของตัวชี้ทำงานอย่างไรในกรณีนี้ เมทริกซ์นี้ไม่ได้รับการจัดสรรและคุณไม่สามารถใช้งานได้ เช่นเดียวกับที่ระบุไว้สองสามครั้ง 1) ข้อมูลโค้ดนี้เป็นการแฮ็กที่ไม่มีการใช้งานจริง 2) คุณต้องเรียนรู้วิธีการทำงานของตัวชี้ทางคณิตศาสตร์เพื่อที่จะเข้าใจแฮ็คนี้
void_ptr

@ เอมานูเอลสิ่งนี้อธิบายถึงการคำนวณทางคณิตศาสตร์ของตัวชี้ Expreesion (& a) [n] คือตัวชี้ไปที่ n- องค์ประกอบของอาร์เรย์สองมิติที่ถ่ายภาพเนื่องจากเลขคณิตของตัวชี้
Vlad จากมอสโกว

4
Expression     | Value                | Explanation
a              | a                    | point to array of int elements
a[n]           | a + n*sizeof(int)    | refer to n-th element in array of int elements
-------------------------------------------------------------------------------------------------
&a             | a                    | point to array of (n int elements array)
(&a)[n]        | a + n*sizeof(int[n]) | refer to n-th element in array of (n int elements array)
-------------------------------------------------------------------------------------------------
sizeof(int[n]) | n * sizeof(int)      | int[n] is a type of n-int-element array

ด้วยประการฉะนี้

  1. ประเภท(&a)[n]คือint[n]ตัวชี้
  2. ประเภทaคือintตัวชี้

ตอนนี้นิพจน์(&a)[n]-aทำการย่อยตัวชี้:

  (&a)[n]-a
= ((a + n*sizeof(int[n])) - a) / sizeof(int)
= (n * sizeof(int[n])) / sizeof(int)
= (n * n * sizeof(int)) / sizeof(int)
= n * n
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.