دسترسی به داده‌های ترازنشده (Unaligned) و مدیریت ساختارها در C - قسمت نوزدهم آموزش برنامه نویسی C

blog
زهرا سورانی
۱۴۰۳-۱۲-۲۲
8 دقیقه

به طور پیش‌فرض، کامپایلر برای بهینه‌سازی دسترسی به حافظه، اعضای یک ساختار را تراز (aligned) می‌کند. این کار باعث افزایش کارایی پردازنده می‌شود. اما گاهی سخت‌افزارها محدودیت‌های خاص خود را دارند و ممکن است نیاز باشد که ساختار داده‌ها بدون پدینگ (padding) ذخیره شود. برای این کار از __attribute__((packed)) استفاده می‌کنیم.

ممکن است این سؤال پیش بیاید که چرا از ساختار فشرده استفاده نمی‌کنیم، درحالی‌که فضای کمتری اشغال می‌کند؟ پاسخ ساده است:
اگر یک مقدار ۳۲ بیتی را از یک ساختار فشرده (packed struct) بخوانیم، پردازنده ممکن است مجبور شود چندین دستورالعمل اضافه اجرا کند. در مقابل، ساختار غیرفشرده حافظه بیشتری مصرف می‌کند، اما دسترسی به داده‌های آن سریع‌تر و کارآمدتر است.

برای درک بهتر اینکه چرا کامپایلر این تنظیمات را انجام می‌دهد، برنامه‌ای می‌نویسیم که دسترسی به داده‌های ۳۲ بیتی را در دو حالت تراز شده (aligned) و تراز نشده (unaligned) مقایسه کند.

برنامه زیر نحوه دسترسی به ساختارهای فشرده (packed) و غیر‌فشرده (unpacked) را نشان می‌دهد:

اول بیایید به کد ایجاد شده برای گرفتن یک uint32_t تراز‌، نگاه کنیم (1): (توضیحات اضافه شده است)

این کد از یک دستورالعمل برای گرفتن آدرس ساختار و یک دستورالعمل برای گرفتن مقدار استفاده می‌کند.

حالا بیایید به گرفتنِ بدون تراز نگاه کنیم(2):

گرفتن یک عدد ۳۲ بیتیِ بدون تراز، به‌مراتب پیچیده‌تر از حالت ترازبندی شده است. در حالت ترازبندی شده، تنها یک دستورالعمل برای به‌دست‌آوردن مقدار موردنظر کافی است. اما در حالت بدون تراز، برنامه مجبور است از چهار دستورالعمل استفاده کند!

دلیل این موضوع چیست؟

چون داده‌های ما به‌جای اینکه به‌صورت یک بلوک منظم در حافظه ذخیره شوند، به دو بخش تقسیم شده‌اند. برای خواندن این مقدار، پردازنده باید دو بار اطلاعات را از حافظه بارگذاری کند (load) و سپس آن‌ها را ترکیب کند تا مقدار صحیح را به دست آورد:

  • دستورالعمل اول، نیمه بالایی عدد را می‌گیرد.
  • دستورالعمل دوم، نیمه پایینی عدد را می‌گیرد. (دقت کنید که در برخی سیستم‌ها، بایت‌های کم‌اهمیت در ابتدای حافظه قرار می‌گیرند.)

بعد از اینکه هر دو نیمه را گرفتیم، باید آن‌ها را با هم ترکیب کنیم. برای این کار، برنامه از یک دستورالعمل شیفت (shift) استفاده می‌کند تا نیمه بالایی را به جایگاه صحیح خودش در رجیستر منتقل کند. در نهایت، با یک دستورالعمل OR منطقی، این دو نیمه را با هم ادغام می‌کند.

این فرایند اضافی، هر بار که می‌خواهیم یک عدد ۳۲ بیتی بدون تراز را بخوانیم یا ذخیره کنیم، تکرار می‌شود. به همین دلیل، کامپایلرها ترجیح می‌دهند با اضافه‌کردن پرکننده (padding) ساختار داده‌ها را ترازبندی کنند. این کار باعث می‌شود تا به‌سادگی و با یک دستورالعمل به تمام اطلاعات دسترسی داشته باشیم.

مقداردهی اولیه ساختار (Structure Initialization)

ما می‌توانیم ساختارها را با قراردادن لیست مقداردهی اولیه داخل آکولاد { } راه‌اندازی کنیم. به‌عنوان‌مثال، عبارت زیر myHouse را با یک عبارت، هم‌زمان تعریف و مقداردهی می‌کند:

