Указатели в C++

Указатель (пойнтер, англ. pointer) - это переменная, содержащая адрес другой переменной. Тип данных pointer равен 4 байта. Указатели очень широко используются в языке C. Это происходит отчасти потому, что иногда они дают единственную возможность выразить нужное действие, а отчасти потому, что они обычно ведут к более компактным и эффективным программам, чем те, которые могут быть получены другими способами.

Следует четко понимать, что компилятору абсолютно безразлично, как написано объявление int *p или int* p. Программист может выбрать свой стиль. Однако символы & и * лучше связывать с переменными, а не типом. Так как в соответствии с правилами языка C++ символ * (как и символ &) связывается с отдельной переменной, а не ее типом.
Адрес переменной - это адрес первого байта переменной.
Указатель должен быть равен NULL или указывать на адрес переменной.

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

  • Объявление указателя.
    int * pi  = NULL;
  • Знак & используется для получения адреса переменной.
            pi = &i;
    	cout<<&i<<endl;//записи эквивалентны
    	cout<<pi<<endl;//записи эквивалентны
  • Разыменование - изменение значения переменной на который указывает указатель.
    *pi=48;
#include <iostream>
using namespace std;

 /* возвращает длину строки  s */
  int strlen(char *s)
  {
      int n; 
      for (n = 0; *s != '\0'; s++)
              n++;
      return(n);
  }

int main()
{
	int i;
	int *pi = NULL;
	pi = &i;
	cout<<&i<<endl;
	cout<<pi<<endl;
	*pi=48; //разыменование - изменение значения переменной на который указывает указатель.
	cout<<i<<endl;
	cout<<strlen("ER345")<<endl;
}

Указатели и массивы

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

int a[10];

определяет массив размера 10, т.е. набор из 10 последовательных объектов, называемых a[0], a[1], …, a[9]. Запись a[i] соответствует элементу массива через i позиций от начала. Если pa - указатель целого, описанный как

int *pa;

то присваивание

pa = &a[0];

приводит к тому, что pa указывает на нулевой элемент массива a. Это означает, что pa содержит адрес элемента a[0]. Теперь присваивание

x = *pa

будет копировать содержимое a[0] в x.

Если ра указывает на некоторый определенный элемент массива a, то по определению pa+1 указывает на следующий элемент, и вообще pa-i указывает на элемент, стоящий на i позиций до элемента, указываемого pa, а pa+i на элемент, стоящий на i позиций после. Таким образом, если pa указывает на a[0], то *(pa+1)

ссылается на содержимое a[1], pa+i - адрес a[i], а *(pa+i) - содержимое a[i].

Эти замечания справедливы независимо от типа переменных в массиве a. Суть определения "добавления 1 к указателю", а также его распространения на всю арифметику указателей, состоит в том, что приращение масштабируется размером памяти, занимаемой объектом, на который указывает указатель. Таким образом, i в pa+i перед прибавлением умножается на размер объектов, на которые указывает pa.

Очевидно существует очень тесное соответствие между индексацией и арифметикой указателей. В действительности компилятор преобразует ссылку на массив в указатель на начало массива. В результате этого имя массива является указательным выражением. Отсюда вытекает несколько весьма полезных следствий. Так как имя массива является синонимом местоположения его нулевого элемента, то присваивание pa = &a[0]

можно записать как pa = a.

Еще более удивительным, по крайней мере на первый взгляд, кажется тот факт, что ссылку на a[i] можно записать в виде *(a+i). При анализировании выражения a[i] в языке C оно немедленно преобразуется к виду *(a+i); эти две формы совершенно эквивалентны. Если применить операцию & к обеим частям такого соотношения эквивалентности, то мы получим, что &a[i] и a+i тоже идентичны: a+i - адрес i-го элемента от начала a. С другой стороны, если pa является указателем, то в выражениях его можно использовать с индексом: pa[i] идентично *(pa+i). Короче, любое выражение, включающее массивы и индексы, может быть записано через указатели и смещения и наоборот, причем даже в одном и том же утверждении.

Имеется одно различие между именем массива и указателем, которое необходимо иметь в виду. Указатель является переменной, так что операции pa=a и pa++ имеют смысл. Но имя массива является константой, а не переменной: конструкции типа a=pa или a++,или p=&a будут незаконными.

Когда имя массива передается функции, то на самом деле ей передается местоположение начала этого массива. Внутри вызванной функции такой аргумент является точно такой же переменной, как и любая другая, так что имя массива в качестве аргумента действительно является указателем, т.е. переменной, содержащей адрес. Мы можем использовать это обстоятельство для написания нового варианта функции strlen, вычисляющей длину строки: /* возвращает длину строки s */

  int strlen(char *s)
  {
      int n; 
      for (n = 0; *s != '\0'; s++)
              n++;
      return(n);
  }

