Saya selalu melihat contoh dan kasus di mana menggunakan makro lebih baik daripada menggunakan fungsi.
Bisakah seseorang menjelaskan kepada saya dengan contoh kelemahan makro dibandingkan dengan suatu fungsi?
Saya selalu melihat contoh dan kasus di mana menggunakan makro lebih baik daripada menggunakan fungsi.
Bisakah seseorang menjelaskan kepada saya dengan contoh kelemahan makro dibandingkan dengan suatu fungsi?
Jawaban:
Makro rawan kesalahan karena mereka mengandalkan substitusi tekstual dan tidak melakukan pemeriksaan tipe. Misalnya, makro ini:
#define square(a) a * a
berfungsi dengan baik saat digunakan dengan integer:
square(5) --> 5 * 5 --> 25
tetapi melakukan hal-hal yang sangat aneh saat digunakan dengan ekspresi:
square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice
Menempatkan tanda kurung di sekitar argumen membantu tetapi tidak sepenuhnya menghilangkan masalah ini.
Saat makro berisi beberapa pernyataan, Anda bisa mendapatkan masalah dengan konstruksi aliran kontrol:
#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;
Strategi yang biasa untuk memperbaiki ini adalah dengan meletakkan pernyataan di dalam loop "lakukan {...} while (0)".
Jika Anda memiliki dua struktur yang kebetulan berisi bidang dengan nama yang sama tetapi semantik berbeda, makro yang sama mungkin berfungsi pada keduanya, dengan hasil yang aneh:
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
Terakhir, makro bisa jadi sulit untuk di-debug, menghasilkan kesalahan sintaks yang aneh atau kesalahan waktu proses yang harus Anda perluas agar dapat dipahami (misalnya dengan gcc -E), karena debugger tidak dapat melewati makro, seperti dalam contoh ini:
#define print(x, y) printf(x y) /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */
Fungsi dan konstanta sebaris membantu menghindari banyak masalah dengan makro ini, tetapi tidak selalu berlaku. Jika makro sengaja digunakan untuk menentukan perilaku polimorfik, polimorfisme yang tidak disengaja mungkin sulit dihindari. C ++ memiliki sejumlah fitur seperti templat untuk membantu membuat konstruksi polimorfik yang kompleks dengan cara yang aman untuk mengetik tanpa menggunakan makro; lihat The C ++ Programming Language dari Stroustrup untuk detailnya.
x++*x++
tidak bisa dikatakan kenaikan x
dua kali; itu sebenarnya memanggil perilaku tidak terdefinisi , yang berarti bahwa kompilator bebas untuk melakukan apapun yang diinginkannya — ia bisa bertambah x
dua kali, atau sekali, atau tidak sama sekali; itu bisa membatalkan dengan suatu kesalahan atau bahkan membuat setan terbang keluar dari hidung Anda .
Fitur makro :
Fitur fungsi :
Efek sampingnya sangat besar. Berikut kasus tipikal:
#define min(a, b) (a < b ? a : b)
min(x++, y)
diperluas ke:
(x++ < y ? x++ : y)
x
bertambah dua kali dalam pernyataan yang sama. (dan perilaku tidak terdefinisi)
Menulis makro multi-baris juga merepotkan:
#define foo(a,b,c) \
a += 10; \
b += 10; \
c += 10;
Mereka membutuhkan \
di akhir setiap baris.
Makro tidak dapat "mengembalikan" apa pun kecuali Anda menjadikannya sebagai ekspresi tunggal:
int foo(int *a, int *b){
side_effect0();
side_effect1();
return a[0] + b[0];
}
Tidak dapat melakukannya di makro kecuali Anda menggunakan pernyataan ekspresi GCC. (EDIT: Anda dapat menggunakan operator koma ... mengabaikan itu ... Tapi itu mungkin masih kurang terbaca.)
Perintah Operasi: (atas kebaikan @ouah)
#define min(a,b) (a < b ? a : b)
min(x & 0xFF, 42)
diperluas ke:
(x & 0xFF < 42 ? x & 0xFF : 42)
Tetapi &
memiliki prioritas lebih rendah dari <
. Jadi 0xFF < 42
dievaluasi dulu.
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;
}
sedangkan:
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;
}
Dibandingkan dengan:
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;
}
Jika ragu, gunakan fungsi (atau fungsi sebaris).
Namun jawaban di sini sebagian besar menjelaskan masalah dengan makro, alih-alih memiliki pandangan sederhana bahwa makro itu jahat karena mungkin terjadi kecelakaan konyol.
Anda bisa menyadari jebakan dan belajar menghindarinya. Kemudian gunakan makro hanya jika ada alasan kuat untuk itu.
Ada beberapa kasus luar biasa tertentu di mana ada keuntungan menggunakan makro, ini termasuk:
va_args
. __FILE__
, __LINE__
, __func__
). periksa kondisi pra / posting, jika assert
gagal, atau bahkan statik-asserts sehingga kode tidak akan dikompilasi pada penggunaan yang tidak benar (sebagian besar berguna untuk build debug).struct
anggota periksa yang ada sebelum transmisi func(FOO, "FOO");
, Anda dapat menentukan makro yang memperluas string untuk Andafunc_wrapper(FOO);
inline
fungsi dapat menjadi pilihan) .Memang, beberapa di antaranya bergantung pada ekstensi compiler yang bukan standar C. Artinya, Anda mungkin akan mendapatkan kode yang kurang portabel, atau harus ifdef
memasukkannya, jadi mereka hanya dimanfaatkan jika kompiler mendukung.
Memperhatikan hal ini karena ini adalah salah satu penyebab kesalahan paling umum di makro (meneruskan x++
misalnya, di mana makro mungkin bertambah beberapa kali) .
mungkin untuk menulis makro yang menghindari efek samping dengan beberapa contoh argumen.
Jika Anda ingin memiliki square
makro yang berfungsi dengan berbagai jenis dan memiliki dukungan C11, Anda dapat melakukan ini ...
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))
Ini adalah ekstensi kompilator yang didukung oleh GCC, Clang, EKOPath & Intel C ++ (tetapi bukan MSVC) ;
#define square(a_) __extension__ ({ \
typeof(a_) a = (a_); \
(a * a); })
Jadi kerugian dengan makro adalah Anda perlu tahu bagaimana menggunakannya untuk memulai, dan bahwa mereka tidak didukung secara luas.
Salah satu manfaatnya adalah, dalam hal ini, Anda dapat menggunakan square
fungsi yang sama untuk berbagai jenis.
Tidak ada jenis pemeriksaan parameter dan kode yang diulang yang dapat menyebabkan kode membengkak. Sintaks makro juga dapat menyebabkan sejumlah kasus tepi aneh di mana titik koma atau urutan prioritas dapat menghalangi. Berikut tautan yang menunjukkan beberapa kejahatan makro
satu kelemahan makro adalah debugger membaca kode sumber, yang tidak memiliki makro yang diperluas, jadi menjalankan debugger dalam makro belum tentu berguna. Tak perlu dikatakan, Anda tidak bisa mengatur breakpoint di dalam makro seperti yang Anda bisa dengan fungsi.
Fungsi melakukan pemeriksaan jenis. Ini memberi Anda lapisan keamanan ekstra.
Menambahkan jawaban ini ..
Makro diganti langsung ke dalam program oleh preprocessor (karena pada dasarnya mereka adalah arahan preprocessor). Jadi mereka pasti menggunakan lebih banyak ruang memori daripada fungsi masing-masing. Di sisi lain, sebuah fungsi membutuhkan lebih banyak waktu untuk dipanggil dan mengembalikan hasil, dan overhead ini dapat dihindari dengan menggunakan makro.
Juga makro memiliki beberapa alat khusus yang dapat membantu portabilitas program pada platform yang berbeda.
Makro tidak perlu diberi tipe data untuk argumennya berbeda dengan fungsi.
Secara keseluruhan mereka adalah alat yang berguna dalam pemrograman. Dan baik instruksi makro maupun fungsi dapat digunakan tergantung pada situasinya.
Saya tidak memperhatikan, dalam jawaban di atas, salah satu keunggulan fungsi dibandingkan makro yang menurut saya sangat penting:
Fungsi bisa diberikan sebagai argumen, makro tidak bisa.
Contoh konkrit: Anda ingin menulis versi alternatif dari fungsi 'strpbrk' standar yang akan menerima, daripada daftar karakter eksplisit untuk dicari dalam string lain, fungsi (penunjuk ke a) yang akan mengembalikan 0 hingga karakter ditemukan yang lulus beberapa tes (ditentukan pengguna). Salah satu alasan Anda mungkin ingin melakukan ini adalah agar Anda dapat mengeksploitasi fungsi pustaka standar lainnya: alih-alih memberikan string eksplisit yang penuh dengan tanda baca, Anda dapat meneruskan 'ispunct' ctype.h, dll. Jika 'ispunct' hanya diterapkan sebagai makro, ini tidak akan berhasil.
Ada banyak contoh lainnya. Misalnya, jika perbandingan Anda diselesaikan dengan makro daripada fungsi, Anda tidak dapat meneruskannya ke 'qsort' stdlib.h.
Situasi analog di Python adalah 'print' dalam versi 2 vs. versi 3 (pernyataan tidak dapat dilewati vs. fungsi yang dapat dilalui).
Jika Anda meneruskan fungsi sebagai argumen ke makro, itu akan dievaluasi setiap saat. Misalnya, jika Anda memanggil salah satu makro paling populer:
#define MIN(a,b) ((a)<(b) ? (a) : (b))
seperti itu
int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));
functionThatTakeLongTime akan dievaluasi 5 kali yang secara signifikan dapat menurunkan kinerja