در این نوشته قصد داریم به معرفی مفهوم ساختار یا استراکچر در سی پلاس پلاس بپردازیم. این مفهوم میتواند پل ورود به بحث شی گرایی باشد. همچون گذشته ابتدا در مورد این صحبت می‌کنیم که چه موقع به ساختار نیاز پیدا می‌کنیم؟

چرا ساختار؟

با وجود اینکه آرایه ها توانایی ما را در ذخیره اطلاعات به شدت بالا می‌برند اما یک اشکال اساسی دارند، و آن این است که باید همه عناصر آرایه از یک جنس باشند. بعضی مواقع نیاز است که شما داده ها با نوع‌های مختلف را در یک موجودیت گروه بندی کنید و با آن کار کنید. مثلا فرض کنید اطلاعات 100 دانشجو را میخواهید ذخیره کنید. این اطلاعات شامل نام (که از نوع String است) شماره دانشجویی (int) و معدل (float) است. شما با استفاده از ساختار قادر خواهید بود که نوع داده ای جدیدی تعریف کنید که در این نوع داده ای هم نام دانشجو ذخیره کنید، هم شماره دانشحویی و هم معدل. پس به زبان ساده تر میتوان اینگونه گفت که شما با استفاده از ساختار می‌توانید نوع داده ای خود (که متناسب با صورت مساله و نیاز شماست) تعریف کنید.

بدون ساختار چه مشکلاتی خواهیم داشت؟

  • فرض کنید میخواهید اطلاعات یک دانشجو را ذخیره کنید. احتمالا از این متغیرها استفاده میکنید: number برای شماره دانشجویی، name برای نام دانشجو و avg برای معدل.
  • حال فرض کنید میخواهید اطلاعات سه دانشجو را ذخیره کنید. احتمالا از این متغیرها برای شماره دانشجویی استفاده میکنید: number1, number2, number3 و به طور مشابه برای نام و معدل هم از این شیوه استفاده می‌کنید.
  • قابل تصور است که این شیوه کد نویسی منجر به یک کد بی نظم می‌شود و مهمتر از آن ارتباط بین معدل دانشجو و نام آن را باید به صورت دستی مدیریت کنید که یک کار اشتباه و سخت است.
  • شیوه اصولی برای این کار این است که داده های مرتبط به هم را یک جا و کنار هم ذخیره کنیم. به این معنی که اطلاعات هر دانشجو در کنار هم ذخیره شوند. این روش به یک کد خوانا و قابل فهم منجر می‌شود. برای پیاده سازی این شیوه به structure نیاز داریم.

با این مقدمه کوتاه Structure یا ساختار را تعریف می‌کنیم:
ساختار مجموعه ای از متغیرها تحت یک نام مشخص است. این متغیرها می‌توانند نوع داده ای متفاوتی داشته باشند که هر کدامشان با یک اسم مشخص قابل دسترس هستند.

بد نیست این نکات تکمیلی در مورد ساختارها هم بخوانید و بعد از آن وارد کدنویسی شویم:

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

تعریف structure

قالب تعریف structure به صورت زیر است:

struct structure_name
{
     type1 field1;
     type2 field2;
          …
     typen fieldn;
};

ساختار با کلمه کلیدی struct تعریف می‌شود و همانطور که در خط اول می‌بینید نام ساختار در ادامه struct نوشته می‌شود. همچنین در بدنه ساختار متغیرهای ساختار تعریف می‌شود؛ یعنی نام متغیر به همراه نوع داده ای آنها. برای ساده تر شدن بحث به مثال خود برگردیم. فرض کنید میخواهیم ساختاری برای اطلاعات دانشجو تعریف کنیم. این ساختار نیاز به یک اسم دارد که اسم آن را student انتخاب می‌کنیم و در بدنه ساختار باید متغیرهای مورد نیاز را تعریف کنیم:

struct student
{
     int number;
     string name;
     float avg;
};

در کد بالا یک ساختار به نام student تعریف کردیم که در واقع می‌توان به آن نوع داده ای جدید نگاه کرد. ساختار student شامل سه متغیر number، name و avg است.
حال که ساختار student را تعریف کردیم باید در مورد طریقه استفاده از ساختار صحبت کنیم. همانطور که گفتیم ساختارها را می‌توان به عنوان نوع داده جدید که برنامه نویس تعریف کرده است نگاه کنیم. بنابراین برای تعریف یک متغیر از جنس ساختار از قالب زیر استفاده می‌کنیم:

; نام متغیر نام ساختار

