ฟังก์ชัน C นี้ควรส่งคืนค่าเท็จเสมอ แต่ไม่เป็นเช่นนั้น


317

ฉันพบคำถามที่น่าสนใจในฟอรัมมานานแล้วและต้องการทราบคำตอบ

พิจารณาฟังก์ชัน C ต่อไปนี้:

f1.c

#include <stdbool.h>

bool f1()
{
    int var1 = 1000;
    int var2 = 2000;
    int var3 = var1 + var2;
    return (var3 == 0) ? true : false;
}

นี้ควรกลับมาตั้งแต่false ฟังก์ชั่นมีลักษณะเช่นนี้var3 == 3000main

main.c

#include <stdio.h>
#include <stdbool.h>

int main()
{
    printf( f1() == true ? "true\n" : "false\n");
    if( f1() )
    {
        printf("executed\n");
    }
    return 0;
}

เนื่องจากf1()ควรกลับมาตลอดโปรแกรมfalseหนึ่งจึงคาดว่าโปรแกรมจะพิมพ์เท็จเพียงหน้าจอเดียว แต่หลังจากการรวบรวมและการทำงาน, การดำเนินการนอกจากนี้ยังจะแสดง:

$ gcc main.c f1.c -o test
$ ./test
false
executed

ทำไมถึงเป็นอย่างนั้น? รหัสนี้มีพฤติกรรมที่ไม่ได้กำหนดบางอย่างหรือไม่?

หมายเหตุ: gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2ผมรวบรวมมันด้วย


9
คนอื่นพูดถึงว่าคุณต้องการต้นแบบเพราะฟังก์ชั่นของคุณอยู่ในไฟล์แยกต่างหาก แต่แม้ว่าคุณจะคัดลอกf1()ไปไว้ในไฟล์เดียวกับmain()คุณจะได้รับความแปลก: ในขณะที่มันถูกต้องใน C ++ เพื่อใช้()สำหรับรายการพารามิเตอร์ที่ว่างเปล่าใน C ที่ใช้สำหรับฟังก์ชั่นที่มีรายการพารามิเตอร์ที่ยังไม่ได้กำหนด โดยทั่วไปคาดว่าจะมีรายการพารามิเตอร์สไตล์ K & R หลังจาก)) เพื่อให้เกิดความถูกต้อง C bool f1(void)คุณควรเปลี่ยนรหัสของคุณ
uliwitness

1
main()อาจจะง่ายไปint main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }- นี้จะแสดงให้เห็นความแตกต่างที่ดีกว่า
Palec

@uliwitness แล้ว K&R 1st ed. (1978) เมื่อไม่มีvoid?
Ho1

@uliwitness มีไม่trueและfalseใน K & R 1 เอ็ด. จึงมีไม่ปัญหาดังกล่าวได้ทั้งหมด มันเป็นเพียง 0 และไม่ใช่ศูนย์สำหรับความจริงและเท็จ ไม่ใช่เหรอ ฉันไม่รู้ว่ามีต้นแบบในเวลานั้นหรือไม่
Ho1

1
K&R 1st Edn นำหน้าต้นแบบ (และมาตรฐาน C) มากกว่าทศวรรษ (1978 สำหรับหนังสือ vs 1989 สำหรับมาตรฐาน) - แท้จริง C ++ (C พร้อมคลาส) ยังคงเป็นอนาคตเมื่อ K & R1 ได้รับการเผยแพร่ นอกจากนี้ก่อนหน้า C99 ก็ไม่มี_Boolประเภทและไม่มี<stdbool.h>ส่วนหัว
Jonathan Leffler

คำตอบ:


396

ดังที่ระบุไว้ในคำตอบอื่น ๆ ปัญหาคือคุณใช้gccโดยไม่มีตัวเลือกคอมไพเลอร์ที่ตั้งค่าไว้ หากคุณทำสิ่งนี้มันจะมีค่าเริ่มต้นสำหรับสิ่งที่เรียกว่า "gnu90" ซึ่งเป็นการใช้งานที่ไม่ได้มาตรฐานของมาตรฐานเก่าที่ถอนออกจาก C90 ตั้งแต่ปี 1990

