خوش آموز درخت تو گر بار دانش بگیرد، به زیر آوری چرخ نیلوفری را


آموزش زبان ++C : گارد هدر (Header guards)

آموزش زبان ++C : گارد هدر (Header guards)
نویسنده : امیر انصاری
مشکل تعریف تکراری (duplicate definition)

نرم افزار سامانه مودیان راهکار



در آموزش مربوط به اعلامیه های پیشاپیش (Forward declarations) و تعاریف (definitions)، اشاره کردیم که هر شناسه (identifier) تنها می تواند یک تعریف (definition) داشته باشد. بنابراین، اگر در یک برنامه شناسه ای از نوع متغیر بیش از یکبار معرفی گردد، منجر به ایجاد خطای زمان کامپایل می شود :

int main()
{
int x; // this is a definition for identifier x
int x; // compile error: duplicate definition

return 0;
}

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

#include "iostream"

int foo()
{
return 5;
}

int foo() // compile error: duplicate definition
{
return 5;
}

int main()
{
std::cout << foo();
return 0;
}

اگر چه، رفع خطاهای موجود در این برنامه ها ساده می باشد و کافی است تا تعریف تکراری (duplicate definition) را حذف کنیم، اما در هنگام استفاده از فایل های هدر (header files)، پیش آمدن وضعیتی که در آن با تعریف تکراری (duplicate definition) برخورد کنید محتمل است، و یا اینکه احتمال اینکه یک فایل هدر بیش از یکبار include شود نیز بسیار محتمل است. این مساله معمولا در مواقعی که یک فایل هدر خودش شامل فایل های هدر دیگری باشد شایع است.

مثال زیر را در نظر بگیرید :

math.h

int getSquareSides()
{
return 4;
}

geometry.h

#include "math.h"

main.cpp

#include "math.h"
#include "geometry.h"

int main()
{
return 0;
}

این برنامه به ظاهر بی گناه و معصوم، به درستی کامپایل نخواهد شد! دلیل این مساله اینست که فایل math.h دارای یک تعریف (definition) می باشد. آنچیزی که در واقعیت اتفاق می افتد اینست. ابتدا در فایل main.cpp فایل math.h با دستور includes کپی می گردد، و منجر می شود تا تابع getSquareSides درون فایل main.cpp کپی گردد. همینطور بعد از آن فایل geometry.h نیز داخل فایل main.cpp کپی می شود، خود فایل geometry.h شامل یک کپی از فایل math.h می باشد که در آنجا includes شده است. در نتیجه، تابع getSquareSides دو بار در فایل main.cpp کپی می گردد.

در نتیجه بعد از اجرای دستورات پیش پردازنده، فایل main.cpp در حافظه کامپیوتر به شکل زیر خواهد در آمد :

int getSquareSides()  // from math.h
{
return 4;
}

int getSquareSides() // from geometry.h
{
return 4;
}

int main()
{
return 0;
}

اینجاست که تلاش کامپایلر برای کامپایل کردن این برنامه به مشکل خواهد خورد و با مساله تعریف تکراری (duplicate definition) مواجه می شود. حالا با فرض اینکه مجبور باشیم این دو include را در فایل main.cpp حفظ کنیم، و همینطور مجبور باشیم تا در فایل geometry.h نیز include مربوط به math.h را حفظ کنیم، چگونه می توانیم این مشکل را حل کنیم؟

گارد هدر (Header guards)


خبر خوب اینست که، حل کردن این مشکل واقعاً ساده می باشد، مکانیزمی با نام گارد هدر (Header guards) وجود دارد - به گارد هدر include guard نیز گفته می شود - که می تواند این مشکل را حل کند. گارد هدر (Header guards) در واقع یک مکانیزم دستورات پیش پردازنده برای کامپایل شرطی (conditional compilation) می باشد که به شکل زیر می باشد :

#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE

// your declarations and definitions here

#endif

