C++. Указатели. Часть 2. Неуправляемые указатели. Операции над указателями. Указатель на тип void. Выделение памяти. Нулевой указатель. Операция взятия адреса &




Указатели. Часть 2. Неуправляемые указатели. Операции над указателями. Указатель на тип void. Выделение памяти. Ключевые слова NULL и nullptr. Операция взятия адреса &

В данной теме описывается работа с неуправляемыми указателями. Как известно, Visual C++ поддерживает также и управляемые указатели.

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


Содержание


Поиск на других ресурсах:

1. Какие операторы можно использовать над неуправляемыми указателями?

Над неуправляемыми указателями используются два оператора:

  • * – предназначен для обращения к переменной, которая размещается по адресу, который задается операндом этого оператора. Этот оператор ссылается на значение переменной, которая записана в указателе. Это унарный оператор, в котором операнд размещается справа.
  • & – возвращает адрес памяти операнда, размещающегося справа. Это унарный оператор.

Операторы * и & дополняют друг друга.

Например. Указатель с именем px ссылается на переменную x вещественного типа. Пусть имеется следующее описание:

float x;
float *px; // указатель на переменную вещественного типа
px = &x; // px указывает на x

это значение *px есть значением переменной x, что демонстрирует следующая строка

*px = 25; // x = 25.0

Значение px есть адресом переменной x в оперативной памяти.

C++ указатель переменная рисунок

Рисунок 1. Указатель px указывает на переменную x

2. Что такое операция косвенного доступа с помощью указателя?

Операция косвенного доступа – это доступ к переменной (объекту) с использованием указателя. С помощью указателя можно получать доступ к массивам переменных (объектов).

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

3. Каким образом компилятор C++ определяет объем обрабатываемой информации, на которую указывает указатель?

Объем обрабатываемой информации определяется базовым типом указателя.

Пример. Пусть дано следующее описание:

double * pf;

базовый тип указателя есть double. Одна переменная типа double занимает в памяти компьютера 8 байт. Поэтому компилятор C++ будет обрабатывать 8 байт информации при доступе к ней по указателю.






4. Операция присваивания указателей. Пример

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

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

Пример.

// присваивание указателей
int a = 5;      // обычная переменная
int * pi1 = &a; // инициализация указателя адресом переменной a
int * pi2;     // указатель на int
double * pi3;  // указатель на double

pi2 = pi1;     // допустимо

//pi3 = pi1; // ошибка - "cannot convert from int to double"
pi3 = (double *) pi1; // работает - операция приведения типов

5. Арифметические операции над указателями. Изменение физического адреса указателя. Примеры

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

  • операции инкремента ++ и декремента ;
  • операции сложения + и вычитания .

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

При изменении значения указателя с помощью арифметических операций физический адрес указателя изменяется по формуле:

N * sizeof(тип)

где

  • N – константа, которая указывает изменение (увеличение/уменьшение) значения указателя;
  • тип – тип данных, на который указывает указатель (базовый тип указателя);
  • sizeof(тип) – операция, которая определяет размер типа данных.

Пример 1. Изменение значения указателя.

// увеличение/уменьшение значения указателя
int a = 5;     // обычная переменная
int * pi1 = &a; // инициализация указателя адресом переменной a
int * pi2;     // указатель на int

pi2 = pi1;     // указывают на один адрес памяти
pi2++;         // физический адрес pi2 на 4 байта больше чем адрес pi1

В вышеприведенном примере физический адрес указателя pi2 больше на 4 байта от физического адреса указателя pi1. Это связано с тем, что тип int (базовый тип указателей pi1 и pi2) имеет размер 4 байта (среда Win32).

Пример 2. Изменение значения указателя на тип double.

// увеличение/уменьшение значения указателя
double x = 3.55;   // обычная переменная типа double
double * p1 = &x; // инициализация указателя адресом переменной a
double * p2;     // указатель на double

p2 = p1-4;   // физический адрес p2 на 32 байта меньше чем адрес p1
p2 = p1 + 2; // физический адрес p2 на 16 байт больше чем адрес p1

6. Какие операции отношения (сравнения) можно выполнять над указателями?

Указатели можно сравнивать. Для сравнения указателей используются операции ==, >, <.

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

Например, если указатели указывают на разные ничем не связанные переменные, то операция сравнения не имеет смысла.

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

Пример. Пусть дан массив вещественных чисел A и 2 указателя.

// сравнение указателей
float A[20]; // массив из 20 вещественных чисел
float *pa1, *pa2; // два указателя на float
bool f_res; // результат сравнения указателей

// ...

pa1 = &A[5]; // указывает на 5-й элемент массива
pa2 = &A[8]; // указывает на 8-й элемент массива

f_res = pa1==pa2; // f_res = false
f_res = pa1>pa2;  // f_res = false
f_res = pa1<pa2;  // f_res = true

7. Какие особенности использования указателя на тип void? Пример

Язык C/C++ позволяет использовать указатель на тип void.

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

Чтобы привести указатель на void к указателю на другой тип нужно использовать операцию приведения типов.

Пример. Использование указателя на тип void для доступа к переменным разных типов.

// указатель на тип void
void * p;
int d;
float x;
char c;

d = 5;
x = 2.58f;
c = 'A';

p = (int *)&d; // p указывает на d
*(int *)p = 8; // d = 8

p = (float *)&x; // p указывает на x
*(float *)p = -8.75; // x = -8.75

p = (char *)&c;   // p указывает на c
*(char *)p = 'r'; // c = 'r'

8. Каким образом указателю присваивается нулевое значение? Ключевые слова NULL и nullptr. Пример

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

В модуле <stdio.h> определена константа NULL с нулевым значением. Ее можно использовать для присвоения нулевого значения указателю.

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

Пример 1. Присвоение нулевого значения указателю.

#include <stdio.h>

...

// нулевой указатель
int * p;
p = nullptr; // работает
p = NULL; // работает, константа NULL описывается в модуле <stdio.h>
p = 0; // работает
p = 0x00; // также работает

Пример 2. Случай, когда лучше использовать указатель nullptr вместо NULL.

#include <iostream>
using namespace std;

void func(std::pair<const char*, double> pr)
{
  // ...
}

void main()
{
  // Не работает - ошибка компиляции
  //func(make_pair(NULL, 3.14)); // NULL => 0 => int

  // Работает
  func(make_pair(nullptr, 3.14)); // nullptr => nullptr_t => const char* 
}

В вышеприведенном примере указатель NULL конвертируется в число 0, которое по умолчанию имеет тип int. А в  нашем случае нужно чтобы был тип const char*. При использовании указателя nullptr этой ошибки не будет.

9. Пример выделения памяти функцией malloc(), которая возвращает указатель на тип void.

Функция malloc() выделяет память для неуправляемого указателя. Функция возвращает тип void *.

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

Пример. Использование функции malloc().

// пример выделения памяти под неуправляемый указатель
int * p;
p = NULL;

// выделение памяти функцией malloc() для указателя на int
p = (int *)malloc(sizeof(int));

// выделение памяти функцией malloc() для указателя на double
double * pd = NULL;
pd = (double *)malloc(sizeof(double));

10. Какие ограничения накладываются на операцию взятия адреса &?

На операцию взятия адреса & накладываются следующие ограничения:

1. Невозможно определить адрес константы. Например, выражение

pi = &0xffe800;

приведет к ошибке.

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

int *pi;
int d;
d = 8;
pi = &(d + 5); // ошибка, невозможно взять адрес выражения

приведет к ошибке.

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

Компилятор Visual C++ допускает возможность присвоения указателю непосредственного адреса памяти. Однако, при доступе к этому адресу возникает критическая ситуация с сообщением об ошибке:

"Attempted to read or write protected memory. This is often an indication that other memory is corrupt."

Это означает, что состоялась попытка доступа к защищенной памяти.

Это касается как консольных приложений, так и приложений, созданных по шаблону Windows Forms Application.

Пример. Присвоение физического адреса указателю.

// присвоение физического адреса указателю
float *pf; // указатель на float
float f;

// ...

// присвоение физического адреса (случайное значение)
pf = (float *)0xffff00; // работает
f = (float)(*pf); // ошибка! Возникает критическая ситуация


Связанные темы