Статьи

C ++ лаконично: указатели, ссылки и правильность констант

Указатель — это не более чем переменная, которая содержит адрес памяти. При правильном использовании указатель содержит действительный адрес памяти, который содержит объект, который совместим с типом указателя. Как и ссылки в C #, все указатели в конкретной среде выполнения имеют одинаковый размер, независимо от типа данных, на которые указывает указатель. Например, когда программа скомпилирована и запущена в 32-разрядной операционной системе, указатель обычно составляет 4 байта (32 бита).

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

Из-за возможных утечек вы никогда не должны выделять динамическую память вне интеллектуального указателя. Стандартная библиотека C ++ предоставляет два интеллектуальных указателя, которые вы должны учитывать: std::shared_ptr и std::unique_ptr .

Помещая объекты динамической длительности в один из них, вы гарантируете, что когда std::unique_ptr или последний std::shared_ptr который содержит указатель на эту память, выходит из области видимости, память будет должным образом освобождена с правильной версией удалить (удалить или удалить []), чтобы не было утечки. Это шаблон RAII из предыдущей главы в действии.

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

Если вы не используете умные указатели, вы просто просите утечку памяти. Любое исключение между выделением памяти с новым или новым [] и освобождением памяти с помощью delete или delete [] может привести к утечке памяти. Если вы не будете осторожны, вы можете случайно использовать указатель, который уже был удален, но не был установлен равным nullptr. После этого вы получите доступ к некоторому случайному месту в памяти и будете обращаться с ним, как с действительным указателем.

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


Указатель const принимает форму SomeClass* const someClass2 = &someClass1; , Другими словами, * предшествует const. В результате сам указатель не может указывать на что-либо еще, но данные, на которые указывает указатель, остаются изменяемыми. Это вряд ли будет очень полезно в большинстве ситуаций.

Указатель на const принимает форму const SomeClass* someClass2 = &someClass1; , В этом случае * следует после const. В результате указатель может указывать на другие вещи, но вы не можете изменять данные, на которые он указывает. Это распространенный способ объявления параметров, которые вы просто хотите проверить без изменения их данных.

Указатель const на const принимает форму const SomeClass* const someClass2 = &someClass1; , Здесь * находится между двумя ключевыми словами const. В результате указатель не может указывать на что-либо еще, и вы не можете изменять данные, на которые он указывает.

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

Например, int GetSomeInt(void) const; объявляет функцию-член const — функцию-член, которая не изменяет данные объекта, которому она принадлежит. Компилятор будет применять эту гарантию. Это также обеспечит гарантию того, что когда вы передаете объект в функцию, которая принимает его как const, эта функция не может вызывать какие-либо неконстантные функции-члены этого объекта.

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


При работе с указателями, включая интеллектуальные указатели, интерес представляют три оператора: *, & и ->.

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

Оператор p_someInt = 5000000; не будет присваивать значение 5000000 целому числу, на которое указывает. Вместо этого указатель указывал бы на адрес памяти 5000000, 0X004C4B40 в 32-разрядной системе. Что находится по адресу памяти 0X004C4B40? Кто знает? Это может быть ваше целое число, но скорее всего, это что-то другое. Если вам повезет, это неверный адрес. В следующий раз, когда вы попытаетесь правильно использовать p_someInt , ваша программа p_someInt крах. Если это действительный адрес данных, то вы, скорее всего, испортите данные.

Оператор *p_someInt = 5000000; присвоит значение 5000000 целому числу, на которое указывает p_someInt. Это оператор косвенного действия в действии; он принимает p_someInt и заменяет его L-значением, которое представляет данные по указанному адресу (мы скоро обсудим L-значения).

Оператор address-of, &, выбирает адрес переменной или функции. Это позволяет вам создать указатель на локальный объект, который вы можете передать функции, которая хочет указатель. Вам даже не нужно создавать локальный указатель, чтобы сделать это; Вы можете просто использовать локальную переменную с оператором address-of перед ней в качестве аргумента, и все будет работать отлично.

Указатели на функции аналогичны экземплярам делегатов в C #. Учитывая это объявление функции: double GetValue(int idx); это будет правильный указатель на функцию: double (*SomeFunctionPtr)(int); ,

