وقتی یه عملگر (operator) دو تا مقدار از نوعهای مختلف داره، اونها طبق یهسری قانون مشخص به یه نوع مشترک تبدیل میشن. معمولاً تنها تبدیلهای خودکاری که اتفاق میافتن، اوناییان که یه مقدار «باریکتر» (narrower) رو به یه مقدار «گستردهتر» (wider) تبدیل میکنن بدون اینکه اطلاعات از بین بره. مثلاً وقتی توی یه عبارت مثل f + i، عدد صحیح (integer) به عدد اعشاری (floating point) تبدیل میشه.
عبارتهایی که بیمعنی هستن (مثلاً استفاده از یه float به عنوان اندیس آرایه)، مجاز نیستن. ولی عبارتهایی که ممکنه باعث از دست رفتن اطلاعات بشن (مثلاً انتساب یه عدد صحیح بلندتر به نوع کوتاهتر، یا یه float به integer) فقط ممکنه یه هشدار بدن، ولی غیرقانونی نیستن.
یه char در واقع یه عدد صحیح کوچیکه، پس میتونه به راحتی توی عبارتهای عددی استفاده بشه. این کار توی بعضی از تبدیلهای کاراکتری مفیده. مثلاً یه پیادهسازی ساده از تابع atoi که یه رشته از اعداد رو به عدد تبدیل میکنه:
/* atoi: convert s to integer */
int atoi(char s[])
{
int i, n;
n = 0;
for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i)
n = 10 * n + (s[i] - '0');
return n;
}
همونطور که توی فصل ۱ گفتیم، عبارت
s[i] - '0'
مقدار عددی کاراکتری که توی s[i] ذخیره شده رو میده، چون مقدارهای '0' تا '9' پشت سر هم هستن.
یه مثال دیگه از تبدیل char به int تابع lower هست که یه کاراکتر رو توی مجموعهی ASCII به حالت حروف کوچیک تبدیل میکنه. اگه کاراکتر حرف بزرگ نباشه، همون رو بدون تغییر برمیگردونه:
/* lower: convert c to lower case; ASCII only */
int lower(int c)
{
if (c >= 'A' && c <= 'Z')
return c + 'a' - 'A';
else
return c;
}
این کار برای ASCII جواب میده چون حروف بزرگ و کوچیک فاصلهی ثابتی دارن و بین A و Z چیزی جز حروف نیست. ولی برای مجموعهی EBCDIC اینطور نیست و ممکنه این کد کاراکترهای دیگهای غیر از حروف رو هم تغییر بده.
هدر استاندارد <ctype.h> (توضیحش توی پیوست B هست) یه سری توابع داره که برای تست و تبدیل کاراکترها به شکل مستقل از نوع مجموعهکاراکتر استفاده میشن. مثلاً تابع tolower جایگزین قابلاعتماد و قابلحمل برای تابع lower هست. همینطور تست
c >= '0' && c <= '9'
میتونه با
isdigit(c)
جایگزین بشه. از این به بعد از توابع <ctype.h> استفاده میکنیم.
یه نکتهی ظریف دربارهی تبدیل char به int اینه که زبان C مشخص نکرده آیا نوع char بهصورت signed هست یا unsigned. پس ممکنه تبدیل یه char به int منفی بشه یا نه — بستگی به معماری ماشین داره. توی بعضی سیستمها اگه بیت سمت چپ 1 باشه، تبدیل باعث منفی شدن میشه (sign extension)، ولی توی بعضی سیستمها با صفر پر میشه و همیشه مثبت میمونه.
C تضمین میکنه که کاراکترهای چاپی استاندارد ماشین همیشه مقدار مثبت دارن، ولی اگه دادهی غیرکاراکتری توی char ذخیره کنی، ممکنه روی یه ماشین منفی و روی یه ماشین دیگه مثبت بشه. پس اگه قراره دادهی غیرکاراکتری توی char ذخیره کنی، بهتره مشخصاً بنویسی signed char یا unsigned char.
عبارتهای مقایسهای مثل i > j و منطقی مثل && و || مقدارشون 1 میشه اگه درست باشن، و 0 اگه غلط باشن. مثلاً:
d = c >= '0' && c <= '9'
باعث میشه اگه c یه عدد باشه، d برابر ۱ بشه وگرنه ۰. البته توابعی مثل isdigit ممکنه هر مقدار غیرصفر رو بهعنوان درست برگردونن. توی شرطهای if و while و...، فقط «غیرصفر» بودن مهمه، پس تفاوتی نمیکنه.
تبدیلهای عددی ضمنی هم تقریباً طبق انتظار کار میکنن. اگه یه عملگر مثل + یا * دو تا عملوند از نوعهای مختلف داشته باشه، نوع پایینتر به نوع بالاتر ارتقا پیدا میکنه و نتیجه از نوع صحیحه.
اگه عملوند بدون unsigned باشن، این قانونهای ساده کافین:
-
اگه یکی long double باشه، اون یکی هم به long double تبدیل میشه.
-
وگرنه اگه یکی double باشه، اون یکی هم double میشه.
-
وگرنه اگه یکی float باشه، اون یکی هم float میشه.
-
وگرنه char و short به int تبدیل میشن.
-
بعد اگه یکی long باشه، اون یکی هم long میشه.
نکته: floatها توی یه عبارت بهصورت خودکار به double تبدیل نمیشن (برخلاف نسخهی اولیهی زبان). معمولاً توابع ریاضی مثل اونایی که توی <math.h> هستن، از double استفاده میکنن. دلیل استفاده از float معمولاً صرفهجویی در فضا توی آرایههای بزرگه، یا توی سیستمهایی که محاسبهی double خیلی گرونه.
وقتی عملوند unsigned وجود داره، قوانین پیچیدهتر میشن چون مقایسهی signed و unsigned به اندازهی نوعها و ماشین بستگی داره. مثلاً اگه int ۱۶ بیتی باشه و long ۳۲ بیتی:
-1L < 1U چون 1U (unsigned int) به signed long تبدیل میشه.
ولی -1L > 1UL چون -1L به unsigned long تبدیل میشه و در نتیجه یه عدد بزرگ مثبت به نظر میاد.
تبدیلها توی انتساب هم اتفاق میافتن: مقدار سمت راست به نوع سمت چپ (که نوع نتیجهست) تبدیل میشه.
کاراکتر به عدد صحیح تبدیل میشه (با یا بدون sign extension). اگه عدد بلندتر به کوتاهتر تبدیل بشه، بیتهای اضافهی سمت چپ حذف میشن.
int i;
char c;
i = c;
c = i;
اینجا مقدار c تغییری نمیکنه، ولی اگه ترتیب برعکس بشه ممکنه اطلاعات از دست بره.
اگه x از نوع float و i از نوع int باشه، هر دو عبارت x = i و i = x باعث تبدیل نوع میشن. تبدیل از float به int قسمت اعشاری رو حذف میکنه. تبدیل از double به float هم ممکنه گرد بشه یا قطع بشه، بستگی به پیادهسازی داره.
چون آرگومان یه تابع هم یه عبارت حساب میشه، موقع فراخوانی تابع هم تبدیل نوع انجام میشه. اگه تابع prototype نداشته باشه، char و short به int تبدیل میشن و float به double. به همین خاطر توابع رو معمولاً با int و double تعریف میکنیم حتی اگه با char یا float صدا زده بشن.
در نهایت، میتونی بهصورت صریح هم نوع یه عبارت رو با استفاده از cast تغییر بدی:
(type name) expression
اینجا expression طبق قانونها به نوع مورد نظر تبدیل میشه. در واقع مثل اینه که اون عبارت به یه متغیر از اون نوع انتساب داده بشه.
مثلاً تابع sqrt از کتابخونه <math.h> ورودی double میخواد، پس اگه n از نوع int باشه باید بنویسی:
sqrt((double) n)
این فقط مقدار n رو موقتاً به double تبدیل میکنه، خود n تغییر نمیکنه.
تابع sqrt اگه prototype داشته باشه:
double sqrt(double)
اون وقت وقتی بنویسی sqrt(2)، عدد صحیح ۲ خودکار به 2.0 (double) تبدیل میشه.
کتابخونه استاندارد C یه مولد عدد تصادفی هم داره که از cast استفاده میکنه:
unsigned long int next = 1;
/* rand: return pseudo-random integer on 0..32767 */
int rand(void)
{
next = next * 1103515245 + 12345;
return (unsigned int)(next/65536) % 32768;
}
/* srand: set seed for rand() */
void srand(unsigned int seed)
{
next = seed;
}
تمرین ۳-۲: تابعی بنویس به نام htoi(s) که یه رشته از اعداد هگزادسیمال (که ممکنه با 0x یا 0X شروع بشه) رو به عدد صحیح معادلش تبدیل کنه. کاراکترهای مجاز ۰ تا ۹، a تا f، و A تا F هستن.