وقتی که این فایل هدر در فایل دیگری include می شود، اولین چیزی که بررسی می شود اینست که آیا شناسه SOME_UNIQUE_NAME_HERE قبلاً تعریف شده است یا نه. اگر اولین باری باشد که این فایل را include می کنیم، طبیعتاً SOME_UNIQUE_NAME_HERE تعریف نشده است. در نتیجه اگر SOME_UNIQUE_NAME_HERE را بعد از آن تعریف کنیم، از آنجا که در بار اول include شدن این شناسه ایجاد شده است، در نتیجه کل هدر ما دیگر تعریف نخواهد شد، در واقع اساساً کامپایل نخواهد شد.

همه فایل های هدر شما باید دارای گارد هدر (Header guards) باشند. شناسه SOME_UNIQUE_NAME_HERE می تواند هر نام دیگری باشد که توسط شما تعیین شده است. اما معمولاً این نام را به شکل استانداردی می سازند که این نام متشکل از نام اصلی فایل هدر و یک زیر خط و پسوند H در انتهای آن می باشد. به عنوان مثال اگر نام فایل هدر شما a.h باشد، نام این شناسه می تواند a_H باشد. با این تفاسیر به فایل math.h به شکل زیر گارد هدر (Header guards) را اضافه می کنیم :

math.h

#ifndef MATH_H
#define MATH_H

int getSquareSides()
{
return 4;
}

#endif

حتی فایل های هدر موجود در کتابخانه استاندارد ++C نیز دارای گارد هدر (Header guards) می باشند. اگر نگاهی به داخل فایل هدر iostream بیندازید، کد زیر را خواهید دید :

#ifndef _IOSTREAM_
#define _IOSTREAM_

// content here

#endif

بروز رسانی مثال قبلی ما با استفاده از گارد هدر (Header guards)


بیایید دوباره به مثال math.h بازگردیم. اما اینبار از گارد هدر (Header guards) در فایل math.h استفاده خواهیم کرد.

math.h
#ifndef MATH_H
#define MATH_H

int getSquareSides()
{
return 4;
}

#endif

geometry.h
#include "math.h"

main.cpp

3
4
5
6
7
#include "math.h"
#include "geometry.h"

int main()
{
return 0;
}

حالا با این تغییراتی که دادیم، هنگامی که در فایل main.cpp دستور مربوط به includes فایل math.h اجرا می شود، پیش پردازنده مشاهده خواهد کرد که شناسه MATH_H هنوز تعریف نشده است. در نتیجه محتویات فایل math.h درون فایل main.cpp کپی می شود و در ضمن شناسه MATH_H نیز تعریف می گردد. سپس در فایل main.cpp دستور مربوط به includes فایل geometry.h اجرا می شود که خود فایل geometry.h نیز فایل math.h را includes می کند. اینبار پیش پردازنده مشاهده می کند که شناسه MATH_H تعریف شده است، و در نتیجه محتویات فایل math.h اینبار نادیده گرفته می شوند و مجدداً کپی نمی گردند.

بنابراین، با افزودن گارد هدر (Header guards)، ما مطمئن می شویم که محتویات فایل math.h تنها یکبار include می شوند.

اگر بخواهیم این کد را بهینه تر بنویسیم، بهتر است که در فایل geometry.h نیز گارد هدر (Header guards) را بیفزاییم.

آیا نمی توانیم فقط از تعاریف در فایل های هدر چشم پوشی کنیم؟


ما بطور کلی به شما گفتیم که تعاریف (definitions) را در داخل فایل های هدر قرار ندهید. بنابراین ممکن است پیش خود فکر کنید، اگر تعاریف (definitions) در داخل فایل هدر قرار ندارند، پس چرا از گارد هدر (Header guards) برای چیزی که در هدر وجود ندارد استفاده می کنیم.

