C / الأساسي: قراءة البرامج وبناؤها / الدرس 2
الأنواع والمديات والتحويلات
اختر التمثيلات وفق عقودها وتتبع التحويلات التي تحدث قبل تخزين القيمة.
في هذه الصفحة
أهداف الدرس
- تمييز الحساب الصحيح والعائم.
- استخدام الحدود وsizeof دون افتراض أحجام الآلة.
- شرح الترقيات والتضييق ومخاطر الإشارة.
النوع يحدد القيم ذات المعنى
يحدد النوع للتنفيذ كيفية تفسير التمثيل المخزن والعمليات المنطبقة عليه. يناسب int الأعداد الصحيحة المعتادة، وdouble الحساب الحقيقي التقريبي، وchar تخزين المحارف. لا يملك int ولا long عرضا ثابتا عالميا بالبايت. تعرض limits.h حدود الأعداد الصحيحة، وتقيس sizeof التخزين ببايتات C؛ ويضم البايت CHAR_BIT بتا وليس بالضرورة ثمانية.
اختر الأنواع من مجال المسألة وقيود المنصة الموثقة. يناسب size_t عدد عناصر المصفوفة، بينما تحتاج درجة حرارة قد تكون سالبة إلى تمثيل بإشارة. ولا يوجد نوع بعرض دقيق مثل uint32_t إلا إذا وفر التنفيذ التمثيل الدقيق المطلوب.
الحساب يسبق الإسناد
عندما يكون المعاملان من int تنتج القسمة خارجا صحيحا مقربا نحو الصفر. إسناد ذلك الخارج إلى double لا يستعيد الكسر المحذوف. حوّل أحد المعاملين إلى نوع عائم قبل القسمة إذا احتاج النموذج إلى كسر. يطلب التحويل الصريح التحويل في موضعه، ولا يغير العمليات السابقة بأثر رجعي.
يستخدم المثال قيما موجبة صغيرة حتى تكون كل النتائج الصحيحة الوسيطة قابلة للتمثيل. تُنسق النتيجة العائمة بمنزلتين عشريتين؛ وهذا قرار عرض، وليس دليلا على أن كل كسر عشري له تمثيل ثنائي دقيق. تحتاج حسابات المال غالبا إلى مقياس صحيح مختار بوضوح أو سياسة حساب أكثر تخصصا.
#include <stdio.h>
int main(void)
{
int total = 7;
int people = 2;
double truncated = total / people;
double share = (double)total / people;
printf("%.2f %.2f\n", truncated, share);
return 0;
}
3.00 3.50
تنتج القسمة الأولى int بقيمة 3؛ وتستخدم الثانية حسابا عائما فتعطي 3.5.
تتبع نوع التعبير
تخضع الأنواع الصحيحة الصغيرة لترقيات صحيحة في كثير من التعابير. ثم تتبع المعاملات المختلطة بإشارة ودونها قواعد تحويل قد تجعل المعامل السالب يتصرف كقيمة كبيرة غير سالبة. لا تعالج ذلك بتحويل شامل؛ قرر أولا هل القيم السالبة مقبولة، وتحقق منها، ثم حوّل بعد الفحص فقط.
تجاوز العدد الصحيح ذي الإشارة سلوك غير معرّف في C17؛ أما الحساب دون إشارة فيلتف بترديد واحد زائد أكبر قيمة. لا يفيد الالتفاف إلا عندما تقصده الخوارزمية. وتحويل نتيجة صحيحة تجاوزت المدى بعد حسابها متأخر. في جمع قيم int موجبة، يمنع فحص a <= INT_MAX - b قبل حساب a + b التجاوز.
الفحص قبل العملية الخطرة
يحافظ البرنامج التالي على عقد بسيط: كلا المدخلين int غير سالب، ولا يسمح بالجمع إلا ضمن INT_MAX. يُقيّم الحارس قبل الجمع، وتبقى حساباته نفسها قابلة للتمثيل. أما الإشارات العامة فتحتاج إلى حالات منفصلة للموجب والسالب؛ ولا ينبغي وصف حارس أحادي الجانب بأنه كاشف شامل للتجاوز.
يجعل طبع نتيجة نعم أو لا بدلا من عرض خاص بآلة هذا المثال قابلا للنقل. وثق في البرامج الفعلية هل يُرفض فشل الحساب أم تُشبّع النتيجة أم يُبلغ المستدعي. استبدال إحدى هذه السياسات بأخرى بصمت يغير معنى البيانات.
#include <limits.h>
#include <stdio.h>
int main(void)
{
int a = 20;
int b = 22;
if (a >= 0 && b >= 0 && a <= INT_MAX - b) {
printf("sum=%d\n", a + b);
} else {
puts("out of range");
}
return 0;
}
sum=42
تفي المدخلات الموثقة بالحارس، فينتج الجمع 42 بأمان.
فصل التمثيل عن المعنى
الثابت المحرفي مثل 'A' من نوع int في C، بينما يدل النص الحرفي على مصفوفة char تنتهي بمحرف صفري. قد يكون char العادي ذا إشارة أو دون إشارة، ويهم ذلك عند تخزين بايتات عشوائية. لا تفترض قيمة ترميز لحرف إلا إذا اشترط التنسيق ذلك الترميز صراحة.
في القيم العائمة، قارن احتياجات الحساب بالمدى والدقة المتاحين. قد تتراكم أخطاء التقريب، وقد لا تناسب المساواة الدقيقة نتيجة محسوبة. يعتمد هامش الخطأ المناسب على المقياس وتحليل الخطأ، وليس ثابتا سحريا عاما. سيعود هذا التمييز بين التمثيل والمعنى في تنسيقات الملفات وواجهات الأنظمة.
الأخطاء الشائعة وإصلاحها
يتلقى double القيمة 3 بدلا من 3.5.
- لماذا يحدث؟
- حدثت القسمة الصحيحة قبل الإسناد.
- كيف تصلحه؟
- حوّل أحد المعاملين إلى double قبل القسمة.
- كيف تتجنب تكراره؟
- دوّن نوع كل تعبير وسيط.
تُقارن قيمة سالبة بحجم بطريقة مفاجئة.
- لماذا يحدث؟
- غيرت التحويلات غير الموقعة مجال المقارنة.
- كيف تصلحه؟
- ارفض المدخل السالب قبل تحويله إلى size_t.
- كيف تتجنب تكراره؟
- اجعل التحقق بإشارة والفهرسة دون إشارة خطوتين صريحتين.
يبدو أن التحويل أصلح تجاوزا.
- لماذا يحدث؟
- وقعت العملية غير الصالحة بإشارة قبل التحويل.
- كيف تصلحه؟
- افحص المعاملات أولا أو اختر تمثيلا ذا مدى مبرر.
- كيف تتجنب تكراره؟
- اشتق حدود النتائج الوسيطة، وليس النتائج المخزنة فقط.
اختبر فهمك
1. احسب متوسط القيم الصحيحة 8 و9 و10 مع توضيح موضع التحويل وسياسة الكسر.
اعرض الحل المشروح
المجموع 27 وقابل للتمثيل في int لهذه المدخلات الثابتة. استخدم (double)sum / 3 لطلب قسمة عائمة؛ والنتيجة 9.0. وللبيانات العامة، أثبت أيضا أن المجموع لا يتجاوز المدى أو استخدم تجميعا مفحوصا. تحويل الخارج الصحيح النهائي وحده يبقي أي اقتطاع سابق.
2. اشرح لماذا sizeof(int) == 4 افتراض منصي، واستبدله بشرط قابل للنقل لقبول 30000.
اعرض الحل المشروح
تعد sizeof بايتات C ولا تحدد عدد بتات القيمة ولا تمثيلا عالميا. لطلب تمثيل 30000 في int، قارن حاجة المجال مع INT_MAX؛ وتضمن ISO C أصلا قدرة int على تمثيل القيم حتى 32767 على الأقل. ولحقل تسلسلي من 32 بتا، وثق الترميز واستخدم دعم العرض الدقيق بعد التحقق من توافره.
أهم ما تعلمته
- تحدد أنواع التعابير الحساب قبل الإسناد.
- أثبت مديات النتائج الوسيطة وافحص تغيرات الإشارة.
- استخدم الحدود الموثقة؛ فحجم التخزين وحده لا يحدد تنسيق بيانات قابلا للنقل.