Углубленный анализ языка C, как понять указатели и указатели структуры, функции указателя, указатели функций

1. Переменные-указатели

  • Прежде всего, вы должны понимать, что указатель — это переменная, вы можете использовать следующий код для проверки:
#include "stdio.h"

int main(int argc, char **argv) {
    
    
    unsigned int a = 10;
    unsigned int *p = NULL;
    p = &a;
    printf("&a = %d\n",a);
    printf("&a = %d\n",&a);
    *p = 20;
    printf("a = %d\n",a);
    return 0;
}
  • Результат операции следующий:
a = 10
&a = 6422216
a = 20
  • Видно, что значение a было изменено, поэтому можно четко понять, что указатель по сути является специальной переменной для размещения адреса переменной, а его сущность по-прежнему является переменной. Поскольку указатель является переменной, должен быть тип переменной.
  • В языке C все переменные имеют типы переменных, такие как целое число, тип с плавающей запятой, символьный тип, тип указателя, структура, объединение, перечисление и т. д., все из которых являются типами переменных. Появление типов переменных является неизбежным результатом управления памятью.Все мы знаем,что все переменные хранятся в памяти компьютера.Поскольку они размещены в памяти компьютера,то они неизбежно будут занимать определенное количество места.Сколько места займет переменная занимает?Что насчет пространства? Или сколько памяти нужно выделить для размещения переменной?
  • Для того, чтобы указать эту проблему, родился тип Для 32-битного компилятора тип int занимает 4 байта, то есть 32 бита, а тип long занимает 8 байтов, то есть 64 бита. В компьютере программы, которые нужно запустить, хранятся в памяти, и все переменные в программе на самом деле являются операциями в памяти. Структура памяти компьютера относительно проста, здесь мы не будем подробно обсуждать физическую структуру памяти, а только модель памяти. Память компьютера можно представить как дом, в котором живут люди, каждой комнате соответствует адрес памяти компьютера, а данные в памяти эквивалентны людям в доме.

вставьте сюда описание изображения

  • Так как указатель тоже переменная, то этот указатель тоже должен храниться в памяти.Для 32-битного компилятора его адресное пространство равно 2 32 = 4GB.Чтобы иметь возможность оперировать всей памятью (на самом деле это невозможно для обычным пользователям для работы со всей памятью), хранилище переменной указателя также должно использовать 32 цифры, то есть 4 байта, так что есть адрес указателя &p, отношение между указателем и переменной может быть представлено следующим фигура:

вставьте сюда описание изображения

  • Видно, что &p — это адрес указателя, который используется для хранения указателя p, а указатель p используется для хранения адреса переменной a, которая есть &a, а в языке C есть *p это "dequote", что означает, что компилятор должен удалить содержимое, хранящееся по этому адресу. Вы сами можете подумать: что означают &(*p) и *(&p) и как их понимать?
  • Что касается вопроса о типе указателя, то для 32-битного компилятора, поскольку любой указатель занимает всего 4 байта, зачем нам вводить тип указателя? Это просто для ограничения переменных одного и того же типа? На самом деле, я должен упомянуть операции с указателями, сначала подумайте о следующих двух операциях: p+1 и ((unsignedint)p)+1, как понять?
  • Значения этих двух операций различны.Во-первых, давайте поговорим о первой операции p+1, как показано на следующем рисунке:

вставьте сюда описание изображения

  • Для разных типов указателей адрес, на который указывает p+1, отличается, это приращение зависит от размера памяти, занимаемой указателем типа, и для ((unsigned int)p)+1 это означает, что адрес, на который указывает указатель p Значение адреса напрямую преобразуется в число, а затем +1, так что независимо от типа указателя p результатом будет адрес после адреса, на который указывает указатель.
  • Из вышеприведенного анализа видно, что наличие указателей позволяет программистам довольно легко манипулировать памятью, что также заставляет некоторых людей думать, что указатели довольно опасны.Это мнение отражено в языках C # и Java, но на самом деле использование указателей может значительно повысить эффективность.
  • Немного углубившись в работу с памятью через указатели, теперь вам нужно заполнить данные 125 в памяти 6422216, вы можете выполнить следующие операции:
unsigned int *p = (unsigned int*)(6422216);
*p = 125;
  • Конечно, в приведенном выше коде используется указатель.На самом деле, в языке C операцию разыменования можно напрямую использовать для более удобного присвоения значений памяти.

2. Интерпретация

  • Так называемая операция удаления кавычек на самом деле является операцией над адресом. Например, если вы хотите присвоить значение переменной a сейчас, общая операция будет a = 125. Теперь используйте операцию удаления кавычек, чтобы завершить ее. следующее:
*(&a) = 125;
  • Вы можете видеть, что оператор удаления кавычек — *.Этот оператор имеет два разных значения для указателей.При объявлении он объявляет указатель, а при использовании указателя p это операция удаления кавычек.Правая часть операции удаления кавычек — это адрес , поэтому операция декавации означает данные в адресной памяти, поэтому для заполнения данных 125 в памяти 6422216 можно использовать следующие операции:
*(unsigned int*)(6422216) = 125;
  • Вышеприведенная операция преобразует значение 6422216 в адрес. Это нужно для того, чтобы сообщить компилятору, что это значение является адресом. Стоит отметить, что все указанные выше адреса памяти не могут быть указаны случайно. Это должна быть память, выделенная компьютером, иначе компьютер будет думать, что указатель находится за пределами границ, а уничтожение операционной системой означает преждевременное завершение программы.

Три, указатель структуры

  • Указатель структуры такой же, как и обычный указатель переменной.Указатель структуры занимает всего 4 байта (32-разрядный компилятор), но указатель структуры может легко получить доступ к любому элементу типа структуры.Это оператор-член указателя -> .
  • Как показано ниже, p — это указатель на структуру, p указывает на первый адрес структуры, а p->a может использоваться для доступа к элементу a в структуре, конечно, p->a и *§ — это одно и то же:

вставьте сюда описание изображения

4. Обязательное преобразование типов

  • Из вышеприведенного тестового кода видно, что компилятор выдаст много предупреждений, а это значит, что типы данных не совпадают, хотя это и не влияет на корректную работу программы, но многие предупреждения всегда будут вызывать у людей дискомфорт. Таким образом, чтобы сообщить компилятору, что с кодом проблем нет, можно использовать приведение для преобразования участка памяти в требуемый тип данных.
  • Существует следующий массив a, который приводится к типу структуры stu:
#include <stdio.h>

typedef struct STUDENT {
    
    
    int name;
    int gender;
}stu;

int a[100] = {
    
    10,20,30,40,50};

int main(int argc, char **argv) {
    
    
    stu *student;
    student = (stu*)a;
    printf("student->name = %d\n", student->name);
    printf("student->gender = %d\n", student->gender);
    return 0;
}
  • Результат операции следующий:
student->name = 10
student->gender = 20
  • Видно, что a[100] приводится к типу структуры stu.Конечно, можно и не использовать приведение, но компилятор выдаст аварийный сигнал. Как показано ниже, первые 12 байт массива a[100] принудительно преобразуются в тип struct stu, что объясняет массив, и то же самое верно для других типов данных, которые по сути являются частью памяти:

вставьте сюда описание изображения

Пять, пустой указатель

  • Тип void легко представить как пустой, но для указателей он означает не пустой, а неопределенный. Во многих случаях указатель может не знать своего типа при его объявлении, или существует несколько типов данных, на которые указывает указатель, или вы просто хотите управлять пространством памяти через указатель, в это время вы можете объявить указатель как тип void.
  • Затем возникает проблема, из-за типа void, при децитировании определенного типа данных компилятор будет разыменовывать соответствующие данные в соответствии с пространством, занимаемым типом, например, int p, тогда p будет разыменован компилятором как p. размер пробела адреса указателя 4 байта. Но для нулевого типа указателя, как компилятор узнает размер памяти для разыменования?
  • Сначала взгляните на следующий фрагмент кода:
#include <stdio.h>

int main(int argc, char **argv) {
    
    
    int a = 10;
    void *p;
    p = &a;
    printf("p = %d\n",*p);
    return 0;
}
  • После компиляции приведенного выше кода можно обнаружить, что компилятор сообщает об ошибке и не может нормально скомпилироваться:
error:invalid use of void expression
  • Это показывает, что компилятор не может определить размер *p при расшифровке кавычек, поэтому он должен сообщить компилятору тип p или размер *p, так как же это определить? На самом деле это очень просто, достаточно использовать обязательное преобразование типов, как показано ниже:
*(int*)p
  • Таким образом, приведенный выше код можно оптимизировать для:
#include <stdio.h>

int main(int argc, char **argv) {
    
    
    int a = 10;
    void *p;
    p = &a;
    printf("p = %d\n", *(int*)p);
    return 0;
}
  • Результат операции следующий:
p = 10
  • Видно, что результат действительно правильный и согласуется с ожидаемой идеей.Поскольку указатель void не имеет атрибута размера пробела, указатель void не имеет операции ++.
  • Резюме: Пустой указатель — это просто указатель без определенного типа, то есть указатель имеет только атрибут данных адреса и не имеет атрибута размера пробела при раскавычивании.

Шесть, указатель функции

① Инструкции по использованию указателей функций

  • Указатели на функции часто используются в ядре Linux, а также при проектировании операционной системы.Так как указатели на функции также являются указателями, указатели на функции также занимают 4 байта (32-битный компилятор).
  • Чтобы проиллюстрировать на простом примере:
#include <stdio.h>

int  add(int a,int b) {
    
    
    return a+b;
}

int main(int argc, char **argv) {
    
    
    int (*p)(int,int);
    p = add;
    printf("add(10,20) = %d\n",(*p)(10,20));
    return 0;
}
  • Результат операции следующий:
add (10, 20) = 30
  • Как видите, объявление указателя на функцию выглядит так:
返回类型(*函数名)(参数列表)
  • Операция разыменования указателя на функцию немного отличается от операции с обычными указателями.Для обычных указателей разыменование требует только извлечения данных в соответствии с типом, но указатель на функцию вызывает функцию, и его разыменование не может быть данными Вынеси, на самом деле разыменование указателя функции - это по сути процесс выполнения функции, но инструкция вызова, используемая для выполнения функции, - это не предыдущая функция, а значение указателя функции, то есть адрес функция. На самом деле процесс выполнения функции по существу использует инструкцию call для вызова адреса функции, поэтому указатель функции по существу сохраняет первый адрес процесса выполнения функции.
  • Указатель функции вызывается следующим образом:
函数指针调用(*(实参列表)
  • Чтобы подтвердить, что указатель на функцию по сути является адресом функции, переданной в инструкцию вызова, ниже показаны два фрагмента кода:
#include <stdio.h>

void add (void) {
    
    
	printf("hello add\n");
}

int main (int arg, char **argv) {
    
    
	void (*p (void);
	p = add;
	(*р) О;
	return 0;
}
#include <stdio.h>

void add (void) {
    
    
	printf("hello add\n");
}

int main (int arg, char **argv) {
    
    
	add();
	return 0;
}
  • Инструкции по сборке после компиляции двух частей кода следующие:
0×4015d5 	push    ebp
0×4015d6	mov     ebp, esp
0×4015d8	and     esp, 0xfffffff0
0×4015db	sub     esp, 0x10
0×4015de	call    0x401690 <__main>
0×4015e3	mov     exa, DWORD PTR [esp+0xc]
0×4015eb	call    exa
0×4015ef	mov     exa 0x0
0×4015f1 	leave
0×4015f6    left
0×4015f7	ret
0×4015d5 	push    ebp
0×4015d6	mov     ebp, esp
0×4015d8	and     esp, 0xfffffff0
0×4015db	call    0x401680 <__main>
0×4015e0	call    0x4016c0 <add>
0×4015e5	mov     exa 0x0
0×4015ea	leave
0×4015eb    left
  • Видно, что при использовании указателя функции для вызова функции инструкции по ее сборке выглядят следующим образом:
0x4015e3    mov    DWORD PTR [esp+0xc],0x4015c0
0x4015eb    mov    eax,DWORD PTR [esp+0xc]
0x4015ef    call   eax
  • Первая строка инструкции mov присваивает непосредственное значение 0x4015c0 адресной памяти регистра esp+0xc, затем присваивает значение адреса регистра esp+0xc регистру eax (аккумулятору), а затем вызывает инструкцию call , в это время указатель pc будет указывать на добавление функции, а 0x4015c0 — это просто первый адрес добавления функции, что завершает вызов функции. Осторожно, вы обнаружили интересное явление: значение указателя функции в вышеприведенном процессе помещается в кадр стека, как и параметры, так что это похоже на процесс передачи параметров, поэтому вы можете видеть, что указатель функции, наконец, переданные как параметры Форма передается в вызываемую функцию, и переданное значение оказывается первым адресом функции.
  • Указатели на функции не могут манипулировать памятью, как обычные указатели, поэтому указатели на функции можно рассматривать как объявления ссылок на функции.

② Применение указателя функции

  • Он чаще всего используется в идее объектно-ориентированного программирования под управлением Linux и использует указатели на функции для реализации инкапсуляции. Следующее:
#include <stdio.h>

typedef struct TFT_DISPLAY {
    
    
    int   pix_width;
    int   pix_height;
    int   color_width;
    void (*init)(void);
    void (*fill_screen)(int color);
    void (*tft_test)(void);
}tft_display; 

static void init(void) {
    
    
    printf("the display is initialed\n");
}

static void fill_screen(int color) {
    
    
    printf("the display screen set 0x%x\n",color);

}

tft_display mydisplay = {
    
    
    .pix_width = 320,
    .pix_height = 240,
    .color_width = 24,
    .init = init,
    .fill_screen = fill_screen,
};

int main(int argc, char **argv) {
    
    

    mydisplay.init();
    mydisplay.fill_screen(0xfff);
    return 0;
}
  • Вышеприведенный пример кода инкапсулирует tft_display в объект. Последний член структуры не инициализируется. Он часто используется в Linux. Наиболее распространенной является структура file_operations. Вообще говоря, в этой структуре необходимо инициализировать только общие функции. Вся инициализация не требуется, и используемый метод инициализации структуры также является наиболее часто используемым методом в Linux. Преимущество этого метода в том, что он не должен быть тождественным в соответствии с порядком структуры.

③ функция обратного вызова

  • Иногда бывает такая ситуация, что когда А передает функцию Б на доработку, А и Б работают синхронно.В это время функция функции не выполнена.В это время А может определить API для передачи Б , и A Пока вы заботитесь об API, вам не нужно заботиться о конкретной реализации. Конкретная реализация может быть завершена B. В этом случае будет использоваться функция обратного вызова (функция обратного вызова). Теперь предположим A нуждается в алгоритме FFT.В это время A будет передавать алгоритм FFT B для завершения, теперь давайте реализуем этот процесс:
#include <stdio.h>

int InputData[100] = {
    
    0};
int OutputData[100] = {
    
    0};

void FFT_Function(int *inputData, int *outputData, int num) {
    
    
    while(num--) {
    
    

    }
}

void TaskA_CallBack(void (*fft)(int*,int*,int)) {
    
    
    (*fft)(InputData, OutputData,100);
}

int main(int argc, char **argv) {
    
    
    TaskA_CallBack(FFT_Function);
    return 0;
}
  • Видно, что TaskA_CallBack — это callback-функция, формальным параметром которой является указатель на функцию, а FFT_Function — это вызываемая функция, и тип указателя на функцию, объявленный в callback-функции, должен быть точно таким же, как у вызываемой функции.

Je suppose que tu aimes

Origine blog.csdn.net/Forever_wj/article/details/128847659
conseillé
Classement