Begin c что это
Перейти к содержимому

Begin c что это

  • автор:

Begin c что это

С — это язык программирования, созданный в 70-х годах XX века для разработки системы UNIX и программного обеспечения для нее. В 80-х годах XX века на основе языка C был создан язык C++, являющийся объектно-ориентированным расширением языка C++. В настоящее время языки C и C++ являются наиболее распространенными языками для профессиональной разработки программного обеспечения для всех операционных систем. Синтаксис языка C и C++ не зависит от используемой системы и компилятора, однако набор доступных библиотек (например, для разработки графических приложений) является системно-зависимым и не стандартизирован.

В данных листках речь будет идти о языке C++. Многое из того, о чем пойдет ниже речь, верно и для языка C, но мы на этом останавливаться не будем. Мы будем использовать компилятор gcc для системы Linux, аналогичный ему компилятор MinGW для системы Windows и программу Dev-C++, называемую средой разработки, облегчающую процесс программирования. При этом все рассматриваемые примеры должны правильно компилироваться любым компилятором, соответствующим стандарту языка C++. Например, таким компилятором является MS Visual C++ последних версий (в то время, как широко распространенный в образовательных учреждениях компилятор Borland C++ 3.1 не соответствует стандарту и для него рассматриваемые программы работать не будут).

1.2 Hello, world

Язык C++ является компилируемым языком. Для того, чтобы написать программу, вам необходимо в любом текстовом редакторе набрать, например, следующий текст и сохранить его в файле, например, hello.cpp .

#include using namespace std; int main()

Будьте внимательны: язык C++ является чувствительным к регистру букв, то есть заменить main на Main или MAIN нельзя. Весь текст (за исключением текстовой строки «Hello, world!» ) нужно набирать в нижнем регистре.

После этого вам нужно откомпилировать этот файл (создать из этого файла исполняемый машинный код) при помощи следующей команды (в системе Linux):

$ c++ hello.cpp

В среде разработки (вроде Dev-C++) для компиляции программы существует пункт меню, вызывающий компилятор. Если ваша программа написана правильно, то компилятор не выдаст никаких сообщений об ошибках и создаст исполняемый файл ( a.out в системе Linux или exe -файл в системе Windows). Этот файл содержит исполняемый двоичный машинный код.

Рассмотрим подробней текст этой программы. В первой строчке мы подключаем к нашей программе файл с именем iostream , в котором содержится описание стандартной библиотеки ввода-вывода языка C++. В этом файле находится, в частности, определение объектов cout и endl , который мы будем использовать позднее. Вторая строка указывает компилятору на то, что мы будем использовать все функции, входящие в пространство имен std , то есть все функции, относящиеся к стандартной библиотеке C++. Третья строка содержит объявление функции main , не принимающей никаких аргументов и возвращающей значение int . Эта функция должна быть в каждой программе, именно эта функция получает управление при запуске программы. Четвертая строка содержит открывающуюся фигурную скобку, что означает начало функции main . В пятой строке мы при помощи оператора

Для начала можно считать, что все строки, кроме пятой, являются некоторым набором “заклинаний”, без которых программа не будет работать и которые обязательно нужно указать, а вот пятую строку можно заменить на другие строки с различными инструкциями. —>

begin Function

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

Синтаксис

template ::Platform::Collections::VectorIterator begin(IVector^ v); template ::Platform::Collections::VectorViewIterator begin(IVectorView^ v); template ::Platform::Collections::InputIterator begin(IIterable^ i); 
Параметры

T
Параметр типа шаблона.

v
Коллекция Vector или VectorView объекты, к которым IVector обращается интерфейс или IVectorView доступ.

i
Коллекция произвольных среда выполнения Windows объектов, к которым обращается IIterable интерфейс.

Возвращаемое значение

Итератор, который указывает на начало коллекции.

Замечания

Первые два шаблона функций возвращают итераторы, а третий шаблон функции возвращает входной итератор.