ในมาตรฐาน C90 เก่ามีข้อบกพร่องที่สำคัญในภาษา C: ถ้าคุณไม่ได้ประกาศต้นแบบก่อนที่จะใช้ฟังก์ชั่นมันจะเริ่มต้นที่int func ()(ซึ่ง( )หมายความว่า "ยอมรับพารามิเตอร์ใด ๆ ") สิ่งนี้เปลี่ยนการเรียกการประชุมของฟังก์ชั่นfuncแต่มันไม่เปลี่ยนนิยามของฟังก์ชั่นที่แท้จริง เนื่องจากขนาดboolและintแตกต่างกันรหัสของคุณจะเรียกใช้พฤติกรรมที่ไม่ได้กำหนดเมื่อเรียกใช้ฟังก์ชัน

พฤติกรรมไร้สาระที่เป็นอันตรายนี้ได้รับการแก้ไขในปี 1999 ด้วยการเปิดตัวมาตรฐาน C99 การประกาศฟังก์ชันโดยนัยถูกห้าม

น่าเสียดายที่ GCC เป็นเวอร์ชัน 5.xx ยังคงใช้มาตรฐาน C เก่าตามค่าเริ่มต้น อาจไม่มีเหตุผลว่าทำไมคุณควรรวบรวมรหัสของคุณเป็นอะไรก็ได้ยกเว้นมาตรฐาน C ดังนั้นคุณต้องบอก GCC อย่างชัดเจนว่าควรรวบรวมรหัสของคุณเป็นรหัส C ที่ทันสมัยแทนที่จะเป็นอึ GNU อายุ 25 ปีขึ้นไปที่ไม่ได้มาตรฐาน .

แก้ไขปัญหาด้วยการคอมไพล์โปรแกรมของคุณเป็น:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 บอกให้พยายามทำแบบครึ่งใจเพื่อคอมไพล์ตามมาตรฐาน C (ปัจจุบัน) (รู้จักอย่างไม่เป็นทางการว่า C11)
  • -pedantic-errors บอกให้ทำอย่างเต็มที่ด้วยใจจริงและให้ข้อผิดพลาดคอมไพเลอร์เมื่อคุณเขียนรหัสที่ไม่ถูกต้องซึ่งละเมิดมาตรฐาน C
  • -Wall หมายถึงให้ฉันคำเตือนพิเศษบางอย่างที่อาจจะมีดี
  • -Wextra หมายถึงให้ฉันเตือนพิเศษอื่น ๆ ที่อาจจะมีดี

19
คำตอบนี้ถูกต้องโดยรวม แต่สำหรับโปรแกรมที่ซับซ้อนกว่า-std=gnu11นั้นมีแนวโน้มที่จะทำงานได้ตามที่คาดหวัง-std=c11เนื่องจากต้องการฟังก์ชันการทำงานของไลบรารีที่นอกเหนือจาก C11 (POSIX, X / Open, etc) ที่มีอยู่ใน "gnu" โหมดขยาย แต่ถูกระงับในโหมดความสอดคล้องที่เข้มงวด ข้อผิดพลาดในส่วนหัวของระบบที่ซ่อนอยู่ในโหมดขยายเช่นสมมติว่ามีความพร้อมของ typedefs ที่ไม่เป็นมาตรฐาน การใช้ Trigraphs โดยไม่ได้ตั้งใจ (การป้อนข้อมูลผิดพลาดมาตรฐานนี้ถูกปิดใช้งานในโหมด "gnu")
zwol

5
ด้วยเหตุผลที่คล้ายกันในขณะที่โดยทั่วไปฉันขอแนะนำให้ใช้ระดับการเตือนที่สูงฉันไม่สามารถรองรับการใช้โหมดคำเตือน - เป็น - ข้อผิดพลาดได้ -pedantic-errorsมีปัญหาน้อยกว่า-Werrorแต่ทั้งสองอย่างสามารถและทำให้โปรแกรมไม่สามารถคอมไพล์ในระบบปฏิบัติการที่ไม่รวมอยู่ในการทดสอบของผู้เขียนต้นฉบับแม้ว่าจะไม่มีปัญหาจริงก็ตาม
zwol

