اذهب إلى المحتوى

المصفوفات في Cpp


محمد بغات

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

تهيئة المصفوفة

المصفوفة هي كتلة مكوّنة من مواقع متسلسلة في الذاكرة والمخصصة لنوع معيّن من المتغيرات. تخصيص المصفوفات يشبه تخصيص المتغيرات العادية، ولكن مع إضافة قوسين مربّعين إلى اسمها [] يحتويان على عدد يمثل عدد العناصر التي يمكن أن تحتويها ذاكرة المصفوفة.

يستخدم المثال التالي مصفوفة تستخدم نوع int، واسم المتغير arrayOfInts، وعدد عناصرها هو [ 5 ]:

int arrayOfInts[5];

يمكن التصريح عن المصفوفة وتهيئتها في نفس الوقت على النحو التالي:

int arrayOfInts[5] = {10, 20, 30, 40, 50};

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

int arrayOfInts[] = {10, 20, 30, 40, 50};

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

int arrayOfInts[5] = {10,20};    

أي أن عناصر المصفوفة السابقة هي (10, 20, 0, 0, 0). كذلك يمكن تهيئة مصفوفات أنواع البيانات الأساسية الأخرى بالطريقة نفسها. انظر المثال التالي للتصريح عن مصفوفة وتخصيص مساحة ذاكرة لها دون تهيئتها:

char arrayOfChars[5];

أو للتصريح عنها مع تهيئتها:

char arrayOfChars[5] = { 'a', 'b', 'c', 'd', 'e' } ; 
double arrayOfDoubles[5] = {1.14159, 2.14159, 3.14159, 4.14159, 5.14159};
string arrayOfStrings[5] = { "C++", "is", "super", "duper", "great!"};

لاحظ أنه عند الوصول إلى عناصر المصفوفة فإن فهرس المصفوفة (أو موضعها) يبدأ عند القيمة 0. انظر المثال التالي، حيث يكون العنصر 10 هو العنصر رقم 0، و20 هو العنصر رقم 1، وهكذا.

int array[5] = { 10, 20, 30, 40, 50};
std::cout << array[4]; // 50
std::cout << array[0]; // 10

المصفوفات متعددة الأبعاد ذات الحجم الثابت

يشير الفهرس m[y]‎ في المثال أدناه إلى الصف رقم yمن المصفوفة m، حيث y مؤشرٌ يبدأ من الصفر، ويمكن فهرسة هذا الصف على النحو التالي my ‎ والذي يشير إلى العنصر/العمود x من الصف y، وذلك يعني أن الفهرس الأخير هو الأسرع تغيرًا، ونطاقه في التصريح -حيث النطاق هنا هو رقم الأعمدة لكل صف- هو آخر و"أعمق" حجم محدَّد.

#include <iostream>
#include <iomanip>
using namespace std;

auto main() -> int
{
    int const n_rows = 3;
    int const n_cols = 7;
    int const m[n_rows][n_cols] = 
    {
        { 1, 2, 3, 4, 5, 6, 7 },
        { 8, 9, 10, 11, 12, 13, 14 },
        { 15, 16, 17, 18, 19, 20, 21 },
    };

    for( int y = 0; y < n_rows; ++y )
    {
        for( int x = 0; x < n_cols; ++x )
        {
 // m[y,x] لا تستخدم
            cout << setw( 4 ) << m[y][x];
        }
        cout << '\n';
    }
}

يكون الناتج:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 

لا تدعم C++‎ صياغة خاصة لفهرسة المصفوفة متعددة الأبعاد، وإنما تتعامل معها على أنها مصفوفة مكونة من مصفوفات أخرى داخلها، وتستخدم الفهرسة العادية لكل مستوى.

وبما أن C++‎ لا توفر دعمًا بشكل افتراضي للمصفوفات ذات الحجم المتغير أو المصفوفات الديناميكية (Dynamic Size Arrays) ما عدا التخصيص الديناميكي (Dynamic Allocation)، فإن المصفوفة الديناميكية تُستخدم غالبًا كصنف (Class).

