Указатели в языке Си

  • Указатель - это переменная содержащая адрес. В 32 битных системах она занимает 32 бита (4 байт), а в 64 битных системах она занимает 64 бита (8 байт). Тип указателя говорит какой тип данных размещен по адресу который содержит указатель.
  • Указатель типа void может ссылаться на любую переменную, структуру или функцию (независимо от размера).
  • Указатель без адреса инициализируется NULL, но это не делается автоматически. Хорошая практика сразу инициализировать указатель адресом переменной, либо присваивать ему NULL.
  • Адрес переменной получается использованием оператора взятия адреса & (знак амперсанда).
  • Оператор разыменования указателя (звездочка перед указателем) возвращает значение переменной на которую ссылается указатель.
  • Перед использованием указатель должен быть инициализирован - указывать на адекватный адрес. Иначе запись или чтение могут быть произведены по произвольному адресу с непредсказуемым результатом (вплоть до краха программы).
  • Знак звездочка может быть отделен от имени переменной пробелом * p, а может и быть слитно *p, никакой разницы - оформляйте код как Вам удобно.

int a; // Объявление переменной a типа int
int *p; // Объявление указателя p на int 
*p = 5; // Ошибка! Указатель указывает на случайную память
p = &a; // Присваиваем указателю адрес переменной a
*p = 5; // Верно (теперь a = 5 и *p = 5)     

#include <stdio.h> //printf()
#include <stdio.h> //printf()
int main(){
    int a = 7; // Целочисленная переменная a
    int *p = &a; // Указатель p на целочисленную переменную a
    printf("*p = %d \n", *p); // Разыменование указателя (*p = 7)
    printf("&a = %p \n", &a); // Оператор взятия адреса & (&a = 000000000061FE1C)
    printf("p = %p \n", p); // Указатель - это адрес (p = 000000000061FE1C)
    printf("&p = %p \n", &p); // Указатель тоже размещен в памяти и имеет адрес (&p = 000000000061FE10)
}   

Константные указатели

Существует мнемоническое правило, позволяющее легко запомнить, к чему относится const. Надо провести черту через *, если const слева, то оно относится к значению данных; если справа — к значению указателя.
Ключевое слово const может стоять и до типа (например int) и после (главное чтобы до *, если после - это уже другое поведение указателя) - разницы никакой


const int * p = &a; // Можно p = &b но нельзя *p = b  (аналогично int const * p = &a;)
int const * p = &a; // Аналогично предыдущей записи
int * const p = &a; // Можно *p = b но нельзя p = &b
const int * const p = &a; // Нельзя *p = b и нельзя p = &b (аналогично  int const * const p = &a;)   

Указатель на функцию

1) Код функции располагается в памяти и на него тоже можно сделать указатель - это будет указатель на функцию (на первый байт исполняемого кода функции).
2) Можно создавать массив указателей на функции

#include <stdio.h> // prinf()
#include <string.h> // strlen()
int my_print_1(const char *str, int num){ // Функция 1
    printf("str = %s, num = %d\n", str, num); // Печатаем переданные параметры
    return strlen(str); // Возвращаем длину строки
}
int my_print_2(const char *str, int num){ // Функция 2
    printf("text = %s, digit = %d\n", str, num); // Печатаем переданные параметры
    return strlen(str); // Возвращаем длину строки
}
int main(){
    int (*p_func)(const char *, int); // Указатель на функцию
    p_func = my_print_1; // Присваиваем указателю адрес функции
    int size = p_func("Hi Victor!", 123); // "str = Hi Victor!, num = 123" Вызываем my_print_1() через указатель
    printf("my_print_return = %d \n", size);  // "my_print_return = 10" Выводим длину строки

    int (*func_ar[2])(const char *, int) = {my_print_1, my_print_2}; // Массив указателей на функции
    int num_1 = func_ar[0]("my_print_1 text", 10);   // "str = my_print_1 text, num = 10" Вызываем my_print_1() через указатель
    int num_2 = func_ar[1]("my_print_2 strint", 20);   // "text = my_print_2 strint, digit = 20" Вызываем my_print_2() через указатель
    printf("func_ar[0] return = %d \n", num_1);  // "func_ar[0] return = 15" Выводим длину строки
    printf("func_ar[1] return = %d \n", num_2);  // "func_ar[1] return = 17" Выводим длину строки
}   
3) Можно создавать динамические массивы указателей на функции

int (**func_ar)(const char *, int) = malloc(2 * sizeof(&my_print_1)); // Динамический массив для 2х указателей на функцию
free(func_ar); // Освобождение памяти   

Возврат указателя или массива из функции

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

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

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


#include <stdio.h> // printf()
#include <stdlib.h> // malloc(), free()

struct st_array{ // Структура массива
    int *ar; // Указатель на массив
    int size; // Размер массива
};

struct st_array * ar_create(int size, int initial_value){ // Возвращает структура с массивом size элементов со значениями от initial_value
    struct st_array *st = malloc(sizeof(struct st_array)); // Создаем указатель на структуру и выделяем под нее память
    st->size = size; // Размер массива
    st->ar = malloc(size * sizeof (initial_value)); // Выделяем память под массив
    for(int i = 0; i < size; i++){ // Заполняем массив
        st->ar[i] = initial_value + i;
    }
    return st; // Возвращаем структуру массива
}

void print_array(struct st_array *st){ // Выводим значения массива
    for(int i = 0; i < st->size; i++){
        printf("%d ", st->ar[i]);
    }
    printf("\n");
}

void delete_ar(struct st_array *st){ // Освобождение памяти структуры массива
    free(st->ar); // Освобождаем память занятую массивом
    free(st); // Освобождаем память занятую структурой
}

int main(){
    struct st_array *st1 = ar_create(10, 5); // Создаем структуру массива
    struct st_array *st2 = ar_create(5, 1); // Создаем структуру массива
    struct st_array *st3 = ar_create(13, 7); // Создаем структуру массива
    print_array(st1); // "5 6 7 8 9 10 11 12 13 14" Печатаем значения массива
    print_array(st2); // "1 2 3 4 5"
    print_array(st3); // "7 8 9 10 11 12 13 14 15 16 17 18 19"
    delete_ar(st1); // Освобождаем память
    delete_ar(st2); // Освобождаем память
    delete_ar(st3); // Освобождаем память
}
2023-03-13



Понравилась страница?
Добавить в закладки
Или поделиться!

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

auto ключевое слово языка Си.
break оператор языка Си. Завершает выполнение операторов do, while, for, switch.
case ключевое слово языка Си. Используется в операторе switch для задания одной из веток ветвления.
Время выполнения программы на языке Си. Функция clock().
const ключевое слово языка Си. Сделать переменную или указатель неизменяемым.
continue оператор языка Си. Пропускает текущую итерацию цикла.
default ключевое слово языка Си.
do while ключевые слова Си.
else ключевое слово языка Си.
Функции работы с файлами в языке Си
for оператор языка Си.
if оператор языка Си.
Ключевые слова языка Си
Указатели в языке Си
register Си, размещение переменных в регистрах процессора
Зарезервированные имена языка Си
signed ключевое слово языка Си.
Структуры в языке Си. Примеры различного использования.
switch оператор языка Си.
typedef ключевое слово языка Си. Задание псевдонимов для типов данных.
Типы данных языка Си
unsigned ключевое слово Си.
void ключевое слово языка Си.
volatile ключевое слово языка Си.
while оператор цикла языка Си.