بررسی ترکیب 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ها هم برقراره.

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