Macro so với Hàm trong C


100

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?


21
Xoay đầu câu hỏi. Trong tình huống nào thì vĩ mô tốt hơn? Sử dụng một hàm thực trừ khi bạn có thể chứng minh rằng macro tốt hơn.
David Heffernan

Câu trả lời:


112

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.


43
Quảng cáo C ++ là gì?
Pacerier

4
Đồng ý, đây là câu C, không cần thêm thiên vị.
ideaman42.

16
C ++ là một phần mở rộng của C bổ sung (trong số những thứ khác) các tính năng nhằm giải quyết hạn chế cụ thể này của C. Tôi không phải là fan hâm mộ của C ++, nhưng tôi nghĩ nó là chủ đề ở đây.
D Coetzee

1
Macro, hàm nội tuyến và mẫu thường được sử dụng để tăng hiệu suất. Chúng được sử dụng quá mức và có xu hướng làm giảm hiệu suất do hiện tượng phồng mã, làm giảm hiệu quả của bộ đệm lệnh CPU. Chúng ta có thể tạo cấu trúc dữ liệu chung nhanh chóng trong C mà không cần sử dụng các kỹ thuật này.
Sam Watkins

1
Theo ISO / IEC 9899: 1999 §6.5.1, "Giữa điểm trình tự trước và điểm tiếp theo, một đối tượng phải có giá trị được lưu trữ của nó được sửa đổi nhiều nhất một lần bằng cách đánh giá một biểu thức." (Từ ngữ tương tự tồn tại trong các tiêu chuẩn C trước đó và tiếp theo.) Vì vậy, biểu thức x++*x++không thể được cho là tăng xhai 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 xhai 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 .
Psychonaut

38

Tính năng macro :

  • Macro được xử lý trước
  • Không kiểm tra loại
  • Độ dài mã tăng
  • Sử dụng macro có thể dẫn đến tác dụng phụ
  • Tốc độ thực hiện nhanh hơn
  • Trước khi tên macro biên dịch được thay thế bằng giá trị macro
  • Hữu ích khi mã nhỏ xuất hiện nhiều lần
  • Macro không kiểm tra lỗi biên dịch

Tính năng chức năng :

  • Chức năng được biên dịch
  • Kiểm tra loại đã xong
  • Độ dài mã vẫn như cũ
  • Không có tác dụng phụ
  • Tốc độ thực hiện chậm hơn
  • Trong cuộc gọi chức năng, Chuyển quyền kiểm soát diễn ra
  • Hữu ích khi mã lớn xuất hiện nhiều lần
  • Kiểm tra chức năng lỗi biên dịch

2
yêu cầu tham chiếu "tốc độ thực thi nhanh hơn". Bất kỳ trình biên dịch nào thậm chí có thẩm quyền của thập kỷ trước sẽ nội tuyến hoạt động tốt nếu nó cho rằng nó sẽ mang lại lợi ích về hiệu suất.
Voo

1
Đó chẳng phải là, trong bối cảnh tính toán MCU mức thấp (AVRs, tức là ATMega32), Macro là lựa chọn tốt hơn, vì chúng không phát triển ngăn xếp cuộc gọi, giống như các lệnh gọi hàm?
hardyVeles

1
@hardyVeles Không phải vậy. Các trình biên dịch, ngay cả đối với AVR, có thể viết mã nội tuyến rất thông minh. Đây là một ví dụ: godbolt.org/z/Ic21iM
Edward

32

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.


5
và việc không đặt dấu ngoặc đơn với các đối số macro trong định nghĩa macro có thể dẫn đến các vấn đề về mức độ ưu tiên: ví dụ:min(a & 0xFF, 42)
ouah

À vâng. Không thấy bình luận của bạn khi tôi đang cập nhật bài đăng. Tôi đoán tôi cũng sẽ đề cập đến điều đó.
Mysticial

14

Ví dụ 1:

#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;
}

Ví dụ 2:

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;
}

13

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:

  • Các hàm chung, như đã lưu ý bên dưới, bạn có thể có một macro có thể được sử dụng trên các loại đối số đầu vào khác nhau.
  • Số biến của tham số có thể ánh xạ vào các chức năng khác nhau thay vì sử dụng C va_args.
    ví dụ: https://stackoverflow.com/a/24837037/432509 .
  • Họ có thể tùy chọn bao gồm thông tin địa phương, chẳng hạn như chuỗi debug:
    ( __FILE__, __LINE__, __func__). kiểm tra các điều kiện trước / sau, assertvề 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).
  • Kiểm tra các args đầu vào, Bạn có thể thực hiện các kiểm tra trên các args đầu vào như kiểm tra kiểu, sizeof của chúng, kiểm tra structcác thành viên có mặt trước khi ép kiểu
    (có thể hữu ích cho các kiểu đa hình) .
    Hoặc kiểm tra một mảng đáp ứng một số điều kiện độ dài.
    xem: https://stackoverflow.com/a/29926435/432509
  • Trong khi lưu ý rằng các hàm thực hiện kiểm tra kiểu, C cũng sẽ ép buộc các giá trị (ví dụ: ints / float). Trong một số trường hợp hiếm hoi, điều này có thể có vấn đề. Có thể viết các macro chính xác hơn sau đó là một hàm về các args đầu vào của chúng. xem: https://stackoverflow.com/a/25988779/432509
  • Việc sử dụng chúng làm trình bao bọc cho các hàm, trong một số trường hợp bạn có thể muốn tránh lặp lại chính mình, ví dụ: ... func(FOO, "FOO");, bạn có thể xác định macro mở rộng chuỗi cho bạnfunc_wrapper(FOO);
  • Khi bạn muốn thao tác với các biến trong phạm vi cục bộ của trình gọi, việc chuyển con trỏ tới một con trỏ hoạt động bình thường, nhưng trong một số trường hợp, bạn sẽ thấy ít rắc rối hơn khi sử dụng macro.
    (phép gán cho nhiều biến, đối với các hoạt động trên mỗi pixel, là một ví dụ bạn có thể thích macro hơn một hàm ... mặc dù nó vẫn phụ thuộc rất nhiều vào ngữ cảnh, vì các inlinehà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ó ifdefchúng, vì vậy chúng chỉ được lợi dụng khi trình biên dịch hỗ trợ.


Tránh tạo nhiều đối số

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ố.

C11 Chung

Nếu bạn muốn có squaremacro 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))

Biểu thức tuyên bố

Đâ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 squarechức năng cho nhiều loại khác nhau.


1
"... được hỗ trợ rộng rãi .." Tôi cá rằng biểu thức tuyên bố bạn đã đề cập không được hỗ trợ bởi cl.exe? (MS của Compiler)
gideon

1
@gideon, câu trả lời được chỉnh sửa đúng, mặc dù đối với mỗi tính năng được đề cập, không chắc chắn rằng nó cần thiết để có một số ma trận hỗ trợ trình biên dịch-tính năng.
ideaman42

12

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ô


6

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.


Điểm dừng là một thỏa thuận rất quan trọng ở đây, cảm ơn bạn đã chỉ ra nó.
Hans

6

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.


6

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.


3

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).


1
Cảm ơn câu trả lời này
Kyrol

1

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.