Указатели, массивы и строки
Последнее обновление: 05.01.2023
В языке Си массивы и указатели тесно связаны. С помощью указателей мы также легко можем манипулировать элементами массива, как и с помощью индексов.
Имя массива без индексов в Си является адресом его первого элемента. Соответственно через операцию разыменования мы можем получить значение по этому адресу:
#include <stdio.h> int main(void) { int array[] = {1, 2, 3, 4, 5}; printf("array[0] = %d", *array); // array[0] = 1 return 0; }
Мы можем пробежаться по всем элементом массива, прибавляя к адресу определенное число:
#include <stdio.h> int main(void) { int array[5] = {1, 2, 3, 4, 5}; for(int i = 0; i < 5; i++) { void* address = array + i; // получаем адрес i-го элемента массива int value = *(array + i); // получаем значение i-го элемента массива printf("array[%d]: address=%p t value=%d n", i, address, value); } return 0; }
То есть, например, адрес второго элемента будет представлять выражение a+1
, а его значение — *(a+1)
.
Со сложением и вычитанием здесь действуют те же правила, что и в операциях с указателями. Добавление единицы означает прибавление к адресу
значения, которое равно размеру типа массива. Так, в данном случае массив представляет тип int, размер которого, как правило, составляет 4 байта,
поэтому прибавление единицы к адресу означает увеличение адреса на 4. Прибавляя к адресу 2, мы увеличиваем значение адреса на 4 * 2 =8. И так далее.
В итоге в моем случае я получу следующий результат работы программы:
array[0]: address=0060FE98 value=1 array[1]: address=0060FE9C value=2 array[2]: address=0060FEA0 value=3 array[3]: address=0060FEA4 value=4 array[4]: address=0060FEA8 value=5
В то же время имя массива это не стандартный указатель, мы не можем изменить его адрес, например, так:
int array[5] = {1, 2, 3, 4, 5}; array++; // так сделать нельзя int b = 8; array = &b; // так тоже сделать нельзя
Использование указателя для работы с массивом
Имя массива всегда хранит адрес самого первого элемента, соответственно его можно присвоить другому указателю и затем через указатель обращаться к элеиментам массива:
#include <stdio.h> int main(void) { int array[5] = {1, 2, 3, 4, 5}; int *ptr = array; // указатель ptr хранит адрес первого элемента массива array printf("value: %d n", *ptr); // 1 return 0; }
Прибавляя (или вычитая) определенное число от адреса указателя, можно переходить по элементам массива. Например, перейдем к третьему элементу:
#include <stdio.h> int main(void) { int array[5] = {1, 2, 3, 4, 5}; int *ptr = array; // указатель ptr хранит адрес первого элемента массива array ptr = ptr + 2; // перемезаем указатель на 2 элемента вперед printf("value: %d n", *ptr); // value: 3 return 0; }
Здесь указатель ptr
изначально указывает на первый элемент массива. Увеличив указатель на 2, мы пропустим 2 элемента в массиве и
перейдем к элементу array[2]
.
И как и другие данные, можно по указателю изменить значение элемента массива:
#include <stdio.h> int main(void) { int array[5] = {1, 2, 3, 4, 5}; int *ptr = array; // указатель ptr хранит адрес первого элемента массива array ptr = ptr + 2; // переходим к третьему элементу *ptr = 8; // меняем значение элемента, на который указывает указатель printf("array[2]: %d n", array[2]); // array[2] : 8 return 0; }
Стоит отметить, что указатель также может использовать индексы, как и массивы:
#include <stdio.h> int main(void) { int array[5] = {1, 2, 3, 4, 5}; int *ptr = array; // указатель ptr хранит адрес первого элемента массива array int value = ptr[2]; // используем индексы - получаем 3-й элемент (элемент с индексом 2) printf("value: %d n", value); // value: 3 return 0; }
Строки и указатели
Ранее мы рассмотрели, что строка по сути является набором символов, окончанием которого служит нулевой символ ». И фактически строку можно представить в виде массива:
char hello[] = "Hello METANIT.COM!";
Но в языке Си также для представления строк можно использовать указатели на тип char:
#include <stdio.h> int main(void) { char *hello = "Hello METANIT.COM!"; // указатель на char - фактически строка printf("%s", hello); return 0; }
Оба определения строки — с помощью массива и указателя будут в равнозначны здесь будут равнозначны.
Перебор массива с помощью указателей
С помощью указателей легко перебрать массив:
int array[5] = {1, 2, 3, 4, 5}; for(int *ptr=array; ptr<=&array[4]; ptr++) { printf("address=%p t value=%d n", (void*)ptr, *ptr); }
Так как указатель хранит адрес, то мы можем продолжать цикл, пока адрес в указателе не станет равным адресу последнего элемента (ptr<=&array[4]
).
Аналогичным образом можно перебрать и многомерный массив:
#include <stdio.h> int main(void) { int array[3][4] = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}}; int n = sizeof(array)/sizeof(array[0]); // число строк int m = sizeof(array[0])/sizeof(array[0][0]); // число столбцов int *final = array[0] + n * m - 1; // указатель на самый последний элемент for(int *ptr=array[0], i = 1; ptr <= final; ptr++, i++) { printf("%d t", *ptr); // если остаток от целочисленного деления равен 0, // переходим на новую строку if(i%m==0) { printf("n"); } } return 0; }
Так как в данном случае мы имеем дело с двухмерным массивом, то адресом первого элемента будет выражение array[0]
. Соответственно указатель указывает на
этот элемент. С каждой итерацией указатель увеличивается на единицу, пока его значение не станет равным адресу последнего элемента, который хранится в
указателе final.
Мы также могли бы обойтись и без указателя на последний элемент, проверяя значение счетчика, пока оно не станет равно общему количеству элементов (m * n):
for(int *ptr = array[0], i = 0; i < m*n;) { printf("%d t", *ptr++); if(++i%m==0) { printf("n"); } }
Но в любом случае программа вывела бы следующий результат:
1 2 3 4 5 6 7 8 9 10 11 12
C++ | ||
|
Аргумент задан неправильно, если хочешь передать массив, то в аргументе напиши
C++ | ||
|
. Когда будешь вызывать эту функцию, то просто передай ей имя массива(типа:
C++ | ||
|
)
Теперь у тебя testar будет равен адресу, в котором лежит a[0], почитай про арифметику указателей.
Добавлено через 22 секунды
C++ | ||
|
Аргумент задан неправильно, если хочешь передать массив, то в аргументе напиши
C++ | ||
|
. Когда будешь вызывать эту функцию, то просто передай ей имя массива(типа:
C++ | ||
|
)
Теперь у тебя testar будет равен адресу, в котором лежит a[0], почитай про арифметику указателей.
Prerequisite: Pointers Introduction
Pointer to Array
Consider the following program:
C++
#include <iostream>
using
namespace
std;
int
main()
{
int
arr[5] = { 1, 2, 3, 4, 5 };
int
*ptr = arr;
cout <<
"n"
<< ptr;
return
0;
}
C
#include<stdio.h>
int
main()
{
int
arr[5] = { 1, 2, 3, 4, 5 };
int
*ptr = arr;
printf
(
"%pn"
, ptr);
return
0;
}
In this program, we have a pointer ptr that points to the 0th element of the array. Similarly, we can also declare a pointer that can point to whole array instead of only one element of the array. This pointer is useful when talking about multidimensional arrays.
Syntax:
data_type (*var_name)[size_of_array];
Example:
int (*ptr)[10];
Here ptr is pointer that can point to an array of 10 integers. Since subscript have higher precedence than indirection, it is necessary to enclose the indirection operator and pointer name inside parentheses. Here the type of ptr is ‘pointer to an array of 10 integers’.
Note : The pointer that points to the 0th element of array and the pointer that points to the whole array are totally different. The following program shows this:
C++
#include <iostream>
using
namespace
std;
int
main()
{
int
*p;
int
(*ptr)[5];
int
arr[5];
p = arr;
ptr = &arr;
cout <<
"p ="
<< p <<
", ptr = "
<< ptr<< endl;
p++;
ptr++;
cout <<
"p ="
<< p <<
", ptr = "
<< ptr<< endl;
return
0;
}
C
#include<stdio.h>
int
main()
{
int
*p;
int
(*ptr)[5];
int
arr[5];
p = arr;
ptr = &arr;
printf
(
"p = %p, ptr = %pn"
, p, ptr);
p++;
ptr++;
printf
(
"p = %p, ptr = %pn"
, p, ptr);
return
0;
}
Output:
p = 0x7fff4f32fd50, ptr = 0x7fff4f32fd50 p = 0x7fff4f32fd54, ptr = 0x7fff4f32fd64
p: is pointer to 0th element of the array arr, while ptr is a pointer that points to the whole array arr.
- The base type of p is int while base type of ptr is ‘an array of 5 integers’.
- We know that the pointer arithmetic is performed relative to the base size, so if we write ptr++, then the pointer ptr will be shifted forward by 20 bytes.
The following figure shows the pointer p and ptr. Darker arrow denotes pointer to an array.
On dereferencing a pointer expression we get a value pointed to by that pointer expression. Pointer to an array points to an array, so on dereferencing it, we should get the array, and the name of array denotes the base address. So whenever a pointer to an array is dereferenced, we get the base address of the array to which it points.
C++
#include <bits/stdc++.h>
using
namespace
std;
int
main()
{
int
arr[] = { 3, 5, 6, 7, 9 };
int
*p = arr;
int
(*ptr)[5] = &arr;
cout <<
"p = "
<< p <<
", ptr = "
<< ptr << endl;
cout <<
"*p = "
<< *p <<
", *ptr = "
<< *ptr << endl;
cout <<
"sizeof(p) = "
<<
sizeof
(p) <<
", sizeof(*p) = "
<<
sizeof
(*p) << endl;
cout <<
"sizeof(ptr) = "
<<
sizeof
(ptr) <<
", sizeof(*ptr) = "
<<
sizeof
(*ptr) << endl;
return
0;
}
C
#include<stdio.h>
int
main()
{
int
arr[] = { 3, 5, 6, 7, 9 };
int
*p = arr;
int
(*ptr)[5] = &arr;
printf
(
"p = %p, ptr = %pn"
, p, ptr);
printf
(
"*p = %d, *ptr = %pn"
, *p, *ptr);
printf
(
"sizeof(p) = %lu, sizeof(*p) = %lun"
,
sizeof
(p),
sizeof
(*p));
printf
(
"sizeof(ptr) = %lu, sizeof(*ptr) = %lun"
,
sizeof
(ptr),
sizeof
(*ptr));
return
0;
}
Output:
p = 0x7ffde1ee5010, ptr = 0x7ffde1ee5010 *p = 3, *ptr = 0x7ffde1ee5010 sizeof(p) = 8, sizeof(*p) = 4 sizeof(ptr) = 8, sizeof(*ptr) = 20
Pointer to Multidimensional Arrays:
- Pointers and two dimensional Arrays: In a two dimensional array, we can access each element by using two subscripts, where first subscript represents the row number and second subscript represents the column number. The elements of 2-D array can be accessed with the help of pointer notation also. Suppose arr is a 2-D array, we can access any element arr[i][j] of the array using the pointer expression *(*(arr + i) + j). Now we’ll see how this expression can be derived.
Let us take a two dimensional array arr[3][4]:
int arr[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
Since memory in a computer is organized linearly it is not possible to store the 2-D array in rows and columns. The concept of rows and columns is only theoretical, actually, a 2-D array is stored in row-major order i.e rows are placed next to each other. The following figure shows how the above 2-D array will be stored in memory.
Each row can be considered as a 1-D array, so a two-dimensional array can be considered as a collection of one-dimensional arrays that are placed one after another. In other words, we can say that 2-D dimensional arrays that are placed one after another. So here arr is an array of 3 elements where each element is a 1-D array of 4 integers.
We know that the name of an array is a constant pointer that points to 0th 1-D array and contains address 5000. Since arr is a ‘pointer to an array of 4 integers’, according to pointer arithmetic the expression arr + 1 will represent the address 5016 and expression arr + 2 will represent address 5032.
So we can say that arr points to the 0th 1-D array, arr + 1 points to the 1st 1-D array and arr + 2 points to the 2nd 1-D array.
In general we can write:
arr + i Points to ith element of arr -> Points to ith 1-D array
- Since arr + i points to ith element of arr, on dereferencing it will get ith element of arr which is of course a 1-D array. Thus the expression *(arr + i) gives us the base address of ith 1-D array.
- We know, the pointer expression *(arr + i) is equivalent to the subscript expression arr[i]. So *(arr + i) which is same as arr[i] gives us the base address of ith 1-D array.
- To access an individual element of our 2-D array, we should be able to access any jth element of ith 1-D array.
- Since the base type of *(arr + i) is int and it contains the address of 0th element of ith 1-D array, we can get the addresses of subsequent elements in the ith 1-D array by adding integer values to *(arr + i).
- For example *(arr + i) + 1 will represent the address of 1st element of 1stelement of ith 1-D array and *(arr+i)+2 will represent the address of 2nd element of ith 1-D array.
- Similarly *(arr + i) + j will represent the address of jth element of ith 1-D array. On dereferencing this expression we can get the jth element of the ith 1-D array.
- Pointers and Three Dimensional Arrays
In a three dimensional array we can access each element by using three subscripts. Let us take a 3-D array-
int arr[2][3][2] = { {{5, 10}, {6, 11}, {7, 12}}, {{20, 30}, {21, 31}, {22, 32}} };
We can consider a three dimensional array to be an array of 2-D array i.e each element of a 3-D array is considered to be a 2-D array. The 3-D array arr can be considered as an array consisting of two elements where each element is a 2-D array. The name of the array arr is a pointer to the 0th 2-D array.
Thus the pointer expression *(*(*(arr + i ) + j ) + k) is equivalent to the subscript expression arr[i][j][k].
We know the expression *(arr + i) is equivalent to arr[i] and the expression *(*(arr + i) + j) is equivalent arr[i][j]. So we can say that arr[i] represents the base address of ith 2-D array and arr[i][j] represents the base address of the jth 1-D array.
C++
#include <iostream>
using
namespace
std;
int
main()
{
int
arr[2][3][2] = {
{
{5, 10},
{6, 11},
{7, 12},
},
{
{20, 30},
{21, 31},
{22, 32},
}
};
int
i, j, k;
for
(i = 0; i < 2; i++)
{
for
(j = 0; j < 3; j++)
{
for
(k = 0; k < 2; k++)
cout << *(*(*(arr + i) + j) +k) <<
"t"
;
cout <<
"n"
;
}
}
return
0;
}
C
#include<stdio.h>
int
main()
{
int
arr[2][3][2] = {
{
{5, 10},
{6, 11},
{7, 12},
},
{
{20, 30},
{21, 31},
{22, 32},
}
};
int
i, j, k;
for
(i = 0; i < 2; i++)
{
for
(j = 0; j < 3; j++)
{
for
(k = 0; k < 2; k++)
printf
(
"%dt"
, *(*(*(arr + i) + j) +k));
printf
(
"n"
);
}
}
return
0;
}
Output:
5 10 6 11 7 12 20 30 21 31 22 32
The following figure shows how the 3-D array used in the above program is stored in memory.
Subscripting Pointer to an Array
Suppose arr is a 2-D array with 3 rows and 4 columns and ptr is a pointer to an array of 4 integers, and ptr contains the base address of array arr.
int arr[3][4] = {{10, 11, 12, 13}, {20, 21, 22, 23}, {30, 31, 32, 33}}; int (*ptr)[4]; ptr = arr;
Since ptr is a pointer to an array of 4 integers, ptr + i will point to ith row. On dereferencing ptr + i, we get base address of ith row. To access the address of jth element of ith row we can add j to the pointer expression *(ptr + i). So the pointer expression *(ptr + i) + j gives the address of jth element of ith row and the pointer expression *(*(ptr + i)+j) gives the value of the jth element of ith row.
We know that the pointer expression *(*(ptr + i) + j) is equivalent to subscript expression ptr[i][j]. So if we have a pointer variable containing the base address of 2-D array, then we can access the elements of array by double subscripting that pointer variable.
C++
#include <iostream>
using
namespace
std;
int
main()
{
int
arr[3][4] = {
{10, 11, 12, 13},
{20, 21, 22, 23},
{30, 31, 32, 33}
};
int
(*ptr)[4];
ptr = arr;
cout << ptr<<
" "
<< ptr + 1<<
" "
<< ptr + 2 << endl;
cout << *ptr<<
" "
<< *(ptr + 1)<<
" "
<< *(ptr + 2)<< endl;
cout << **ptr<<
" "
<< *(*(ptr + 1) + 2)<<
" "
<< *(*(ptr + 2) + 3)<< endl;
cout << ptr[0][0]<<
" "
<< ptr[1][2]<<
" "
<< ptr[2][3]<< endl;
return
0;
}
C
#include<stdio.h>
int
main()
{
int
arr[3][4] = {
{10, 11, 12, 13},
{20, 21, 22, 23},
{30, 31, 32, 33}
};
int
(*ptr)[4];
ptr = arr;
printf
(
"%p %p %pn"
, ptr, ptr + 1, ptr + 2);
printf
(
"%p %p %pn"
, *ptr, *(ptr + 1), *(ptr + 2));
printf
(
"%d %d %dn"
, **ptr, *(*(ptr + 1) + 2), *(*(ptr + 2) + 3));
printf
(
"%d %d %dn"
, ptr[0][0], ptr[1][2], ptr[2][3]);
return
0;
}
Output:
0x7ffead967560 0x7ffead967570 0x7ffead967580 0x7ffead967560 0x7ffead967570 0x7ffead967580 10 22 33 10 22 33
This article is contributed by Anuj Chauhan. If you like GeeksforGeeks and would like to contribute, you can also write an article using write.geeksforgeeks.org or mail your article to review-team@geeksforgeeks.org. See your article appearing on the GeeksforGeeks main page and help other Geeks.
Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above.,
Теги: Си указатели. Указатель на указатель. Тип указателя. Арифметика указателей. Сравнение указателей.
Указатели
Это, пожалуй, самая сложная и самая важная тема во всём курсе. Без понимания указателей дальнейшее изучении си будет бессмысленным.
Указатели – очень простая концепция, очень логичная, но требующая внимания к деталям.
Определение
Указатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип.
Синтаксис объявления указателей
<тип> *<имя>;
Например
float *a;
long long *b;
Два основных оператора для работы с указателями – это оператор & взятия адреса, и оператор * разыменования. Рассмотрим простой пример.
#include <conio.h> #include <stdio.h> void main() { int A = 100; int *p; //Получаем адрес переменной A p = &A; //Выводим адрес переменной A printf("%pn", p); //Выводим содержимое переменной A printf("%dn", *p); //Меняем содержимое переменной A *p = 200; printf("%dn", A); printf("%d", *p); getch(); }
Рассмотрим код внимательно, ещё раз
int A = 100;
Была объявлена переменная с именем A. Она располагается по какому-то адресу в памяти. По этому адресу хранится значение 100.
int *p;
Создали указатель типа int.
p = &A;
Теперь переменная p хранит адрес переменной A. Используя оператор * мы получаем доступ до содержимого переменной A.
Чтобы изменить содержимое, пишем
*p = 200;
После этого значение A также изменено, так как она указывает на ту же область памяти.
Ничего сложного.
Теперь другой важный пример
#include <conio.h> #include <stdio.h> void main() { int A = 100; int *a = &A; double B = 2.3; double *b = &B; printf("%dn", sizeof(A)); printf("%dn", sizeof(a)); printf("%dn", sizeof(B)); printf("%dn", sizeof(b)); getch(); }
Будет выведено
4
4
8
4
Несмотря на то, что переменные имеют разный тип и размер, указатели на них имеют один размер. Действительно, если указатели хранят адреса, то они
должны быть целочисленного типа. Так и есть, указатель сам по себе хранится в переменной типа size_t (а также ptrdiff_t),
это тип, который ведёт себя как целочисленный, однако его размер зависит от разрядности системы. В большинстве
случаев разницы между ними нет. Зачем тогда указателю нужен тип?
Арифметика указателей
Во-первых, указателю нужен тип для того, чтобы корректно работала операция разыменования (получения содержимого по адресу).
Если указатель хранит адрес переменной, необходимо знать, сколько байт нужно взять, начиная от этого адреса, чтобы получить всю переменную.
Во-вторых, указатели поддерживают арифметические операции. Для их выполнения необходимо знать размер.
операция + N
сдвигает указатель вперёд на N*sizeof(тип)
байт.
Например, если указатель
int *p;
хранит адрес CC02, то после
p += 10;
он будет хранить адрес
СС02 + sizeof(int)*10 = CC02 + 28 = CC2A (Все операции выполняются в шестнадцатиричном формате).
Пусть мы создали указатель на начало массива. После этого мы можем «двигаться» по этому массиву, получая доступ до отдельных элементов.
#include <conio.h> #include <stdio.h> void main() { int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p; p = A; printf("%dn", *p); p++; printf("%dn", *p); p = p + 4; printf("%dn", *p); getch(); }
Заметьте, каким образом мы получили адрес первого элемента массива
p = A;
Массив, по сути, сам является указателем, поэтому не нужно использовать оператор &. Мы можем переписать пример по-другому
p = &A[0];
Получить адрес первого элемента и относительно него двигаться по массиву.
Кроме операторов + и — указатели поддерживают операции сравнения.
Если у нас есть два указателя a и b, то a > b, если адрес, который хранит a, больше адреса, который хранит b.
#include <conio.h> #include <stdio.h> void main() { int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *a, *b; a = &A[0]; b = &A[9]; printf("&A[0] == %pn", a); printf("&A[9] == %pn", b); if (a < b) { printf("a < b"); } else { printf("b < a"); } getch(); }
Если же указатели равны, то они указывают на одну и ту же область памяти.
Указатель на указатель
Указатель хранит адрес области памяти. Можно создать указатель на указатель, тогда он будет хранить адрес указателя и сможет обращаться к его содержимому.
Указатель на указатель определяется как
<тип> **<имя>;
Очевидно, ничто не мешает создать и указатель на указатель на указатель, и указатель на указатель на указатель на указатель и так далее. Это нам понадобится при работе с двумерными и многомерными массивами. А вот простой пример, как можно работать с указателем на указатель.
#include <conio.h> #include <stdio.h> #define SIZE 10 void main() { int A; int B; int *p; int **pp; A = 10; B = 111; p = &A; pp = &p; printf("A = %dn", A); *p = 20; printf("A = %dn", A); *(*pp) = 30; //здесь скобки можно не писать printf("A = %dn", A); *pp = &B; printf("B = %dn", *p); **pp = 333; printf("B = %d", B); getch(); }
Указатели и приведение типов
Так как указатель хранит адрес, можно кастовать его до другого типа. Это может понадобиться, например, если мы хотим взять
часть переменной, или если мы знаем, что переменная хранит нужный нам тип.
#include <conio.h> #include <stdio.h> #define SIZE 10 void main() { int A = 10; int *intPtr; char *charPtr; intPtr = &A; printf("%dn", *intPtr); printf("--------------------n"); charPtr = (char*)intPtr; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); getch(); }
В этом примере мы пользуемся тем, что размер типа int равен 4 байта, а char 1 байт. За счёт этого, получив адрес первого байта, можно пройти по остальным байтам числа и вывести их содержимое.
NULL pointer — нулевой указатель
Указатель до инициализации хранит мусор, как и любая другая переменная. Но в то же время, этот «мусор» вполне может оказаться валидным адресом. Пусть, к примеру, у нас есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак.
Для решения этой проблемы был введён макрос NULL библиотеки stdlib.
Принято при определении указателя, если он не инициализируется конкретным значением, делать его равным NULL.
int *ptr = NULL;
По стандарту гарантировано, что в этом случае указатель равен NULL, и равен нулю, и может быть использован как булево значение false.
Хотя в зависимости от реализации NULL может и не быть равным 0 (в смысле, не равен нулю в побитовом представлении, как например, int или float).
Это значит, что в данном случае
int *ptr = NULL; if (ptr == 0) { ... }
вполне корректная операция, а в случае
int a = 0; if (a == NULL) { ... }
поведение не определено. То есть указатель можно сравнивать с нулём, или с NULL, но нельзя NULL сравнивать с переменной целого типа или типа с плавающей точкой.
#include <stdlib.h> #include <stdio.h> #include <conio.h> void main() { int *a = NULL; unsigned length, i; printf("Enter length of array: "); scanf("%d", &length); if (length > 0) { //При выделении памяти возвращается указатель. //Если память не была выделена, то возвращается NULL if ((a = (int*) malloc(length * sizeof(int))) != NULL) { for (i = 0; i < length; i++) { a[i] = i * i; } } else { printf("Error: can't allocate memory"); } } //Если переменая была инициализирована, то очищаем её if (a != NULL) { free(a); } getch(); }
Примеры
Теперь несколько примеров работы с указателями
1. Пройдём по массиву и найдём все чётные элементы.
#include <conio.h> #include <stdio.h> void main() { int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int even[10]; int evenCounter = 0; int *iter, *end; //iter хранит адрес первого элемента массива //end хранит адрес следующего за последним "элемента" массива for (iter = A, end = &A[10]; iter < end; iter++) { if (*iter % 2 == 0) { even[evenCounter++] = *iter; } } //Выводим задом наперёд чётные числа for (--evenCounter; evenCounter >= 0; evenCounter--) { printf("%d ", even[evenCounter]); } getch(); }
2. Когда мы сортируем элементы часто приходится их перемещать. Если объект занимает много места, то операция обмена местами двух элементов будет дорогостоящей. Вместо этого можно создать массив указателей на исходные элементы и отсортировать его. Так как размер указателей меньше, чем размер элементов целевого массива, то и сортировка будет происходить быстрее. Кроме того, массив не будет изменён, часто это важно.
#include <conio.h> #include <stdio.h> #define SIZE 10 void main() { double unsorted[SIZE] = {1.0, 3.0, 2.0, 4.0, 5.0, 6.0, 8.0, 7.0, 9.0, 0.0}; double *p[SIZE]; double *tmp; char flag = 1; unsigned i; printf("unsorted arrayn"); for (i = 0; i < SIZE; i++) { printf("%.2f ", unsorted[i]); } printf("n"); //Сохраняем в массив p адреса элементов for (i = 0; i < SIZE; i++) { p[i] = &unsorted[i]; } do { flag = 0; for (i = 1; i<SIZE; i++) { //Сравниваем СОДЕРЖИМОЕ if (*p[i] < *p[i-1]) { //обмениваем местами АДРЕСА tmp = p[i]; p[i] = p[i-1]; p[i-1] = tmp; flag = 1; } } } while(flag); printf("sorted array of pointersn"); for (i = 0; i < SIZE; i++) { printf("%.2f ", *p[i]); } printf("n"); printf("make sure that unsorted array wasn't modifiedn"); for (i = 0; i < SIZE; i++) { printf("%.2f ", unsorted[i]); } getch(); }
3. Более интересный пример. Так как размер типа char всегда равен 1 байт, то с его помощью можно реализовать операцию swap – обмена местами содержимого двух переменных.
#include <conio.h> #include <conio.h> #include <stdio.h> void main() { int length; char *p1, *p2; char tmp; float a = 5.0f; float b = 3.0f; printf("a = %.3fn", a); printf("b = %.3fn", b); p1 = (char*) &a; p2 = (char*) &b; //Узнаём сколько байт перемещать length = sizeof(float); while (length--) { //Обмениваем местами содержимое переменных побайтно tmp = *p1; *p1 = *p2; *p2 = tmp; //не забываем перемещаться вперёд p1++; p2++; } printf("a = %.3fn", a); printf("b = %.3fn", b); getch(); }
В этом примере можно поменять тип переменных a и b на double или любой другой (с соответствующим изменением вывода и вызова sizeof), всё равно мы будет обменивать местами байты двух переменных.
4. Найдём длину строки, введённой пользователем, используя указатель
#include <conio.h> #include <stdio.h> void main() { char buffer[128]; char *p; unsigned length = 0; scanf("%127s", buffer); p = buffer; while (*p != '') { p++; length++; } printf("length = %d", length); getch(); }
Обратите внимание на участок кода
while (*p != '') { p++; length++; }
его можно переписать
while (*p != 0) { p++; length++; }
или
while (*p) { p++; length++; }
или, убрав инкремент в условие
while (*p++) { length++; }
Q&A
Всё ещё не понятно? – пиши вопросы на ящик
Строки
Итак, небольшая статья на эту страшную и малопонятную новичкам С++ тему.
Допустим, что вы уже знаете, что такое переменная в языке Си/Си++.
Просто напомним, в компьютере есть память — громадный ряд маленьких пронумерованных ячеек(по 1Байту). От 1 до … 8млрд, к примеру(у меня оперативная память 8Гб)
1 | 2 | 3 | 4 | … | 3221225472 | … | 8589934592 |
Так уж сложилось, что такие громадные величины лучше удобнее кодировать в 16ричной системе исчисления. 0х1 до 0х200000000(в моем случае, 8ГБ памяти же)
1 | 2 | 3 | 4 | … | 3221225472 | … | 8589934592 |
0х1 | 0х2 | 0х3 | 0х4 | … | 0хC0000000 | … | 0х200000000 |
Вот с номерами ячеек программисту ниразу не удобнее работать, поэтому придумали символьные имена для этих ячеек
1 | 2 | 3 | 4 | … | 3221225472 | … | 8589934592 |
0х1 | 0х2 | 0х3 | 0х4 | … | 0хC0000000 | … | 0х200000000 |
0x00 | char a; | 0хFF | char b; | … | 0xFF | … | 0x31 |
Ячейки с номерами 0х2, 0х4 имеют символьные имена a и b. Это называются переменные. При чем размер этих переменных всего 1 Байт — из-за типа данных char. А могут занимать и 4 байта (к примеру тип данных int (integer))
Давайте для удобства превратим ряд в таблицу, стеллаж.
Переменная — имя для ячейки памяти.
Удобное программисту.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | d | e | f | |
0x0000 | a | |||||||||||||||
0x0010 | b | |||||||||||||||
0x0020 | ||||||||||||||||
0x0030 | c | |||||||||||||||
0x0040 | ||||||||||||||||
0x0050 | ||||||||||||||||
0x0060 |
char a; // переменная находится по адресу 0х0004, не проинициализирована
char b; // переменная находится по адресу 0х0018, не проинициализирована
int c=344; // переменная находится по адресу 0х0030, проинициализирована значением 344
Объявляя переменную, мы по сути выделяем ей память определенного размера(1,4,8 байт). Размер зависит от типа данных.
После объявления переменной, в этой занятой ячейке памяти ничего не находится, точнее, там мусор от прошлых использований.
При инициализации переменной, в эту занятую ячейку памяти заносится заданное программой значение.
Процессору без разницы как вы назовете переменную. Он все равно оперирует адресами.
Указатель — тоже переменная.
В ней хранится АДРЕС ячейки.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | d | e | f | |
0x0000 | a | |||||||||||||||
0x0010 | b | |||||||||||||||
0x0020 | ||||||||||||||||
0x0030 | c=344 | |||||||||||||||
0x0040 | *d | |||||||||||||||
0x0050 | *e=&c | |||||||||||||||
0x0060 |
char *d; // переменная находится по адресу 0х004c, не проинициализирована(значение указателя неизвестно)
int *e=&c; // переменная находится по адресу 0х0052, проинициализирована адресом переменной с(значение указателя содержит адрес переменной с — 0х0030)
printf(«%d»,e);//это выведет адрес переменной с 0х0030, но в 10тичном формате = 48 (из-за %d)
printf(«%d»,*e);//а это выведет значение по адресу 0х0030 -> 344
Объявляя переменную-указатель, мы по сути выделяем ей память всегда одинакового размера, необходимого для адресации любой ячейки памяти (4 байта для 32битных систем, 8 байт для 64 битных). Размер зависит от разрядности системы и задается при компиляции программы.
Чтобы узнать адрес любой переменной, используется символ & перед именем.
Чтобы узнать что лежит по адресу в указателе, нужно «разыменовать» его, используя символ * (звездочка).
Если еще непонятно, предлагаю перейти к примерам.
*имя — вначале создаем указатель (объявление указателя)
&имя1 — узнать адрес абсолютно любой переменной (взятие адреса)
*имя — узнать что находится по адресу в указателе (разыменование)
Вот небольшой пример указателей на языке Си++. Желательно самому все проверить(DevCpp)
#include <stdio.h> struct proba { int a; int b; char c; }; int main() { proba a1; proba *a2= new proba; a2=&a1; (a1).a=100; (a1).b=200; int a=-255; int c[5]={56,12,87,-12,255}; int *d=&a; int *e=c; printf("d= %dn",*d);//-255 printf("e[0]= %dn",*e);//56 printf("e[1]= %dn",*(e+1));//12 printf("e[1]= %dn",e[1]);//12 printf("a2.a= %dn",a2->a);//100 printf("a2.a= %dn",(*a2).a);//100 printf("size char* %dn",sizeof(char *));//8 printf("size int* %dn",sizeof(int *));//8 printf("size void* %dn",sizeof(void *));//8 printf("size a1 %dn",sizeof(a1));//12 printf("size a2 %dn",sizeof(a2));//8 return 0; }
Кое-что интересно есть в примере.
*(e+1) и e[1] дают идентичный результат.
*(e+1) — к указателю е прибавляется 1 int смещение (4байта). и затем этот смещенный указатель разыменовывается. Получилось сдвинулись на 1 ячейку в массиве.
e[1] — тоже задаем смещение указателя на 1 (на 4 байта). По сути вся работа с массивами осуществляется через указатели. Будь то обычные численные массивы или строки. Все втихаря ведется указателями.
Есть еще кое-что интересно в примере на C/C++.
a2->a при работе напрямую с указателем на структуру используется стрелка «->»
(*a2).a — можно разыменовать указатель на структуру и обратиться к самой структуре по адресу. Тогда доступ к членам структуры осуществляется через точку «.»
Указатели на массив.
И на структуру.
Особенности использования в C++.
В массиве можно обратиться к элементам через указатель 2 способами:
- *(e+i)
- e[i]
В структуре можно обратиться к членам через указатель 2 способами:
- (*a2).a
- a2->a
Все это справедливо для языка программирования Си++