Операция увеличения s совершенно законна, поскольку эта переменная является указателем, s++ никак не влияет на символьную строку в обратившейся к strlen функции, а только увеличивает локальную для функции strlen копию адреса.

Описания формальных параметров в определении функции в виде char s[];

и char *s;

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

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

        массив           указатель
           ____________       _____
    array: | array[0] |   ptr:| * |
           | array[1] |         |
           | array[2] |<--------- сейчас равен &array[2]
           |  ...     |

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

  ptr = array;  или  ptr = &array[0];
          но не
  ptr = &array;

Операция & перед одиноким именем массива не нужна и недопустима! Такое родство указателей и массивов позволяет нам применять операцию * к имени массива: value = *array; означает то же самое, что и value = array[0];

Указатели - не целые числа! Хотя физически это и номера байтов, адресная арифметика отличается от обычной.

Как описывать ссылки (указатели) на двумерные массивы?

Как описывать ссылки (указатели) на двумерные массивы? Рассмотрим такую программу:

    #include <stdio.h>
    #define First  3
    #define Second 5
    char arr[First][Second] = {
            "ABC.",
            { 'D', 'E', 'F', '?', '\0' },
            { 'G', 'H', 'Z', '!', '\0' }
    };
    char (*ptr)[Second];
    main(){
            int i;
            ptr = arr;      /* arr и ptr теперь взаимозаменимы */
            for(i=0; i < First; i++)
                    printf("%s\t%s\t%c\n", arr[i], ptr[i], ptr[i][2]);
    }

Указателем здесь является ptr. Отметим, что у него задана размерность по второму измерению: Second, именно для того, чтобы компилятор мог правильно вычислить двумерные индексы.

Попробуйте сами объявить

    char (*ptr)[4];
    char (*ptr)[6];
    char **ptr;

и увидеть, к каким невеселым эффектам это приведет (компилятор, кстати, будет ругаться; но есть вероятность, что он все же странслирует это для вас. Но работать оно будет плачевно). Попробуйте также использовать ptr[x][y].

Обратите также внимание на инициализацию строк в нашем примере. Строка "ABC." равносильна объявлению

          { 'A', 'B', 'C', '.', '\0' },

Например, используется в функции: Быстрая сортировка (англ. quicksort), часто называемая qsort. В функции qsort указатель на функцию применяется для указания способа сортировки.

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

А теперь путём последовательности утверждений придем к обсуждению темы данного раздела урока.

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

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

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

4. Указатель на функцию определяется следующим образом:

тип_функции   (*имя_указателя)(спецификация_параметров);

Например: int (*func1Ptr) (char); - определение указателя func1Ptr на функцию с параметром типа char, возвращающую значение типа int.

Примечание: Будьте внимательны!!! Если приведенную синтаксическую конструкцию записать без первых круглых скобок, т.е. в виде int *fun (char); то компилятор воспримет ее как прототип некой функции с именем fun и параметром типа char, возвращающей значение указателя типа int *. Второй пример: char * (*func2Ptr) (char * ,int); - определение указателя func2Ptr на функцию с параметрами типа указатель на char и типа int, возвращающую значение типа указатель на char.

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

#include <iostream>
using namespace std;
void f1(void)        // Определение f1.
{ 
	cout << "Load f1()\n";
 }
void f2(void)        // Определение f2.
{
	 cout << "Load f1()\n";
}
void main()
{ 
	void (*ptr)(void);  // ptr - указатель на функцию.
	ptr = f2;		// Присваивается адрес f2().
	(*ptr)();		// Вызов f2() по ее адресу.
	ptr = f1;		// Присваивается адрес f1().
	(*ptr)();		// Вызов f1() по ее адресу.
	ptr();			// Вызов эквивалентен (*ptr)();
}

Результат выполнения программы:

Load f2()
Load f1()
Load f1()
Press any key to continue

Здесь значением имени_указателя служит адрес функции, а с помощью операции разыменования * обеспечивается обращение по адресу к этой функции. Однако будет ошибкой записать вызов функции без скобок в виде *ptr();. Дело в том, что операция () имеет более высокий приоритет, нежели операция обращения по адресу *. Следовательно, в соответствии с синтаксисом будет вначале сделана попытка обратиться к функции ptr(). И уже к результату будет отнесена операция разыменования, что будет воспринято как синтаксическая ошибка.

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

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