مثلا برای تعریف یک متغیر از نوع ساختار student از کد زیر استفاده می‌کنیم:

student   s;

اگر به خاطر داشته باشید در تعریف متغیر هم از قالب بالا استفاده میکنیم (مثلا int x) که این مورد عجیبی نیست! دوباره تاکید میکنیم که می‌توان به ساختار به عنوان یک نوع داده ای جدید نگاه کرد که برنامه نویس مطابق با نیاز مساله آن را تعریف میکند. بعد از اینکه متغیر s را تعریف کردیم باید در مورد این موضوع صحبت کنیم که چگونه میتوان به متغیرهای درون s (که s از جنس student است) دسترسی پیدا کرد. بهتر است جواب این سوال و نکات تکمیلی را در مثال زیر ببینید:

مثال 1- برنامه‌ای بنویسید که با استفاده از مفهوم ساختار، اطلاعات یک دانشجو را از ورودی دریافت کند و همان اطلاعات را در خروجی چاپ کند.

اگه دوست دارید میتونید آموزش استراکچر رو در قالب 2 ویدیو در کانال یوتیوبم ببینید:

یوتیوب
#include <iostream>
#include <string>

using namespace std;

struct student{
	int number;
	string name;
	float avg;	
};

int main(){
	student s;
	
	cin>>s.number>>s.name>>s.avg;
	cout<<s.number<<" "<<s.name<<" "<<s.avg;
	
}

توضیح کد:
در خط 6 تا 10 ساختار student تعریف شده است.
در خط 13 متغیر s از جنس student تعریف شده است. برای اینکه بهتر با این متغیر آشنا شوید توضیحات تکمیلی را بخوانید:
وقتی متغیر i را از جنس int تعریف می‌کنیم، به این معنی است که در این متغیر فقط یک عدد صحیح می‌تواند ذخیره کنیم. اما وقتی متغیر s را از نوع student تعریف میکنیم در این متغیر سه مقدار قابل ذخیره شدن هستند: name، number و avg. بنابراین در اینجا دستور cin>>s کاملا بی معنی است. زیرا کامپایلر نمیداند شما میخواهید متغیر name از s را مقدار دهی کنید ، یا متغیر number و یا avg. دلیل استفاده از دات (نقطه) در خط 15 و 16 هم به همین دلیل است. وقتی می‌نویسیم s.name یعنی میخواهیم به name از متغیر s دسترسی داشته باشیم. به همین صورت s.number و s.avg.

با توضیحات ارائه شده احتمالا متوجه شدید که ساختار (استراکچر) موجود پیچیده و عجیب غریبی نیست! از اسم آن پیداست که ما با استراکچر، ساختاری ترتیب میدهیم که راحت تر بتوانیم داده های مساله را ذخیره و مدیریت کنیم. قبل از ادامه آموزش و طرح مثالهای جدید این نکات را با دقت بخوانید:

نکات مهم ساختار

  1. قالب ساختار را قبل از int main تعریف کنید. (خط 6 تا 10 قبل از int main نوشته شده اند)
  2. بعد از آکولاد بسته در پایان تعریف قالب ساختار، حتما سیمی کالون قرار دهید (خط 10)
  3. به متغیرهایی که در قالب ساختار تعریف می‌شوند، اعضای ساختار نیز میگویند (member) مثلا در ساختار بالا number، name و avg اعضای student هستند.

مثال 2- برنامه‌ای بنویسید که با استفاده از ساختار تاریخ تولد فردی را از ورودی دریافت کند و همان تاریخ دوباره در خروجی چاپ کند.

قبل از توضیح مساله ذکر این نکته ضروریست که در مسائل واقعی هیچ وقت به شما نمیگویند که از ساختار استفاده کنید! این شما هستید که باید با بررسی صورت مساله متوجه شوید که مثلا این سوال به ساختار نیاز دارد، به آرایه نیاز دارد یا ….
اما چرا بهتر است در این مثال از ساختاراستفاده کنیم؟ چون داده های مرتبط به هم داریم. روز، ماه و سال. با این مقدمه کد زیر را ببینید:

#include <iostream>
#include <string>

using namespace std;

struct date{
	int day,month,year;	
};

int main(){
	date d;
	
	cin>>d.day>>d.month>>d.year;
	cout<<d.day<<"/"<<d.month<<"/"<<d.year;
	
}

در کد بالا، ابتدا ساختار date تعریف شده است که شامل سه متغبر day، month و year است. سپس در تابع main به این متغیرها دسترسی پیدا کرده ایم.
جالب است بدانید که از ساختار date میتوان در ساختار student هم استفاده کرد. مثلا اگر بخواهیم تاریخ تولد دانشجو را هم ذخیره کنیم می‌توانیم ساختار student را به صورت زیر اصلاح کنیم:

struct date{
	int day,month,year;	
};

struct student{
	int number;
	string name;
	float avg;	
	date birth_day;	
};

همانطور که در کد بالا می‌بیند ابتدا ساختار date تعریف شده است و در ادامه در ساختار student از ساختار date استفاده شده است. نکته قابل توجه در خواندن تاریخ تولد دانشجو این است که شما نمیتوانید بنویسید cin>>s.birth_day .چرا؟ به این دلیل که birth_day از جنس date است و خود شامل سه متغیر شما باید دقیق مشخص کنید که چه به متغیری از birth_day نیاز دارید. پس برای مقدار دهی تاریخ تولد دانشجو باید به صورت زیر عمل کنید:

cin>>s.birth_day.day>>s.birth_day.month>>s.birth_day.year;

پیشنهاد میکنم این سوالات رو هم ببینید:

سازنده

شاید بدون دانستن مفهموم سازنده بتوانید بدون مشکل از ساختارها استفاده کنید اما همانطور که بالاتر گفتم ساختارها میتوانند پل ورود ما به شی گرایی باشند و بهتر است این مفهوم را یاد بگیرید.
با این مثال شروع میکنیم: اگر بخواهید متغیر x که از جنس int است را مقدار دهی اولیه کنید از این کد استفاده میکنیم:
int x = 0;
اما آیا می‌توان متغیر s که از جنس student است نیز به این صورت مقدار دهی کنیم؟ قطعا خیر! زیرا در متغیر s چند عضو داده ای داریم. یک روش این است که ابتدا متغیر را تعریف کنیم و سپس تک تک اعضای داده‌ای را مقدار دهی اولیه کنیم:

student s;
s.name="ali";
s.number=9912512;
...

روش اصولی تر برای این کار استفاده از سازنده است که البته سازنده هم دقیقا همین خروجی کار را برای ما رقم می‌زند. مثال زیر یک مثال ساده از کاربرد سازنده است:

struct date
{
	int day,month, year ;
	date()
	{
		day=1;
		month=1;
		year=1399;
	}
};


در کد بالا استراکچر date تعریف شده است که شامل سه عضو داده ای month، day و year است. علاوه بر آن تابعی هم نام با نام استراکچر تعریف شده است (تابع date که در خط 4 میبینید) که یک فرق اساسی با توابعی که تا الان با آنها کار میکردیم دارد و آن این است که نوع برگشتی ندارد. در این تابع (که ما به آن سازنده می‌گوییم) مقدار دهی اولیه صورت گرفته است. نکته مهم در مورد سازنده ها این است که شما به هیچ وجه نمیتوانید این تابع را به صورت مستقیم فراخوانی کنید. حتما این سوال برای شما پیش می‌آید پس چگونه دستورات تابع فراخوانی می‌شوند؟ در واقع شما هر بار که یک متغیر از جنس استراکچر date تعریف می‌کنید به صورت اتوماتیک دستورات سازنده اجرا می‌شود. مثلا در این مثال با هر بار تعریف متغیر از جنس date مقادیر روز، ماه و سال با تاریخ 99/1/1 مقداردهی اولیه می‌شوند.

همه مطالب عنوان شده را در قالب نکات زیر می‌توان خلاصه کرد

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

