ใน C ตัวดำเนินการ shift ( <<
, >>
) เป็นเลขคณิตหรือตรรกะหรือไม่?
ใน C ตัวดำเนินการ shift ( <<
, >>
) เป็นเลขคณิตหรือตรรกะหรือไม่?
คำตอบ:
ตามK&R ฉบับที่ 2ผลลัพธ์จะขึ้นอยู่กับการใช้งานสำหรับการเปลี่ยนค่าที่ลงนามอย่างถูกต้อง
Wikipediaกล่าวว่า C / C ++ 'โดยปกติ' จะใช้การเปลี่ยนแปลงทางคณิตศาสตร์กับค่าที่ลงชื่อ
โดยทั่วไปคุณต้องทดสอบคอมไพเลอร์ของคุณหรือไม่ต้องพึ่งพาคอมไพเลอร์ ความช่วยเหลือ VS2008 ของฉันสำหรับคอมไพเลอร์ MS C ++ ปัจจุบันบอกว่าคอมไพเลอร์ของพวกเขาทำการเปลี่ยนเลขคณิต
เมื่อเลื่อนไปทางซ้ายจะไม่มีความแตกต่างระหว่างการเลื่อนเลขคณิตและตรรกะ เมื่อเลื่อนไปทางขวาประเภทของการเลื่อนจะขึ้นอยู่กับประเภทของค่าที่เลื่อน
(ในฐานะที่เป็นพื้นหลังสำหรับผู้อ่านที่ไม่คุ้นเคยกับความแตกต่างการเลื่อนไปทางขวาแบบ "ตรรกะ" ทีละ 1 บิตจะเลื่อนบิตทั้งหมดไปทางขวาและเติมบิตทางซ้ายสุดด้วย 0 การเลื่อน "เลขคณิต" จะปล่อยค่าเดิมในบิตทางซ้ายสุด ความแตกต่างกลายเป็นสิ่งสำคัญเมื่อจัดการกับจำนวนลบ)
เมื่อเปลี่ยนค่าที่ไม่ได้ลงชื่อตัวดำเนินการ >> ใน C จะเป็นการกะแบบตรรกะ เมื่อเปลี่ยนค่าที่เซ็นแล้วตัวดำเนินการ >> จะเป็นการกะเลขคณิต
ตัวอย่างเช่นสมมติว่าเป็นเครื่อง 32 บิต:
signed int x1 = 5;
assert((x1 >> 1) == 2);
signed int x2 = -5;
assert((x2 >> 1) == -3);
unsigned int x3 = (unsigned int)-5;
assert((x3 >> 1) == 0x7FFFFFFD);
พิจารณาi
และn
เป็นตัวถูกดำเนินการด้านซ้ายและขวาตามลำดับของตัวดำเนินการกะ ประเภทของหลังจากที่โปรโมชั่นจำนวนเต็มเป็นi
T
สมมติว่าn
อยู่ใน[0, sizeof(i) * CHAR_BIT)
- ไม่ได้กำหนดเป็นอย่างอื่น - เรามีกรณีเหล่านี้:
| Direction | Type | Value (i) | Result |
| ---------- | -------- | --------- | ------------------------ |
| Right (>>) | unsigned | ≥ 0 | −∞ ← (i ÷ 2ⁿ) |
| Right | signed | ≥ 0 | −∞ ← (i ÷ 2ⁿ) |
| Right | signed | < 0 | Implementation-defined† |
| Left (<<) | unsigned | ≥ 0 | (i * 2ⁿ) % (T_MAX + 1) |
| Left | signed | ≥ 0 | (i * 2ⁿ) ‡ |
| Left | signed | < 0 | Undefined |
†คอมไพเลอร์ส่วนใหญ่ใช้สิ่งนี้เป็นกะเลขคณิต
‡ไม่ได้กำหนดหากค่ามากเกินประเภทผลลัพธ์ T; ประเภทที่เลื่อนตำแหน่งของ i
ประการแรกคือความแตกต่างระหว่างการเปลี่ยนแปลงทางตรรกะและเลขคณิตจากมุมมองทางคณิตศาสตร์โดยไม่ต้องกังวลเกี่ยวกับขนาดชนิดข้อมูล การเปลี่ยนแปลงเชิงตรรกะจะเติมบิตที่ถูกละทิ้งด้วยศูนย์เสมอในขณะที่การเปลี่ยนแปลงทางคณิตศาสตร์จะเติมด้วยศูนย์สำหรับการเลื่อนไปทางซ้ายเท่านั้น แต่สำหรับการเลื่อนไปทางขวาจะคัดลอก MSB ซึ่งจะรักษาสัญลักษณ์ของตัวถูกดำเนินการ (สมมติว่าการเข้ารหัสเสริมของสองค่าเป็นค่าลบ)
กล่าวอีกนัยหนึ่งการเลื่อนแบบลอจิคัลจะมองไปที่ตัวถูกดำเนินการที่ถูกเปลี่ยนเป็นเพียงกระแสของบิตและย้ายไปโดยไม่ต้องกังวลเกี่ยวกับสัญลักษณ์ของค่าผลลัพธ์ การเปลี่ยนเลขคณิตจะมองว่ามันเป็นตัวเลข (ลงนาม) และรักษาเครื่องหมายไว้เมื่อมีการกะ
การเลื่อนเลขคณิตทางซ้ายของจำนวน X โดย n นั้นเทียบเท่ากับการคูณ X ด้วย 2 nและเทียบเท่ากับการเลื่อนซ้ายเชิงตรรกะ การเปลี่ยนแปลงเชิงตรรกะจะให้ผลลัพธ์เช่นเดียวกันเนื่องจาก MSB ตกลงไปจากจุดสิ้นสุดและไม่มีอะไรจะรักษา
การเปลี่ยนเลขคณิตที่ถูกต้องของจำนวน X โดย n จะเทียบเท่ากับการหารจำนวนเต็มของ X โดย 2 nเท่านั้นถ้า X ไม่เป็นลบ! การหารจำนวนเต็มไม่ใช่อะไรนอกจากการหารทางคณิตศาสตร์และปัดเศษเป็น 0 ( trunc )
สำหรับจำนวนลบซึ่งแสดงโดยการเข้ารหัสเสริมของสองตัวการเลื่อนไปทางขวาด้วย n บิตมีผลในทางคณิตศาสตร์หารด้วย 2 nและปัดเศษไปทาง −∞ ( พื้น ) ดังนั้นการขยับที่ถูกต้องจึงแตกต่างกันสำหรับค่าที่ไม่ใช่ค่าลบและค่าลบ
สำหรับ X ≥ 0, X >> n = X / 2 n = trunc (X ÷ 2 n )
สำหรับ X <0, X >> n = ชั้น (X ÷ 2 n )
การ÷
หารทางคณิตศาสตร์อยู่ที่ไหนคือการหาร/
จำนวนเต็ม ลองดูตัวอย่าง:
37) 10 = 100101) 2
37 ÷ 2 = 18.5
37/2 = 18 (ปัดเศษ 18.5 ไปทาง 0) = 10010) 2 [ผลลัพธ์ของการเลื่อนขวาเลขคณิต]
-37) 10 = 11011011) 2 (พิจารณาส่วนเติมเต็มของสองส่วนการแทนค่า 8 บิต)
-37 ÷ 2 = -18.5
-37 / 2 = -18 (ปัดเศษ 18.5 ไปทาง 0) = 11101110) 2 [ไม่ใช่ผลลัพธ์ของการเลื่อนขวาทางคณิตศาสตร์]
-37 >> 1 = -19 (ปัดเศษ 18.5 ไปทาง −∞) = 11101101) 2 [ผลลัพธ์ของการเลื่อนขวาทางคณิตศาสตร์]
ดังที่Guy Steele ชี้ให้เห็นความแตกต่างนี้ทำให้เกิดข้อบกพร่องในคอมไพเลอร์มากกว่าหนึ่งตัว ที่นี่ที่ไม่ใช่ลบ (คณิตศาสตร์) สามารถจับคู่กับค่าที่ไม่เป็นลบและไม่ได้ลงนามและลงนาม (C); ทั้งสองได้รับการปฏิบัติเหมือนกันและการเลื่อนไปทางขวาทำได้โดยการหารจำนวนเต็ม
ดังนั้นตรรกะและเลขคณิตจึงเทียบเท่ากันในการขยับซ้ายและสำหรับค่าที่ไม่เป็นลบในการขยับขวา มันเป็นการเปลี่ยนค่าเชิงลบที่แตกต่างกันอย่างเหมาะสม
มาตรฐาน C99 §6.5.7 :
แต่ละตัวถูกดำเนินการต้องมีประเภทจำนวนเต็ม
การส่งเสริมการขายจำนวนเต็มจะดำเนินการในแต่ละตัวถูกดำเนินการ ประเภทของผลลัพธ์คือของตัวถูกดำเนินการด้านซ้ายที่เลื่อนขั้น ถ้าค่าของตัวถูกดำเนินการด้านขวาเป็นลบหรือมากกว่าหรือเท่ากับความกว้างของตัวถูกดำเนินการด้านซ้ายที่เลื่อนขั้นพฤติกรรมจะไม่ได้กำหนดไว้
short E1 = 1, E2 = 3;
int R = E1 << E2;
ในตัวอย่างข้างต้นตัวถูกดำเนินการทั้งสองจะกลายเป็นint
(เนื่องจากการส่งเสริมจำนวนเต็ม) ถ้าE2
เป็นลบหรือE2 ≥ sizeof(int) * CHAR_BIT
ไม่ได้กำหนดการดำเนินการ เนื่องจากการขยับมากกว่าบิตที่มีอยู่นั้นจะล้นออกมาอย่างแน่นอน ได้R
รับการประกาศให้เป็นshort
ที่int
ผลการดำเนินงานเปลี่ยนแปลงจะถูกแปลงโดยปริยายshort
; Conversion ที่แคบลงซึ่งอาจนำไปสู่พฤติกรรมที่กำหนดการนำไปใช้หากไม่สามารถแสดงค่าในประเภทปลายทางได้
ผลลัพธ์ของ E1 << E2 คือ E1 ตำแหน่งบิต E2 ที่เลื่อนไปทางซ้าย บิตที่ว่างจะเต็มไปด้วยศูนย์ ถ้า E1 มีประเภทที่ไม่ได้ลงนามค่าของผลลัพธ์คือ E1 × 2 E2ลดโมดูโลหนึ่งค่ามากกว่าค่าสูงสุดที่แสดงได้ในประเภทผลลัพธ์ ถ้า E1 มีประเภทที่ลงนามและค่าที่ไม่เป็นลบและ E1 × 2 E2สามารถแสดงได้ในประเภทผลลัพธ์นั่นคือค่าผลลัพธ์ มิฉะนั้นจะไม่มีการกำหนดพฤติกรรม
เนื่องจากการเลื่อนด้านซ้ายเหมือนกันสำหรับทั้งสองบิตที่ว่างจะเต็มไปด้วยศูนย์ จากนั้นระบุว่าสำหรับทั้งประเภทที่ไม่ได้ลงนามและประเภทที่ลงนามแล้วจะเป็นการเปลี่ยนแปลงทางคณิตศาสตร์ ฉันตีความว่ามันเป็นการเลื่อนเลขคณิตเนื่องจากการเปลี่ยนแปลงเชิงตรรกะไม่ได้กังวลเกี่ยวกับค่าที่แทนด้วยบิตเพียงแค่มองว่ามันเป็นกระแสของบิต แต่การเจรจาไม่ได้มาตรฐานในแง่ของบิต แต่ด้วยการกำหนดมันในแง่ของมูลค่าที่ได้จากผลิตภัณฑ์ของ E1 กับ 2 E2
ข้อแม้คือสำหรับประเภทที่ลงนามแล้วค่าควรเป็นค่าที่ไม่เป็นลบและค่าผลลัพธ์ควรเป็นตัวแทนในประเภทผลลัพธ์ มิฉะนั้นการดำเนินการจะไม่ถูกกำหนด ประเภทผลลัพธ์จะเป็นประเภทของ E1 หลังจากใช้การส่งเสริมแบบอินทิกรัลและไม่ใช่ประเภทปลายทาง (ตัวแปรที่จะเก็บผลลัพธ์) ค่าผลลัพธ์จะถูกแปลงเป็นประเภทปลายทางโดยปริยาย หากไม่สามารถแสดงได้ในประเภทนั้นการแปลงจะถูกกำหนดให้ใช้งาน (C99 §6.3.1.3 / 3)
ถ้า E1 เป็นประเภทที่เซ็นชื่อด้วยค่าลบพฤติกรรมของการขยับซ้ายจะไม่ได้กำหนดไว้ นี่เป็นเส้นทางที่ง่ายสำหรับพฤติกรรมที่ไม่ได้กำหนดซึ่งอาจถูกมองข้ามไปได้ง่าย
ผลลัพธ์ของ E1 >> E2 คือ E1 ตำแหน่งบิต E2 ที่เลื่อนไปทางขวา ถ้า E1 มีประเภทที่ได้รับการรับรองหรือถ้า E1 มีการลงนามชนิดและค่าที่ไม่ใช่เชิงลบค่าของผลที่ได้เป็นส่วนหนึ่งของความฉลาดของ E1 / 2 E2 ถ้า E1 มีประเภทที่ลงนามและค่าติดลบค่าผลลัพธ์จะถูกกำหนดให้ใช้งานได้
การเลื่อนด้านขวาสำหรับค่าที่ไม่ได้ลงชื่อและไม่ติดลบนั้นค่อนข้างตรงไปตรงมา บิตว่างจะเต็มไปด้วยศูนย์ สำหรับค่าลบที่มีการลงนามผลลัพธ์ของการเปลี่ยนขวาจะถูกกำหนดให้นำไปใช้งาน ที่กล่าวว่าการใช้งานส่วนใหญ่เช่น GCC และVisual C ++ใช้การเปลี่ยนขวาเป็นการเปลี่ยนเลขคณิตโดยการรักษาบิตเครื่องหมาย
แตกต่างจาก Java ซึ่งมีตัวดำเนินการพิเศษ>>>
สำหรับการเปลี่ยนตรรกะนอกเหนือจากปกติ>>
และ<<
C และ C ++ มีเฉพาะการเปลี่ยนเลขคณิตโดยมีบางพื้นที่ที่ไม่ได้กำหนดและกำหนดการนำไปใช้งาน เหตุผลที่ฉันถือว่าพวกเขาเป็นเลขคณิตเกิดจากการใช้ถ้อยคำมาตรฐานในการดำเนินการทางคณิตศาสตร์แทนที่จะถือว่าตัวถูกดำเนินการเปลี่ยนเป็นกระแสของบิต นี่อาจเป็นเหตุผลว่าทำไมจึงปล่อยให้พื้นที่เหล่านั้นไม่ถูกกำหนด / กำหนดการนำไปใช้งานแทนที่จะกำหนดเฉพาะกรณีทั้งหมดเป็นการเปลี่ยนแปลงเชิงตรรกะ
-Inf
สำหรับทั้งจำนวนลบและบวก ปัดเศษต่อ 0 -Inf
ของจำนวนบวกเป็นกรณีส่วนตัวของปัดเศษต่อ เมื่อตัดทอนคุณจะทิ้งค่าที่ถ่วงน้ำหนักในเชิงบวกเสมอดังนั้นคุณจึงลบออกจากผลลัพธ์ที่แน่นอนเป็นอย่างอื่น
ในแง่ของประเภทของการเปลี่ยนแปลงที่คุณได้รับสิ่งสำคัญคือประเภทของค่าที่คุณกำลังขยับ แหล่งที่มาของจุดบกพร่องแบบคลาสสิกคือเมื่อคุณเลื่อนตัวอักษรเป็นพูดและปิดบังบิต ตัวอย่างเช่นหากคุณต้องการทิ้งจำนวนเต็มทางซ้ายสุดที่ยังไม่ได้ลงชื่อคุณอาจลองใช้เป็นมาสก์:
~0 >> 1
น่าเสียดายที่สิ่งนี้จะทำให้คุณมีปัญหาเนื่องจากมาสก์จะมีการตั้งค่าบิตทั้งหมดไว้เนื่องจากค่าที่ถูกเลื่อน (~ 0) มีการเซ็นชื่อดังนั้นจึงมีการเปลี่ยนเลขคณิต คุณต้องการบังคับให้มีการเปลี่ยนแปลงเชิงตรรกะโดยการประกาศค่าอย่างชัดเจนว่าไม่ได้ลงนามแทนกล่าวคือทำสิ่งนี้:
~0U >> 1;
นี่คือฟังก์ชั่นในการรับประกันการเลื่อนขวาเชิงตรรกะและการเลื่อนขวาทางคณิตศาสตร์ของ int ใน C:
int logicalRightShift(int x, int n) {
return (unsigned)x >> n;
}
int arithmeticRightShift(int x, int n) {
if (x < 0 && n > 0)
return x >> n | ~(~0U >> n);
else
return x >> n;
}
เมื่อคุณทำ - เลื่อนซ้ายด้วย 1 คุณคูณด้วย 2 - กะขวาด้วย 1 คุณหารด้วย 2
x = 5
x >> 1
x = 2 ( x=5/2)
x = 5
x << 1
x = 10 (x=5*2)
ฉันค้นดูในวิกิพีเดียและพวกเขามีสิ่งนี้ที่จะพูด:
อย่างไรก็ตาม C มีตัวดำเนินการกะขวาเพียงตัวเดียวคือ >> คอมไพเลอร์ C หลายตัวเลือกว่าจะเลื่อนการเปลี่ยนแปลงใดที่เหมาะสมขึ้นอยู่กับประเภทของจำนวนเต็มที่จะถูกเลื่อน จำนวนเต็มที่ลงนามมักจะถูกเลื่อนโดยใช้การเลื่อนเลขคณิตและจำนวนเต็มที่ไม่ได้ลงชื่อจะถูกเลื่อนโดยใช้การเลื่อนเชิงตรรกะ
ดูเหมือนจะขึ้นอยู่กับคอมไพเลอร์ของคุณ นอกจากนี้ในบทความนั้นโปรดทราบว่าการเลื่อนด้านซ้ายจะเหมือนกันสำหรับเลขคณิตและตรรกะ ฉันอยากจะแนะนำให้ทำการทดสอบง่ายๆด้วยตัวเลขที่ลงชื่อและไม่ได้ลงนามในกรณีชายแดน (ชุดบิตสูงแน่นอน) และดูว่าผลลัพธ์เป็นอย่างไรในคอมไพเลอร์ของคุณ ฉันขอแนะนำให้หลีกเลี่ยงการขึ้นอยู่กับว่ามันเป็นอย่างใดอย่างหนึ่งเนื่องจากดูเหมือนว่า C ไม่มีมาตรฐานอย่างน้อยก็มีเหตุผลและเป็นไปได้ที่จะหลีกเลี่ยงการพึ่งพาดังกล่าว
เลื่อนซ้าย <<
นี่เป็นวิธีที่ง่ายและเมื่อใดก็ตามที่คุณใช้ตัวดำเนินการ shift การดำเนินการที่ชาญฉลาดอยู่เสมอดังนั้นเราจึงไม่สามารถใช้กับการทำงานแบบ double และ float ได้ เมื่อใดก็ตามที่เราปล่อยให้ shift หนึ่งศูนย์มันจะถูกเพิ่มเข้าไปในบิตที่มีนัยสำคัญน้อยที่สุด ( LSB
) เสมอ
แต่ในการเปลี่ยนอย่างถูกต้อง>>
เราต้องปฏิบัติตามกฎเพิ่มเติมหนึ่งข้อและกฎนั้นเรียกว่า "sign bit copy" ความหมายของ "sign bit copy" คือถ้าMSB
มีการตั้งค่าbit ที่สำคัญที่สุด ( ) จากนั้นหลังจาก shift ขวาอีกครั้งMSB
จะถูกตั้งค่าหากรีเซ็ตแล้วจะถูกรีเซ็ตอีกครั้งหมายความว่าถ้าค่าก่อนหน้าเป็นศูนย์หลังจากเปลี่ยนอีกครั้ง บิตเป็นศูนย์ถ้าบิตก่อนหน้าเป็นหนึ่งหลังจากการเปลี่ยนแปลงอีกครั้งหนึ่ง กฎนี้ใช้ไม่ได้กับการเลื่อนซ้าย
ตัวอย่างที่สำคัญที่สุดในการกะทางขวาหากคุณเลื่อนจำนวนลบไปเป็นการกะทางขวาหลังจากนั้นค่าบางส่วนก็จะถึงศูนย์แล้วหลังจากนี้ถ้าเลื่อนค่านี้ -1 จำนวนครั้งใด ๆ ค่าจะยังคงเหมือนเดิม โปรดตรวจสอบ.
GCC ทำ
สำหรับ -ve -> การเปลี่ยนเลขคณิต
สำหรับ + ve -> Logical Shift
ตามมากมาย ค คอมไพเลอร์:
<<
คือกะซ้ายเลขคณิตหรือกะซ้ายแบบบิต>>
คือตัวเลื่อนขวาทางคณิตศาสตร์>>
เลขคณิตหรือบิต (ตรรกะ)" คุณตอบว่า " >>
เป็นเลขคณิตหรือบิต" นั่นไม่ตอบคำถาม
<<
และ>>
ตัวดำเนินการเป็นตรรกะไม่ใช่เลขคณิต