ثابت (برمجة الحاسوب)

في برمجة الحاسوب ، الثابت هو قيمة لا يُغيرها البرنامج أثناء التنفيذ العادي . وعندما يرتبط الثابت بمعرّف ، يُقال إنه "مُسمى"، مع أن مصطلحي "ثابت" و"ثابت مُسمى" يُستخدمان غالبًا بشكل متبادل. ويُقابل ذلك المتغير ، وهو معرّف ذو قيمة قابلة للتغيير أثناء التنفيذ العادي. ولتبسيط الأمر، تبقى قيمة الثوابت ثابتة، بينما تتغير قيمة المتغيرات ، ومن هنا جاءت تسميتهما.

تُعد الثوابت مفيدة لكل من المبرمجين والمترجمات : فبالنسبة للمبرمجين، فهي شكل من أشكال التعليمات البرمجية ذاتية التوثيق وتسمح بالاستدلال حول الصحة ، بينما بالنسبة للمترجمات، فإنها تسمح بإجراء فحوصات وقت الترجمة ووقت التشغيل التي تتحقق من عدم انتهاك افتراضات الثبات، [ أ ] وتسمح أو تبسط بعض تحسينات المترجم .

توجد تطبيقات محددة ومتنوعة للمفهوم العام للثابت، مع وجود فروق دقيقة غالباً ما يتم تجاهلها. وأهمها: الثوابت في وقت الترجمة (الثوابت ذات القيم الثابتة)، والثوابت في وقت التشغيل (الثوابت ذات القيم الديناميكية)، والكائنات غير القابلة للتغيير ، وأنواع الثوابت const.

تشمل الأمثلة النموذجية للثوابت في وقت الترجمة الثوابت الرياضية، والقيم من المعايير (هنا وحدة الإرسال القصوى )، أو قيم التكوين الداخلية (هنا الأحرف لكل سطر )، مثل أمثلة لغة C التالية:

const float PI = 3.1415927 ; // أقصى دقة عددية أحادية الفاصلة العائمة const unsigned int MTU = 1500 ; // إيثرنت الإصدار 2، RFC 894 const unsigned int COLUMNS = 80 ;

من الأمثلة النموذجية لثوابت وقت التشغيل القيم المحسوبة بناءً على مدخلات دالة، مثل مثال C++ هذا:

استيراد std ؛باستخدام std :: string ;void f ( string & s ) { const size_t len ​​= s . length (); // ... }

يستخدم

تُجري بعض لغات البرمجة تمييزًا نحويًا صريحًا بين الرموز الثابتة والمتغيرة، على سبيل المثال تعتبر إسناد قيمة إلى ثابت خطأً نحويًا، بينما في لغات أخرى تعتبر متماثلة نحويًا (كلاهما مجرد مُعرّف)، والفرق في المعاملة دلالي (إسناد قيمة إلى مُعرّف صحيح نحويًا، ولكن إذا كان المُعرّف ثابتًا فهو غير صحيح دلاليًا).

تُعرَّف القيمة الثابتة مرة واحدة، ويمكن الرجوع إليها عدة مرات في البرنامج. استخدام قيمة ثابتة بدلاً من تحديد القيمة نفسها عدة مرات يُسهِّل صيانة الكود (أي تجنب التكرار )، كما أنه يُوثِّق نفسه بنفسه من خلال توفير اسم ذي معنى للقيمة، على سبيل المثال، PIبدلاً من 3.1415926.

مقارنة مع القيم الحرفية والماكرو

توجد عدة طرق رئيسية للتعبير عن قيمة بيانات لا تتغير أثناء تنفيذ البرنامج، وهي طرق متسقة عبر مجموعة واسعة من لغات البرمجة. إحدى أبسط هذه الطرق هي كتابة رقم أو حرف أو سلسلة نصية حرفية في كود البرنامج، وهو أمر سهل في لغات مثل C وC++ وما شابهها.

في لغة التجميع ، تُعالج الأرقام والحروف الحرفية باستخدام تعليمات "الوضع الفوري" المتوفرة في معظم المعالجات الدقيقة. يُشتق اسم "الوضع الفوري" من كون القيم متاحة مباشرةً من سلسلة التعليمات ، بدلاً من تحميلها بشكل غير مباشر عبر البحث عن عنوان في الذاكرة. [ 1 ] من ناحية أخرى، تُعالج القيم الأطول من طول كلمة المعالج الدقيق، مثل السلاسل النصية والمصفوفات، بشكل غير مباشر، وعادةً ما توفر مُجمِّعات البرامج عملية "بيانات" وهمية لتضمين جداول البيانات هذه في البرنامج.

هناك طريقة أخرى وهي تعريف ماكرو رمزي . توفر العديد من لغات البرمجة عالية المستوى، والعديد من المجمعات، خاصية الماكرو حيث يمكن للمبرمج تعريف أسماء لقيم مختلفة، عادةً في بداية ملف المصدر أو في ملف تعريف منفصل. يقوم المعالج المسبق بعد ذلك باستبدال هذه الأسماء بالقيم المناسبة قبل الترجمة، مما ينتج عنه شيء مطابق وظيفيًا لاستخدام القيم الحرفية، مع مزايا سرعة الوضع الفوري. نظرًا لصعوبة صيانة التعليمات البرمجية التي تُكتب فيها جميع القيم حرفيًا، إذا تم استخدام قيمة ما بطريقة متكررة أو غير بديهية، فغالبًا ما يتم تسميتها بواسطة ماكرو.

ثمة طريقة ثالثة تتمثل في تعريف متغير على أنه "ثابت". يمكن تعريف متغير عام أو ثابتconst (أو رمز مُعرَّف بلغة التجميع) باستخدام مُحدِّد كلمات مفتاحية مثل `static` constexprأو constant`static` أو `static` (عند تطبيقه على الأنواع الأولية)، مما يعني أن قيمته ستُحدَّد في وقت الترجمة ولا ينبغي تغييرها في وقت التشغيل. عادةً ما تضع المُترجمات الثوابت الثابتة في قسم النص من ملف الكائن مع الكود نفسه، بدلاً من قسم البيانات حيث تُحفظ البيانات غير الثابتة. يمكن لبعض المُترجمات إنشاء قسم مُخصَّص للثوابت. يمكن تطبيق حماية الذاكرة على هذا القسم لمنع الكتابة فوق هذه الثوابت بواسطة مؤشرات خاطئة.final

تختلف هذه الثوابت عن القيم الحرفية في عدة جوانب. عادةً ما تضع المترجمات الثابت في موقع ذاكرة واحد يُحدد برمز، بدلاً من توزيعه في جميع أنحاء الملف التنفيذي كما هو الحال مع الماكرو. ورغم أن هذا يحرم المستخدم من مزايا السرعة التي يوفرها الوضع الفوري، إلا أنه يوفر مزايا في كفاءة استخدام الذاكرة، كما يمكن لأدوات تصحيح الأخطاء التعامل مع هذه الثوابت أثناء التشغيل. كذلك، فبينما قد يُعاد تعريف الماكرو عن طريق الخطأ بسبب ملفات رأسية متضاربة في لغتي C وC++، يتم اكتشاف الثوابت المتضاربة أثناء الترجمة.

بحسب لغة البرمجة، يمكن أن تكون الثوابت غير محددة النوع أو محددة النوع. في لغتي C و C++، توفر وحدات الماكرو النوع الأول، بينما يوفر الرمز const/ constexprالنوع الثاني:

#عرّف PI 3.1415926535constexpr double PI = 3.1415926535 ; // ثابت وقت الترجمة const double pi = 3.1415926535 ; // ثابت وقت التشغيل

بينما في لغة آدا، توجد أنواع رقمية عامة يمكن استخدامها، إذا رغبت في ذلك:

باي : ثابت := 3.1415926535 ؛pi2 : ثابت عدد عشري := 3.1415926535 ;

مع تحويل النوع غير المحدد ضمنيًا إلى النوع المناسب عند كل استخدام. [ 2 ]

الثوابت ذات القيم الديناميكية

إلى جانب الثوابت الثابتة الموضحة أعلاه، فإن العديد من اللغات الإجرائية مثل Ada و C++ توسع مفهوم الثبات ليشمل المتغيرات العامة التي يتم إنشاؤها في وقت التهيئة، والمتغيرات المحلية التي يتم إنشاؤها تلقائيًا في وقت التشغيل على المكدس أو في السجلات، والذاكرة المخصصة ديناميكيًا والتي يتم الوصول إليها بواسطة المؤشر، وقوائم المعلمات في رؤوس الدوال.

لا تُحدد الثوابت ذات القيم الديناميكية موقع المتغير في منطقة معينة من الذاكرة، كما لا يتم تعيين قيمها في وقت الترجمة. في كود C++ مثل

دالة float ( const float myFloat ) { const float result = someGlobalVariable * someOtherFunction ( myFloat ); // ... }

إنّ التعبير الذي تُهيّأ به القيمة الثابتة ليس ثابتًا في حد ذاته. استخدام خاصية الثبات ليس ضروريًا هنا لضمان صحة البرنامج أو دلالاته، ولكنه يحمل ثلاث مزايا:

  1. من الواضح للقارئ أن الكائن لن يتم تعديله بعد تعيينه.
  2. سيتم رفض محاولات تغيير قيمة الكائن (من قبل مبرمجين لاحقين لا يفهمون منطق البرنامج بشكل كامل) بواسطة المترجم
  3. قد يتمكن المترجم من إجراء تحسينات على التعليمات البرمجية مع العلم أن قيمة الكائن لن تتغير بمجرد إنشائه. [ 3 ]

ظهرت الثوابت ذات القيم الديناميكية كميزة لغوية مع لغة ALGOL 68. [ 3 ] وقد أظهرت دراسات على أكواد لغتي Ada وC++ أن استخدام الثوابت ذات القيم الديناميكية نادر، عادةً في 1% أو أقل من الكائنات، بينما يمكن استخدامها على نطاق أوسع، حيث أن حوالي 40-50% من الكائنات المحلية غير المصنفة تظل ثابتة بمجرد إنشائها. [ 3 ] [ 4 ] من ناحية أخرى، تميل هذه "المتغيرات غير القابلة للتغيير" إلى أن تكون الخيار الافتراضي في اللغات الوظيفية لأنها تُفضل أساليب البرمجة التي لا تُسبب آثارًا جانبية (مثل الاستدعاء الذاتي) أو تجعل معظم التصريحات غير قابلة للتغيير افتراضيًا، كما هو الحال في لغة ML . بل إن اللغات الوظيفية البحتة تمنع الآثار الجانبية تمامًا.

يُستخدم مفهوم الثبات غالبًا في تعريفات الدوال، كضمانةٍ بأن الدالة المُستدعاة لن تُغير الكائن عند تمريره بالمرجع. وبحسب الصيغة، قد يكون المؤشر أو الكائن المُشار إليه ثابتًا، إلا أن الخيار الأخير هو المُفضل عادةً. في لغتي C++ وC تحديدًا، يُطلق على مبدأ ضمان ثبات هياكل البيانات في جميع أنحاء البرنامج اسم " الصحة الثابتة" .

معلمات الدالة الثابتة

في لغتي C/C++، يُمكن تعريف مُعامل دالة أو طريقة على أنه ثابت. يضمن هذا عدم إمكانية تعديل هذا المُعامل عن غير قصد بعد تهيئته من قِبل المُستدعي. إذا كان المُعامل من نوع مُعرّف مُسبقًا (مُدمج)، فإنه يُستدعى بالقيمة ولا يُمكن تعديله. أما إذا كان من نوع مُعرّف من قِبل المُستخدم، فإن المُتغير يكون عنوان المؤشر، والذي لا يُمكن تعديله أيضًا. مع ذلك، يُمكن تعديل محتوى الكائن بلا حدود. قد يكون تعريف المُعاملات على أنها ثوابت وسيلةً للإشارة إلى أنه لا ينبغي تغيير هذه القيمة، ولكن يجب على المُبرمج أن يُدرك أن المُترجم لا يُمكنه التحقق من تعديل الكائن.

إلى جانب هذه الميزة، من الممكن في لغة C++ أيضًا تعريف دالة أو طريقة على أنها const. وهذا يمنع هذه الدوال أو الطرق من تعديل أي شيء باستثناء المتغيرات المحلية.

في لغة C#، توجد الكلمة المفتاحية const، لكنها لا تُؤثر على معاملات الدوال بنفس الطريقة التي تُؤثر بها في لغتي C/C++. مع ذلك، توجد طريقة لحث المُصرّف على إجراء هذا الفحص، وإن كانت هذه الطريقة مُعقدة بعض الشيء. [ 5 ]

الثوابت الموجهة للكائنات

يُشار إلى بنية البيانات الثابتة أو الكائن بـ " غير قابل للتغيير " في مصطلحات البرمجة الكائنية. ويُضفي عدم قابلية الكائن للتغيير بعض المزايا في تصميم البرامج. على سبيل المثال، يمكن "نسخه" ببساطة عن طريق نسخ مؤشره أو مرجعه، مما يتجنب عملية النسخ التي تستغرق وقتًا طويلاً ويوفر الذاكرة.

تُوسّع لغات البرمجة كائنية التوجه، مثل C++، مفهوم الثبات إلى أبعد من ذلك. إذ يُمكن جعل عناصر مُحددة في بنية أو فئة ثابتة (const) حتى لو لم تكن الفئة نفسها mutableثابتة. في المقابل، تسمح الكلمة المفتاحية `const` بتغيير عنصر في الفئة حتى لو تم إنشاء كائن منها كـ `const` const. قد يكون هذا مفيدًا في الحالات التي يُراد فيها أن تكون البيانات فقط ثابتة const، ولكن قد تتغير بعض البيانات الوصفية أو المعلومات العامة للفئة.

حتى الدوال يمكن أن تكون ثابتة (const) في لغة C++. والمعنى هنا هو أنه لا يمكن استدعاء سوى دالة ثابتة (const) لكائن تم إنشاؤه كـ const؛ فالدالة الثابتة لا تُغير أي بيانات غير قابلة للتغيير.

تحتوي لغة C# على كل من المحددات a constو a readonly؛ فمحدد const الخاص بها مخصص فقط للثوابت في وقت الترجمة، بينما readonlyيمكن استخدام المحددات في المنشئات وتطبيقات وقت التشغيل الأخرى.

في لغة C++، هناك أربعة أنواع مختلفة من const:

  • constالثابت الزمني التقليدي
  • constexprثابت أو تعبير يمكن تقييمه في وقت الترجمة
  • consteval، والذي ينص على أن أي استدعاء لدالة يجب أن ينتج قيمة عبارة عن تعبير وقت الترجمة
  • constinit، مما يُعلن أن التعبير له تهيئة ثابتة (ثابتة)

في لغة Rust ، جميع التصريحات ثابتة ما لم يتم تعريفها mutعلى أنها قابلة للتغيير. أما بالنسبة للثوابت التي يتم تحديدها أثناء الترجمة، constفيتم استخدام .

جافا

تحتوي لغة جافا على مُحدِّد يُسمى `input` يمنع تغيير مرجع الكائن ويضمن عدم إشارته إلى كائن آخر. هذا لا يمنع التغييرات التي تطرأ على الكائن المُشار إليه نفسه. يُعادل `input` في جافا مؤشرًا في لغة C++، ولكنه لا يُوفر الميزات الأخرى لـ `input` .finalfinalconstconst

في لغة جافا ، يشير المحدد إلى أن عنصر البيانات أو المتغير المتأثر غير قابل للتخصيص، كما يلي:final

final int i = 3 ; i = 4 ; // خطأ! لا يمكن تعديل كائن "نهائي"

يجب أن يكون بإمكان المترجمات تحديد مكان تهيئة المتغير الذي يحمل العلامة، ويجب تنفيذ ذلك مرة واحدة فقط، وإلا فلن يتم تجميع الفئة. للكلمات المفتاحية في لغتي Java وC++ نفس المعنى عند استخدامها مع المتغيرات الأولية.finalfinalconst

const int i = 3 ; // تعريف C++ i = 4 ; // خطأ!

فيما يتعلق بالمؤشرات، فإن المرجع في لغة جافا يعني شيئًا مشابهًا للمؤشر في لغة سي++. في لغة سي++، يمكن تعريف "نوع مؤشر ثابت".finalconst

Foo * const bar = mem_location ; // نوع المؤشر الثابت

هنا، barيجب تهيئة المتغير عند تعريفه ولا يمكن تغييره لاحقًا، لكن ما يشير إليه قابل للتعديل. أي أنه صالح. لكن لا يمكنه الإشارة إلى موقع آخر. تعمل المراجع النهائية في جافا بنفس الطريقة، باستثناء أنه يمكن تعريفها بدون تهيئة.*bar=value

final Foo i ; // تعريف جافا

ملاحظة: لا تدعم لغة جافا المؤشرات. [ 6 ] والسبب هو أن المؤشرات (مع بعض القيود) هي الطريقة الافتراضية للوصول إلى الكائنات في جافا، ولا تستخدم جافا النجوم للإشارة إليها. على سبيل المثال، i في المثال الأخير هو مؤشر ويمكن استخدامه للوصول إلى الكائن.

يمكن أيضًا تعريف مؤشر إلى بيانات "للقراءة فقط" في لغة C++.

const Foo * bar ;

يمكن تعديل هذا العنصر barليشير إلى أي شيء، في أي وقت؛ لكن لا يمكن تعديل القيمة المشار إليها من خلالbar المؤشر.

لا توجد آلية مماثلة في جافا، وبالتالي لا توجد constدوال. لا يمكن فرض صحة الثوابت في جافا، ولكن باستخدام الواجهات وتحديد واجهة للقراءة فقط للفئة وتمريرها، يمكن ضمان إمكانية تمرير الكائنات في النظام بطريقة تمنع تعديلها.

يوفر إطار عمل مجموعات جافا طريقة لإنشاء غلاف غير قابل للتغيير لـ عبر طرق مماثلة.CollectionCollections.unmodifiableCollection()

يمكن تعريف طريقة في لغة جافا على أنها "نهائية"، مما يعني أنه لا يمكن تجاوزها في الفئات الفرعية.

تحتفظ لغة جافا بالكلمة المفتاحية، لكنها لا تزال غير مستخدمة حاليًا.const

سي شارب

في لغة C# ، يُحدث المُؤهِّل readonlyنفس التأثير على عناصر البيانات كما هو الحال في Java و C++؛ أما المُعدِّل فيُحدث تأثيرًا مشابهًا لتأثيره في C/C++. ويُمكن إحداث التأثير الآخر، وهو تثبيط الوراثة، الذي يُحدثه Java عند تطبيقه على الدوال والفئات، في C# باستخدام الكلمة المفتاحية ` .finalconstconstconstexprfinalsealed

على عكس لغة C++، لا تسمح لغة C# بوضع علامات على الطرق والمعاملات كـ const. ومع ذلك، يمكن أيضًا تمرير الفئات الفرعية للقراءة فقط، ويوفر إطار عمل .NET بعض الدعم لتحويل المجموعات القابلة للتغيير إلى مجموعات غير قابلة للتغيير والتي يمكن تمريرها كأغلفة للقراءة فقط.

حسب النموذج

تختلف معالجة الثوابت اختلافًا كبيرًا باختلاف نماذج البرمجة . تُعدّ صحة الثوابت مشكلة في اللغات الإجرائية مثل C++، لأنّ ربط الأسماء يُنشئ عادةً متغيرات افتراضيًا ، والتي قد تتغير، كما يوحي الاسم، وبالتالي، إذا رغب المستخدم في تحديد ربط ما على أنه ثابت، فهذا يتطلب إشارة إضافية. [ ب ] في نماذج لغات البرمجة الأخرى، تظهر مشكلات مشابهة، مع وجود بعض النظائر لمشكلة صحة الثوابت.

في البرمجة الوظيفية ، تكون البيانات ثابتة افتراضيًا، وليست متغيرة. فبدلًا من إسناد قيمة لمتغير (مساحة تخزين لها اسم وقيمة متغيرة محتملة)، يتم ربط الاسم بالقيمة، كما هو الحال letفي العديد من لهجات لغة ليسب . في بعض اللغات الوظيفية، وخاصة متعددة الأنماط مثل كومون ليسب ، يُعد تعديل البيانات أمرًا شائعًا، بينما في لغات أخرى يُتجنب أو يُعتبر استثنائيًا؛ وهذا هو الحال في لغة سكيم (لهجة أخرى من لهجات ليسب)، التي تستخدم هذا set!التركيب لتعديل البيانات، حيث تشير علامة التعجب ! إلى ذلك. تحقق هذه اللغات هدف الثبات الافتراضي، إذ تُركز على التعديل بدلًا من الثبات.

في العديد من لغات البرمجة كائنية التوجه ، يوجد مفهوم الكائن غير القابل للتغيير ، والذي يُستخدم تحديدًا للأنواع الأساسية مثل السلاسل النصية؛ ومن الأمثلة البارزة على ذلك لغات Java و JavaScript وPython وC#. وتختلف هذه اللغات فيما إذا كان بالإمكان وضع علامة "غير قابل للتغيير" على الأنواع المُعرَّفة من قِبل المستخدم، وقد تسمح بوضع علامة "غير قابل للتغيير" على حقول (سمات) مُحدَّدة لكائن أو نوع ما.

في بعض لغات البرمجة متعددة الأنماط التي تسمح بكل من البرمجة الكائنية والبرمجة الوظيفية، يمكن الجمع بين هاتين الميزتين. على سبيل المثال، في لغة OCaml ، تكون حقول الكائنات غير قابلة للتغيير افتراضيًا، ويجب تحديدها صراحةً باستخدام الكلمة المفتاحية `remove` mutableلتكون قابلة للتغيير، بينما في لغة Scala، تكون الروابط غير قابلة للتغيير صراحةً عند تعريفها باستخدام ` valremove` لـ "value"، وقابلة للتغيير صراحةً عند تعريفها باستخدام `remove` varلـ "variable".

اصطلاحات التسمية

تختلف اصطلاحات تسمية الثوابت. فبعضها يسميها ببساطة كما يسمي أي متغير آخر. بينما يستخدم البعض الآخر الأحرف الكبيرة والشرطات السفلية للثوابت بطريقة مشابهة لاستخدامها التقليدي مع وحدات الماكرو الرمزية، مثل SOME_CONSTANT. [ 7 ] في الترميز المجري ، يشير الحرف "k" كبادئة إلى الثوابت وكذلك وحدات الماكرو والأنواع المُعَدَّة .

أحد الاتفاقات المفروضة هو أنه في لغة روبي ، يعتبر أي متغير يبدأ بحرف كبير ثابتًا، بما في ذلك أسماء الفئات.

انظر أيضاً

ملحوظات

  1. في بعض الحالات يمكن تجاوز ذلك، على سبيل المثال باستخدام التعليمات البرمجية ذاتية التعديل أو عن طريق الكتابة فوق موقع الذاكرة حيث يتم تخزين القيمة.
  2. هذا ليس عالميًا: في لغة Ada، تكون معلمات الإدخال ومعلمات الحلقة ثابتة ضمنيًا، على سبيل المثال.

مراجع

  1. مثال: معلومات أنظمة IBM . مجموعة التعليمات - مرجع لغة التجميع لـ PowerPC.
  2. بوتش، جرادي (1983). هندسة البرمجيات باستخدام لغة آدا . بنجامين كامينغز . الصفحات 116-117 . ISBN  0-8053-0600-5.
  3. 1 2 3 شيلينغ، جوناثان ل. (أبريل 1995). "الثوابت ذات القيم الديناميكية: ميزة لغوية غير مستغلة بشكل كافٍ" . إشعارات SIGPLAN . 30 (4): 13-20 . doi : 10.1145/202176.202177 . S2CID 17489672 . 
  4. بيركنز، جيه إيه. ممارسات البرمجة: تحليل مصدر لغة آدا المطورة للقوات الجوية والجيش والبحرية . وقائع مؤتمر TRI-Ada '89. الصفحات 342-354 . doi : 10.1145/74261.74287 . 
  5. Timwi (2010-09-09). "معاملات الدوال للقراءة فقط ("شبيهة بالثابت") في لغة C#" . Stack Overflow . تم الاسترجاع في 2012-05-06 . [...] يمكنك بعد ذلك تعريف دوال يُحدد نوع معاملها ما إذا كانت ستُغير المتغير أم لا. [...] يُحاكي هذا عمليات التحقق أثناء الترجمة، على غرار خاصية الثبات في لغة C++. وكما أشار إريك ليبرت بشكل صحيح، فإن هذا لا يُعادل عدم قابلية التغيير. ولكن بصفتك مبرمج C++، أعتقد أنك تُدرك ذلك.
  6. "شبكة أوراكل التقنية لمطوري جافا | شبكة أوراكل التقنية | أوراكل" . Java.sun.com. 14 أغسطس 2013. تم الاطلاع عليه بتاريخ 18 أغسطس 2013 .
  7. مطور مايكروسوفت أوفيس إكس بي: الأسماء الثابتة