char f1(char) {...}       // Определение функции.
char f2(int) {...}        // Определение функции.
void f3(float) {...}      // Определение функции.
int* f4(char *){...}     // Определение функции.
char (*pt1)(int);        // Указатель на функцию.
char (*pt2)(int);         // Указатель на функцию.
void (*ptr3)(float) = f3; // Инициализированный указатель.
void main()
{ 
    pt1 = f1;  // Ошибка - несоответствие сигнатур.
    pt2 = f3;  // Ошибка - несоответствие типов (значений и сигнатур).
    pt1 = f4;  // Ошибка - несоответствие типов.
    pt1 = f2;  // Правильно.
    pt2 = pt1; // Правильно.
    char с = (*pt1)(44); // Правильно.
    с = (*pt2)('\t');    // Ошибка - несоответствие сигнатур.
}

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

#include <iostream>
using namespace std;
// Функции одного типа с одинаковыми сигнатурами:
int add(int n, int m) { return n + m; }
int division(int n, int m) { return n/m; }
int mult(int n, int m) { return n * m; }
int subt(int n, int m) { return n - m; }
void main()
{ 
	int (*par)(int, int); // Указатель на функцию.
	int a =  6, b = 2; 
	char c = '+';
	while   (c != ' ')
	{
		cout << "\n Arguments: a = " << a <<", b = " << b;
		cout << ". Result for c = \'" << c << "\':";

		switch (c)
		{
		case '+': 
			par = add;
			c = '/';
			break;
		case '-': 
			par = subt; 
			c = ' '; 
			break;
		case '*': 
			par = mult; 
			c = '-'; 
			break;
		case '/': 
			par = division;
			c = '*'; 
			break;
		}
		cout << (a = (*par)(a,b))<<"\n"; //Вызов  по  адресу.
	}
}

Результаты выполнения программы:

	Arguments: a = 6, b = 2. Result for c = '+':8

	Arguments: a = 8, b = 2. Result for c = '/':4

 	Arguments: a = 4, b = 2. Result for c = '*':8

 	Arguments: a = 8, b = 2. Result for c = '-':6
	Press any key to continue

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

Массивы указателей на функции. Указатели на функции могут быть объединены в массивы. Например, float (*ptrArray[4]) (char) ; - описание массива с именем ptrArray из четырех указателей на функции, каждая из которых имеет параметр типа char и возвращает значение типа float. Чтобы обратиться, например, к третьей из этих функций, потребуется такой оператор:

float а = (*ptrArray[2])('f');

Как обычно, индексация массива начинается с 0, и поэтому третий элемент массива имеет индекс 2.

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

#include <iostream> 
using namespace std;

/* Определение функций для обработки меню
(функции фиктивны т. е. реальных действий не выполняют):*/

void act1 (char* name)
{
	cout <<"Create file..." << name; 
} 
void act2 (char* name)
{ 
	cout << "Delete file... " << name; 
}
void act3 (char* name)
{ 
	cout << "Read file... " << name; 
} 
void act4 (char* name)
{ 
	cout << "Mode file... " << name; 
} 
void act5 (char* name) 
{ 
	cout << "Close file... " << name;	
}
 
void main()
{ 
	// Создание и инициализация массива указателей 
	void (*MenuAct[5])(char*)  = {act1, act2, act3, act4, act5};

	int number;  // Номер выбранного пункта меню.
	char FileName[30];  // Строка для имени файла.

	// Реализация меню
	cout << "\n 1 - Create";
	cout << "\n 2 - Delete";
	cout << "\n 3 - Read";
	cout << "\n 4 - Mode";
	cout << "\n 5 - Close";

	while (1)  // Бесконечный цикл.
	{ 
		while (1)
		{ // Цикл продолжается до ввода правильного номера. 
			cout << "\n\nSelect action: "; 
			cin >> number;
			if (number>>= 1 && number <= 5) break; 
	
			cout << "\nError number!"; 
		} 
		if (number != 5)
		{ 
			cout << "Enter file name: ";
			cin >> FileName; // Читать имя файла. 
		}
		else break;
		// Вызов функции по указателю на нее:
		(*MenuAct[number-1])(FileName);
	} // Конец бесконечного цикла.
}

Пункты меню повторяются, пока не будет введен номер 5 - закрытие.

📌 Для тестирования скриптов, установщиков VPN, Python ботов рекомендуем использовать надежные VPS на короткий срок. Если вам нужна помощь с более сложными задачами, вы можете найти фрилансера, который поможет с настройкой. Узнайте больше о быстрой аренде VPS для экспериментов и о фриланс-бирже для настройки VPS, WordPress. 📌

💥 Подпишись в Телеграм 💥 и задай вопрос по сайтам и хостингам бесплатно!