7
@Lundin ในทางตรงกันข้ามปัญหาที่สองที่ผมกล่าวถึง (ข้อบกพร่องในส่วนหัวของระบบที่ได้รับการเปิดเผยโดยโหมดสอดคล้องเข้มงวด) จะแพร่หลาย ; ฉันได้ทำการทดสอบอย่างเป็นระบบและไม่มีระบบปฏิบัติการที่ใช้กันอย่างแพร่หลายและไม่มีข้อผิดพลาดดังกล่าวอย่างน้อยหนึ่งข้อ (เหมือนเมื่อสองปีก่อน) โปรแกรม C ที่ต้องการเฉพาะฟังก์ชั่นการทำงานของ C11 ที่ไม่มีการเพิ่มเติมใด ๆ ก็เป็นข้อยกเว้นมากกว่ากฎในประสบการณ์ของฉัน
zwol

6
@joop หากคุณใช้ C bool/ มาตรฐาน_Boolคุณสามารถเขียนรหัส C ของคุณในแบบ "C ++ - esque" โดยที่คุณคิดว่าการเปรียบเทียบทั้งหมดและตัวดำเนินการเชิงตรรกะกลับมาboolเหมือนใน C ++ แม้ว่าพวกเขาจะส่งกลับเป็นintC ด้วยเหตุผลทางประวัติศาสตร์ . นี่เป็นข้อได้เปรียบที่ยอดเยี่ยมที่คุณสามารถใช้เครื่องมือวิเคราะห์แบบคงที่เพื่อตรวจสอบความปลอดภัยของประเภทของนิพจน์ทั้งหมดและแสดงข้อผิดพลาดทุกชนิดในเวลาคอมไพล์ นอกจากนี้ยังเป็นวิธีการแสดงเจตนาในรูปแบบของรหัสเอกสารด้วยตนเอง และที่สำคัญน้อยกว่ามันยังช่วยประหยัด RAM ไม่กี่ไบต์
Lundin

7
โปรดทราบว่าสิ่งใหม่ส่วนใหญ่ใน C99 มาจากอึ GNU อายุ 25 ปีขึ้นไป
Shahbaz

141

คุณไม่จำเป็นต้องเป็นแบบอย่างที่ดีสำหรับการประกาศf1()ใน main.c จึงถูกกำหนดโดยปริยายเป็นความหมายมันคือฟังก์ชั่นที่ใช้เวลาไม่ทราบจำนวนของการขัดแย้งและผลตอบแทนอีกด้วยint f1()int

ถ้าintและboolมีขนาดที่แตกต่างกันนี้จะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด ตัวอย่างเช่นบนเครื่องของฉันintคือ 4 ไบต์และboolเป็นหนึ่งไบต์ เนื่องจากฟังก์ชันถูกกำหนดให้ส่งคืนboolมันจะวางหนึ่งไบต์บนสแต็กเมื่อส่งคืน อย่างไรก็ตามเนื่องจากมีการประกาศโดยปริยายว่าจะส่งคืนintจาก main.c ฟังก์ชันการเรียกจะพยายามอ่าน 4 ไบต์จากสแต็ก

ตัวเลือกคอมไพเลอร์เริ่มต้นใน gcc จะไม่บอกคุณว่ากำลังทำสิ่งนี้อยู่ แต่ถ้าคุณคอมไพล์ด้วย-Wall -Wextraคุณจะได้สิ่งนี้:

main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’

ในการแก้ไขปัญหานี้ให้เพิ่มการประกาศสำหรับf1ใน main.c ก่อนmain:

bool f1(void);

โปรดทราบว่ารายการอาร์กิวเมนต์มีการตั้งค่าอย่างชัดเจนvoidซึ่งบอกคอมไพเลอร์ฟังก์ชั่นจะไม่ขัดแย้งเมื่อเทียบกับรายการพารามิเตอร์ที่ว่างเปล่าซึ่งหมายถึงจำนวนอาร์กิวเมนต์ที่ไม่รู้จัก คำจำกัดความf1ใน f1.c ควรเปลี่ยนเพื่อสะท้อนถึงสิ่งนี้


