Menu

C'de Makrolar: #define, Fonksiyon Benzeri Makrolar ve Parantez Kuralları

#define ile nesne benzeri ve fonksiyon benzeri makrolar - her argümanın ve tüm gövdenin neden parantez istediği, çoklu değerlendirme tuzağı, do-while(0) ile çok satırlı makrolar ve bir fonksiyonun ya da const'un ne zaman daha iyi araç olduğu.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

Makro, ön işlemcinin derleyici çalışmadan önce bir metinle değiştirdiği bir isimdir. Mekanizmanın tamamı budur ve bu sayfadaki her kural bunun bir sonucudur: makroların türü yoktur, kapsama saygı duymazlar, argümanları değerlendirmezler ve bir fonksiyon çağrısının ne olduğunu bilmezler. Metin kopyalarlar.

Dikkatli kullanıldıklarında vazgeçilmezdirler - taşınabilirlik anahtarları, satır numarasını yakalayan doğrulama makroları, dizi boyutlarında çalışan derleme zamanı sabitleri. Dikkatsiz kullanıldıklarında, dosyayı genişletene dek imkânsız görünen hatalar üretirler.

Nesne Benzeri Makrolar

En basit biçim, bir değeri temsil eden bir isim tanımlar:

Geleneksel olarak bu isimler BUYUK_SNAKE_CASE'dir; böylece okuyucu bir sembolün değişken değil makro olduğunu bir bakışta anlar. Burada o gelenek her zamankinden daha önemlidir, çünkü makrolar kapsamı yok sayar: bir fonksiyonun içindeki #define dosyanın geri kalanı için de geçerlidir ve başka bir fonksiyondaki aynı adlı bir değişkeni memnuniyetle yeniden yazar.

#undef NAME bir tanımı kaldırır, dolayısıyla bir isim, yeniden tanımlama uyarısı olmadan sonradan yeniden tanımlanabilir.

Fonksiyon Benzeri Makrolar

İsmin hemen ardına - açılış parantezinden önce boşluk olmadan - bir parametre listesi koyun ve makro argüman alsın:

Boşluk kuralı gerçektir: #define SQUARE (x) ((x)*(x)), değiştirme metni (x) ile başlayan SQUARE adında nesne benzeri bir makro tanımlar. Ardından gelen derleyici hatası boşluktan hiç söz etmeyecektir.

Parantez Kuralları

İki tane var ve ikisi de gövdenin, makroyu yazarken göremediğiniz bir ifadeye yapıştırılması yüzünden var.

Kural 1: her parametreyi parantez içine alın. Onsuz, kendisi bir ifade olan bir argüman öncelik tarafından paramparça edilir:

#define SQUARE_BAD(x) x * x

SQUARE_BAD(2 + 3)   /* 2 + 3 * 2 + 3 olur  ==  25 değil, 11 */

Kural 2: tüm gövdeyi parantez içine alın. Onsuz, çevredeki ifade sonucu paramparça eder:

#define DOUBLE_BAD(x) (x) + (x)

10 / DOUBLE_BAD(5)  /* 10 / (5) + (5) olur  ==  1 değil, 7 */

İki başarısızlığı düzeltilmiş sürümleriyle yan yana çalıştırın:

Her iki kötü sürüm de tek bir uyarı olmadan derlenir ve yanlış sayılar üretir. Kurulacak alışkanlık mekaniktir: her parametreyi sarın, sonra sonucu sarın. ((a) > (b) ? (a) : (b)) gürültülü görünür ve gürültü meselenin ta kendisidir.

Çoklu Değerlendirme Tuzağı

Parantezler sizi ikinci tehlikeden kurtaramaz. Bir makro, argüman metnini parametrenin göründüğü her yere yapıştırır, dolayısıyla yan etkisi olan bir argüman birden fazla kez gerçekleşir:

Makro ((i++) > (j) ? (i++) : (j)) şeklinde genişler. Karşılaştırma i'yi 6'ya çıkarır ve 5 ile 3'ü karşılaştırır; doğru dal i'yi tekrar 7'ye çıkarır ve 6 üretir. Dolayısıyla m 6, i 7'dir - hiçbiri "i ile j'nin büyüğü"nün vermesi gereken şey değildir. Bir max(i++, j) fonksiyonu doğru olurdu, çünkü argüman çağrıdan önce bir kez değerlendirilir.

