В этой статье будет рассказано:
- Указатели и свободное хранилище
- Указатели и философия С++
- Объявление и инициализация указателей
- Опасность, связанная с указателями
- Золотое правило указателей
Указатели и свободное хранилище
Есть три фундаментальных свойства, которые должна отслеживать компьютерная программа, когда она сохраняет данные.
Перечислю их:
• где хранится информация;
• какое значение сохранено;
• разновидность сохраненной информации.
Пока что вы использовали только одну стратегию: объявление простых переменных. В операторе объявления предоставляется тип и символическое имя значения. Он также заставляет программу выделить память для этого значения и внутренне отслеживать ее местоположение.
Давайте рассмотрим другую стратегию, важность которой проявляется при разработке классов С++. Эта стратегия основана на указателях, которые представляют собой переменные, хранящие адреса значений вместо самих значений. Но прежде чем обратиться к указателям, давайте поговорим о том, как явно получить адрес обычной переменной. Для этого применяется операция взятия адреса, обозначаемая символом &, к переменной, адрес которой интересует. Например, если home - переменная, то &home - се адрес.
Продемонстрирую использование этой операции:
*знак решетки* include <iostream>
int main ()
{
using namespace std;
int donuts = 6;
double cups = 4.5 ;
cout << "donuts value = " << donuts ;
cout << " and donuts address = " << &donuts << endl ;
cout << " cups value = " << cups;
cout << " and cups address = " << &cups << endl ;
return 0;
}
Результат
donuts value = 6 and donuts address = 0x0065fd40
cups value = 4.5 and cups address = 0x0065fd44
В показанной здесь конкретной реализации cout используется шестнадцатеричная нотация при отображении значений адресов, т.к. это обычная нотация, применяемая для указания адресов памяти. (Некоторые реализации применяют десятичную нотацию.) Наша реализация сохраняет donuts в памяти с меньшими адресами , чем cups. Разница между этими двумя адресами составляет 0x0065fd44-0x0065fd40, или 4 байта. Конечно, в разных системах вы получите разные значения этих адресов. К тому же некоторые системы могут сохранять cups перед donuts, и разница между адресами составит 8, потому что cups имеет тип double. Другие системы могут даже разместить эти переменные в памяти далеко друг от друга, а не рядом.
Таким образом, использование обычных переменных трактует значение как именованную величину, а ее местоположение - как производную величину. Теперь рассмотрим стратегию указателей, которая представляет важнейшую часть философии программирования С++ в части управления памятью.
Указатели и философия С++
Объектно-ориентированное программирование (ООП) отличается от традиционного процедурного программирования в том, что ООП делает особый акцент на принятии решений во время выполнения вместо времени компиляции. Время выполнения означает период работы программы, а время компиляции - период сборки программы компилятором в единое целое. Решения, принимаемые во время выполнения - это вроде того, как, будучи в отпуске, , вы принимаете решение о том, какие достопримечательности стоит осмотреть , в зависимости от погоды и вашего настроения, в то время как решения, принимаемые во время компиляции, больше похожи на следование заранее разработанному плану, вне зависимости от любых условий.
Решения времени выполнения обеспечивают гибкость, позволяющую программе приспосабливаться к текущим условиям. Например, рассмотрим выделение памяти для массива. Традиционный сnособ предполагает объявление массива. Чтобы объявить массив в С++, вы должны заранее решить, какого он должен быть размера. Таким образом, размер массива устанавливается во время компиляции программы, т.е. это решение времени компиляции. 1 68 глава 4 Возможно, вы думаете, что массив из 20 элементов будет достаточным в течение 80% времени, но однажды программе понадобится разместить 200 элементов. Чтобы обезопасить себя, вы используете массив размером в 200 элементов. Это приводит к тому, что ваша программа большую часть времени расходует память впустую. ООП пытается сделать программы более гибкими, откладывая принятие таких решений на стадию выполнения. Таким образом, после того, как программа запущена, она самостоятельно сможет решить, когда ей нужно размещать 20 элементов, а когда 200.
Короче говоря, с помощью ООП вы можете сделать выяснение размера массива решением времени выполнения. Чтобы обеспечить такой подход, язык должен предоставлять возможность создавать массивы - или что-то им подобное - непосредственно во время работы программы. Как вы вскоре увидите, метод, используемый С++, включает применение ключевого слова new для запроса необходимого объема памяти и применение указателей для нахождения выделенной по запросу памяти. Принятие решений во время выполнения не является уникальной особенностью ООП. Но язык С++ делает написание соответствующего кода более прямолинейным, чем это позволяет С
Новая стратегия хранения данных изменяет трактовку местоположения как именованной величины, а значения - как производной величины. Для этого предусмотрен специальный тип переменной - указатель, который может хранить адрес значения. Таким образом, имя указателя представляет местоположение. Применяя операцию * , называемую косвенным значением или операцией разыменования, можно получить значение, хранящееся в указанном месте. (Да, это тот же символ * , который применяется для обозначения арифметической операции умножения; С++ использует контекст для определения того, что подразумевается в каждом конкретном случае - умножение или разыменование.) Предположим, например, что manly - это указатель. В таком случае manly представляет адрес, а *manly - значение, находящееся по этому адресу. Комбинация *manly становится эквивалентом простой переменной типа int.
Продемонстрирую:
*знак решетки* include <iostream>
int main ()
{
using namespace std;
int updates = 6; / / объявление переменной
int * p_upda tes; // объявление указателя на int
p_upda tes = & updates ; / / присвоить адрес int указателю
// Выразить значения двумя способами
cout << "Value s : updates = " << updates;
cout << ", *p_updates =" << *p_updates << endl ;
/ / Выразить адреса двумя способами
cout << "Addresses : & upda tes = " << &updates;
cout << ", p_updates = " << p_upda tes << endl ;
/ / Изменить значение через указатель
*p_upda tes = *p_upda tes + 1;
cout << "Now upda tes = " << updates << endl ;
return 0
}
Результат
Values : updates = 6, *p_updates = 6
Addresses : &updates = 0x0065fd48, p_updates = 0x0065fd48
Now updates = 7
Как видите, переменная updates типа int и переменная-указатель p_updates - это две стороны одной монеты. Переменная updates в первую очередь представляет значение, а для получения его адреса используется операция &, в то время как p_updates представляет адрес, а для получения значения применяется операция * . Поскольку p_updates указывает на updates, конструкции *p_updates и updates полностью эквивалентны. Вы можете использовать *p_updates точно так же, как используете переменную типа int. Как показано в примере, можно даже присваивать значения *p_updates. Это изменяет значение указываемой переменной - updates.
Объявление и инициализация указателей
Давайте рассмотрим процесс объявления указателей. Компьютеру нужно отслеживать тип значения, на которое ссылается указатель. Например, адрес char обычно выглядит точно так же, как и адрес double, но char и double использует разное количество байт и разный внутренний формат представления значений. Поэтому объявление указателя должно задавать тип данных указываемого значения. Например, предыдущий пример содержит следующее объявление:
int * p_updates;
Этот оператор устанавливает, что комбинация *p_updates имеет тип int. Поскольку вы используете операцию *, применяя ее к указателю, сама переменная p_updates должна быть указателем. Мы говорим, что p_update указывает на тип int. Мы также говорим, что тип p_updates - это указатель на int, или точнее, int *. Итак, повторим еще раз: p_updates - это указатель (адрес) , а * p_updates - это int, а не указатель. К слову, пробелы вокруг операции * не обязательны. Традиционно программисты на С используют следующую форму:
int *ptr;
Это подчеркивает идею, что комбинация *ptr является значением типа int. С другой стороны, многие программисты на С++ отдают предпочтение такой форме:
int* ptr;
Это подчеркивает идею о том, что int* - это тип "указатель на int". Для компилятора не важно, с какой стороны вы поместите пробел. Можно даже записать так: int*ptr;
Однако учтите , что следующее объявление создает один указатель, (p1) и одну обычную переменную типа int (р2):
int* p1 , р2 ;
Знак * должен быть помещен возле каждой переменной типа указателя.
Тот же самый синтаксис применяется для объявления указателей па другие типы:
double * tax_ptr; // tax_ptr указывает на тип double
char * str; // str указывает на тип char
Поскольку вы объявляете tax_ptr как указатель на double, компилятор знает, что * tax_ptr - это значение типа double. То есть ему известно, что * tax_ptr представляет число, сохраненное в формате с плавающей точкой и занимающее (в большинстве систем) 8 байт. Переменная указателя никогда не бывает просто указателем. Она всегда указывает на определенный тип. tax_ptr имеет тип "указатель на double" ( или тип double *), а str - это тип "указатель на char" (или тип char *). Хотя оба они являются указателями, но указывают они на значения двух разных типов. Подобно массивам, указатели базируются на других типах.
Обратите внимание, что в то время как tax _ptr и str указывают на типы данных двух разных размеров, сами переменные tax_ptr и str обычно имеют одинаковый размер. То есть адрес char имеет тот же размер, что и адрес double - точно так же, как 1016 может быть номером дома, в котором располагается огромный склад, в то время как 1024 - номером небольшого коттеджа. Размер или значение адреса на самом деле ничего не говорят о том, какого вида и размера переменная или строение находится по этому адресу. Обычно адрес требует от 2 до 4 байт, в зависимости от компьютерной системы. (В некоторых системах могут применяться и более длинные адреса, а иногда система использует разный размер адресов для разных типов.)
Инициализировать указатель можно в операторе объявления. В этом случае инициализируется указатель, а не значение, на которое он указывает. То есть следующие операторы устанавливают pt, а не *pt равным значению &higgens :
int higgens = 5 ;
int *pt = &higgens;
Продемонстрирую:
*знак решетки*include <iostream>
int main()
{
using namespace std;
int higgens = 5 ;
int * pt = &higgens ;
cout << "Value of higgens = " << higgens << " ;
Address of higgens = " << &higgens << endl ;
cout << "Value of *pt = " << *pt << " ;
Value of pt = " << pt << endl ;
return 0;
}
Результат
Value of higgens = 5; Address of higgens = 0012FED4
Value of *pt = 5; Value of pt = 0012FED4
Как видите , программа инициализирует pt, а не *pt, адресом переменной higgens. (Скорее всего, вы получите в своей системе другое значение адреса и, возможно, отображенное в другом формате.)
Опасность, связанная с указателями
Опасность подстерегает тех, кто использует указатели неосмотрительно. Очень важно понять, что при создании указателя в коде С++ компьютер выделяет память для хранения адреса, но не выделяет памяти для хранения данных, на которые указьmает этот адрес. Выделение места для данных требует отдельного шага. Если пропустить этот шаг, как в следующем фрагменте, то это обеспечит прямой путь к проблемам:
long * fellow ; // создать указатель на long
* fellow = 223323; // поместить значение в неизвестное место
Конечно, fellow - это указатель. Но на что он указывает? Никакого адреса переменной fellow в коде не присвоено. Так куда будет помещено значение 223323? Ответить на это невозможно. Поскольку переменная fellow не была инициализирована, она может иметь какое угодно значение. Что бы в ней ни содержалось, программа будет интерпретировать это как адрес , куда и поместит 223323. Если так случится, что fellow будет иметь значение 1200, компьютер попытается поместить данные по адресу 1200, даже если этот адрес окажется в середине вашего программного кода. На что бы ни указывал fellow, скорее всего, это будет не то место, куда вы хотели бы поместить число 223323. Ошибки подобного рода порождают самое непредсказуемое поведение программы и такие ошибки очень трудно отследить.
ВНИМАНИЕ!
Золотое правило указателей: всегда инициализируйте указатель, чтобы определить точный и правильный адрес, прежде чем применять к нему операцию разыменования (*) .
Конец
Спасибо, если вы прочитали эту статью. Надеюсь вы что-то новое узнали для себя, и конечно же поняли. Подпишитесь, поставьте лайк, напишите комментарий, поддержите меня. Хотелось бы увидеть как улучшать статьи и чего не хватает. Буду анализировать и улучшать контент. Еще раз спасибо, до свидания! :)
#c++ #компиляторы #программирование #разработка #указатели #ооп