Концепция пустого указателя в программировании на C


129

Можно ли разыменовать указатель void без преобразования типов в языке программирования C?

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

например:

void abc(void *a, int b)
{
   if(b==1)
      printf("%d",*(int*)a);     // If integer pointer is received
   else if(b==2)
      printf("%c",*(char*)a);     // If character pointer is received
   else if(b==3)
      printf("%f",*(float*)a);     // If float pointer is received
}

Я хочу сделать эту функцию универсальной без использования операторов if-else - возможно ли это?

Кроме того, если в Интернете есть хорошие статьи, объясняющие концепцию пустого указателя, было бы полезно, если бы вы могли предоставить URL-адреса.

Кроме того, возможна ли арифметика указателей с указателями void?

Ответы:


97

Можно ли разыменовать указатель void без приведения типов на языке программирования C ...

Нет, voidуказывает на отсутствие типа, это не то, что вы можете разыменовать или присвоить.

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

Вы не можете просто разыменовать его переносимым способом, так как он может быть неправильно выровнен. Это может быть проблемой в некоторых архитектурах, таких как ARM, где указатель на тип данных должен быть выровнен по границе размера типа данных (например, указатель на 32-битное целое число должен быть выровнен по 4-байтовой границе для разыменования).

Например, чтение uint16_tиз void*:

/* may receive wrong value if ptr is not 2-byte aligned */
uint16_t value = *(uint16_t*)ptr;
/* portable way of reading a little-endian value */
uint16_t value = *(uint8_t*)ptr
                | ((*((uint8_t*)ptr+1))<<8);

Кроме того, возможна арифметика указателей с указателями void ...

Арифметика указателей невозможна для указателей из- voidза отсутствия конкретного значения под указателем и, следовательно, размера.

void* p = ...
void *p2 = p + 1; /* what exactly is the size of void?? */

2
Если я не помню неправильно, вы можете безопасно разыменовать void * двумя способами. Приведение к char * всегда приемлемо, и если вы знаете исходный тип, на который он указывает, вы можете привести к этому типу. Void * потерял информацию о типе, поэтому ее пришлось бы сохранить в другом месте.
Дэн Олсон,

1
Да, вы всегда можете разыменовать, если приведете к другому типу, но вы не сможете сделать то же самое, если не приведете. Короче говоря, компилятор не будет знать, какие инструкции сборки использовать для математических операций, пока вы его не приведете .
Zachary Hamm

20
Я думаю, что GCC обрабатывает арифметику void *указателей так же, как char *, но это не стандарт, и вы не должны на него полагаться.
ephemient

5
Я не уверен, что понимаю. В примере, указанном выше OP, входящие типы гарантированно верны пользователем функции. Вы говорите, что если у нас есть что-то, где мы присваиваем указателю значение известного типа, преобразуем его в указатель void, а затем возвращаем его обратно в исходный тип указателя, чтобы он мог стать невыровненным? Я не понимаю, почему компилятор так подрывает вас, просто беспорядочно отменяя выравнивание, просто потому, что вы приводите их к указателю void. Или вы просто говорите, что мы не можем доверять пользователю знание типа аргументов, которые они передали функции?
doliver

2
@doliver Я имел в виду №2 («мы не можем доверять пользователю знать тип аргументов, которые он передал функции»). Но, вернувшись к моему ответу 6 лет назад (ох, неужели это было так долго?), Я понимаю, что вы имеете в виду, это не всегда так: когда исходный указатель указывал на правильный тип, он был бы уже выровнен, поэтому он Было бы безопасно забросить без проблем с выравниванием.
Alex B

31

В C a void *можно преобразовать в указатель на объект другого типа без явного приведения:

