-
فرشید پوشنده
1405/05/30
-
31 مرداد 1405
بازگشت به: آموزش برنامه نویسی c++ سی پلاس پلاس
۱.۴ — تخصیص مقدار به متغیر (Variable Assignment) و مقداردهی اولیه (Initialization)
در درس قبلی (۱.۳ — آشنایی با اشیاء و متغیرها)، نحوه تعریف یک متغیر را که میتوانیم برای ذخیره مقادیر استفاده کنیم، پوشش دادیم. در این درس، نحوه قرار دادن مقادیر در متغیرها را بررسی خواهیم کرد.
به عنوان یک یادآوری، در اینجا یک برنامه کوتاه آورده شده است که ابتدا یک متغیر صحیح به نام x تخصیص میدهد، سپس دو متغیر صحیح دیگر به نامهای y و z تخصیص میدهد:
1 int main()
2 {
3 int x; // define an integer variable named x (preferred)
4 int y, z; // define two integer variables, named y and z
5
6 return 0;
7 }
به عنوان یادآوری، ترجیح داده میشود که هر متغیر را در یک خط تعریف کنید. بعداً در این درس به مواردی که چندین متغیر را تعریف میکنیم، بازخواهیم گشت.
تخصیص مقدار به متغیر (Variable Assignment)
پس از تعریف یک متغیر، میتوانید با استفاده از عملگر = به آن یک مقدار بدهید (در یک دستور جداگانه). این فرآیند تخصیص (Assignment) نامیده میشود و عملگر =، عملگر تخصیص (Assignment Operator) نامیده میشود.
1 int width; // define an integer variable named width
2 width = 5; // assignment of value 5 into variable width
3
4 // variable width now has value 5
بهطور پیشفرض، تخصیص مقدار، مقدار سمت راست عملگر = را به متغیر سمت چپ عملگر کپی میکند. به این کار، تخصیص کپی (Copy-Assignment) میگویند.
هنگامی که به یک متغیر مقداری داده شد، مقدار آن متغیر را میتوان از طریق std::cout و عملگر << چاپ کرد.
از تخصیص میتوان هر زمان که بخواهیم مقدار نگهداری شده توسط یک متغیر را تغییر دهیم، استفاده کرد. در اینجا مثالی آورده شده است که در آن دو بار از تخصیص استفاده میکنیم:
1 #include <iostream>
2
3 int main()
4 {
5 int width; // define a variable named width
6 width = 5; // copy assignment of value 5 into variable width
7
8 std::cout << width; // prints 5
9
10 width = 7; // change value stored in variable width to 7
11
12 std::cout << width; // prints 7
13
14 return 0;
15 }
16
17 /*
18 Output:
19 57
20
21 Explanation:
22 - Line 8 prints the value of width (which is 5)
23 - Line 12 prints the value of width (which is now 7)
24 - Since there is no separator between them, the output is 57
25 - To separate them, you can use: std::cout << width << '\n'; or std::cout << width << std::endl;
26 */
هنگامی که این برنامه اجرا میشود، اجرا از بالای تابع main شروع شده و بهصورت ترتیبی پیش میرود. ابتدا، حافظه برای متغیر width تخصیص داده میشود. سپس مقدار ۵ را به width تخصیص میدهیم. وقتی مقدار width را خروجی میدهیم، عدد ۵ را در کنسول چاپ میکند. وقتی سپس مقدار ۷ را به width تخصیص میدهیم، هر مقدار قبلی (در این مورد ۵) بازنویسی (Overwritten) میشود. بنابراین وقتی دوباره width را خروجی میدهیم، این بار عدد ۷ را چاپ میکند.
متغیرهای معمولی فقط میتوانند یک مقدار را در یک زمان نگهداری کنند.
اگر می خوای حرفه ای برنامه نویسی یاد بگیری!
آموزش برنامه نویسی c++
«++C یکی از قدرتمندترین زبانهای برنامهنویسی دنیاست که هم سرعت و کارایی زبان C را دارد و هم امکانات پیشرفتهی شیگرایی را در اختیار میگذارد. با ++C میتوان از ساخت سیستمعاملها و نرمافزارهای سیستمی گرفته تا طراحی بازیهای سهبعدی و پروژههای پیچیدهی صنعتی را انجام داد. یادگیری این زبان مثل داشتن کلید ورود به دنیای عمیقتر کامپیوتر است؛ جایی که میتوانی کنترل کامل روی حافظه، پردازش و ساختار برنامه داشته باشی و ایدههایت را با نهایت قدرت و سرعت به واقعیت تبدیل کنی.»
هشدار
یکی از رایجترین اشتباهاتی که برنامهنویسان جدید مرتکب میشوند، اشتباه گرفتن عملگر تخصیص (=) با عملگر برابری (==) است. از تخصیص (=) برای اختصاص یک مقدار به یک متغیر استفاده میشود. از برابری (==) برای آزمایش اینکه آیا دو عملوند از نظر مقدار برابر هستند یا نه، استفاده میشود.
مقداردهی اولیه متغیر (Variable Initialization)
یکی از معایب تخصیص (Assignment) این است که اختصاص یک مقدار به یک شیء تازه تعریفشده نیاز به دو دستور دارد: یکی برای تعریف متغیر، و دیگری برای تخصیص مقدار.
این دو مرحله را میتوان ترکیب کرد. هنگامی که یک شیء تعریف میشود، میتوانید بهصورت اختیاری یک مقدار اولیه برای آن شیء ارائه دهید. فرآیند تعیین یک مقدار اولیه برای یک شیء، مقداردهی اولیه (Initialization) نامیده میشود و روشی که برای مقداردهی اولیه یک شیء استفاده میشود، مقداردهنده (Initializer) نامیده میشود. بهطور غیررسمی، مقدار اولیه اغلب “مقداردهنده” نیز نامیده میشود.
برای مثال، دستور زیر هم یک متغیر به نام width (از نوع int) را تعریف میکند و هم آن را با مقدار ۵ مقداردهی اولیه میکند:
1 #include <iostream>
2
3 int main()
4 {
5 int width { 5 }; // define variable width and initialize with initial value 5
6 std::cout << width; // prints 5
7
8 return 0;
9 }
10
11 /*
12 Output:
13 5
14
15 Explanation:
16 - Line 5 defines a variable named 'width' of type int and initializes it with the value 5 using brace initialization
17 - Line 6 prints the value of width (which is 5) to the console
18 - Brace initialization {} is the recommended way to initialize variables in modern C++
19 */
در مقداردهی اولیه متغیر width در بالا، { 5 } مقداردهنده (Initializer) است و 5 مقدار اولیه (Initial Value) است.
نکته کلیدی
مقداردهی اولیه (Initialization) یک مقدار اولیه برای یک متغیر فراهم میکند. به «مقداردهی اولیه» فکر کنید.
اشکال مختلف مقداردهی اولیه
برخلاف تخصیص (Assignment) که معمولاً ساده است، مقداردهی اولیه (Initialization) در C++ به طرز شگفتآوری پیچیده است. بنابراین در اینجا یک نمای سادهشده برای شروع ارائه میدهیم.
۵ شکل رایج مقداردهی اولیه در C++ وجود دارد:
1 int a; // default-initialization (no initializer)
2
3 // Traditional initialization forms:
4 int b = 5; // copy-initialization (initial value after equals sign)
5 int c ( 6 ); // direct-initialization (initial value in parenthesis)
6
7 // Modern initialization forms (preferred):
8 int d { 7 }; // direct-list-initialization (initial value in braces)
9 int e {}; // value-initialization (empty braces)
ممکن است اشکال بالا را با فاصلهگذاری متفاوت مشاهده کنید (به عنوان مثال int b=5;، int c(6);، int d{7};، int e{};). اینکه آیا برای خوانایی از فاصلههای اضافی استفاده کنید یا نه، یک موضوع سلیقه شخصی است.
از C++17 به بعد، مقداردهی کپی (Copy-initialization)، مقداردهی مستقیم (Direct-initialization) و مقداردهی مستقیم-لیست (Direct-list-initialization) در بیشتر موارد بهطور یکسان رفتار میکنند. ما مرتبطترین موردی را که در آن تفاوت دارند، در زیر پوشش میدهیم.
طلب مرتبط
ما بقیه تفاوتهای بین مقداردهی کپی (Copy-initialization)، مقداردهی مستقیم (Direct-initialization) و مقداردهی لیست (List-initialization) را در درس ۱۴.۱۵ — مقداردهی اولیه کلاس و حذف کپی (Class initialization and copy elision) پوشش میدهیم.
برای خوانندگان پیشرفته
سایر اشکال مقداردهی اولیه عبارتند از:
مقداردهی اولیه تجمیعی (Aggregate initialization) (به درس ۱۳.۸ — مقداردهی اولیه تجمیعی ساختارها مراجعه کنید).
مقداردهی اولیه کپی-لیست (Copy-list-initialization) (در زیر بحث شده است).
مقداردهی اولیه مرجع (Reference initialization) (به درس ۱۲.۳ — مراجع Lvalue مراجعه کنید).
مقداردهی اولیه ایستا (Static-initialization)، مقداردهی اولیه ثابت (Constant-initialization) و مقداردهی اولیه پویا (Dynamic-initialization) (به درس ۷.۸ — چرا متغیرهای سراسری (غیر-const) بد هستند مراجعه کنید).
مقداردهی اولیه صفر (Zero-initialization) (در زیر بحث شده است).
مقداردهی اولیه پیشفرض (Default-initialization)
زمانی که هیچ مقداردهندهای (Initializer) ارائه نمیشود (مانند متغیر a در بالا)، به این کار مقداردهی اولیه پیشفرض (Default-initialization) میگویند. در بسیاری از موارد، مقداردهی اولیه پیشفرض هیچ مقداردهیای انجام نمیدهد و متغیر را با یک مقدار نامعین (Indeterminate Value) (مقداری که قابل پیشبینی نیست، که گاهی “مقدار زباله (Garbage Value)” نامیده میشود) باقی میگذارد.
ما این مورد را بیشتر در درس (۱.۶ — متغیرهای مقداردهینشده و رفتار تعریفنشده) بحث خواهیم کرد.
مقداردهی اولیه کپی (Copy-initialization)
زمانی که یک مقدار اولیه بعد از یک علامت مساوی ارائه میشود، به این کار مقداردهی اولیه کپی (Copy-initialization) میگویند. این شکل از مقداردهی اولیه از زبان C به ارث رسیده است.
1 int width = 5; // copy-initialization of value 5 into variable width
بسیار شبیه به تخصیص کپی (Copy-assignment)، این کار مقدار سمت راست علامت مساوی را به متغیری که در سمت چپ در حال ایجاد است، کپی میکند. در قطعه کد بالا، متغیر width با مقدار ۵ مقداردهی اولیه میشود.
مقداردهی اولیه کپی در C++ مدرن به دلیل اینکه برای برخی انواع پیچیده، کارایی کمتری نسبت به سایر اشکال مقداردهی اولیه دارد، از محبوبیت افتاده بود. با این حال، C++17 بخش عمده این مسائل را برطرف کرد و مقداردهی اولیه کپی اکنون طرفداران جدیدی پیدا میکند. همچنین آن را در کدهای قدیمیتر (به ویژه کدهای منتقلشده از C) یا توسط توسعهدهندگانی که به سادگی فکر میکنند طبیعیتر به نظر میرسد و خواندن آن آسانتر است، خواهید دید.
برای خوانندگان پیشرفته
مقداردهی اولیه کپی (Copy-initialization) همچنین هر زمان که مقادیر بهطور ضمنی (Implicitly) کپی میشوند، استفاده میشود، مانند زمانی که آرگومانها به یک تابع با مقدار (By Value) ارسال میشوند، از یک تابع با مقدار بازگشت داده میشوند، یا استثناها (Exceptions) با مقدار گرفته میشوند.
مقداردهی اولیه مستقیم (Direct-initialization)
زمانی که یک مقدار اولیه در داخل پرانتز ارائه میشود، به این کار مقداردهی اولیه مستقیم (Direct-initialization) میگویند.
1 int width ( 5 ); // direct initialization of value 5 into variable width
مقداردهی اولیه مستقیم در ابتدا برای امکان مقداردهی اولیه کارآمدتر اشیاء پیچیده (آنهایی که دارای انواع کلاس هستند، که در یک فصل آینده پوشش خواهیم داد) معرفی شد. درست مانند مقداردهی اولیه کپی، مقداردهی اولیه مستقیم در C++ مدرن تا حد زیادی به دلیل جایگزینی با مقداردهی اولیه مستقیم-لیست (Direct-list-initialization) از محبوبیت افتاده بود. با این حال، مقداردهی اولیه مستقیم-لیست خود دارای چند ویژگی عجیب است، و بنابراین مقداردهی اولیه مستقیم دوباره در موارد خاصی در حال استفاده است.
برای خوانندگان پیشرفته
مقداردهی اولیه مستقیم (Direct-initialization) همچنین زمانی استفاده میشود که مقادیر بهطور صریح به نوع دیگری تبدیل میشوند (به عنوان مثال از طریق static_cast).
مقداردهی اولیه لیست (List-initialization)
روش مدرن برای مقداردهی اولیه اشیاء در C++، استفاده از شکلی از مقداردهی اولیه است که از آکولادهای مجعد (Curly Braces) استفاده میکند. این کار مقداردهی اولیه لیست (List-initialization) (یا مقداردهی اولیه یکسان (Uniform initialization) یا مقداردهی اولیه آکولادی (Brace initialization)) نامیده میشود.
مقداردهی اولیه لیست به دو شکل ارائه میشود:
1 int width { 5 }; // direct-list-initialization of initial value 5 into variable width (preferred)
2 int height = { 6 }; // copy-list-initialization of initial value 6 into variable height (rarely used)
قبل از C++11، برخی از انواع مقداردهی اولیه نیاز به استفاده از مقداردهی اولیه کپی (Copy-initialization) داشتند، و برخی دیگر نیاز به استفاده از مقداردهی اولیه مستقیم (Direct-initialization) داشتند. تشخیص مقداردهی اولیه کپی از تخصیص کپی (Copy-assignment) میتواند دشوار باشد (زیرا هر دو از = استفاده میکنند). و تشخیص مقداردهی اولیه مستقیم از عملیاتهای مربوط به توابع نیز میتواند دشوار باشد (زیرا هر دو از پرانتز استفاده میکنند).
مقداردهی اولیه لیست برای ارائه یک روش مقداردهی اولیه معرفی شد که تقریباً در همه موارد کار میکند، بهطور یکسان رفتار میکند، و یک روش بدون ابهام دارد که تشخیص اینکه در حال مقداردهی اولیه یک شیء هستیم را آسان میکند.
نکته کلیدی
وقتی آکولادهای مجعد (Curly Braces) را میبینیم، میدانیم که در حال مقداردهی اولیه لیست (List-initializing) یک شیء هستیم.
علاوه بر این، مقداردهی اولیه لیست همچنین روشی برای مقداردهی اولیه اشیاء با یک لیست از مقادیر به جای یک مقدار واحد فراهم میکند (به همین دلیل “مقداردهی اولیه لیست” نامیده میشود). ما مثالی از این را در درس ۱۶.۲ — آشنایی با std::vector و سازندههای لیست نشان میدهیم.
مقداردهی اولیه لیست، تبدیلات محدودکننده (Narrowing Conversions) را ممنوع میکند
یکی از مزایای اصلی مقداردهی اولیه لیست برای برنامهنویسان جدید C++ این است که “تبدیلات محدودکننده (Narrowing Conversions)” ممنوع هستند. این بدان معناست که اگر سعی کنید یک متغیر را با استفاده از مقداری که متغیر نمیتواند بهطور ایمن نگه دارد، مقداردهی اولیه لیست کنید، کامپایلر موظف است یک تشخیص (خطای کامپایل یا هشدار) برای اطلاعرسانی به شما تولید کند. برای مثال:
1 int main()
2 {
3 // An integer can only hold non-fractional values.
4 // Initializing an int with fractional value 4.5 requires the compiler to convert 4.5 to a value an int can hold.
5 // Such a conversion is a narrowing conversion, since the fractional part of the value will be lost.
6
7 int w1 { 4.5 }; // compile error: list-init does not allow narrowing conversion
8
9 int w2 = 4.5; // compiles: w2 copy-initialized to value 4
10 int w3 (4.5); // compiles: w3 direct-initialized to value 4
11
12 return 0;
13 }
در خط ۷ برنامه بالا، ما از یک مقدار (۴.۵) با یک جزء کسری (.۵) برای مقداردهی اولیه لیست یک متغیر صحیح (که فقط میتواند مقادیر غیرکسری را نگه دارد) استفاده میکنیم. از آنجایی که این یک تبدیل محدودکننده (Narrowing Conversion) است، کامپایلر موظف است در چنین مواردی یک تشخیص تولید کند.
مقداردهی اولیه کپی (خط ۹) و مقداردهی اولیه مستقیم (خط ۱۰) هر دو بیسروصدا جزء .۵ را حذف کرده و متغیر را با مقدار ۴ مقداردهی اولیه میکنند (که احتمالاً چیزی نیست که ما میخواهیم). کامپایلر شما ممکن است در مورد این موضوع به شما هشدار دهد (زیرا از دست دادن داده به ندرت مطلوب است)، اما ممکن است هم هشدار ندهد.
توجه داشته باشید که این محدودیت در مورد تبدیلات محدودکننده فقط برای مقداردهی اولیه لیست اعمال میشود، نه برای تخصیصهای بعدی به متغیر:
1 int main()
2 {
3 int w1 { 4.5 }; // compile error: list-init does not allow narrowing conversion of 4.5 to 4
4
5 w1 = 4.5; // okay: copy-assignment allows narrowing conversion of 4.5 to 4
6
7 return 0;
8 }
مقداردهی اولیه مقداری (Value-initialization) و مقداردهی اولیه صفر (Zero-initialization)
هنگامی که یک متغیر با استفاده از یک مجموعه خالی از آکولادها مقداردهی اولیه میشود، شکل خاصی از مقداردهی اولیه لیست به نام مقداردهی اولیه مقداری (Value-initialization) رخ میدهد. در بیشتر موارد، مقداردهی اولیه مقداری بهطور ضمنی متغیر را به صفر (یا هر مقداری که برای یک نوع معین به صفر نزدیکترین است) مقداردهی اولیه میکند. در مواردی که صفر شدن رخ میدهد، به این کار مقداردهی اولیه صفر (Zero-initialization) میگویند.
1 int width {}; // value-initialization / zero-initialization to value 0
برای خوانندگان پیشرفته
برای انواع کلاس (Class Types)، مقداردهی اولیه مقداری (Value-initialization) (و مقداردهی اولیه پیشفرض) ممکن است در عوض شیء را به مقادیر پیشفرض از پیش تعریفشده مقداردهی اولیه کند، که ممکن است غیرصفر باشند.
مقداردهی اولیه لیست، شکل ترجیحی مقداردهی اولیه در C++ مدرن است
مقداردهی اولیه لیست، شکل ترجیحی مقداردهی اولیه در C++ مدرن است
مقداردهی اولیه لیست (از جمله مقداردهی اولیه مقداری) معمولاً نسبت به سایر اشکال مقداردهی اولیه ترجیح داده میشود، زیرا در بیشتر موارد کار میکند (و بنابراین سازگارترین است)، تبدیلات محدودکننده را ممنوع میکند (که معمولاً نمیخواهیم)، و از مقداردهی اولیه با یک لیست از مقادیر پشتیبانی میکند (چیزی که در یک درس آینده پوشش خواهیم داد).
بهترین روش
برای مقداردهی اولیه متغیرهای خود، مقداردهی اولیه مستقیم-لیست (Direct-list-initialization) یا مقداردهی اولیه مقداری (Value-initialization) را ترجیح دهید.
یادداشت نویسنده
بیارنه استراستروپ (Bjarne Stroustrup) (خالق C++) و هِرب ساتِر (Herb Sutter) (متخصص C++) نیز استفاده از مقداردهی اولیه لیست را برای مقداردهی اولیه متغیرهای خود توصیه میکنند.
در C++ مدرن، مواردی وجود دارد که مقداردهی اولیه لیست آنطور که انتظار میرود کار نمیکند. ما یکی از این موارد را در درس ۱۶.۲ — آشنایی با std::vector و سازندههای لیست پوشش میدهیم. به دلیل چنین ویژگیهای عجیبی، برخی از توسعهدهندگان باتجربه اکنون استفاده از ترکیبی از مقداردهی کپی، مستقیم و لیست را بسته به شرایط توصیه میکنند. هنگامی که به اندازه کافی با زبان آشنا شدید تا تفاوتهای ظریف هر نوع مقداردهی و منطق پشت چنین توصیههایی را درک کنید، میتوانید خودتان ارزیابی کنید که آیا این استدلالها را قانعکننده میدانید یا خیر.
سوال: چه زمانی باید با { 0 } مقداردهی اولیه کنم در مقابل {}؟
زمانی که واقعاً از مقدار اولیه استفاده میکنید، از مقداردهی اولیه مستقیم-لیست (Direct-list-initialization) استفاده کنید:
1 int x { 0 }; // direct-list-initialization with initial value 0
2 std::cout << x; // we're using that 0 value here
زمانی که مقدار شیء موقتی است و قرار است جایگزین شود، از مقداردهی اولیه مقداری (Value-initialization) استفاده کنید:
1 int x {}; // value initialization
2 std::cin >> x; // we're immediately replacing that value so an explicit 0 would be meaningless
متغیرهای خود را مقداردهی اولیه کنید
متغیرهای خود را هنگام ایجاد، مقداردهی اولیه کنید. ممکن است در نهایت مواردی را پیدا کنید که به دلیل خاصی میخواهید این توصیه را نادیده بگیرید (مثلاً یک بخش بحرانی از کد از نظر عملکرد که از متغیرهای زیادی استفاده میکند)، و این اشکالی ندارد، تا زمانی که این انتخاب آگاهانه انجام شود.
مطلب مرتبط
برای بحث بیشتر در این زمینه، بیارنه استراستروپ (Bjarne Stroustrup) (خالق C++) و هِرب ساتِر (Herb Sutter) (متخصص C++) خودشان این توصیه را در اینجا مطرح کردهاند. توصیه مستقیم آنها، یعنی «همیشه یک شیء را مقداردهی اولیه کن»، به عنوان یک قانون اساسی در راهنمای هسته C++ (C++ Core Guidelines) ثبت شده است .
📜 دلیل این توصیه چیست؟
دلیل اصلی این قانون، جلوگیری از دستهای از باگهای بسیار رایج و خطرناک است که ناشی از استفاده از یک متغیر قبل از مقداردهی میشوند. چنین خطاهایی منجر به رفتار تعریفنشده (Undefined Behavior) میشوند، یعنی برنامه شما ممکن است هر بار به شکلی تصادفی و غیرقابلپیشبینی عمل کند .
🤔 آیا استثنایی وجود دارد؟
بله، در عمل ممکن است مواردی وجود داشته باشد. رایجترین سناریو، برنامهنویسی با عملکرد بالا (Performance-Critical) است. برای مثال، اگر قصد دارید آرایهای عظیم از اشیاء ساده (POD) ایجاد کنید و سپس به سرعت همه آنها را با دادههای معتبر پر کنید، مقداردهی اولیه اولیه ممکن است یک «زمان تلف شده» محسوب شود .
با این حال، حتی در این موارد نیز باید با دقت بسیار عمل کنید. این یک بهینهسازی زودهنگام (Premature Optimization) است و باید با حاشیهنویسیهای واضح در کد مستند شود تا سایر برنامهنویسان متوجه شوند که چرا یک متغیر بدون مقدار اولیه رها شده است . اگر متغیر در مسیرهای مختلف کد، مقادیر متفاوتی دریافت میکند، میتوان از یک تابع لامبدا (Lambda) برای مقداردهی اولیه استفاده کرد تا متغیر بهدرستی مقداردهی شود، حتی اگر این فرآیند پیچیده باشد .
نتیجهگیری: قانون همیشه مقداردهی اولیه، یک اصل طلایی برای نوشتن کد ایمن و خوانا است. با رشد تجربه شما، متوجه خواهید شد که چه زمانی انحراف از آن، با آگاهی کامل و دلیل موجه، قابل قبول است.
ما در درس ۱.۶ — متغیرهای مقداردهینشده و رفتار تعریفنشده بررسی میکنیم که اگر سعی کنید از متغیری استفاده کنید که مقدار مشخصی ندارد، چه اتفاقی میافتد.
بهترین روش
متغیرهای خود را هنگام ایجاد، مقداردهی اولیه کنید.
نمونهسازی (Instantiation)
اصطلاح نمونهسازی (Instantiation) یک کلمه فانتزی است که به این معنی است که یک متغیر ایجاد (تخصیص) و مقداردهی اولیه شده است (این شامل مقداردهی اولیه پیشفرض نیز میشود). به یک شیء نمونهسازیشده، گاهی نمونه (Instance) گفته میشود. بیشتر اوقات، این اصطلاح برای اشیاء از نوع کلاس به کار میرود، اما گاهی اوقات برای اشیاء از انواع دیگر نیز به کار میرود.
مقداردهی اولیه چندین متغیر
در بخش آخر، اشاره کردیم که امکان تعریف چندین متغیر از یک نوع در یک دستور واحد با جدا کردن نامها با کاما وجود دارد:
1 int a, b; // create variables a and b, but do not initialize them
همچنین اشاره کردیم که بهترین روش، اجتناب کامل از این روش است. با این حال، از آنجایی که ممکن است با کدهای دیگری مواجه شوید که از این سبک استفاده میکنند، باز هم مفید است که کمی بیشتر در مورد آن صحبت کنیم، اگر به هیچ دلیل دیگری نباشد، برای تقویت برخی از دلایلی که باید از آن اجتناب کنید.
میتوانید چندین متغیر تعریفشده در یک خط را مقداردهی اولیه کنید:
1 int a = 5, b = 6; // copy-initialization
2 int c ( 7 ), d ( 8 ); // direct-initialization
3 int e { 9 }, f { 10 }; // direct-list-initialization
4 int i {}, j {}; // value-initialization
متأسفانه، یک دام رایج در اینجا وجود دارد که زمانی رخ میدهد که برنامهنویس به اشتباه سعی میکند هر دو متغیر را با استفاده از یک دستور مقداردهی اولیه کند:
1 int a, b = 5; // wrong: a is not initialized to 5!
2 int a = 5, b = 5; // correct: a and b are initialized to 5
در دستور بالا، متغیر a مقداردهینشده باقی خواهد ماند و کامپایلر ممکن است شکایت کند یا نکند. اگر شکایت نکند، این یک راه عالی برای خراب شدن متناوب برنامه یا تولید نتایج تصادفی است. به زودی بیشتر در مورد اینکه اگر از متغیرهای مقداردهینشده استفاده کنید، چه اتفاقی میافتد، صحبت خواهیم کرد.
بهترین راه برای به خاطر سپردن اینکه این کار اشتباه است، توجه به این نکته است که هر متغیر فقط میتواند توسط مقداردهنده (Initializer) مخصوص خود مقداردهی اولیه شود:
1 int a = 4, b = 5; // correct: a and b both have initializers
2 int a, b = 5; // wrong: a doesn't have its own initializer
هشدارهای متغیرهای مقداردهیشده ولی استفادهنشده
کامپایلرهای مدرن معمولاً در صورتی که یک متغیر مقداردهی اولیه شده اما استفاده نشده باشد، هشدار تولید میکنند (زیرا این به ندرت مطلوب است). و اگر «رفتار با هشدارها مانند خطا» فعال باشد، این هشدارها به خطا ارتقا یافته و باعث شکست کامپایل میشوند.
برنامه به ظاهر بیآزار زیر را در نظر بگیرید:
1 int main()
2 {
3 int x { 5 }; // variable x defined
4
5 // but not used anywhere
6
7 return 0;
8 }
هنگام کامپایل این با GCC و فعال بودن «رفتار با هشدارها مانند خطا»، خطای زیر تولید میشود
prog.cc: In function 'int main()': prog.cc:3:9: error: unused variable 'x' [-Werror=unused-variable]
و برنامه کامپایل نمیشود.
چند راه آسان برای رفع این مشکل وجود دارد:
۱. اگر متغیر واقعاً استفاده نشده است و به آن نیاز ندارید، سادهترین گزینه حذف تعریف x است (یا حاشیهنویسی کردن آن). به هر حال، اگر استفاده نمیشود، حذف آن هیچ تأثیری نخواهد داشت.
۲. گزینه دیگر این است که به سادگی از متغیر در جایی استفاده کنید:
1 #include <iostream>
2
3 int main()
4 {
5 int x { 5 };
6
7 std::cout << x; // variable now used somewhere
8
9 return 0;
10 }
اما این کار نیاز به نوشتن کدی دارد که از آن استفاده کند و این عیب را دارد که ممکن است رفتار برنامه شما را تغییر دهد.
ویژگی [[maybe_unused]] در C++17
در برخی موارد، هیچیک از گزینههای بالا مطلوب نیستند. حالتی را در نظر بگیرید که مجموعهای از مقادیر ریاضی/فیزیکی داریم که در بسیاری از برنامههای مختلف از آنها استفاده میکنیم:
1 #include <iostream>
2
3 int main()
4 {
5 // Here's some math/physics values that we copy-pasted from elsewhere
6 double pi { 3.14159 };
7 double gravity { 9.8 };
8 double phi { 1.61803 };
9
10 std::cout << pi << '\n'; // pi is used
11 std::cout << phi << '\n'; // phi is used
12
13 // The compiler will likely complain about gravity being defined but unused
14
15 return 0;
16 }
اگر از این مقادیر زیاد استفاده کنیم، احتمالاً آنها را در جایی ذخیره کرده و همه را با هم کپی/پیست/وارد میکنیم.
با این حال، در هر برنامهای که از همه این مقادیر استفاده نکنیم، کامپایلر احتمالاً درباره هر متغیری که واقعاً استفاده نشده است، شکایت خواهد کرد. در مثال بالا، به راحتی میتوانستیم تعریف gravity را حذف کنیم. اما اگر به جای ۳ متغیر، ۲۰ یا ۳۰ متغیر داشتیم چه؟ و اگر از آنها در چندین مکان استفاده میکردیم چه؟ مرور لیست متغیرها برای حذف/حاشیهنویسی کردن موارد استفادهنشده، زمان و انرژی میبرد. و بعداً اگر به یکی از مواردی که قبلاً حذف کردهایم نیاز پیدا کنیم، باید زمان و انرژی بیشتری صرف کنیم تا به عقب برگردیم و آن را دوباره اضافه/از حالت حاشیهنویسی خارج کنیم.
برای رفع چنین مواردی، C++17 ویژگی [[maybe_unused]] را معرفی کرد که به ما امکان میدهد به کامپایلر بگوییم که از استفاده نشدن یک متغیر راضی هستیم. کامپایلر برای چنین متغیرهایی هشدار متغیر استفادهنشده تولید نخواهد کرد.
برنامه زیر نباید هیچ هشدار/خطایی تولید کند:
1 #include <iostream>
2
3 int main()
4 {
5 [[maybe_unused]] double pi { 3.14159 }; // Don't complain if pi is unused
6 [[maybe_unused]] double gravity { 9.8 }; // Don't complain if gravity is unused
7 [[maybe_unused]] double phi { 1.61803 }; // Don't complain if phi is unused
8
9 std::cout << pi << '\n';
10 std::cout << phi << '\n';
11
12 // The compiler will no longer warn about gravity not being used
13
14 return 0;
15 }
علاوه بر این، کامپایلر احتمالاً این متغیرها را از برنامه حذف میکند (بهینهسازی میکند)، بنابراین هیچ تأثیری بر عملکرد ندارند.
ویژگی [[maybe_unused]] باید فقط بهصورت انتخابی برای متغیرهایی اعمال شود که دلیل خاص و معتبری برای استفاده نشدن دارند (به عنوان مثال، زیرا به لیستی از مقادیر نامدار نیاز دارید، اما اینکه کدام مقادیر خاص در یک برنامه مشخص استفاده میشوند، ممکن است متفاوت باشد). در غیر این صورت، متغیرهای استفادهنشده باید از برنامه حذف شوند.
یادداشت نویسنده
در درسهای آینده، ما اغلب متغیرهایی را تعریف میکنیم که دوباره از آنها استفاده نمیکنیم، تا روش نوشتاری (Syntax) مفاهیم خاصی را نشان دهیم. استفاده از [[maybe_unused]] به ما امکان میدهد این کار را بدون هشدار/خطای کامپایل انجام دهیم.