جدول ۲.۱ قوانین مربوط به اولویت (precedence) و جهت ارزیابی (associativity) تمام عملگرها رو خلاصه میکنه — حتی اونهایی که هنوز در موردشون حرف نزدیم.
عملگرهایی که در یک سطر هستن، اولویت یکسانی دارن؛
سطرها هم به ترتیب کاهش اولویت مرتب شدن.
مثلاً *, /, و % همگی یه سطح اولویت دارن که بالاتر از عملگرهای جمع و تفریق (+, -) هست.
عملگر () مربوط به فراخوانی تابع (function call) هست.
عملگرهای -> و . برای دسترسی به اعضای ساختارها (structures) استفاده میشن که در فصل ۶ بهشون میپردازیم، همراه با sizeof (که اندازهی یک شیء رو مشخص میکنه).
در فصل ۵ هم در مورد * (برای دسترسی غیرمستقیم از طریق اشارهگر) و & (گرفتن آدرس یک شیء) صحبت میشه،
و فصل ۳ عملگر , رو توضیح میده.
جدول ۲.۱ — اولویت و جهت ارزیابی عملگرها
| عملگرها | جهت ارزیابی |
|---|---|
| () [] -> . | چپ به راست |
| ! ~ ++ -- + - * (type) sizeof | راست به چپ |
| * / % | چپ به راست |
| + - | چپ به راست |
| << >> | چپ به راست |
| < <= > >= | چپ به راست |
| == != | چپ به راست |
| & | چپ به راست |
| ^ | چپ به راست |
| ` | چپ به راست |
| && | چپ به راست |
| ` | چپ به راست |
| ?: | راست به چپ |
| = += -= *= /= %= &= ^= |= <<= >>= | راست به چپ |
| , | چپ به راست |
عملگرهای تکعملوندی (unary) مثل &, +, -, و * اولویت بیشتری از نوع دوتاییشون دارن.
توجه کن که اولویت عملگرهای بیتی (&, ^, |) پایینتر از عملگرهای برابری (==, !=) هست.
یعنی برای نوشتن عبارتهایی مثل تست بیت، باید پرانتز بذاری تا نتیجه درست بهدست بیاد.
مثلاً:
if ((x & MASK) == 0) ...
بدون پرانتز ممکنه نتیجه اشتباه بشه.
مثل بیشتر زبانها، C هم ترتیب ارزیابی عملوندها رو در اکثر عملگرها مشخص نمیکنه.
(بهجز موارد خاص مثل &&, ||, ?:, و ,.)
برای مثال:
x = f() + g();
ممکنه f قبل از g اجرا بشه، یا برعکس.
پس اگه یکی از این توابع مقدار متغیری رو تغییر بده که اون یکی هم ازش استفاده میکنه، مقدار نهایی x ممکنه متفاوت بشه.
برای جلوگیری از این مشکل، میشه نتیجههای میانی رو توی متغیرهای جدا ذخیره کرد تا ترتیب اجرای دلخواه حفظ بشه.
به همین شکل، ترتیب ارزیابی آرگومانهای توابع هم مشخص نیست.
یعنی این قطعه کد:
printf("%d %d\n", ++n, power(2, n)); /* WRONG */
ممکنه در کامپایلرهای مختلف، نتیجههای متفاوتی بده؛ چون ممکنه n قبل یا بعد از power افزایش پیدا کنه.
راه درستش اینه که مقدار n رو جدا افزایش بدی و بعد توی printf استفاده کنی:
++n;
printf("%d %d\n", n, power(2, n));
فراخوانی توابع، دستورهای انتساب تو در تو (nested assignments)، و عملگرهای ++ و -- باعث ایجاد اثرات جانبی (side effects) میشن — یعنی در حین ارزیابی یه عبارت، یه متغیر هم تغییر پیدا میکنه.
در هر عبارت شامل اثرات جانبی، ممکنه وابستگیهای پنهانی به ترتیب ارزیابی وجود داشته باشه.
یه مثال کلاسیک از این مشکل:
a[i] = i++;
سؤال اینه: آیا اندیس آرایه مقدار قدیمی i هست یا مقدار جدیدش؟
کامپایلرهای مختلف ممکنه اینو به شکلهای متفاوتی تفسیر کنن و در نتیجه خروجی فرق کنه.
استاندارد C عمداً این موارد رو نامشخص (unspecified) گذاشته،
چون ترتیب بهینهی اجرای این نوع دستورات بستگی زیادی به معماری ماشین داره.
(البته استاندارد مشخص کرده که تمام اثرات جانبی آرگومانها باید قبل از فراخوانی تابع انجام بشن،
ولی این قانون تو مثال printf بالا کمکی نمیکنه.)
نتیجه اخلاقی:
نوشتن کدی که به ترتیب ارزیابی وابسته باشه، در هر زبانی یه عادت بده.
طبیعیه که باید بدونی چی رو نباید انجام بدی،
ولی اگه ندونی در ماشینهای مختلف ترتیب چجوریه،
اصلاً وسوسه نمیشی از اون جزئیات خاصِ پیادهسازی سوءاستفاده کنی.