Объект VectorIterator , возвращаемый begin прокси-итератором, который хранит элементы типа VectorProxy . Однако объект прокси-сервера практически никогда не отображается в пользовательском коде. Дополнительные сведения см. в разделе Collections (C++/CX).

Требования

Заголовок: collection.h

Пространство имен Windows::Foundation::Collections :

Основы C++: Указатели и Итераторы

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

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

Сегодня мы с вами обсудим указатели и итераторы.

Указатели

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

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

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

Я говорю здесь «должен», потому что если указатель правильно инициализирован, то в нем хранится либо nullptr , либо адрес другой переменной (он даже может хранить адрес другого указателя), но если он не был инициализирован должным образом, то в нем будут содержаться произвольные данные, что довольно опасно, так как это может привести к неопределенному поведению.

Как можно инициализировать указатель?

В нашем распоряжении есть сразу три способа!

  • Взять адрес другой переменной:
#include int main()
  • Указать на переменную в куче
#include int main()< int* p = new int ; >
  • Или просто взять значение другого указателя
#include int main()< int* p = new int ; int* p2 = p; >
Значения указателей и значения, на которые они указывают

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

#include int main()< int* p = new int ; int* p2 = p; std::cout /* 0x215dc20 42 0x215dc20 42 0x7fff77592cb0 0x7fff77592cb8 */

В данном примере мы видим, что p и p2 хранят один и тот же адрес в памяти, а это значит, что они указывают на одно и то же значение. Но если мы воспользуемся оператором & , то мы увидим, что адреса у этих указателей разные.

Деаллокация памяти

Если выделение памяти (аллокация) происходит с помощью оператора new, другими словами, если вы выделяете память в куче, то кто-то должен впоследствии высвободить (деаллоцировать) выделенную память. Это можно сделать с помощью оператора delete . Если забыть это сделать, то, когда указатель выйдет за пределы области видимости, произойдет утечка памяти (memory leak).

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

#include int main() < int* p = new int ; std::cout

Если вы попытаетесь получить доступ к указателю уже после удаления или же попытаетесь удалить его во второй раз, то это вызовет неопределенное поведение и, скорее всего, вы наткнетесь на core dump.

Подобные ошибки часто возникают в легаси-коде, например, в таких сценариях:

#include int main()< int* p = new int ; std::cout // . delete p; >

Очевидно, что на практике значение error определяется в результате куда более сложного вычисления, и обычно эти два оператора delete добавляются в код не одновременно (и скорее всего разными программистами).

Простейший способ перестраховаться от такой ситуации — сразу после удаления присвоить p nullptr . Если попытаться удалить указатель еще раз, то это не даст никакого эффекта, так как удаление nullptr является no-op.

#include int main()< int* p = new int ; std::cout // . delete p; p = nullptr; >

Еще один важный момент — всегда проверяйте пригодность (валидность) указателя перед обращением к нему. Даже если не брать в расчет мороку с потокобезопасностью, мы не можем расслабляться. Что если указатель уже был удален и не установлен в nullptr ? Неопределенное поведение, потенциальные краши. Или еще хуже.

#include int main()< int* p = new int ; if (p != nullptr) < std::cout delete p; // we forget to set it to nullptr if (p != nullptr) < // we pass the condition std::cout > /* 0x22f3c20 42 0x22f3c20 0 */

А что может пойти не так, если сделать копию указателя? Давайте представим, что мы удаляем один указатель и устанавливаем его в nullptr . Его скопированный брат не будет знать, что он был удален:

#include int main()< int* p = new int ; int* p2 = p; if (p != nullptr) < std::cout delete p; // we forget to set it to nullptr p = nullptr; if (p != nullptr) < // p is nullptr, we skip this block std::cout if (p2 != nullptr) < // we pass the condition and anything can happen std::cout > /* 0x1133c20 42 0x1133c20 0 */

