Урок №80. указатели
Содержание:
Переменные-ссылки и возвращение ссылки
Последнее обновление: 24.07.2018
Начиная с первых версий языка C# мы могли использовать в методе параметры с ключевым словом ref. В версии C# 7.0 была добавлена возможность возвращать ссылку на объект с помощью ключевого слова ref и определять локальную переменную, которая будет хранить ссылку.
Локальная переменная-ссылка
Для определения локальной переменной-ссылки (ref local) перед ее типом ставится ключевое слово ref:
int x = 5; ref int xRef = ref x;
Здесь переменная xRef указывает не просто на значение переменной x, а на область в памяти, где располагается эта переменная. Для этого перед x также указывается ref.
При этом мы не можем просто определить переменную-ссылку, нам обязательно надо присвоить ей некоторое значение. Так, следующий код вызовет ошибку:
ref int xRef; // ошибка
Получив ссылку, мы можем манипулировать значением по этой ссылке. Например:
static void Main(string[] args) { int x = 5; ref int xRef = ref x; Console.WriteLine(x); // 5 xRef = 125; Console.WriteLine(x); // 125 x = 625; Console.WriteLine(xRef); // 625 Console.Read(); }
Стоит учитывать, возможность переназначить ссылку появилась только с версии 7.3:
int a = 5; int b = 8; ref int pointer = ref a; pointer = 34; pointer = ref b; // до версии 7.3 так делать было нельзя pointer = 6;
Ссылка как результат функции
Для возвращения из функции ссылки в сигнатуре функции перед возвращаемым типом, а также после оператора return
следует указать ключевое слово ref:
static void Main(string[] args) { int[] numbers = { 1, 2, 3, 4, 5, 6, 7 }; ref int numberRef = ref Find(4, numbers); // ищем число 4 в массиве numberRef = 9; // заменяем 4 на 9 Console.WriteLine(numbers); // 9 Console.Read(); } static ref int Find(int number, int[] numbers) { for (int i = 0; i < numbers.Length; i++) { if (numbers == number) { return ref numbers; // возвращаем ссылку на адрес, а не само значение } } throw new IndexOutOfRangeException("число не найдено"); }
В методе ищем число в массиве, но вместо самого значения числа возвращаем ссылку на него в памяти. Для этого в сигнатуре метода
в качестве типа результата функции указывается не просто int, а ref int.
Кроме того, в самом методе после слова return также ставится :
return ref numbers;
Тем самым мы получаем не просто значение, а ссылку на объект в памяти.
В методе Main для определения переменной, которая будет содержать ссылку, используется ключевое слово ref. При вызове метода также указывается слово ref:
ref int numberRef = ref Find(7, numbers);
В итоге переменная numberRef будет содержать ссылку на объект int, и через данную переменную в последствиии мы можем изменить объект по этой ссылке.
Другой пример — возвращение ссылки на максимальное число из двух:
static ref int Max(ref int n1, ref int n2) { if (n1 > n2) return ref n1; else return ref n2; } static void Main(string[] args) { int a = 5; int b = 8; ref int pointer = ref Max(ref a, ref b); pointer = 34; // меняем значением максимального числа Console.WriteLine($"a: {a} b: {b}"); // a: 5 b: 34 Console.ReadKey(); }
Стоит обратить внимание, что параметры метода в этом случае определены с ключевым словом ref. При определении метода, который возвращает ссылку, следует учитывать, что такой метод естественно не может иметь тип void
Кроме того, такой метод не может
возвращать:
При определении метода, который возвращает ссылку, следует учитывать, что такой метод естественно не может иметь тип void. Кроме того, такой метод не может
возвращать:
-
Значение
-
Константу
-
Значение перечисления enum
-
Свойство класса или структуры
-
Поле для чтения (которое имеет модификатор read-only)
НазадВперед
Ответы на вопросы
Вопрос
Мы сначала передали адрес a в функцию AddFive, затем создали указатель int px(но почему именно в аргументе функции?), далее значение по адресу указателя увеличили на 5. Но тут непонятно, разве так будет работать? То есть, нужно сначала адрес присвоить указателю, как Вы показывали ранее в статье. Получится вот так:
void AddFive(int *px)
{
px = &a;
*px = *px+5;
}
……
AddFive(&a);
1 |
voidAddFive(int*px) { px=&a; *px=*px+5; } …… AddFive(&a); |
Ответ
В аргументах функции ничего не создается. В аргументах указываются типы, чтобы компилятор мог проверить их при вызове функции. Адрес указателю px не надо присваивать внутри функции, так как этот адрес уже передан в качестве аргумента при вызове функции. То есть во время работы функции указатель px уже указывает на нужный адрес.
Вопрос
Я проверил код в CodeBlocks. Если мы укажем, например, вот так:
то возникнет ошибка: «px undeclared». Т.е. как видите указатель px не объявлен. А чтобы всё работало мы одновременно, c одной стороны, объявляем указатель в аргументе функции AddFive, а с другой стороны, записываем в указатель адрес a. Поэтому, непонятно почему Вы считаете, что ничего там не создаётся. Ведь память под указатель выделилась, так? И как раз, так как мы создали указатель, пусть и в аргументе функции, программа и работает.
Ответ
На этапе компиляции программы при проверке аргументов функции компилятор ничего не создает, а только проверяет соответствие типов аргументов в описании функции и при ее вызове. В данном примере в описании функции нет аргументов, а при вызове передается адрес — это первая ошибка в данном фрагменте. Вторая ошибка заключается в том, что идет обращение к переменной px, но она не объявлена, поэтому компилятор пишет, что «px undeclared», то есть «переменная px не объявлена».
Когда ошибки будут устранены и программа будет запущена, то в момент вызова AddFive(&a) произойдет следующее:
- Программа считает адрес переменной a и передаст управление функции AddFive.
- Аргументы функции (в данном случае адрес) будут размещены на стеке функции (это временное хранилище данных).
- Во время выполнения функции данные будут взяты со стека и обработаны.
- После выхода из функции стек будет очищен.
То есть в данном примере память для указателя специально не создается, а используется обычный стек для аргументов функции.
Вопрос
При передаче аргументов по значению в функцию передается копия значения переменной х=5, которая хранится в памяти по некоторому адресу, и функция выполняет над ним необходимые операции. Оригинал же значения х по этому адресу остается неизменным. Как бы понятно. Но тогда возникает попутный вопрос – а зачем нужна такая функция, если результат ее выполнения уничтожается при выходе из функции (т.е. при возврате в основную программу) и далее не используется?
Ответ
Результат выполнения функции — это возвращаемое значение.
Например
int AddFive(int x)
{
return x+5;
}
1 |
intAddFive(intx) { returnx+5; } |
Его легко видно, оно идет после слова return.
Любой другой результат работы функции является дополнительным, но главное, что он не очевиден.
То есть при работе с указателями внутри функции нам надо помнить, что эта функция еще что-то меняет в данных.
Когда таких функций две-три — это терпимо. А если более десятка, то уже есть риск ошибиться.
Критический недостаток
Класс Auto_ptr1, приведенный выше, имеет критическую ошибку, которая скрывается за некоторым автоматически генерируемым кодом. Прежде чем продолжить, посмотрите, сможете ли вы определить, что это за ошибка.
Подсказка: Подумайте, какие части класса генерируются автоматически, если вы их не предоставляете самостоятельно.
(Напряжённая музыка)
Хорошо, время истекло.
Мы не будем сейчас вам это говорить, мы сейчас вам это покажем:
#include <iostream>
// Шаблон класса тот же, что и в примере, приведенном выше
template<class T>
class Auto_ptr1
{
T* m_ptr;
public:
Auto_ptr1(T* ptr=nullptr)
:m_ptr(ptr)
{
}
~Auto_ptr1()
{
delete m_ptr;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
};
class Item
{
public:
Item() { std::cout << «Item acquired\n»; }
~Item() { std::cout << «Item destroyed\n»; }
};
int main()
{
Auto_ptr1<Item> item1(new Item);
Auto_ptr1<Item> item2(item1); // в качестве альтернативы вы можете не инициализировать item2 значением item1, а просто выполнить присваивание item2 = item1
return 0;
}
1 |
#include <iostream> // Шаблон класса тот же, что и в примере, приведенном выше template<classT> classAuto_ptr1 { T*m_ptr; public Auto_ptr1(T*ptr=nullptr) m_ptr(ptr) { } ~Auto_ptr1() { delete m_ptr; } T&operator*()const{return*m_ptr;} T*operator->()const{returnm_ptr;} }; classItem { public Item(){std::cout<<«Item acquired\n»;} ~Item(){std::cout<<«Item destroyed\n»;} }; intmain() { Auto_ptr1<Item>item1(newItem); Auto_ptr1<Item>item2(item1);// в качестве альтернативы вы можете не инициализировать item2 значением item1, а просто выполнить присваивание item2 = item1 return; } |
Результат выполнения программы:
Очень вероятно (но не обязательно), что в нашей программе произойдет сбой именно в этот момент. Нашли проблему? Поскольку мы не предоставили конструктор копирования или свой оператор присваивания (перегрузку оператора присваивания), то язык C++ предоставил их самостоятельно. И то, что он предоставил, выполняет . Поэтому, когда мы инициализируем значением , оба объекта класса Auto_ptr1 указывают на один и тот же Item. Когда выходит из области видимости, он удаляет Item, оставляя с висячим указателем. Когда же отправляется на удаление своего (уже удаленного) Item, происходит «Бум!».
Вы получите ту же проблему, используя следующую функцию:
void passByValue(Auto_ptr1<Item> item)
{
}
int main()
{
Auto_ptr1<Item> item1(new Item);
passByValue(item1)
return 0;
}
1 |
voidpassByValue(Auto_ptr1<Item>item) { intmain() { Auto_ptr1<Item>item1(newItem); passByValue(item1) return; } |
В этой программе передается по значению в параметр функции passByValue(), что приведет к дублированию указателя Item. Мы вновь получим «Бум!».
Так быть не должно. Что мы можем сделать?
Мы можем явно определить и удалить конструктор копирования с оператором присваивания, тем самым предотвращая выполнение любого копирования. Это также предотвратит передачу по значению.
Но как нам тогда вернуть Auto_ptr1 из функции обратно в caller?
??? generateItem()
{
Item *item = new Item;
return Auto_ptr1(item);
}
1 |
???generateItem() { Item*item=newItem; returnAuto_ptr1(item); } |
Другой вариант — переопределить конструктор копирования и оператор присваивания для выполнения глубокого копирования. Таким образом, мы, по крайней мере, гарантированно избежим дублирования указателей (которые будут указывать на один и тот же объект). Но глубокое копирование может быть затратной операцией (а также нежелательной или даже невозможной), и мы не хотим делать ненужные копии объектов просто для того, чтобы вернуть Auto_ptr1 из функции. Кроме того, присваивание или инициализация глупого указателя не копирует объект, на который указывает, так почему же мы ожидаем, что умные указатели будут вести себя по-другому?
Что же делать?
В чем польза указателей?
Сейчас вы можете подумать, что указатели являются непрактичными и вообще ненужными. Зачем использовать указатель, если мы можем использовать исходную переменную?
Однако, оказывается, указатели полезны в следующих случаях:
Случай №1: Массивы реализованы с помощью указателей. Указатели могут использоваться для итерации по массиву.
Случай №2: Они являются единственным способом динамического выделения памяти в C++. Это, безусловно, самый распространенный вариант использования указателей.
Случай №3: Они могут использоваться для передачи большого количества данных в функцию без копирования этих данных.
Случай №4: Они могут использоваться для передачи одной функции в качестве параметра другой функции.
Случай №5: Они используются для достижения полиморфизма при работе с наследованием.
Случай №6: Они могут использоваться для представления одной структуры/класса в другой структуре/классе, формируя, таким образом, целые цепочки.
Указатели применяются во многих случаях. Не волнуйтесь, если вы многого не понимаете из вышесказанного. Теперь, когда мы разобрались с указателями на базовом уровне, мы можем начать углубляться в отдельные случаи, в которых они полезны, что мы и сделаем на последующих уроках.
Указатели
Мощным средством С++ является возможность осуществления непосредственного доступа к памяти. Для этой цели предусматривается специальный тип переменных – указатели. Понятие указатель связано с понятиями компьютерной архитектуры – это адрес, адресация, организация внутренней памяти.
Внутренняя память – упорядоченная последовательность байтов или машинных слов. Номер слова в памяти называется адресом.
Прямая адресация – машинное слово, которое содержит непосредственно адрес памяти.
Косвенная адресация – машинное слово, которое содержит адрес другого машинного слова.
Указатель – переменная, содержимым которой является адрес другой переменной. Указатели используют чаще всего для манипуляций с динамически размещенными объектами.
Другой подход к указателям.
Переменная – это объект, имеющий тип, имя и значение.Имя переменной – это адрес того участка памяти, который выделен для неё.
Значение переменной – это содержимое этого участка памяти. Для работы с адресами переменных используются указатели.
Указатель – это переменная, значением которой служит адрес участка памяти, выделенной для объекта конкретного типа.
Основная операция для указателя – косвенное обращение к переменной (*). Переменная, адрес которой содержится в указателе называется указуемой переменной. Последовательность действий с указателем включает три шага.
Первый шаг — определение указуемых переменных и переменной указателя.
Синтаксис указателя: тип_указателя *имя указателя
Пример: int a, x; // обычные переменные (указуемые)
int *pа; // переменная – указатель на переменную а
Указатели могут ссылаться не на любые переменные, а только на переменные заданного типа. В данном примере только на переменную целого типа.
Второй шаг — связывание указателя с указуемой переменной (инициализация указателя).. Значением указателя является адрес другой переменной, т.е указатель должен быть настроен на переменную, на которую он будет ссылаться.
pа=&a; // указатель содержит адрес переменной а
Объединив два шага можно записать int *pа=&a;
Третий шаг — выполнение операций с указателями.
В любом выражении косвенное обращение по указателю интерпретируется кака переход от него к указуемой переменной с выполнением над ней всех далее перечисленных в выражении операций.
*pа = 100; эта запись эквивалентна записи а=100
х = х + *pа эта запись эквивалентна записи х = х + а
(*pа)++ эта запись эквивалентна записи а++
Адресная арифметика указателя
Указатель даёт «степень свободы» любому алгоритму обработки данных. Если некоторый фрагмент программы получает данные непосредственно в переменной, то он может обрабатывать её и только её.
Если же данные программа получает через указатель, то обработка данных (указуемых переменных) может производиться в любой области памяти компьютера (или программы). Любой указатель в С++ потенциально ссылается на неограниченную в обе стороны область памяти (массив), заполненную переменными указуемого типа с индексацией элементов массива относительно текущего положения указателя.
Особенности:
- любой указатель потенциально ссылается на неограниченную в обе стороны область памяти, заполненную переменными указуемого типа;
- переменные в области нумеруются от текущей указуемой переменной, которая получает относительный индекс нуль. Переменные в направлении возрастания адресов памяти нумеруются положительными значениями 1, 2, 3, и т.д., а в направлении убывания – отрицательными.
p+i переместить указатель на i переменную после указуемой.
p-i переместить указатель на i переменную перед указуемой.
p++ переместить указатель на следующую переменную.
p— переместить указатель на предыдущую переменную.
В операциях адресной арифметики транслятором автоматически учитывается размер указуемых переменных, т.е. +i понимается не как смещение на i байтов, слов и прочее, а как смещение на i указуемых переменных. При объявлении указателей необходимо указывать тип объекта, чтобы компилятору было известно, сколько байтов добавлять к адресу при увеличении указателя на единицу.
Достоинство указателя – если программа получает данные через указатель, то обработка этих данных производится в любой свободной части памяти.
Применение ссылок
Помимо удобной замены указателям, ещё одним полезным применением ссылок являются списки параметров функции, при помощи которых они могут передавать параметры, используемые для вывода без явного взятия адреса вызывающим. Например:
void square(int x, int& result) { result = x * x; }
Тогда следующий вызов поместит 9 в y:
square(3, y);
Тем не менее, следующий вызов приведёт к ошибке компиляции, так как только ссылочные параметры с квалификатором могут быть связаны с правосторонними значениями, например, с числовой константой:
square(3, 6);
Возврат ссылки из функции означает, что вызов данной функции есть левостороннее значение. Появляется возможность присваивания вызову функции:
int& preinc(int& x) { ++x; return x; } preinc(y) = 5; // то же, что и ++y, y = 5
Во многих реализациях обычные механизмы передачи параметров часто подразумевают весьма затратную операцию копирования больших параметров. Ссылки, помеченные , полезны в качестве способа передачи больших объектов между функциями без накладных расходов:
void f_slow(BigObject x) { /* ... */ } void f_fast(const BigObject& x) { /* ... */ } BigObject y; f_slow(y); // медленно, копирует y в параметр x f_fast(y); // быстро, даёт прямой доступ к y (только для чтения) _
Если действительно требует собственной копии x для модификации, то она должна создать копию явным образом. Хотя того же эффекта можно достичь с использованием указателей, это потребует модификации каждого вызова функции, добавив к аргументу неуклюжий оператор разыменования (), причём отменить изменения будет не менее сложно, если в дальнейшем объект станет маленьким.
Причиной введения ссылок в язык С++ в основном являлась необходимость перегрузки операторов, применяемых к объектам пользовательских типов (классов). Как упоминалось выше, передача по значению громоздких объектов в качестве операндов вызывала бы лишние затраты на их копирование. С другой стороны, передача операндов по адресу с использованием указателей приводит к необходимости взятия адресов операндов в выражениях. Например:
class BigClass { //... friend BigClass operator-(BigClass* left, BigClass* right); //... }; BigClass x, y, z; //... x = &y - &z;
Однако, выражение уже имело определённый смысл в языке С.
При перегрузке оператора присваивания разумно будет возвращать именно ссылку на объект. Это увеличивает производительность, особенно в случаях использования нескольких операторов присваивания подряд (например, A=B=C), так как не происходит вызов конструктора копирования.
Константные указатели
Мы также можем сделать указатель константным. Константный указатель — это указатель, значение которого не может быть изменено после инициализации. Для объявления константного указателя используется ключевое слово const между звёздочкой и именем указателя:
int value = 7;
int *const ptr = &value;
1 |
intvalue=7; int*constptr=&value; |
Подобно обычным константным переменным, константный указатель должен быть инициализирован значением при объявлении. Это означает, что он всегда будет указывать на один и тот же адрес. В вышеприведенном примере всегда будет указывать на адрес (до тех пор, пока указатель не выйдет из и не уничтожится):
int value1 = 7;
int value2 = 8;
int * const ptr = &value1; // ок: константный указатель инициализирован адресом value1
ptr = &value2; // не ок: после инициализации константный указатель не может быть изменен
1 |
intvalue1=7; intvalue2=8; int*constptr=&value1;// ок: константный указатель инициализирован адресом value1 ptr=&value2;// не ок: после инициализации константный указатель не может быть изменен |
Однако, поскольку переменная , на которую указывает указатель, не является константой, то её значение можно изменить путем разыменования константного указателя:
int value = 7;
int *const ptr = &value; // ptr всегда будет указывать на value
*ptr = 8; // ок, так как ptr указывает на тип данных (неконстантный int)
1 |
intvalue=7; int*constptr=&value;// ptr всегда будет указывать на value *ptr=8;// ок, так как ptr указывает на тип данных (неконстантный int) |
Связь с указателями
C++ ссылки отличаются от указателей несколькими особенностями:
Невозможно ссылаться напрямую на объект ссылочного типа после его определения; каждое упоминание его имени напрямую представляет объект, на который он ссылается. Это происходит потому, что ссылочные типы являются «не совсем полноценными» в системе типов C++ — не существует значений ссылочных типов, причем ни лево-, ни правосторонних значений. Не-существование левосторонних значений ссылочных типов Страуструп и Эллис формулировали как «ссылка не есть объект».
В качестве результата первого указания не могут быть выполнены никакие арифметические вычисления, приведения типов, взятия адреса и т.п. Дело в том, что результаты всех этих операций есть правосторонние значения (в случае классовых типов — временные объекты), а не-константная ссылка не может быть инициализирована правосторонним значением (константная — может, при необходимости создается невидимая переменная, значение укладывается туда, ссылка указывает на невидимую переменную).
После создания ссылки её нельзя перевести на другой объект; в таких случаях говорят, что ссылка не может быть переопределена. Это часто делают с указателями.
Ссылки не могут быть null (т.е. указывать в никуда), тогда как указатели — могут; каждая ссылка ссылается на некий объект, вне зависимости от его корректности.
Ссылки не могут быть неинициализированными. Так как невозможно переинициализировать ссылку, она должна быть инициализирована сразу после создания. В частности, локальные и глобальные переменные должны быть проинициализированы там же, где они определены, а ссылки, которые являются данными-членами сущностей класса, должны быть инициализированы в списке инициализатора конструктора класса
Важное замечание: класс, в котором есть поля-ссылки (а также поля const), не может иметь operator=(), и некоторые компиляторы (Microsoft) дают об этом предупреждение.
Пример:
int& k; // компилятор выдаст сообщение: ошибка: 'k' declared as reference but not initialized // ('k' объявлена как ссылка, но не инициализирована)
Кроме того, из-за ограничения операций над ссылками, они намного легче в понимании, чем указатели, а также более защищены от ошибок. Тогда как указатели могут стать некорректными благодаря множеству причин, начиная с указания на null-значения и выходов за границы и до использования недопустимых приведений типов, ссылка может стать некорректной лишь в двух случаях:
- Если она ссылается на объект с автоматическим размещением в памяти, с завершившимся временем жизни.
- Если она ссылается на объект, находящийся в блоке динамической памяти, который был освобождён.
Первый вариант легко обнаруживается автоматически, если ссылка имеет статическое размещение, но возникают проблемы, если ссылка — член динамически размещённого объекта; от второго защищаться сложнее. Это единственный недостаток ссылок, который может быть нивелирован при разумной политике выделения памяти (далеко не единственный. Например, даже в случае out параметра функции зачастую лучше иметь T*, а не T&, ибо это приводит к необходимости иметь & во всех вызовах функции, который подсказывает человеку, читающему код, что данный параметр есть out. Вообще, ссылки не есть замена указателям).
Тест
Задание №1
Какие значения мы получим в результате выполнения следующей программы (предположим, что это 32-битное устройство, и тип short занимает 2 байта):
short value = 7; // &value = 0012FF60
short otherValue = 3; // &otherValue = 0012FF54
short *ptr = &value;
std::cout << &value << ‘\n’;
std::cout << value << ‘\n’;
std::cout << ptr << ‘\n’;
std::cout << *ptr << ‘\n’;
std::cout << ‘\n’;
*ptr = 9;
std::cout << &value << ‘\n’;
std::cout << value << ‘\n’;
std::cout << ptr << ‘\n’;
std::cout << *ptr << ‘\n’;
std::cout << ‘\n’;
ptr = &otherValue;
std::cout << &otherValue << ‘\n’;
std::cout << otherValue << ‘\n’;
std::cout << ptr << ‘\n’;
std::cout << *ptr << ‘\n’;
std::cout << ‘\n’;
std::cout << sizeof(ptr) << ‘\n’;
std::cout << sizeof(*ptr) << ‘\n’;
1 |
shortvalue=7;// &value = 0012FF60 shortotherValue=3;// &otherValue = 0012FF54 short*ptr=&value; std::cout<<&value<<‘\n’; std::cout<<value<<‘\n’; std::cout<<ptr<<‘\n’; std::cout<<*ptr<<‘\n’; std::cout<<‘\n’; *ptr=9; std::cout<<&value<<‘\n’; std::cout<<value<<‘\n’; std::cout<<ptr<<‘\n’; std::cout<<*ptr<<‘\n’; std::cout<<‘\n’; ptr=&otherValue; std::cout<<&otherValue<<‘\n’; std::cout<<otherValue<<‘\n’; std::cout<<ptr<<‘\n’; std::cout<<*ptr<<‘\n’; std::cout<<‘\n’; std::cout<<sizeof(ptr)<<‘\n’; std::cout<<sizeof(*ptr)<<‘\n’; |
Ответ №1
Значения:
Краткое объяснение по поводу последней пары: и . 32-битное устройство означает, что размер указателя составляет 32 бита, но всегда выводит размер в байтах: 32 бита = 4 байта. Таким образом, равен . Поскольку является указателем на значение типа short, то является типа short. Размер short в этом примере составляет 2 байта. Таким образом, равен .
Задание №2
Что не так со следующим фрагментом кода:
int value = 45;
int *ptr = &value; // объявляем указатель и инициализируем его адресом переменной value
*ptr = &value; // присваиваем адрес value для ptr
1 |
intvalue=45; int*ptr=&value;// объявляем указатель и инициализируем его адресом переменной value *ptr=&value;// присваиваем адрес value для ptr |
Ответ №2
Последняя строка не скомпилируется. Рассмотрим эту программу детально.
В первой строке находится стандартное определение переменной вместе с инициализируемым значением. Здесь ничего особенного.
Во второй строке мы определяем новый указатель с именем и присваиваем ему адрес переменной . Помним, что в этом контексте звёздочка является частью синтаксиса объявления указателя, а не оператором разыменования. Так что и в этой строке всё нормально.
В третьей строке звёздочка уже является оператором разыменования, и используется для вытаскивания значения, на которое указывает указатель. Таким образом, эта строка говорит: «Вытаскиваем значение, на которое указывает (целочисленное значение), и переписываем его на адрес этого же значения». А это уже какая-то чепуха — вы не можете присвоить адрес целочисленному значению!
Третья строка должна быть:
ptr = &value;
1 | ptr=&value; |
В вышеприведенной строке мы корректно присваиваем указателю адрес значения переменной.
Оператор разыменования *
Оператор разыменования позволяет получить значение по указанному адресу:
#include <iostream>
int main()
{
int a = 7;
std::cout << a << ‘\n’; // выводим значение переменной a
std::cout << &a << ‘\n’; // выводим адрес переменной a
std::cout << *&a << ‘\n’; /// выводим значение ячейки памяти переменной a
return 0;
}
1 |
#include <iostream> intmain() { inta=7; std::cout<<a<<‘\n’;// выводим значение переменной a std::cout<<&a<<‘\n’;// выводим адрес переменной a std::cout<<*&a<<‘\n’;/// выводим значение ячейки памяти переменной a return; } |
Результат на моем компьютере:
Примечание: Хотя оператор разыменования выглядит так же, как и оператор умножения, отличить их можно по тому, что оператор разыменования — унарный, а оператор умножения — бинарный.
Возврат сразу нескольких значений
Иногда нам может понадобиться, чтобы функция возвращала сразу несколько значений. Однако оператор return позволяет функции иметь только одно возвращаемое значение. Одним из способов возврата сразу нескольких значений является использование ссылок в качестве параметров:
#include <iostream>
#include <math.h> // для sin() и cos()
void getSinCos(double degrees, double &sinOut, double &cosOut)
{
// sin() и cos() принимают радианы, а не градусы, поэтому необходима конвертация
const double pi = 3.14159265358979323846; // значение Пи
double radians = degrees * pi / 180.0;
sinOut = sin(radians);
cosOut = cos(radians);
}
int main()
{
double sin(0.0);
double cos(0.0);
// функция getSinCos() возвратит sin и cos в переменные sin и cos
getSinCos(30.0, sin, cos);
std::cout << «The sin is » << sin << ‘\n’;
std::cout << «The cos is » << cos << ‘\n’;
return 0;
}
1 |
#include <iostream> voidgetSinCos(doubledegrees,double&sinOut,double&cosOut) { // sin() и cos() принимают радианы, а не градусы, поэтому необходима конвертация constdoublepi=3.14159265358979323846;// значение Пи doubleradians=degrees *pi180.0; sinOut=sin(radians); cosOut=cos(radians); } intmain() { doublesin(0.0); doublecos(0.0); // функция getSinCos() возвратит sin и cos в переменные sin и cos getSinCos(30.0,sin,cos); std::cout<<«The sin is «<<sin<<‘\n’; std::cout<<«The cos is «<<cos<<‘\n’; return; } |
Лично я не рекомендую смешивать параметры ввода и вывода именно по этой причине, но если вы это делаете, то обязательно добавляйте комментарии к коду, описывая, что вы делаете и как это делаете.
Неконстантные ссылки могут ссылаться только на неконстантные l-values (например, на неконстантные переменные), поэтому параметр-ссылка не может принять аргумент, который является константным l-value или r-value (например, литералом или результатом выражения).