بررسی ترکیب struct
و typedef
در زبان C
یکی از اون الگوهای رایجی که خیلی جاها میبینیم، ترکیب کردن تعریف یه struct
با یه اعلان typedef
هست؛ اما واقعاً معنی و مفهوم این تعریفها چیه؟ چرا ممکنه که بخوایم تعریف یه struct
رو با یه typedef
همراه کنیم؟ در این مقاله تلاش میکنیم تا إنشاءالله به این سؤالات پاسخ بدیم.
اگه به سورسکدهای مختلف یه نگاهی بندازید، با احتمال خیلی زیادی با یه الگویی شبیه ساختار زیر مواجه میشید:
typedef struct
{
/* Struct Members ... */
} Sample_Type;
باز هم الگوهای تکراری. همونطوری که داخل بررسی #include guard هم گفتیم، استفاده از ساختارها و الگوهای تکراری داخل برنامهها، نرمافزارها، و libararyهای مختلف، معمولاً برای حل مشکلات متداول و رایج هست. حالا باید دید که چه مشکلی وجود داشته که خواستن با استفاده از این ساختار، اون مشکل رو حل کنن. آقا اصلاً خود این ترکیب، ممکنه که اولش یهکمی مبهم بهنظر برسه و معنی و مفهومش برامون معلوم نباشه. پس وقت اون رسیده که برای حل این سؤالات و ابهامات، بررسی بیشتری انجام بدیم.
تعریف و استفاده از struct
توی حالت کلی، نحوهی تعریف یه struct
میتونه به این شکل باشه:
struct Sample
{
/* Struct Members ... */
};
و اگه بخوایم که یه متغیر از نوع اون struct
تعریف کنیم، میشه به این صورت عمل میکنیم:
struct Sample my_var;
حالا تعریف بالا رو میذاریم کنار تعریف یه متغیر ساده از نوع int
:
struct Sample my_var;
int length;
در کد بالا، وقتی خواستهایم نوع دادهی متغیر length
رو مشخص کنیم، اسم نوع داده رو (یعنی int
) پشت سر اسم متغیر (یعنی length
) نوشتهایم. اگر تعریف my_var
رو با تعریف length
مقایسه کنیم، میبینیم که انگار، اسم نوع دادهی ما struct Sample
هست و نه Sample
خالی و بهنوعی میشه گفت که اسم Sample
(یا حالا هر اسم دیگهای که انتخاب کرده باشیم) بدون ذکر کلمهی struct
، معنی خاصی برای زبان C نمیده.
پس توجه میکنید که اگر بخوایم به هر نحوی از این نوع دادهی جدیدی که تعریف کردهایم استفاده کنیم، باید علاوه بر نوشتن اسمش (مثلاً همین Sample
)، خودِ کلمهی struct
رو هم هر بار تکرار کنیم. نمیشه نوشت Sample
خالی، بلکه باید بنویسیم struct Sample
. پس همینجا یه نتیجهگیری انجام میدیم:
در زبان C، اگر بخوایم که با نوع دادهی تعریفشده توسط یه struct
کار کنیم، باید اسمش رو به همراه کلمهی کلیدی struct
ذکر کنیم تا معنیدار باشه.
و افرادی هستن که فکر میکنن این تکرار کلمهی کلیدی struct
، خیلی بد و بیخوده.
از نظر بعضیها، مشکل همینجاست!
بعضیها به نظرشون میاد که این شیوهی زبان C برای استفاده از نوع دادهی تعریفشده توسط یه struct
، روش جالبی نیست؛ اونها میخوان که با struct
هاشون هم مثل سایر انواع دادهای برخورد کنن و مثلاً بتونن یه متغیر از نوع یه struct
رو هم بهسادگی یه متغیر از نوع int
تعریف کنن. در واقع، اونها تمایل دارن که برای هر بار ارجاع به نوع دادهی جدیدی که تعریف کردهاند، نیازی نباشه که کلمهی struct
رو هم ذکر کنن. بهنظر اونها، خوانایی و نگهداری چنین کدی هم راحتتر میشه. یه راه برای رسیدن به این هدف و ساده کردن استفاده از struct
ها، همون ساختار ترکیبی struct
و typedef
هست که در ابتدای مقاله معرفی کردیم.
کلاً این مسألهی تکرار کلمهی کلیدی struct
از نظر یک سری از افراد، یه مشکله که باید حل بشه و برای همین هم براش راهحل ارائه دادهاند. در مقابل، بعضی دیگه از افراد هم بهنظرشون میرسه که این اصلاً ایرادی نداره و اتفاقاً خیلی هم خوبه که کلمهی struct
هی تکرار بشه… حالا هدف ما این نیست که دربارهی ایراد داشتن و نداشتن این مورد، قضاوتی بکنیم. قضاوت با تکتک توسعهدهندههاست که ببینن چطوری میخوان از struct
هاشون استفاده بکنن یا قراردادها و عُرف (convention) پروژه و سازمانشون چطوری هست. اما فارغ از اینکه ما این مسأله رو یه مشکل بدونیم یا نه، از اونجایی که خیلی با اون ساختار ترکیبی مواجه میشیم، باید حداقل متوجه مفهومش بشیم و ببینیم که دقیقاً چطوری عمل میکنه. برای این منظور، لازمه که یهکمی بیشتر دربارهی تعریف و استفاده از struct
و typedef
بدونیم.
یهکمی بیشتر دربارهی تعریف و استفاده از struct
اول از همه بگیم که این امکان وجود داره که همزمان با تعریف ساختار یه struct
، متغیرهایی هم از اون نوع تعریف کنیم؛ مثلاً در کد زیر، همزمان با تعریف خود struct Sample
، متغیر my_var
هم از نوع اون struct
ساخته شده:
struct Sample
{
/* Struct Members ... */
} my_var;
باید بدونیم که کلاً گذاشتن نام برای struct
اجباری نیست و میشه که یه struct
بدون نام تعریف کرد:
struct
{
/* Struct Members ... */
};
البته توی پرانتز بگیم که اگر متغیری از این نوع دادهی بدون نام ساخته نشه، بعیده که این نوع دادهی جدید ما، به همین شکلی که تعریف شده، بتونه کاربردی داشته باشه. کامپایل کردن یه همچین تعریفی، با یه warning منطقی و متین از طرف کامپایلر مواجه میشه که به ما یادآوری میکنه که هیچ متغیری از نوع دادهای بدون نام خودمون نساختهایم:
warning: unnamed struct/union that defines no instances
اگر بخوایم که از این نوع دادهای جدید، یک یا چندتا متغیر هم بسازیم، باید این کار رو همون جا در زمان تعریف نوع دادهای انجام بدیم؛ مثلاً در کد زیر، my_var
یه متغیر از نوع اون struct
بدون نام هست:
struct
{
/* Struct Members ... */
} my_var;
حالا یه نتیجهی دیگه هم میگیریم:
امکان تعریف struct
بدون نام در زبان C، وجود داره.
این نکات رو دربارهی استفاده از struct
داشته باشید، تا بریم یه بررسی روی typedef
هم انجام بدیم.
استفاده از typedef
به زبان ساده، typedef
میاد برای یه نوع دادهای، یه اسم جدید معرفی میکنه. همین. تمام! اسم جدید و اسم قبلی، مترادف هم هستن و با هم فرقی ندارن. مثلاً کد زیر رو ببینید:
typedef float temperature;
temperature t1;
float t2;
در این کد، ما اومدهایم به کمک خط اول، یه کلمهی مترادف و هممعنی برای نوع دادهی float
اعلام کردهایم و از این به بعد، هرجایی که دلمون خواست، میتونیم بهجای float
بنویسیم temperature
. در خط دوم، ما یه متغیر از نوع temperature
تعریف کردهایم و این کار رو دقیقاً به همون شکلی انجام دادهایم که در سطر سوم، متغیری از نوع float
تعریف شده. و حقیقت اینه که متغیرهای t1
و t2
، همنوع هستن و temperature
صرفاً یه اسم دیگه برای float
هست.
استفاده از typedef
ممکنه که بتونه خوانایی کد رو افزایش بده و نگهداری از اون رو راحتتر کنه (البته این مطلب، میتونه خیلی سلیقهای باشه و ممکنه که گاهی بهنظر برسه که typedef
، خوانایی رو کاهش داده یا نگهداری رو سختتر کرده). مثلاً در کد بالا، ما از تعریف متغیر t1
متوجه میشیم که این متغیر برای نگهداری دما استفاده میشه (چون کلمهی «temperature» بهمعنی «دما» است)، اما تعریف متغیر t2
، صرفاً نشون میده که این متغیر برای نگهداری یه مقدار اعشاری بهکار میره. نوع دادهای که ما داخل این دوتا متغیر میتونیم نگه داریم همون float
هست، ولی کاربرد t1
و t2
شاید برای ما متفاوت باشه. این مثال استفاده از typedef
، ممکنه که خوانایی کد رو افزایش داده باشه و نگهداری از اون رو آسونتر کرده باشه.
پس الآن هم یه نتیجهگیری دیگه میکنیم:
در زبان C، با استفاده از typedef
، میتونیم که یک نام جدید برای یه نوع داده معرفی کنیم. بعد از این کار، نام جدید و نام قبلی مترادف هم میشن و هرجایی که بشه از نام قبلی استفاده کرد، استفاده از نام جدید هم ممکن و معتبر هست.
ترکیب struct
و typedef
قبلاً بیان کردیم که یکی از راهحلهایی که برای سادهسازی استفاده از struct
ها در C پیشنهاد میشه، ترکیب struct
و typedef
هست؛ گفتیم هدف این بوده که نیاز به نوشتن چند بارهی کلمهی struct
وجود نداشته باشه. بعد از گفتن این همه مقدمات، الآن دیگه میدونیم که برای رسیدن به این هدف، کافیه که با استفاده از typedef
، یه اسم جدید برای نوع دادهی struct
خودمون معرفی کنیم. کد زیر رو در نظر بگیرید:
struct Sample
{
/* Struct Members ... */
};
typedef struct Sample Sample_Type;
ابتدا یه struct
به اسم Sample
تعریف شده و بعدش با استفاده از typedef
، یه اسم دیگه برای این نوع دادهی جدید معرفی کردهایم. نوع دادهی ما struct Sample
هست ولی ما گفتهایم که از این به بعد، میخوایم به جای نوشتن struct Sample
، بنویسیم Sample_Type
. حالا این قابلیت رو داریم که برای ارجاع به نوع دادهی struct Sample
، از کلمهی Sample_Type
استفاده کنیم. الآن میشه که یه متغیر s1
رو به صورت زیر تعریف کنیم:
Sample_Type s1;
بهسادگیِ همون زمانی که میخوایم یه متغیر از نوع int
بسازیم. البته هنوز هم میتونیم که متغیر s2
رو به این شکل تعریف کنیم:
struct Sample s2;
و باید بدونیم که s1
و s2
، درنهایت یک نوع دادهای دارن و اون هم struct Sample
هست.
حالا یه کار دیگه هم میشه کرد: میتونیم تعریف struct Sample
رو بَرِش داریم و با اعلان typedef
ادغام کنیم! پس اگر کل بلاک تعریف struct Sample
رو داخل خط اعلان typedef
جایگذاری کنیم، کد زیر بهدست میاد:
typedef struct Sample
{
/* Struct Members ... */
} Sample_Type;
پس با یه حرکت، هم struct Sample
رو تعریف کردهایم و هم با typedef
، یه اسم جدید براش معرفی شده تا نیازی نباشه که برای استفاده ازش، کلمهی کلیدی struct
رو تکرار کنیم.
الگوی کد بالا، در حقیقت، همون حالت کلی الگوی ترکیبی struct
و typedef
اول این مقاله هست که البته در نمونهی بالا، خود struct
هم اسم داره؛ اما اگه قرارمون بر این هست که همیشه از Sample_Type
بهجای struct Sample
استفاده کنیم، پس اسم struct
(یعنی Sample
) اصلاً کاربردی برامون نداره و ممکنه دلمون بخواد که کلاً حذف بشه. پس در واقع میشه بیایم یه struct
بدون نام تعریف بکنیم و همون موقع، با استفاده از typedef
یه اسم براش معرفی کنیم:
typedef struct
{
/* Struct Members ... */
} Sample_Type;
این چیزی که داخل کد بالا نوشته شده، ترکیبی از تعریف یه struct
بینام بههمراه یه typedef
هست و این یکی نمونه دیگه دقیقاً همون الگوییه که در ابتدای این مقاله نشون دادیم. اگر کمی شکل و شمایل کد بالا رو عوض کنیم و همهی کد رو توی یه خط بنویسیم، چیزی شبیه کد زیر بهدست میاد:
typedef struct { /* Struct Members ... */ } Sample_Type;
میبینید که این یه typedef
معمولی هست و میاد برای نوع دادهی بدون نام struct { /* Struct Members ... */ }
، یه اسمی تحت عنوان Sample_Type
معرفی میکنه. این مفهوم در کد زیر نشون داده شده:
typedef struct { /* Struct Members ... */ } Sample_Type ;
// typedef <------------DATA TYPE------------> <-NEWNAME-> ;
در کامنت کد بالا، از DATA TYPE برای نشون دادن مکان نوع دادهای و از NEWNAME برای نشون دادن مکان اسم جدید در خط typedef
استفاده شده. الان دیگه میتونیم که از Sample_Type
برای ارجاع به نوع دادهی struct
بینام خودمون استفاده کنیم و دیگه نیازی به نامگذاری خود struct
و تکرار کلمهی کلیدی struct
هم نداریم.
نکات
اول- توجه داشته باشید که نامگذاریهایی که داخل این مقاله انجام دادیم، قانون خاصی در زبان C نداره و همین که از کاراکترهای مجاز برای نامگذاری استفاده بشه و از کلمههای رزروشده هم استفاده نشه، کافی هست؛ پس این اسمهای Sample
و Sample_Type
، اسامی کاملاً دلخواهی هستن که صرفاً برای مثال آورده شدهاند و هر اسم مجاز دیگهای با هر الگوی نامگذاری که شما صلاح بدونید، میشه که جای اونها رو بگیره. جالبه که حتی اسم struct
و نام معرفی شده توسط typedef
، میتونن یکسان باشن! [مرجع: صفحهی 175 کتاب 21st Century C، نوشتهی Ben Klemens، انتشارات O'Reilly؛ به نقل از صفحهی 213 ویرایش دوم کتاب K&R و بخش 6.2.3 استاندارد C99 و C11]
دوم- همهی این قصههایی که گفتیم، دربارهی union
هم برقراره و شما ممکنه که ترکیب تعریف union
رو همراه با اعلان typedef
ببینید و بهکار ببرید.
جمعبندی
برای استفاده از struct
در زبان C، لازمه که کلمهی struct
رو بههمراه اسم نوع دادهمون تکرار کنیم و این مورد، ممکنه که یه ایراد تلقی بشه. اگر این مسأله رو یه ایراد بدونیم، میتونیم که برای حل اون، همزمان با تعریف struct
خودمون از یه typedef
هم استفاده بکنیم و برای کل این نوع دادهی تعریفشده، یه اسم جدید معرفی کنیم. بعدش میشه که از اون اسم جدید، بهتنهایی استفاده کرد و دیگه نیازی به تکرار کلمهی کلیدی struct
نخواهد بود. همین نکات و موارد، برای union
ها هم برقراره.
حتی اگر خودمون هم از این الگو استفاده نکنیم، اما خیلی زیاد با اون مواجه میشیم. پس خوبه که دیگه معنی و مفهومش رو میدونیم و دلیل استفاده ازش هم برامون روشن شده.