Aynı tuzak yan etkisi olan her şeyi yakalar: SQUARE(rand()), rand()'ı iki kez çağırır ve iki farklı sayıyı çarpar. CHECK(read_byte()) iki bayt tüketir.

Tercih sırasıyla iki savunma:

  1. Gerçek bir fonksiyon kullanın. Birkaç tür için gerekiyorsa tür başına bir tane yazın ya da bir başlıkta static inline bir fonksiyon kullanın.
  2. Makro olmak zorundaysa, argümanların birden fazla kez değerlendirildiğini yüksek sesle belgeleyin ve çağrı yerlerini yan etkilerden uzak tutun.

Çok Satırlı Makrolar ve do-while(0)

Bir makro gövdesi, sonuncusu dışındaki her satır ters eğik çizgiyle bitiyorsa satırlara yayılabilir. Saf sürüm iyi görünür:

#define LOG_TWICE(msg) \
    printf("%s\n", msg); \
    printf("%s\n", msg)

Ve sonra önemli olduğu tek yerde sessizce bozulur:

if (verbose)
    LOG_TWICE("merhaba");
else
    printf("sessiz\n");

Yerleştirmeden sonra if yalnızca ilk printf'e sahip olur, ikincisi koşulsuz çalışır ve else'in artık eşleşen bir if'i yoktur - mesajı hiçbir yararlı yeri göstermeyen bir derleme hatası. Gövdeyi düz süslü parantezlere sarmak da daha iyi değildir: LOG_TWICE("merhaba")'dan sonraki ;, if'i sonlandıran boş bir deyim olur ve else yine bozulur.

İşe yarayan deyim do { ... } while (0)'dır:

do { ... } while (0) tek bir deyimdir, gövdesini tam olarak bir kez çalıştırır ve ardından bir noktalı virgül ister - dolayısıyla çağrı yeri sıradan bir fonksiyon çağrısı gibi okunur ve her denetim akışı bağlamında öyle davranır. Ters eğik çizgilere dikkat edin: bir ters eğik çizgiden sonraki başıboş bir boşluk devamı bitirir ve şaşırtıcı bir hata üretir.

Önceden Tanımlı Makrolar ve Doğrulama Kalıpları

Ön işlemci birkaç makroyu kendisi tanımlar ve bazı şeylerin neden fonksiyon olamayacağının sebebi bunlardır:

__FILE__ ve __LINE__, yazıldıkları yerde genişler, dolayısıyla onları bir makronun içine koymak çağıranın konumunu yakalar. Bir fonksiyon bunu yapamazdı - bir fonksiyonun içinde her zaman günlük fonksiyonunun kendi dosyasını ve satırını bildirirlerdi. Standart assert makrosu, başarısız ifadenin konumunu tam olarak böyle bildirir.

Diğer yararlı önceden tanımlı isimler: __DATE__, __TIME__ ve __STDC_VERSION__ (örneğin C17 için 201710L).

Bilmeye Değer İki Operatör

Bir makro gövdesinin içinde #, bir parametreyi bir metin sabitine dönüştürür ("metinleştirme") ve ## belirteçleri birbirine yapıştırır ("belirteç birleştirme").

SHOW(width * height), printf("width * height" " = %d\n", (width * height)); olur - komşu metin sabitleri derleyici tarafından birleştirilir, dolayısıyla tek bir çağrı hem ifade metnini hem değerini yazdırır. Hatırlamaya değer bir hata ayıklama numarasıdır.

## daha nadirdir ve çoğunlukla kod üretme makrolarında görünür: #define MAKE_VAR(n) int var_##n, MAKE_VAR(3)int var_3 yapar. Onu az kullanın; ön işlemcinin oluşturduğu tanımlayıcılar isimle aranamaz, bu da kodda gezinmeyi zorlaştırır.

Makrolar, Fonksiyonlar ve const

İşi yapan en zayıf aracı kullanın:

  • const değişken - const double PI = 3.14159; bir türe sahiptir, kapsama uyar, hata ayıklayıcıda görünür ve alakasız bir başlık tarafından yeniden tanımlanamaz. Çalışma zamanında kullanılan değerler için onu tercih edin.
  • enum - enum { MAX_USERS = 100 };, C'nin derleme zamanı sabiti istediği yerlerde çalışan, türü ve kapsamı olan adlandırılmış bir tam sayı sabiti verir. Tam sayı sınırları için iyidir.
  • static inline fonksiyon - tür denetimlidir, her argümanı tam olarak bir kez değerlendirir ve modern derleyiciler onu bir makronun yapacağı gibi satır içine alır. Neredeyse her fonksiyon benzeri makronun doğru yerine geçecek olan budur.
  • Makro - __LINE__'a ihtiyacınız olduğunda, türler var olmadan önce çalışması gerektiğinde (eski koddaki dizi boyutları), koşullu derlemeyle kod değiştirdiğinizde ya da tekrarlayan kod ürettiğinizde.

Makroların rakipsiz kaldığı bir yer dizi boyutlandırmadır, çünkü sonucun bir derleme zamanı sabiti olması gerekir:

Bunun kendine ait iyi bilinen bir uyarısı vardır: yalnızca gerçek bir dizi için doğrudur. Bir gösterici geçirin - bir dizi, bir fonksiyona verildiğinde bu hâle gelir - ve sizeof göstericiyi ölçer, yanlış ve kendinden emin biçimde sessiz bir cevap verir.

Bir Makronun Hatasını Ayıklamak

Bir makro yanlış davrandığında ona bakıp durmayın. Onu genişletin:

gcc -E program.c | tail -30

Yerleştirilen metin size sorunun eksik bir parantez mi, çifte değerlendirme mi yoksa başka bir şeyle çakışan bir isim mi olduğunu hemen söyler. -Wall -Wextra ile de derleyin - GCC ve clang, bildirilen hata satırını tanıma bağlayan "in expansion of macro" notları ekler.

Sıradaki: başlık dosyaları; orada #define ve #include birleşerek bir projenin aynı bildirimleri iki kez yapıştırmadan birçok kaynak dosyaya yayılmasını sağlar.

Sıkça Sorulan Sorular

C'de makro nedir?

Ön işlemcinin, derlemeden önce kaynağınıza yerleştirdiği adlandırılmış bir metin parçasıdır. #define MAX 100, sonraki her MAX100 yapar; #define SQUARE(x) ((x) * (x)) argüman alır ve onları bir kalıba yerleştirir. Makroların türü yoktur ve hiçbir kapsam kuralına uymazlar - metin değiştirmedirler.

C makroları neden bu kadar çok parantez ister?

Çünkü gövde çevredeki koda yapıştırılır ve sonra bir bütün olarak ayrıştırılır. #define SQUARE(x) x * x, SQUARE(2 + 3)2 + 3 * 2 + 3'e dönüştürür ki bu 25 değil 11'dir. Her argümanı ve tüm gövdeyi sarmak - ((x) * (x)) - yerleştirilen metnin, etrafında ne olursa olsun amaçladığınız gibi gruplanmasını sağlar.

C'de makro ile fonksiyon arasındaki fark nedir?

Bir fonksiyon bir kez derlenir, tür denetimi yapılır ve her argümanı tam olarak bir kez değerlendirir. Bir makro her kullanımda yapıştırılır, hiçbir şeyi denetlemez ve bir argümanı birkaç kez değerlendirebilir - dolayısıyla MAX(i++, j), i'yi iki kez artırabilir. Fonksiyonları (ve sabitler için const/enum'u) tercih edin; makroları fonksiyonların yapamadığı şeyler için kullanın, __LINE__ yakalamak ya da kod üretmek gibi.

Çok satırlı bir makro neden do { ... } while (0) ile sarılır?

Tek bir deyim gibi davransın diye. Çıplak bir { ... } gövdesi, bir if/else içinde ardından noktalı virgül geldiğinde bozulur (noktalı virgül if'i erken bitirir) ve düz bir deyim dizisi parantezsiz bir if içinde bozulur. do { ... } while (0), sondaki bir noktalı virgülü kabul eden ve gövdesini tam olarak bir kez çalıştıran tek bir deyimdir.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA