สำหรับโค้ดที่ฝังตัวทำไมฉันจึงควรใช้ประเภท“ uint_t” แทน“ int ที่ไม่ได้ลงชื่อ”


22

ฉันกำลังเขียนแอปพลิเคชันใน c สำหรับ STM32F105 โดยใช้ gcc

ในอดีตที่ผ่านมา (กับโครงการง่าย) ผมได้กำหนดไว้เสมอเป็นตัวแปรchar, int, unsigned intและอื่น ๆ

ผมเห็นว่ามันเป็นเรื่องธรรมดาที่จะใช้ประเภทที่กำหนดไว้ใน stdint.h เช่นint8_t, uint8_t, uint32_tฯลฯ นี้มันเป็นจริงในหลาย API ที่ฉันใช้และยังอยู่ในห้องสมุด ARM CMSIS จาก ST

ฉันเชื่อว่าฉันเข้าใจว่าทำไมเราควรทำเช่นนั้น เพื่อให้คอมไพเลอร์เพิ่มประสิทธิภาพพื้นที่หน่วยความจำให้ดีขึ้น ฉันคาดว่าอาจมีเหตุผลเพิ่มเติม

แต่เนื่องจากกฎการส่งเสริมจำนวนเต็มคผมให้ทำงานขึ้นกับคำเตือนการแปลงเวลาใด ๆ ที่ฉันพยายามที่จะเพิ่มค่าสองค่าดำเนินการค่าที่เหมาะสม ฯลฯ conversion to 'uint16_t' from 'int' may alter its value [-Wconversion]เตือนอ่านสิ่งที่ชอบ ปัญหาที่จะกล่าวถึงที่นี่และที่นี่

มันไม่ได้เกิดขึ้นเมื่อใช้ตัวแปรประกาศเป็นหรือintunsigned int

ในการให้ตัวอย่างสองสามตัวอย่างให้สิ่งนี้:

uint16_t value16;
uint8_t value8;

ฉันจะต้องเปลี่ยนสิ่งนี้:

value16 <<= 8;
value8 += 2;

สำหรับสิ่งนี้:

value16 = (uint16_t)(value16 << 8);
value8 = (uint8_t)(value8 + 2);

มันน่าเกลียด แต่ฉันสามารถทำได้ถ้าจำเป็น นี่คือคำถามของฉัน:

  1. มีกรณีที่การแปลงจากที่ไม่ได้ลงชื่อไปสู่การลงชื่อและกลับสู่การไม่ได้ลงชื่อจะทำให้ผลลัพธ์ไม่ถูกต้องหรือไม่

  2. มีเหตุผลใหญ่อื่น ๆ สำหรับ / กับการใช้ชนิดจำนวนเต็ม stdint.h?

จากคำตอบที่ฉันได้รับดูเหมือนว่าประเภท stdint.h จะเป็นที่ต้องการโดยทั่วไปแม้ว่า c จะแปลงuintไปintและกลับ สิ่งนี้นำไปสู่คำถามที่ใหญ่กว่า:

  1. ฉันสามารถป้องกันคำเตือนของคอมไพเลอร์โดยใช้ typecasting (เช่นvalue16 = (uint16_t)(value16 << 8);) ฉันเพิ่งซ่อนปัญหาหรือไม่ มีวิธีที่ดีกว่าที่จะไปเกี่ยวกับเรื่องนี้?

ใช้ตัวอักษรที่ไม่ได้ลงชื่อ: IE และ8u 2u
หยุดทำร้ายโมนิก้า

ขอบคุณ @OrangeDog ฉันคิดว่าฉันเข้าใจผิด ฉันพยายามทั้งสองvalue8 += 2u;และvalue8 = value8 + 2u;แต่ฉันได้รับคำเตือนเดียวกัน
bitsmack

ใช้เพื่อหลีกเลี่ยงคำเตือนที่ลงชื่อเมื่อคุณยังไม่มีคำเตือนแบบกว้าง :)
หยุดทำร้ายโมนิก้า

คำตอบ:


11

คอมไพเลอร์ที่intเป็นไปตามมาตรฐานที่ใดก็ได้จาก 17-32 บิตอาจทำสิ่งที่ถูกต้องตามกฎหมายด้วยรหัสต่อไปนี้:

uint16_t x = 46341;
uint32_t y = x*x; // temp result is signed int, which can't hold 2147488281

