بنابراین، ما قبلاً می دانیم که چگونه توابع را در برنامه ها اعلام، تعریف و استفاده کنیم. در این فصل، ما در مورد شکل خاص آنها صحبت خواهیم کرد - توابع بیش از حد. دو تابع اگر نام یکسانی داشته باشند، در محدوده یکسانی اعلان شده باشند، اما لیست های متفاوتی از پارامترهای رسمی داشته باشند، سربار خوانده می شوند. توضیح خواهیم داد که چگونه چنین توابعی اعلام می شوند و چرا مفید هستند. سپس مسئله حل آنها را بررسی خواهیم کرد، یعنی. در مورد اینکه کدام یک از چندین تابع سربارگذاری شده در حین اجرای برنامه فراخوانی می شود. این مشکل یکی از سخت ترین مشکلات در ++C است. برای کسانی که می خواهند وارد جزئیات شوند خواندن دو بخش انتهای فصل که مبحث تبدیل نوع آرگومان و وضوح اضافه بار را با جزئیات بیشتر پوشش می دهد، برایشان جالب خواهد بود.

9.1. اعلان های عملکرد بیش از حد

اکنون با آموختن نحوه اعلان، تعریف و استفاده از توابع در برنامه ها، با اضافه باریکی دیگر از جنبه های C++ است. بارگذاری بیش از حد به شما امکان می دهد چندین تابع به یک نام داشته باشید که عملیات مشابهی را بر روی آرگومان های انواع مختلف انجام می دهند.
شما قبلاً از عملکرد بارگذاری شده از پیش تعریف شده استفاده کرده اید. به عنوان مثال، برای ارزیابی عبارت

عمل جمع اعداد صحیح نامیده می شود، در حالی که ارزیابی عبارت

1.0 + 3.0

جمع ممیز شناور را انجام می دهد. انتخاب این یا آن عملیات به طور نامحسوس برای کاربر انجام می شود. عملگر جمع برای قرار دادن عملوندهای انواع مختلف بیش از حد بارگذاری می شود. این وظیفه کامپایلر است، نه برنامه نویس، که زمینه را بشناسد و عملیات متناسب با انواع عملوندها را اعمال کند.
در این فصل، ما به شما نشان خواهیم داد که چگونه توابع اضافه بار خود را تعریف کنید.

9.1.1. چرا باید نام تابع را بیش از حد بارگذاری کنید

همانند عملیات افزودن داخلی، ممکن است به مجموعه‌ای از توابع نیاز داشته باشیم که عملکرد مشابهی را انجام دهند، اما بر روی پارامترهایی از انواع مختلف. فرض کنید می‌خواهیم توابعی را تعریف کنیم که بیشترین مقدار پارامتر ارسال شده را برمی‌گردانند. اگر اضافه بار وجود نداشت، به هر یک از این تابع ها باید یک نام منحصر به فرد داده شود. برای مثال، خانواده max() از توابع ممکن است به شکل زیر باشد:

int i_max(int, int); int vi_max(بردار const &); int matrix_max(const matrix &);

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

int ix = max(j, k); بردار vec; //... int iy = max(vec);

این روش ثابت می کند که در بسیاری از موقعیت ها بسیار مفید است.

9.1.2. نحوه بارگذاری بیش از حد نام تابع

در C++، دو یا چند تابع را می توان به یک نام داد، مشروط بر اینکه لیست پارامترهای آنها از نظر تعداد پارامترها یا در انواع آنها متفاوت باشد. AT این مثالما یک تابع max() سربارگذاری شده را اعلام می کنیم:

intmax (int, int); int max(بردار const &); int max (ماتریس const &);

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

  • اگر لیست پارامترهای دو تابع بر اساس عدد یا متفاوت باشد انواع پارامتر، سپس توابع اضافه بار در نظر گرفته می شوند: // توابع بیش از حد بارگذاری شده void print(const string &); چاپ خالی (وکتور &);
  • اگر نوع برگشتی و لیست های پارامتر در اعلان های دو تابع یکسان باشد، اعلان دوم تکرار شده در نظر گرفته می شود: // اعلان های همان تابع void print(const string &str); void print (const string &); هنگام مقایسه اعلان ها، نام پارامترها در نظر گرفته نمی شود.
    اگر لیست پارامترهای دو تابع یکسان است، اما انواع برگشتی متفاوت است، اعلان دوم نادرست در نظر گرفته می شود (ناسازگار با اولی) و توسط کامپایلر به عنوان یک خطا علامت گذاری می شود: unsigned int max(int ​​· i1, int i2)؛ int max (int · i1، int i2)؛
    // خطا: فقط انواع متفاوت است
    // مقادیر را برمی گرداند

توابع اضافه بار نمی توانند تنها در انواع برگشتی خود متفاوت باشند. اگر لیست پارامترهای دو تابع فقط در مقادیر آرگومان پیش‌فرض آنها متفاوت باشد، اعلان دوم تکرار شده در نظر گرفته می‌شود:

// اعلان های همان تابع int max (int *ia, int sz); int max (int *ia، int = 10);

کلمه کلیدی typedef یک نام جایگزین برای آن ایجاد می کند نوع موجودداده، هیچ نوع جدیدی ایجاد نمی شود. بنابراین، اگر لیست پارامترهای دو تابع فقط از این جهت متفاوت باشد که یکی از typedef استفاده می‌کند و دیگری از نوعی استفاده می‌کند که typedef نام مستعار آن است، لیست‌ها مانند دو اعلان تابع calc() یکسان در نظر گرفته می‌شوند. در این مورد، اعلان دوم یک خطای کامپایل می دهد زیرا مقدار بازگشتی با مقدار مشخص شده قبلی متفاوت است:

// typedef نوع جدیدی از typedef double DOLLAR معرفی نمی کند. // خطا: لیست پارامترهای یکسان، اما انواع بازگشت // متفاوت extern DOLLAR calc(DOLLAR); extern int calc(double);

در چنین مقایسه‌ای، مشخص‌کننده‌های const یا volatile در نظر گرفته نمی‌شوند. بنابراین، دو اعلامیه زیر یکسان در نظر گرفته می شوند:

// همان تابع را void f(int) اعلام کنید. void f(const int);

مشخص کننده const فقط در داخل تعریف تابع مهم است: نشان می دهد که تغییر مقدار پارامتر در بدنه تابع ممنوع است. با این حال، یک آرگومان ارسال شده توسط مقدار می تواند در بدنه یک تابع مانند یک متغیر راه اندازی شده معمولی استفاده شود: خارج از تابع، تغییرات قابل مشاهده نیستند. (روش‌های ارسال آرگومان‌ها، به‌ویژه عبور از مقدار، در بخش 7.3 مورد بحث قرار گرفته‌اند.) افزودن یک مشخص‌کننده const به پارامتری که توسط مقدار ارسال می‌شود، بر تفسیر آن تأثیر نمی‌گذارد. هر مقدار از نوع int را می توان به تابعی که به صورت f(int) اعلام شده است، و همچنین تابع f(const int) ارسال کرد. از آنجا که هر دو مجموعه ای از مقادیر آرگومان را دریافت می کنند، اعلان های بالا اضافه بار در نظر گرفته نمی شوند. f() را می توان به صورت تعریف کرد

باطل f(int i) ( )

Void f(const int i) ( )

وجود این دو تعریف در یک برنامه یک خطا است، زیرا یک تابع دو بار تعریف شده است.
با این حال، اگر یک مشخص کننده const یا volatile به پارامتری از یک اشاره گر یا نوع مرجع اعمال شود، هنگام مقایسه اعلان ها در نظر گرفته می شود.

// توابع مختلف void f(int*) اعلام می شوند. void f(const int*); // و توابع مختلف در اینجا اعلام شده است
void f(int&);
void f(const int&);

9.1.3. زمانی که نام تابع بیش از حد بارگذاری نشود

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

void setDate(Date&, int, int, int); تاریخ &convertDate(رشته const &); void printDate(const Date&);

این توابع بر روی یک نوع داده، کلاس Date، کار می کنند، اما اقدامات معنایی متفاوتی را انجام می دهند. در این مورد، پیچیدگی واژگانی مرتبط با استفاده از نام‌های مختلف ناشی از قراردادی است که برنامه‌نویس برای ارائه مجموعه‌ای از عملیات بر روی نوع داده و نام‌گذاری توابع مطابق با معنایی این عملیات اتخاذ کرده است. درست است، مکانیسم کلاس C++ چنین قراردادی را اضافی می کند. ما باید چنین توابعی را به اعضای کلاس Date تبدیل کنیم، اما نام‌های مختلفی را بگذاریم که منعکس کننده معنای عملیات باشد:

#عبارتند از کلاس Date ( public: set(int, int, int); Date& convert (const string &); void print(); // ...
};

بیایید مثال دیگری بزنیم. پنج تابع عضو صفحه زیر، عملیات مختلفی را بر روی مکان‌نمای صفحه، که عضوی از همان کلاس است، انجام می‌دهند. ممکن است منطقی به نظر برسد که این توابع تحت نام عمومی move():

Screen& moveHome(); Screen& moveAbs (int, int); Screen& moveRel (int, int, char *direction); Screen& moveX(int); Screen& moveY(int);

با این حال، دو تابع آخر را نمی توان بیش از حد بارگذاری کرد، زیرا آنها لیست پارامترهای یکسانی دارند. برای منحصر به فرد کردن امضا، بیایید آنها را در یک تابع ترکیب کنیم:

// تابعی که ترکیبی از moveX() و moveY() Screen& move(int, char xy);

اکنون همه توابع دارای لیست پارامترهای مختلف هستند، بنابراین می توان آنها را تحت نام move() بارگذاری کرد. با این حال، این کار نباید انجام شود: نام های مختلف حاوی اطلاعاتی هستند که بدون آنها درک برنامه دشوارتر خواهد بود. به عنوان مثال، عملیات حرکت مکان نما که توسط این توابع انجام می شود متفاوت است. به عنوان مثال، moveHome() نوع خاصی از حرکت را به گوشه سمت چپ بالای صفحه انجام می دهد. کدام یک از دو تماس زیر کاربرپسندتر است و به خاطر سپردن آسان تر است؟

// کدام تماس واضح تر است؟ myScreen.home(); // ما فکر می کنیم این یکی! myScreen.move();

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

MoveAbs (int, int); moveAbs (int، int، char*);

در حضور پارامتر سوم از نوع char* متفاوت است. اگر پیاده‌سازی‌های آن‌ها مشابه باشند و یک مقدار پیش‌فرض معقول برای آرگومان سوم پیدا شود، هر دو تابع را می‌توان با یک جایگزین کرد. در این حالت، یک اشاره گر با مقدار 0 برای نقش مقدار پیش فرض مناسب است:

Move(int، int، char* = 0);

زمانی که منطق برنامه به آن نیاز دارد، باید از ویژگی های خاصی استفاده کنید. لزومی ندارد که توابع بیش از حد بارگذاری شده را فقط به دلیل وجود آنها در یک برنامه قرار دهید.

9.1.4. اضافه بار و دامنه A

همه توابع اضافه بار در یک محدوده اعلام می شوند. به عنوان مثال، یک تابع اعلام شده به صورت محلی بیش از حد بارگذاری نمی کند، بلکه به سادگی یک تابع عمومی را پنهان می کند:

#عبارتند از void print (const string &); voidprint (دوگانه)؛ // overloads print() void fooBar(int ival)
{
// محدوده جداگانه: هر دو پیاده سازی print() را پنهان می کند
extern void print(int); // خطا: print(const string &) در این قسمت قابل مشاهده نیست
print("مقدار:");
print(ival); // correct: print(int) قابل مشاهده است
}

از آنجایی که هر کلاس محدوده خود را تعریف می کند، توابعی که اعضای دو کلاس مختلف هستند یکدیگر را بیش از حد بارگذاری نمی کنند. (توابع عضو کلاس در فصل 13 پوشش داده شده است. وضوح اضافه بار برای توابع عضو کلاس در فصل 15 پوشش داده شده است.)
همچنین امکان اعلام چنین توابعی در داخل فضای نام وجود دارد. هر یک از آنها محدوده خاص خود را نیز دارد، به طوری که توابع اعلام شده در محدوده های مختلف، یکدیگر را بیش از حد بارگذاری نمی کنند. مثلا:

#عبارتند از فضای نام IBM ( extern void print (const string &); extern void print (double); // overloads print() ) namespace Disney ( // محدوده جداگانه: // تابع print() را از فضای نام خارجی IBM اضافه نمی کند چاپ (int)؛

استفاده از اعلان ها و استفاده از دستورالعمل ها به در دسترس قرار دادن اعضای فضای نام در حوزه های دیگر کمک می کند. این مکانیسم ها تا حدودی بر اعلان عملکردهای بارگذاری شده تأثیر دارند. (استفاده از اعلامیه ها و استفاده از دستورالعمل ها در بخش 8.6 مورد بحث قرار گرفت.)

چگونه استفاده-اعلام بر اضافه بار عملکرد تأثیر می گذارد؟ به یاد بیاورید که یک نام مستعار برای عضو فضای نام در محدوده ای که اعلان در آن انجام می شود، معرفی می کند. چنین اعلامیه هایی در برنامه زیر چه می کنند؟

فضای نام libs_R_us ( int max(int, int); int max(double, double); extern void print(int);
چاپ خالی خارجی (دوگانه)؛
) // استفاده از اعلان ها
با استفاده از libs_R_us::max;
با استفاده از libs_R_us::print(double); // خطای void func()
{
max(87, 65); // libs_R_us::max(int, int) را فرا می خواند
حداکثر (35.5، 76.6); // libs_R_us::max (دوبل، دو برابر) را فرا می خواند

اولین اعلان استفاده‌کننده، هر دو تابع libs_R_us::max را به دامنه جهانی می‌آورد. اکنون هر یک از توابع max() را می توان در داخل func() فراخوانی کرد. انواع آرگومان ها تعیین می کنند که کدام تابع را فراخوانی کنیم. دومین استفاده از اعلان یک اشکال است: نمی تواند فهرستی از پارامترها داشته باشد. تابع libs_R_us::print() فقط به این صورت اعلام می شود:

با استفاده از libs_R_us::print;

اعلان استفاده همیشه همه توابع بارگذاری شده را با نام مشخص شده در دسترس قرار می دهد. این محدودیت تضمین می کند که رابط فضای نام libs_R_us نقض نشود. واضح است که در صورت تماس

چاپ(88);

نویسنده فضای نام انتظار دارد که تابع libs_R_us::print(int) فراخوانی شود. اگر به کاربر اجازه دهید که به طور انتخابی تنها یکی از چندین عملکرد بارگذاری شده را در محدوده قرار دهد، رفتار برنامه غیرقابل پیش بینی می شود.
چه اتفاقی می‌افتد اگر استفاده از اعلان تابعی با نامی از قبل موجود را وارد محدوده کند؟ این توابع به نظر می‌رسند که درست در جایی که اعلان استفاده وجود دارد، اعلان شده‌اند. بنابراین، توابع معرفی شده در فرآیند حل نام تمام توابع اضافه بار موجود در محدوده داده شده شرکت می کنند:

#عبارتند از فضای نام libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // libs_R_us::print(int) و libs_R_us::print(double)
// اضافه بار چاپ (رشته const &)
با استفاده از libs_R_us::print; void fooBar (int ival)
{
// چاپ (رشته const &)
}

اعلان استفاده دو اعلان به دامنه جهانی اضافه می کند: یکی برای چاپ (int) و دیگری برای چاپ (دوبار). آنها نام مستعار در فضای libs_R_us هستند و در مجموعه ای از توابع بیش از حد بارگذاری شده به نام print گنجانده شده اند، جایی که چاپ سراسری (رشته const &) از قبل وجود دارد. هنگام رفع اضافه بار چاپ در fooBar، هر سه عملکرد در نظر گرفته می شوند.
اگر یک اعلان استفاده، تابعی را به محدوده‌ای وارد کند که قبلاً تابعی با همان نام و لیست پارامترهای مشابه دارد، خطا در نظر گرفته می‌شود. اگر print(int) از قبل در محدوده جهانی وجود داشته باشد، یک اعلان استفاده نمی‌تواند تابع print(int) در فضای نام libs_R_us باشد. مثلا:

فضای نام libs_R_us ( void print(int); void print(double); ) void print(int); با استفاده از libs_R_us::print; // خطا: اعلان مکرر print(int) void fooBar(int ival)
{
print(ival); // کدام چاپ؟ ::print یا libs_R_us::print
}

ما نشان داده‌ایم که چگونه استفاده از اعلان‌ها و توابع اضافه بار با هم مرتبط هستند. حال بیایید به ویژگی های استفاده از use-directive نگاه کنیم. دستورالعمل use باعث می شود اعضای فضای نام خارج از آن فضا ظاهر شوند و آنها را به یک محدوده جدید اضافه می کند. اگر قبلاً تابعی با همین نام در این محدوده وجود داشته باشد، اضافه بار رخ می دهد. مثلا:

#عبارتند از فضای نام libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // با استفاده از دستورالعمل
// print(int)، print(double) و print(const string &) عناصر هستند
// همان مجموعه توابع اضافه بار
با استفاده از فضای نام libs_R_us; void fooBar (int ival)
{
print("مقدار:"); // تابع سراسری را فراخوانی می کند
// چاپ (رشته const &)
print(ival); // libs_R_us::print(int) را فرا می خواند
}

این نیز زمانی صادق است که چندین دستورالعمل استفاده کنند. توابع با نام یکسان، که اعضای فضاهای مختلف هستند، در یک مجموعه قرار می گیرند:

فضای نام IBM ( int print(int); ) namespace Disney ( double print(double); ) // using-directive // ​​بسیاری از توابع سربارگذاری شده را از // فضاهای نام مختلف با استفاده از فضای نام IBM تولید می کند. استفاده از فضای نام دیزنی؛ چاپ دوبل بلند (طولان دوتایی); int main() (
چاپ (1); // به نام IBM::print(int)
چاپ (3.1); // تماس با دیزنی::چاپ (دوبل)
بازگشت 0;
}

مجموعه ای از توابع بیش از حد بارگذاری شده به نام print در محدوده جهانی شامل توابع print(int)، print(double) و print (long double) می باشد. همه آنها در هنگام وضوح اضافه بار در main() در نظر گرفته می شوند، اگرچه آنها در ابتدا در فضاهای نام مختلف تعریف شده بودند.
بنابراین، دوباره، توابع اضافه بار در همان محدوده هستند. به ویژه، آنها در نتیجه استفاده از اعلامیه ها و استفاده از دستورالعمل هایی که نام هایی را از حوزه های دیگر در دسترس قرار می دهند، به آنجا ختم می شوند.

9.1.5. دستور C خارجی و توابع A اضافه بار

ما در بخش 7.7 دیدیم که دستور bind خارجی "C" را می توان در یک برنامه C++ برای نشان دادن اینکه برخی از شی ها در یک قسمت C است استفاده کرد. آیا توابع نوشته شده در C++ و C می توانند در یک مجموعه باشند؟
یک دستورالعمل bind مجاز است فقط یکی از بسیاری از توابع اضافه بار را مشخص کند. به عنوان مثال، برنامه زیر نادرست است:

// خطا: دستورالعمل مشخص شده برای دو تابع اضافه بار extern "C" extern "C" void print(const char*); extern "C" void print(int);

مثال زیر از یک تابع calc() اضافه بار، استفاده معمولی از دستور خارجی "C" را نشان می دهد:

ClassSmallInt(/* ... */); کلاس BigNum(/* ... */); // یک تابع نوشته شده در C را می توان هر دو از یک برنامه فراخوانی کرد،
// نوشته شده در C یا از یک برنامه نوشته شده در C ++.
// توابع C++ پارامترهایی را کنترل می کنند که کلاس هستند
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
خارجی BigNum calc(const BigNum&);

تابع calc() نوشته شده در C را می توان هم از C و هم از برنامه C++ فراخوانی کرد. دو تابع دیگر یک کلاس را به عنوان پارامتر می گیرند و بنابراین فقط در یک برنامه ++C قابل استفاده هستند. ترتیب اعلامیه ها مهم نیست.
دستورالعمل bind در تصمیم گیری برای فراخوانی کدام تابع بی ربط است. فقط انواع پارامترها مهم هستند. تابعی که بهترین تطابق را با انواع آرگومان های ارسال شده دارد انتخاب می شود:

Smallint si = 8; int main() ( calc(34); // فراخوانی تابع C calc(double) calc(si)؛ // فراخوانی تابع C++ calc(const SmallInt &) // ... بازگشت 0؛ )

9.1.6. اشاره گرها به توابع اضافه بار A

شما می توانید یک اشاره گر را به یکی از توابع بیش از حد بارگذاری شده اعلام کنید. مثلا:

extern void ff(بردار ) extern void ff(unsigned int); // pf1 به کدام تابع اشاره می کند؟
void (*pf1)(unsigned int) =

از آنجایی که تابع ff() بیش از حد بارگذاری شده است، مقدار اولیه &ff به تنهایی برای انتخاب گزینه صحیح کافی نیست. برای درک اینکه کدام تابع نشانگر را مقداردهی اولیه می کند، کامپایلر در مجموعه همه توابع سربارگذاری شده به دنبال تابعی می گردد که نوع بازگشتی و لیست پارامترهای مشابه تابعی را که اشاره گر به آن اشاره می کند، دارد. در مورد ما، تابع ff(unsigned int) انتخاب خواهد شد.
اما اگر هیچ تابعی وجود نداشته باشد که دقیقاً با نوع اشاره گر مطابقت داشته باشد چه؟ سپس کامپایلر یک پیغام خطا می دهد:

extern void ff(بردار ) extern void ff(unsigned int); // خطا: مطابقت یافت نشد: لیست پارامتر نامعتبر void (*pf2)(int) = // خطا: مطابقت یافت نشد: نوع بازگشت اشتباه double (*pf3)(بردار ) = &ff;

تخصیص به روشی مشابه عمل می کند. اگر مقدار اشاره گر باید آدرس یک تابع اضافه بار باشد، از نوع اشاره گر به تابع برای انتخاب عملوند در سمت راست عملگر تخصیص استفاده می شود. و اگر کامپایلر تابعی را پیدا نکند که دقیقاً با نوع مورد نظر مطابقت داشته باشد، پیغام خطا صادر می کند. بنابراین، تبدیل نوع بین نشانگرهای تابع هرگز انجام نمی شود.

ماتریس calc (ماتریس const &); intcalc (int, int); int (*pc1)(int, int) = 0;
int (*pc2)(int, double) = 0; //...
// correct: تابع calc(int, int) انتخاب شده است
pc1 = // خطا: مطابقت ندارد: نوع پارامتر دوم نامعتبر است
pc2=

9.1.7. اتصال ایمن A

هنگام استفاده از بارگذاری بیش از حد، این تصور به وجود می آید که یک برنامه می تواند چندین عملکرد به یک نام با لیست های مختلف پارامترها داشته باشد. با این حال، این راحتی واژگانی فقط در سطح متن مبدأ وجود دارد. در اکثر سیستم های کامپایل، برنامه هایی که این متن را برای تولید کد اجرایی پردازش می کنند، نیاز دارند که همه نام ها متمایز باشند. ویرایشگرهای پیوند معمولاً اجازه می دهند لینک های خارجیاز نظر لغوی اگر چنین ویرایشگری دو یا چند بار با چاپ نام روبرو شود، نمی تواند آنها را با تجزیه و تحلیل نوع تشخیص دهد (اطلاعات نوع معمولاً در این نقطه از بین می رود). بنابراین فقط یک پیام در مورد چاپ کاراکتر بازتعریف شده چاپ می کند و خارج می شود.
برای حل این مشکل، نام تابع، همراه با لیست پارامترهای آن، برای دادن یک نام داخلی منحصر به فرد تزئین شده است. برنامه هایی که بعد از کامپایلر فراخوانی می شوند فقط این نام داخلی را می بینند. اینکه این وضوح نام دقیقاً چگونه انجام می شود به پیاده سازی بستگی دارد. ایده کلی این است که تعداد و انواع پارامترها را به عنوان یک رشته کاراکتر نشان دهیم و آن را به نام تابع اضافه کنیم.
همانطور که در بخش 8.2 ذکر شد، چنین کدگذاری به ویژه تضمین می کند که دو اعلان توابع با نام یکسان با لیست های پارامترهای مختلف که در فایل های مختلف قرار دارند توسط پیوند دهنده به عنوان اعلان های یک تابع درک نمی شوند. از آنجایی که این روش به تمایز بین توابع بیش از حد بارگذاری شده در مرحله ویرایش پیوند کمک می کند، ما در مورد پیوند امن صحبت می کنیم.
تزئین نام برای توابع اعلام شده با دستور خارجی "C" اعمال نمی شود، زیرا تنها یکی از بسیاری از توابع اضافه بار را می توان با C خالص نوشت. دو تابع با لیست های پارامترهای مختلف که به عنوان "C" خارجی اعلام شده اند توسط پیوند دهنده به عنوان یکی در نظر گرفته می شوند. و همین شخصیت

تمرین 9.1

چرا باید توابع اضافه بار را اعلام کنید؟

تمرین 9.2

نحوه اعلان نسخه های بارگذاری شده تابع error() به طوری که فراخوانی های زیر صحیح باشد:

شاخص Int; int upperBound; char selectVal; // ... error("Array out of bounds: ", index, upperBound); خطا ("تقسیم بر صفر"); خطا ("انتخاب نامعتبر"، selectVal)؛

تمرین 9.3

تأثیر اعلان دوم را در هر یک از مثال های زیر توضیح دهید:

(الف) intcalc(int، int); int calc(const int, const int); (ب) int get(); double get(); (ج) int *reset(int *); double *reset(double *): (d) extern "C" int compute(int *, int); extern "C" double compute(double *, double);

تمرین 9.4

کدام یک از مقداردهی اولیه زیر منجر به خطا می شود؟ چرا؟

(الف) تنظیم مجدد void (int *); void (*pf)(void *) = reset; (ب) intcalc(int, int); int (*pf1)(int، int) = calc; (c) extern "C" int compute(int *, int); int (*pf3)(int*, int) = محاسبه; (d) void (*pf4)(const matrix &) = 0;

9.2. سه مرحله وضوح اضافه بار

وضوح عملکرد اضافه بارفرآیند انتخاب تابع از مجموعه overloaded نامیده می شود که باید فراخوانی شود. این فرآیند بر اساس آرگومان هایی است که هنگام فراخوانی مشخص شده است. به یک مثال توجه کنید:

T t1, t2; void f(int, int); void f(float, float); int main() (
f(t1, t2);
بازگشت 0;
}

در اینجا، در طول فرآیند تفکیک اضافه بار، بسته به نوع T، مشخص می شود که آیا هنگام پردازش عبارت f(t1,t2) تابع f(int,int) یا f(float,float) فراخوانی می شود یا یک خطا. ثبت خواهد شد.
وضوح اضافه بار تابع یکی از پیچیده ترین جنبه های زبان C++ است. تلاش برای درک تمام جزئیات، برنامه نویسان تازه کار با مشکلات جدی مواجه خواهند شد. بنابراین در این بخش فقط ارائه می کنیم بررسی کوتاهدر مورد نحوه عملکرد وضوح اضافه بار، به طوری که حداقل تصوری از فرآیند به دست آورید. برای کسانی که می خواهند بیشتر بدانند، در دو بخش بعدی توضیحات مفصل تری ارائه می شود.
فرآیند حل یک تابع اضافه بار شامل سه مرحله است که در مثال زیر نشان خواهیم داد:

void f(); void f(int); void f (دو برابر، دو برابر = 3.4)؛ void f(char *، char *); void main() (
f(5.6);
بازگشت 0;
}

هنگام رفع اضافه بار عملکرد، مراحل زیر انجام می شود:

  1. مجموعه ای از توابع اضافه بار برای یک فراخوانی مشخص، و همچنین ویژگی های لیست آرگومان های ارسال شده به تابع، برجسته می شود.
  2. آن دسته از توابع اضافه بار که می توان با آرگومان های داده شده فراخوانی کرد، با در نظر گرفتن تعداد و انواع آنها انتخاب می شوند.
  3. تابعی که به بهترین شکل با تماس مطابقت دارد پیدا می شود.

بیایید هر مورد را به نوبه خود در نظر بگیریم.
اولین قدم شناسایی مجموعه ای از توابع اضافه بار است که در این فراخوانی در نظر گرفته می شود. توابع موجود در این مجموعه نامزد نامیده می شوند. تابع کاندید تابعی است همنام با تابع فراخوانی شده و اعلان آن در نقطه فراخوانی قابل مشاهده است. در مثال ما، چهار نامزد وجود دارد: f()، f(int)، f(double، double) و f(char*، char*).
پس از آن، ویژگی های لیست آرگومان های تصویب شده شناسایی می شوند، یعنی. تعداد و انواع آنها در مثال ما، لیست از دو آرگومان دوگانه تشکیل شده است.
در مرحله دوم، از میان مجموعه نامزدها، موارد قابل اجرا انتخاب می شوند - آنهایی که می توان با آرگومان های داده شده فراخوانی کرد. سپس برای هر پارامتر اضافی، مقدار پیش فرض. برای اینکه یک تابع پایدار در نظر گرفته شود، هر آرگومان واقعی ارسال شده در فراخوانی باید دارای تبدیل به نوع پارامتر رسمی مشخص شده در اعلان باشد.

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

  • تابع f(int) زنده ماند زیرا فقط یک پارامتر دارد و تبدیل آرگومان دوگانه واقعی به پارامتر رسمی int وجود دارد.
  • تابع f(double,double) زنده ماند زیرا یک مقدار پیش فرض برای آرگومان دوم وجود دارد و اولین پارامتر رسمی از نوع double است که دقیقاً از نوع آرگومان واقعی است.

اگر بعد از مرحله دوم هیچ توابع پایداری پیدا نشد، آنگاه فراخوانی اشتباه در نظر گرفته می شود. در چنین مواردی می گوییم مکاتبه صورت نمی گیرد.
مرحله سوم این است که عملکردی را انتخاب کنید که به بهترین وجه با زمینه تماس مطابقت دارد. چنین عملکردی بهترین حالت ایستاده (یا بهترین تناسب) نامیده می شود. در این مرحله، تبدیل های مورد استفاده برای ریختن انواع آرگومان های واقعی به انواع پارامترهای رسمی تابع ایجاد شده رتبه بندی می شوند. مناسب ترین عملکردی در نظر گرفته می شود که برای آن شرایط زیر وجود داشته باشد:
تبدیل‌های اعمال شده بر روی آرگومان‌های واقعی بدتر از تبدیل‌های لازم برای فراخوانی هر تابع تثبیت‌شده دیگری نیستند.
برای برخی از آرگومان‌ها، تبدیل‌های اعمال‌شده بهتر از تبدیل‌های لازم برای فرستادن همان آرگومان‌ها در فراخوانی‌های دیگر توابع به خوبی تثبیت‌شده هستند.
تبدیل نوع و رتبه بندی آنها با جزئیات بیشتر در بخش 9.3 مورد بحث قرار گرفته است. در اینجا، ما فقط به طور خلاصه به تبدیل های رتبه بندی برای مثال خود نگاه می کنیم. برای تابع ثابت f(int)، قالب استاندارد آرگومان واقعی از نوع double به int باید اعمال شود. برای تابع ثابت f(double,double) نوع آرگومان واقعی double دقیقاً با نوع پارامتر رسمی مطابقت دارد. از آنجایی که یک تطابق دقیق بهتر از تبدیل استاندارد است (هیچ تبدیلی همیشه بهتر از داشتن یک تبدیل نیست)، f(double,double) مناسب ترین تابع برای این فراخوانی در نظر گرفته می شود.
اگر در مرحله سوم امکان یافتن تنها بهترین توابع ایجاد شده وجود نداشته باشد، به عبارت دیگر، چنین تابع ثابتی وجود نداشته باشد که بیش از همه جا بیفتد، آنگاه فراخوانی مبهم در نظر گرفته می شود، یعنی. اشتباه
(بخش 9.4 تمام مراحل تفکیک اضافه بار را با جزئیات بیشتری مورد بحث قرار می دهد. فرآیند تفکیک همچنین هنگام فراخوانی یک تابع عضو کلاس سربارگذاری شده و یک اپراتور سربارگذاری شده استفاده می شود. بخش 15.10 قوانین تفکیک اضافه بار را که برای توابع عضو کلاس اعمال می شود، و بخش 15.11 بحث می کند. قوانین مربوط به (اپراتورهای بیش از حد بارگذاری شده. وضوح اضافه بار باید عملکردهای نمونه سازی شده از الگوها را نیز در نظر بگیرد. بخش 10.8 نحوه تأثیر الگوها بر این وضوح را مورد بحث قرار می دهد.)

تمرین 9.5

در آخرین مرحله (سومین) فرآیند تفکیک اضافه بار تابع چه اتفاقی می افتد؟

9.3. تبدیل نوع آرگومان A

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

  • مطابقت کامل. نوع آرگومان واقعی دقیقاً با نوع پارامتر رسمی مطابقت دارد. به عنوان مثال، اگر مجموعه توابع print() overloaded دارای این موارد باشد: void print(unsigned int); void print (const char*); voidprint(char);
  • سپس هر یک از سه فراخوانی زیر یک تطابق دقیق به دست می‌دهد:
    بدون امضا int a;
چاپ ("a"); // مطابق با print(char); چاپ ("a"); // مطابق چاپ (const char*); چاپ (a)؛ // مطابق با print(unsigned int);
  • مطابقت با تبدیل نوع نوع آرگومان واقعی با نوع پارامتر رسمی مطابقت ندارد، اما می تواند به آن تبدیل شود: void ff(char); ff(0); // آرگومان از نوع int به نوع char فرستاده می شود
  • عدم انطباق نوع آرگومان واقعی را نمی توان به نوع پارامتر رسمی در اعلان تابع فرستاد زیرا تبدیل مورد نیاز وجود ندارد. برای هر یک از دو فراخوانی زیر تابع print() مطابقت ندارد:
  • توابع // print() به صورت فوق تعریف می شوند int *ip; کلاس SmallInt ( /* ... */ ); SmallInt si; چاپ (IP)؛ // خطا: مطابقت ندارد
    چاپ (si); // خطا: مطابقت ندارد
  • برای ایجاد یک تطابق دقیق، نوع آرگومان واقعی لازم نیست با نوع پارامتر رسمی مطابقت داشته باشد. برخی از تبدیل‌های بی‌اهمیت را می‌توان برای استدلال اعمال کرد، یعنی:

    • تبدیل l-value به r-value.
    • تبدیل آرایه به اشاره گر؛
    • تبدیل تابع به اشاره گر؛
    • تبدیل های مشخص کننده

    (در زیر با جزئیات بیشتر مورد بحث قرار می گیرند.) مقوله انطباق با تبدیل نوع پیچیده ترین است. انواع مختلفی از چنین بازیگرانی وجود دارد که باید در نظر گرفته شوند: نوع پسوند (تبلیغات)، تبدیل‌های استاندارد، و تبدیل‌های تعریف‌شده توسط کاربر. (برنامه‌های افزودنی و تبدیل‌های استاندارد در این فصل پوشش داده شده‌اند. تبدیل‌های تعریف‌شده توسط کاربر بعداً معرفی می‌شوند، پس از اینکه کلاس‌ها به طور مفصل مورد بحث قرار گرفتند؛ آنها توسط مبدل انجام می‌شوند، یک تابع عضو که به شما امکان می‌دهد مجموعه استاندارد خود را تعریف کنید. تبدیلات در یک کلاس. در فصل 15، ما به این گونه مبدل ها و چگونگی تأثیر آنها بر وضوح اضافه بار تابع نگاه خواهیم کرد.)
    هنگام انتخاب بهترین تابع به خوبی تثبیت شده برای یک فراخوانی مشخص، کامپایلر به دنبال تابعی می گردد که تبدیل های اعمال شده به آرگومان های واقعی برای آن "بهترین" هستند. تبدیل‌های نوع به صورت زیر رتبه‌بندی می‌شوند: تطابق دقیق بهتر از پسوند نوع، پسوند نوع بهتر از تبدیل استاندارد است، و این به نوبه خود بهتر از تبدیل تعریف‌شده توسط کاربر است. ما به رتبه بندی در بخش 9.4 باز خواهیم گشت، اما در حال حاضر مثال های سادهبیایید نشان دهیم که چگونه به انتخاب مناسب ترین عملکرد کمک می کند.

    9.3.1. درباره تطابق دقیق بیشتر بدانید

    ساده ترین حالت زمانی رخ می دهد که انواع آرگومان های واقعی با انواع پارامترهای رسمی یکسان باشند. به عنوان مثال، دو تابع max() سربارگذاری شده وجود دارد که در زیر نشان داده شده است. سپس هر یک از فراخوانی های max() دقیقاً با یکی از اعلان ها مطابقت دارد:

    int max(int, int); حداکثر دو برابر (دو برابر، دو برابر)؛ int i1; محاسبه خالی (d1 d1) (
    max(56, i1); // دقیقاً با حداکثر (int, int) مطابقت دارد.
    max(d1, 66.9); // دقیقاً با حداکثر مطابقت دارد (دبل، دو برابر)؛
    }

    یک نوع شمارش شده دقیقاً فقط با مواردی که در آن تعریف شده است مطابقت دارد. عناصر شمارشو همچنین اشیایی که از آن نوع اعلام شده اند:

    رمزهای Enum ( INLINE = 128; VIRTUAL = 129; ); نشانه‌ها curTok = INLINE; enum Stat( Fail, Pass ); extern void ff(Tokens);
    extern void ff(Stat);
    extern void ff(int); int main() (
    ff(Pass)؛ // دقیقاً با ff (آمار) مطابقت دارد
    ff(0); // دقیقاً با ff(int) مطابقت دارد
    ff(curTok); // دقیقاً با ff (Tokens) مطابقت دارد
    // ...
    }

    در بالا ذکر شد که آرگومان واقعی می‌تواند دقیقاً با پارامتر رسمی مطابقت داشته باشد، حتی اگر تبدیل کمی برای ریختن انواع آنها لازم باشد، که اولین مورد تبدیل l-value به r-value است. l-value یک شی است که شرایط زیر را برآورده می کند:

    • می توانید آدرس شی را دریافت کنید.
    • می توانید مقدار یک شی را بدست آورید.
    • این مقدار به راحتی قابل تغییر است (مگر اینکه یک مشخص کننده const در اعلان شی وجود داشته باشد).

    در مقابل، r-value عبارتی است که مقدار آن ارزیابی می شود، یا عبارتی است که نشان دهنده یک شی موقت است که آدرسی برای آن نمی توان به دست آورد و مقدار آن را نمی توان تغییر داد. در اینجا یک مثال ساده است:

    intcalc(int); int main() (int lval, res; lval = 5; // lvalue: lval; rvalue: 5
    res = calc(lval);
    // lvalue:res
    // rvalue: شی موقت برای ذخیره مقدار،
    // توسط تابع calc() برگردانده می شود
    بازگشت 0;
    }

    در اولین دستور انتساب، متغیر lval مقدار l و مقدار واقعی 5 مقدار r است. در دستور انتساب دوم، res مقدار l است و شی موقتی که نتیجه برگردانده شده توسط تابع calc() را ذخیره می کند، مقدار r است.
    در برخی شرایط، در زمینه‌ای که یک مقدار انتظار می‌رود، می‌توان از عبارتی استفاده کرد که یک مقدار l است:

    intobj1; intobj2; int main() (
    // ...
    intlocal = obj1 + obj2;
    بازگشت 0;
    }

    در اینجا obj1 و obj2 مقادیر l هستند. با این حال، برای انجام جمع در تابع main()، مقادیر آنها از متغیرهای obj1 و obj2 استخراج می‌شوند. عمل استخراج مقدار یک شی که با یک عبارت l نشان داده می شود، تبدیل یک مقدار l به یک مقدار r نامیده می شود.
    وقتی تابع انتظار دارد که یک آرگومان توسط مقدار ارسال شود، اگر آرگومان یک مقدار l باشد، به یک مقدار r تبدیل می شود:

    #عبارتند از رنگ رشته ("بنفش")؛ voidprint (رشته); int main() (
    چاپ (رنگی)؛ // تطابق دقیق: تبدیل lvalue
    // در مقدار rvalue
    بازگشت 0;
    }

    از آنجایی که آرگومان در فراخوانی print(color) توسط مقدار ارسال می شود، مقدار l به یک مقدار r تبدیل می شود تا مقدار رنگ را استخراج کرده و به تابع نمونه اولیه print(string) منتقل کند. با این حال، حتی اگر چنین ریخته‌گری صورت گرفته باشد، آرگومان رنگ واقعی دقیقاً با اعلان چاپ (رشته) مطابقت دارد.
    هنگام فراخوانی توابع، همیشه لازم نیست چنین تبدیلی در آرگومان ها اعمال شود. مرجع یک مقدار l است. اگر تابع دارای یک پارامتر مرجع باشد، پس از فراخوانی تابع، یک مقدار l دریافت می کند. بنابراین، تبدیل توصیف شده برای آرگومان واقعی که پارامتر مرجع رسمی با آن مطابقت دارد، اعمال نمی شود. برای مثال، فرض کنید تابع زیر اعلام شده است:

    #عبارتند از چاپ خالی (لیست &);

    در فراخوانی زیر، li مقدار l است که شی لیست را نشان می دهد ، به تابع print() منتقل می شود:

    فهرست کنید li (20); int main() (
    // ...
    print(li); // تطابق دقیق: بدون تبدیل از lvalue به
    // rvalue
    بازگشت 0;
    }

    تطبیق li با یک پارامتر مرجع یک تطابق دقیق در نظر گرفته می شود.
    تبدیل دوم، که هنوز یک تطابق دقیق را برطرف می کند، تبدیل یک آرایه به یک اشاره گر است. همانطور که در بخش 7.3 اشاره شد، یک پارامتر تابع هرگز یک نوع آرایه نیست، در عوض به یک اشاره گر به عنصر اول خود تبدیل می شود. به طور مشابه، آرگومان نوع آرایه واقعی از NT (که در آن N تعداد عناصر آرایه و T نوع هر عنصر است) همیشه به یک اشاره گر به T فرستاده می شود. این نوع تبدیل آرگومان واقعی آرایه به نامیده می شود. تبدیل اشاره گر با وجود این، آرگومان واقعی دقیقاً مطابق با پارامتر رسمی نوع "اشاره گر به T" در نظر گرفته می شود. مثلا:

    int ai; void putValues(int *); int main() (
    // ...
    putValues ​​(ai); // تطابق دقیق: تبدیل آرایه به
    // اشاره گر
    بازگشت 0;
    }

    قبل از فراخوانی تابع putValues() آرایه به یک اشاره گر تبدیل می شود و در نتیجه آرگومان واقعی ai (آرایه ای از سه عدد صحیح) به یک اشاره گر به یک int فرستاده می شود. اگرچه پارامتر رسمی تابع putValues() یک اشاره گر است و آرگومان واقعی هنگام فراخوانی تبدیل می شود، اما مطابقت دقیقی بین آنها برقرار می شود.
    هنگام ایجاد یک تطابق دقیق، امکان تبدیل تابع به یک اشاره گر نیز وجود دارد. (در بخش 7.9 ذکر شد.) مانند یک پارامتر آرایه، یک پارامتر تابع به یک نشانگر تابع تبدیل می شود. آرگومان واقعی از نوع "function" نیز به طور خودکار به نوع اشاره گر تابع فرستاده می شود. این نوع تبدیل آرگومان واقعی، تبدیل تابع به اشاره گر نامیده می شود. در حالی که تبدیل انجام می شود، آرگومان واقعی دقیقاً مطابق با پارامتر رسمی در نظر گرفته می شود. مثلا:

    Int lexicoCompare(const string &, const string &); typedef int (*PFI)(const string &, const string &);
    مرتب سازی خالی (رشته *، رشته *، PFI)؛ رشته به عنوان; int main()
    {
    // ...
    مرتب سازی (به عنوان
    as + sizeof(as)/sizeof(as - 1),
    lexicoCompare // مطابقت دقیق
    // تبدیل تابع به اشاره گر
    ) بازگشت 0;
    }

    قبل از فراخوانی sort() یک تبدیل تابع به اشاره گر اعمال می شود که آرگومان lexicoCompare را از نوع "function" به نوع "اشاره گر به تابع" می فرستد. اگرچه آرگومان رسمی تابع یک اشاره گر و آرگومان واقعی نام تابع است و بنابراین تابع به اشاره گر تبدیل شده است، آرگومان واقعی دقیقاً سومین پارامتر رسمی تابع sort() در نظر گرفته می شود. .
    آخرین مورد فوق، تبدیل مشخص کننده ها است. این فقط برای اشاره گرها اعمال می شود و شامل افزودن مشخص کننده های const یا volatile (یا هر دو) به نوعی است که نشانگر داده شده را نشان می دهد:

    Int a = ( 4454، 7864، 92، 421، 938)؛ int*pi = a; bool is_equal(const int * , const int *); void func(int *parm) ( // مطابقت دقیق بین pi و parm: تبدیل مشخص کننده ها
    if (is_equal (pi، parm))
    // ... بازگشت 0;
    }

    قبل از فراخوانی تابع ()is_equal، آرگومان های واقعی pi و parm از نوع "pointer به int" به نوع "pointer to const int" تبدیل می شوند. این تبدیل شامل اضافه کردن یک مشخص کننده const به نوع آدرس‌دهی شده است و بنابراین به دسته تبدیل‌های مشخص‌کننده تعلق دارد. حتی با وجود اینکه تابع انتظار دارد دو نشانگر به یک const int دریافت کند و آرگومان‌های واقعی نشانگرهای یک int هستند، فرض بر این است که تطابق دقیقی بین پارامترهای رسمی و واقعی تابع ()is_equal وجود دارد.
    تبدیل Specifier فقط به نوعی اعمال می شود که نشانگر آدرس دهی می کند. زمانی استفاده نمی شود که پارامتر رسمی دارای یک مشخص کننده const یا فرار باشد اما آرگومان واقعی ندارد.

    extern void takeCI(const int); int main() (
    int ii = ...;
    takeCI (ii); // تبدیل مشخص کننده اعمال نمی شود
    بازگشت 0;
    }

    اگرچه پارامتر رسمی تابع takeCI() از نوع const int است و با آرگومان ii از نوع int فراخوانی می شود، هیچ تبدیل مشخص کننده ای وجود ندارد: تطابق دقیقی بین آرگومان واقعی و پارامتر رسمی وجود دارد.
    تمام موارد فوق برای مواردی نیز صادق است که آرگومان یک اشاره گر است و مشخص کننده های const یا volatile به آن اشاره گر اشاره می کنند:

    extern void init(int *const); extern int *pi; int main() (
    // ...
    init(pi); // تبدیل مشخص کننده اعمال نمی شود
    بازگشت 0;
    }

    مشخص‌کننده const در پارامتر رسمی تابع init() به خود اشاره‌گر اشاره می‌کند، نه به نوعی که آدرس می‌دهد. بنابراین، کامپایلر هنگام تجزیه تبدیل هایی که باید در آرگومان واقعی اعمال شوند، این مشخص کننده را در نظر نمی گیرد. هیچ تبدیل مشخص کننده ای برای آرگومان pi اعمال نمی شود: این آرگومان و پارامتر رسمی دقیقاً مطابقت دارند.
    سه مورد اول از این تبدیل ها (L-value به r-value، array-to-pointer و function-to-pointer) اغلب به عنوان تبدیل های l-value نامیده می شوند. (در بخش 9.4 خواهیم دید که اگرچه تبدیل‌های مقدار l و تبدیل‌های مشخص‌کننده در دسته تبدیل‌هایی قرار می‌گیرند که تطابق دقیق را نقض نمی‌کنند، اما درجه آن در مواردی که فقط به اولین تبدیل نیاز است بالاتر در نظر گرفته می‌شود. در بخش بعدی. ، ما در این مورد با جزئیات بیشتری صحبت خواهیم کرد. .)
    با استفاده از ریخته‌گری نوع صریح می‌توان تطابق دقیق را اجبار کرد. به عنوان مثال، اگر دو تابع اضافه بار وجود داشته باشد:

    extern void ff(int); extern void ff(void *);

    FF(0xffbc); // فراخوانی ff(int)

    دقیقاً با ff(int) مطابقت دارد، حتی اگر 0xffbc تحت اللفظی به عنوان یک ثابت هگزادسیمال نوشته شود. برنامه نویس می تواند کامپایلر را وادار به فراخوانی تابع ff(void *) با اجرای صریح عملیات Cast کند:

    FF(reinterpret_cast (0xffbc))؛ // call ff(void*)

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

    9.3.2. اطلاعات بیشتر در مورد نوع پسوند

    پسوند نوع یکی از تبدیل های زیر است:

    • آرگومان واقعی نوع char، char بدون علامت یا short به نوع int گسترش می یابد. اگر اندازه ماشین int بزرگتر از short باشد، آرگومان واقعی type unsigned short به نوع int بسط می‌یابد، و در غیر این صورت به نوع unsigned int بسط می‌یابد.
    • آرگومان نوع float به نوع double گسترش می یابد.
    • یک آرگومان نوع شمارش شده به اولین نوع از انواع زیر گسترش می یابد که می تواند تمام مقادیر اعضای enum را نشان دهد: int, unsigned int, long, unsigned long;
    • آرگومان bool به نوع int گسترش یافته است.

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

    extern void manip(int); int main() (
    manip ("a"); // تایپ char به int گسترش می یابد
    بازگشت 0;
    }

    یک کاراکتر تحت اللفظی از نوع char است. به int گسترش می یابد. از آنجایی که نوع توسعه یافته با نوع پارامتر رسمی تابع manip() مطابقت دارد، می گوییم که فراخوانی آن مستلزم گسترش نوع آرگومان است.
    به مثال زیر توجه کنید:

    extern void print (unsigned int); extern void print(int); چاپ خالی خارجی (char); بدون امضا char uc;
    چاپ (uc); // print(int); uc فقط به یک پسوند نوع نیاز دارد

    در یک پلت فرم سخت افزاری که در آن char بدون علامت یک بایت حافظه و int چهار بایت مصرف می کند، برنامه افزودنی char بدون علامت را به int تبدیل می کند زیرا می تواند تمام مقادیر char بدون علامت را نشان دهد. برای چنین معماری ماشینی، از میان بسیاری از توابع اضافه بار نشان داده شده در مثال، print(int) بهترین تطابق را برای آرگومان char بدون علامت ارائه می دهد. برای دو عملکرد دیگر، تطبیق نیاز به یک بازیگر استاندارد دارد.
    مثال زیر بسط یک آرگومان نوع شمارش شده واقعی را نشان می دهد:

    Enum Stat (شکست، عبور)؛ extern void ff(int);
    extern void ff(char); int main() (
    // correct: enum Member Pass برای تایپ int گسترش می یابد
    ff(Pass)؛ // ff(int)
    ff(0); // ff(int)
    }

    گاهی اوقات گسترش enum ها شگفتی هایی را به همراه دارد. کامپایلرها اغلب نمایش یک enum را بر اساس مقادیر عناصر آن انتخاب می کنند. فرض کنید معماری فوق (یک بایت برای char و چهار بایت برای int) یک enum را به این صورت تعریف می کند:

    شماره e1 (a1، b1، c1)؛

    از آنجایی که تنها سه عنصر a1، b1 و c1 به ترتیب با مقادیر 0، 1 و 2 وجود دارد - و از آنجایی که همه این مقادیر را می توان با نوع char نمایش داد، کامپایلر معمولاً char را برای نشان دادن نوع e1 انتخاب می کند. با این حال، شمارش e2 را با مجموعه عناصر زیر در نظر بگیرید:

    تعداد e2 (a2، b2، c2=0x80000000)؛

    از آنجایی که یکی از ثابت ها دارای مقدار 0x80000000 است، کامپایلر باید انتخاب کند که e2 را با نوعی نمایش دهد که برای ذخیره مقدار 0x80000000 کافی است، یعنی int بدون علامت.
    بنابراین، اگرچه e1 و e2 هر دو عدد هستند، اما نمایش آنها متفاوت است. به همین دلیل، e1 و e2 به انواع مختلف گسترش می یابند:

    #عبارتند از قالب رشته (int);
    قالب رشته (int بدون امضا)؛ int main() (
    قالب (a1)؛ // قالب تماس (int)
    فرمت (a2)؛ // قالب تماس (int بدون امضا)
    بازگشت 0;
    }

    اولین time format() فراخوانی می شود، آرگومان واقعی به نوع int گسترش می یابد زیرا char برای نشان دادن نوع e1 استفاده می شود و بنابراین تابع فرمت (int) سربارگذاری شده فراخوانی می شود. در فراخوانی دوم، نوع آرگومان واقعی e2 بدون علامت int است و آرگومان به unsigned int گسترش می‌یابد، که باعث می‌شود تابع فرمت بیش از حد (unsigned int) فراخوانی شود. بنابراین، توجه داشته باشید که رفتار دو عدد در رابطه با فرآیند تفکیک اضافه بار می‌تواند متفاوت باشد و به مقادیر عناصری بستگی دارد که نحوه وقوع نوع گسترش را تعیین می‌کنند.

    9.3.3. درباره تبدیل استاندارد بیشتر بدانید

    پنج نوع تبدیل استاندارد وجود دارد که عبارتند از:

    1. تبدیل نوع عدد صحیح: ریخته‌گری از نوع عدد صحیح یا شمارش به هر نوع عدد صحیح دیگر (به استثنای تبدیل‌هایی که در بالا به عنوان پسوند نوع دسته‌بندی شدند).
    2. تبدیل نوع ممیز شناور: ریخته‌گری از هر نوع ممیز شناور به هر نوع ممیز شناور دیگری (به استثنای تبدیل‌هایی که در بالا به عنوان پسوند نوع دسته‌بندی شدند).
    3. تبدیل بین یک نوع عدد صحیح و یک نوع ممیز شناور: ریخته گری از هر نوع ممیز شناور به هر نوع عدد صحیح یا بالعکس.
    4. تبدیل اشاره گر: ریختن مقدار صحیح 0 به یک نوع اشاره گر یا تبدیل یک اشاره گر از هر نوع به یک نوع void*.
    5. تبدیل به نوع bool: ریخته گری از هر نوع عدد صحیح، نوع ممیز شناور، نوع برشماری، یا نوع اشاره گر به نوع bool.

    در اینجا چند نمونه آورده شده است:

    چاپ باطل خارجی (void*); چاپ خالی خارجی (دوگانه)؛ int main() (
    int i;
    چاپ (i)؛ // مطابق چاپ (دوبار);
    // i تحت تبدیل استاندارد از int به double قرار می گیرد
    چاپ (&i); // مطابق با print(void*);
    // &i تحت تبدیل استاندارد قرار می گیرد
    // از int* تا void*
    بازگشت 0;
    }

    تبدیل های متعلق به گروه های 1، 2 و 3 به طور بالقوه خطرناک هستند زیرا نوع هدف ممکن است تمام مقادیر نوع منبع را نشان ندهد. به عنوان مثال، شناورها نمی توانند به اندازه کافی همه مقادیر int را نشان دهند. به همین دلیل است که تبدیل های موجود در این گروه ها به عنوان تبدیل های استاندارد و نه پسوند نوع دسته بندی می شوند.

    int i; voidcalc(float); int main() ( calc(i)؛ // تبدیل استاندارد بین یک نوع عدد صحیح و یک نوع ممیز شناور // به طور بالقوه خطرناک است بسته به // مقدار i بازگشت 0؛ )

    هنگامی که تابع calc() فراخوانی می شود، تبدیل استاندارد از نوع عدد صحیح int به نوع شناور float اعمال می شود. بسته به مقدار متغیر i، ممکن است امکان ذخیره آن به عنوان یک شناور بدون از دست دادن دقت وجود نداشته باشد.
    فرض بر این است که همه تغییرات استاندارد نیاز به کار یکسانی دارند. برای مثال، تبدیل از char به char بدون علامت اولویتی بالاتر از تبدیل از char به double ندارد. نزدیکی انواع در نظر گرفته نشده است. اگر دو تابع ایجاد شده نیاز به تبدیل استاندارد آرگومان واقعی برای مطابقت داشته باشند، آنگاه فراخوانی مبهم در نظر گرفته می شود و توسط کامپایلر به عنوان یک خطا پرچم گذاری می شود. به عنوان مثال، با توجه به دو تابع اضافه بار:

    extern void manip(long); extern void manip(float);

    پس تماس زیر مبهم است:

    int main() ( manip(3.14)؛ // خطا: ابهام // manip(float) بهتر از manip(int) بازگشت 0 نیست؛ )

    ثابت 3.14 از نوع double است. با کمک یک یا آن تبدیل استاندارد، می توان یک مکاتبه با هر یک از توابع اضافه بار ایجاد کرد. از آنجایی که دو تغییر منجر به هدف وجود دارد، تماس مبهم در نظر گرفته می شود. هیچ یک از تحولات بر دیگری اولویت ندارد. برنامه نویس می تواند ابهام را با ریخته گری نوع صریح حل کند:

    دستکاری (static_cast (3.14))؛ // دستکاری (طولانی)

    یا با استفاده از پسوندی که نشان می دهد ثابت از نوع float است:

    دستکاری (3.14F))؛ // دستکاری (شناور)

    در اینجا چند نمونه دیگر از تماس‌های مبهم وجود دارد که به‌عنوان خطا علامت‌گذاری شده‌اند، زیرا با چندین تابع بارگذاری شده مطابقت دارند:

    extern void farith(unsigned int); extern void farith(float); int main() (
    // هر یک از تماس های زیر مبهم است
    farith ("a"); // آرگومان از نوع char است
    farith(0); // آرگومان از نوع int است
    farith(2ul); // آرگومان از نوع unsigned long است
    farith(3.14159); // آرگومان از نوع double است
    فریث (درست)؛ // آرگومان از نوع bool است
    }

    تبدیل نشانگرهای استاندارد گاهی اوقات غیر شهودی هستند. به طور خاص، مقدار 0 به یک اشاره گر به هر نوع ریخته می شود. اشاره گر به دست آمده تهی نامیده می شود. مقدار 0 را می توان به عنوان یک عبارت ثابت از یک نوع عدد صحیح نشان داد:

    مجموعه خالی (int*); int main() (
    // تبدیل اشاره گر از 0 به int* روی آرگومان ها اعمال شد
    // در هر دو تماس
    مجموعه (0L)؛
    مجموعه (0x00)؛
    بازگشت 0;
    }

    عبارت ثابت 0L (مقدار 0 از نوع long int) و عبارت ثابت 0x00 (مقدار صحیح هگزا دسیمال 0) از نوع عدد صحیح هستند و بنابراین می توانند به یک اشاره گر تهی از نوع int* تبدیل شوند.
    اما از آنجایی که enum ها انواع عدد صحیح نیستند، عنصر برابر با 0 به یک نوع اشاره گر فرستاده نمی شود:

    Enum EN ( zr = 0 ); set(zr); // خطا: zr را نمی توان به نوع int* تبدیل کرد

    فراخوانی تابع set() یک خطا است زیرا هیچ تبدیلی بین مقدار zr عنصر شمارش و پارامتر رسمی از نوع int* وجود ندارد، حتی اگر zr 0 باشد.
    توجه داشته باشید که عبارت ثابت 0 از نوع int است. برای فرستادن آن به یک نوع اشاره گر، یک تبدیل استاندارد مورد نیاز است. اگر تابعی با پارامتر رسمی از نوع int در مجموعه توابع اضافه بار وجود داشته باشد، در صورتی که آرگومان واقعی 0 باشد، اضافه بار به نفع آن مجاز خواهد بود:

    چاپ خالی (int); voidprint(void*); مجموعه خالی (const char*);
    مجموعه خالی (char*); int main()(
    چاپ (0); // به نام print(int);
    set(0); // گنگ
    بازگشت 0;
    }

    فراخوانی برای چاپ (int) یک تطابق دقیق است، در حالی که فراخوانی برای چاپ (void*) نیاز به ارسال مقدار 0 به یک نوع اشاره گر دارد. از آنجایی که تطبیق بهتر از تبدیل است، برای حل این فراخوانی، شخص انتخاب می کند عملکرد چاپ(int). فراخوانی set() مبهم است، زیرا 0 با اعمال تبدیل استاندارد با پارامترهای رسمی هر دو تابع سربارگذاری شده مطابقت دارد. از آنجایی که هر دو عملکرد به یک اندازه خوب هستند، یک ابهام برطرف شده است.
    آخرین تبدیل اشاره گر ممکن به شما امکان می دهد یک اشاره گر از هر نوع را به نوع void* ارسال کنید، زیرا void* یک اشاره گر عمومی برای هر نوع داده ای است. در اینجا چند نمونه آورده شده است:

    #عبارتند از extern void reset(void *); void func(int *pi، رشته *ps) (
    // ...
    تنظیم مجدد (pi)؛ // تبدیل اشاره گر: int* به void*
    /// ...
    تنظیم مجدد (ps)؛ // تبدیل اشاره گر: رشته* به void*
    }

    فقط اشاره‌گرها به انواع داده‌ها را می‌توان با استفاده از تبدیل استاندارد به void* فرستاد، اشاره‌گرها به توابع را نمی‌توان به این روش انجام داد:

    Typedef int(*PFV)(); موارد تست PFV خارجی; // آرایه ای از اشاره گرها به توابع extern void reset(void *); int main() (
    // ...
    تنظیم مجدد (متن متن)؛ // خطا: بدون تبدیل استاندارد
    // بین int(*)() و void*
    بازگشت 0;
    }

    9.3.4. پیوندها

    آرگومان واقعی یا پارامتر رسمی یک تابع می تواند ارجاع باشد. این چگونه بر قوانین تبدیل نوع تأثیر می گذارد؟
    در نظر بگیرید که چه اتفاقی می افتد زمانی که مرجع یک استدلال واقعی است. نوع آن هرگز یک نوع مرجع نیست. آرگومان مرجع به عنوان یک مقدار l در نظر گرفته می شود که نوع آن با نوع شی مربوطه یکسان است:

    int i; int&ri = i; voidprint(int); int main() (
    چاپ (i)؛ // آرگومان lvalue از نوع int است
    print(ri); // یکسان
    بازگشت 0;
    }

    آرگومان واقعی در هر دو فراخوانی از نوع int است. استفاده از یک مرجع برای ارسال آن در فراخوانی دوم بر نوع خود آرگومان تأثیری ندارد.
    تبدیل ها و پسوندهای نوع استاندارد در نظر گرفته شده توسط کامپایلر زمانی که آرگومان واقعی ارجاع به نوع T باشد و زمانی که خود از آن نوع باشد یکسان است. مثلا:

    int i; int&ri = i; voidcalc (دو برابر)؛ int main() (
    calc (i)؛ // تبدیل استاندارد بین نوع عدد صحیح
    // و نوع ممیز شناور
    کالک (ri); // یکسان
    بازگشت 0;
    }

    و چگونه پارامتر مرجع رسمی بر تبدیل های اعمال شده در آرگومان واقعی تأثیر می گذارد؟ مقایسه نتایج زیر را به دست می دهد:

    • آرگومان واقعی به عنوان مقداردهی اولیه پارامتر مرجع مناسب است. در این صورت می گوییم که یک تطابق دقیق بین آنها وجود دارد: void swap(int &, int &); دستکاری خالی (int i1، int i2) (
      // ...
      مبادله (i1, i2)؛ // صحیح: تعویض تماس (int &، int &)
      // ...
      بازگشت 0;
      }
    • آرگومان واقعی نمی تواند پارامتر مرجع را مقداردهی اولیه کند. در چنین شرایطی، مطابقت دقیقی وجود ندارد و نمی توان از آرگومان برای فراخوانی تابع استفاده کرد. مثلا:
    • int obj; void fred (دو و); int main() (frd(obj); // خطا: پارامتر باید از نوع const double باشد و 0; )
    • فراخوانی تابع frd() یک خطا است. آرگومان واقعی از نوع int است و باید به نوع double تبدیل شود تا با پارامتر مرجع رسمی مطابقت داشته باشد. نتیجه این تبدیل یک متغیر موقت است. از آنجایی که یک مرجع مشخص کننده const ندارد، نمی توان از چنین متغیرهایی برای مقداردهی اولیه آن استفاده کرد.
      در اینجا مثال دیگری وجود دارد که در آن هیچ تطابقی بین پارامتر مرجع رسمی و آرگومان واقعی وجود ندارد:
    • کلاس B؛ void takeB(B&); BgiveB(); int main() (
      takeB(giveB()); // خطا: پارامتر باید از نوع const B & باشد
      بازگشت 0;
      }
    • فراخوانی تابع takeB() یک خطا است. آرگومان واقعی مقدار بازگشتی است، یعنی. یک متغیر موقت که نمی تواند برای مقداردهی اولیه یک مرجع بدون مشخص کننده const استفاده شود.
      در هر دو مورد، می بینیم که اگر پارامتر مرجع رسمی دارای مشخص کننده const باشد، می توان تطابق دقیقی بین آن و آرگومان واقعی ایجاد کرد.

    توجه داشته باشید که هم تبدیل l-value به r-value و هم مقدار اولیه مرجع مطابقت دقیقی در نظر گرفته می شوند. در این مثال، اولین فراخوانی تابع منجر به یک خطا می شود:

    چاپ خالی (int); voidprint(int&); intiobj;
    int &ri = iobj; int main() (
    چاپ (iobj); // خطا: ابهام
    print(ri); // خطا: ابهام
    چاپ (86); // صحیح: call print(int)
    بازگشت 0;
    }

    شی iobj یک آرگومان است که می تواند به هر دو تابع print () نگاشت شود، یعنی فراخوانی مبهم است. همین امر در مورد خط بعدی نیز صدق می کند، جایی که مرجع ri نشان دهنده شیء مربوط به هر دو تابع print() است. با تماس سوم اما همه چیز مرتب است. برای او، print(int&) به خوبی تثبیت نشده است. یک عدد صحیح یک مقدار r است، بنابراین نمی تواند پارامتر مرجع را مقداردهی اولیه کند. تنها تابع ایجاد شده برای فراخوانی print(86) print(int) است، به همین دلیل است که در وضوح اضافه بار انتخاب می شود.
    به طور خلاصه، اگر آرگومان رسمی یک مرجع باشد، اگر آرگومان واقعی بتواند مرجع را مقداردهی اولیه کند، آرگومان واقعی مطابقت دقیقی دارد و نه در غیر این صورت.

    تمرین 9.6

    دو تبدیل بی اهمیتی که هنگام ایجاد یک تطابق دقیق مجاز هستند نام ببرید.

    تمرین 9.7

    رتبه هر یک از تبدیل‌های آرگومان در فراخوانی تابع زیر چقدر است:

    (الف) چاپ خالی (int *، int)؛ int arr; print(arr, 6); // فراخوانی تابع (b) void manip(int, int); manip ("a"، "z"); // فراخوانی تابع (c) int calc(int, int); دوبج; double = calc(55.4, dobj) // فراخوانی تابع (d) void set(const int *); int*pi; set(pi); // فراخوانی تابع

    تمرین 9.8

    کدام یک از این فراخوانی ها اشتباه است زیرا هیچ تبدیلی بین نوع آرگومان واقعی و پارامتر رسمی وجود ندارد:

    (الف) enum Stat( Fail, Pass )؛ آزمون باطل (Stat); متن (0); // فراخوانی تابع (ب) void reset(void *); تنظیم مجدد (0); // فراخوانی تابع (c) void set(void *); int*pi; set(pi); // فراخوانی تابع (د) #include فهرست opera(); void print(oper()); // فراخوانی تابع (e) void print(const int); intiobj; چاپ (iobj); // فراخوانی تابع

    9.4. جزئیات وضوح عملکرد اضافه بار

    قبلاً در بخش 9.2 اشاره کردیم که فرآیند تفکیک اضافه بار تابع شامل سه مرحله است:

    1. مجموعه ای از توابع کاندید را برای حل یک فراخوان داده شده و همچنین ویژگی های لیست آرگومان واقعی تنظیم کنید.
    2. از میان مجموعه ای از نامزدها، توابع تعیین شده را انتخاب کنید - آنهایی که می توان آنها را با لیست معینی از آرگومان های واقعی، با در نظر گرفتن تعداد و انواع آنها فراخوانی کرد.
    3. با رتبه‌بندی تبدیل‌هایی که باید روی آرگومان‌های واقعی اعمال شوند تا با پارامترهای رسمی تابع تعیین‌شده مطابقت داشته باشند، تابعی را انتخاب کنید که به بهترین شکل با فراخوانی مطابقت دارد.

    ما اکنون آماده هستیم تا این مراحل را با جزئیات بیشتر بررسی کنیم.

    9.4.1. توابع نامزد

    تابع کاندید تابعی است که نامی مشابه با تابع فراخوانی شده دارد. نامزدها به دو صورت انتخاب می شوند:

    • اعلان تابع در نقطه تماس قابل مشاهده است. در مثال زیر
      void f(); void f(int); void f (دو برابر، دو برابر = 3.4)؛ void f(char*, char*); int main() (
      f(5.6); // چهار نامزد برای حل این تماس وجود دارد
      بازگشت 0;
      }
    • هر چهار تابع f() این شرط را برآورده می کنند. بنابراین، مجموعه نامزدها شامل چهار عنصر است.
    • اگر نوع آرگومان واقعی در فضای نامی اعلان شده باشد، آنگاه توابع عضو این فضا که همنام تابع فراخوانی شده هستند به مجموعه نامزد اضافه می شوند: فضای نام NS ( کلاس C ( /* ... */ ); void takeC(C&);) // type cobj یک کلاس C است که در فضای نام NS اعلام شده است.
      NS::Cobj; int main() (
      // هیچ یک از توابع takeC() در نقطه تماس قابل مشاهده نیست
      takeC(cobj); // صحیح: NS::takeC(C&) فراخوانی می شود،
      // چون آرگومان از نوع NS::C است، بنابراین
      // تابع takeC() در نظر گرفته شده است،
      // در فضای نام NS اعلام شده است
      بازگشت 0;
      }

    بنابراین، مجموعه کاندیداها، اتحاد مجموعه ای از توابع قابل مشاهده در نقطه فراخوانی و مجموعه ای از توابع اعلام شده در فضای نام مشابه با انواع آرگومان واقعی است.
    هنگام شناسایی مجموعه ای از توابع بیش از حد بارگذاری شده که در نقطه تماس قابل مشاهده هستند، قوانینی که قبلاً در مورد آنها صحبت شد اعمال می شود.
    تابعی که در یک محدوده تودرتو اعلام شده است، به جای بارگذاری بیش از حد، تابع همنام را در محدوده بیرونی پنهان می کند. در چنین شرایطی، تنها توابع از داخل محدوده تودرتو، کاندید خواهند بود، یعنی. آنهایی که هنگام فراخوانی پنهان نمی شوند. در مثال زیر، توابع کاندید قابل مشاهده در نقطه تماس عبارتند از: format(double) و format(char*):

    فرمت Char*(int); void g() (char *format(double)؛ char* format(char*); format(3); // calling format(double)
    }

    از آنجا که فرمت (int) اعلام شده در دامنه جهانی پنهان است، در مجموعه توابع کاندید گنجانده نشده است.
    کاندیداها را می توان با استفاده از اعلان های قابل مشاهده در محل فراخوان معرفی کرد:

    فضای نام libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char); void func()
    {
    // توابع از فضای نام نامرئی هستند
    // هر سه فراخوانی به نفع تابع سراسری max حل می شوند (char, char)
    max(87, 65);
    حداکثر (35.5، 76.6);
    max ("J"، "L");
    }

    توابع max() تعریف شده در فضای نام libs_R_us در نقطه تماس نامرئی هستند. تنها تابع قابل مشاهده max() از دامنه جهانی است. فقط در مجموعه توابع کاندید گنجانده شده است و در هر یک از سه فراخوانی تابع () فراخوانی می شود. می‌توانیم از اعلان استفاده برای نمایش توابع max() از فضای نام libs_R_us استفاده کنیم. اعلان استفاده را کجا قرار دهیم؟ اگر آن را در محدوده جهانی قرار دهید:

    Char max (char، char); با استفاده از libs_R_us::max; // با استفاده از اعلان

    سپس توابع max() از libs_R_us به مجموعه توابع سربارگذاری شده اضافه می شوند که از قبل حاوی max() اعلام شده در محدوده جهانی است. اکنون هر سه تابع در داخل func() قابل مشاهده هستند و کاندید می شوند. در این شرایط، فراخوانی های func() به صورت زیر حل می شوند:

    Void func() (max(87, 65); // به نام libs_R_us::max(int, int) max("J", "L"); // نامیده می شود::max(char، char) )

    اما اگر همانطور که در این مثال نشان داده شده است، یک اعلان استفاده را به محدوده محلی تابع ()func تزریق کنیم، چه اتفاقی می افتد؟

    void func() (// using-declaration با استفاده از libs_R_us::max; // همان تابع فوق را فراخوانی می کند
    }

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

    Libs_R_us::max(int, int); libs_R_us::max(double, double);

    آنها نامزد هستند. اکنون فراخوانی های func() به صورت زیر حل می شوند:

    Void func() (// using-declaration // تابع سراسری max(char, char) با استفاده از libs_R_us::max؛ max(87, 65) پنهان می شود؛ // libs_R_us::max(int, int) فراخوانی می شود
    حداکثر (35.5، 76.6); // libs_R_us::max(double, double) فراخوانی می شود
    max ("J"، "L"); // فراخوانی libs_R_us::max(int, int)
    }

    دستورالعمل های استفاده نیز بر ترکیب مجموعه توابع کاندید تأثیر می گذارد. فرض کنید تصمیم داریم از آنها استفاده کنیم تا توابع max() را از فضای نام libs_R_us در func() قابل مشاهده کنیم. اگر دستور زیر را در محدوده جهانی قرار دهیم، مجموعه توابع کاندید شامل تابع سراسری max(char، char) و توابع max(int، int) و max(double، double) اعلام شده در libs_R_us خواهد بود:

    فضای نام libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char);
    با استفاده از فضای نام libs_R_us; // با استفاده از دستور void func()
    {
    max(87, 65); // فراخوانی libs_R_us::max(int, int)
    حداکثر (35.5، 76.6); // libs_R_us::max(double, double) فراخوانی می شود
    }

    اگر دستورالعمل استفاده را مانند مثال زیر در محدوده محلی قرار دهید چه اتفاقی می افتد؟

    Void func() (// using-directive using namespace libs_R_us; // همان تابع فوق را فراخوانی می کند
    }

    کدام یک از توابع max() در میان نامزدها قرار خواهد گرفت؟ به یاد بیاورید که یک دستورالعمل استفاده، اعضای فضای نام را به گونه‌ای قابل مشاهده می‌کند که گویی خارج از آن فضا، در نقطه‌ای که چنین دستورالعملی قرار می‌گیرد، اعلام شده‌اند. در مثال ما، اعضای libs_R_us در محدوده محلی تابع ()func قابل مشاهده هستند، گویی خارج از فضا - در محدوده جهانی - اعلام شده اند. نتیجه می شود که مجموعه توابع اضافه بار قابل مشاهده در داخل func() مانند قبل است، یعنی. شامل می شود

    حداکثر (کاراکتر، کاراکتر)؛ libs_R_us::max(int, int); libs_R_us::max(double, double);

    یک دستورالعمل use در محدوده محلی یا جهانی ظاهر می شود، وضوح فراخوانی ها به تابع ()func تحت تأثیر قرار نمی گیرد:

    Void func() (با استفاده از فضای نام libs_R_us; max(87, 65)؛ // فراخوانی libs_R_us::max(int, int)
    حداکثر (35.5، 76.6); // libs_R_us::max(double, double) فراخوانی می شود
    max ("J"، "L"); // نامیده شد:: max(int, int)
    }

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

    فضای نام BasicLib ( int print(int); double print (double); ) namespace matrixLib ( class matrix (/* ... */)؛ void print(const maxtrix &); ) void display() (با استفاده از basicLib::print matrixLib::ماتریس mObj;
    چاپ (mObj); // فراخوانی maxtrixLib::print(const maxtrix &) print(87); // basicLib::print(const maxtrix &) فراخوانی می شود
    }

    نامزدهای print(mObj) عبارت‌اند از استفاده از اعلان‌های داخل display() توابع basicLib::print(int) و basicLib::print(double) زیرا در نقطه تماس قابل مشاهده هستند. از آنجایی که آرگومان تابع واقعی از نوع matrixLib::matrix است، تابع print() اعلام شده در فضای نام matrixLib نیز یک نامزد خواهد بود. توابع کاندید برای چاپ (87) چیست؟ فقط BasicLib::print(int) و basicLib::print(double) در نقطه تماس قابل مشاهده است. از آنجایی که آرگومان از نوع int است، هیچ فضای نام اضافی در جستجوی سایر نامزدها در نظر گرفته نمی شود.

    9.4.2. ویژگی های تثبیت شده

    یک کارکرد تثبیت شده یکی از نامزدها است. لیست پارامترهای رسمی آن یا تعداد عناصر مشابه فهرست آرگومان های واقعی تابع فراخوانی شده دارد یا بیشتر. در مورد دوم، برای گزینه های اضافیمقادیر پیش فرض داده شده است، در غیر این صورت نمی توان تابع را با تعداد آرگومان های داده شده فراخوانی کرد. برای اینکه یک تابع پایدار در نظر گرفته شود، باید از هر آرگومان واقعی به نوع پارامتر رسمی مربوطه تبدیل شود. (چنین تحولاتی در بخش 9.3 مورد بحث قرار گرفت.)
    در مثال زیر، فراخوانی f(5.6) دو تابع ثابت دارد: f(int) و f(double).

    void f(); void f(int); void f (دو برابر); void f(char*, char*); int main() (
    f(5.6); // 2 تابع ایجاد شده: f(int) و f(double)
    بازگشت 0;
    }

    تابع f(int) زنده ماند زیرا فقط یک پارامتر رسمی دارد که با تعداد آرگومان های واقعی در فراخوانی مطابقت دارد. علاوه بر این، یک تبدیل استاندارد یک آرگومان از نوع double به int وجود دارد. تابع f(double) نیز زنده ماند. همچنین دارای یک پارامتر از نوع double است و دقیقاً با آرگومان واقعی مطابقت دارد. توابع کاندید f() و f(char*، char*) از لیست توابع باقیمانده حذف می شوند زیرا نمی توان آنها را با یک آرگومان فراخوانی کرد.
    در مثال زیر، تنها تابع ایجاد شده برای فراخوانی format(3) format(double) است. اگرچه فرمت(char*) نامزد را می توان با یک آرگومان فراخوانی کرد، اما هیچ تبدیلی از نوع آرگومان int واقعی به نوع پارامتر رسمی char* وجود ندارد و بنابراین نمی توان تابع را به خوبی تثبیت شده در نظر گرفت.

    فرمت Char*(int); void g() (// قالب تابع سراسری(int) فرمت char* مخفی است(دوبار)؛ char* قالب(char*)؛ قالب(3)؛ // تنها یک تابع ثابت وجود دارد: format(double) )

    در مثال زیر، هر سه تابع کاندید در نهایت قادر به فراخوانی max() در داخل func() هستند. همه آنها را می توان با دو آرگومان فراخوانی کرد. از آنجایی که آرگومان های واقعی از نوع int هستند، دقیقاً با پارامترهای رسمی تابع libs_R_us::max(int, int) مطابقت دارند و می توانند به انواع پارامترهای تابع libs_R_us::max(double, double) ارسال شوند. با تبدیل اعداد صحیح به شناور و همچنین به انواع پارامترهای تابع libs_R_us::max(char, char) از طریق تبدیل نوع صحیح.


    با استفاده از libs_R_us::max; char max (char, char);
    void func()
    {
    // هر سه تابع max() به خوبی تثبیت شده اند
    max(87, 65); // با استفاده از libs_R_us::max(int, int) فراخوانی شد
    }

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

    خارجی دو دقیقه (دو، دو برابر); extern double min(char*, int); void func()
    {
    // یک تابع کاندید حداقل (دو، دوتایی)
    min(87, 65); // حداقل تماس (دوبل، دوبل)
    }

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

    چاپ باطل (int بدون امضا)؛ voidprint(char*); voidprint(char); int*ip;
    کلاس SmallInt ( /* ... */ );
    SmallInt si; int main() (
    چاپ (IP)؛ // خطا: هیچ توابعی ایجاد نشده: مطابقت پیدا نشد
    چاپ (si); // خطا: هیچ توابعی ایجاد نشده: مطابقت پیدا نشد
    بازگشت 0;
    }

    9.4.3. بهترین ویژگی تثبیت شده

    بهترین توابع ایجاد شده در نظر گرفته می شود که پارامترهای رسمی آن بیشتر با انواع آرگومان های واقعی مطابقت دارد. برای چنین تابعی، تبدیل‌های نوع اعمال شده برای هر آرگومان رتبه‌بندی می‌شوند تا مشخص شود که چقدر با پارامتر مطابقت دارد. (بخش 6.2 تبدیل نوع پشتیبانی شده را توضیح می دهد.) بهترین تابع تثبیت شده تابعی است که به طور همزمان دو شرط را برآورده می کند:

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

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

    Int arr; void putValues(const int *); int main() (
    putValues(arr); // 2 تبدیل مورد نیاز است
    // تبدیل آرایه به اشاره گر + مشخص کننده
    بازگشت 0;
    }

    برای ریختن آرگومان arr از نوع "آرایه سه اینتی" به نوع "اشاره گر به const int"، دنباله ای از تبدیل ها اعمال می شود:

    1. تبدیل آرایه به اشاره گر، که یک آرایه از سه اینت را به یک اشاره گر به int تبدیل می کند.
    2. تبدیل مشخص کننده که یک اشاره گر به int را به یک اشاره گر به const int تبدیل می کند.

    بنابراین، صحیح‌تر است که بگوییم برای اینکه آرگومان واقعی را به نوع پارامتر رسمی تابع تعیین‌شده وارد کنیم، به دنباله‌ای از تبدیل‌ها نیاز است. از آنجایی که نه یک، بلکه چندین تبدیل اعمال می شود، مرحله سوم فرآیند تفکیک بیش از حد تابع در واقع توالی تبدیل ها را رتبه بندی می کند.
    رتبه چنین دنباله ای به عنوان رتبه بدترین تحولات موجود در آن در نظر گرفته می شود. همانطور که در بخش 9.2 توضیح داده شد، تبدیل‌های نوع به صورت زیر رتبه‌بندی می‌شوند: تطابق دقیق بهتر از پسوند نوع، و پسوند نوع بهتر از تبدیل استاندارد است. در مثال قبلی، هر دو تغییر دارای رتبه مطابقت دقیق هستند. بنابراین، کل دنباله دارای رتبه یکسانی است.
    چنین مجموعه ای متشکل از چندین تبدیل است که به ترتیب نشان داده شده اعمال می شود:

    تبدیل l-value -> نوع پسوند یا تبدیل استاندارد -> تبدیل مشخص کننده

    عبارت تبدیل l-value به سه مورد اول از تبدیل های تطبیق دقیق مورد بحث در بخش 9.2 اشاره دارد: تبدیل مقدار l به مقدار r، تبدیل آرایه به اشاره گر و تبدیل تابع به اشاره گر. دنباله ای از تبدیل ها شامل تبدیل صفر یا یک l-value، به دنبال صفر یا یک نوع پسوند یا تبدیل استاندارد و در نهایت صفر یا یک تبدیل مشخص کننده است. فقط یک تبدیل از هر نوع را می توان برای ارسال یک استدلال واقعی به نوع پارامتر رسمی اعمال کرد.

    دنباله توصیف شده، دنباله تبدیل های استاندارد نامیده می شود. یک سکانس نیز وجود دارد تعریف شده توسط کاربرتبدیل، که با یک تابع مبدل که عضوی از کلاس است مرتبط است. (مبدل ها و توالی تبدیل های تعریف شده توسط کاربر در فصل 15 مورد بحث قرار گرفته اند.)

    توالی هایی که آرگومان های واقعی در مثال زیر تغییر می کنند کدامند؟

    فضای نام libs_R_us ( int max(int, int); double max(double, double); ) // using-declaration
    با استفاده از libs_R_us::max; void func()
    {
    char c1, c2;
    max(c1, c2); // فراخوانی libs_R_us::max(int, int)
    }

    آرگومان های فراخوانی تابع max() از نوع char هستند. توالی تبدیل‌های آرگومان هنگام فراخوانی تابع libs_R_us::max(int,int) به شرح زیر است:

    1a. از آنجایی که آرگومان ها توسط مقدار ارسال می شوند، تبدیل مقدار l به یک مقدار r، مقادیر آرگومان های c1 و c2 را استخراج می کند.

    2a. آرگومان ها با استفاده از نوع expansion از char به int تبدیل می شوند.
    توالی تبدیل‌های آرگومان هنگام فراخوانی تابع libs_R_us::max(double,double) به شرح زیر است:
    1b. با تبدیل l-value به r-value، مقادیر آرگومان های c1 و c2 استخراج می شوند.

    2b. تبدیل استاندارد بین انواع عدد صحیح و شناور، آرگومان‌ها را از نوع char به نوع double می‌فرستد.

    رتبه دنباله اول یک پسوند نوع است (بدترین تغییر اعمال شده)، در حالی که رتبه دوم یک تبدیل استاندارد است. از آنجایی که پسوند نوع بهتر از تبدیل نوع است، تابع libs_R_us::max(int,int) به عنوان بهترین مناسب برای این فراخوانی انتخاب شده است.
    اگر دنباله‌های رتبه‌بندی تبدیل‌های آرگومان نتوانند یک تابع ثابت را نشان دهند، آنگاه فراخوانی مبهم در نظر گرفته می‌شود. در این مثال، هر دو فراخوانی calc() به دنباله زیر نیاز دارند:

    1. l-value را به r-value تبدیل کنید تا مقادیر آرگومان i و j را استخراج کنید.
    2. یک تبدیل استاندارد برای ریختن آرگومان های واقعی به پارامترهای رسمی مربوطه.

    از آنجایی که نمی توان گفت کدام یک از این دنباله ها بهتر از دیگری است، تماس مبهم است:

    int i, j; extern long calc (طولانی، بلند)؛ extern double calc (دو، دو برابر)؛ void jj() (// خطا: ابهام، بهترین تطابق وجود ندارد
    calc (i, j);
    }

    تبدیل مشخص کننده (افزودن یک مشخص کننده ثابت یا فرار به نوعی که نشانگر را نشان می دهد) دارای رتبه مطابقت دقیق است. با این حال، اگر دو دنباله از تبدیل تنها در این است که یکی از آنها دارای یک تبدیل مشخص کننده اضافی در پایان باشد، آنگاه دنباله بدون آن بهتر در نظر گرفته می شود. مثلا:

    void reset(int *); void reset (const int *); int*pi; int main() (
    تنظیم مجدد (pi)؛ // بدون تبدیل مشخص کننده ها بهتر است:
    // Reset را انتخاب کنید (int *)
    بازگشت 0;
    }

    دنباله تبدیل‌های استاندارد اعمال شده به آرگومان واقعی برای اولین تابع کاندید reset(int*) دقیقاً مطابقت دارد، فقط برای استخراج مقدار آرگومان از l-value به r-value نیاز است. برای بازنشانی تابع نامزد دوم (const int *)، تبدیل l-value به r-value نیز اعمال می‌شود، اما با تبدیل مشخص‌کننده نیز دنبال می‌شود تا مقدار حاصل از اشاره‌گر به int به اشاره‌گر به const int ارسال شود. هر دو سکانس کاملاً مطابقت دارند، اما هیچ ابهامی وجود ندارد. از آنجایی که دنباله دوم با وجود یک تبدیل مشخص کننده در انتها با توالی اول متفاوت است، دنباله بدون چنین تبدیلی بهترین در نظر گرفته می شود. بنابراین، reset(int*) بهترین تابع ایستاده است.
    در اینجا مثال دیگری وجود دارد که در آن مشخص کننده های ریخته گری بر روی انتخاب توالی تأثیر می گذارد:

    استخراج int(void*);
    int extract(const void *);

    int main() (
    استخراج (pi)؛ // انتخاب Extract(void *)
    بازگشت 0;
    }

    دو تابع ایجاد شده برای فراخوانی در اینجا وجود دارد: extract(void*) و extract (const void*). دنباله تبدیل ها برای تابع استخراج (void*) شامل تبدیل مقدار l به مقدار r برای استخراج است. مقادیر آرگومان، به دنبال تبدیل اشاره گر استاندارد: از یک اشاره گر به int به یک اشاره گر به void. برای تابع استخراج (const void*)، این دنباله با تبدیل اضافی مشخص‌کننده‌ها برای فرستادن نوع نتیجه از یک اشاره‌گر به void به یک اشاره‌گر برای const void، با اولین دنباله متفاوت است. از آنجایی که توالی‌ها فقط با این تبدیل متفاوت هستند، اولین مورد به عنوان مناسب‌تر انتخاب می‌شود و بنابراین، تابع استخراج (const void*) بهترین حالت ایستاده است.
    مشخص‌کننده‌های const و volatile نیز بر رتبه‌بندی اولیه‌سازی پارامترهای مرجع تأثیر می‌گذارند. اگر دو چنین مقداردهی اولیه فقط در اضافه کردن یک مشخص کننده const و volatile با هم تفاوت داشته باشند، در این صورت مقدار اولیه بدون مشخص کننده اضافی در وضوح اضافه بار بهتر در نظر گرفته می شود:

    #عبارتند از دستکاری خالی (بردار &); void manip(بردار const &); بردار f();
    بردار خارجی vec; int main() (
    manip(vec); // انتخاب manip(بردار &)
    manip(f()); // انتخاب manip (بردار const &)
    بازگشت 0;
    }

    در فراخوانی اول، مقداردهی اولیه مرجع برای هر فراخوانی تابع دقیقاً مطابقت دارد. اما این چالش همچنان مبهم نخواهد بود. از آنجایی که هر دو مقدار اولیه یکسان هستند به جز وجود یک مشخصه اضافی اضافی در مورد دوم، مقداردهی اولیه بدون چنین مشخصاتی بهتر در نظر گرفته می شود، بنابراین اضافه بار به نفع دستکاری (بردار) به خوبی تثبیت شده حل خواهد شد. &).
    برای فراخوانی دوم، تنها یک دستکاری تابع به خوبی تثبیت شده وجود دارد (بردار const &). از آنجایی که آرگومان واقعی یک متغیر موقت است که حاوی نتیجه بازگردانده شده توسط f() است، چنین آرگومان یک مقدار r است که نمی تواند برای مقداردهی اولیه پارامتر مرجع رسمی غیر ثابت manip(vector) استفاده شود. &). بنابراین، تنها دستکاری به خوبی تثبیت شده (بردار const &).
    البته توابع می توانند چندین آرگومان واقعی داشته باشند. انتخاب بهترین از بین موارد ایستاده باید با در نظر گرفتن رتبه بندی دنباله های تبدیل همه استدلال ها انجام شود. به یک مثال توجه کنید:

    extern int ff(char*, int); extern int ff(int, int); int main() (ff(0, "a"); // ff(int, int)
    بازگشت 0;
    }

    تابع ff() که دو آرگومان int می گیرد، به دلایل زیر به عنوان بهترین تابع ثابت انتخاب می شود:

    1. استدلال اول او بهتر است. 0 دقیقاً با یک پارامتر int رسمی مطابقت دارد، در حالی که تبدیل اشاره گر استاندارد برای مطابقت با پارامتر نوع char * لازم است.
    2. آرگومان دوم آن نیز همین رتبه را دارد. برای آرگومان "a" از نوع char، برای ایجاد مطابقت با دومین پارامتر رسمی هر یک از دو تابع، باید دنباله ای از تبدیل ها اعمال شود که دارای رتبه پسوند نوع باشد.

    در اینجا یک مثال دیگر وجود دارد:

    int compute(const int&, short); int compute(int&, double); خارجی intiobj;
    int main() (
    محاسبه (iobj، "c"); // محاسبه (int&, double)
    بازگشت 0;
    }

    هر دو تابع compute (const int&, short) و compute (int&, double) زنده ماندند. دومی به دلایل زیر به عنوان بهترین انتخاب می شود:

    1. استدلال اول او بهتر است. مقداردهی اولیه مرجع برای اولین تابع ایجاد شده بدتر است زیرا نیاز به اضافه کردن یک مشخص کننده const دارد که برای تابع دوم مورد نیاز نیست.
    2. آرگومان دوم آن نیز همین رتبه را دارد. برای آرگومان "c" از نوع char، برای ایجاد مطابقت با دومین پارامتر رسمی هر یک از دو تابع، باید دنباله ای از تبدیل ها با رتبه تبدیل استاندارد اعمال شود.

    9.4.4. آرگومان هایی با مقادیر پیش فرض

    داشتن آرگومان هایی با مقادیر پیش فرض می تواند بسیاری از توابع به خوبی تثبیت شده را گسترش دهد. Residual توابعی هستند که با لیست معینی از آرگومان های واقعی فراخوانی می شوند. اما چنین تابعی می تواند پارامترهای رسمی بیشتری نسبت به آرگومان های واقعی ارائه شده داشته باشد، در صورتی که مقداری پیش فرض برای هر پارامتر مشخص نشده باشد:

    extern void ff(int); extern void ff(long, int = 0); int main() (
    ff (2L)؛ // مطابق با ff(long, 0); ff(0, 0); // با ff(long, int) مطابقت می دهد.
    ff(0); // با ff(int);
    ff(3.14); // خطا: ابهام
    }

    برای فراخوانی های اول و سوم، تابع ff() حل می شود، حتی اگر تنها یک آرگومان واقعی ارسال شده باشد. این به دلایل زیر است:

    1. یک مقدار پیش فرض برای پارامتر رسمی دوم وجود دارد.
    2. اولین پارامتر از نوع long دقیقاً با آرگومان واقعی در اولین فراخوانی مطابقت دارد و می تواند با دنباله ای که دارای رتبه تبدیل استاندارد است به نوع آرگومان در فراخوانی سوم ارسال شود.

    آخرین فراخوانی مبهم است زیرا هر دو تابع ایجاد شده را می توان با اعمال تبدیل استاندارد به آرگومان اول انتخاب کرد. تابع ff(int) فقط به دلیل داشتن یک پارامتر ترجیح داده نمی شود.

    تمرین 9.9

    توضیح دهید که چه اتفاقی می‌افتد وقتی اضافه بار را برای فراخوانی compute() در main() حل می‌کنید. نامزدها چه ویژگی هایی دارند؟ کدام یک از آنها بعد از اولین قدم می ایستند؟ چه دنباله ای از تبدیل ها باید به آرگومان واقعی اعمال شود تا با پارامتر رسمی برای هر تابع ایجاد شده مطابقت داشته باشد؟ کدام عملکرد بهترین جایگاه را خواهد داشت؟

    فضای نام primerLib ( void compute(); void compute(const void *); ) با استفاده از primerLib::compute;
    void compute(int);
    محاسبه void (دبل، دو برابر = 3.4)؛
    محاسبه void (char*, char* = 0); int main() (
    محاسبه (0);
    بازگشت 0;
    }

    چه اتفاقی می افتد اگر اعلان استفاده در داخل main() قبل از فراخوانی compute() قرار گیرد؟ به همین سوالات پاسخ دهید.

    اضافه بار تابع تعریف چندین تابع (دو یا بیشتر) با نام یکسان اما پارامترهای متفاوت است. مجموعه پارامترهای توابع اضافه بار ممکن است به ترتیب، تعداد و نوع متفاوت باشند. بنابراین، برای جلوگیری از تکرار نام توابعی که اقدامات مشابه را انجام می دهند، اما با منطق برنامه متفاوت، بارگذاری بیش از حد تابع مورد نیاز است. برای مثال، تابع areaRectangle() را در نظر بگیرید که مساحت یک مستطیل را محاسبه می کند.

    Float areaRectangle(float, float) // تابعی که مساحت مستطیل را با دو پارامتر a(cm) و b(cm) محاسبه می کند (a * b را برمی گرداند؛ // طول اضلاع مستطیل را ضرب می کند و برمی گرداند. محصول حاصل)

    بنابراین، این یک تابع با دو پارامتر از نوع float است و آرگومان های ارسال شده به تابع باید بر حسب سانتی متر باشد، مقدار برگشتی نوع float نیز بر حسب سانتی متر است.

    فرض کنید که داده های اولیه ما (اضلاع مستطیل) به متر و سانتی متر داده شده است، به عنوان مثال: a = 2m 35 cm; b = 1m 86 cm. در این مورد، استفاده از یک تابع با چهار پارامتر راحت خواهد بود. یعنی هر طول اضلاع مستطیل در دو پارامتر متر و سانتی متر به تابع منتقل می شود.

    Float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // تابعی که مساحت مستطیل را با 4 پارامتر a(m) a(cm) محاسبه می کند. b(m) b(cm) (بازگشت (a_m * 100 + a_sm) * (b_m * 100 + b_sm)؛ )

    در بدنه تابع، مقادیری که بر حسب متر عبور داده شده اند (a_m و b_m) به سانتی متر تبدیل می شوند و به مقادیر a_sm b_sm اضافه می شوند و پس از آن مجموع ها را ضرب می کنیم و مساحت مستطیل را بدست می آوریم. در سانتی متر البته امکان تبدیل داده های اصلی به سانتی متر و استفاده از تابع اول وجود داشت، اما اکنون در مورد آن نیست.

    حالا مهمترین چیز این است که ما دو تابع داریم، با امضاهای مختلف، اما نام های یکسان (توابع اضافه بار). امضا ترکیبی از نام تابع با پارامترهای آن است. چگونه این توابع را فراخوانی کنیم؟ و فراخوانی توابع اضافه بار با فراخوانی توابع معمولی تفاوتی ندارد، به عنوان مثال:

    AreaRectangle(32, 43); // تابعی فراخوانی می شود که مساحت یک مستطیل را با دو پارامتر a(cm) و b(cm) areaRectangle(4, 43, 2, 12) محاسبه می کند. // تابعی فراخوانی می شود که مساحت یک مستطیل را با 4 پارامتر a(m) a(cm) محاسبه می کند. b(m) b(cm)

    همانطور که می بینید، کامپایلر انتخاب خواهد کرد عملکرد مورد نظر، تنها امضاهای توابع اضافه بار را تجزیه و تحلیل می کند. با دور زدن اضافه بار تابع، می توان به سادگی یک تابع را با نام دیگری اعلام کرد و کار خود را به خوبی انجام داد. اما تصور کنید اگر به بیش از دو تابع مانند 10 نیاز داشته باشید، چه اتفاقی می‌افتد. و برای هر کدام باید یک نام معنادار پیدا کنید، و سخت‌ترین چیز برای به خاطر سپردن آنهاست. دقیقاً به همین دلیل است که اضافه بار توابع آسان تر و بهتر است، مگر اینکه البته نیازی به این کار وجود داشته باشد. منبعبرنامه ها در زیر نشان داده شده است.

    #include "stdafx.h" #include << "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

    // کد کد::Blocks

    // کد Dev-C++

    #عبارتند از با استفاده از namespace std. // نمونه های اولیه توابع اضافه بار float areaRectangle(float a, float b); float areaRectangle(float a_m, float a_sm, float b_m, float b_sm); int main() (cout<< "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

    نتیجه برنامه در شکل 1 نشان داده شده است.

    اضافه بار عملکرد

    بارگذاری بیش از حد عملیات (اپراتورها، توابع، رویه ها)- در برنامه نویسی - یکی از راه های پیاده سازی چندشکلی، که شامل امکان وجود همزمان در یک محدوده از چندین نوع مختلف یک عملیات (عملگر، تابع یا رویه) است که نام یکسانی دارند، اما در انواع پارامترها متفاوت هستند. که به آنها اعمال می شود.

    واژه شناسی

    اصطلاح "overload" مقاله ردیابی انگلیسی "overloading" است که در ترجمه روسی کتابهای زبان های برنامه نویسی در نیمه اول دهه 1990 ظاهر شد. شاید این بهترین گزینه ترجمه نباشد، زیرا کلمه "بیش از حد" در روسی معنای مشخصی دارد که کاملاً متفاوت از پیشنهادی جدید است ، اما ریشه پیدا کرده و کاملاً گسترده است. در نشریات دوران اتحاد جماهیر شوروی، مکانیسم های مشابه به روسی "تعریف مجدد" یا "تعریف مجدد" عملیات نامیده می شد، اما این گزینه قابل بحث نیست: اختلافات و سردرگمی در ترجمه های انگلیسی "نادیده گرفتن"، "بیش از حد" و "به وجود می آید. دوباره تعریف کن.»

    دلایل ظاهر شدن

    اکثر زبان‌های برنامه‌نویسی اولیه این محدودیت را داشتند که بیش از یک عملیات با همان نام نمی‌توانست در یک برنامه به طور همزمان در دسترس باشد. بر این اساس، تمام توابع و رویه‌های قابل مشاهده در یک نقطه از برنامه باید نام‌های متفاوتی داشته باشند. نام و نام گذاری توابع، رویه ها و عملگرهایی که بخشی از زبان برنامه نویسی هستند توسط برنامه نویس برای نامگذاری توابع، رویه ها و عملگرهای خود قابل استفاده نیستند. در برخی موارد، یک برنامه نویس می تواند شیء برنامه خود را با نام یکی دیگر از موارد موجود ایجاد کند، اما سپس شیء جدید ایجاد شده با قبلی "همپوشانی" پیدا می کند و استفاده همزمان از هر دو گزینه غیرممکن می شود.

    این وضعیت در برخی موارد نسبتاً رایج ناخوشایند است.

    • گاهی اوقات نیاز به توصیف و اعمال عملیات بر روی انواع داده ایجاد شده توسط برنامه نویس وجود دارد که از نظر معنایی معادل آنهایی هستند که قبلاً در زبان موجود است. یک مثال کلاسیک کتابخانه ای برای کار با اعداد مختلط است. آنها، مانند انواع عددی معمولی، از عملیات حسابی پشتیبانی می کنند، و طبیعی است که برای این نوع عملیات "بعلاوه"، "منهای"، "ضرب"، "تقسیم" ایجاد شود، که آنها را با همان علائم عملیاتی مانند سایر اعداد نشان می دهد. انواع ممنوعیت استفاده از عناصر تعریف شده در زبان باعث ایجاد بسیاری از توابع با نام هایی مانند ComplexPlusComplex، IntegerPlusComplex، ComplexMinusFloat و غیره می شود.
    • هنگامی که عملیات به یک معنی برای عملوندهای انواع مختلف اعمال می شود، آنها مجبور به نامگذاری متفاوت می شوند. عدم امکان استفاده از توابع با نام یکسان برای انواع مختلف توابع منجر به نیاز به اختراع نام های مختلف برای یک چیز می شود که باعث سردرگمی و حتی ممکن است منجر به خطا شود. به عنوان مثال، در زبان کلاسیک C، دو نسخه از تابع کتابخانه استاندارد برای یافتن مدول یک عدد وجود دارد: abs() و fabs() - اولی برای یک آرگومان عدد صحیح، دومی برای یک آرگومان واقعی در نظر گرفته شده است. این وضعیت، همراه با بررسی ضعیف نوع C، می‌تواند منجر به یک خطای سخت شود: اگر یک برنامه‌نویس abs(x) را در محاسبات بنویسد، جایی که x یک متغیر واقعی است، برخی از کامپایلرها بدون اخطار کد تولید می‌کنند. x را با حذف قطعات کسری به عدد صحیح تبدیل کنید و مدول را از عدد صحیح بدست آمده محاسبه کنید!

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

    بارگذاری بیش از حد اپراتور به خودی خود صرفاً قند نحوی است، اگرچه حتی به این ترتیب می‌تواند مفید باشد زیرا به توسعه‌دهنده اجازه می‌دهد تا به روشی طبیعی‌تر برنامه‌نویسی کند و باعث می‌شود انواع سفارشی بیشتر شبیه موارد داخلی رفتار کنند. اگر از یک موضع کلی تر به موضوع بپردازیم، می بینیم که ابزارهایی که به شما امکان می دهند زبان را گسترش دهید، آن را با عملیات جدید و ساختارهای نحوی تکمیل کنید (و بارگذاری بیش از حد عملیات یکی از این ابزارها است، همراه با اشیا، ماکروها. ، عملکردها، بسته ها) آن را در حال حاضر به فرازبان تبدیل می کند - وسیله ای برای توصیف زبان هایی که برای وظایف خاص هستند. با کمک آن، می توان برای هر کار خاص که برای آن مناسب تر است، یک پسوند زبان ساخت، که به شما امکان می دهد راه حل آن را به طبیعی ترین، قابل فهم ترین و ساده ترین شکل توصیف کنید. به عنوان مثال، در برنامه برای بارگذاری بیش از حد عملیات: ایجاد یک کتابخانه از انواع پیچیده ریاضی (بردارها، ماتریس ها) و توصیف عملیات با آنها به شکل طبیعی و "ریاضی"، یک "زبان برای عملیات برداری" ایجاد می کند که در آن پیچیدگی محاسبات پنهان است و می توان حل مسائل را از نظر عملیات بردار و ماتریس توصیف کرد، با تمرکز بر ماهیت مسئله، نه روی تکنیک. به همین دلایل بود که زمانی چنین ابزارهایی در زبان Algol-68 گنجانده شد.

    مکانیزم اضافه بار

    پیاده سازی

    بارگذاری بیش از حد اپراتور شامل معرفی دو ویژگی مرتبط به هم در زبان است: توانایی اعلام چندین رویه یا توابع با نام یکسان در یک محدوده، و توانایی توصیف پیاده سازی های خود از عملیات (یعنی علائم عملیات، معمولاً نوشته شده در نماد infix، بین عملوندها). اساساً اجرای آنها بسیار ساده است:

    • برای اجازه دادن به وجود چندین عملیات به یک نام، کافی است قاعده ای را به زبان وارد کنیم که براساس آن یک عملیات (رویه، تابع یا عملگر) توسط کامپایلر نه تنها با نام (نشان) شناسایی می شود، بلکه همچنین با انواع پارامترهای آنها. بنابراین abs(i)، جایی که i به عنوان یک عدد صحیح اعلام می شود، و abs(x)، که در آن x به عنوان واقعی اعلام می شود، دو عملیات متفاوت هستند. اساساً هیچ مشکلی در ارائه چنین تفسیری وجود ندارد.
    • برای فعال کردن تعریف و بازتعریف عملیات، لازم است ساختارهای نحوی مناسبی به زبان وارد شود. گزینه های بسیار زیادی می تواند وجود داشته باشد ، اما در واقع آنها با یکدیگر تفاوتی ندارند ، کافی است به یاد داشته باشید که ورود فرم "<операнд1> <знакОперации> <операнд2>» اساساً شبیه فراخوانی تابع «<знакОперации>(<операнд1>,<операнд2>)". کافی است به برنامه نویس اجازه دهیم تا رفتار عملگرها را در قالب توابع توصیف کند - و مشکل توصیف حل می شود.

    گزینه ها و مشکلات

    بارگذاری بیش از حد رویه ها و عملکردها در سطح یک ایده کلی، به عنوان یک قاعده، اجرا یا درک آن دشوار نیست. با این حال، حتی در آن برخی از "تله ها" وجود دارد که باید در نظر گرفته شود. اجازه اضافه بارگذاری اپراتور مشکلات بسیار بیشتری را هم برای پیاده‌کننده زبان و هم برای برنامه‌نویسی که در آن زبان کار می‌کند ایجاد می‌کند.

    مشکل شناسایی

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

    int add(int a1, int a2); float add(float a1, float a2);

    چگونه باید کامپایلر عبارت y = add(x, i) را مدیریت کند که در آن x یک شناور و i یک int است؟ بدیهی است که تطابق دقیقی وجود ندارد. دو گزینه وجود دارد: یا y=add_int((int)x,i) یا به صورت y=add_flt(x, (float)i) (در اینجا نام های add_int و add_float به ترتیب نشان دهنده نسخه اول و دوم تابع هستند) .

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

    • شناسایی نادرست را اصلاً ممنوع کنید. مستلزم این است که برای هر جفت نوع خاص یک نوع دقیقاً مناسب از رویه یا عملیات اضافه بار وجود داشته باشد. اگر چنین گزینه ای وجود نداشته باشد، کامپایلر باید یک خطا ایجاد کند. برنامه نویس در این مورد باید یک تبدیل صریح برای ریختن پارامترهای واقعی به مجموعه مورد نظر از انواع اعمال کند. این رویکرد در زبان‌هایی مانند C++ ناخوشایند است، که میزان آزادی مناسبی را در برخورد با انواع اجازه می‌دهد، زیرا منجر به تفاوت قابل‌توجهی در رفتار عملگرهای داخلی و بارگذاری شده می‌شود (عملیات حسابی را می‌توان برای اعداد معمولی اعمال کرد. بدون تفکر، اما به انواع دیگر - فقط با تبدیل صریح) یا به ظهور تعداد زیادی گزینه برای عملیات.
    • قوانین خاصی را برای انتخاب "نزدیک ترین مناسب" وضع کنید. معمولاً در این نوع، کامپایلر آن دسته از واریانت‌هایی را انتخاب می‌کند که تماس‌های آن‌ها از منبع فقط با تبدیل‌های نوع ایمن (اطلاعات بدون ضرر) قابل دریافت است و اگر چندین مورد از آنها وجود داشته باشد، می‌تواند بر اساس این که کدام نوع کمتر نیاز دارد، انتخاب کند. چنین تبدیل هایی اگر نتیجه بیش از یک احتمال را به جا بگذارد، کامپایلر یک خطا ایجاد می کند و از برنامه نویس می خواهد که به صراحت نوع آن را مشخص کند.

    مسائل خاص بارگذاری بیش از حد عملیات

    بر خلاف رویه ها و توابع، عملیات infix زبان های برنامه نویسی دارای دو ویژگی اضافی هستند که به طور قابل توجهی بر عملکرد آنها تأثیر می گذارد: اولویت و انجمن، که وجود آنها به دلیل امکان ضبط "زنجیره ای" اپراتورها است (نحوه درک a + b * ج: به صورت (a + b )*c یا مانند a+(b*c) عبارت a-b+c (a-b)+c یا a-(b+c) ?) است.

    عملیات تعبیه شده در زبان همیشه دارای تقدم و تداعی سنتی از پیش تعریف شده است. این سوال مطرح می‌شود: نسخه‌های بازتعریف شده این عملیات یا علاوه بر این، عملیات جدید ایجاد شده توسط برنامه‌نویس چه اولویت‌ها و ارتباطی خواهند داشت؟ نکات ظریف دیگری وجود دارد که ممکن است نیاز به توضیح داشته باشد. به عنوان مثال، در C دو شکل عملگر افزایش و کاهش ++ و --- پیشوند و پسوند وجود دارد که رفتار متفاوتی دارند. نسخه های بارگذاری شده چنین اپراتورهایی چگونه باید رفتار کنند؟

    زبان های مختلف به روش های مختلف با این مسائل برخورد می کنند. بنابراین، در C++، تقدم و تداعی نسخه‌های بارگذاری‌شده اپراتورها مانند آنچه در زبان تعریف شده است حفظ می‌شود. می توان به طور جداگانه فرم های پیشوند و پسوند عملگرهای افزایش و کاهش را با استفاده از امضاهای ویژه بارگذاری کرد:

    بنابراین از int برای ایجاد تفاوت در امضا استفاده می شود

    اعلام عملیات جدید

    وضعیت با اعلام عملیات جدید حتی پیچیده تر است. گنجاندن امکان چنین اعلامیه ای در زبان دشوار نیست، اما اجرای آن مملو از مشکلات قابل توجهی است. اعلام یک عملیات جدید در واقع ایجاد یک کلمه کلیدی زبان برنامه نویسی جدید است که با این واقعیت پیچیده است که عملیات در متن معمولاً می تواند نشانه های دیگر را بدون جداکننده دنبال کند. هنگامی که آنها ظاهر می شوند، مشکلات اضافی در سازماندهی تحلیلگر واژگانی ایجاد می شود. به عنوان مثال، اگر زبان از قبل دارای عملیات "+" و یک واحد "-" (تغییر علامت) باشد، عبارت a+-b را می توان به طور دقیق به صورت + (-b) تفسیر کرد، اما اگر یک عملیات جدید +- باشد. در برنامه اعلام شده است، بلافاصله ابهام ایجاد می شود، زیرا همان عبارت قبلاً می تواند به صورت a (+-) b تجزیه شود. توسعه دهنده و پیاده کننده زبان باید به نحوی با چنین مشکلاتی مقابله کند. گزینه‌ها، دوباره، می‌توانند متفاوت باشند: مستلزم این است که تمام عملیات‌های جدید تک کاراکتری باشند، فرض کنید که برای هر گونه اختلاف، «طولانی‌ترین» نسخه عملیات انتخاب می‌شود (یعنی تا زمانی که مجموعه بعدی کاراکترهای خوانده شده توسط مترجم مطابقت داشته باشد. هر عملیاتی، خواندن آن ادامه می یابد)، سعی کنید برخوردها را در حین ترجمه تشخیص دهید و در موارد بحث برانگیز خطا ایجاد کنید ... به هر حال زبان هایی که امکان اعلام عملیات جدید را فراهم می کنند این مشکلات را حل می کنند.

    این نکته را نباید فراموش کرد که برای عملیات جدید نیز موضوع تعیین تداعی و اولویت وجود دارد. دیگر راه حل آماده ای در قالب یک عملیات زبان استاندارد وجود ندارد و معمولا فقط باید این پارامترها را با قوانین زبان تنظیم کنید. به عنوان مثال، تمام عملیات های جدید را به سمت چپ مرتبط کنید و به آنها یکسان، ثابت، اولویت بدهید، یا ابزار مشخص کردن هر دو را به زبان معرفی کنید.

    اضافه بار و متغیرهای چند شکلی

    هنگامی که عملگرها، توابع و رویه‌های بارگذاری شده در زبان‌هایی با تایپ قوی استفاده می‌شوند، جایی که هر متغیر دارای یک نوع از پیش اعلام‌شده است، این بر عهده مترجم است که تصمیم بگیرد از کدام نوع عملگر بارگذاری شده در هر مورد خاص استفاده کند، مهم نیست چقدر پیچیده است. . این بدان معنی است که برای زبان های کامپایل شده، استفاده از بارگذاری بیش از حد اپراتور منجر به کاهش عملکرد نمی شود - در هر صورت، یک عملیات یا فراخوانی عملکرد کاملاً تعریف شده در کد شی برنامه وجود دارد. وقتی امکان استفاده از متغیرهای چند شکلی در زبان وجود دارد، یعنی متغیرهایی که می توانند مقادیری از انواع مختلف در زمان های مختلف داشته باشند، وضعیت متفاوت است.

    از آنجایی که نوع مقداری که عملیات اضافه بار روی آن اعمال خواهد شد در زمان ترجمه کد مشخص نیست، کامپایلر از توانایی انتخاب گزینه مناسب از قبل محروم است. در این حالت، مجبور است قطعه ای را در کد شی جاسازی کند که بلافاصله قبل از انجام این عملیات، انواع مقادیر موجود در آرگومان ها را تعیین می کند و به صورت پویا یک نوع متناظر با این مجموعه از انواع را انتخاب می کند. علاوه بر این، چنین تعریفی باید در هر اجرای عملیات ارائه شود، زیرا حتی همان کدی که برای بار دوم فراخوانی می شود، ممکن است به طور متفاوتی اجرا شود.

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

    نقد

    استفاده از اضافه بار توسط همه کارشناسان یک مزیت محسوب نمی شود. اگر بارگذاری بیش از حد تابع و رویه عموماً قابل اعتراض نیست (تا حدی به این دلیل که منجر به برخی مشکلات معمول "اپراتور" نمی شود، و تا حدی به این دلیل که سوء استفاده از آن کمتر وسوسه انگیز است)، در این صورت بارگذاری بیش از حد اپراتور، در اصل، و در پیاده سازی های زبانی خاص است. ، مورد انتقاد شدید بسیاری از نظریه پردازان و متخصصان برنامه نویسی قرار گرفته است.

    منتقدان خاطرنشان می‌کنند که مشکلات شناسایی، تقدم و ارتباط بالا اغلب کار با اپراتورهای بارگذاری شده را به طور غیرضروری دشوار یا غیرطبیعی می‌کند:

    • شناسایی. اگر زبان قوانین شناسایی دقیقی داشته باشد، برنامه نویس مجبور می شود به خاطر بیاورد که برای کدام ترکیب از انواع عملیات اضافه بار وجود دارد و به صورت دستی عملوندها را به آنها ارسال می کند. اگر زبان اجازه شناسایی «تقریبی» را بدهد، هرگز نمی‌توان مطمئن بود که در موقعیت‌های نسبتاً پیچیده، دقیقاً نسخه‌ای از عملیاتی که برنامه‌نویس در ذهن داشت، انجام می‌شود.
    • اولویت و همراهی. اگر آنها به طور دقیق تعریف شوند، ممکن است ناخوشایند باشد و به حوزه موضوعی مرتبط نباشد (به عنوان مثال، برای عملیات با مجموعه ها، اولویت ها با موارد حسابی متفاوت است). اگر برنامه‌نویس بتواند آن‌ها را تنظیم کند، این یک منبع اضافی از خطاها می‌شود (البته فقط به این دلیل که انواع مختلف یک عملیات دارای اولویت‌ها یا حتی ارتباط متفاوت هستند).

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

    از نظر پیاده سازی زبان، همین مشکلات منجر به پیچیدگی مترجمان و کاهش کارایی و پایایی آنها می شود. و استفاده از اضافه بار همراه با متغیرهای چند شکلی نیز آشکارا کندتر از فراخوانی عملیات کدگذاری شده در طول کامپایل است و فرصت های کمتری برای بهینه سازی کد شی فراهم می کند. ویژگی های خاص اجرای اضافه بار در زبان های مختلف مورد انتقاد جداگانه قرار می گیرد. بنابراین، در C++، موضوع انتقاد می تواند عدم توافق در مورد نمایش داخلی نام توابع اضافه بار باشد، که باعث ناسازگاری در سطح کتابخانه های کامپایل شده توسط کامپایلرهای مختلف C++ می شود.

    برخی از منتقدان بر اساس اصول کلی تئوری توسعه نرم‌افزار و عمل صنعتی واقعی، علیه عملیات‌های اضافه بار صحبت می‌کنند.

    • طرفداران رویکرد "پوریتن" برای ساختن زبان، مانند Wirth یا Hoare، با بارگذاری بیش از حد اپراتور مخالف هستند، زیرا به راحتی می توان از آن صرف نظر کرد. به نظر آنها، چنین ابزارهایی فقط زبان و مترجم را پیچیده می کنند، بدون اینکه ویژگی های اضافی مربوط به این عارضه را ارائه دهند. به نظر آنها، ایده ایجاد یک برنامه افزودنی وظیفه محور از زبان فقط جذاب به نظر می رسد. در واقع، استفاده از ابزارهای پسوند زبان، برنامه را فقط برای نویسنده آن قابل درک می کند - کسی که این پسوند را توسعه داده است. درک و تجزیه و تحلیل برنامه برای دیگر برنامه نویسان بسیار دشوارتر می شود و نگهداری، اصلاح و توسعه تیم را دشوارتر می کند.
    • خاطرنشان می شود که خود امکان استفاده از اضافه بار اغلب نقش تحریک کننده ای دارد: برنامه نویسان در هر کجا که ممکن است شروع به استفاده از آن می کنند، در نتیجه ابزاری که برای ساده سازی و ساده سازی برنامه طراحی شده است دلیل پیچیدگی و سردرگمی آن می شود.
    • اپراتورهای پربار ممکن است دقیقاً آنچه از آنها انتظار می رود، بر اساس نوع خود، انجام ندهند. به عنوان مثال، a + b معمولا (اما نه همیشه) به معنای همان b + a است، اما "یک" + "دو" با "دو" + "یک" در زبان هایی که در آن عملگر + بیش از حد بارگذاری شده است متفاوت است. الحاق رشته
    • بارگذاری بیش از حد اپراتور باعث می شود قطعات برنامه به زمینه حساس تر شوند. بدون دانستن انواع عملوندهای درگیر در یک عبارت، نمی توان فهمید که اگر از عملگرهای بارگذاری شده بیش از حد استفاده می کند، چه کاری انجام می دهد. به عنوان مثال، در یک برنامه C++، عبارت<< может означать и побитовый сдвиг, и вывод в поток. Выражение a << 1 возвращает результат побитового сдвига значения a на один бит влево, если a - целая переменная, но если a является выходным потоком , то же выражение выведет в этот поток строку «1» .

    طبقه بندی

    در زیر طبقه‌بندی برخی از زبان‌های برنامه‌نویسی با توجه به اینکه آیا آنها اجازه بارگذاری بیش از حد اپراتور را می‌دهند و اینکه آیا اپراتورها به یک مجموعه از پیش تعریف‌شده محدود می‌شوند، ارائه شده است:

    عملیات بدون اضافه بار اضافه بار وجود دارد
    مجموعه محدودی از عملیات
    • هدف-C
    • پایتون
    امکان تعریف عملیات جدید وجود دارد
    • PostgreSQL
    • همچنین ببینید

      بنیاد ویکی مدیا 2010 .

      ببینید "بارگذاری بیش از حد تابع" در فرهنگ های دیگر چیست:

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



    چگونه می توان به اضافه بار تابع در C رسید؟ (ده)

    آیا راهی برای دستیابی به اضافه بار تابع در C وجود دارد؟ من به توابع ساده ای نگاه می کنم که می توانند مانند آنها بیش از حد بارگذاری شوند

    foo (int a) foo (char b) foo (float c , int d)

    من فکر می کنم هیچ راه مستقیمی وجود ندارد. من به دنبال راه حلی هستم، اگر راه حلی وجود داشته باشد.

    امیدوارم کد زیر به شما در درک اضافه بار عملکرد کمک کند

    #عبارتند از #عبارتند از int fun(int a, ...); int main(int argc, char *argv)( fun(1,10); fun(2"bank سوال"); return 0; ) int fun(int a, ...)(va_list vl; va_start(vl,a )؛ if(a==1) printf("%d",va_arg(vl,int)); else printf("\n%s",va_arg(vl,char *)); )

    منظورم این است که شما نمی توانید.

    شما می توانید تابع va_arg را به عنوان اعلام کنید

    void my_func (فرمت char*، ...);

    اما باید اطلاعاتی در مورد تعداد متغیرها و انواع آنها در آرگومان اول ارسال کنید - مانند printf() .

    بله، مانند.

    در اینجا شما یک مثال می زنید:

    void printA(int a)( printf("Hello world from printA: %d\n",a); ) void printB(const char *buff)( printf("Hello world from printB: %s\n",buff) ; ) #define Max_ITEMS() 6, 5, 4, 3, 2, 1, 0 #define __VA_ARG_N(_1, _2, _3, _4, _5, _6, N, ...) N #define _Num_ARGS_(... ) __VA_ARG_N(__VA_ARGS__) #define NUM_ARGS(...) (_Num_ARGS_(_0, ## __VA_ARGS__, Max_ITEMS()) - 1) #define CHECK_ARGS_MAX_LIMIT(t) if(mingsHE_t) if(mingsHE_t) if(NUM_ARGS(args) #define print(x, args...) \ CHECK_ARGS_MIN_LIMIT(1) printf("خطا");fflush(stdout); \ CHECK_ARGS_MAX_LIMIT(4) printf("خطا"); = * argv) (int a=0؛ print(a)؛ print ("سلام")؛ بازگشت (EXIT_SUCCESS)؛ )

    خروجی 0 و hello از printA و printB خواهد بود.

    اگر کامپایلر شما gcc است و برایتان مهم نیست که هر بار اضافه بار جدید را به‌روزرسانی‌های دستی انجام دهید، می‌توانید یک macromass بسازید و از نظر تماس‌گیرندگان به نتیجه دلخواهتان برسید، نوشتن آنقدرها هم خوب نیست... اما ممکن است

    به __builtin_types_compatible_p نگاه کنید سپس از آن برای تعریف یک ماکرو استفاده کنید که کاری شبیه به

    #define foo(a) \ ((__builtin_types_compatible_p(int, a)?foo(a):(__builtin_types_compatible_p(float, a)?foo(a):)

    اما آره، بد، نه

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

    #define cbrt(X) _Generic((X)، long double: cbrtl، \ پیش‌فرض: cbrt، \ float: cbrtf) (X)

    همانطور که قبلاً گفته شد، اضافه بار به معنایی که منظور شماست توسط C پشتیبانی نمی‌شود. اصطلاح معمول برای حل مشکل این است که تابع یک اتحادیه برچسب‌گذاری شده را بگیرد. این با استفاده از پارامتر struct پیاده‌سازی می‌شود، جایی که ساختار خود از نوعی نشانگر نوع، مانند enum و ترکیبی از انواع مقادیر مختلف تشکیل شده است. مثال:

    #عبارتند از typedef enum (T_INT، T_FLOAT، T_CHAR، ) my_type; typedef struct ( my_type type; union (int a; float b; char c; ) my_union; ) my_struct; void set_overload (my_struct *whatever) ( switch (whatever->type) ( case T_INT: whatever->my_union.a = 1; break; case T_FLOAT: whatever->my_union.b = 2.0; break; case T_CHAR: whatever-> my_union.c = "3"; ) ) void printf_overload (my_struct *whatever) (سوئیچ (whatever->type) ( case T_INT: printf("%d\n", whatever->my_union.a); break; case T_FLOAT : printf("%f\n", whatever->my_union.b)؛ break؛ case T_CHAR: printf("%c\n", whatever->my_union.c); break; ) ) int main (int argc, char* argv) ( my_struct s; s.type=T_INT; set_overload(&s); printf_overload(&s); s.type=T_FLOAT; set_overload(&s); printf_overload(&s); s.type=T_CHAR; set_overload(&s) ; printf_overload(&s); )

    آیا نمی توانید فقط از C++ استفاده کنید و علاوه بر این یکی از سایر ویژگی های C++ استفاده نکنید؟

    اگر C به شدت سختگیرانه تا کنون وجود نداشته است، من توابع variadic را به جای آن توصیه می کنم.

    رویکرد زیر مشابه است a2800276، اما با برخی از ماکروهای C99:

    // ما به "size_t" #include نیاز داریم // انواع آرگومان برای پذیرش enum sum_arg_types ( SUM_LONG, SUM_ULONG, SUM_DOUBLE ); // ساختاری برای نگه داشتن آرگومان struct sum_arg ( enum sum_arg_types type; union (long as_long; unsigned long as_ulong; double as_double; ) value; ); // تعیین اندازه یک آرایه #define count(ARRAY) ((sizeof (ARRAY))/(sizeof *(ARRAY))) // به این ترتیب تابع ما #define sum (...) نامیده می شود. count(sum_args(__VA_ARGS__)), sum_args(__VA_ARGS__)) // ایجاد آرایه ای از `struct sum_arg` #define sum_args(...) ((struct sum_arg )( __VA_ARGS__ )) // ایجاد مقدار اولیه برای آرگومان ها #longfine (VALUE) ( SUM_LONG, ( .as_long = (VALUE) ) ) #define sum_ulong(VALUE) ( SUM_ULONG, ( .as_ulong = (VALUE) ) ) #define sum_double (VALUE) ( SUM_DOUBLE, ( .VAL_u) ) // تابع چندشکل ما long double _sum (size_t count, struct sum_arg * args) ( long double value = 0; for(size_t i = 0; i< count; ++i) { switch(args[i].type) { case SUM_LONG: value += args[i].value.as_long; break; case SUM_ULONG: value += args[i].value.as_ulong; break; case SUM_DOUBLE: value += args[i].value.as_double; break; } } return value; } // let"s see if it works #include int main() ( long foo بدون علامت = -1؛ long double value = sum(sum_long(42)، sum_ulong(foo)، sum_double(1e10)); printf("%Le\n"، مقدار)؛ بازگشت 0؛ )

    در حال حاضر، _Generic از آنجایی که سوال _Generic، استاندارد C (بدون پسوند) به طور موثر است. اخذ شدهپشتیبانی از توابع اضافه بار (به جای عملگرها) به لطف افزودن کلمه _Generic _Generic در C11. (در GCC از نسخه 4.9 پشتیبانی می شود)

    (بارگذاری بیش از حد واقعاً به روشی که در سؤال نشان داده شده «ساخت‌شده» نیست، اما از بین بردن چیزی که به این شکل عمل می‌کند آسان است.)

    Generic یک عملگر زمان کامپایل در همان خانواده sizeof و _Alignof است. در بخش استاندارد 6.5.1.1 توضیح داده شده است. این دو پارامتر اصلی را می‌طلبد: یک عبارت (که در زمان اجرا ارزیابی نمی‌شود) و یک لیست از تداعی‌های نوع/عبارت، که کمی شبیه یک بلوک سوئیچ است. _Generic نوع عمومی عبارت را دریافت می کند و سپس به آن "سوئیچ" می کند تا عبارت نتیجه نهایی را در لیست برای نوع آن انتخاب کند:

    Generic(1، float: 2.0، char *: "2"، int: 2، پیش فرض: get_two_object());

    عبارت بالا به 2 ارزیابی می شود - نوع عبارت کنترل int است، بنابراین عبارت مرتبط با int را به عنوان مقدار انتخاب می کند. هیچ کدام از اینها در زمان اجرا باقی نمی ماند. (بند پیش فرض اجباری است: اگر آن را مشخص نکنید و نوع مطابقت نداشته باشد، باعث خطای کامپایل می شود.)

    تکنیکی که برای بارگذاری بیش از حد تابع مفید است این است که می توان آن را توسط پیش پردازنده C درج کرد و یک عبارت نتیجه را بر اساس نوع آرگومان های ارسال شده به ماکرو کنترل کننده انتخاب کرد. بنابراین (به عنوان مثال از استاندارد C):

    #define cbrt(X) _Generic((X)، \ long double: cbrtl, \ default: cbrt, \ float: cbrtf \)(X)

    این ماکرو با انتقال نوع آرگومان به ماکرو، انتخاب تابع پیاده سازی مناسب و سپس ارسال ماکرو اصلی به آن تابع، عملیات cbrt سربارگذاری شده را پیاده سازی می کند.

    بنابراین، برای پیاده سازی مثال اصلی شما، می توانیم این کار را انجام دهیم:

    Foo_int (int a) foo_char (char b) foo_float_int (float c , int d) #define foo(_1, ...) _Generic((_1), \ int: foo_int, \ char: foo_char, \ float: _Generic(( FIRST(__VA_ARGS__,)), \int: foo_float_int))(_1, __VA_ARGS__) #define FIRST(A, ...) A

    در این مورد، می‌توانیم از پیش‌فرض: binding برای مورد سوم استفاده کنیم، اما این نشان نمی‌دهد که چگونه می‌توان اصل را به چندین آرگومان تعمیم داد. نتیجه نهایی این است که می توانید بدون نگرانی (بسیار) در مورد نوع آرگومان های خود از foo(...) در کد خود استفاده کنید.

    برای موقعیت‌های پیچیده‌تر، مانند توابعی که آرگومان‌های بیشتری را بارگذاری می‌کنند یا اعداد متفاوت، می‌توانید از ماکروهای ابزار برای تولید خودکار ساختارهای ارسال استاتیک استفاده کنید:

    void print_ii(int a, int b) ( printf("int, int\n"); ) void print_di(double a, int b) ( printf("double, int\n"); ) void print_iii(int a, int b, int c) ( printf("int, int, int\n"); ) void print_default(void) ( printf("آگومان های ناشناخته\n"); ) #define print(...) OVERLOAD(print, (__VA_ARGS__), \ (print_ii, (int, int)), \ (print_di, (double, int)), \ (print_iii, (int, int, int)) \) #define OVERLOAD_ARG_TYPES (int, double) #define OVERLOAD_FUNCTIONS (چاپ) #include "activate-overloads.h" int main(void) ( print(44, 47); // prints "int, int" print(4.4, 47); // prints "double, int" print (1، 2، 3)؛ // چاپ "int, int, int" print("")؛ // چاپ "آگومان های ناشناخته")

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

    گذشته از این، بارگذاری بیش از حد امکان پذیر بود میزانآرگومان ها (به جای تایپ) در C99.

    توجه داشته باشید که روش ارزیابی C می تواند شما را به حرکت درآورد. برای مثال، اگر بخواهید یک کاراکتر تحت اللفظی به آن منتقل کنید، این گزینه foo_int را انتخاب می‌کند، و اگر می‌خواهید اضافه‌بارهای شما از حرف‌های رشته‌ای پشتیبانی کنند، به مقداری foo_int نیاز دارید. با این حال، به طور کلی بسیار جالب است.

    پاسخ Leushenko واقعا جالب است: فقط مثال foo با GCC کامپایل نمی شود، که در foo(7) با زدن FIRST ماکرو و فراخوانی واقعی تابع ((_1, __VA_ARGS__) با یک کاما اضافی باقی می ماند. اگر بخواهیم اضافه بارهای اضافی مانند foo(double) ارائه دهیم، با مشکل مواجه می شویم.

    بنابراین تصمیم گرفتم به این سوال با جزئیات بیشتر پاسخ دهم، از جمله اجازه دادن به اضافه بار خالی (foo(void) - که باعث ایجاد مشکل شد...).

    ایده در حال حاضر این است: بیش از یک ژنریک را در ماکروهای مختلف تعریف کنید و اجازه دهید با توجه به تعداد آرگومان ها، یک مورد صحیح انتخاب شود!

    بر اساس این پاسخ، تعداد استدلال ها بسیار ساده است:

    #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) #define CONCAT(X, Y) CONCAT_(X, Y) # CONCAT_(X, Y) X ## Y را تعریف کنید

    این خوب است، ما در حال تصمیم گیری برای SELECT_1 یا SELECT_2 هستیم (یا آرگومان های بیشتری در صورت تمایل/نیاز به آنها)، بنابراین ما فقط به تعاریف مناسب نیاز داریم:

    #define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char: foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic((_1), \ double : _Generic((_2)، \ int: foo_double_int \) \)

    اول، یک فراخوانی ماکرو خالی (foo()) همچنان یک توکن ایجاد می کند، اما خالی است. بنابراین ماکرو شمارش در واقع 1 را به جای 0 برمی گرداند، حتی اگر ماکرو خالی نامیده شود. اگر __VA_ARGS__ را با کاما بعد از __VA_ARGS____VA_ARGS__ کنیم، می‌توانیم به راحتی این مشکل را برطرف کنیم. مشروطبسته به خالی بودن یا نبودن لیست:

    #define NARG(...) ARG4_(__VA_ARGS__ COMMA(__VA_ARGS__) 4, 3, 2, 1, 0)

    آی تی نگاه کردآسان است، اما ماکرو COMMA بسیار سنگین است. خوشبختانه، این موضوع قبلاً در وبلاگ Jens Gustedt پوشش داده شده است (با تشکر، Jens). ترفند اصلی این است که ماکروهای توابع گسترش نمی‌یابند مگر اینکه پرانتز دنبال شوند، برای توضیح بیشتر به وبلاگ Jens مراجعه کنید... فقط باید ماکروها را برای نیازهایمان کمی تغییر دهیم (از نام‌های کوتاه‌تر و آرگومان‌های کمتری برای اختصار استفاده می‌کنم) .

    #define ARGN(...) ARGN_(__VA_ARGS__) #define ARGN_(_0, _1, _2, _3, N, ...) N #تعریف HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 1, 0 ) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_COMMA(SET_COMMA __VA_ARGS__), \ HASVA_COMMA_ARGS__MA \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3 # تعریف COMMA_0000، #تعریف COMMA_0001 #تعریف COMMA_0010، // ... (همه دیگران با کاما) #تعریف COMMA_1111،

    و حالا ما خوبیم...

    کد کامل در یک بلوک:

    /* * demo.c * * ایجاد در: 14/09/2017 * نویسنده: sboehler */ #include void foo_void(void) ( puts("void"); ) void foo_int(int c) (printf("int: %d\n", c); ) void foo_char(char c) (printf("char: %c \n"، c)؛ ) void foo_double(double c) ( printf("double: %.2f\n"، c); ) void foo_double_int(double c, int d) (printf("double: %.2f, int: %d\n", c, d); ) #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) # تعریف CONCAT(X,Y) CONCAT_(X,Y) #define CONCAT_(X,Y) X ## Y #define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char : foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic((_1), \ double: _Generic((_2), \ int: foo_double_int \) \) #define ARGN(...) ARGN_( __VA_ARGS__) #تعریف ARGN_(_0، _1، _2، N، ...) N #تعریف NARG(...) ARGN(__VA_ARGS__ COMMA(__VA_ARGS__) 3، 2، 1، 0) #تعریف HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 0) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_C OMMA(SET_COMMA __VA_ARGS__), \ HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define __SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define __SELECT_COMMA, _3, COMMA_ ## _0 ## _1 ## _2 ## _3 COMMA_1001 , #define COMMA_1010 , #define COMMA_1011 , #define COMMA_1100 , #define COMMA_1101 , #define COMMA_1101 , #define COMMA_1defin, #define COMMA_1defin, #define COMMA_1define ( foo(); foo (7); foo (10.12); foo(12.10, 7); foo((char)"s"); بازگشت 0; )

    زبان C++ توانایی استفاده از یک شناسه را برای توابعی که اعمال متفاوتی را بر روی انواع داده‌های مختلف انجام می‌دهند، پیاده‌سازی می‌کند، در نتیجه می‌توانید از چندین تابع با نام یکسان، اما با لیست‌های پارامترهای مختلف، هم از نظر تعداد و هم از نظر نوع استفاده کنید.

    چنین توابعی نامیده می شوند بیش از حد بارگذاری شده استو خود مکانیسم اضافه بارکارکرد.

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

    یافتن تابع برای فراخوانی در سه مرحله جداگانه انجام می شود:

    1. تابعی را با تطابق دقیق پارامترها جستجو کنید و در صورت یافتن از آن استفاده کنید.

    2. با استفاده از تبدیل نوع داده داخلی، تابع مناسب را جستجو کنید.

    3. تابع مناسب را با استفاده از تبدیل های تعریف شده توسط کاربر جستجو کنید.

    مثال بارگذاری بیش از حد تابع

    بیایید یک مثال از تابع ارائه دهیم اس 1 با دو پارامتر ایکس,در، که بسته به نوع آرگومان های ارسال شده به شرح زیر عمل می کند:

    – اگر نوع پارامتر عدد صحیح باشد، تابع اس 1 مقادیر آنها را اضافه می کند و مجموع حاصل را برمی گرداند.

    - اگر نوع پارامتر باشد طولانی، عملکرد اس 1 مقادیر آنها را ضرب می کند و محصول حاصل را برمی گرداند.

    – اگر نوع پارامتر واقعی باشد، تابع اس 1 مقادیر آنها را تقسیم می کند و ضریب را برمی گرداند.

    #عبارتند از

    int S1 (int x، int y) (

    بلند S1 (طول x، بلند y) (

    دو برابر S1 (دو برابر x، دو برابر y) (

    int a = 1, b = 2, c;

    طولانی i = 3، j = 4، k;

    دو برابر x = 10، y = 2، z.

    printf("\n c = %d; k = %ld; z = %lf . \n", c, k, z);

    در نتیجه، دریافت می کنیم:

    ج = 3; ک = 12; z = 5.000000 .

    توابع پارامتر متغیر

    یک بیضی در لیست پارامترهای یک تابع تعریف شده توسط کاربر زمانی استفاده می شود که تعداد آرگومان ها از قبل مشخص نباشد. در این حالت، تعداد نامحدودی از پارامترها را می توان در نمونه اولیه آن به صورت زیر مشخص کرد:

    void f1 (int a , دوبل ب , …);

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

    ما ویژگی های اصلی استفاده از این مکانیسم را فهرست می کنیم.

    1. چندین دستور ماکرو برای دسترسی به پارامترهای چنین توابعی استفاده می شود که عبارتند از:

    وا _ فهرست و وا _ شروع کنید - ماکروها برای آماده سازی دسترسی به پارامترها.

    وا _ ارگ - استفاده از پارامترها؛

    وا _ پایان - تمیز کردن قبل از خروج

    آنها در فایل هدر اعلام شده اند stdarg . ساعت .

    2. چنین تابعی باید حداقل یک پارامتر (با نام) داشته باشد تا تعداد آرگومان هایی که باید به آن ارسال شود، ارسال شود.

    3. برای ماکرو وا_ شروع کنیدشما باید دو آرگومان ارسال کنید - نام لیست پارامترها که مشخص می کند وا_ فهرستو تعداد آنها

    4. شکستن ترتیب مشخص شده ماکروها غیرممکن است. در غیر این صورت، می توانید عواقب غیر قابل پیش بینی داشته باشید.

    5. برای ماکرو وا_ ارگعلاوه بر نام لیست پارامترها، باید نوع مورد نظر را نیز پاس کنید. اگر انواع مطابقت نداشته باشند، خطا رخ می دهد.

    استفاده از بیضی بررسی نوع پارامتر را به طور کامل غیرفعال می کند. بیضی تنها زمانی مورد نیاز است که تعداد پارامترها و نوع آنها تغییر کند.

    مثال زیر این احتمال را نشان می دهد.

    #عبارتند از

    #عبارتند از

    void f1(s double، int n...) (

    va_start(p, n);

    printf(" \n Double S = %lf ", s);

    برای (int i=1; i<=n; i++) {

    v = va_arg(p, int);

    printf("\n آرگومان %d = %d ", i, v);

    خالی اصلی (باطل) (

    f1(1.5، 3، 4، 5، 6);

    در نتیجه، دریافت می کنیم:

    دو برابر اس = 1.500000

    استدلال 1 = 4

    استدلال 2 = 5

    استدلال 3 = 6

    کلیدی را برای ادامه فشار دهید