Зачем нужны указатели в c
Перейти к содержимому

Зачем нужны указатели в c

  • автор:

Указатели (C++)

Указатель — это переменная, в которой хранится адрес памяти объекта. Указатели широко используются как в C, так и в C++ для трех основных целей:

  • для выделения новых объектов в куче,
  • передача функций другим функциям
  • для итерации элементов в массивах или других структурах данных.

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

В этом разделе

  • Необработанные указатели
  • Константные и переменные указатели
  • новые и удаленные операторы
  • Интеллектуальные указатели
  • Практическое руководство. Создание и использование экземпляров unique_ptr
  • Практическое руководство. Создание и использование экземпляров shared_ptr
  • Практическое руководство. Создание и использование экземпляров weak_ptr
  • Практическое руководство. Создание и использование экземпляров CComPtr и CComQIPtr

Что такое указатели в программировании

В программировании есть понятие указателей — особенно часто о них можно услышать в языках вроде C. Указатели считаются сложной темой, и про тех, кто ими пользуются, ходят легенды. Но на самом деле в указателях нет ничего сложного. Сейчас разберём.

Что такое указатель

Когда мы заводим новую переменную, компьютер выделяет для неё место в оперативной памяти:

Что такое указатели в программировании

Количество этих ячеек зависит от типа данных, который хранится в этой переменной: обычно для целого числа выделяют 2 или 4 байта, для дробного — 8 байт, для строки — столько же, сколько и символов и ещё 1 служебный байт и так далее. Но сколько бы байтов ни выделил компьютер для хранения, он выделяет эти байты подряд, друг за другом, и запоминает два момента:

  1. Сколько байтов занимает переменная.
  2. По какому адресу в памяти находится первый байт этой переменной.

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

Что такое указатели в программировании

Зачем это нужно

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

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

Почему указателями редко пользуются

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

Дело в том, что компьютеру всё равно, что лежит по адресу, написанному в указателе — данные, переменная или что-то чужое. Всё, что он делает — это идёт по этому адресу и стирает либо записывает туда что-то своё, не задавая лишних вопросов. Это мощная штука, но чтобы ей пользоваться, нужно чётко знать внутреннюю архитектуру памяти и работы компилятора.

Что такое указатели в программировании

В каких языках есть указатели

Самые популярные языки с поддержкой указателей — это всё семейство Си-языков:

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

Также полноценные указатели есть в некоторых современных языках высокого уровня: Java, Pascal и Go.

Получите ИТ-профессию

В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.

Для чего же нужны указатели?

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

  • Вопрос задан более трёх лет назад
  • 9199 просмотров

2 комментария

Оценить 2 комментария

Vitaly @vt4a2h Куратор тега C++

Напишите сначала, чтовы сделали для того, чтобы разобраться с этим вопросом и что конкретно не поняли. Например, читали ли вы книги по C++, статьи, пробовали ли писать программы? Если нет, то почитатйте. Просто так объяснять не имеет смысла, вы поймёте не больше, чем сейчас.

AtomKrieg

«Используют ли он указатели? Это хороший признак. Многие программисты на Си просто не знают, как заставить работать указатели. Я, как правило, не отказываюсь от кандидата из-за отсутствия у него какого-то навыка. Однако я обнаружил, что понимание указателей в Си — это не навык, а способность. В начале первого курса на факультете кибернетики набирается человек 200 вундеркиндов, писавших в четырехлетнем возрасте игрушки для Atari 800 на BASIC’е. Они хорошо проводят время, изучая Паскаль, но в один прекрасный день профессор заводит речь об указателях, и они внезапно перестают понимать. То есть абсолютно. 90% потока переходит на отделение политологии, объясняя это тем, что на кибернетике мало симпатичных студенток. На самом же деле, по неизвестной причине часть человечества просто рождается без того отдела мозга, который понимает указатели. Указатели — это не навык, а способность, требующая особого мышления, и некоторые люди им просто не обладают.»
Джоэль Спольски предлагает вам изучать политологию вместо программирования.

Решения вопроса 1

Nipheris

Станислав Макаров @Nipheris Куратор тега C++

Ну и я тогда тоже попробую.

Посмотрим на вопрос с другой стороны.
Переменная — это контейнер для значения. Что нам вообще нужно для работы с переменной? Что нам нужно, чтобы считать/записать значение из/в нее? Нам нужны тип данных и адрес в памяти.
Тип данных — отдельная история, оставим пока его в стороне. Скажем только, что он определяет допустимые операции со значением и количество памяти, необходимое для хранения значения.
Поговорим об адресе. В языках, где доступна прямая работа с памятью, любая переменная имеет свой адрес в памяти. Также, в чуть более узком смысле, переменная находится в такой области памяти, в которую разрешена запись. Не имеет особого значения, какой структурой данных управляется эта память — стеком или кучей — важно, чтобы на момент использования эта память была доступна.
Корректный адрес в памяти — это и уникальный «ключ» переменной, ее отличительная черта. Работая с переменными, программист на низкоуровневом языке неизбежно работает с адресами.
Другой вопрос — это способ работы с адресом переменной. Когда вы создаете обычную локальную переменную, в работе с ней принимает участие компилятор. Когда вы пишете int a, компилятор (если не вдаваться в детали) размещает у себя в таблице идентификаторов пару: (имя_переменной, адрес_в_памяти). Обычная локальная переменная «а» (еще ее называют «автоматическая переменная») — способ создания переменной средствами компилятора. Компилятор освободит память, занимаемую этой переменной, когда она уйдет из области видимости. Однако, пока вам точно известно, что эта переменная «живет», вы можете совершенно спокойно получить ее адрес с помощью операции & — компилятор отдаст вам его из своей таблицы идентификаторов.