لكن هناك بعض التعقيدات المرتبطة بصياغة الفهرسة m[y][x]‎، إما بكشف الاستخدام ليستحيل عرض منقول المصفوفة مثلًا (Transposed Matrix) أو بإضافة حمل زائد على البرنامج عند تنفيذه بإعادة كائن وكيل (Proxy Object) من عامل الفهرسة operator[OD1][]‎، وعليه قد تكون صياغة الفهرسة مختلفة سواء في الشكل أو في ترتيب الفهارس، كما هو الحال في m(x,y)‎ أو m.at(x,y)‎ أو m.item(x,y)‎.

المصفوفات الديناميكية (Dynamically sized raw array)

انظر المثال التالي كمثال على مصفوفة ديناميكية، واعلم أن الأفضل عمومًا هو استخدام std::vector:

#include <algorithm>            // std::sort
#include <iostream>
using namespace std;

auto int_from( istream& in ) -> int { int x; in >> x; return x; }
auto main()
       -> int
{
    cout << "Sorting n integers provided by you.\\n";
cout << "n? ";
int const n = int_from( cin );
// n تخصيص مصفوفة عدد عناصرها 
int* a = new int[n];

     for( int i = 1; i <= n; ++i )
{
cout << "The #" << i << " number, please: ";
a[i-1] = int_from( cin );
}
sort( a, a + n );
for( int i = 0; i < n; ++i ) { cout << a[i] << ' '; }
cout << '\\n';

delete[] a;    
}

في بعض المُصرِّفات التي تدعم المصفوفات ذات الطول المتغير وفق معيار C99 ‏(variadic length arrays أو VLAs) كإضافة للّغة، يمكن تصريف برنامج يعلن عن مصفوفة T a[n];‎، حيث لا تُحدَّد n حتى وقت التشغيل (run-time). لكنّ C++‎ القياسية لا تدعم تلك المصفوفات، لذا يمكن استخدام تعبير new[]‎ لتخصيص مصفوفة ديناميكية بشكل يدوي، انظر المثال التالي حيث نخصص مصفوفة مكونة من n عنصر:

int*   a   = new int[n]; 

تستطيع إلغاء تخصيص المصفوفة بعد استخدامها عبر delete[]‎:

delete[] a;

قيم المصفوفة التي صرّحنا عنها أعلاه غير محددة، ولكن يمكن تهيئة عناصرها عند القيمة صفر عبر إضافة قوسين فارغين () هكذا: new int[n]()‎، وعمومًا فإنه لأي نوع من أنواع البيانات، يهيئ هذا التعبير قيمَ المصفوفة بالقيمة الافتراضية لذلك النوع.

لن تكون هذه الشيفرة آمنة للتنفيذ كجزء من دالة في أسفل هرمية الاستدعاء، ذلك أنه قد يحدث تسريب للذاكرة في حالة وجود استثناء قبل تعبير delete[]‎‎، (أو بعد ‎new[]‎)، وحل ذلك يكون بأتمتة عملية التنظيف عبر استخدام المؤشر الذكي std::unique_ptr، رغم أن الأفضل هو استخدام متجه (std::vector) إذ تلك هي وظيفته الأساسية.

حجم المصفوفة

انظر المثال التالي:

#include      // size_t, ptrdiff_t
 //-----------------------------------:
using Size = ptrdiff_t;
template < class Item, size_t n >
    constexpr auto n_items(Item( & )[n]) noexcept 
    -> Size 
   { return n; }
