Makro vs Fungsi di C


101

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?


21
Balikkan pertanyaan di atas kepalanya. Dalam situasi apa makro lebih baik? Gunakan fungsi nyata kecuali Anda dapat menunjukkan bahwa makro lebih baik.
David Heffernan

Jawaban:


114

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.


43
Ada apa dengan iklan C ++?
Pacerier

4
Setuju, ini pertanyaan C, tidak perlu menambahkan bias.
ideasman42

16
C ++ adalah perpanjangan dari C yang menambahkan (antara lain) fitur yang dimaksudkan untuk mengatasi batasan khusus C. Saya bukan penggemar C ++, tapi saya pikir itu sesuai topik di sini.
D Coetzee

1
Makro, fungsi sebaris, dan templat sering kali digunakan untuk meningkatkan kinerja. Mereka digunakan secara berlebihan, dan cenderung merusak kinerja karena penggelembungan kode, yang mengurangi keefektifan cache instruksi CPU. Kita dapat membuat struktur data umum dengan cepat dalam C tanpa menggunakan teknik ini.
Sam Watkins

1
Menurut ISO / IEC 9899: 1999 §6.5.1, "Antara titik urutan sebelumnya dan berikutnya, sebuah objek harus memiliki nilai simpanan yang dimodifikasi paling banyak satu kali dengan evaluasi ekspresi." (Kata-kata serupa ada dalam standar C sebelumnya dan berikutnya.) Jadi ungkapan itu x++*x++tidak bisa dikatakan kenaikan xdua kali; itu sebenarnya memanggil perilaku tidak terdefinisi , yang berarti bahwa kompilator bebas untuk melakukan apapun yang diinginkannya — ia bisa bertambah xdua kali, atau sekali, atau tidak sama sekali; itu bisa membatalkan dengan suatu kesalahan atau bahkan membuat setan terbang keluar dari hidung Anda .
Psikonot

40

Fitur makro :

  • Makro Diproses Sebelumnya
  • Tidak Ada Jenis Pemeriksaan
  • Kode Panjang Meningkat
  • Penggunaan makro bisa menimbulkan efek samping
  • Kecepatan Eksekusi Lebih Cepat
  • Sebelum nama makro Kompilasi diganti dengan nilai makro
  • Berguna saat kode kecil muncul berkali-kali
  • Makro tidak Memeriksa Kesalahan Kompilasi

Fitur fungsi :

  • Fungsi Dikompilasi
  • Jenis Pemeriksaan Selesai
  • Panjang Kode tetap Sama
  • Tidak ada Efek samping
  • Kecepatan Eksekusi Lebih Lambat
  • Selama panggilan fungsi, Transfer of Control berlangsung
  • Berguna saat kode besar muncul berkali-kali
  • Fungsi Memeriksa Kesalahan Kompilasi

2
Referensi "kecepatan eksekusi lebih cepat" diperlukan. Kompiler yang bahkan agak kompeten dalam dekade terakhir akan berfungsi sebaris dengan baik jika dianggap akan memberikan manfaat kinerja.
Voo

1
Bukankah itu, dalam konteks komputasi MCU tingkat rendah (AVRs, yaitu ATMega32), Macro adalah pilihan yang lebih baik, karena mereka tidak menumbuhkan tumpukan panggilan, seperti yang dilakukan oleh panggilan fungsi?
hardyVeles

1
@yVeles Tidak begitu. Compiler, bahkan untuk AVR, dapat menyebariskan kode dengan sangat cerdas. Berikut ini contohnya: godbolt.org/z/Ic21iM
Edward

34

Efek sampingnya sangat besar. Berikut kasus tipikal:

#define min(a, b) (a < b ? a : b)

min(x++, y)

diperluas ke:

(x++ < y ? x++ : y)

xbertambah 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 < 42dievaluasi dulu.


5
dan tidak menempatkan tanda kurung dengan argumen makro dalam definisi makro dapat menyebabkan masalah prioritas: misalnya,min(a & 0xFF, 42)
ouah

Ah iya. Tidak melihat komentar Anda saat saya memperbarui postingan. Saya kira saya akan menyebutkan itu juga.
Mysticial

14

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

sedangkan:

int square(int x) {
  return x * x;
}

int main() {
  int x = 2;
  int y = square(x++); // fine
  return 0;
}

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

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

13

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:

  • Fungsi umum, seperti yang disebutkan di bawah, Anda dapat memiliki makro yang dapat digunakan pada berbagai jenis argumen input.
  • Variabel jumlah argumen dapat memetakan untuk fungsi yang berbeda daripada menggunakan C va_args.
    misalnya: https://stackoverflow.com/a/24837037/432509 .
  • Mereka dapat secara opsional termasuk info lokal, seperti string men-debug:
    ( __FILE__, __LINE__, __func__). periksa kondisi pra / posting, jika assertgagal, atau bahkan statik-asserts sehingga kode tidak akan dikompilasi pada penggunaan yang tidak benar (sebagian besar berguna untuk build debug).
  • Memeriksa argumen masukan, Anda dapat melakukan pengujian pada argumen masukan seperti memeriksa jenis, ukuran, structanggota periksa yang ada sebelum transmisi
    (dapat berguna untuk jenis polimorfik) .
    Atau periksa array memenuhi beberapa kondisi panjang.
    lihat: https://stackoverflow.com/a/29926435/432509
  • Sementara dicatat bahwa fungsi melakukan pemeriksaan tipe, C akan memaksa nilai juga (ints / floats misalnya). Dalam kasus yang jarang terjadi, ini mungkin bermasalah. Dimungkinkan untuk menulis makro yang lebih tepat daripada fungsi tentang argumen inputnya. lihat: https://stackoverflow.com/a/25988779/432509
  • Penggunaannya sebagai pembungkus untuk fungsi, dalam beberapa kasus Anda mungkin ingin menghindari pengulangan sendiri, misalnya ... func(FOO, "FOO");, Anda dapat menentukan makro yang memperluas string untuk Andafunc_wrapper(FOO);
  • Saat Anda ingin memanipulasi variabel dalam lingkup lokal pemanggil, meneruskan pointer ke pointer berfungsi dengan baik secara normal, tetapi dalam beberapa kasus tidak terlalu merepotkan untuk menggunakan makro still.
    (penugasan ke beberapa variabel, untuk operasi per piksel, adalah contoh Anda mungkin lebih memilih makro daripada fungsi ... meskipun itu masih sangat bergantung pada konteks, karena inlinefungsi 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 ifdefmemasukkannya, jadi mereka hanya dimanfaatkan jika kompiler mendukung.


Menghindari beberapa contoh argumen

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.

C11 Generik

Jika Anda ingin memiliki squaremakro 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))

Ekspresi pernyataan

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 squarefungsi yang sama untuk berbagai jenis.


1
"... didukung secara luas .." Saya yakin ekspresi pernyataan yang Anda sebutkan tidak didukung oleh cl.exe? (Penyusun MS)
gideon

1
@gideon, jawaban yang diedit dengan benar, meskipun untuk setiap fitur yang disebutkan, tidak yakin perlu memiliki beberapa matriks compiler-feature-support.
ideasman42

12

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


6

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.


Breakpoint adalah kesepakatan yang sangat penting di sini, terima kasih telah menunjukkannya.
Hans


6

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.


3

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


1
Terima kasih atas jawaban ini
Kyrol

1

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

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.