Такая ситуация легко может возникнуть, если в вашем распоряжении есть классы, управляющие ресурсами через голые указатели, и операции копирования/перемещения в них реализованы некорректно.

Итерация по массивам

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

#include int main()< int numbers[5] = ; int* p = numbers; for(size_t i=0; i < 5; ++i) < std::cout for(size_t i=0; i < 5; ++i) < std::cout std::cout /* 1 2 3 4 5 5 4 3 2 1 4 */

Это конечно хорошо, но стоит ли использовать указатели для итерации по массивам в 2023 году?

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

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

Не используйте голые указатели!

На самом деле, на сегодняшний день уже вообще нет особого смысла использовать голые/сырые указатели (raw pointers). Особенно это касается указателей, которые выделяются с помощью new, и указателей, владеющих ресурсами. Передача ресурсов через голый указатель — это еще куда ни шло, но владение этими ресурсами, использование указателей в качестве итераторов или для выражение того, что значение может быть, а может и не быть — это то, чего не следует допускать в коде.

В нашем распоряжении есть средства более подходящие для этих задач.

Прежде всего, для замены владеющих ресурсом голых указателей мы можем использовать умные указатели (smart pointers).

Мы можем использовать невладеющие (non-owning) указатели, мы можем использовать ссылки, если что-то не может быть nullptr , или если мы хотим выразить, что что-то может присутствовать или не присутствовать, мы можем попробовать std::optional . Но я расскажу об этом подробнее в другой раз.

Сегодня мы сосредоточимся на итерации по массиву и рассмотрим другие возможности, которые мы могли бы использовать.

Что такое итератор?

Итераторы являются неотъемлемой частью стандартной библиотеки шаблонов. В STL можно выделить четыре основные группы элементов:

  • алгоритмы ( std::rotate , std::find_if и т.д.)
  • контейнеры ( std::vector , std::list и т.д.)
  • функциональные объекты ( std::greater , std::logical_and и т.д.)
  • итераторы ( std::iterator , std::back_inserter и т.д.)

Итераторы являются результатом обобщения концепции указателя. Они могут использоваться для перебора элементов контейнера STL и предоставления доступа к отдельным элементам.

Упоминание контейнеров STL также не спроста — итераторы нельзя использовать с C-массивами. И это нормально, в 2023 году мы вообще уже не должны использовать массивы в стиле C.

Пять категорий итераторов

По сути, итераторы можно разделить на пять категорий:

  • итераторы ввода (input iterators)
  • итераторы вывода (output iterators)
  • однонаправленные итераторы (forward iterators)
  • двунаправленные итераторы (bidirectional iterators)
  • итераторы произвольного доступа (random access iterators)

Итераторы ввода являются простейшей формой итераторов. Они поддерживают операции чтения и могут двигаться только вперед. Итераторы ввода можно использовать для сравнения на (не)равенство, и они могут быть инкрементированы. Хорошим примером может послужить итератор std::list .

Итераторы вывода также являются однонаправленными итераторами, но они используются для присвоения значений в контейнере, это итераторы исключительно для записи. Их нельзя использовать для чтения значений. В качестве примера такого итератора можно привести std::back_inserter .

Однонаправленные итераторы представляют собой комбинацию итераторов ввода и вывода. Они позволяют нам как получать доступ к значениям, так и изменять их. Например, такие итераторы используются в std::replace . Однонаправленные итераторы являются DefaultConstructible и могут обращаться/разыменовывать одни и те же позиции несколько раз.

Двунаправленные итераторы подобны однонаправленным итераторам, но они могут быть еще и декрементированы, поэтому могут двигаться как вперед, так и назад. std::reverse_copy использует такие итераторы, поскольку ему нужно и обращать значения контейнера (декрементировать), и помещать результаты в новый контейнер один за другим (инкрементировать).