void abc(void *a, int b)
{
    int *test = a;
    /* ... */

Однако это не помогает при написании вашей функции в более общем виде.

Вы не можете разыменовать a void *с преобразованием его в другой тип указателя, так как разыменование указателя приводит к получению значения указанного объекта. Голый voidтип не является допустимым, поэтому разграничение void *невозможно.

Арифметика указателя заключается в изменении значений указателя на количество, кратное sizeofуказанным объектам. Опять же, поскольку voidэто не истинный тип, sizeof(void)не имеет значения, поэтому арифметика указателя недействительна void *. (Некоторые реализации позволяют это, используя эквивалентную арифметику указателя для char *.)


1
@CharlesBailey В своем коде вы пишете void abc(void *a, int b). Но почему бы не использовать void abc(int *a, int b), поскольку в вашей функции, которую вы в конечном итоге определите int *test = a;, она сохранит одну переменную, и я не вижу других преимуществ при определении другой переменной. Я вижу много кодов, написанных таким образом, с использованием в void *качестве параметров и приведением переменных позже в функции. Но так как эта функция написана автором, он должен знать использование переменной, поэтому зачем использовать void *? Спасибо
Lion Lai

15

Вы должны знать, что в C, в отличие от Java или C #, нет абсолютно никакой возможности успешно «угадать» тип объекта, на который void*указывает указатель. getClass()Ничего подобного просто не существует, так как этой информации нигде нет. По этой причине тип «универсального», который вы ищете, всегда идет с явной метаинформацией, такой как int bв вашем примере или строка формата в printfсемействе функций.


7

Указатель void известен как универсальный указатель, который может ссылаться на переменные любого типа данных.


6

Пока что я недооцениваю указатель void следующим образом.

Когда переменная-указатель объявляется с использованием ключевого слова void - она ​​становится переменной-указателем общего назначения. Адрес любой переменной любого типа данных (char, int, float и т. Д.) Может быть назначен переменной-указателю типа void.

main()
{
    int *p;

    void *vp;

    vp=p;
} 

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

Я попытался написать простой код на C, который принимает в качестве аргумента целое число или число с плавающей запятой и пытается сделать его + ve, если он отрицательный. Я написал следующий код,

#include<stdio.h>

void absolute_value ( void *j) // works if used float, obviously it must work but thats not my interest here.
{
    if ( *j < 0 )
        *j = *j * (-1);

}

int main()
{
    int i = 40;
    float f = -40;
    printf("print intiger i = %d \n",i);
    printf("print float f = %f \n",f);
    absolute_value(&i);
    absolute_value(&f);
    printf("print intiger i = %d \n",i);
    printf("print float f = %f \n",f);
    return 0;
}   

Но я получал ошибку, поэтому я понял, что мое понимание с указателем void неверно :(. Итак, теперь я перейду к сбору очков, почему это так.

Что мне нужно больше понять о недействительных указателях, так это то, что.

Нам нужно привести тип переменной указателя void, чтобы разыменовать ее. Это связано с тем, что указатель void не имеет связанного с ним типа данных. Компилятор не может знать (или догадываться?), На какой тип данных указывает указатель void. Таким образом, чтобы взять данные, на которые указывает указатель void, мы приводим его к правильному типу данных, хранящихся внутри местоположения указателей void.

void main()

{

    int a=10;

    float b=35.75;

    void *ptr; // Declaring a void pointer

    ptr=&a; // Assigning address of integer to void pointer.

    printf("The value of integer variable is= %d",*( (int*) ptr) );// (int*)ptr - is used for type casting. Where as *((int*)ptr) dereferences the typecasted void pointer variable.

    ptr=&b; // Assigning address of float to void pointer.

    printf("The value of float variable is= %f",*( (float*) ptr) );

}

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

void funct(void *a, int z)
{
    if(z==1)
    printf("%d",*(int*)a); // If user inputs 1, then he means the data is an integer and type casting is done accordingly.
    else if(z==2)
    printf("%c",*(char*)a); // Typecasting for character pointer.
    else if(z==3)
    printf("%f",*(float*)a); // Typecasting for float pointer
}

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

void *ptr;

int a;

ptr=&a;

ptr++; // This statement is invalid and will result in an error because 'ptr' is a void pointer variable.

Итак, теперь я понял, в чем была моя ошибка. Я исправляю то же самое.

Ссылки :

http://www.antoarts.com/void-pointers-in-c/

http://www.circuitstoday.com/void-pointers-in-c .

Новый код показан ниже.


#include<stdio.h>
#define INT 1
#define FLOAT 2

void absolute_value ( void *j, int *n)
{
    if ( *n == INT) {
        if ( *((int*)j) < 0 )
            *((int*)j) = *((int*)j) * (-1);
    }
    if ( *n == FLOAT ) {
        if ( *((float*)j) < 0 )
            *((float*)j) = *((float*)j) * (-1);
    }
}


int main()
{
    int i = 0,n=0;
    float f = 0;
    printf("Press 1 to enter integer or 2 got float then enter the value to get absolute value\n");
    scanf("%d",&n);
    printf("\n");
    if( n == 1) {
        scanf("%d",&i);
        printf("value entered before absolute function exec = %d \n",i);
        absolute_value(&i,&n);
        printf("value entered after absolute function exec = %d \n",i);
    }
    if( n == 2) {
        scanf("%f",&f);
        printf("value entered before absolute function exec = %f \n",f);
        absolute_value(&f,&n);
        printf("value entered after absolute function exec = %f \n",f);
    }
    else
    printf("unknown entry try again\n");
    return 0;
}   

Спасибо,


2

Нет, это невозможно. Какой тип должно иметь разыменованное значение?


2
void abc(void *a, int b) {
  char *format[] = {"%d", "%c", "%f"};
  printf(format[b-1], a);
}

Возможна ли эта программа ... Пожалуйста, проверьте, возможно ли это в программировании на C ...
AGeek

2
Да, это возможно (хотя код опасен без проверки диапазона для b). Printf принимает переменное количество аргументов, а просто помещается в стек как любой указатель (без какой-либо информации о типе) и вставляется в функцию printf с использованием макроса va_arg с использованием информации из строки формата.
hlovdal

@SiegeX: опишите, что нельзя переносить, а что может быть неопределенным.
Готье

@Gauthier, передающий переменную, которая содержит спецификаторы формата, не является переносимым и потенциально неопределенным поведением. Если вы хотите сделать что-то подобное, посмотрите на вариант «против» printf. В этом ответе есть хороший пример.
SiegeX 01

На практике я бы, вероятно, заменил int bэто перечислением, но внесение каких-либо последующих изменений в это перечисление нарушило бы эту функцию.
Джек Стаут

1

Я хочу сделать эту функцию универсальной без использования ifs; Является ли это возможным?

Единственный простой способ, который я вижу, - это использовать перегрузку .., которая недоступна в языке программирования C AFAIK.

Рассматривали ли вы язык программирования C ++ для своей программы? Или есть какое-то ограничение, запрещающее его использование?


Хорошо, это позор. С моей точки зрения, решений тогда я не вижу. Фактически, это то же самое, что printf () & cout: два разных способа реализации печати. printf () используйте оператор if () для декодирования строки формата (я полагаю или что-то подобное), в то время как для cout case operator << является перегруженной функцией
Ив Баумес

1

Вот краткий указатель на voidуказатели: https://www.learncpp.com/cpp-tutorial/613-void-pointers/

6.13 - Указатели пустоты

Поскольку указатель void не знает, на какой тип объекта он указывает, его нельзя разыменовать напрямую! Скорее, указатель void должен быть сначала явно приведен к другому типу указателя, прежде чем он будет разыменован.

Если указатель void не знает, на что он указывает, как мы узнаем, к чему его привести? В конечном итоге это зависит от вас.

Сборник указателей пустоты

Невозможно выполнить арифметику указателя на указатель void. Это связано с тем, что арифметика указателя требует, чтобы указатель знал, какой размер объекта он указывает, чтобы он мог соответствующим образом увеличивать или уменьшать указатель.

Предполагая , что память аппарата являешься байтами-адресации и не требует выровненных доступов, наиболее общий и атомный (ближе к представлению уровня машины) способом интерпретации void*является как указатель на-а-байты, uint8_t*. Преобразование a void*в a uint8_t*позволит вам, например, распечатать первые 1/2/4/8 / сколько бы вы ни пожелали байтов, начиная с этого адреса, но вы больше ничего не можете сделать.

uint8_t* byte_p = (uint8_t*)p;
for (uint8_t* i = byte_p; i < byte_p + 8; i++) {
  printf("%x ",*i);
}

0

Вы можете легко распечатать пустой принтер

int p=15;
void *q;
q=&p;
printf("%d",*((int*)q));

1
Кто гарантирует, что voidразмер такой же int?
Ant_222 06

Технически это не печать voidуказателя, а печать по intадресу памяти, который содержит указатель (который в данном случае будет значением p).
Marionumber1 05

0

Поскольку C - это статически типизированный язык со строгой типизацией, вы должны выбрать тип переменной перед компиляцией. Когда вы пытаетесь имитировать дженерики на C, вы снова попытаетесь переписать C ++, поэтому было бы лучше использовать C ++.


0

Указатель void - это универсальный указатель. Адрес любого типа данных любой переменной может быть назначен указателю void.

int a = 10;
float b = 3.14;
void *ptr;
ptr = &a;
printf( "data is %d " , *((int *)ptr)); 
//(int *)ptr used for typecasting dereferencing as int
ptr = &b;
printf( "data is %f " , *((float *)ptr));
//(float *)ptr used for typecasting dereferencing as float

0

Вы не можете разыменовать указатель без указания его типа, потому что разные типы данных будут иметь разные размеры в памяти, то есть int - 4 байта, char - 1 байт.


-1

По сути, в C «типы» - это способ интерпретации байтов в памяти. Например, что следующий код

struct Point {
  int x;
  int y;
};

int main() {
  struct Point p;
  p.x = 0;
  p.y = 0;
}

Говорит: «Когда я запускаю main, я хочу выделить 4 (размер целого числа) + 4 (размер целого числа) = 8 (всего байтов) памяти. Когда я пишу '.x' как lvalue для значения с меткой типа Укажите во время компиляции, извлеките данные из области памяти указателя плюс четыре байта. Присвойте возвращаемому значению метку времени компиляции "int".

Внутри компьютера во время выполнения ваша структура «Point» выглядит так:

00000000 00000000 00000000 00000000 00000000 00000000 00000000

И вот как void*может выглядеть ваш тип данных: (на 32-битном компьютере)

10001010 11111001 00010010 11000101

Это не ответ на вопрос
MM

-2

Это не сработает, но void * может очень помочь в определении универсального указателя на функции и передаче его в качестве аргумента другой функции (аналогично обратному вызову в Java) или определение его структуры, аналогичной oop.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.