Macro cũng giống như bất kỳ công cụ nào khác - một cái búa được sử dụng trong một vụ giết người không phải là xấu xa vì nó là một cái búa. Nó xấu xa theo cách mà người sử dụng nó theo cách đó. Nếu bạn muốn đóng đinh, búa là một công cụ hoàn hảo.
Có một số khía cạnh đối với macro khiến chúng trở nên "xấu" (tôi sẽ mở rộng về từng khía cạnh sau và đề xuất các lựa chọn thay thế):
- Bạn không thể gỡ lỗi macro.
- Mở rộng vĩ mô có thể dẫn đến các tác dụng phụ kỳ lạ.
- Macro không có "không gian tên", vì vậy nếu bạn có macro xung đột với tên được sử dụng ở nơi khác, bạn sẽ nhận được các thay thế macro ở nơi bạn không muốn và điều này thường dẫn đến các thông báo lỗi lạ.
- Macro có thể ảnh hưởng đến những điều bạn không nhận ra.
Vì vậy, hãy mở rộng một chút ở đây:
1) Không thể gỡ lỗi macro.
Khi bạn có macro dịch thành một số hoặc một chuỗi, mã nguồn sẽ có tên macro và nhiều trình gỡ lỗi, bạn không thể "nhìn thấy" macro dịch sang. Vì vậy, bạn thực sự không biết những gì đang xảy ra.
Thay thế : Sử dụng enum
hoặcconst T
Đối với macro "giống như hàm", vì trình gỡ lỗi hoạt động ở cấp độ "mỗi dòng nguồn nơi bạn ở", macro của bạn sẽ hoạt động giống như một câu lệnh duy nhất, bất kể đó là một câu lệnh hay một trăm câu lệnh. Làm cho khó để tìm ra những gì đang xảy ra.
Thay thế : Sử dụng các hàm - nội tuyến nếu nó cần "nhanh" (nhưng lưu ý rằng quá nhiều nội tuyến không phải là điều tốt)
2) Mở rộng vĩ mô có thể có tác dụng phụ lạ.
Một trong những nổi tiếng là #define SQUARE(x) ((x) * (x))
và sử dụng x2 = SQUARE(x++)
. Điều đó dẫn đến x2 = (x++) * (x++);
, ngay cả khi nó là mã hợp lệ [1], gần như chắc chắn sẽ không phải là điều mà lập trình viên mong muốn. Nếu đó là một hàm, bạn có thể thực hiện x ++, và x sẽ chỉ tăng một lần.
Một ví dụ khác là "if else" trong macro, giả sử chúng ta có điều này:
#define safe_divide(res, x, y) if (y != 0) res = x/y;
và sau đó
if (something) safe_divide(b, a, x);
else printf("Something is not set...");
Nó thực sự trở thành một điều hoàn toàn sai lầm ....
Thay thế : các chức năng thực.
3) Macro không có không gian tên
Nếu chúng ta có một macro:
#define begin() x = 0
và chúng tôi có một số mã trong C ++ sử dụng begin:
std::vector<int> v;
... stuff is loaded into v ...
for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
std::cout << ' ' << *it;
Bây giờ, bạn nghĩ bạn nhận được thông báo lỗi nào và bạn tìm lỗi ở đâu [giả sử bạn đã hoàn toàn quên - hoặc thậm chí không biết về - macro bắt đầu nằm trong một số tệp tiêu đề mà người khác đã viết? [và thậm chí còn thú vị hơn nếu bạn bao gồm macro đó trước khi bao gồm - bạn sẽ bị chìm trong những lỗi kỳ lạ hoàn toàn không có ý nghĩa khi bạn nhìn vào chính mã.
Thay thế : Không có quá nhiều thay thế như một "quy tắc" - chỉ sử dụng tên viết hoa cho macro và không bao giờ sử dụng tất cả tên hoa cho những thứ khác.
4) Macro có những hiệu ứng mà bạn không nhận ra
Thực hiện chức năng này:
#define begin() x = 0
#define end() x = 17
... a few thousand lines of stuff here ...
void dostuff()
{
int x = 7;
begin();
... more code using x ...
printf("x=%d\n", x);
end();
}
Bây giờ, nếu không nhìn vào macro, bạn sẽ nghĩ rằng begin là một hàm, không nên ảnh hưởng đến x.
Điều này và tôi đã thấy nhiều ví dụ phức tạp hơn, THỰC SỰ có thể làm rối tung cả ngày của bạn!
Thay thế : Không sử dụng macro để đặt x hoặc chuyển x vào làm đối số.
Có những lúc việc sử dụng macro chắc chắn có lợi. Một ví dụ là bọc một hàm bằng macro để chuyển thông tin về tệp / dòng:
#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x) my_debug_free(x, __FILE__, __LINE__)
Bây giờ chúng ta có thể sử dụng my_debug_malloc
như malloc thông thường trong mã, nhưng nó có thêm các đối số, vì vậy khi nói đến phần cuối và chúng ta quét "phần tử bộ nhớ nào chưa được giải phóng", chúng ta có thể in nơi phân bổ được thực hiện để lập trình viên có thể theo dõi sự rò rỉ.
[1] Cập nhật một biến nhiều lần "trong một điểm trình tự" là hành vi không xác định. Một điểm trình tự không hoàn toàn giống với một câu lệnh, nhưng đối với hầu hết các ý định và mục đích, đó là những gì chúng ta nên coi nó là. Vì vậy, thực hiện x++ * x++
sẽ cập nhật x
hai lần, không được xác định và có thể sẽ dẫn đến các giá trị khác nhau trên các hệ thống khác nhau và giá trị kết quả cũng khác nhau x
.
#pragma
không phải là vĩ mô.