در نسخه‌های ابتدایی زبان C، روشِ قراردادن لیست مقداردهی اولیه داخل آکولاد { }، تنها راه برای راه‌اندازی ساختارها بود. اما بعدها، با نهایی‌شدن استاندارد C99 در سال ۱۹۹۹، قابلیت جدیدی به نام مقداردهی اولیه‌ی تعیین‌شده (designated initializer) معرفی شد. این قابلیت به شما امکان می‌دهد که فیلدهای یک ساختار را به‌صورت جداگانه و با ذکر نامشان مقداردهی اولیه کنید. در ادامه مثالی از این روش را مشاهده می‌کنیم:

فیلدها باید به همان ترتیبی که در ساختار تعریف شده‌اند، مقداردهی شوند.

کامپایلر GCC یک قابلیت اضافی دارد که به شما امکان می‌دهد مقداردهی اولیه‌ی تعیین‌شده را با روش دیگری استفاده کنید:

در این حالت، ترتیب مقداردهی می‌تواند با ترتیب تعریف فیلدها در ساختار متفاوت باشد.

انتساب ساختار (Structure Assignment)

در زبان C، برخلاف انتساب آرایه به آرایه دیگر، انتساب یک ساختار به ساختار دیگر مجاز است. در ادامه مثالی از این موضوع را مشاهده می‌کنیم:

در مثال بالا، اگر struct1 و struct2 آرایه بودند، انتساب struct1 = struct2 غیرمجاز می‌بود. اما ازآنجایی‌که آن‌ها ساختار هستند، این انتساب کاملاً مجاز و صحیح است.

⚠ توجه

در انتساب ساختار، فقط مقادیر فیلدهای ساختار مبدأ به ساختار مقصد کپی می‌شوند. آدرس حافظه‌ی ساختارها تغییر نمی‌کند.

اشاره‌گرهای ساختار (Structure Pointers)

یکی از روش‌های ارسال آرگومان در زبان C، فراخوانی بر اساس مقدار (call by value) است. یعنی زمانی که پارامتری را به یک تابع می‌دهیم، مقدار پارامتر را در ورودی قرار دهیم با این کار یک کپی از مقدار داده ورودی ما روی استک (stack) کپی می‌شود. این کار برای داده‌های کوچک مانند اعداد صحیح ۲ بایتی مشکلی ایجاد نمی‌کند، اما اکثر ساختارها کوچک نیستند و در واقع می‌توانند بسیار حجیم باشند. زمانی که یک ساختار به‌عنوان پارامتر فرستاده می‌شود، کل ساختار روی استک کپی می‌شود و این عملیاتی پرهزینه است. در اینجا مثالی آورده شده است:

در اینجا، برای انجامِ «call by value»، کامپایلر باید کل ساختار aRectangle  را روی استک کپی کند. برای ساختارهای بزرگ‌تر، این کار می‌تواند از فضای زیادی روی استک استفاده کرده و زمان زیادی را صرف کپی‌کردن داده‌ها کند.

برای صرفه‌جویی در منابع، بهتر است ساختار را با استفاده از یک اشاره‌گر (pointer) پاس بدهیم:

در این حالت، تنها اشاره‌گر (که حجم کمی دارد) به‌عنوان پارامتر فرستاده می‌شود. در کامپایلر ARM، این کار با قراردادن اشاره‌گر در یک رجیستر انجام می‌شود. این روش سریع، ساده و بدون استفاده از فضای استک است.

یکی از مزایای «call by value» این است که هر تغییری در پارامتر به فراخواننده (caller) منتقل نمی‌شود. اما در مثال بالا، ما هیچ تغییری در ساختار ایجاد نکردیم، پس این موضوع مشکلی ایجاد نمی‌کند.

هنگامی که پارامترها را به‌صورت اشاره‌گر پاس می‌دهیم و در عین حال از تغییر ناخواسته مقادیر آن جلوگیری کنیم، از کلیدواژه‌ی const برای مشخص‌کردن عدم مجاز بودنِ تغییر در پارامترها استفاده می‌کنیم.

دسترسی به اعضای یک ساختار از طریق اشاره گر:

روش معمول

(*rectangle).height

روش ساده تر با استفاده از عملگر

<-

در زبان C، آرایه ها و اشاره گرها به شدت به مرتبط هستند، در واقع C می‌تواند یک آرایه را مثل یک اشاره‌گر در نظر بگیرد و بالعکس.

 

وقتی یک آرایه را به‌عنوان پارامتر به یک تابع ارسال می‌کنیم، C به‌طور خودکار آن را به یک اشاره‌گر تبدیل می‌کند. به‌عبارت دیگر، آرایه در هنگام ارسال به تابع، نامش به آدرس اولین عنصر آن تبدیل می‌شود.