การใช้งานที่ต้องการจะทำเช่นนั้นอย่างถูกต้องสามารถสร้างโปรแกรมที่จะไม่ทำอะไรเลยนอกจากเอาท์พุทสตริง "เฟร็ด" ซ้ำ ๆ บนทุกพอร์ตพินโดยใช้โปรโตคอลที่สามารถจินตนาการได้ทั้งหมด ความน่าจะเป็นของโปรแกรมที่นำเข้าสู่การปรับใช้ซึ่งจะทำสิ่งนั้นต่ำมาก แต่เป็นไปได้ในทางทฤษฎี หากต้องการอยากจะเขียนโค้ดข้างต้นเพื่อที่มันจะได้รับการรับประกันไม่ได้ที่จะมีส่วนร่วมในพฤติกรรมที่ไม่ได้กำหนดก็จำเป็นจะต้องมีการเขียนการแสดงออกหลังเป็นหรือ(uint32_t)x*x 1u*x*xบนคอมไพเลอร์ที่intอยู่ระหว่าง 17 ถึง 31 บิตนิพจน์หลังจะลิดจากบิตด้านบน แต่จะไม่เข้าร่วมในพฤติกรรมที่ไม่ได้กำหนด

ฉันคิดว่าคำเตือน gcc อาจพยายามแนะนำว่ารหัสที่เขียนไม่สามารถพกพาได้อย่างสมบูรณ์ 100% มีบางครั้งที่ควรเขียนโค้ดเพื่อหลีกเลี่ยงพฤติกรรมที่จะไม่ได้กำหนดไว้ในการติดตั้งใช้งานบางอย่าง แต่ในหลาย ๆ กรณีเราควรคิดว่าโค้ดไม่น่าจะถูกนำไปใช้กับการใช้งานซึ่งจะทำสิ่งที่น่ารำคาญมากเกินไป

โปรดทราบว่าการใช้ประเภทเช่นintและshortอาจกำจัดคำเตือนบางอย่างและแก้ไขปัญหาบางอย่าง แต่มีแนวโน้มว่าจะสร้างประเภทอื่น ๆ การทำงานร่วมกันระหว่างประเภทเช่นuint16_tและกฎการส่งเสริมการจำนวนเต็มของ C นั้นน่ากลัว แต่ประเภทดังกล่าวอาจยังดีกว่าทางเลือกอื่น ๆ


6

1) หากคุณเพิ่งแปลงจากจำนวนเต็มแบบไม่ได้ลงนามไปเป็นลายเซ็นที่มีความยาวเท่ากันกลับไปกลับมาโดยไม่มีการดำเนินการใด ๆ ในระหว่างนั้นคุณจะได้รับผลลัพธ์เดียวกันทุกครั้งดังนั้นจึงไม่มีปัญหาที่นี่ แต่การดำเนินการเชิงตรรกะและเชิงคณิตศาสตร์ต่างๆนั้นทำหน้าที่แตกต่างกันในโอเปอแรนด์ที่ลงชื่อและไม่ได้ลงชื่อ
2) เหตุผลหลักที่จะใช้stdint.hประเภทคือว่าขนาดบิตของประเภทดังกล่าวจะมีการกำหนดและเท่าเทียมกันในทุกแพลตฟอร์มซึ่งไม่เป็นความจริงสำหรับint, longฯลฯ เช่นเดียวกับcharไม่มี signess มาตรฐานก็สามารถลงนามหรือไม่ได้ลงนามโดย ค่าเริ่มต้น. ทำให้ง่ายต่อการจัดการข้อมูลที่รู้ขนาดที่แน่นอนโดยไม่ต้องใช้การตรวจสอบและการตั้งสมมติฐานเพิ่มเติม


2
ขนาดของint32_tและuint32_tมีค่าเท่ากันข้ามแพลตฟอร์มทั้งหมดที่พวกเขามีการกำหนด หากโปรเซสเซอร์ไม่มีประเภทฮาร์ดแวร์ที่ตรงกันประเภทเหล่านี้จะไม่ถูกกำหนด ดังนั้นข้อดีของintฯลฯ และบางทีint_least32_tฯลฯ
Pete Becker

1
@PeteBecker - นั่นเป็นข้อได้เปรียบเนื่องจากข้อผิดพลาดในการรวบรวมทำให้คุณทราบถึงปัญหาทันที ฉันจะค่อนข้างดีกว่าประเภทของฉันที่เปลี่ยนขนาดกับฉัน
sapi

@sapi - ในหลาย ๆ สถานการณ์ขนาดพื้นฐานไม่เกี่ยวข้อง โปรแกรมเมอร์ C ทำงานได้ดีโดยไม่มีขนาดคงที่เป็นเวลาหลายปี
Pete Becker

6

เนื่องจากอันดับ # 2 ของ Eugene น่าจะเป็นจุดที่สำคัญที่สุดฉันแค่ต้องการเพิ่มเติมว่าเป็นคำแนะนำ

MISRA (directive 4.6): "typedefs that indicate size and signedness should be used in place of the basic types".

Jack Ganssle ก็ดูเหมือนจะเป็นผู้สนับสนุนกฎดังกล่าว: http://www.ganssle.com/tem/tem265.html


2
มันแย่มากที่ไม่มีประเภทใด ๆ สำหรับระบุ "จำนวนเต็ม N-bit ที่ไม่ได้ลงชื่อซึ่งอาจถูกคูณด้วยจำนวนเต็มขนาดเดียวกันอื่น ๆ อย่างปลอดภัยเพื่อให้ได้ผลลัพธ์ที่มีขนาดเท่ากัน" uint32_tกฎการส่งเสริมจำนวนเต็มโต้ตอบอย่างน่ากลัวกับชนิดที่มีอยู่เช่น
supercat

3

วิธีง่ายๆในการกำจัดคำเตือนคือหลีกเลี่ยงการใช้ -Wconversion ใน GCC ฉันคิดว่าคุณต้องเปิดใช้งานตัวเลือกนี้ด้วยตนเอง แต่ถ้าไม่คุณสามารถใช้ -Wno-conversion เพื่อปิดการใช้งาน คุณสามารถเปิดใช้งานคำเตือนสำหรับการแปลงเครื่องหมายและความแม่นยำของ FP ผ่านตัวเลือกอื่น ๆหากคุณยังต้องการ

คำเตือน -Wconversion มักเป็นผลบวกที่ผิดพลาดซึ่งอาจเป็นเหตุผลว่าทำไมถึงไม่ -Wextra จะเปิดใช้งานตามค่าเริ่มต้น คำถามกองมากเกินมีจำนวนมากของข้อเสนอแนะสำหรับชุดตัวเลือกที่ดี จากประสบการณ์ของฉันเองนี่เป็นจุดเริ่มต้นที่ดี:

-std = c99 -pedantic -Wall -Wextra -Wshadow

เพิ่มมากขึ้นถ้าคุณต้องการพวกเขา แต่คุณจะไม่ได้ราคา

หากคุณต้องเก็บไว้ - การแปลงคุณสามารถทำให้รหัสของคุณสั้นลงโดยพิมพ์ตัวถูกดำเนินการที่เป็นตัวเลขเท่านั้น:

value16 <<= (uint16_t)8;
value8 += (uint8_t)2;

ไม่ใช่เรื่องง่ายที่จะอ่านหากไม่มีการเน้นไวยากรณ์


2

ในโครงการซอฟต์แวร์ใด ๆ สิ่งสำคัญคือต้องใช้คำจำกัดความชนิดพกพา (แม้แต่เวอร์ชั่นถัดไปของคอมไพเลอร์ตัวเดียวกันก็ต้องพิจารณาข้อนี้ด้วย) ตัวอย่างที่ดีหลายปีที่ผ่านมาฉันทำงานโครงการที่คอมไพเลอร์ปัจจุบันกำหนด 'int' เป็น 8bits คอมไพเลอร์รุ่นต่อไปกำหนด 'int' เป็น 16 บิต เนื่องจากเราไม่ได้ใช้คำจำกัดความแบบพกพาสำหรับ 'int' หน่วยความจำ (อย่างมีประสิทธิภาพ) จะเพิ่มขนาดเป็นสองเท่าและลำดับรหัสจำนวนมากที่ขึ้นอยู่กับ 8 บิต int ล้มเหลว การใช้คำจำกัดความชนิดพกพาจะหลีกเลี่ยงปัญหา (หลายร้อยชั่วโมงคนเพื่อแก้ไข) ปัญหา


ไม่มีรหัสที่สมเหตุสมผลควรใช้intเพื่ออ้างถึงประเภท 8 บิต แม้ว่าคอมไพเลอร์ที่ไม่ใช่ C เช่น CCS ทำเช่นนั้นรหัสที่เหมาะสมควรใช้อย่างใดอย่างหนึ่งcharหรือประเภท typedefed สำหรับ 8 บิตและประเภท typedefed (ไม่ "ยาว") สำหรับ 16 บิต ในทางกลับกันการย้ายรหัสจากบางอย่างเช่น CCS ไปยังคอมไพเลอร์ตัวจริงมักจะเป็นปัญหาแม้ว่าจะใช้ typedefs ที่เหมาะสมเนื่องจากคอมไพเลอร์เหล่านี้มักจะ "ผิดปกติ" ในรูปแบบอื่น
supercat

1
  1. ใช่. จำนวนเต็มที่ลงนามโดย n-bit สามารถเป็นตัวแทนของจำนวนที่ไม่เป็นลบได้ประมาณครึ่งหนึ่งเป็นจำนวนเต็มที่ไม่ได้ลงนามแบบ n-bit และการใช้คุณสมบัติโอเวอร์โฟลว์นั้นเป็นพฤติกรรมที่ไม่ได้กำหนดไว้ ตัวประมวลผลปัจจุบันและอดีตส่วนใหญ่ใช้ twos complement จึงมีการดำเนินการจำนวนมากที่ทำสิ่งเดียวกันกับประเภทอินทิกรัลที่ลงชื่อและไม่ได้ลงนาม แต่ถึงกระนั้นการดำเนินการทั้งหมดก็อาจไม่ได้ผลลัพธ์ที่เหมือนกัน คุณจะถามถึงปัญหาเพิ่มเติมในภายหลังเมื่อคุณไม่ทราบสาเหตุที่รหัสของคุณใช้งานไม่ได้

  2. ในขณะที่intและไม่ได้ลงนามมีขนาดที่กำหนดการใช้งานเหล่านี้มักจะเลือก "ฉลาด" โดยการดำเนินการอย่างใดอย่างหนึ่งด้วยเหตุผลขนาดหรือความเร็ว ฉันมักจะยึดติดกับสิ่งเหล่านี้เว้นแต่ฉันจะมีเหตุผลที่ดีในการทำอย่างอื่น ในทำนองเดียวกันเมื่อพิจารณาว่าจะใช้ int หรือไม่ได้ลงนามฉันมักจะชอบ int เว้นแต่ว่าฉันมีเหตุผลที่ดีที่จะทำเช่นนั้น

ในกรณีที่ฉันต้องการการควบคุมขนาดหรือการลงนามของประเภทที่ดีกว่าฉันมักจะชอบใช้ typedef ที่ระบบกำหนด (size_t, intmax_t, ฯลฯ ) หรือทำให้ typedef ของตัวเองซึ่งระบุหน้าที่ของ ประเภท (prng_int, adc_int ฯลฯ )


0

บ่อยครั้งที่รหัสนี้ใช้กับ ARM thumb และ AVR (และ x86, powerPC และสถาปัตยกรรมอื่น ๆ ) และ16 หรือ 32 บิตอาจมีประสิทธิภาพมากกว่า (ทั้งสองวิธี: แฟลชและรอบ) บน STM32 ARM แม้สำหรับตัวแปรที่เหมาะกับ 8 บิต ( 8 บิตมีประสิทธิภาพมากขึ้นใน AVR) อย่างไรก็ตามถ้า SRAM เกือบเต็มแล้วการย้อนกลับเป็น 8 บิตสำหรับ vars ทั่วโลกอาจสมเหตุสมผล (แต่ไม่ใช่สำหรับ vars ในพื้นที่) สำหรับการพกพาและการบำรุงรักษา (โดยเฉพาะอย่างยิ่งสำหรับ 8 บิต vars) มันมีข้อดี (โดยไม่มีข้อเสีย) เพื่อระบุขนาดที่เหมาะสมขั้นต่ำแทนขนาดที่แน่นอนและ typedef ที่เดียว. h (ปกติภายใต้ ifdef) เพื่อปรับแต่ง uint_least8_t) ระหว่างการย้ายเวลา / สร้างเวลาเช่น:

// apparently uint16_t is just as efficient as 32 bit on STM32, but 8 bit is punished (with more flash and cycles)
typedef uint16_t uintG8_t; // 8bit if SRAM is scarce (use fol global vars that fit in 8 bit)
typedef uint16_t uintL8_t; // 8bit on AVR (local var, 16 or 32 bit is more efficient on STM + less flash)
// might better reserve 32 bits on some arch, STM32 seems efficient with 16 bits:
typedef uint16_t uintG16_t; // 16bit if SRAM is scarce (use fol global vars that fit in 16 bit)
typedef uint16_t uintL16_t; // 16bit on AVR (local var, 16 or 32 bit whichever is more efficient on other arch)

ห้องสมุด GNUช่วยเล็กน้อย แต่โดยปกติแล้วคนที่พิมพ์ดีดก็มีเหตุผลอยู่ดี:

typedef uint_least8_t uintG8_t;
typedef uint_fast8_t uintL8_t;

// แต่ uint_fast8_t สำหรับทั้งสองเมื่อ SRAM ไม่ใช่ข้อกังวล

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.