ฉันมักจะเห็นตัวอย่างและกรณีที่การใช้มาโครดีกว่าการใช้ฟังก์ชัน
ใครช่วยอธิบายตัวอย่างข้อเสียของมาโครเทียบกับฟังก์ชันได้ไหม
ฉันมักจะเห็นตัวอย่างและกรณีที่การใช้มาโครดีกว่าการใช้ฟังก์ชัน
ใครช่วยอธิบายตัวอย่างข้อเสียของมาโครเทียบกับฟังก์ชันได้ไหม
คำตอบ:
มาโครมีข้อผิดพลาดได้ง่ายเนื่องจากอาศัยการแทนที่ข้อความและไม่ทำการตรวจสอบประเภท ตัวอย่างเช่นมาโครนี้:
#define square(a) a * a
ทำงานได้ดีเมื่อใช้กับจำนวนเต็ม:
square(5) --> 5 * 5 --> 25
แต่ทำสิ่งที่แปลกมากเมื่อใช้กับนิพจน์:
square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice
การใส่วงเล็บรอบอาร์กิวเมนต์ช่วยได้ แต่ไม่สามารถขจัดปัญหาเหล่านี้ได้ทั้งหมด
เมื่อมาโครมีหลายคำสั่งคุณอาจมีปัญหากับโครงสร้างโฟลว์การควบคุม:
#define swap(x, y) t = x; x = y; y = t;
if (x < y) swap(x, y); -->
if (x < y) t = x; x = y; y = t; --> if (x < y) { t = x; } x = y; y = t;
กลยุทธ์ปกติในการแก้ไขปัญหานี้คือการใส่ข้อความไว้ในลูป "do {... } while (0)"
หากคุณมีสองโครงสร้างที่มีเขตข้อมูลที่มีชื่อเดียวกัน แต่มีความหมายต่างกันแมโครเดียวกันอาจใช้ได้กับทั้งสองอย่างโดยมีผลลัพธ์แปลก ๆ :
struct shirt
{
int numButtons;
};
struct webpage
{
int numButtons;
};
#define num_button_holes(shirt) ((shirt).numButtons * 4)
struct webpage page;
page.numButtons = 2;
num_button_holes(page) -> 8
ในที่สุดมาโครอาจแก้ไขจุดบกพร่องได้ยากทำให้เกิดข้อผิดพลาดทางไวยากรณ์แปลก ๆ หรือข้อผิดพลาดรันไทม์ที่คุณต้องขยายเพื่อทำความเข้าใจ (เช่นด้วย gcc -E) เนื่องจากดีบักเกอร์ไม่สามารถผ่านมาโครได้ดังตัวอย่างนี้:
#define print(x, y) printf(x y) /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */
ฟังก์ชันและค่าคงที่แบบอินไลน์ช่วยหลีกเลี่ยงปัญหาเหล่านี้กับมาโครได้มากมาย แต่ก็ไม่สามารถใช้ได้เสมอไป ในกรณีที่ใช้มาโครเพื่อระบุพฤติกรรมหลายรูปแบบโดยเจตนาความแตกต่างโดยไม่ตั้งใจอาจหลีกเลี่ยงได้ยาก C ++ มีคุณสมบัติมากมายเช่นเทมเพลตที่ช่วยสร้างโครงสร้างโพลีมอร์ฟิกที่ซับซ้อนในรูปแบบที่ปลอดภัยโดยไม่ต้องใช้มาโคร ดูรายละเอียดในภาษาการเขียนโปรแกรม C ++ของ Stroustrup
x++*x++
จึงไม่สามารถบอกได้ว่าเพิ่มขึ้นx
สองครั้ง มันเรียกใช้พฤติกรรมที่ไม่ได้กำหนดซึ่งหมายความว่าคอมไพเลอร์มีอิสระที่จะทำทุกอย่างที่ต้องการ - มันอาจเพิ่มขึ้นx
สองครั้งหรือครั้งเดียวหรือไม่ทำเลยก็ได้ มันอาจจะยกเลิกการมีข้อผิดพลาดหรือแม้กระทั่งทำให้ปีศาจบินออกจากจมูกของคุณ
คุณสมบัติมาโคร :
คุณสมบัติของฟังก์ชัน :
ผลข้างเคียงเป็นเรื่องใหญ่ นี่เป็นกรณีทั่วไป:
#define min(a, b) (a < b ? a : b)
min(x++, y)
ขยายเป็น:
(x++ < y ? x++ : y)
x
ได้รับการเพิ่มขึ้นสองครั้งในคำสั่งเดียวกัน (และพฤติกรรมที่ไม่ได้กำหนด)
การเขียนมาโครหลายบรรทัดก็เป็นความเจ็บปวดเช่นกัน:
#define foo(a,b,c) \
a += 10; \
b += 10; \
c += 10;
พวกเขาต้องการ\
ที่ส่วนท้ายของแต่ละบรรทัด
มาโครไม่สามารถ "ส่งคืน" อะไรได้เว้นแต่คุณจะทำให้เป็นนิพจน์เดียว:
int foo(int *a, int *b){
side_effect0();
side_effect1();
return a[0] + b[0];
}
ไม่สามารถทำได้ในมาโครเว้นแต่คุณจะใช้คำสั่งนิพจน์ของ GCC (แก้ไข: คุณสามารถใช้ตัวดำเนินการลูกน้ำแม้ว่า ... มองข้ามสิ่งนั้นไป ... แต่อาจยังอ่านได้น้อยกว่า)
ลำดับปฏิบัติการ: (เอื้อเฟื้อโดย @ouah)
#define min(a,b) (a < b ? a : b)
min(x & 0xFF, 42)
ขยายเป็น:
(x & 0xFF < 42 ? x & 0xFF : 42)
แต่&
มีความสำคัญต่ำกว่า<
. ดังนั้น0xFF < 42
ได้รับการประเมินก่อน
min(a & 0xFF, 42)
#define SQUARE(x) ((x)*(x))
int main() {
int x = 2;
int y = SQUARE(x++); // Undefined behavior even though it doesn't look
// like it here
return 0;
}
ในขณะที่:
int square(int x) {
return x * x;
}
int main() {
int x = 2;
int y = square(x++); // fine
return 0;
}
struct foo {
int bar;
};
#define GET_BAR(f) ((f)->bar)
int main() {
struct foo f;
int a = GET_BAR(&f); // fine
int b = GET_BAR(&a); // error, but the message won't make much sense unless you
// know what the macro does
return 0;
}
เปรียบเทียบกับ:
struct foo {
int bar;
};
int get_bar(struct foo *f) {
return f->bar;
}
int main() {
struct foo f;
int a = get_bar(&f); // fine
int b = get_bar(&a); // error, but compiler complains about passing int* where
// struct foo* should be given
return 0;
}
หากมีข้อสงสัยให้ใช้ฟังก์ชัน (หรือฟังก์ชันอินไลน์)
อย่างไรก็ตามคำตอบส่วนใหญ่จะอธิบายถึงปัญหาเกี่ยวกับมาโครแทนที่จะมองง่ายๆว่ามาโครเป็นสิ่งชั่วร้ายเพราะอาจเกิดอุบัติเหตุโง่ ๆ ได้
คุณสามารถตระหนักถึงข้อผิดพลาดและเรียนรู้ที่จะหลีกเลี่ยง จากนั้นใช้มาโครเมื่อมีเหตุผลที่ดีเท่านั้น
มีบางกรณีพิเศษที่มีข้อดีในการใช้มาโคร ได้แก่ :
va_args
C __FILE__
, __LINE__
, __func__
) ตรวจสอบเงื่อนไขก่อน / หลังassert
ความล้มเหลวหรือแม้กระทั่งการยืนยันแบบคงที่ดังนั้นโค้ดจะไม่รวบรวมเมื่อใช้งานที่ไม่เหมาะสม (ส่วนใหญ่มีประโยชน์สำหรับการสร้างดีบัก)struct
สมาชิกที่มีอยู่ก่อนที่จะหล่อfunc(FOO, "FOO");
คุณสามารถกำหนดมาโครที่ขยายสตริงให้คุณfunc_wrapper(FOO);
inline
ฟังก์ชั่นอาจเป็นตัวเลือก)เป็นที่ยอมรับว่าบางส่วนอาศัยส่วนขยายของคอมไพเลอร์ซึ่งไม่ใช่มาตรฐาน C ซึ่งหมายความว่าคุณอาจมีโค้ดแบบพกพาน้อยกว่าหรือต้องใช้โค้ดifdef
เหล่านี้ดังนั้นจึงถูกใช้ประโยชน์จากเมื่อคอมไพเลอร์รองรับเท่านั้น
สังเกตนี้ตั้งแต่หนึ่งของสาเหตุที่พบบ่อยที่สุดของความผิดพลาดในแมโคร(ผ่านในx++
เช่นที่แมโครอาจเพิ่มขึ้นหลายครั้ง)
เป็นไปได้ที่จะเขียนมาโครที่หลีกเลี่ยงผลข้างเคียงด้วยการโต้ตอบหลาย ๆ ครั้ง
หากคุณต้องการมีsquare
มาโครที่ใช้ได้กับประเภทต่างๆและรองรับ C11 คุณสามารถทำได้ ...
inline float _square_fl(float a) { return a * a; }
inline double _square_dbl(float a) { return a * a; }
inline int _square_i(int a) { return a * a; }
inline unsigned int _square_ui(unsigned int a) { return a * a; }
inline short _square_s(short a) { return a * a; }
inline unsigned short _square_us(unsigned short a) { return a * a; }
/* ... long, char ... etc */
#define square(a) \
_Generic((a), \
float: _square_fl(a), \
double: _square_dbl(a), \
int: _square_i(a), \
unsigned int: _square_ui(a), \
short: _square_s(a), \
unsigned short: _square_us(a))
นี้เป็นส่วนขยายของคอมไพเลอร์สนับสนุนโดย GCC, เสียงดังกราว, EKOPath อินเทล c ++ ( แต่ไม่ MSVC) ;
#define square(a_) __extension__ ({ \
typeof(a_) a = (a_); \
(a * a); })
ดังนั้นข้อเสียของมาโครคือคุณจำเป็นต้องรู้เพื่อใช้สิ่งเหล่านี้เพื่อเริ่มต้นและไม่ได้รับการสนับสนุนอย่างกว้างขวาง
ประโยชน์อย่างหนึ่งคือในกรณีนี้คุณสามารถใช้square
ฟังก์ชันเดียวกันได้หลายประเภท
ไม่มีการตรวจสอบประเภทของพารามิเตอร์และโค้ดซ้ำซึ่งอาจนำไปสู่การขยายโค้ด ไวยากรณ์มาโครยังสามารถนำไปสู่กรณีขอบแปลก ๆ จำนวนเท่าใดก็ได้ที่กึ่งโคลอนหรือลำดับความสำคัญสามารถเข้ามาขวางทางได้ นี่คือลิงค์ที่แสดงให้เห็นถึงความชั่วร้ายของมาโคร
ข้อเสียเปรียบประการหนึ่งของมาโครคือดีบักเกอร์อ่านซอร์สโค้ดซึ่งไม่มีมาโครขยายดังนั้นการเรียกใช้ดีบักเกอร์ในมาโครจึงไม่จำเป็นต้องมีประโยชน์ ไม่จำเป็นต้องพูดว่าคุณไม่สามารถตั้งค่าเบรกพอยต์ภายในมาโครได้เช่นเดียวกับที่คุณสามารถทำได้ด้วยฟังก์ชัน
ฟังก์ชั่นทำการตรวจสอบประเภท ช่วยเพิ่มความปลอดภัยอีกชั้น
กำลังเพิ่มคำตอบนี้ ..
มาโครจะถูกแทนที่โดยตรงในโปรแกรมโดยพรีโปรเซสเซอร์ (เนื่องจากโดยพื้นฐานแล้วมันเป็นคำสั่งของตัวประมวลผลล่วงหน้า) ดังนั้นพวกเขาจึงใช้พื้นที่หน่วยความจำมากกว่าฟังก์ชันที่เกี่ยวข้องอย่างหลีกเลี่ยงไม่ได้ ในทางกลับกันฟังก์ชันต้องใช้เวลามากขึ้นในการเรียกใช้และเพื่อให้ผลลัพธ์กลับมาและค่าใช้จ่ายนี้สามารถหลีกเลี่ยงได้โดยใช้มาโคร
มาโครยังมีเครื่องมือพิเศษบางอย่างที่สามารถช่วยในการเคลื่อนย้ายโปรแกรมบนแพลตฟอร์มต่างๆ
มาโครไม่จำเป็นต้องกำหนดชนิดข้อมูลสำหรับอาร์กิวเมนต์ซึ่งแตกต่างจากฟังก์ชัน
โดยรวมแล้วเป็นเครื่องมือที่มีประโยชน์ในการเขียนโปรแกรม และสามารถใช้ทั้งคำแนะนำและฟังก์ชันมาโครได้ขึ้นอยู่กับสถานการณ์
ฉันไม่สังเกตเห็นในคำตอบข้างต้นข้อดีอย่างหนึ่งของฟังก์ชันเหนือมาโครที่ฉันคิดว่าสำคัญมาก:
ฟังก์ชันสามารถส่งผ่านเป็นอาร์กิวเมนต์มาโครไม่สามารถ
ตัวอย่างที่เป็นรูปธรรม: คุณต้องการเขียนเวอร์ชันทางเลือกของฟังก์ชัน 'strpbrk' มาตรฐานที่จะยอมรับแทนที่จะเป็นรายการอักขระที่ชัดเจนเพื่อค้นหาภายในสตริงอื่นฟังก์ชัน (ตัวชี้ไปยัง a) ที่จะส่งกลับ 0 จนกว่าอักขระจะเป็น พบว่าผ่านการทดสอบบางอย่าง (กำหนดโดยผู้ใช้) เหตุผลหนึ่งที่คุณอาจต้องการทำเช่นนี้ก็เพื่อให้คุณสามารถใช้ประโยชน์จากฟังก์ชันไลบรารีมาตรฐานอื่น ๆ ได้: แทนที่จะจัดเตรียมสตริงที่ชัดเจนเต็มไปด้วยเครื่องหมายวรรคตอนคุณสามารถส่ง 'ispunct' ของ ctype.h แทนได้เป็นต้นหากใช้ 'ispunct' เป็น มาโครนี้จะใช้ไม่ได้
มีตัวอย่างอื่น ๆ อีกเพียบ ตัวอย่างเช่นหากการเปรียบเทียบของคุณทำได้โดยมาโครแทนที่จะเป็นฟังก์ชันคุณจะไม่สามารถส่งต่อไปยัง 'qsort' ของ stdlib.h ได้
สถานการณ์ที่คล้ายคลึงกันใน Python คือ 'พิมพ์' ในเวอร์ชัน 2 เทียบกับเวอร์ชัน 3 (คำสั่งที่ไม่สามารถส่งผ่านเทียบกับฟังก์ชันที่ผ่านได้)
หากคุณส่งฟังก์ชันเป็นอาร์กิวเมนต์ไปยังมาโครระบบจะประเมินทุกครั้ง ตัวอย่างเช่นหากคุณเรียกหนึ่งในมาโครยอดนิยม:
#define MIN(a,b) ((a)<(b) ? (a) : (b))
เช่นนั้น
int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));
functionThatTakeLongTime จะได้รับการประเมิน 5 ครั้งซึ่งสามารถลดประสิทธิภาพลงได้อย่างมาก