Но «переменные» в широком смысле можно создавать не только средствами компилятора, но и вручную с помощью malloc (Си) или new (С++). Эти динамические переменные живут столько, сколько вам нужно — вы их создаете, вам их и уничтожать. Об этих переменных компилятор ничего не знает, т.к. вы создаете их динамически во время выполнения программы. Для доступа к этим переменным вам также нужен адрес, но у компилятора его не попросишь: поэтому необходимо самому сохранять те адреса, что вернули вам функция malloc или оператор new. Этот адрес вы можете сохранить в ДРУГОЙ переменной, и такая переменная, хранящая адреса — и есть указатель (кроме того, если указатель не бестиповый (void*) то его тип (float*) еще и подсказывает нам тип переменной, на которую он указывает (float)).

Очень важно, что в указатель можно сохранить адрес ЛЮБОЙ переменной — как автоматической, которую вам создал компилятор, так и «ручной» — которую создали ВЫ с помощью malloc/new. И передать, например, этот адрес в функцию. Фактически, в языке Си указатели это и есть способ передачи САМОЙ ПЕРЕМЕННОЙ в функцию, а не ее ЗНАЧЕНИЯ на момент вызова. В C++ есть еще ссылки, но это отдельная история (ссылки — это указатель, «обернутый» в обычный идентификатор), по ним задайте отдельный вопрос.

если можно напрямую данной переменной присвоить новое значение.

Как раз таки «напрямую» вы не присвоите, т.к. у вас не может быть в одной области видимости ВСЕХ переменных, имеющихся в программе. Хотя бы потому, что у вас могут быть динамические переменные, и единственный способ работы с ними — работать через указатель.

Если вы никогда не работали с динамической памятью, вы можете спросить, какой смысл в «ручных» переменных, если для хранения их адреса нужна парная переменная-указатель? А вся фишка в том, что адрес динамически созданной переменной также можно хранить в динамически созданном указателе. Тут-то и открывается вся бесконечная свобода построения динамических структур данных. Если вы не слышали про связный список, то самое время почитать хорошую книгу с примерами. В одном ответе этого не расскажешь, это основы программирования.

Указатели в C++: зачем нужны, когда использовать и чем отличаются от обращения к объекту напрямую

В какой ситуации нужно использовать указатели, а в какой сами объекты? Ниже мы постарались ответить на этот вопрос.

Обложка поста Указатели в C++: зачем нужны, когда использовать и чем отличаются от обращения к объекту напрямую

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

Вопрос

Я заметил, что нередко программисты, чей код я видел, используют указатели на объекты чаще, чем сами эти объекты, т.е., например, используют следующую конструкцию:

Object *myObject = new Object; 
Object myObject; 

Аналогично с методами. Почему вместо этого:

myObject.testFunc(); 

мы должны писать вот это:

myObject->testFunc(); 

Я так понимаю, что это дает выигрыш в скорости, т.к. мы обращаемся напрямую к памяти. Верно? P.S. Я перешел с Java.

Ответ

Заметим, кстати, что в Java указатели не используются в явном виде, т.е. программист не может в коде обратиться к объекту через указатель на него. Однако на деле в Java все типы, кроме базовых, являются ссылочными: обращение к ним происходит по ссылке, хотя явно передать параметр по ссылке нельзя. И еще, на заметку, new в C++ и в Java или C# — абсолютно разные вещи.

Очередной фреймворк на PHP — конкурс пет-проектов

Для того, чтобы дать небольшое представление, что же такое указатели в C++, приведем два аналогичных фрагмента кода:

Object object1 = new Object(); // Новый объект Object object2 = new Object(); // Еще один новый объект object1 = object2;// Обе переменные ссылаются на объект, на который раньше ссылалась object2 // При изменении объекта, на который ссылается object1, изменится и // object2, потому что это один и тот же объект 

Ближайший эквивалент на C++:

Object * object1 = new Object(); // Память выделена под новый объект // На эту память ссылается object1 Object * object2 = new Object(); // Аналогично со вторым объектом delete object1; // В C++ нет системы сборки мусора, поэтому если этого не cделать, // к этой памяти программа уже не сможет получить доступ, // как минимум, до перезапуска программы // Это называется утечкой памяти object1 = object2; // Как и в Java, object1 указывает туда же, куда и object2 

Однако вот это – совершенно другая вещь (C++):