مواردی وجود دارند که در آینده مطرح خواهیم کرد و خواهید دید که در برخی مواقع مطلوب است که تعاریف (definitions) در داخل فایل هدر قرار بگیرند، برای مثال انواع معرفی شده توسط کاربر (user-defined types) مثل structs و classes . این موارد را تاکنون مطرح نکرده ایم، اما در آینده و در طی همین دوره آموزشی، این کار را خواهیم کرد. بنابراین، با وجود اینکه تا اینجای آموزش استفاده از گارد هدر (Header guards) الزامی نمی باشد، اما خوب است که این عادت را پیدا کنید که همیشه از گارد هدر (Header guards) استفاده کنید، در نتیجه، نیاز نخواهد بود تا در آینده عادت های بد را ترک کنید، که کار سخت تری هم می باشد.

گارد هدر (Header guards) در مواقعی که یک فایل هدر در داخل فایل های مختلف include شده باشد، تضمینی بر یکبار وارد شدن هدر نیست


توجه داشته باشید که هدف از گارد هدر (Header guards) اینست که در یک فایل کد به صورت تکراری بیش از یک هدر وارد نشود. ماهیت گارد هدر (Header guards) اینست که نمی تواند از وارد شدن یک فایل هدر در چندین فایل کد به صورت تکراری جلوگیری کند. این مساله می تواند منجر به مشکلات غیر منتظره ای شود. مثال زیر را در نظر بگیرید :

square.h
#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides()
{
return 4;
}

int getSquarePerimeter(int sideLength); // forward declaration for getSquarePerimeter

#endif

square.cpp
#include "square.h"  // square.h is included once here

int getSquarePerimeter(int sideLength)
{
return sideLength * getSquareSides();
}

main.cpp
#include "iostream"
#include "square.h" // square.h is also included once here

int main()
{
std::cout << "a square has " << getSquareSides() << " sides" << std::endl;
std::cout << "a square of length 5 has perimeter length " << getSquarePerimeter(5) << std::endl;

return 0;
}

توجه داشته باشید که با وجودیکه هدر square.h دارای گارد هدر (Header guards) می باشد، محتویات فایل square.h یکبار داخل فایل square.cpp نیز قرار داده شده است و همینطور در داخل فایل main.cpp نیز include شده است.

بیایید دقیقتر بررسی کنیم که چرا این اتفاق افتاده است. وقتیکه square.h در داخل square.cpp قرار داده شده است، شناسه SQUARE_H تنها تا پایان فایل square.cpp تعریف شده است. این تعریف، از قرار گرفتن به صورت تکراری فایل square.h درون square.cpp جلوگیری می کند (که هدف از گارد هدر (Header guards) نیز همین می باشد). با این حال، وقتیکه square.cpp پایان می یابد، شناسه SQUARE_H دیگر یک شناسه تعریف شده محسوب نمی شود. این موضوع بدین معنا می باشد که هنگامی که دستورات پیش پردازنده درون فایل main.cpp اجرا می شوند، شناسه SQUARE_H دیگر درون فایل main.cpp یک چیز تعریف شده نمی باشد.

نتیجه نهایی این می شود که هر دو فایل square.cpp و main.cpp یک کپی از تعریف تابع getSquareSides را دریافت می کنند. این برنامه به درستی مرحله کامپایل را پشت سر می گذارد، اما در مرحله لینکر به دلیل تکراری بودن تعریف تابع getSquareSides شکست خواهد خورد!

برای حل این مشکل، چند راه حل وجود دارد. یک راه اینست که تعریف تابع را در یکی از فایلهای cpp. قرار بدهیم، و بنابراین هدر ما تنها شامل یک اعلامیه پیشاپیش (Forward declaration) باشد :

square.h
#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides(); // forward declaration for getSquareSides
int getSquarePerimeter(int sideLength); // forward declaration for getSquarePerimeter

#endif

square.cpp
// It would be okay to #include square.h here if needed
// This program doesn't need to.

int getSquareSides() // actual definition for getSquareSides
{
return 4;
}

int getSquarePerimeter(int sideLength)
{
return sideLength * getSquareSides();
}

main.cpp
#include "iostream"
#include "square.h" // square.h is also included once here

int main()
{
std::cout << "a square has " << getSquareSides() << "sides" << std::endl;
std::cout << "a square of length 5 has perimeter length " << getSquarePerimeter(5) << std::endl;

return 0;
}

حالا تابع getSquareSides تنها یک تعریف دارد که آنهم در فایل square.cpp قرار دارد، بنابراین لینکر (linker) خوشحال و راضی خواهد بود. فایل Main.cpp قادر خواهد بود تا این تابع را فراخوانی کند (حتی با وجودیکه تعریف آن در فایل square.cpp قرار دارد)، چرا که فایل هدر square.h در آن include شده است و در ضمن در داخل فایل هدر square.h اعلامیه پیشاپیش (Forward declaration) مربوط به تابع getSquareSides موجود می باشد.

روش های دیگر حل این مشکل را در درس های آینده مرور خواهیم کرد.

pragma once#


خیلی از کامپایلرها از یک دستور پیش پردازنده (directive) ساده تر با نام pragma once# استفاده می کنند، که جایگزینی برای گارد هدر (Header guards) می باشد.

#pragma once

// your code here

دستور pragma once# دقیقاً هدف مشابهی با گارد هدر (Header guards) دارد، و از مزایای آن می توان به ساده تر بودن، کوتاهتر بودن و همینطور کمتر مستعد خطا بودن پرداخت. فایل هدر stdafx.h در ویژوال استودیو باید در پروژه شما include شود تا بتوانید از دستور pragma once# به عنوان جایگزینی برای گارد هدر (Header guards) بهره مند شوید.

با این وجود، فراموش نکنید که دستور pragma once# یک بخش رسمی در زبان ++C نمی باشد و همه کامپایلرها از آن پشتیبانی نمی کنند (البته کامپایلرهای مدرن آن را پشتیبانی می کنند).

به منظور رعایت سازگاری بیشتر در برنامه هایتان، توصیه ما اینست که به همان گارد هدر (Header guards) بچسبید.

خلاصه


گارد هدر (Header guards) طراحی شده است تا به شما این اطمینان را بدهد که محتویات یک فایل هدر، بیش از یک مرتبه در یک فایل مشخص کپی نگردند، تا از تعاریف (definitions) تکراری جلوگیری شود.

توجه داشته باشید که اعلامیه های تکراری (duplicate declarations) منجر به ایجاد مشکل نخواهند شد، اما تعاریف (definitions) تکراری هستند که برنامه را منجر به مشکل می کنند. اما حتی اگر فایل هدر شما صرفاً دارای اعلامیه ها می باشد، باز هم عادت کنید که همیشه از گارد هدر (Header guards) استفاده کنید.

توجه داشته باشید که گارد هدر (Header guards) از کپی شدن محتویات یک فایل هدر در فایلهای مختلف یک پروژه جلوگیری نمی کند و صرفاً از کپی شدن تکراری محتویات هدر، در یک فایل جلوگیری می کند. البته این مساله چیز خوب و ایده آلی نیز می باشد، چرا که ما اغلب نیاز داریم تا محتویات یک فایل هدر را در فایلهای مختلف پروژه خود مورد استفاده قرار بدهیم.


آموزش قبلی : آموزش زبان ++C : مروری بر پیش پردازنده ها (preprocessor)

آموزش بعدی : آموزش زبان ++C : چگونه اولین برنامه های خود را طراحی کنید؟



نمایش دیدگاه ها (1 دیدگاه)

دیدگاه خود را ثبت کنید:

انتخاب تصویر ویرایش حذف
توجه! حداکثر حجم مجاز برای تصویر 500 کیلوبایت می باشد.