Если ваша функция вернула указатель, скажем так: int* GetIntPtr(void); тогда это будет правильный указатель на функцию: int* (*SomeIntPtrDelegate)(void); , Не позволяйте двойным звездочкам беспокоить вас; просто запомните первый набор скобок вокруг * и имени указателя функции, чтобы компилятор правильно интерпретировал это как указатель на функцию, а не объявление функции.

Оператор доступа -> — это то, что вы используете для доступа к членам класса, когда у вас есть указатель на экземпляр класса. Он функционирует как комбинация оператора косвенности и. член доступа оператора. Итак, p_someClassInstance->SetValue(10); и (*p_someClassInstance).SetValue(10); оба делают одно и то же.

Это не было бы C ++, если бы мы не говорили о L-значениях и R-значениях хотя бы кратко. L-значения называются так, потому что они традиционно появляются на левой стороне знака равенства. Другими словами, они являются значениями, которые могут быть назначены — те, которые выживут при оценке текущего выражения. Наиболее известным типом L-значения является переменная, но она также включает результат вызова функции, которая возвращает ссылку на L-значение.

R-значения традиционно отображаются в правой части уравнения или, возможно, более точно, они являются значениями, которые не могли отображаться слева. Это такие вещи, как константы или результат оценки уравнения. Например, a + b, где a и b могут быть L-значениями, но результатом сложения их вместе является R-значение или возвращаемое значение функции, которая возвращает что-либо кроме void или ссылку на L-значение.

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

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

Существует два типа ссылок: ссылки на L-значения и ссылки на R-значения. Ссылка на L-значение помечается символом &, добавляемым к имени типа (например, SomeClass &), тогда как ссылка на R-значение помечается символом &&, добавляемым к имени типа (например, SomeClass &&). По большей части они действуют одинаково; Основное отличие состоит в том, что ссылка R-значения чрезвычайно важна для перемещения семантики.


В следующем примере показано использование указателя и ссылки с пояснениями в комментариях.

Пример: PointerSample \ PointerSample.cpp

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#include <memory>
//// See the comment to the first use of assert() in _pmain below.
//#define NDEBUG 1
#include <cassert>
#include «../pchar.h»
 
using namespace std;
 
void SetValueToZero(int& value)
{
    value = 0;
}
 
void SetValueToZero(int* value)
{
    *value = 0;
}
 
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
    int value = 0;
 
    const int intArrCount = 20;
    // Create a pointer to int.
    int* p_intArr = new int[intArrCount];
 
    // Create a const pointer to int.
    int* const cp_intArr = p_intArr;
 
    // These two statements are fine since we can modify the data that a
    // const pointer points to.
    // Set all elements to 5.
    uninitialized_fill_n(cp_intArr, intArrCount, 5);
    // Sets the first element to zero.
    *cp_intArr = 0;
 
    //// This statement is illegal because we cannot modify what a const
    //// pointer points to.
    //cp_intArr = nullptr;
 
    // Create a pointer to const int.
    const int* pc_intArr = nullptr;
 
    // This is fine because we can modify what a pointer to const points
    // to.
    pc_intArr = p_intArr;
 
    // Make sure we «use» pc_intArr.
    value = *pc_intArr;
 
    //// This statement is illegal since we cannot modify the data that a
    //// pointer to const points to.
    //*pc_intArr = 10;
 
    const int* const cpc_intArr = p_intArr;
 
    //// These two statements are illegal because we cannot modify
    //// what a const pointer to const points to or the data it
    //// points to.
    //cpc_intArr = p_intArr;
    //*cpc_intArr = 20;
 
    // Make sure we «use» cpc_intArr.
    value = *cpc_intArr;
 
    *p_intArr = 6;
 
    SetValueToZero(*p_intArr);
 
    // From <cassert>, this macro will display a diagnostic message if the
    // expression in parentheses evaluates to anything other than zero.
    // Unlike the _ASSERTE macro, this will run during Release builds.
    // disable it, define NDEBUG before including the <cassert> header.
    assert(*p_intArr == 0);
 
    *p_intArr = 9;
 
    int& r_first = *p_intArr;
 
    SetValueToZero(r_first);
 
    assert(*p_intArr == 0);
 
    const int& cr_first = *p_intArr;
 
    //// This statement is illegal because cr_first is a const reference,
    //// but SetValueToZero does not take a const reference, only a
    //// non-const reference, which makes sense considering it wants to
    //// modify the value.
    //SetValueToZero(cr_first);
 
    value = cr_first;
 
    // We can initialize a pointer using the address-of operator.
    // Just be wary because local non-static variables become
    // invalid when you exit their scope, so any pointers to them
    // become invalid.
    int* p_firstElement = &r_first;
 
    *p_firstElement = 10;
 
    SetValueToZero(*p_firstElement);
 
    assert(*p_firstElement == 0);
 
    // This will call the SetValueToZero(int*) overload because we
    // are using the address-of operator to turn the reference into
    // a pointer.
    SetValueToZero(&r_first);
 
    *p_intArr = 3;
 
    SetValueToZero(&(*p_intArr));
 
    assert(*p_firstElement == 0);
 
    // Create a function pointer.
    // variable name in parentheses with a * before it.
    void (*FunctionPtrToSVTZ)(int&) = nullptr;
 
    // Set the function pointer to point to SetValueToZero.
    // the correct overload automatically.
    FunctionPtrToSVTZ = &SetValueToZero;
 
    *p_intArr = 20;
 
    // Call the function pointed to by FunctionPtrToSVTZ, ie
    // SetValueToZero(int&).
    FunctionPtrToSVTZ(*p_intArr);
 
    assert(*p_intArr == 0);
 
    *p_intArr = 50;
 
    // We can also call a function pointer like this.
    // closer to what is actually happening behind the scenes;
    // FunctionPtrToSVTZ is being de-referenced with the result
    // being the function that is pointed to, which we then
    // call using the value(s) specified in the second set of
    // parentheses, ie *p_intArr here.
    (*FunctionPtrToSVTZ)(*p_intArr);
 
    assert(*p_intArr == 0);
 
    // Make sure that we get value set to 0 so we can «use» it.
    *p_intArr = 0;
    value = *p_intArr;
 
    // Delete the p_intArray using the delete[] operator since it is a
    // dynamic p_intArray.
    delete[] p_intArr;
    p_intArr = nullptr;
    return value;
}

Я упоминаю volatile только для того, чтобы не использовать его. Как и const, переменная может быть объявлена ​​как volatile. Вы можете даже иметь постоянную изменчивость; два не являются взаимоисключающими.

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

Эрик Липперт, член языковой команды C # в Microsoft, описал использование volatile как: «Знак того, что вы делаете что-то совершенно сумасшедшее: вы пытаетесь читать и записывать одно и то же значение в двух разных потоках без блокировки на месте ». Он прав, и его аргумент прекрасно переносится в C ++.

Использование volatile следует воспринимать с большим скептицизмом, чем использование goto. Я говорю это потому, что могу вспомнить хотя бы одно правильное универсальное использование goto: выход из глубоко вложенной циклической конструкции после выполнения неисключительного условия. volatile, напротив, действительно полезна, только если вы пишете драйвер устройства или пишете код для какого-либо типа микросхемы ПЗУ. На этом этапе вы действительно должны быть хорошо знакомы со стандартом языка программирования ISO / IEC C ++, со спецификациями оборудования для среды исполнения, в которой будет выполняться код, и, вероятно, со стандартом языка ISO / IEC C.

Примечание. Вы также должны быть знакомы с языком ассемблера для целевого оборудования, чтобы вы могли взглянуть на сгенерированный код и убедиться, что компилятор генерирует правильный код (PDF) для использования вами volatile.

Я игнорировал существование ключевого слова volatile и буду продолжать это делать до конца этой книги. Это совершенно безопасно, так как:

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

Последнее замечание о volatile: один из возможных эффектов — более медленный код. Когда-то люди думали, что изменчивость дает тот же результат, что и атомарность. Это не так. При правильной реализации атомарность гарантирует, что несколько потоков и несколько процессоров не смогут одновременно считывать и записывать фрагмент памяти с атомарным доступом. Механизмами для этого являются замки, мьютексы, семафоны, заборы, специальные инструкции процессора и тому подобное. Единственное, что делает volatile — это заставляет ЦП извлекать переменную из памяти, а не использовать любое значение, которое она могла бы кэшировать в регистре или в стеке. Извлечение памяти замедляет все.

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

Этот урок представляет собой главу из C ++ Succinctly , бесплатной книги от команды Syncfusion .