Object object1; // Новый объект Object object2; // Еще один object1 = object2;// Полное копирование объекта object2 в object1, // а не переопределение указателя – очень дорогая операция 

Но получим ли мы выигрыш в скорости, обращаясь напрямую к памяти?

На самом деле, совсем нет. Работа с указателями оформлена в виде кучи, в то время как работа с объектами – это стек, более простая и быстрая структура. Если вы новичок, то у нас для вас есть материал, в котором мы подробно рассказываем, что такое стек и куча.

Строго говоря, этот вопрос объединяет в себе два различных вопроса. Первый: когда стоит использовать динамическое распределение памяти? Второй: когда стоит использовать указатели? Естественно, здесь мы не обойдемся без общих слов о том, что всегда необходимо выбирать наиболее подходящий инструмент для работы. Почти всегда существует реализация лучше, чем с использованием ручного динамического распределения (dynamic allocation) и / или сырых указателей.

Динамическое распределение

В формулировке вопроса представлены два способа создания объекта. И основное различие заключается в сроке их жизни (storage duration) в памяти программы. Используя Object myObject; , вы полагаетесь на автоматическое определение срока жизни, и объект будет уничтожен сразу после выхода из его области видимости. А вот Object *myObject = new Object; сохраняет жизнь объекту до того момента, пока вы вручную не удалите его из памяти командой delete . Используйте последний вариант только тогда, когда это действительно необходимо. А потому всегда делайте выбор в пользу автоматического определения срока хранения объекта, если это возможно.

Как работает функция print в Python

Обычно принудительное установления срока жизни применяется в следующих ситуациях:

  • Вам необходимо, чтобы объект существовал и после выхода из области его видимости — именно этот объект, именно в этой области памяти, а не его копия. Если для вас это не принципиально (в большинстве случаев это так), положитесь на автоматическое определение срока жизни. Однако вот пример ситуации, когда вам может понадобиться обратить к объекту вне его области видимости, однако это можно сделать, не сохраняя его в явном виде: записав объект в вектор, вы можете “разорвать связь” с самим объектом — на самом деле он (а не его копия) будет доступен при вызове из вектора.
  • Вам необходимо использовать много памяти, которая может переполнить стек. Здорово, если с такой проблемой не приходится сталкиваться (а с ней сталкиваются очень редко), потому что это “вне компетенции” C++, но к сожалению, иногда приходится решать и эту задачу.
  • Вы, например, точно не знаете размер массива, который придется использовать. Как известно, в C++ массивы при определении имеют фиксированный размер. Это может вызвать проблемы, например, при считывании пользовательского ввода. Указатель же определяет только тот участок в памяти, куда будет записано начало массива, грубо говоря, не ограничивая его размер.

Если использование динамического распределения необходимо, то вам стоит инкапсулировать его с помощью умного указателя (что такое умный указатель можете прочитать в нашей статье) или другого типа, поддерживающего идиому “Получение ресурса есть инициализация” (стандартные контейнеры ее поддерживают — это идиома, в соответствии с которой ресурс: блок памяти, файл, сетевое соединение и т.п. — при получении инициализируется в конструкторе, а затем аккуратно уничтожается деструктором). Умными являются, например, указатели std::unique_ptr и std::shared_ptr .

Указатели

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

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

  • Ссылочная семантика. Иногда может быть необходимо обратиться к объекту (вне зависимости от того, как под него распределена память), поскольку вы хотите обратиться в функции именно в этому объекту, а не его копии — т.е. когда вам требуется реализовать передачу по ссылке. Однако в большинстве случаев, здесь достаточно использовать именно ссылку, а не указатель, потому что именно для этого ссылки и созданы. Заметьте, что это несколько разные вещи с тем, что описано в пункте 1 выше. Но если вы можете обратиться к копии объекта, то и ссылку использовать нет необходимости (но заметьте, копирование объекта — дорогая операция).
  • Полиморфизм. Вызов функций в рамках полиморфизма (динамический класс объекта) возможен с помощью ссылки или указателя. И снова, использование ссылок более предпочтительно.
  • Необязательный объект. В этом случае можно использовать nullptr , чтобы указать, что объект опущен. Если это аргумент функции, то лучше сделайте реализацию с аргументами по умолчанию или перегрузкой. С другой стороны, можно использовать тип, который инкапсулирует такое поведение, например, boost::optional (измененный в C++14 std::optional ).
  • Повышение скорости компиляции. Вам может быть необходимо разделить единицы компиляции (compilation units). Одним из эффективных применений указателей является предварительная декларация (т.к. для использования объекта вам необходимо предварительно его определить). Это позволит вам разнести единицы компиляции, что может положительно сказаться на ускорении времени компиляции, внушительно уменьшив время, затрачиваемое на этот процесс.
  • Взаимодействие с библиотекой C или C-подобной. Здесь вам придется использовать сырые указатели, освобождение памяти из-под которых вы производите в самый последний момент. Получить сырой указатель можно из умного указателя, например, операцией get . Если библиотека использует память, которая впоследствии должна быть освобождена вручную, вы можете оформить деструктор в умном указателе.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *