Передача параметрів значення си. Передача параметрів за посиланням та значенням. Передача параметрів за значенням та за посиланням

Параметри в функцію можуть передаватися одним із таких способів:

При передачі аргументів за значенням компілятор створює тимчасову копію об'єкта, який має бути переданий, та розміщує її в області стікової пам'яті, призначеної для зберігання локальних об'єктів. Функція, що викликається, оперує саме з цією копією, не впливаючи на оригінал об'єкта. Прототипи функцій, що приймають аргументи за значенням, передбачають як параметри вказівку типу об'єкта, а чи не його адреси. Наприклад, функція

int GetMax(int, int);

приймає два цілих аргументи за значенням.

Якщо необхідно, щоб функція модифікувала оригінал об'єкта, використовується передача параметрів за посиланням. У цьому функцію передається не сам об'єкт, лише його адресу. Отже, все модифікації у тілі функції переданих їй за посиланням аргументів впливають об'єкт. Беручи до уваги той факт, що функція може повертати лише єдине значення, використання передачі адреси об'єкта виявляється дуже ефективним способом роботи з великою кількістю даних. З іншого боку, оскільки передається адресу, а чи не сам об'єкт, істотно економиться стекова пам'ять.

За допомогою покажчиків.

Синтаксис передачі з використанням посилань передбачає застосування як аргумент посилання на тип об'єкта. Наприклад, функція

double Glue(long&var1, int&var2);

отримує два посилання на змінні типу long та int. При передачі параметра-посилання компілятор автоматично передає адресу змінної, вказаної як аргумент. Ставити знак амперсанда перед аргументом у виклик функції не потрібно. Наприклад, для попередньої функції виклик з передачею параметрів за посиланням виглядає так:

Glue(var1, var2);

Приклад прототипу функції при передачі параметрів через покажчик наведено нижче:

void SetNumber(int*, long*);

Крім того, функції можуть повертати не тільки значення деякої змінної, а й покажчик або посилання на нього. Наприклад, функції, прототип яких:

* int Count (int); &int Increase();

повертають покажчик і посилання відповідно на цілісну змінну типу int. Слід мати на увазі, що повернення посилання або вказівника з функції може призвести до проблем, якщо змінна, на яку робиться посилання, вийшла з видимості. Наприклад,

Ефективність передачі адреси об'єкта замість самої змінної відчутна й у швидкості роботи, особливо, якщо використовуються великі об'єкти, зокрема масиви (розглянуті пізніше).

Якщо потрібно в функцію передати досить великий об'єкт, проте його модифікація не передбачається, практично використовується передача константного покажчика. Цей тип виклику передбачає використання ключового слова const, наприклад, функція

const int* FName(int* const Number)

приймає та повертає покажчик на константний об'єкт типу int. Будь-яка спроба модифікувати такий об'єкт у межах тіла викликається повідомлення компілятора про помилку. Розглянемо приклад, що ілюструє використання константних покажчиків.

#include

int * const call (int * const);

int X = 13; int * pX = call (pX);

int * const call (int * const x)

//*x++; ІІ не можна модифікувати об'єкт! return x;

Замість наведеного вище синтаксису константного покажчика як альтернативу при передачі параметрів можна використовувати константні посилання, наприклад:

const int&FName (const int&Number)

мають той самий сенс, як і константні покажчики.

#include

const int& call(const int& x)

// Не можна модифікувати об'єкт!

Заздалегідь вибачаюсь за пафосну інструкцію про "розстановку крапок", але треба ж якось залучити вас до статті)) Зі свого боку постараюся, щоб анотація все ж таки виправдовувала ваші очікування.

Коротко про що йдеться

Все це і так знають, але все ж таки на початку нагадаю, як в 1С можуть передаватися параметри методу. Передаватися вони можуть "за посиланням" та "за значенням". У першому випадку ми передаємо в метод те саме значення, що і в точці виклику, а в другому - його копію.

За умовчанням в 1С аргументи передаються за посиланням, і зміна параметра всередині методу буде видно ззовні методу. Тут подальше розуміння питання залежить від цього, що ви розумієте під словом " зміна параметра " . Так ось, мається на увазі повторне привласнення і нічого більше. Причому привласнення може бути неявним, наприклад викликом методу платформи, який повертає щось у вихідному параметрі.

Але якщо ми не хочемо, щоб наш параметр передавався за посиланням, ми можемо вказати перед параметром ключове слово Знач.

Процедура Позначення(Знач Параметр) Параметр = 2; КінецьПроцедури Параметр = 1; Позначення (параметр); Повідомити (Параметр); // виведе 1

Все працює, як обіцяно - зміна (а правильніше сказати "заміна") значення параметра не призводить до зміни значення поза методом.

Ну а в чому прикол?

Цікаві моменти починаються, коли ми починаємо передавати як параметри не примітивні типи (рядки, числа, дати тощо), а об'єкти. Ось тут і спливають такі поняття, як "дрібна" і "глибока" копія об'єкта, а також покажчики (не в термінах C ++, а як абстрактні дескриптори (handles)).

При передачі об'єкта (наприклад, ТаблиціЗначень) за посиланням, ми передаємо саме значення вказівника (такий собі handle), який у пам'яті платформи "тримає" об'єкт. При передачі за значенням платформа зробить копію цього покажчика.

Іншими словами, якщо, передаючи об'єкт за посиланням, у методі ми надамо параметру значення "Масив", то в точці виклику отримаємо масив. Повторне надання значення, переданого за посиланням, видно з місця виклику.

Процедура ОбробитиЗначення(Параметр) Параметр = Новий Масив; КінецьПроцедури Таблиця = Новий ТаблицяЗначень; ОбробитиЗначення(Таблиця); Повідомити(ТипЗнч(Таблиця)); // виведе Масив

Якщо ж ми передамо об'єкт за значенням, то в точці виклику наша ТаблицяЗначень не пропаде.

Вміст об'єкта та його стан

При передачі за значенням копіюється не весь об'єкт, лише його покажчик. Примірник об'єкта залишається тим самим. Неважливо, як ви передаєте об'єкт, за посиланням або значенням - очищення таблиці значень призведе до очищення саме таблиці. Ця очищення буде видно всюди, т.к. об'єкт був один-єдиний і неважливо, як саме він передавався у метод.

Процедура ОбробитиЗначення(Параметр) Параметр.Очистити(); КінецьПроцедури Таблиця = Новий ТаблицяЗначень; Таблиця.Додати(); ОбробитиЗначення(Таблиця); Повідомити(Таблиця.Кількість()); // виведе 0

При передачі об'єктів методи платформа оперує покажчиками (умовними, не прямими аналогами з C++). Якщо об'єкт передається за посиланням, то осередок пам'яті віртуальної машини 1С, де лежить даний об'єкт, може бути перезаписана іншим об'єктом. Якщо об'єкт передається за значенням, то покажчик копіюється і перезапис об'єкта не призводить до перезапису осередку пам'яті з вихідним об'єктом.

У той же час будь-яка зміна стануоб'єкта (очищення, додавання властивостей тощо) змінює сам об'єкт, і взагалі ніяк не пов'язано з тим, як і куди передавався об'єкт. Змінився стан екземпляра об'єкта, на нього може бути купа "посилань" і "по-значень", але екземпляр завжди один і той же. Передаючи об'єкт у метод, ми створюємо копію всього об'єкта.

І це правильно завжди, за винятком...

Клієнт-серверна взаємодія

У платформі дуже прозоро реалізовано серверні дзвінки. Ми просто викликаємо метод, а під капотом платформа серіалізує (перетворює на рядок) усі параметри методу, передає на сервер, а потім повертає вихідні параметри назад на клієнта, де вони десеріалізуються та живуть, начебто ні на який сервер не їздили.

Як відомо, не всі об'єкти платформи серіалізуються. Саме звідси зростає обмеження, що не всі об'єкти можна передати на серверний метод із клієнта. Якщо передати об'єкт, що не серіалізується, то платформа почне лаятися поганими словами.

  • Явне оголошення намірів програміста. Дивлячись на сигнатуру методу, можна чітко сказати, які вхідні параметри, а які вихідні. Такий код легше читати та супроводжувати
  • Для того, щоб зміна на сервері параметра "за посиланням" була видна в точці виклику на клієнті, пПараметри, що передаються на сервер за посиланням, платформа обов'язково сама повертатиме на клієнта, щоб забезпечити поведінку, описану на початку статті. Якщо параметр не потрібно повертати, буде перевитрата трафіку. Для оптимізації обміну даними параметри, значення яких нам не потрібні на виході, слід позначати словом Знач.

Тут примітний другий пункт. Для оптимізації трафіку платформа не повертатиме значення параметра клієнта, якщо параметр позначений словом Знач. Все це чудово, але призводить до цікавого ефекту.

Як уже говорив, під час передачі об'єкта на сервер відбувається серіалізація, тобто. виконується "глибока" копія об'єкта. А за наявності слова Значенняоб'єкт не поїде із сервера назад на клієнта. Складаємо ці два факти та отримуємо наступне:

&На Сервері Процедура Посилання(Параметр) Параметр.Очистити(); КінецьПроцедури &На Сервері Процедура Позначення(Знач Параметр) Параметр.Очистити(); КінецьПроцедури &НаКлієнті Процедура ПозначенняКлієнт(Знач Параметр) Параметр.Очистити(); КінецьПроцедури &НаКлієнті Процедура ПеревіритиЗнач() Список1= Новий СписокЗначень; Список1.Додати("привіт"); Список2 = Список1.Скопіювати(); Список3 = Список1.Скопіювати(); // Об'єкт копіюється повністю, // передається на сервер, потім повертається. // Очищення списку видно в точці виклику Посилання (Список1); // Об'єкт копіюється повністю, // передається на сервер. Назад не повертається. // Очищення списку НЕ ВИДНА в точці виклику ПоЗначення (Список2); // копіюється лише покажчик об'єкта // очищення списку видно у точці виклику ПоЗначениюКлиент(Список3); Повідомити(Список1.Кількість()); Повідомити(Список2.Кількість()); Повідомити(Список3.Кількість()); КінецьПроцедури

Резюме

Якщо коротко, то резюмувати можна так:

  • Передача за посиланням дозволяє "затерти" об'єкт зовсім іншим об'єктом
  • Передача за значенням не дозволяє "затерти" об'єкт, але зміни внутрішнього стану об'єкта буде видно, т.к. йде робота з одним і тим самим екземпляром об'єкта
  • При серверному виклику робота з Різними примірниками об'єкта, т.к. виконувалось глибоке копіювання. Ключове слово Значеннязаборонить копіювання серверного екземпляра назад у клієнтський, і зміна внутрішнього стану об'єкта на сервері не призведе до аналогічної зміни клієнта.

Сподіваюся, що цей нескладний перелік правил дозволить вам легше вирішувати суперечки з колегами щодо передачі параметрів "за значенням" та "за посиланням"

Коли я починав програмувати на C++ і посилено студіював книги та статті, то незмінно натикався на ту саму пораду: якщо нам потрібно передати в функцію якийсь об'єкт, який не повинен змінюватися в функції, то він завжди повинен передаватися за посиланням на константу(ППСК), крім тих випадків, коли потрібно передати або примітивний тип, або подібну з ними за розміром структуру. Т.к. за більш ніж 10 років програмування на C + + я дуже часто зустрічався з цією порадою (та й сам його давав неодноразово), він давно «ввібрався» в мене - я на автоматі передаю всі аргументи за посиланням на константу. Але час іде і вже минуло 7 років, як ми маємо у своєму розпорядженні C++11 з його семантикою переміщення, у зв'язку з якою я все більше чую голосів, які піддають стару добру догму сумнівам. Багато хто починає стверджувати, що передача за посиланням на константу це минуле століття і тепер потрібно передавати за значенням(ПЗЗ). Що стоїть за цими розмовами, а також які висновки ми можемо зробити з цього всього, я й хочу обговорити в цій статті.

Книжкова мудрість

Для того, щоб зрозуміти, якого правила нам варто дотримуватися, пропоную звернутися до книг. Книги це чудове джерело інформації, яку ми не зобов'язані приймати, але прислухатися до якої, безперечно, варто. І почнемо ми з історії, з витоків. Я не з'ясовуватиму хто був першим апологетом ППСК, просто наведу в приклад ту книгу, що особисто на мене справила найбільший вплив, у питанні використання ППСК.

Мейерс

Добре, ми маємо клас, у якому всі параметри передаються за посиланням, чи є з цим класом якісь проблеми? На жаль, є і ця проблема лежить на поверхні. У нас у класі функціонально 2 сутності: перша набуває значення на етапі створення об'єкта, а друга дозволяє змінити раніше встановлене значення. Сутності ж у нас дві, а ось функції чотири. А тепер уявіть, що у нас може бути не 2 подібні сутності, а 3, 5, 6, що тоді? Тоді на нас чекає сильне роздмухування коду. Тому, щоб не плодити маси функцій, з'явилася пропозиція відмовитись від посилань у параметрах взагалі:

Template class Holder ( public: explicit Holder (T value): m_Value(move(value)) ( ) void setValue(T value) ( ​​m_Value = move(value); ) const T&value() const noexcept ( return m_Value; T m_Value;);

Перша перевага, яка відразу впадає в око, полягає в тому, що коду стало значно менше. Його навіть менше ніж у першому варіанті, за рахунок видалення const і & (правда, додали move ). Але нас завжди вчили, що передача за посиланням продуктивніша, ніж передача за значенням! Так воно було до C++11, так воно і є досі, але тепер, якщо ми подивимося на цей код, то побачимо, що копіювання тут не більше, ніж у першому варіанті, за умови, що у T є конструктор переміщення. Тобто. сама по собі ППСК була і буде швидше за ППЗ, але код якось використовує передане посилання, і часто цей аргумент копіюється.

Однак це не вся історія. На відміну від першого варіанта, де ми маємо лише копіювання, тут додається ще й переміщення. Але ж рух це дешева операція, правда? На цю тему, у розглядуваної нами книги Мейерса, також є глава («Item 29»), яка озаглавлена ​​так: «Assume that move operations are not present, no cheap and not used». Основна думка має бути зрозуміла з назви, але якщо хочеться подробиць, то неодмінно ознайомтеся - я на цьому зупинятись не буду.

Тут було б доречним провести повний порівняльний аналіз першого та останнього методів, але я не хотів би відступати від книги, тому аналіз відкладемо для інших розділів, а тут продовжимо розглядати аргументи Скотта. Отже, крім того факту, що третій варіант очевидно коротший за другий, у чому Скотт бачить перевагу ППЗ над ППСК у сучасному коді?

Бачить він у тому, що у разі передачі rvalue, тобто. якийсь такий виклик: Holder holder(string("me")); , Випадок з ППСК дасть нам копіювання, а варіант з ППЗ дасть нам переміщення. З іншого боку, якщо передача буде такою: Holder holder(someLvalue); , то ППЗ однозначно програє рахунок того, що він виконає і копіювання, і переміщення, тоді як у варіанті з ППСК буде тільки одне копіювання. Тобто. виходить, що ППЗ, якщо розглядати суто ефективність, це певний компроміс між кількістю коду та «повноцінною» (через &&) підтримкою семантики переміщення.

Саме тому Скотт так ретельно сформулював свою пораду і так обережно її просуває. Мені навіть здалося, що він приводить його знехотя, як би під тиском: він не міг не розмістити міркування на цю тему в книзі, т.к. вона досить обговорювалася, а Скотт завжди був збирачем колективного досвіду. Крім того, дуже мало доводів він приводить на захист ППЗ, а от тих, що ставлять цю «техніку» під сумнів, він наводить чимало. Ми ще розглянемо його докази «проти» в наступних розділах, тут ми коротко повторимо аргумент, який Скотт приводить на захист ППЗ (подумки додаємо "якщо об'єкт підтримує переміщення і воно дешево"): дозволяє уникнути копіювання при передачі rvalue-вираження як аргумент функції. Але вистачить мучити книгу Мейерса, давайте вже перейдемо до іншої книги.

До речі, якщо хтось читав книгу і дивується, що я не наводжу тут варіант з тим, що Мейєрс називав універсальними посиланнями (universal references) - тепер вони відомі як посилання, що прокидаються (forwarding references), - то це легко пояснюється. Я розглядаю лише ППЗ та ППСК, т.к. вводити шаблонні функції для методів, які шаблонами не є, тільки для того, щоб підтримати передачу за посиланнями обох типів (rvalue/lvalue) вважаю поганим тоном. Не кажучи вже про те, що код виходить іншим (більше немає константності) і несе інші проблеми.

Джосаттіс та компанія

Останньою книгою ми розглянемо «C++ Templates», вона є найсвіжішою зі всіх згаданих у цій статті книг. Вийшла вона під кінець 2017 року (а всередині книги взагалі 2018 вказано). На відміну від інших книг, ця цілком присвячена шаблонам, а не порадам (як у Мейерса) або C++ загалом, як у Страуструпа. Тому плюси/мінуси тут розглядаються з погляду написання шаблонів.

Ця тема присвячена ціла глава 7, яка має промовисту назву «By value or by reference?». У цьому розділі автори досить коротко, але ємно описують усі методи передачі з усіма їх плюсами та мінусами. Аналіз ефективності тут практично не наводиться, і як належне приймається те, що ППСК буде швидше за ППЗ. Але при всьому цьому в кінці глави автори рекомендують використовувати ППЗ для шаблонних функцій за умовчанням. Чому? Тому що використовуючи посилання, шаблонні параметри виводяться повністю, а без посилання розкладаються (decay), що сприятливо позначається на обробці масивів і рядкових літералів. Автори вважають, що якщо для якогось типу ППЗ виявиться неефективним, то можна використовувати std::ref і std::cref . Така собі порада, чесно кажучи, багато ви бачили бажаючих використовувати вищезазначені функції?

Що ж вони радять щодо ППСК? Вони радять використовувати ППСК тоді, коли продуктивність критична чи є інші вагоміпричини не використовувати ППЗ. Звичайно, ми тут говоримо лише про шаблонний код, але ця порада прямо суперечить усьому, чому навчали програмістів упродовж десятка років. Це не просто порада розглянути ППЗ як альтернативу – ні, це порада альтернативою зробити ППСК.

У цьому завершимо наш книжковий тур, т.к. мені не відомі інші книги, з якими нам варто було б ознайомитися з цього питання. Перейдемо в інший медіапростір.

Мережева мудрість

Т.к. живемо у вік інтернету, то на одну книжкову мудрість покладатися не варто. Тим більше, що багато авторів, які раніше писали книжки, наразі просто пишуть блоги, а від книжок відмовилися. Одним з таких авторів є Герб Саттер, який у травні 2013 року опублікував у своєму блозі статтю «GotW #4 Solution: Class Mechanics» , яка хоч і не є цілком присвяченою проблемі, що висвітлюється нами, все-таки зачіпає її.

Отже, у первісному варіанті статті Саттер просто повторив стару мудрість: «Передавайте параметри за посиланням на константу», але цього варіанта статті ми не побачимо, т.к. у статті знаходиться зворотна порада: « якщопараметр все одно буде скопійовано, тоді передавайте його за значенням. Знову горезвісне «якщо». Чому Саттер змінив статтю, і звідки я про це дізнався? З коментарів. Почитайте коментарі до його статті, вони, до речі, цікавіші та корисніші за саму статтю. Щоправда, вже після написання статті, Саттер таки змінив свою думку і такої поради він більше не дає. Думку, що змінилася, можна виявити в його виступі на CppCon у 2014 році: «Back to the Basics! Essentials of Modern C++ Style». Подивіться обов'язково, ми ж перейдемо до наступного інтернет-посилання.

А на черзі ми маємо головний програмістський ресурс 21 століття: StackOverflow. А точніше відповідь, з кількістю позитивних реакцій, що перевищує 1700 на момент написання цієї статті. Запитання звучить так: What is the copy-and-swap idiom? , і, як має бути зрозуміло з назви, не зовсім на тему, що ми розглядаємо. Але у своїй відповіді на це питання, автор зачіпає і тему, яка нас цікавить. Він також радить використовувати ППЗ «якщо аргумент все одно буде скопійований» (пора вже і на це абревіатуру вводити їй Богу). І в цілому ця порада виглядає цілком доречною, в рамках її відповіді та обговорюваного там operator = , але автор бере на себе сміливість давати таку пораду в ширшому ключі, а не тільки в цьому окремому випадку. Більше того, він йде далі за всі розглянуті нами раніше поради і закликає робити це навіть у C++03 коді! Що ж спонукало автора на подібні висновки?

Зважаючи на все, основне натхнення автор відповіді черпав зі статті ще одного книжкового автора та за сумісництвом розробника Boost.MPL – Дейва Абрахамса. Стаття називається Want Speed? Pass by Value.» , і вона була опублікована ще серпні 2009 року, тобто. за 2 роки до ухвалення C++11 та введення семантики переміщення. Як і в попередніх випадках, рекомендую читачеві самостійно ознайомитися зі статтею, я ж наведу основні докази (довод, по суті, один), які Дейв наводить на користь ППЗ: потрібно використовувати ППЗ, тому що з ним добре працює оптимізація «перепустка копіювання» ( copy elision), яка відсутня при ППСК. Якщо почитати коментарі до статті, то можна побачити, що порада, що їм просувається, не є універсальною, що підтверджує сам автор, відповідаючи на критику коментаторів. Тим не менш, стаття містить явну пораду (guideline) використовувати ППЗ, якщо аргумент все одно буде скопійований. До речі, кому цікаво, можете прочитати статтю «Want speed? Don't (always) pass by value.» . Як має бути ясно з назви, ця стаття є відповіддю на статтю Дейва, так що якщо прочитали першу, то і цю прочитайте обов'язково!

На жаль (для когось на щастя), такі ось статті та (тим більше) популярні відповіді на популярних сайтах породжують масове застосування сумнівних технік (банальний приклад) просто тому, що так потрібно менше писати, а стара догма більше не є непорушною. завжди можна послатися на «він та популярна порада», якщо тебе припруть до стінки. Тепер пропоную ознайомитися з тим, що нам пропонують різні ресурси з рекомендаціями щодо написання коду.

Т.к. різні стандарти та рекомендації зараз теж розміщуються в мережі, то я вирішив віднести цей розділ до «мережевої мудрості». Отже, тут я хотів би поговорити про два джерела, призначення яких - зробити код C++ програмістів кращим, шляхом надання останнім порад (guidelines) по тому, як цей код писати.

Перший набір правил, який я хочу розглянути, став останньою краплею, яка змусила мене все-таки взятися за цю статтю. Цей набір є частиною утиліти clang-tidy і поза не існує. Як і все, що пов'язано з clang, ця утиліта дуже популярна і вже отримала інтеграцію з CLion та Resharper C++ (саме так я з нею зіштовхнувся). Отже, clang-tydy містить правило modernize-pass-by-value, яке спрацьовує на конструкторах, що приймають аргументи за допомогою ППСК. Це правило пропонує нам замінити ППСК на ППЗ. Більше того, на момент написання статті в описі цього правила міститься ремарка, що це правило Бувайпрацює лише для конструкторів, але вони (хто вони?) із задоволенням приймуть допомогу від тих, хто поширить це правило на інші сутності. Там же, в описі, є й посилання на статтю Дейва – зрозуміло, звідки ноги ростуть.

Нарешті, на завершення розгляду чужої мудрості та авторитетних думок, пропоную подивитися на офіційні рекомендації щодо написання C++ коду: C++ Core Guidelines, основними редакторами яких є Герб Саттер і Б'ярн Страуструп (непогано, правда?). Так ось, ці рекомендації містять наступне правило: «For “in” parameters, pass cheaply-copied types by value and others by reference to const» , яке повторює стару мудрість: ППСК скрізь і ППЗ для невеликих об'єктів. В описі цієї поради наводяться кілька альтернатив, які пропонується розглянути якщо передача аргументів потребує оптимізації. Але у списку альтернатив ППЗ не представлено!

Так як більше джерел, що заслуговують на увагу, у мене немає, пропоную перейти до безпосереднього аналізу обох методів передачі.

Аналіз

Весь попередній текст написаний у дещо не властивій мені манері: я наводжу чужі думки і намагаюся не висловлювати своє (знаю, що виходить погано). Багато в чому той факт, що думки чужі, а моєю метою було зробити їх короткий оглядя відкладав детальний розгляд тих чи інших аргументів, які знаходив в інших авторів. У цьому ж розділі я не посилатимуся на авторитети та наводити думки, тут ми розглянемо деякі об'єктивні переваги та недоліки ППСК та ППЗ, які будуть приправлені моїм суб'єктивним сприйняттям. Звичайно, дещо з розглянутого раніше буде повторюватися, але, на жаль, така структура цієї статті.

А чи є перевага у ППЗ?

Отже, перш ніж розглядати аргументи "за" і "проти", я пропоную подивитися яку і в яких випадках перевагу нам дає передача за значенням. Нехай у нас є якийсь такий клас:

Class CopyMover ( public: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByValuerAndNotMover(Accounter byValuerAndNotMover) ( m_ByValuerAndNotMover = byValuerAndNotMover; ) void setRvaluer (Accounter& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Хоча в рамках цієї статті нам цікаві лише перші дві функції, я навів чотири варіанти, щоб просто використовувати їх як контраст.

Клас Accounter - це простий клас, який вважає, скільки разів він був скопійований/переміщений. А у класі CopyMover у нас реалізовані функції, які дозволяють розглянути такі варіанти:

    переміщеннямпереданого аргументу.

    Передача за значенням, з наступним копіюваннямпереданого аргументу.

Тепер, якщо ми передаємо lvalue в кожну з цих функцій, наприклад ось так:

Accounter byRefer; Accounter byValuer; Accounter byValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

то отримаємо такі результати:

Очевидним переможцем є ППСК, т.к. дає лише одне копіювання, тоді як ППЗ дає одне копіювання та одне переміщення.

Тепер спробуємо передати rvalue:

CopyMover copyMover; copyMover.setByRefer(Accounter()); copyMover.setByValuer(Accounter()); copyMover.setByValuerAndNotMover(Accounter()); copyMover.setRvaluer(Accounter());

Отримаємо таке:

Тут однозначного переможця немає, т.к. і в ППЗ, і в ППСК по одній операції, але через те, що ППЗ використовує переміщення, а ППСК - копіювання, можна віддати перемогу ППЗ.

Але на цьому експерименти наші не закінчуються, давайте додамо такі функції для імітації непрямого виклику (з наступною передачею аргументу):

Void setByValuer(Accounter byValuer, CopyMover& copyMover) ( copyMover.setByValuer(std::move(byValuer))) ;

Використовувати їх ми будемо так само, як робили без них, тому повторювати код не стану (дивіться у сховище, якщо потрібно). Отже, для lvalue результати будуть такими:

Зверніть увагу, що ППСК збільшує розрив з ППЗ, залишаючись з єдиною копією, тоді як у ППЗ вже цілих 3 операції (на одне переміщення більше)!

Тепер передаємо rvalue та отримуємо такі результати:

Тепер ППЗ має 2 переміщення, а ППСК все те саме копіювання. Чи можна тепер висунути ППЗ у переможці? Ні, т.к. якщо одне переміщення має бути як мінімум не гірше, ніж одне копіювання, про два переміщення ми подібного сказати вже не можемо. Тому переможця у цьому прикладі не буде.

Мені можуть заперечити: «Авторе, у Вас упереджена думка і Ви притягуєте за вуха те, що Вам вигідно. Навіть 2 переміщення будуть дешевшими ніж копіювання!». Я не можу погодитися з таким твердженням в загальному, т.к. те, наскільки переміщення швидше за копіювання залежить від конкретного класу, але ми ще розглянемо «дешеве» переміщення в окремому розділі.

Тут ми торкнулися цікавої речі: ми додали один непрямий виклик, і ППЗ додало у «ваги» рівно одну операцію. Думаю, що не потрібно мати диплом МДТУ для розуміння того, що чим більше непрямих викликів ми маємо, тим більше операцій буде виконано при використанні ППЗ, тоді як для ППСК кількість залишатиметься незмінною.

Все розглянуте вище навряд чи стало для когось одкровенням, ми могли навіть не проводити експериментів - всі ці числа мають бути очевидними для більшості C++ програмістів з першого погляду. Правда, один момент все ж таки заслуговує на пояснення: чому у випадку з rvalue у ППЗ немає копіювання (або ще одного переміщення), а є тільки одне переміщення.

Що ж, ми розглянули різницю у передачі між ППЗ та ППСК, наочно поспостерігавши за кількістю копій та переміщень. Хоча очевидно, що перевага ППЗ над ППСК навіть у таких простих прикладах м'яко кажучи неочевидно, я все ж таки, трохи кривлячи душею, зроблю наступний висновок: якщо ми все одно копіюватимемо аргумент функції, то є сенс розглянути передачу аргументу в функцію за значенням. Навіщо я зробив цей висновок? Щоб плавно перейти до наступного розділу.

Якщо копіюємо...

Отже, ми дісталися горезвісного «якщо». Більшість зустрінутих нами аргументів не закликали повсюдно впроваджувати ППЗ замість ППСК, вони лише закликали робити це «якщо все одно аргумент буде скопійовано». Настав час розібратися, що не так із цим аргументом.

Хочу почати з невеликого опису, як я пишу код. Останнім часом мій процес написання коду дедалі більше схожий на TDD, тобто. написання будь-якого методу класу починається з написання тесту, у якому цей метод фігурує. Відповідно, починаючи писати тест, і створюючи після написання тесту метод, я ще не знаю чи буду копіювати аргумент. Звичайно, не всі функції створюються таким чином, часто ще в процесі написання тесту ти точно знаєш, що там буде за реалізація. Але ж так відбувається не завжди!

Хтось може мені заперечити, що не важливо як метод був написаний спочатку, ми можемо змінити те, як ми передаємо аргумент тоді, коли метод знайшов плоть і нам цілком ясно, що там відбувається (тобто є чи ні у нас копіювання ). Я з цим частково погоджуся - дійсно, можна чинити і так, але це залучає нас до якоїсь дивної гри, де ми повинні змінювати інтерфейси лише тому, що реалізація змінилася. Що призводить нас до наступної проблеми.

Виходить, що ми модифікуємо (або взагалі плануємо) інтерфейс виходячи з того, як він буде реалізований. Не вважаю себе експертом з ОВП та інших теоретичних викладок архітектури ПЗ, але подібні дії явно суперечать базовим правилам, коли реалізація не повинна впливати на інтерфейс. Звичайно, певні деталі реалізації (будь то особливості мови або цільової платформи) так чи інакше все одно просочуються через інтерфейс, але потрібно намагатися скорочувати, а не збільшувати кількість таких речей.

Ну та Бог з ним, нехай ми пішли цим шляхом і все-таки змінюємо інтерфейси в залежності від того, що ми робимо в реалізації в частині копіювання аргументу. Припустимо, ми написали такий метод:

Void setName(Name name) ( m_Name = move(name); )

та зафіксували наші зміни у сховищі. Минав час, наш програмний продукт обростав новим функціоналом, інтегрувалися нові фреймворки, і постало завдання повідомляти зовнішньому світупро зміни у нашому класі. Тобто. до нашого методу додасться деякий механізм повідомлення, нехай це буде схоже на сигнали Qt:

Void setName(Name name) ( m_Name = move(name); emit nameChanged(m_Name); )

Чи є у цьому коді проблема? Є. На кожен виклик setName ми надсилаємо сигнал, тому сигнал буде надісланий навіть тоді, коли значення m_Name не змінилося. Крім питань продуктивності, така ситуація може призвести до нескінченного циклу через те, що код, який отримує зазначене повідомлення, якимось чином приходить до того, щоб викликати setName . Щоб уникнути всіх цих проблем, подібні методи найчастіше виглядають приблизно так:

Void setName(Name name) ( if(name == m_Name) return; m_Name = move(name); emit nameChanged(m_Name); )

Ми позбулися вищеописаних проблем, але тепер наше правило «якщо все одно копіюємо...» дало збій – більше немає безумовного копіювання аргументу, тепер ми його копіюємо лише за умови зміни! І що нам тепер робити? Змінювати інтерфейс? Добре, змінимо інтерфейс класу через це виправлення. А якщо наш клас успадкував цей метод з деякого абстрактного інтерфейсу? Поміняємо і там! Чи не багато змін через те, що змінилася реалізація?

Знову мені можуть заперечити, мовляв автор, ти чого тут на сірниках економити надумав, коли там ця умова відпрацює? Та більшість викликів воно буде помилковим! А у цьому є впевненість? Звідки? І якщо я вирішив заощаджувати на сірниках, то хіба сам факт того, що ми використовували ППЗ, чи не був наслідком саме такої економії? Я лише продовжую «лінію партії», яка бореться за ефективність.

Конструктори

Коротко пройдемося і по конструкторах, тим більше, що для них є спеціальне правило в clang-tidy, яке для інших методів/функції поки не працює. Припустимо, у нас є такий клас:

Class JustClass ( public: JustClass(const string& justString): m_JustString(justString) ( ) private: string m_JustString; );

Очевидно, що параметр копіюється, і clang-tidy нам повідомить, що було б непогано переписати конструктор на такий:

JustClass(string justString): m_JustString(move(justString)) ( )

І мені, чесно кажучи, складно тут заперечити - адже й справді завжди копіюємо. І найчастіше, коли ми передаємо щось через конструктор, ми це щось копіюємо. Але найчастіше не означає завжди. Ось вам інший приклад:

Class TimeSpan ( public: TimeSpan(DateTime start, DateTime end) ( if(start > end) throw InvalidTimeSpan(); m_Start = move(start); m_End = move(end); ) private: DateTime m_Start; DateTime m_End; );

Тут ми копіюємо не завжди, а лише тоді, коли дати подано коректно. Звісно, ​​у переважній більшості випадків так і буде. Але не завжди.

Можна навести ще один приклад, але цього разу без коду. Уявіть, що ви маєте клас, який приймає великий об'єкт. Клас існує давно, і ось настав час його реалізацію поновити. Ми усвідомлюємо, що від великого об'єкта (який виріс за ці роки) нам потрібно не більше половини, а може й менше. Чи можемо ми щось із цим зробити, маючи передачу за значенням? Ні, ми нічого зробити не зможемо, тому що копія все одно створюватиметься. А от якби ми використовували ППСК, то просто змінили б те, що ми робимо всерединіконструктора. І це ключовий момент: використовуючи ППСК ми контролюємо, що і коли відбувається в реалізації нашої функції (конструктора), якщо ми використовуємо ППЗ, то ми втрачаємо будь-який контроль над копіюванням.

Що можна винести із цього розділу? Те, що аргумент «якщо однаково копіюємо...» є дуже спірним, т.к. далеко не завжди ми знаємо, що копіюватимемо, а навіть коли знаємо, ми дуже часто не впевнені в тому, що це так і продовжуватиметься надалі.

Переміщення дешево

З самого моменту появи семантики переміщення, вона почала серйозно впливати на те, як пишеться сучасний C++-код, і за минулий час цей вплив тільки посилився: він і не дивний, адже переміщення так дешевопроти копіюванням. Але чи це так? Чи правда, що переміщення це завждидешева операція? Ось з цим ми спробуємо розібратися в цьому розділі.

Великий двійковий об'єкт

Почнемо з банального прикладу, хай ми маємо такий клас:

Struct Blob ( std::array data; );

Звичайний великий двійковий об'єкт(БДО, англ. BLOB), який може застосовуватися в різних ситуаціях. Давайте розглянемо, що нам буде коштувати передача за посиланням і за значенням. Використовуватиметься наш БДО приблизно так:

Void Storage::setBlobByRef(const Blob& blob) ( m_Blob = blob; ) void Storage::setBlobByVal(Blob blob) ( m_Blob = move(blob); )

А викликатимемо ці функції так:

Const Blob blob(); Storage storage; storage.setBlobByRef(blob); storage.setBlobByVal(blob);

Код для інших прикладів буде ідентичний цьому, тільки з іншими іменами і типами, тому наводити для прикладів, що залишилися, я його не стану - все є в сховищі.

Перш ніж перейдемо до вимірювань, спробуємо передбачити результат. Отже, у нас є std::array розміром 4 Кб, який ми хочемо зберегти в об'єкті класу Storage . Як ми з'ясували раніше, для ППСК у нас буде одне копіювання, тоді як для ППЗ буде одне копіювання та одне переміщення. Виходячи з того, що array перемістити неможливо, для ППЗ буде 2 копіювання, проти одного для ППСК. Тобто. ми маємо право очікувати дворазової переваги у продуктивності для ППСК.

Тепер погляньмо на результати тестування:

Цей та всі наступні тести виконувались на одній машині з використанням MSVS 2017 (15.7.2) та з прапором /O2 .

Практика збіглася з припущенням - передача за значенням виходить у 2 рази дорожче, тому що для array переміщення повністю еквівалентне копіюванню.

Рядок

Розглянемо інший приклад, звичайний рядок std::string. Що ми можемо очікувати? Ми знаємо (я розглядав це у статті), що сучасні реалізації розрізняють string двох типів: короткі (близько 16 символів) і довгі (ті, що більші за короткі). Для коротких використовується внутрішній буфер, який є звичайним C-масивом з char, а ось довгі вже будуть розміщуватися в купі. Короткі рядки нас цікавлять, т.к. результат там буде той самий, що і з БДО, тому зосередимося на довгих рядках.

Отже, маючи довгий рядок, очевидно, що його переміщення має бути досить дешево (просто перемістити покажчик), тому можна розраховувати на те, що переміщення рядка не повинно взагалі ніяк позначитися на результатах, і ППЗ має дати результат не гірше за ППСК. Перевіримо на практиці та отримаємо такі результати:

Ми ж перейдемо до пояснення цього «феномену». Отже, що відбувається, коли ми копіюємо існуючий рядок у вже існуючий рядок? Давайте розглянемо банальний приклад:

String first(64, "C"); string second(64, "N"); //... second = first;

У нас два рядки розміром 64 символи, тому при їх створенні внутрішнього буфера недостатньо, в результаті обидва рядки розміщуються в купі. Тепер ми копіюємо першу в second . Т.к. розміри рядків у нас однакові, очевидно, що у second виділено достатньо місця, щоб вмістити всі дані з first тому second = first; буде банальним memcpy , не більше того. Але якщо ми розглянемо трохи змінений приклад:

String first(64, "C"); string second = first;

то тут не буде виклику operator= , але буде викликаний конструктор копіювання. Т.к. ми маємо справу з конструктором, то існуючої пам'яті у ньому немає. Її спочатку треба виділити і потім скопіювати first . Тобто. це виділення пам'яті, а потім memcpy. Як ми з вами знаємо, виділення пам'яті в глобальній купі це, як правило, дорога операція, тому копіювання з другого прикладу буде дорожчим за копіювання з першого. Найдорожче на одне виділення пам'яті в купі.

Яке це стосується нашої теми? Найпряміше, адже перший приклад показує те, що відбувається при ППСК, а другий те, що відбувається при ППЗ: для ППЗ завжди створюється новий рядок, тоді як для ППСК відбувається перевикористання існуючої. Різницю в часі виконання ви вже бачили, тому тут додати нічого.

Тут ми знову зіткнулися з тим, що при використанні ППЗ ми працюємо поза контекстом, тому не можемо користуватися всіма благами, які він може надати. І якщо раніше ми міркували в рамках теоретичних майбутніх змін, то тут ми спостерігаємо конкретний провал у продуктивності.

Мені, безперечно, можуть заперечити, що, мовляв, string стоїть окремо, і більшість типів так не працюють. На що я можу відповісти наступне: все, що описано раніше, буде справедливо для будь-якого контейнера, який виділяє пам'ять у купі відразу під пачку елементів. Крім того, хто знає, які інші контекстно-залежні оптимізації застосовуються в інших типах?

Що варто отримати з цього розділу? Те, що навіть якщо переміщення дійсно дешеве, не означає того, що заміщення копіювання на копіювання+переміщення завжди даватиме результат порівнянний за продуктивністю.

Складний тип

Зрештою, давайте розглянемо тип, який складатиметься з кількох об'єктів. Нехай це буде клас Person, який складається з даних притаманної будь-якої особистості. Зазвичай це ім'я, прізвище, поштовий індекс тощо. Все це можна уявити у вигляді рядків і припустити, що рядки, що містяться в поля класу Person, швидше за все, будуть короткими. Хоча я і вважаю, що для реального життя найбільш корисним буде вимір саме коротких рядків, ми все ж таки розглянемо рядки різних розмірів, щоб картина вийшла повнішою.

Також я використовуватиму Person з 10-ма полями, але для цього не буду створювати 10 полів прямо в тілі класу. Реалізація Person приховує у своїх надрах контейнер - так зручніше змінювати параметри тестів, практично не уникаючи того, як це працювало, якби Person реальним класом. Тим не менш, реалізація доступна і ви завжди можете перевірити код і вказати мені, якщо я щось не так зробив.

Отже, поїхали: Person з 10 полями типу string , який ми передаємо за допомогою ППСК і ППЗ в Storage :

Як ви можете бачити, ми маємо колосальну різницю у продуктивності, що після попередніх розділів не мало стати для читачів несподіванкою. Також я вважаю, що клас Person є достатньо «реальним», щоб не відкидати подібні результати як абстрактні.

До речі, коли я готував цю статтю, підготував ще один приклад: клас, який використовує кілька об'єктів std::function . За моїм задумом він теж повинен був показати перевагу у продуктивності ППСК над ППЗ, але вийшло навпаки! Але я не наводжу цей приклад тут не тому, що мені не сподобалися результати, а тому, що в мене не знайшлося часу розібратися, чому ж такі результати виходять. Проте код у сховищі є (Printers), тести – теж, якщо у когось є бажання розібратися, я був би радий почути про результати дослідження. Я ж планую повернутися до цього прикладу пізніше, і якщо ніхто цих результатів до мене не опублікує, то я розгляну їх в окремій статті.

Підсумки

Отже, ми розглянули різні плюси та мінуси передачі за значенням та за посиланням на константу. Розглянули деякі приклади та подивилися на продуктивність обох методів у цих прикладах. Зрозуміло, ця стаття не може і не є вичерпною, але, на мій погляд, у ній достатньо інформації, щоб ухвалити самостійне і зважене рішення щодо того, який спосіб краще використовувати. Хтось може заперечити: "навіщо використовувати один спосіб, давайте відштовхуватися від завдання!". Хоча я згоден із цією тезою у загальному вигляді, я не згоден з нею в даній ситуації. Я вважаю, що в мові може бути лише один спосіб передачі аргументів, який використовується за умовчанням.

Що означає за умовчанням? Це означає, що коли я пишу функцію, я не думаю про те, як мені передавати аргумент, я просто використовую «замовчання». Мова C++ є досить складною мовою, яку багато хто обходить стороною. І на мою думку, складність викликана не стільки складністю мовних конструкцій, які є в мові (типовий програміст може з ними ніколи не зіткнутися), скільки тим, що мова змушує дуже багато думати: чи я звільнив пам'ять, чи не дорого використовувати тут цю функцію і т.п.

Багато програмістів (C, C++ та інші) з недовірою та страхом ставляться до того C++, який став виявлятися після 2011 року. Я чув чимало критики, що мова стає складнішою, що писати нею тепер можуть лише «гуру» тощо. Особисто я вважаю, що це не так - комітет навпаки багато часу приділяє тому, щоб мова стала більш доброзичливою до новачків і щоб програмістам менше потрібно було думати над особливостями мови. Адже якщо нам не потрібно боротися з мовою, то час подумати над завданням. До цих спрощень я відношу і розумні покажчики, і лямбда-функції та багато іншого, що з'явилося в мові. При цьому я не заперечую того факту, що вивчати тепер треба більше, але що поганого у навчанні? Чи в інших популярних мовах немає змін, які потрібно вивчати?

Далі, я не сумніваюся, що знайдуться сноби, які можуть сказати у відповідь: Думати не хочеться? Іди тоді на PHP пиши». Таким людям я навіть не хочу відповідати. Наведу лише приклад із ігрової дійсності: у першій частині Starcraft, коли новий робітник створюється в будівлі, то щоб він почав добувати мінерали (або газ), потрібно було його вручну туди послати. Більш того, кожна пачка мінералів мала ліміт, при досягненні якого нарощування робітників було марним, і вони навіть могли заважати один одному, погіршуючи видобуток. У Starcraft 2 це змінили: робітники автоматично починають видобувати мінерали (або газ), а також вказується скільки робітників зараз видобувають та скільки ліміт цього родовища. Це дуже спростило взаємодію гравця з базою, дозволивши йому зосередитися на більш важливих аспектахігри: побудова бази, накопичення військ та знищення противника. Здавалося б, це просто відмінне нововведення, але що почалося в мережі! Люди (хто вони?) почали верещати, що гра «оказується» і «вони вбили Starcraft». Очевидно, що такі повідомлення могли виходити тільки від «охоронців таємного знання» та «адептів високого APM», яким подобалося перебувати у певному «елітному» клубі.

Так от, повертаючись до нашої теми, чим менше мені потрібно думати над тим, як мені писати код, тим більше мені залишається часу на те, щоб думати над вирішенням безпосереднього завдання. Думати над тим, який метод мені використовувати – ППСК чи ППЗ – ні на йоту не наближає мене до вирішення завдання, тому думати над такими речами я просто відмовляюся і вибираю один варіант: передача за посиланням на константу. Чому? Тому що я не бачу жодних переваг у ППЗ у загальних випадках, а окремі випадки потрібно розглядати окремо.

Окремий випадок, він на те й окремий, що помітивши те, що в якомусь методі ППСК виявляється вузьким місцем, і, змінивши передачу на ППЗ, ми отримаємо важливий приріст у продуктивності, я не замислююся застосую ППЗ. Але за умовчанням я застосовуватиму ППСК як у звичайних функціях, так і в конструкторах. І по можливості пропагуватиму саме цей спосіб скрізь, де тільки можна. Чому? Тому що вважаю практику пропаганди ППЗ порочною через те, що левова частка програмістів не надто обізнані (або в принципі, або ще просто не ввійшли в курс справи), і вони просто дотримуються порад. Плюс, якщо є кілька порад, що суперечать один одному, то вони вибирають той, що простіше, а це призводить до того, що в коді з'являється песимізація просто тому, що хтось десь щось чув. Ах так, ще цей хтось може привести посилання на статтю Абрахамса, щоб довести, що він має рацію. А ти потім сидиш, читаєш код і думаєш: а ось те, що тут параметр передається за значенням, це тому що програміст, який це писав, прийшов з Java, просто начитався «розумних» статей чи справді потрібно ППЗ?

ППСК читається набагато простіше: людина явно знає «хороший тон» C++ і ми йдемо далі – погляд не затримується. Практика застосування ППСК викладалася програмістам C++ роками, яка причина від неї відмовлятися? Це призводить до ще одного висновку: якщо в інтерфейсі методу використовується ППЗ, значить там же повинен бути коментар, чому це саме так. В інших випадках має застосовуватись ППСК. Зрозуміло, є типи-виключення, але про це не згадую тут просто тому, що це мається на увазі: string_view , initializer_list , різні ітератори і т.п. Але це винятки, список яких може розширюватись залежно від того, які типи використовуються у проекті. Але суть залишається незмінною з часів C++98: за умовчанням ми завжди застосовуємо ППСК.

Для std::string різниці на маленьких рядках, швидше за все, не буде, ми поговоримо про це пізніше.

Програмування методів із використанням рядків

Ціль лабораторної роботи : вивчити методи у мові C#, правила роботи з символьними даними та компонентом ListBox. Написати програму для роботи з рядками.

Методи

Метод є елементом класу, який містить програмний код. Метод має таку структуру:

[атрибути] [спеціфіктори] тип ім'я ([параметри])

Тіло методу;

Атрибути - це спеціальні вказівки компілятора на властивості способу. Атрибути використовують рідко.

Специфікатори – це ключові слова, призначені для різних цілей, наприклад:

· Визначають доступність методу для інших класів:

o private– метод буде доступний лише всередині цього класу

o protected– метод буде доступний також дочірнім класам

o public– метод буде доступний будь-якому іншому класу, який може отримати доступ до цього класу

· Вказівні доступність методу без створення класу

· Задають тип

Тип визначає результат, який повертає метод: це може бути будь-який тип, доступний C#, а також ключове слово void, якщо результат не потрібен.

Ім'я методу – це ідентифікатор, який використовуватиметься для виклику методу. До ідентифікатора застосовуються ті самі вимоги, що й до імен змінних: він може складатися з букв, цифр і символу підкреслення, але не може починатися з цифри.

Параметри – це список змінних, які можна передавати до методу виклику. Кожен параметр складається з типу та назви змінної. Параметри розділяються комою.

Тіло методу – це звичайний програмний код, за винятком того, що він не може містити визначення інших методів, класів, просторів імен тощо. значенням. Якщо повернення результатів не потрібне, використання ключового слова return не обов'язково, хоча і допускається.

Приклад методу, що обчислює вираз:

public double Calc(double a, double b, double c)

return Math.Sin(a) * Math.Cos(b);

double k = Math.Tan (a * b);

return k * Math.Exp (c / k);

Перевантаження методів

Мова C# дозволяє створювати кілька методів із однаковими іменами, але різними параметрами. Компілятор автоматично підбере найбільш підходящий метод під час побудови програми. Наприклад, можна написати два окремих методи зведення числа в ступінь: для цілих чисел застосовуватиметься один алгоритм, а для речових – інший:

///

/// Обчислення X у ступені Y для цілих чисел

///

private int Pow(int X, int Y)

///

/// Обчислення X у ступені Y для дійсних чисел

///

private double Pow(double X, double Y)

return Math.Exp(Y * Math.Log(Math.Abs(X)));

else if (Y == 0)

Викликається такий код однаково, різниця лише в параметрах - у першому випадку компілятор викличе метод Pow з цілими параметрами, а в другому - з речовими:

Параметри за замовчуванням

Мова C#, починаючи з версії 4.0 (Visual Studio 2010), дозволяє задавати деяким параметрам значення за замовчуванням – так, щоб при виклику методу можна було опускати частину параметрів. Для цього при реалізації методу потрібним параметрам слід присвоїти значення у списку параметрів:

private void GetData(int Number, int Optional = 5 )

Console.WriteLine("Number: (0)", Number);

Console.WriteLine("Optional: (0)", Optional);

У цьому випадку викликати метод можна так:

GetData(10, 20);

У першому випадку параметр Optional дорівнюватиме 20, так як він явно заданий, а в другому дорівнюватиме 5, т.к. явно не заданий і компілятор бере значення за замовчуванням.

Параметри за замовчуванням можна ставити лише у правій частині списку параметрів, наприклад, така сигнатура методу компілятором не буде прийнята:

private void GetData(int Optional = 5 , int Number)

Коли параметри передаються методом звичайним чином (без додаткових ключових слів ref і out), будь-які зміни параметрів усередині методу не впливають на його значення в основній програмі. Припустимо, ми маємо наступний метод:

private void Calc(int Number)

Видно, що всередині методу відбувається зміна змінної Number, яка була передана як параметр. Спробуємо викликати метод:

Console.WriteLine(n);

На екрані з'явиться число 1, тобто, незважаючи на зміну змінної в методі Calc, змінної в головній програмі не змінилося. Це з тим, що з виклику методу створюється копіяпереданої змінної, саме її змінює метод. Після завершення методу значення копій втрачається. Такий спосіб передачі параметра називається передачею за значенням.

Щоб метод міг змінювати передану йому змінну, її слід передавати з ключовим словом ref – воно має бути як у сигнатурі методу, так і за виклику:

private void Calc(ref int Number)

Console.WriteLine(n);

У цьому випадку на екрані з'явиться число 10: зміна значення методу позначилося і на головній програмі. Така передача методу називається передачею за посиланням, тобто. передається не копія, а посилання реальну змінну у пам'яті.

Якщо метод використовує змінні за посиланням тільки для повернення значень і йому не важливо що в них спочатку, то можна не ініціалізувати такі змінні, а передавати їх з ключовим словом out. Компілятор розуміє, що початкове значення змінної не є важливим і не лається на відсутність ініціалізації:

private void Calc(out int Number)

int n; // Нічого не привласнюємо!

Тип даних string

Для зберігання рядків у мові C# використовується тип string. Для того, щоб оголосити (і, як правило, одразу ініціалізувати) рядкову змінну, можна написати наступний код:

string a = "Текст";

string b = "рядки";

Над рядками можна виконувати операцію додавання – у цьому випадку текст одного рядка буде додано до тексту іншого:

string c = a + " " + b; // Результат: Текст рядка

Тип string насправді є псевдонімом для класу String, за допомогою якого над рядками можна виконувати низку складніших операцій. Наприклад, метод IndexOf може здійснювати пошук підрядки у рядку, а метод Substring повертає частину рядка вказаної довжини, починаючи з вказаної позиції:

string a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int index = a.IndexOf("OP"); // Результат: 14 (рахунок з 0)

string b = a. Substring (3, 5); //Результат: DEFGH

Якщо потрібно додати до рядка спеціальні символи, це можна зробити за допомогою escape-послідовностей, що починаються зі зворотного слешу:

Компонент ListBox

Компонент ListBoxє список, елементи якого вибираються за допомогою клавіатури або миші. Список елементів задається властивістю Items. Items – це елемент, який має свої властивості та свої методи. Методи Add, RemoveAtі Insertвикористовуються для додавання, видалення та вставки елементів.

Об'єкт Itemsзберігає об'єкти, які у списку. Об'єкт може бути будь-яким класом – дані класу перетворюються для відображення у рядкову виставу методом ToString. У нашому випадку як об'єкт виступатимуть рядки. Однак, оскільки об'єкт Items зберігає об'єкти, приведені до типу object, перед використанням необхідно привести їх назад до початкового типу, у нашому випадку string:

string a = (string) listBox1.Items;

Для визначення номера виділеного елемента використовується властивість SelectedIndex.

Отже, нехай Factorial(n) – це функція обчислення факторіалу числа n. Тоді, враховуючи, що нам відомий факторіал 1 – це 1, можна побудувати наступний ланцюжок:

Factorial(4)=Factorial(3)*4

Factorial(3)=Factorial(2)*3

Factorial(2)=Factorial(1)*2

Але, якби у нас не було термінальної умови, що при n=1 функція Factorial повинна повернути 1, то такий теоретично ланцюжок ніколи не завершився б, і це могло помилкою Call Stack Overflow – переповнення стека виклику. Щоб зрозуміти, що таке стек виклику і як він може переповнитися, давайте подивимося на рекурсивну реалізацію нашої функції:

Function factorial(n: Integer): LongInt;

If n=1 Then

Factorial:=Factorial(n-1)*n;

End;

Як ми бачимо, для того, щоб ланцюжок працював коректно, необхідно перед кожним черговим викликом функції самої себе, десь зберігати всі локальні змінні, щоб потім при зворотному розгортанні ланцюжка результат був правильним (обчислене значення факторіалу від n-1 помножилося на n ). У нашому випадку - при кожному виклик функції factorial з самої себе, повинні зберігатися всі значення змінної n . Область, у якій зберігаються локальні змінні функції при рекурсивному зверненні до себе, називається стеком виклику. Зрозуміло, цей стек не є нескінченним і при неправильній побудові рекурсивних викликів може бути вичерпаний. Кінцевість ітерацій нашого прикладу гарантується тим, що n=1 виклик функції припиниться.

Передача параметрів за значенням та за посиланням

Досі ми не могли змінити в підпрограмі значення фактичного параметра(тобто того параметра, який вказується під час виклику підпрограми), а деяких прикладних завданнях це було зручним. Згадаймо процедуру Val , яка змінює значення відразу двох її фактичних параметрів: перший – це той параметр, куди буде записано перетворене значення рядкової змінної, а другий – це параметр Code , куди міститься номер помилкового символу, у разі невдачі під час перетворення типу. Тобто. Проте існує механізм, з якого підпрограма може змінювати фактичні параметри. Це можливо завдяки різним способам передачі параметрів. Давайте розберемося детально у цих способах.

Програмування мовою Pascal

Передача параметрів за значенням

По суті саме таким чином ми передавали всі параметри в наші підпрограми. Механізм наступний: при вказанні фактичного параметра його значення копіювалося в область пам'яті, де розташовується підпрограма і потім, після того, як функція або процедура завершила свою роботу, ця область очищається. Грубо кажучи, під час роботи підпрограми, існує дві копії її параметрів: одна в області видимості програми, що викликає, а друга – в області видимості функції.

При такому способі передачі параметрів потрібно більше часу на виклик підпрограми, оскільки, крім самого виклику, необхідно скопіювати всі значення всіх фактичних параметрів. Якщо підпрограму передається великий обсяг даних (наприклад, масив з великою кількістю елементів), час, потрібний на копіювання даних у локальну область, може бути суттєвим і це необхідно враховувати при розробці програм і пошуку вузьких місць у їх продуктивності.

При такому способі передачі фактичні параметри не можуть бути змінені підпрограмою, оскільки зміни торкнуться тільки ізольованої локальної області, яка вивільниться після завершення роботи функції або процедури.

Передача параметрів за посиланням

При такому способі значення фактичних параметрів не копіюються в підпрограму, а передаються адреси в пам'яті (посилання на змінні), за якими вони розташовуються. У цьому випадку підпрограма вже змінює значення, що знаходяться не в локальній області, тому всі зміни будуть видні і програмі, що викликає.

Щоб вказати, що будь-який аргумент необхідно передати за посиланням, перед його оголошенням додається ключове словоvar :

Procedure getTwoRandom(var n1, n2:Integer; range: Integer);

n1:=random(range);

n2:=random(range); end;

var rand1, rand2: Integer;

Begin getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

End.

У цьому прикладі в процедуру getTwoRandom в якості фактичних параметрів передаються посилання на дві змінні: rand1 і rand2. Третій фактичний параметр (10) передається за значенням. Процедура записує за допомогою формальних параметрів