Tôi luôn thấy các ví dụ và trường hợp sử dụng macro tốt hơn sử dụng hàm.
Ai đó có thể giải thích cho tôi một ví dụ về nhược điểm của macro so với một hàm không?
Tôi luôn thấy các ví dụ và trường hợp sử dụng macro tốt hơn sử dụng hàm.
Ai đó có thể giải thích cho tôi một ví dụ về nhược điểm của macro so với một hàm không?
Câu trả lời:
Macro dễ bị lỗi vì chúng dựa vào sự thay thế văn bản và không thực hiện kiểm tra kiểu. Ví dụ, macro này:
#define square(a) a * a
hoạt động tốt khi được sử dụng với một số nguyên:
square(5) --> 5 * 5 --> 25
nhưng thực hiện những điều rất lạ khi được sử dụng với các biểu thức:
square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice
Đặt dấu ngoặc đơn xung quanh các đối số giúp ích nhưng không loại bỏ hoàn toàn những vấn đề này.
Khi macro chứa nhiều câu lệnh, bạn có thể gặp rắc rối với cấu trúc luồng điều khiển:
#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;
Chiến lược thông thường để sửa lỗi này là đặt các câu lệnh bên trong vòng lặp "do {...} while (0)".
Nếu bạn có hai cấu trúc chứa một trường có cùng tên nhưng ngữ nghĩa khác nhau, thì cùng một macro có thể hoạt động trên cả hai, với kết quả lạ:
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
Cuối cùng, macro có thể khó gỡ lỗi, tạo ra các lỗi cú pháp kỳ lạ hoặc lỗi thời gian chạy mà bạn phải mở rộng để hiểu (ví dụ: với gcc -E), vì trình gỡ lỗi không thể bước qua macro, như trong ví dụ này:
#define print(x, y) printf(x y) /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */
Các hàm và hằng số nội tuyến giúp tránh nhiều vấn đề này với macro, nhưng không phải lúc nào cũng áp dụng được. Trong trường hợp macro được cố ý sử dụng để chỉ định hành vi đa hình, có thể khó tránh được đa hình không chủ ý. C ++ có một số tính năng như khuôn mẫu giúp tạo các cấu trúc đa hình phức tạp theo cách an toàn về kiểu chữ mà không cần sử dụng macro; xem Ngôn ngữ lập trình C ++ của Stroustrup để biết thêm chi tiết.
x++*x++
không thể được cho là tăng x
hai lần; nó thực sự gọi hành vi không xác định , có nghĩa là trình biên dịch có thể tự do làm bất cứ điều gì nó muốn — nó có thể tăng x
hai lần hoặc một lần, hoặc hoàn toàn không; nó có thể hủy bỏ với một lỗi hoặc thậm chí làm cho ma quỷ bay ra khỏi mũi của bạn .
Tính năng macro :
Tính năng chức năng :
Tác dụng phụ là một trong những tác dụng lớn. Đây là một trường hợp điển hình:
#define min(a, b) (a < b ? a : b)
min(x++, y)
được mở rộng thành:
(x++ < y ? x++ : y)
x
được tăng lên hai lần trong cùng một câu lệnh. (và hành vi không xác định)
Viết macro nhiều dòng cũng là một khó khăn:
#define foo(a,b,c) \
a += 10; \
b += 10; \
c += 10;
Họ yêu cầu một \
ở cuối mỗi dòng.
Macro không thể "trả về" bất kỳ thứ gì trừ khi bạn biến nó thành một biểu thức duy nhất:
int foo(int *a, int *b){
side_effect0();
side_effect1();
return a[0] + b[0];
}
Không thể làm điều đó trong macro trừ khi bạn sử dụng câu lệnh biểu thức của GCC. (CHỈNH SỬA: Bạn có thể sử dụng một toán tử dấu phẩy mặc dù ... đã bỏ qua điều đó ... Nhưng nó vẫn có thể khó đọc hơn.)
Thứ tự hoạt động: (lịch sự của @ouah)
#define min(a,b) (a < b ? a : b)
min(x & 0xFF, 42)
được mở rộng thành:
(x & 0xFF < 42 ? x & 0xFF : 42)
Nhưng &
có mức độ ưu tiên thấp hơn <
. Vì vậy, 0xFF < 42
được đánh giá đầu tiên.
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;
}
trong khi:
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;
}
So với:
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;
}
Khi nghi ngờ, hãy sử dụng các hàm (hoặc các hàm nội tuyến).
Tuy nhiên, các câu trả lời ở đây chủ yếu giải thích các vấn đề với macro, thay vì có một số quan điểm đơn giản rằng macro là xấu vì những tai nạn ngớ ngẩn có thể xảy ra.
Bạn có thể nhận thức được những cạm bẫy và học cách tránh chúng. Sau đó, chỉ sử dụng macro khi có lý do chính đáng.
Có một số trường hợp ngoại lệ nhất định có lợi thế khi sử dụng macro, bao gồm:
va_args
. __FILE__
, __LINE__
, __func__
). kiểm tra các điều kiện trước / sau, assert
về lỗi hoặc thậm chí xác nhận tĩnh để mã không được biên dịch khi sử dụng không đúng cách (chủ yếu hữu ích cho các bản dựng gỡ lỗi).struct
các thành viên có mặt trước khi ép kiểu func(FOO, "FOO");
, bạn có thể xác định macro mở rộng chuỗi cho bạnfunc_wrapper(FOO);
inline
hàm có thể là một tùy chọn) .Phải thừa nhận rằng một số trong số này dựa vào các phần mở rộng của trình biên dịch không phải là tiêu chuẩn C. Có nghĩa là bạn có thể kết thúc với mã ít di động hơn hoặc phải có ifdef
chúng, vì vậy chúng chỉ được lợi dụng khi trình biên dịch hỗ trợ.
Lưu ý điều này vì nó là một trong những nguyên nhân phổ biến nhất gây ra lỗi trong macro ( x++
ví dụ: chuyển vào trong đó macro có thể tăng lên nhiều lần) .
có thể viết macro để tránh các tác dụng phụ với nhiều khởi tạo đối số.
Nếu bạn muốn có square
macro hoạt động với nhiều loại khác nhau và có hỗ trợ C11, bạn có thể làm điều này ...
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))
Đây là phần mở rộng trình biên dịch được hỗ trợ bởi GCC, Clang, EKOPath & Intel C ++ (nhưng không phải MSVC) ;
#define square(a_) __extension__ ({ \
typeof(a_) a = (a_); \
(a * a); })
Vì vậy, bất lợi với macro là bạn cần biết cách sử dụng chúng để bắt đầu và chúng không được hỗ trợ rộng rãi.
Một lợi ích là, trong trường hợp này, bạn có thể sử dụng cùng một square
chức năng cho nhiều loại khác nhau.
Không có kiểu kiểm tra các tham số và mã được lặp lại, điều này có thể dẫn đến hiện tượng phồng mã. Cú pháp macro cũng có thể dẫn đến bất kỳ số trường hợp cạnh kỳ lạ nào trong đó dấu chấm phẩy hoặc thứ tự ưu tiên có thể cản trở. Đây là một liên kết chứng minh một số điều ác vĩ mô
một hạn chế đối với macro là trình gỡ lỗi đọc mã nguồn, mã này không có macro mở rộng, vì vậy việc chạy trình gỡ lỗi trong macro không nhất thiết hữu ích. Không cần phải nói, bạn không thể đặt điểm ngắt bên trong macro giống như bạn có thể làm với các hàm.
Các chức năng kiểm tra loại. Điều này mang lại cho bạn thêm một lớp an toàn.
Thêm vào câu trả lời này ..
Macro được thay thế trực tiếp vào chương trình bởi bộ tiền xử lý (vì về cơ bản chúng là chỉ thị của bộ tiền xử lý). Vì vậy, chúng chắc chắn sử dụng nhiều không gian bộ nhớ hơn một chức năng tương ứng. Mặt khác, một hàm đòi hỏi nhiều thời gian hơn để được gọi và trả về kết quả, và có thể tránh được chi phí này bằng cách sử dụng macro.
Ngoài ra, macro có một số công cụ đặc biệt hơn có thể giúp chương trình khả chuyển trên các nền tảng khác nhau.
Macro không cần được gán kiểu dữ liệu cho các đối số của chúng ngược lại với các hàm.
Nhìn chung chúng là một công cụ hữu ích trong lập trình. Và cả hai lệnh macro và hàm đều có thể được sử dụng tùy thuộc vào hoàn cảnh.
Tôi đã không nhận thấy, trong các câu trả lời ở trên, một lợi thế của hàm so với macro mà tôi nghĩ là rất quan trọng:
Các hàm có thể được truyền dưới dạng đối số, macro thì không.
Ví dụ cụ thể: Bạn muốn viết một phiên bản thay thế của hàm 'strpbrk' tiêu chuẩn sẽ chấp nhận, thay vì một danh sách rõ ràng các ký tự để tìm kiếm trong một chuỗi khác, một hàm (con trỏ tới a) sẽ trả về 0 cho đến khi một ký tự là thấy rằng vượt qua một số thử nghiệm (do người dùng xác định). Một lý do bạn có thể muốn làm điều này là để bạn có thể khai thác các hàm thư viện tiêu chuẩn khác: thay vì cung cấp một chuỗi rõ ràng đầy dấu câu, bạn có thể chuyển 'ispunct' của ctype.h để thay thế, v.v. Nếu 'ispunct' chỉ được triển khai như một macro, điều này sẽ không hoạt động.
Có rất nhiều ví dụ khác. Ví dụ: nếu so sánh của bạn được thực hiện bằng macro thay vì hàm, bạn không thể chuyển nó vào 'qsort' của stdlib.h.
Một tình huống tương tự trong Python là 'in' trong phiên bản 2 so với phiên bản 3 (câu lệnh không thể vượt qua so với hàm có thể truyền).
Nếu bạn chuyển hàm làm đối số cho macro, nó sẽ được đánh giá mọi lúc. Ví dụ: nếu bạn gọi một trong những macro phổ biến nhất:
#define MIN(a,b) ((a)<(b) ? (a) : (b))
như thế
int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));
functionThatTakeLongTime sẽ được đánh giá 5 lần, điều này có thể làm giảm đáng kể hiệu suất