В чём польза указателей?
Сейчас вы можете подумать, что указатели являются непрактичными и вообще ненужными. Зачем использовать указатель, если мы можем использовать исходную переменную?
Однако, оказывается, указатели полезны в следующих случаях:
Случай №1: Массивы реализованы с помощью указателей. Указатели могут использоваться для итерации по массиву.
Случай №2: Они являются единственным способом динамического выделения памяти в C++. Это, безусловно, самый распространенный вариант использования указателей.
Случай №3: Они могут использоваться для передачи большого количества данных в функцию без копирования этих данных.
Случай №4: Они могут использоваться для передачи одной функции в качестве параметра другой функции.
Случай №5: Они используются для достижения полиморфизма при работе с наследованием.
Случай №6: Они могут использоваться для представления одной структуры/класса в другой структуре/классе, формируя, таким образом, целые цепочки.
Указатели применяются во многих случаях. Не волнуйтесь, если вы многого не понимаете из вышесказанного. Теперь, когда мы разобрались с указателями на базовом уровне, мы можем начать углубляться в отдельные случаи, в которых они полезны, что мы и сделаем на последующих уроках.
Что такое указатели
Последнее обновление: 22.09.2017
Указатели представляют собой объекты, значением которых служат адреса других объектов (переменных, констант, указателей) или функций. Как и ссылки, указатели применяются для косвенного доступа к объекту. Однако в отличие от ссылок указатели
обладают большими возможностями.
Для определения указателя надо указать тип объекта, на который указывает указатель, и символ звездочки *. Например, определим указатель на
объект типа int:
int *p;
Пока указатель не ссылается ни на какой объект. При этом в отличие от ссылки указатель необязательно инициализировать каким-либо значением.
Теперь присвоим указателю адрес переменной:
int x = 10; // определяем переменную int *p; // определяем указатель p = &x; // указатель получает адрес переменной
Для получения адреса переменной применяется операция &
Что важно, переменная x имеет тип int, и указатель,
который указывает на ее адрес, тоже имеет тип int. То есть должно быть соответствие по типу
Если мы попробуем вывести адрес переменной на консоль, то увидим, что он представляет шестнадцатиричное значение:
#include <iostream> int main() { int x = 10; // определяем переменную int *p; // определяем указатель p = &x; // указатель получает адрес переменной std::cout << "p = " << p << std::endl; return 0; }
Консольный вывод программы:
p = 0x60fe98
В каждом отдельном случае адрес может отличаться, но к примеру, в моем случае машинный адрес переменной x — 0x60fe98. То есть в памяти
компьютера есть адрес 0x60fe98, по которому располагается переменная x. Так как переменная x представляет тип int,
то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом,
переменная типа int последовательно займет ячейки памяти с адресами 0x60FE98, 0x60FE99, 0x60FE9A, 0x60FE9B.
И указатель p будет ссылаться на адрес, по которому располагается переменная x, то есть на адрес 0x60FE98.
Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной x. Для этого применяется
операция * или операция разыменования, то есть та операция, которая применяется при определении указателя. Результатом этой
операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной x:
#include <iostream> int main() { int x = 10; int *p; p = &x; std::cout << "Address = " << p << std::endl; std::cout << "Value = " << *p << std::endl; return 0; }
Консольный вывод:
Address = 0x60fe98 Value = 10
Значение, которое получено в результате операции разыменования, можно присвоить другой переменной:
int x = 10; int *p = &x; int y = *p; std::cout << "Value = " << y << std::endl; // 10
И также используя указатель, мы можем менять значение по адресу, который хранится в указателе:
int x = 10; int *p = &x; *p = 45; std::cout << "x = " << x << std::endl; // 45
Так как по адресу, на который указывает указатель, располагается переменная x, то соответственно ее значение изменится.
Создадим еще несколько указателей:
#include <iostream> int main() { short c = 12; int d = 10; short s = 2; short *pc = &c; // получаем адрес переменной с типа short int *pd = &d; // получаем адрес переменной d типа int short *ps = &s; // получаем адрес переменной s типа short std::cout << "Variable c: address=" << pc << "\t value=" << *pc << std::endl; std::cout << "Variable d: address=" << pd << "\t value=" << *pd << std::endl; std::cout << "Variable s: address=" << ps << "\t value=" << *ps << std::endl; return 0; }
В моем случае я получу следующий консольный вывод:
Variable c: address=0x60fe92 value=12 Variable d: address=0x60fe8c value=10 Variable s: address=0x60fe8a value=2
По адресам можно увидеть, что переменные часто расположены в памяти рядом, но не обязательно в том порядке, в котором они определены в коде программы:
НазадВперед
То есть перевод будет абсолютно идентичен оригиналу?
Оформление переведенного документа максимально приближено к оригинальному, но не всегда повторяет его на 100%.
Не стоит забывать, что все языки разные и количество знаков в языке перевода обычно отличается от их числа в языке оригинала. Скорее всего, вы не определите размер отступов или полей на глаз, но измерив их, наверняка заметите разницу. Пожалуйста, учтите это, договариваясь о печати с типографией. То же касается шрифтов: не во всех языках можно найти аналог того или иного шрифта. В таких случаях наши специалисты подбирают максимально похожие варианты.
Вообще пожелания по оформлению перевода могут быть настолько разными, что лучше просто посмотреть на примеры, которые мы привели в конце статьи.
Двумерные динамически выделенные массивы
Другим распространенным применением указателей на указатели является динамическое выделение многомерных массивов. В отличие от двумерного фиксированного массива, который можно легко объявить следующим образом:
int array;
1 | intarray157; |
Динамическое выделение двумерного массива немного отличается. У вас может возникнуть соблазн написать что-то вроде следующего:
int **array = new int; // не будет работать!
1 | int**array=newint157;// не будет работать! |
Здесь вы получите ошибку. Есть два возможных решения. Если правый индекс является константой типа compile-time, то вы можете сделать следующее:
int (*array) = new int;
1 | int(*array)7=newint157; |
Скобки здесь потребуются для соблюдения приоритета. В C++11 хорошей идеей будет использовать ключевое слово auto для автоматического определения типа данных:
auto array = new int; // намного проще!
1 | auto array=newint157;// намного проще! |
К сожалению, это относительно простое решение не работает, если правый индекс не является константой типа compile-time. В таком случае всё немного усложняется. Сначала мы выделяем массив указателей (как в примере, приведенном выше), а затем перебираем каждый элемент массива указателей и выделяем динамический массив для каждого элемента этого массива. Итого, наш динамический двумерный массив — это динамический одномерный массив динамических одномерных массивов!
int **array = new int*; // выделяем массив из 15 указателей типа int — это наши строки
for (int count = 0; count < 15; ++count)
array = new int; // а это наши столбцы
1 |
int**array=newint*15;// выделяем массив из 15 указателей типа int — это наши строки for(intcount=;count<15;++count) arraycount=newint7;// а это наши столбцы |
Доступ к элементам массива выполняется как обычно:
array = 4; // это то же самое, что и (array) = 4;
1 | array83=4;// это то же самое, что и (array) = 4; |
Этим методом, поскольку каждый столбец массива динамически выделяется независимо, можно сделать динамически выделенные двумерные массивы, которые не являются прямоугольными. Например, мы можем создать массив треугольной формы:
int **array = new int*; // выделяем массив из 15 указателей типа int — это наши строки
for (int count = 0; count < 15; ++count)
array = new int; // а это наши столбцы
1 |
int**array=newint*15;// выделяем массив из 15 указателей типа int — это наши строки for(intcount=;count<15;++count) arraycount=newintcount+1;// а это наши столбцы |
В примере, приведенном выше, — это массив длиной 1, а — массив длиной 2 и т.д.
Для освобождения памяти динамически выделенного двумерного массива (который создавался с помощью этого способа) также потребуется цикл:
for (int count = 0; count < 15; ++count)
delete[] array;
delete[] array; // это следует выполнять в конце
1 |
for(intcount=;count<15;++count) deletearraycount; deletearray;// это следует выполнять в конце |
Обратите внимание, мы удаляем массив в порядке, противоположном его созданию. Если мы удалим массив перед удалением элементов массива, то нам придется получать доступ к освобожденной памяти для удаления элементов массива
А это, в свою очередь, приведет к неожиданным результатам.
Поскольку процесс выделения и освобождения двумерных массивов является несколько запутанным (можно легко наделать ошибок), то часто проще «сплющить» двумерный массив в одномерный массив:
// Вместо следующего:
int **array = new int*; // выделяем массив из 15 указателей типа int — это наши строки
for (int count = 0; count < 15; ++count)
array = new int; // а это наши столбцы
// Делаем следующее:
int *array = new int; // двумерный массив 15×7 «сплющенный» в одномерный массив
1 |
// Вместо следующего: int**array=newint*15;// выделяем массив из 15 указателей типа int — это наши строки for(intcount=;count<15;++count) arraycount=newint7;// а это наши столбцы // Делаем следующее: int*array=newint105;// двумерный массив 15×7 «сплющенный» в одномерный массив |
Простая математика используется для конвертации индексов строки и столбца прямоугольного двумерного массива в один индекс одномерного массива:
int getSingleIndex(int row, int col, int numberOfColumnsInArray)
{
return (row * numberOfColumnsInArray) + col;
}
// Присваиваем array значение 3, используя наш «сплющенный» массив
array = 3;
1 |
intgetSingleIndex(introw,intcol,intnumberOfColumnsInArray) { return(row *numberOfColumnsInArray)+col; } // Присваиваем array значение 3, используя наш «сплющенный» массив arraygetSingleIndex(9,4,5)=3; |
Что делает указатель?
Он указывает. Более конкретно, он указывает на данные другой переменной или на данные, которые хранятся в памяти, но не связаны с переменной.
Рисунок 1 – На что указывает указатель
Обычно мы думаем о переменной как о чем-то, что хранит данные, а под «данным» мы подразумеваем информацию, которая будет использоваться в вычислениях, или отправляться на другое устройство, или загружаться в регистр конфигурации, или использоваться для управления пикселями LCD дисплея. Указатель – это переменная, но она не используется для хранения такого типа данных. Вернее указатель хранит адрес памяти.
Рисунок 2 – Данные о температуре хранятся в переменной, расположенной по адресу 0x01, а синяя переменная является указателем, который содержит адрес, по которому хранятся данные о температуре
Возможно, именно в этот момент некоторые люди начинают немного путаться, и я думаю, что это происходит потому, что легко упустить из виду физическую реальность памяти процессора. Блок памяти представляет собой набор цифровых ячеек памяти, которые организованы в группы. В случае 8-разрядного процессора каждая группа ячеек памяти соответствует одному байту. Единственный способ отличить одну группу от другой – это адрес, а этот адрес – просто число. Указатель – это переменная, в которой хранится число, но это число интерпретируется как адрес, т.е. как значение, указывающее точное местоположение в памяти.
Подкрепим эту концепцию краткой аналогией. Представьте, что я стою в библиотеке, и кто-то подходит ко мне и говорит: «Кто такой Ричард Львиное Сердце?». Если я отвечаю, говоря: «Король Англии с 1189 по 1199 года», я похож на обычную переменную. Я предоставляю информацию, данные, которые хочет получить человек. И напротив, если я отвечаю, указывая на книгу под названием «Монархи средневековой Англии», я действую как указатель. Вместо того чтобы предоставлять нужные данные, я указываю, где именно эти данные можно найти. Я до сих пор храню полезную информацию, но эта информация – это не сам факт, а место, где человек может получить доступ к этому факту.
Для чего нужен указатель в Си
Функции в Си принимают аргументы, передавая или копируя значения в стек функции. Такой метод иногда называется передачей по значению. Поскольку функции в Си и переменные, переданные им, в действительности не связываются, любые внесённые изменения в эти переменные не будут сохраняться за пределами действия функции. Это может вызвать сложности, потому что в некоторых функциях необходимо изменять текущие переменные. Здесь-то нам и пригодится указатель. С его помощью можно получить доступ к памяти, находящейся за пределами стекового кадра
Однако важно отметить, что с помощью указателя можно получить доступ лишь к переменным, расположенным ниже текущего кадра
// gcc -o pointer pointer.c && ./pointer #include <stdio.h> #include <stdlib.h>// Эта функция не будет работать, так как в Си функция передаётся по значению.// Внесённые изменения не действительны за пределами функции.void increment(int i) { i = i + 1;}// Передайте указатель на i, а не на само значение, тогда всё заработает.void incrementWorks ( int* i) { *i = *i + 1;}int main() { int i = 765;printf("Original value: %d\n", i); increment(i); printf("After the increment function is called: %d\n", i);printf("Original value: %d\n", i); incrementWorks (&i); printf("After the increment function is called %d\n", i);return (EXIT_SUCCESS);}
В примере выше простая функция с задачей — увеличить на единицу число, проходящее через параметры. Функция написана следующим образом:
// Эта функция не будет работать, так как в языке Си функция передаётся по значению.// Внесённые изменения не действительны за пределами функции.void increment(int i) { i = i + 1;}
Однако при запуске кода никаких изменений с переменной не происходит. Связанно это с тем, что в функцию (увеличения) копируется только значение переменной, и остальная часть программы не видит изменений, внесённых в эту переменную. Другими словами, переменная находится внутри функции , и несмотря на одинаковые названия, это не одна и та же переменная, что , расположенная за пределами этой функции.
Для того, чтобы решить эту проблему, нужно вместо самой переменной передать в функцию указатель этой переменной. Таким образом мы предоставим текущей функции доступ к переменной , которая не попадает в область действия при выполнении этой функции. Выглядит она вот так:
// Передайте указатель на i, а не на само значение, тогда всё заработаетvoid increment(int *i) { *i = *i + 1;}
- Использование методов расширения в C# для элегантного и плавного кода
- Игра на C# меньше 8 Кб
- 4 golang-сниппета, которые вводят в заблуждение разработчиков C#!
Читайте нас в Telegram, VK и
Указатель на указатель
Так как указатель — обычная переменная, возможен указатель на указатель. И указатель на указатель на указатель. И указатель (на указатель) n раз для натурального n. Максимальный уровень вложенности задаётся компилятором, но на практике уровни больше 2 практически не используются.
Процитируем Three Star Programmer:
“Система ранжирования C-программистов.
Чем выше уровень косвенности ваших указателей (т. е. чем больше “*” перед вашими переменными), тем выше ваша репутация. Беззвёздочных C-программистов практически не бывает, так как практически все нетривиальные программы требуют использования указателей. Большинство являются однозвёздочными программистами. В старые времена (ну хорошо, я молод, поэтому это старые времена на мой взгляд) тот, кто случайно сталкивался с кодом, созданный трёхзвёздочным программистом, приходил в благоговейный трепет.
Некоторые даже утверждали, что видели трёхзвёздочный код, в котором указатели на функции применялись более чем на одном уровне косвенности. Как по мне, так эти рассказы столь же правдивы, сколь рассказы об НЛО.
Просто чтобы было ясно: если вас назвали Трёхзвёздочным Программистом, то обычно это не комплимент.»
Условия для проверки себя на “трёхзвёздность” перечислены на другой странице того же сайта.
В случае C указатели на указатели (уровень косвенности 2) используются довольно часто, например, для возвращения указателя из функции, которая возвращает ещё что-то, или для организации двумерных массивов. Пример такой функции из Windows API:
Функция принимает имя файла как указатель на си-строку lpFileName, а также размер буфера nBufferLength в символах и адрес буфера lpBuffer, куда записывается в виде си-строки полное имя файла. Функция возвращает длину строки, записанной в буфер, или 0, если произошла ошибка. Кроме того, последний параметр функции — указатель на указатель на си-строку lpFilePart, который используется, чтобы вернуть из функции указатель на последнюю часть имени файла, записанного в буфер.
В случае C++ с помощью ссылок и Стандартной библиотеки можно вообще избежать использования “классических” указателей. Так что “беззвёздочный” C++-программист возможен.
См. также в .
Бестиповый указатель
Вместо типа данных при объявлении указателя можно поставить ключевое слово . Данное ключевое слово означает, что мы описываем указатель “на что угодно”, т. е. просто адрес в памяти. Любой указатель автоматически приводится к типу — бестиповому указателю typeless pointer. Прочие указатели, соответственно, называются типизированными или типизованными typed. Приведение от к типизованному указателю возможно с помощью оператора явного приведения типа.
В C бестиповые указатели широко применяются или реализации обобщённых функций, которые могут работать со значениями разных типов. В последнем случае конкретный тип маскируется с помощью void (“пустышка”). При использовании таких функций обычно приходится где-то явно приводить тип указателей. C++ позволяет отказаться от подобной практики благодаря поддержке полиморфизма и обобщённого программирования (материал 2-го семестра).
О цикле см. .
Оператор разыменования *
Оператор разыменования позволяет получить значение по указанному адресу:
#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; } |
Результат на моем компьютере:
Примечание: Хотя оператор разыменования выглядит так же, как и оператор умножения, отличить их можно по тому, что оператор разыменования — унарный, а оператор умножения — бинарный.
Понятие типов данных указателей
Как вы могли заметить в приведенных выше примерах, указатели объявляются с типом данных. Возможно, это усугубляет сложность понимания, что такое указатель. Если указатель – это просто число, соответствующее адресу ячейки памяти, то как могут использоваться разные типы данных? Например, если ваш микроконтроллер имеет 4 КБ RAM, как вы можете иметь указатель с типом данных ? Максимальное значение составляет 255; что произойдет, если этот указатель должен указывать на переменную, расположенную по адресу памяти 3000?
Ключ к пониманию этой проблемы заключается в следующем: тип данных указателя не указывает, сколько байтов используется для хранения его значения. Скорее, число байтов, используемых для хранения значения указателя, соответствует количеству адресов памяти, к которым необходимо получать доступ, независимо от типа данных указателя. Кроме того, размер указателя определяется компилятором и непосредственно не виден программисту.
Рассмотрим следующую диаграмму:
Рисунок 3 – Представления в памяти переменной и указателя на эту переменную
Допустим, мы используем жалкий микроконтроллер с оперативной памятью всего 11 байтов. Диапазон значений, предлагаемых 8-разрядным числом, составляет от 0 до 255, поэтому один байт памяти более чем достаточен для представления всех возможных областей памяти в этом устройстве.
Диаграмма подчеркивает тот факт, что даже переменная, объявленная как , может быть доступна через однобайтовый указатель. Синяя переменная – это указатель, который содержит адрес 32-битной переменной . Эта переменная использует четыре байта памяти, но адрес переменной (который в этом примере соответствует младшему значащему байту) всегда будет числом, равным или меньшим . Указатель должен быть объявлен с типом данных , потому что он используется вместе с переменными , но сам указатель потребляет один байт памяти, а не четыре.
Общие сведения
Что такое указатель pointer уже рассказывалось во .
В C и C++ указатель определяется с помощью символа после типа данных, на которые этот указатель будет указывать.
Указатель — старший родственник ссылки. Указатели активно использовались ещё в машинных языках и оттуда были перенесены в C. Ссылки же доступны только в C++.
Указатели — простые переменные. Указатели не “делают вид”, что они — те значения в памяти, к которым они привязаны. Чтобы получить указатель на переменную, нужно явно взять её адрес с помощью оператора . Чтобы обратиться к переменной, на которую указывает указатель, требуется явно разыменовать его с помощью оператора .
Так же, как и в случае ссылок, можно использовать ключевое слово , чтобы создать указатель на константу.
Указатели можно сравнивать друг с другом. Указатели равны, если указывают на один и тот же объект, и не равны в противном случае.
Указатели можно передавать в функции и возвращать из функций как и любые “элементарные” значения. Ещё пример с указателями:
Для обращения к полю структуры по указателю на объект структуры предусмотрен специальный оператор (“стрелка”).
В отличие от ссылок, указатели не обязательно инициализировать. Указатели можно инициализировать специальным значением нулевой указатель nullptr, которое сигнализирует об отсутствии привязки указателя к чему-либо. Присваивание указателю другого адреса меняет его привязку. Это позволяет использовать указатели там, где семантика ссылок слишком сильно ограничивает наши возможности.
Соответственно, ограничения, накладываемые на ссылки по сравнению с указателями, позволяют, с одной стороны, защитить программиста от ряда ошибок, и, с другой стороны, открывают ряд возможностей оптимизации кода для компилятора. Ссылки используются там, где нет нужды в “полноценных” указателях или есть желание не перегружать код взятиями адреса и разыменованиями.
Наличие нулевого указателя позволяет, например, возвращать указатель на искомый объект и в том случае, когда ничего не было найдено. Просто в этой ситуации возвращаем нулевой указатель, а принимающая сторона должна быть готова к такому развитию событий. Указатель автоматически преобразуется к булевскому значению: нулевой указатель даёт , прочие указатели дают , поэтому, если — указатель, то
есть то же самое, что
И напротив,
есть то же самое, что
Например, поиск самого левого нуля в массиве чисел с плавающей точкой может быть записан так:
Данный пример использует арифметику указателей и массивы. Данная тема освещена в разделе .
Заключение
В этой статье я познакомил вас с ключевым словом , рассказал о том, что означают данные, которые выводятся в результате его работы и как вы можете их использовать для оптимизации запросов. Использовать в реальном приложении это будет более полезно, чем на демонстрационной базе данных из этой статьи. Почти всегда вы будете объединять несколько таблиц вместе, и использовать
Простое добавление индексов нескольким полям обычно не приносит выгоды, поэтому в процессе составления запросов особое внимание надо уделять самим запросам
Перевод – Земсков Матвей
Оригинал статьи: http://phpmaster.com/using-explain-to-write-better-mysql-queries/