✅ وقتی یک آرایه به‌عنوان پارامتر یک تابع تعریف می‌شود، C پشت پرده آن را به یک اشاره‌گر تبدیل می‌کند. این یعنی تابع، به جای دریافت کل آرایه، تنها آدرس اولین عنصر آن را دریافت می‌کند.

✅ معمولاً گفته می‌شود که C از فراخوانی با مقدار (Call by Value) استفاده می‌کند، اما این بیان کاملاً دقیق نیست. توضیح درست‌تر این است که:
🔹 C از Call by Value استفاده می‌کند، به‌جز در مورد آرایه‌ها، که در واقع با مقدار اشاره‌گر (Pointer Value) ارسال می‌شوند.

نام‌گذاری ساختار در C

  • در زبان C، نام‌گذاری ساختارها (structs) کمی پیچیده است، زیرا در یک تعریف ساختار می‌توانیم:
    یک نام برای ساختار مشخص کنیم (یا نکنیم).
      یک متغیر از نوع ساختار تعریف کنیم (یا نکنیم).

برای درک بهتر، بیایید به سراغ ترکیب کلی برای تعریف ساختار برویم:

۱. ساختار بدون نام (Anonymous Struct)

در این روش، یک متغیر تعریف می‌شود اما ساختار، نامی ندارد:

این کد متغیری به نام aBox تعریف می‌کند، اما نوعِ آن چیست؟

نوع aBox یک ساختار بدون نام یا یک ساختار ناشناس (anonymous structure) است. زمانی که ساختار بدون نام تعریف می‌شود، در آن لحظه هم متغیر و هم سا‌ختار هم‌زمان ایجاد می‌شوند و ما نمیتوانیم یک بعدا در جای دیگری به ان ارجاع دهیم و و متغیرهای جدید از همان نوع ساختار بسازیم.

۲. تعریف یک ساختار با نام، اما بدون متغیر

این کد یک نوع ساختار به نام box تعریف می‌کند؛ اما هیچ متغیری نمی‌سازد. از این نوع ساختار می‌توانیم بعداً برای تعریف متغیر استفاده کنیم:

۳. تعریف همزمان ساختار و متغیر

همچنین می‌توانیم هم‌نام ساختار و هم‌نام متغیر را در یک اعلان داشته باشیم:

این کد هم یک ساختار به نام box  و هم یک متغیر به نام aBox تعریف می‌کند.

۴. ساختار بدون نام و بدون متغیر (بی‌فایده!)

C یک ترفند دیگر هم در آستین دارد؛ ما می‌توانیم تعریفی برای ساختار داشته باشیم که نه نام ساختار داشته باشد و نه نام متغیر:

از آنجایی که نامی برای ساختار تعریف نشده است، تنها می‌توانیم از این تعریف برای دسترسی به متغیری که در اینجا تعریف شده استفاده کنیم. البته هیچ متغیری در اینجا تعریف نشده، پس به هیچ‌چیزی نمی‌توانیم دسترسی داشته باشیم. در نتیجه، با اینکه این تعریف از نظر فنی مجاز است، اما کاملاً بی‌فایده است.

اتحاد (Union) در C

اتحاد (Union) در زبان C شباهت زیادی به ساختار (Struct) دارد، با این تفاوت که همه فیلدهای آن در یک مکان مشترک از حافظه ذخیره می‌شوند. به عبارت دیگر، همه اعضای یک اتحاد از همان فضای حافظه استفاده می‌کنند و مقداردهی به یک عضو، مقدار اعضای دیگر را بازنویسی می‌کند.

تعریف یک اتحاد

در مثال زیر، یک اتحاد (Union) تعریف شده است:

در اینجا کامپایلر ۴ بایت به value اختصاص می‌دهد، که می‌توان آن را هم به‌عنوان uint32_t و هم به‌عنوان float استفاده کرد.

 بیایید ببینیم این موضوع در عمل چگونه کار می‌کند:

دومین دستور انتساب، در واقع anInteger را به معادل هگزادسیِمال 0x3f800000 تغییر می‌دهد. این عدد صحیح به نظر بسیار عجیب می‌آید، اما اگر آن را به عنوان یک عدد ممیز شناور در نظر بگیریم، مقدار 1.0 را نشان می‌دهد.

بهترین روش هنگام کار با Union این است که مقدار را با همان فیلدی که مقداردهی شده، بازیابی کنیم. این کار به خوانایی و وضوح کد شما می‌افزاید و از بروز خطاهای احتمالی جلوگیری می‌کند. به‌عنوان‌مثال:

هنگامی که از فیلدهای مختلف استفاده می‌کنید، نتیجه وابسته به معماری پردازنده خواهد بود:

در این حالت، مقدار someInt بستگی به موارد زیر دارد:

  • اندازه عدد صحیح (int)
  • فرمت ممیز شناور (Floating Point Format)
  • ترتیب بایت‌ها (Endianness)
  • معماری پردازنده

مسئله‌ی ترتیب بایت‌ها (Endianness)

فرض کنید ۴ کارت شماره‌دار (۱، ۲، ۳ و ۴) دارید و آن‌ها را در ۴ محفظه قرار می‌دهید.

  • در یک روش، کارت‌ها از چپ به راست ذخیره می‌شوند:
1234

 

  • در روش دیگر، کارت‌ها از راست به چپ ذخیره می‌شوند:
4321

 

حالا اگر دو کارت اول را بردارید، در روش اول ۱ و ۲ و در روش دوم ۳ و ۴ دریافت می‌کنید!

همین اتفاق هنگام ذخیره داده‌ها در پردازنده‌های مختلف رخ می‌دهد. برخی پردازنده‌ها از Little Endian و برخی از Big Endian استفاده می‌کنند.

نتیجه‌گیری

اگر مقداری را درtheValue.anInteger قرار دهید، باید آن را فقط با theValue.anInteger بخوانید. خواندن مقدار با یک فیلد دیگر (مثلاً theValue.aFloat) در معماری‌های مختلف، نتایج متفاوتی خواهد داشت و ممکن است رفتار غیرمنتظره‌ای ایجاد کند.

اطلاعات
0
0
لینک و اشتراک
profile

Alireza Abbasi

متخصص الکترونیک

مقالات بیشتر
slide

پالت | بازار خرید و فروش قطعات الکترونیک

قطعات اضافه و بدون استفاده همیشه یکی از سرباره‌‌های شرکتها و طراحان حوزه برق و الکترونیک بوده و هست. پالت سامانه‌ای است که بصورت تخصصی اجازه خرید و فروش قطعات مازاد الکترونیک را فراهم می‌کند. فروش در پالت
family

آیسی | موتور جستجوی قطعات الکترونیک

سامانه آی سی سیسوگ (Isee) قابلیتی جدید و کاربردی از سیسوگ است. در این سامانه سعی شده است که جستجو، انتخاب و خرید مناسب تر قطعات برای کاربران تسهیل شود. وقتی شما در این سامانه، قطعه الکترونیکی را جستجو می‌کنید؛ آی سی به سرعت نتایج جستجوی شما در اکثر فروشگاه‌های آنلاین در حوزه قطعات الکترونیک را نمایش می‌دهد. جستجو در آیسی
family

سیسوگ‌شاپ | فروشگاه محصولات Quectel

فروشگاه سیسوگ مجموعه ای متمرکز بر تکنولوژی های مبتنی بر IOT و ماژول های M2M نظیر GSM، GPS، LTE، NB-IOT، WiFi، BT و ... جایی که با تعامل فنی و سازنده، بهترین راهکارها انتخاب می شوند. برو به فروشگاه سیسوگ
family

سیسوگ فروم | محلی برای پاسخ پرسش‌های شما

دغدغه همیشگی فعالان تخصصی هر حوزه وجود بستری برای گفتگو و پرسش و پاسخ است. سیسوگ فروم یک انجمن آنلاین است که بصورت تخصصی امکان بحث، گفتگو و پرسش و پاسخ در حوزه الکترونیک را فراهم می‌کند. پرسش در سیسوگ فرم
family

سیکار | اولین مرجع متن باز ECU در ایران

بررسی و ارائه اطلاعات مربوط به ECU (واحد کنترل الکترونیکی) و نرم‌افزارهای متن باز مرتبط با آن برو به سیکار
become a writer

نویسنده شو !

سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.

ارسال مقاله
become a writer

نویسنده شو !

سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.

ارسال مقاله
خانواده سیسوگ

سیسوگ‌شاپ

فروشگاه محصولات Quectel

پالت

سیسوگ فروم

محلی برای پاسخ پرسش‌های شما

سیسوگ جابز
سیسوگ
سیسوگ فروم

سی‌کار

اولین مرجع متن باز ECU در ایران

سیسوگ مگ

آی‌سی

موتور جستجوی قطعات الکترونیکی

سیسوگ آکادمی

پالت

بازار خرید و فروش قطعات الکترونیک

دیدگاه ها

become a writer

نویسنده شو !

سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.

ارسال مقاله
become a writer

نویسنده شو !

سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.

ارسال مقاله