//----------------------------------- الاستخدام:
#include
using namespace std;
auto main()
-> int
{
int const a[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
Size const n = n_items( a );
        Size
        const n = n_items(a);
        int b[n] = {}; // a مصفوفة لها نفس حجم  
        (void) b;
        cout <}

يمكن الحصول على حجم المصفوفة في لغة C عبر: sizeof(a)‎ أو sizeof(a[0])‎، في حال تمرير مؤشر كوسيط، لكن تكون النتيجة بشكل عام غير دقيقة. أما في C++11، يمكنك الحصول على حجم المصفوفة بالتعبير التالي:

std::extent<decltype(MyArray)>::value;

مثال:

char MyArray[] = { 'X','o','c','e' };
const auto n = std::extent<decltype(MyArray)>::value;
std::cout << n << "\n"; // يطبع 4

لم يكن في أي إصدار من C++‎ إلى الإصدار C++17 أي آلية أو مكتبة قياسية مضمَّنة فيها للحصول على أحجام المصفوفات، وإنما نحصل على ذلك بتمرير مرجع يشير للمصفوفة إلى قالب دالة (function template) كما هو موضح أعلاه.

معامل حجم القالب (template size parameter) هو size_t، وهو غير متوافق مع النوع المؤشَّر (signed type) للقيمة المعادة من الدّالة Size، وذلك لأجل التوافق مع المُصرّف g++‎ الذي يصر أحيانًاعلى size_t من أجل مطابقة القوالب. كبديل عن ذلك، يمكن استخدام std::size في C++17 والإصدارات اللاحقة لها، إذ هو تابع مخصص للمصفوفات.

توسيع المصفوفات الديناميكية باستخدام المتجهات

انظر المثال التالي الذي يوضح استخدام متجه std::vector كمصفوفة ديناميكية قابلة للتوسع:

#include <algorithm>            // std::sort
#include <iostream>
#include <vector>               // std::vector
using namespace std;

int int_from( std::istream& in ) { int x = 0; in >> x; return x; } 
int main() 
{     cout << "Sorting integers provided by you.\n";
    cout << "You can indicate EOF via F6 in Windows or Ctrl+D in Unix-land.\n";
    vector < int > a; // ← الحجم يساوي 0 افتراضيا
while( cin ) {
        cout << "One number, please, or indicate EOF: ";
        int const x = int_from( cin );
        if( !cin.fail() ) { a.push_back( x ); }          // التوسيع بحسب الضرورة
    }
    sort( a.begin(), a.end() );
    int const n = a.size();
    for( int i = 0; i < n; ++i ) { cout << a[i] << ' '; }
    cout << '\n';
}

std::vector هو قالب صنف في المكتبة القياسية التي توفر مفهوم المصفوفة ذات الحجم المتغير (الديناميكية)، وتتكفل تلك المكتبة بإدارة الذاكرة، كما أنّ المخزن المؤقت (buffer) متصل (contiguous)، لذا يمكن تمرير مؤشر يشير إلى المخزن المؤقت (على سبيل المثال ‎&v[0]‎ أو v.data()‎) إلى دوال الواجهة البرمجية (API) التي تتطلّب مصفوفةً خام (raw array). يمكن توسيع المتجهة vector في وقت التشغيل عبر التابع push_back الذي يضيف عنصرًا إلى المصفوفة.

يُحدَّد تعقيد سلسلة عمليات push_back عددها n، بما في ذلك عمليات النسخ والنقل المستخدمتين في توسيعات المتجهات، في المتوسط من خلال O(n)، ويُنفّذ ذلك داخليًا عبر مضاعفة حجم المخزن المؤقّت للمتجهة vector وسِعتها عند الحاجة إلى حجم أكبر.

فمثلًا، إذا كان المخزن المؤقّت يبدأ بالحجم 1، ثم يتضاعف حجمه بشكل متكرر حسب الحاجة، فإنّه في مقابل n=17 عملية استدعاءً للتابع push_back، ستكون هناك 1 + 2 + 4 + 8 + 16 = 31 عملية نسخ، أي أقل من 2 × n = 34. لكن بشكل عام فلا يمكن أن يتجاوز مجموع هذا التسلسل القيمة 2 × n.

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

استخدام std::vector في المصفوفة الديناميكية للتخزين

بدءًا من الإصدار C++14 لم يعد هناك أي صنف (Class) مخصص للمصفوفات الديناميكية في المكتبة القياسية، وإنما ستجد مثل تلك الأصناف التي تدعم الحجم المتغير في بعض مكتبات الطرف الثالث، بما في ذلك مكتبة Boost Matrix (مكتبة فرعية داخل مكتبة Boost). وإن لم ترد الاعتماد على Boost أو أيّ مكتبة أخرى، فيمكنك كتابة المصفوفات متعددة الأبعاد الديناميكية في ++C على نحو ما في المثال التالي، حيث vector هي متجهة من النوع std::vector.

vector<vector<int>> m( 3, vector<int>( 7 ) );

تُنشأ المصفوفة هنا عن طريق نسخ متجه صفِّي (row vector) عددًا من المرات قدره n مرة، حيث n هو عدد الصفوف الذي يساوي 3 في مثالنا أعلاه. تمتاز تلك الطريقة بدعمها لصيغة الفهرسة m[y][x]‎ كما في المصفوفة متعددة الأبعاد ثابتة الحجم، لكنها غير فعّالة من جهة أخرى إذ تتطلّب تخصيص ذاكرة ديناميكيّ لكل صفّ، كما أنّها غير آمنة بسبب إمكانية تغيير حجم الصف عن غير عمد.

وعلى أي حال توجد طريقة أخرى أفضل وهي استخدام متجه واحد كتخزين للمصفوفة، وتوجيه شيفرة العميل (x,y) إلى فهرس مناسب في ذلك المتجه. انظر المثال التالي لمصفوفة متغير الحجم (ديناميكية) تستخدم std::vector للتخزين:

//--------------------------------------------- الألية:
#include // std::copy
#include // assert
#include // std::initializer_list
#include // std::vector
#include // ptrdiff_t
namespace my
{
   using Size = ptrdiff_t;
   using std::initializer_list;
   using std::vector;
   template <class Item>
   class Matrix
   {
   private:
       vector items_;
       Size n_cols_;
       auto index_for(Size const x, Size const y) const
           -> Size
       {
           return y * n_cols_ + x;
       }

   public:
       auto n_rows() const -> Size { return items_.size() / n_cols_; }
       auto n_cols() const -> Size { return n_cols_; }
       auto item(Size const x, Size const y)
           -> Item &
       {
           return items_[index_for(x, y)];
       }
       auto item(Size const x, Size const y) const
           -> Item const &
       {
           return items_[index_for(x, y)];
       }
       Matrix() : n_cols_(0) {}
       Matrix(Size const n_cols, Size const n_rows)
           : items_(n_cols * n_rows), n_cols_(n_cols)
       {
       }
       Matrix(initializer_list<initializer_list> const &values)
           : items_(), n_cols_(values.size() == 0 ? 0 : values.begin()->size())
       {
           for (auto const &row : values)
           {
               assert(Size(row.size()) == n_cols_);
               items_.insert(items_.end(), row.begin(), row.end());
           }
       }
   };
} // namespace my
//--------------------------------------------- الاستخدام:
using my::Matrix;
auto some_matrix()
   -> Matrix
{
   return {
       {1, 2, 3, 4, 5, 6, 7},
       {8, 9, 10, 11, 12, 13, 14},
       {15, 16, 17, 18, 19, 20, 21}};
}

#include
#include
using namespace std;
auto main() -> int
{
   Matrix const m = some_matrix();
   assert(m.n_cols() == 7);
   assert(m.n_rows() == 3);
   for (int y = 0, y_end = m.n_rows(); y < y_end; ++y)
   {
       for (int x = 0, x_end = m.n_cols(); x < x_end; ++x)
       {
           cout <← Note : not `m[y][x]`!
       }
       cout <
   }
}

يكون الخرج:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 

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

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 8: Arrays من كتاب C++ Notes for Professionals


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...