نکته آخر در مورد سازنده ها این که شما می‌توانید پارامترهای ورودی به سازنده بدهید و در واقع هنگام تعریف متغیر مقدار دهی انجام دهید. (توجه داشته باشید در کد بالا به ازای هر تعریف متغیر تاریخ شما فقط با 99/1/1 مقداردهی اولیه می‌شود ولی با دقت در کد زیر (که برای سازنده پارامتر ورودی مشخص کرده‌ایم شما میتوانید تاریخ‌ها متفاوت به متغیرتان نسبت دهید.)

#include <iostream>
using namespace std;

struct date
{
	int day,month, year ;
	date(int y,int m,int d)
	{
		year=y;
		month=m;
		day=d;
	}
};

int main(){
	
	date d1 ( 99,1,1);
	date d2 ( 99,4,1);
	
	cout<<d1.year<<"/"<<d1.month<<"/"<<d1.day<<endl;
	cout<<d2.year<<"/"<<d2.month<<"/"<<d2.day<<endl;

}

ارسال استراکچر به تابع

نحوه ارسال یک متغیر از جنس استراکچر به یک تابع همانند ارسال یک متغیر معمولی به تابع است و هیچ تفاوت دیگری ندارد. در مثال زیر سعی شده است همه نکاتی که در سازنده ها و نحوه ارسال استراکچر به تابع نیاز است را پوشش دهیم. با دقت مثال زیر را دنبال کنید.

مثال 3- برنامه ای بنویسید که دو تاریخ از ورودی دریافت کند. تاریخ اول تاریخ تولد کاربر و تاریخ دوم تاریخ امروز! سپس سن کاربر را بر اساس تعداد روزهای سپری شده نمایش دهد. (مثلا در خروجی نمایش دهد 7825 روز از سن کاربر میگذرد)

در این مثال هم نیاز است که از استراکچر date استفاده کنیم. در این استراکچر به گونه ای که در ادامه میبینید از سازنده استفاده می‌شود. توجه داشته باشید که ما در این مثال به دو تاریخ نیاز داریم. بناراین دو متغیر از جنس date تعریف می‌کنیم. برای پیدا کردن سن کاربر به چند طریق می‌توان اقدام کرد. شاید سرراست ترین روشی که به ذهنتان برسد این باشد که تاریخهای دریافت شده را در نظر میگیریم، روزها را از هم کم میکنیم، ماهها را از هم و به همین ترتیب سالها را از هم کم میکنیم. مثلا اگر روز تولد کاربر 15 باشد و امروز 20 ام باشد به راحتی 15 را از 20 کم میکنیم و 5 به دست می‌آِید، به همین ترتیب برای ماه و سال هم همین کار را میکنیم. اما مشکل زمانی پیش می آید که عدد روز تولد بزرگتر باشد. مثلا اگر تاریخ امروز 10 ام باشد و روز تولد 15ام باشد شما مجبورید یک واحد از ماه کم کنید و 30 روز به عدد 10 اضافه کنید که همانطور که مشخص این مشکل برای ماه هم میتواند پیش بیاید و مدیریت و کدنویسی به این روش سخت است. بنابراین از یک روش ساده تر استفاده می‌کنیم. و آن این است که ابتدا تاریخ داده شده به روز تبدیل می‌کنیم. یعنی ماه را در 30، سال را در 365 و دو عدد محاسبه شده را با روز جمع می‌کنیم. مثلا اگر تاریخ امروز 1399/4/1 باشد به این عدد میرسیم:

510756= 1 + 30* 4 + 365 * 1399

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

#include <iostream>
using namespace std;

struct date
{
	int day,month, year ;
	date(int y,int m,int d)
	{
		year=y;
		month=m;
		day=d;
	}
};

int date_to_days(date d);

int main(){
	
	int y,m,d;

	cout<<"tarikhe Tavallod ra vared konid (y,m,d):  ";
	cin>>y>>m>>d;
	
	date birth_day(y,m,d);
	
	cout<<"tarikhe emrooz ra vared konid (y,m,d):  ";
	cin>>y>>m>>d;
	
	date now(y,m,d);
	
	cout<<"senne shoma: "<<date_to_days(now) - date_to_days(birth_day);	

}

int date_to_days(date d){
	int sum=0;
	sum = sum + d.year * 365;
	sum = sum + d.month*30;
	sum = sum + d.day;
	
	return sum;
}

توضیح کد:
در خط 4 تا 13 ساختار date تعریف شده است که با دقت در آن می‌بینید که از سازنده با پارامتر استفاده شده است. یعنی هنگام تعریف متغیر از جنس date می‌توانیم اعضای داده ای استراکچر مقدار دهی اولیه کنیم. کاری در خط 24 و 29 انجام شده است. یعنی ابتدا y و m و d را از ورودی دریافت می‌کنیم (یک بار در خط 22 برای تاریخ تولید و یک بار در خط 27 برای تاریخ امروز) و متغیر birth_day و now را هم تعریف و هم مقدار دهی میکنیم.
در خط 35 تابع date_to_days تعریف شده است که یک متغیر از جنس date تعریف می‌کند و آن تاریخ را به تعداد روزهای سپری شده از روز اول تاریخ شمسی تبدیل می‌کند. این تابع دو بار و برای دو تاریخ now و birth_day فراخوانی شده است که در خط 31 جواب نهایی نمایش داده شده است.

در این نوشته سعی بر آن شد که همه نکات لازم در بحث استراکچرها ارائه شود. در نوشته بعدی به چند مثال سخت تر و کاربردی تر در این زمینه پرداخته می‌شود.