Qbik-club
Дата публикации:25.10.21 15:18;Автор:Евгений;Категория: программирование;Теги:, , ;

Передача переменных по ссылке и по указателю в C++

Сегодня я бы хотел продолжить тему переменных и функций в языке С++. В первой части мы узнали, что это такое и зачем нужно. Сегодня же предлагаю углубиться в эту тему дальше и узнать несколько тонкостей и лайфхаков по работе с ними, которые свойственны именно языку С.

Передача переменных по ссылке и по указателю в C++

Функция setlocale

И так, в прошлый раз я упоминал, что если вы попытались вывести на экран русский текст, а у вас вместо этого вывелись различные непонятные символы — в этом нет ничего страшного. Дело в том, что у вас просто неверно указана локаль. Вернее вы её вовсе не указали. По этому давайте рассмотрим такой пример:

int main(){
 setlocale(LC_ALL, "ru_RU.UTF-8");
 cout << "Привет, мир!" << endl;
 return 0;
}

В прошлой публикации я уже упоминал, но на всякий раз ещё раз напомню, что во всех примерах я опускаю «магические строчки», которые нам пока что неизвестны. Если вы копируете код себе, не забывайте, что в начале у вас должны быть #include <iostream> и using namespace std;. Однако было бы лучше, если бы вы не просто копировали код, а именно переписывали, осознавая, зачем нужная каждая строчка и что она делает.

А теперь непосредственно к примеру. Как видим, в начале функции main у нас появился вызов некой функции, которую мы ранее не создавали и которой ранее не было. Дело в том, что «магическая строчка» в начале, подключает в нашу программу не только cout, который позволяет выводить на экран текст, но ещё и делает кучу всего полезного, о чём мы поговорим позже. В данном случае, она ещё и добавляет функцию setlocale, которая позволяет указать, какую кодировку установить в данный поток вывода, чтоб корректно отображать символы, выводимые на экран.

Важно понимать, что вызывать функцию setlocale нужно в самом начале. Ну или по крайней мере до того, как мы начинаем что то отправлять в поток вывода (да, это не обязательно экран, о чём мы поговорим позже). И если вы всё сделали правильно — вы сможете писать не только на Русском, но и на любом другом языке! :)

Сменить локаль в потоке вывода C++

Все возможности данной функции, равно как и другие возможности библиотеки iostream, мы рассмотрим в отдельной публикации, но сегодня я думаю ещё не помешало бы рассказать о том, как посмотреть текущую локаль. Сделать это довольно просто. Мы просто вместо параметра языка передаём NULL. Что это такое — мы тоже рассмотрим в сегодняшней публикации, чуть ниже. Итого у нас получается такой код:

int main(){
 setlocale(LC_ALL, "ru_RU.UTF-8");
 cout << "Привет, мир!" << endl;
 cout << "прывітанне, сябра!" << endl;
 cout << "Текущая локаль: " << setlocale(LC_ALL, NULL) << endl;
 return 0;
}

Просмотреть текущую локаль потока вывода C++

Подробнее о переменных в С++

И так, друзья, мы уже знаем основы. Знаем, что такое переменные, знаем, что такое функции, как выводить любые символы на экран. Наверное пришло время углубиться на вторую ступень. Узнать, что такое переменная не по принципу «это коробочка с данными», а по принципу: Это область оперативной памяти компьютера. Зачем нам это надо? Давайте немного вернёмся к прошлой публикации и вспомним пример кода оттуда:

int GetAge(){
 return 19;
}

void PrintInfo(int UzerAge){
  cout << "Нашему пользователю " << UzerAge << " лет!" << endl;
}

int main(){
 setlocale(LC_ALL, "ru_RU.UTF-8");
 int age = GetAge();
 PrintInfo(age);
 return 0;
}

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

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

Область видимости переменной

Для наглядной демонстрации, давайте рассмотрим такой вариант кода:

int GetAge(){
 return 19;
}

int main(){
 setlocale(LC_ALL, "ru_RU.UTF-8");
 int age = GetAge();
 cout << "Нашему пользователю " << age << " лет!" << endl;
 return 0;
}

Тут я думаю уже очевидно, что сразу мы получаем занчение, затем выводим его на экран. Теперь сравним его с этим кодом:

int GetAge(){
 return 19;
}

void PrintInfo(){
 cout << "Нашему пользователю " << age << " лет!" << endl;
}

int main(){
 setlocale(LC_ALL, "ru_RU.UTF-8");
 int age = GetAge();
 PrintInfo();
 return 0;
}

Давайте попробуем его запустить? Что получится? Получится вот такая ошибка.

Пример ошибки С++

Тут нам компилятор сообщает, что в функции void PrintInfo() нет переменной age! Как её может не быть? Дело в том, что переменная age находится в функции main, а не PrintInfo!

По этому запомните, что каждая переменная — имеет свою область видимости! Эта область видимости начианется в пределах фигурных скобочек. Выглядит это примерно так:

{
тут живут одни переменные
}

{
тут живут другие переменные
}

{
тут живут третьи переменные
}

И эти переменные никак не могут между собой пересекаться. И как раз тут вы можете нарушить то самое правило, о котором я писал в прошлой публикации, когда говорил, что нельзя создавать две переменные с одинаковыми именами. Можно! Если они находятся в разных областях видимости! Примерно вот так:

int GetAge(){
 return 19;
}

void PrintInfo(int age){
 cout << "Сразу нашему пользователю " << age << " лет!" << endl;
 age = 20;
 cout << "Теперь уже " << age << "!" << endl;
}

int main(){
 setlocale(LC_ALL, "ru_RU.UTF-8");
 int age = GetAge();
 PrintInfo(age);
 cout << "А тут всё ещё " << age << "." << endl;
 return 0;
}

И тут я сразу же показал пример того, почему по началу лучше так не делать. Дело в том, что так можно запутать самого себя. Дело в том, что теперь в функции PrintInfo есть переменная, с таким же именем, но при этом не имеющая ничего общего с функцией main. И как видите, я изменил возраст пользователя внутри функции PrintInfo, он действительно изменился, но при этом, в функции main — возраст как им был, таким и остался. И это по началу вас может неплохо так путать. По этому, по началу, я бы рекомендовал называть переменные разными именами даже в случае, когда эти переменные находятся в разных областях видимости.

Пример двух переменных с одинаковым именем в разных областях видимости

Передача по значению, по ссылке и по указателю

И так, пришло время поподробнее познакомиться с передачей переменных из одной функции в другую. И как ни странно, с одним из этих трёх умных терминов мы уже знакомы. Это передача по значению. Это как раз и есть рассмотренный случай выше, когда мы передаём переменную, а в принимающей функции создаётся переменная с точно таким же значением. Если бы мы говорили, к примеру, о языке Js — то собственно на рассказе об областях видимости и передачи переменных мы бы и закончили. Но у языка С++ есть секретный козырь в рукаве! Это возможность передавать переменную по ссылке или по указателю!

Но перед тем, как рассказать о передаче, необходимо немного углубиться в теорию. Что такое переменная? Как помните, переменная — это некое место в оперативной памяти компьютера, где хранится значение, которое мы туда положили. И зная, адрес памяти, а так же зная размер переменной, мы же можем сказать другой функции, что вот по такому то адресу, живёт переменная, читай её оттуда. Да? Да, именно так. Давайте рассмотрим именно такой пример:

int GetAge(){
 return 19;
}

void PrintInfo(int *age){
 cout << "Получили адрес памяти: " << age << endl;
 *age = 20;
 cout << "В PrintInfo() изменили возраст пользователя, теперь ему " << *age << " лет." << endl;
}

int main(){
 setlocale(LC_ALL, "ru_RU.UTF-8");
 int age = GetAge();
 PrintInfo(&age);
 cout << "И в main, теперь пользователю тоже " << age << " лет." << endl;
 return 0;
}

Давайте внимательно посмотрим на вызов функции  PrintInfo(&age);. Как видим, перед передачей переменной age мы поставили амперсанд. Это ни что иное, как оператор взятия адреса. Т.е. мы взяли адрес памяти этой переменной, а затем предали его в функцию.

А в функции void PrintInfo(int *age), в свою очередь, мы поставили звёздочку. Это оператор разыменования. Т.е. поставив звёздочку, мы говорим компилятору, что сюда будет отправлена не сама переменная, а лишь её адрес в памяти, по этому выделять новое место в оперативной памяти компьютера — ненужно, сохрани лишь этот адрес. Давайте попробуем запустить эту программу и посмотреть, что из этого получилось.

Прмиер работы программы с пердачей по указателю

И как мы видим из скриншота, в данном случае есть три важных момента.

  1. Мы не можем просто так обратиться к переменной. Если мы просто обращаемся к переменной — вместо значения мы получаем адрес памяти.
  2. Если нам нужно получить значение переменной, там нужно переменную разыменовывать. Т.е. поставить перед ней звёздочку.
  3. Обращаясь к этой переменной — мы работаем с той же областью памяти, что и в основной функции main. Это значит, что все изменения, произведённые с переменной в функции PrintInfo, сохранятся в перенной из функции main, даже если вы назовёте переменные разными именами.

Теперь давайте попробуем передать значение по указателю. Модифицируем код следующим образом:

int GetAge(){
 return 19;
}

void PrintInfo(int &pipl){
 cout << "Получили адрес памяти: " << &pipl << endl;
 pipl = 20;
 cout << "В PrintInfo() изменили возраст пользователя, теперь ему " << pipl << " лет." << endl;
}

int main(){
 setlocale(LC_ALL, "ru_RU.UTF-8");
 int age = GetAge();
 PrintInfo(age);
 cout << "И в main, теперь пользователю тоже " << age << " лет." << endl;
 return 0;
}

И сразу посмотрим, что изменилось:

Пример передачи переменной по указателю

Как видим, на выводе, единственное, что изменилось — это изменился адрес памяти. Т.к. в этот раз компьютер решил сохранить возраст пользователя в другом месте оперативной памяти. На самом деле, если вы 10 раз запустите эту программу, вы с вероятностью в 99% получите 10 разных адресов. Просто по тому что компьютер постоянно работает, записывает и удаляет различные данные от других, параллельно работающих программ. Но это отдельная тема для разговора. Что изменилось в исходном коде?

Как видим, передача переменной в функции main вернулась к тому варианту, который был в случае с передачей по значению. Однако, в функции void PrintInfo(int &pipl) мы звёздочку, оператор разыменования, заменили на амперсанд, оператор взятия адреса. Т.е. мы как бы сказали компилятору, что ту переменную, которую нам передали из функции main, нам она целиком ненужна, мы просто сохраним этот адрес. И далее, в теле функции PrintInfo, мы уже можем обращаться по этому адресу, не разыменовывая переменную.

Обратите внимание, что во втором случае я специально изменил название переменной, чтоб более наглядно продемонстрировать, что имя переменной — это штука, которая нужна только Вам, как разработчику, по факту — она ни на что не влияет. Компьютер работает с областью памяти, а не именем переменной. И чуть позже, когда мы дойдём до массивов, это будет продемонстрировано ещё нагляднее на арифметике указателей.

А пока что мы можем это увидеть, посмотрев на область памяти двух переменных вот таким образом:

int GetAge(){
 int age = 19;
 cout << "GetAge():\t" << &age << endl;
 return age;
}

void PrintInfo(int &age){
 cout << "PrintInfo():\t" << &age << endl;
}

int main(){
 setlocale(LC_ALL, "ru_RU.UTF-8");
 int age = GetAge();
 PrintInfo(age);
 cout << "main():\t\t" << &age << endl;
 return 0;
}

В данном примере мы в функции GetAge() создали переменную, вывели её адрес, затем, в функциях PrintInfo() и main() сделали то же самое. Как видим, во всех трёх функциях у нас переменные с одним и тем же именем. Т.е. у нас три переменные с одним и тем же именем, но со сколькими перенными по факту работает программа? Попробуйте сосчитать!

Вывод адреса памяти на экран С++\

Как видим на скриншоте — программа работает с двумя адресами памяти. Это значит, что для программы у нас две переменные. Одна была создана в функции GetAge() и тут же передана в main, а в функции PrintInfo мы работаем с указателем на переменную из функции main.

Когда стоит использовать ссылки и указатели?

Почему я делаю такой акцент на ссылках и указателях? Дело в том, что если вы собираетесь стать хорошим разработчиком, вам просто необходимо чётко знать и понимать, что такое адрес памяти, что такое значение, как передать ссылку, как передать указатель. Это именно те вещи, без которых С++ не был бы тем языком программирования, коим сегодня является.

Теперь вы уже должны чётко усвоить, что когда вы передаёте переменную по значению, как мы делали в прошлом уроке, как делали в начале этого, то происходит копирование этой переменной. Т.е. у нас создаётся ещё одна переменная с таким же типом и с таким же значением. И в случае с цифрой, как у нас сегодня, это не большая проблема, современные компьютеры даже не заметят этого. Но что делать, когда мы имеем дело со здоровенными массивами? Ведь не забываем, что основная среда обитания С++ — это разработка ПО для обработки больших объёмов данных, разработка ПО микроконтроллеров, где борьба идёт за каждый байт, 3D игры и остальные случаи, когда мы не можем разбрасываться ресурсами. Ведь мы же не хотим, играя в игру, ждать, когда класс с нашим оружием скопируется из одной функции в другую, верно? :)

По этому давайте сразу договоримся, что везде, где это возможно, сразу привыкаем передавать по указателю. Это хорошая привычка пригодится вам тогда, когда перейдём от цифр к массивам данных.

А на последок — ещё один пример того, где могу пригодиться указатели. Как бы поступили, если бы вам нужно было получить из функции два или три параметра? К примеру в Js пришлось бы или создавать глобальные переменные, что не хорошо, или добавлять их в массив. Но благо, когда у вас на вооружении есть указатели и ссылки — это совсем не проблема. Давайте попробуем имитировать случай, когда мы передаём в функцию ID пользователя, а она нам возвращает его имя и фамилию.

#include <iostream>
#include <string>

using namespace std;

int GetID(){
 return 19;
}

void GetInfo(int &ID, string &Name, string &Family){
 Name = "Иван";
 Family = "Иванов";
}

void PrintInfo(int &ID, string &Name, string &Family){
 cout << "ID пользователя:"
 << ID << "; имя пользователя: "
 << Name << "; фамилия пользователя: "
 << Family << ";" << endl;
}

int main(){
 setlocale(LC_ALL, "ru_RU.UTF-8");
 int ID = GetID();
 string Name;
 string Family;

 GetInfo(ID, Name, Family);
 PrintInfo(ID, Name, Family);
 return 0;
}

И так, в этом примере сразу мы получаем ID пользователя, затем по ID получаем его имя и фамилию, после чего отправляем в третью функцию, которая уже на основании этих данных выводит красивый текст. Как работает эта программа? Я думаю, если вы внимательно читали эту и предыдущую публикации, вам не составит труда тут разобраться. Если нет — попробуйте перечитать ещё раз.

А более подробно, как всё работает, я расскажу в следующей публикации, за одно, там же мы познакомимся с тем, что такое строки и как работать со строками.

На сегодня у меня всё, хочется пожелать Вам удачи во всех начинаниях, особенно, если эти начинания связаны с таким по настоящему сложным языком программирования, как С++! ;)

 

Публикация относится к тематической подборке: «Уроки C++»

В данной подборке вы по шагам узнаете о увлекательном мире С++ и узнаете, на сколько глубока кроличья нора. Узнаете, для чего используется этот язык программирования, что с его помощью можно сделать и научитесь самостоятельно создавать самые сложные программы...

Понравилась публикация? Поделись ей с друзьями!

Понравился сайт? Подпишьсь на нас в соцсетях!

Мы в TelegramМы ВконтактеМы в ТвиттерМы на фейсбукМы в одноклассниках
Опубликовать
Загрузка рекомендуемых публикаций