Итераторы произвольного доступа могут делать все то же, что и двунаправленные итераторы. Кроме того, они могут не только инкрементироваться или декрементироваться, но и изменять свою позицию на любое значение. Другими словами, они поддерживают операторы + и - . Различные итераторы произвольного доступа можно также сравнивать с помощью различных операторов сравнения (а не только с помощью равенства/неравенства). Произвольный доступ означает, что к контейнерам, принимающим такие итераторы, можно просто обращаться с помощью оператора сдвига. Алгоритм, которому нужны итераторы с произвольным доступом, — это std::random_shuffle() .

Использование итераторов

Итераторы могут быть получены из контейнеров двумя различными способами:

  • через функции-члены, такие как std::vector::begin() или std::vector::end()
  • или через свободные функции, такие как std::begin() или std::end()

Существуют различные вариации итераторов, с практической точки зрения они могут быть как const , так и реверсивно-направленными.

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

#include #include int main() < std::vectorv ; for (std::vector::const_iterator it=v.begin(); it != v.end(); ++it) < std::cout >

С появлением языка C++11 и ключевого слова auto использование итераторов значительно упростилось:

#include #include int main() < std::vectorv ; for (auto it=v.begin(); it != v.end(); ++it) < std::cout >

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

Чем итератор отличается от указателя

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

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

Учитывая, что указатель всегда хранит адрес памяти, его всегда можно преобразовать в целое число (которое и является адресом). Большинство итераторов не могут быть преобразованы в целые числа.

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

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

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

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

Если требуется выполнить итерацию по стандартному контейнеру, используйте итератор. Так как он был разработан именно для этого, он безопаснее и именно его вы и получите, если вызовете begin() или end() на контейнере. Более того, алгоритмы STL принимают на вход именно итераторы, а не указатели, и именно их они зачастую и возвращают.

Но есть две ситуации, где вам не нужно использовать итераторы:

  • использование цикла for с диапазоном, который действительно предпочтительнее, но под капотом, в большинстве случаев, он все равно использует итераторы
  • использование массив в стиле C. Но в 2023 году вообще нет смысла использовать C-массив, ведь можно использовать std::array или другой STL-контейнер.

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

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

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

Заключение

Мне бы очень хотелось иметь наиболее полное понимание основ языка C++ в начале своей карьеры разработчика я.

Мне бы очень хотелось иметь его и сегодня.

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

Ссылки

  • Apache C++ Standard Library User’s Guide: Varieties of Iterators
  • University of Helsinki: STL Iterators
  • GeeksForGeeks: Difference between Iterators and Pointers in C/C++ with Examples
  • Microsoft: Raw pointers (C++)
  • Stackoverflow: Why should I use a pointer rather than the object itself?

Материал подготовлен в преддверии старта специализации "C++ Developer".

Десять возможностей C++11, которые должен использовать каждый C++ разработчик

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

  • auto
  • nullptr
  • range-based циклы
  • override и final
  • строго-типизированный enum
  • интеллектуальные указатели
  • лямбды
  • non-member begin() и end()
  • static_assert и классы свойств
  • семантика перемещения
#1 — auto

До С++11, ключевое слово auto использовалось как спецификатор хранения переменной (как, например, register, static, extern ). В С++11 auto позволяет не указывать тип переменной явно, говоря компилятору, чтобы он сам определил фактический тип переменной, на основе типа инициализируемого значения. Это может использоваться при объявлении переменных в различных областях видимости, как, например, пространство имен, блоки, инициализация в цикле и т.п.

auto i = 42; // i - int auto l = 42LL; // l - long long auto p = new foo(); // p - foo* 

Использование auto позволяет сократить код (если, конечно, тип не int , который на одну букву меньше). Подумайте об итераторах STL, которые вы должны были всегда писать для прохода контейнеров. Таким образом, это делает устаревшим определение typedef только ради простоты.

