Указатели и константы
В операциях с указателями участвуют два объекта: сам указатель и объект, на который он ссылается. Помещение ключевого слова const перед объявлением указателя делает константным объект, а не указатель. Для объявления самого указателя в качестве константы, используется оператор объявления * const, а не просто *.
Примеры:
const int *p1; // указатель на константу типа int
int const *p2; // указатель на константу типа int
int *const p3; // константный указатель на объект типа int
const int *const p4; // константный указатель на константу типа int
int const *const p4; // константный указатель на константу типа int
Первый и второй варианты записи являются синонимами и обозначают, что константой является объект, на который указывает указатель. То есть нельзя изменять значение, хранящееся в указываемом объекте.
Третий вариант указывает, что константен указатель, то есть его нельзя установить на другой объект – в него нельзя занести другой адрес в памяти.
Четвёртый и пятый вариант являются синонимами и указывают, что константен как указатель, так и указываемый объект.
Указатели на строки
Поскольку текстовая строка имеет тип const char [] и является массивом, к ней применимы все ранее приведённые соображения о массивах и указателях.
Строковый литерал можно присвоить переменной типа char *. Это сделано для совместимости с ранними версиями языка C, в которых не было ключевого слова const.
Однако изменение строкового литерала через такой указатель является ошибкой:
void f()
{
char *p=”text”;
p[3] = ‘a’; // ОШИБКА
}
Память под строковые литералы выделяется статически, поэтому их свободно можно возвращать в качестве значения функции:
const char *access(int i)
{
…
return “access denied”;
}
Будут ли одинаковые литералы записываться в одно место памяти или нет – зависит от реализации.
Строки оканчиваются нуль-байтом (‘\0’), что делает их удобными для использования указателей. Рассмотрим копирование строк:
const char src[]=”Строка, которую надо скопировать”;
char dst[sizeof src];
const char *p_src = src;
char *p_dst = dst;
while (*p_dst++ = *p_src++) ;
Создаются два указателя – константный указатель p_src, который хранит адрес копируемого элемента и указатель p_dst, хранящий адрес, куда будет скопирован элемент. Копирование будет продолжаться до тех пор, пока не будет скопирован нуль-байт, завершающий строку.
Примечание.
Цикл
while (*p_dst++ = *p_src++) ;
может быть записан в виде:
while (*p_src) // while (*p_src!=”\0”)
{
*p_dst = *p_src;
p_dst++ ;
p_src++;
}
*p_dst =”\0”;
Указатель на void
В Cи существует особый тип указателей – указатели типа void или пустые указатели. Эти указатели используются в том случае, когда тип переменной не известен. Так как void не имеет типа, то к нему не применима операция разадресации (взятие содержимого) и адресная арифметика, так как неизвестно представление данных. Тем не менее, если мы работаем с указателем типа void, то нам доступны операции сравнения. Указатель на объект любого типа можно присвоить переменной типа void*. void* можно присваивать, сравнивать и явно преобразовать в указатель любого другого типа.
Если необходимо работать с пустым указателем, то сначала нужно явно привести тип. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <conio.h> #include <stdio.h>
int main() { void *p = NULL; int intVar = 10; char charVar = 'A'; float floatVar = 24.3; float *floatPtr = NULL; p = &intVar; *((int*) p) = 20; printf("intVar = %d\n", intVar);
p = &charVar; printf("charVar = %c\n", *((char*) p));
p = &floatVar; floatPtr = (float*) p; printf("floatVar = %.3f", *floatPtr);
getch(); } |
Переменная не может иметь типа void, этот тип определён только для указателей. Пустые указатели нашли широкое применение при вызове функций. Можно написать функцию общего назначения, которая будет работать с любым типом. Например, напишем функцию swap, которая обменивает местами содержимое двух переменных. У этой функции будет три аргумента – указатели на переменные, которые необходимо обменять местами и их размер.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#include <conio.h> #include <stdio.h> #include <stdlib.h> #include <string.h>
void swap(void *a, void *b, size_t size) { char* tmp; //создаём временную переменную для обмена tmp = (char*) malloc(size); memcpy(tmp, a, size); memcpy(a, b, size); memcpy(b, tmp, size); free(tmp); }
int main() { float a = 10.f; float b = 20.f; double c = 555; double d = 777; unsigned long e = 2ll; unsigned long f = 3ll;
printf("a = %.3f, b = %.3f\n", a, b); swap(&a, &b, sizeof(float)); printf("a = %.3f, b = %.3f\n", a, b);
printf("c = %.3f, d = %.3f\n", c, d); swap(&c, &d, sizeof(double)); printf("c = %.3f, d = %.3f\n", c, d);
printf("e = %ld, f = %ld \n", e, f); swap(&e, &f, sizeof(unsigned long)); printf("e = %ld, f = %ld \n", e, f);
getch(); } |
Наша функция может выглядеть и по-другому. Обойдёмся без дорогостоящего выделения памяти и будем копировать побайтно.
?
6 7 8 9 10 11 12 13 14 |
void swap(void *a, void *b, size_t size) { char tmp; size_t i; for (i = 0; i < size; i++) { tmp = *((char*) b + i); *((char*) b + i) = *((char*) a + i); *((char*) a + i) = tmp; } } |
Пустые указатели позволяют создавать функции, которые возвращают и принимают одинаковые параметры, но имеют разное название. Это пригодится в дальнейшем, при изучении указателей на функции. Например
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
int cmpInt(void* a, void* b) { return *((int*) a) - *((int*) b); }
int cmpString(void *a, void* b) { return strcmp((char*) a, (char*) b); }
int cmpFloat(void* a, void* b) { float fdiff = *((float*) a) - *((float*) b); if (fabs(fdiff) < 0.000001f) { return 0; } if (fdiff < 0) { return -1; } else { return 1; } } |
Массивы, как параметры функции
При передаче массива как аргумента функции происходит неявное преобразование имени массива в указатель на его начальный элемент с потерей информации о размере массива. Таким образом, массив всегда передаётся по указателю – его копия не создаётся .