2
บางสิ่งที่ฉันเคยทำในโครงการของฉัน (เมื่อฉันยังใช้ GCC) ได้ถูกเพิ่ม-Werror-implicit-function-declarationเข้าไปในตัวเลือกของ GCC ด้วยวิธีนี้สิ่งนี้จะไม่ผ่านพ้นไปอีกต่อไป ทางเลือกที่ดียิ่งขึ้นคือ-Werrorการเปลี่ยนคำเตือนทั้งหมดเป็นข้อผิดพลาด บังคับให้คุณแก้ไขคำเตือนทั้งหมดเมื่อปรากฏขึ้น
uliwitness

2
คุณไม่ควรใช้วงเล็บว่างเพราะการทำเช่นนั้นเป็นคุณสมบัติที่ล้าสมัย หมายความว่าพวกเขาสามารถแบนรหัสดังกล่าวได้ในเวอร์ชันถัดไปของมาตรฐาน C
Lundin

1
@uliwitness Ah ข้อมูลที่ดีสำหรับผู้ที่มาจาก C ++ ที่ตะลุยใน C เท่านั้น
SeldomNeedy

ค่าส่งคืนมักจะไม่ใส่ลงในสแต็ก แต่ลงในการลงทะเบียน ดูคำตอบของโอเว่น นอกจากนี้คุณมักจะไม่ใส่หนึ่งไบต์ลงในสแต็ก แต่มีขนาดของคำจำนวนมาก
rsanchez

GCC รุ่นใหม่กว่า (5.xx) ให้คำเตือนนั้นโดยไม่มีการตั้งค่าสถานะพิเศษ
โอเวอร์

36

ฉันคิดว่ามันน่าสนใจที่จะเห็นว่าขนาดที่ไม่ตรงกันในคำตอบที่ยอดเยี่ยมของ Lundin เกิดขึ้นจริง

หากคุณรวบรวม--save-tempsคุณจะได้รับไฟล์ประกอบที่คุณสามารถดู นี่คือส่วนที่f1()จะทำการ== 0เปรียบเทียบและคืนค่า:

cmpl    $0, -4(%rbp)
sete    %al

sete %alส่วนที่กลับมาเป็น ใน C x 86 เรียกประชุมค่าตอบแทน 4 ไบต์หรือเล็กกว่า (ซึ่งรวมถึงintและbool) %eaxจะถูกส่งกลับผ่านการลงทะเบียน เป็นไบต์ต่ำสุดของ%al %eaxดังนั้น 3 ไบต์บนของ%eaxจะถูกปล่อยให้อยู่ในสถานะที่ไม่สามารถควบคุมได้

ตอนนี้ในmain():

call    f1
testl   %eax, %eax
je  .L2

การตรวจสอบนี้ไม่ว่าจะเป็นทั้งการ%eaxเป็นศูนย์เพราะมันคิดว่ามันเป็นทดสอบ int

การเพิ่มการประกาศฟังก์ชันที่ชัดเจนเปลี่ยนmain()ไปเป็น:

call    f1
testb   %al, %al
je  .L2

ซึ่งเป็นสิ่งที่เราต้องการ


27

กรุณารวบรวมด้วยคำสั่งเช่นนี้:

gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c

เอาท์พุท:

main.c: In function 'main':
main.c:14:5: error: implicit declaration of function 'f1' [-Werror=impl
icit-function-declaration]
     printf( f1() == true ? "true\n" : "false\n");
     ^
cc1.exe: all warnings being treated as errors

ด้วยข้อความเช่นนี้คุณควรรู้ว่าต้องทำอย่างไรเพื่อแก้ไข

แก้ไข: หลังจากอ่านความคิดเห็น (ลบแล้ว) ฉันพยายามรวบรวมรหัสของคุณโดยไม่มีการตั้งค่าสถานะ ดีนี้ทำให้ฉันไปข้อผิดพลาด linker โดยไม่มีคำเตือนของคอมไพเลอร์แทนข้อผิดพลาดของคอมไพเลอร์ และข้อผิดพลาด linker เหล่านั้นยากต่อการเข้าใจดังนั้นแม้ว่า-std-gnu99ไม่จำเป็นโปรดลองใช้ allways อย่างน้อย-Wall -Werrorมันจะช่วยให้คุณเจ็บปวดมากในตูด

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