std::map> map; for(auto it = begin(map); it != end(map); ++it) < // do smth >// Или, сравним С++03 и С++11 // C++03 for (std::vector>::const_iterator it = container.begin(); it != container.end(); ++it) < // do smth >// C++11 for (auto it = container.begin(); it != container.end(); ++it) < // do smth >

Стоить отметить, что возвращаемое значение не может быть auto . Однако, вы можете использовать auto вместо типа возвращаемого значения функции. В таком случае, auto не говорит компилятору, что он должен определить тип, он только дает ему команду искать возвращаемый тип в конце функции. В примере ниже, возвращаемый тип функции compose — это возвращаемый тип оператора +, который суммирует значения типа T и E .

template auto compose(T a, E b) -> decltype(a+b) // decltype - позволяет определить тип на основе входного параметра < return a+b; >auto c = compose(2, 3.14); // c - double 
#2 — nullptr

Раньше, для обнуления указателей использовался макрос NULL, являющийся нулем — целым типом, что, естественно, вызывало проблемы (например, при перегрузке функций). Ключевое слово nullptr имеет свой собственный тип std::nullptr_t , что избавляет нас от бывших проблем. Существуют неявные преобразования nullptr к нулевому указателю любого типа и к bool (как false ), но преобразования к целочисленных типам нет.

void foo(int* p) <> void bar(std::shared_ptr p) <> int* p1 = NULL; int* p2 = nullptr; if(p1 == p2) <> foo(nullptr); bar(nullptr); bool f = nullptr; int i = nullptr; // ошибка: для преобразования в int надо использовать reinterpret_cast 
#3 — range-based циклы

В С++11 была добавлена поддержка парадигмы foreach для итерации по набору. В новой форме возможно выполнять итерации в случае, если для объекта итерации перегружены методы begin() и end() .

Это полезно, когда вы просто хотите получить элементы массива/контейнера или сделать с ними что-то, не заботясь об индексах, итераторах или кол-ве элементов.

std::map> map; std::vector v; v.push_back(1); v.push_back(2); v.push_back(3); map["one"] = v; for(const auto &kvp: map) < std::cout int arr[] = ; for(int &e: arr) e *= e; 
#4 — override и final

Мне всегда не нравились виртуальные функции в С++. Ключевое слово virtual опционально и поэтому немного затрудняло чтение кода, заставляя вечно возвращаться в вершину иерархии наследования, чтобы посмотреть объявлен ли виртуальным тот или иной метод. Я всегда использовал этой ключевое слово так же и в производных классах (и поощрял людей, кто так делал), чтобы код был понятнее. Тем не менее, есть ошибки, которые могут все таки возникнуть. Возьмем следующий пример:

class B < public: virtual void f(short) >; class D : public B < public: virtual void f(int) >; 

D::f переопределяет B::f . Однако они имеют разную сигнатуру, один метод принимает short , другой — int , поэтому B::f — это просто другой метод с тем же именем, перегруженный, а не переопределенный. Таким образом, работая через указатель на базовый класс, Вы можете вызвать f() и ожидать вывода «переопределенного» вами метода: «D::f», однако вывод будет «B::f».

Вот другая возможная ошибка: параметры одни и те же, но в базовом классе метод константный, а в производном — нет.

class B < public: virtual void f(int) const >; class D : public B < public: virtual void f(int) >; 

И снова это две перегруженные, а не переопределенные функции.
К счастью, теперь есть способ избавиться от этих ошибок. Были добавлены два новых идентификатора (не ключевые слова): override , для указания того, что метод является переопределением виртуального метода в базовом классе и final , указывающий что производный класс не должен переопределять виртуальный метод. Первый пример теперь выглядит так:

class B < public: virtual void f(short) >; class D : public B < public: virtual void f(int) override >; 

Теперь это вызовет ошибку при компиляции (точно так же, если бы вы использовали override во втором примере):

'D::f': method with override specifier 'override' did not override any base class methods 

С другой стороны, если вы хотите сделать метод, не предназначенный для переопределения (ниже в иерархии), его следует отметить как final . В производном классе можно использовать сразу оба идентификатора.

class B < public: virtual void f(int) >; class D : public B < public: virtual void f(int) override final >; class F : public D < public: virtual void f(int) override >; 

Функция, объявленная как final , не может быть переопределена функцией F::f() — в этом случае, она переопределяет метод базового класса ( В ) для класса D .

#5 — строго-типизированный enum

У «традиционных» перечислений в С++ есть некоторые недостатки: они экспортируют свои значения в окружающую область видимости (что может привести к конфликту имен), они неявно преобразовываются в целый тип и не могут иметь определенный пользователем тип.

Эти проблемы устранены в С++11 с введением новой категории перечислений, названных strongly-typed enums. Они определяются ключевым словом enum class . Они больше не экспортируют свои перечисляемые значения в окружающую область видимости, больше не преобразуются неявно в целый тип и могут иметь определенный пользователем тип (эта опция так же добавлена и для «традиционных» перечислений").

enum class Options ; Options o = Options::All; 
#6 — интеллектуальные указатели
  1. unique_ptr: должен использоваться, когда ресурс памяти не должен был разделяемым (у него нет конструктора копирования), но он может быть передан другому unique_ptr
  2. shared_ptr: должен использоваться, когда ресурс памяти должен быть разделяемым
  3. weak_ptr: содержит ссылку на объект, которым управляет shared_ptr , но не осуществляет подсчет ссылок; позволяет избавиться от циклической зависимости
void foo(int* p) < std::cout std::unique_ptr p1(new int(42)); std::unique_ptr p2 = std::move(p1); // transfer ownership if(p1) foo(p1.get()); (*p2)++; if(p2) foo(p2.get()); 

Второй пример демонстрирует shared_ptr . Использование похоже, хотя семантика отличается, поскольку теперь владение совместно используемое.

void foo(int* p) < >void bar(std::shared_ptr p) < ++(*p); >std::shared_ptr p1(new int(42)); std::shared_ptr p2 = p1; bar(p1); foo(p2.get()); 

Первое объявление эквивалентно следующему:

auto p3 = std::make_shared(42); 

make_shared — это функция, имеющая преимущество при выделении памяти для совместно используемого объекта и интеллектуального указателя с единственным выделением, в отличие от явного получения shared_ptr через конструктор, где требуется, по крайней мере, два выделения. Из-за этого может произойти утечка памяти. В следующем примере как раз это демонстрируется, утечка может произойти в случае, если seed() бросит исключение.

void foo(std::shared_ptr p, int init) < *p = init; >foo(std::shared_ptr(new int(42)), seed()); 

Эта проблема решается использованием make_shared .
И, наконец, пример с weak_ptr . Заметьте, что вы должны получить shared_ptr для объекта, вызывая lock() , чтобы получить доступ к объекту.

auto p = std::make_shared(42); std::weak_ptr wp = p; < auto sp = wp.lock(); std::cout p.reset(); if(wp.expired()) std::cout  
#7 — лямбды

В новом стандарте наконец-то была добавлена поддержка лямбда-выражений. Мы можете использовать лямбды везде, где ожидается функтор или std::function . Лямбда, вообще говоря, представляет собой более короткую запись функтора, что-то вроде анонимного функтора. Подробнее можно почитать, например, на MSDN.

std::vector v; v.push_back(1); v.push_back(2); v.push_back(3); std::for_each(std::begin(v), std::end(v), [](int n) ); auto is_odd = [](int n) ; auto pos = std::find_if(std::begin(v), std::end(v), is_odd); if(pos != std::end(v)) std::cout  

Теперь немного более хитрые — рекурсивные лямбды. Представьте лямбду, представляющую функцию Фибоначчи. Если вы попытаетесь записать ее, используя auto , то получите ошибку компиляции:

auto fib = [&fib](int n) ; 
error C3533: 'auto &': a parameter cannot have a type that contains 'auto' error C3531: 'fib': a symbol whose type contains 'auto' must have an initializer error C3536: 'fib': cannot be used before it is initialized error C2064: term does not evaluate to a function taking 1 arguments 

Здесь имеет место циклическая зависимость. Чтобы избавиться от нее, необходимо явно определить тип функции, используя std::function .

std::function lfib = [&lfib](int n) ; 
#8 — non-member begin() и end()

Вы, вероятно, заметили, что в примерах ранее, я использовал функции begin() и end() . Это новое дополнение к стандартной библиотеке. Они работают со всеми контейнерами STL и могут быть расширены для работы с любым типом.

Давайте возьмем, например, предыдущий пример, где я выводил вектор и затем искал первый нечетный элемент. Если std::vector заменить С-подобным массивом, то код будет выглядеть так:

int arr[] = ; std::for_each(&arr[0], &arr[0]+sizeof(arr)/sizeof(arr[0]), [](int n) ); auto is_odd = [](int n) ; auto begin = &arr[0]; auto end = &arr[0]+sizeof(arr)/sizeof(arr[0]); auto pos = std::find_if(begin, end, is_odd); if(pos != end) std::cout  

С begin() и end() его можно переписать следующим образом:

int arr[] = ; std::for_each(std::begin(arr), std::end(arr), [](int n) ); auto is_odd = [](int n) ; auto pos = std::find_if(std::begin(arr), std::end(arr), is_odd); if(pos != std::end(arr)) std::cout  

Это почти полностью идентично коду с std::vector . Таким образом, мы можем написать один универсальный метод для всех типов, которые поддерживаются функциями begin() и end() .

template void bar(Iterator begin, Iterator end) < std::for_each(begin, end, [](int n) ); auto is_odd = [](int n) ; auto pos = std::find_if(begin, end, is_odd); if(pos != end) std::cout template void foo(C c) < bar(std::begin(c), std::end(c)); >template void foo(T(&arr)[N]) < bar(std::begin(arr), std::end(arr)); >int arr[] = ; foo(arr); std::vector v; v.push_back(1); v.push_back(2); v.push_back(3); foo(v); 
#9 — static_assert и классы свойств

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

template class Vector < static_assert(Size >3, "Size is too small"); T _points[Size]; >; int main() < Vectora1; Vector a2; return 0; > 
error C2338: Size is too small see reference to class template instantiation 'Vector' being compiled with [ T=double, Size=2 ] 

static_assert становится более полезен, когда используется с классами свойств. Это набор классов, которые предоставляют информацию о типах во время компиляции. Они доступны в заголовке . Есть несколько видов классов в этом заголовке: классы-помощники, классы преобразований и непосредственно классы свойств.
В следующем примере, функция add , как предполагается, работает только с целочисленными типами.

template auto add(T1 t1, T2 t2) -> decltype(t1 + t2)

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

std::cout  

Программа просто выведет «4.14» и «е». Используя static_assert , эти две строки вызовут ошибку во время компиляции.

template auto add(T1 t1, T2 t2) -> decltype(t1 + t2) < static_assert(std::is_integral::value, "Type T1 must be integral"); static_assert(std::is_integral::value, "Type T2 must be integral"); return t1 + t2; > 
error C2338: Type T2 must be integral see reference to function template instantiation 'T2 add(T1,T2)' being compiled with [ T2=double, T1=int ] error C2338: Type T1 must be integral see reference to function template instantiation 'T1 add(T1,T2)' being compiled with [ T1=const char *, T2=int ] 
#10 — семантика перемещения

Это — еще одна важная тема, затронутая в С++11. На эту тему можно написать несколько статей, а не абзацев, поэтому я не буду сильно углубляться.

C++11 ввел понятие rvalue ссылок (указанных с &&), чтобы отличать ссылка на lvalue (объект, у которого есть имя) и rvalue (объект, у которого нет имени). Семантика перемещения позволяет изменять rvalues (ранее они считались неизменными и не отличались от типов const T&).

Класс/структура раньше имели некоторые неявные функции-члены: конструктор по умолчанию (если другой конструктор не определен), конструктор копирования и деструктор. Конструктор копирования выполняет поразрядное копирование переменных. Это означает, что если у вас есть класс с указателями на какие-то объекты, то конструктор копирования скопирует указатели, а не объекты, на которые они указывают. Если вы хотите получить в копии именно объекты, а не лишь указатели на них, вы должны это явно описать в конструкторе копирования.

Конструктор перемещения и оператор присваивания перемещения — эти две специальные функции принимают параметр T&&, который является rvalue. Фактически, они могут изменять объект.

Следующий пример показывает фиктивную реализацию буфера. Буфер идентифицируется именем, имеет указатель (обернутый в std::unique_ptr ) на массив элементов типа Т и переменную, содержащую размер массива.

template class Buffer < std::string _name; size_t _size; std::unique_ptr_buffer; public: // default constructor Buffer(): _size(16), _buffer(new T[16]) <> // constructor Buffer(const std::string& name, size_t size): _name(name), _size(size), _buffer(new T[size]) <> // copy constructor Buffer(const Buffer& copy): _name(copy._name), _size(copy._size), _buffer(new T[copy._size]) < T* source = copy._buffer.get(); T* dest = _buffer.get(); std::copy(source, source + copy._size, dest); >// copy assignment operator Buffer& operator=(const Buffer& copy) < if(this != &copy) < _name = copy._name; if(_size != copy._size) < _buffer = nullptr; _size = copy._size; _buffer = (_size >0)? new T[_size] : nullptr; > T* source = copy._buffer.get(); T* dest = _buffer.get(); std::copy(source, source + copy._size, dest); > return *this; > // move constructor Buffer(Buffer&& temp): _name(std::move(temp._name)), _size(temp._size), _buffer(std::move(temp._buffer)) < temp._buffer = nullptr; temp._size = 0; >// move assignment operator Buffer& operator=(Buffer&& temp) < assert(this != &temp); // assert if this is not a temporary _buffer = nullptr; _size = temp._size; _buffer = std::move(temp._buffer); _name = std::move(temp._name); temp._buffer = nullptr; temp._size = 0; return *this; >>; template Buffer getBuffer(const std::string& name) < Bufferb(name, 128); return b; > int main() < Bufferb1; Buffer b2("buf2", 64); Buffer b3 = b2; Buffer b4 = getBuffer("buf4"); b1 = getBuffer("buf5"); return 0; > 

Конструктор копирования по умолчанию и оператор присваивания копии должны быть вам знакомы. Новое в С++11 — это конструктор перемещения и оператор присваивания перемещения, Если вы выполните этот код, то увидите, что когда создается b4 — вызывается конструктор перемещения. Кроме того, когда b1 присваивается значение — вызывается оператор присваивания перемещения. Причина — значение, возвращаемое функцией getBuffer() — rvalue.

Вы, вероятно, заметили использование std::move в конструкторе перемещения, при инициализации имени переменной и указателя на буфер. Имя — это строка std::string и std::string также реализует семантику перемещения. То же самое касается и unique_ptr . Однако, если бы мы записали просто _name(temp._name) , то был бы вызван конструктор копирования. Но почему в этом случае не был вызван конструктор перемещения для std::string ? Дело в том, что даже если конструктор перемещения для Buffer был вызван с rvalue, внутри конструктора это все равно представляется как lvalue. Чтобы сделать его снова rvalue и нужно использовать std::move . Эта функция просто превращает ссылку lvalue в rvalue.

Вместо заключения

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

  • Веб-разработка
  • Программирование
  • C++

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

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