Punteros y direcciones de memoria en C

Aprende cómo un programa en C distribuye sus variables en memoria y cómo los punteros te permiten trabajar directamente con direcciones de memoria. Obtén direcciones con &, lee y modifica valores mediante punteros y pasa variables a funciones por referencia. Recorre arrays y cadenas mediante aritmética de punteros, utiliza punteros a punteros para los argumentos de la línea de comandos y para funciones que desplazan un puntero de la función llamadora, y llama a funciones mediante punteros a funciones, incluidas las funciones de comparación para la función estándar qsort.

PrincipianteUnidad 4 de 8 del curso Curso de Programación en C

Lo que aprenderás

  1. Visualiza cómo las variables de un programa en C ocupan bytes en memoria, almacena direcciones en variables de tipo puntero y utiliza esos punteros para leer y modificar las variables a las que apuntan.
    • Visualizar cómo se distribuyen las variables en memoria
      Describe la memoria como bytes numerados, averigua el tamaño y la dirección de variables y elementos de arrays, y explica cuánto tiempo existe cada tipo de variable.
    • Declarar punteros y obtener direcciones con &
      Declara variables de tipo puntero del tipo adecuado, almacena la dirección de una variable con el operador de obtención de dirección e imprime, redirige o restablece un puntero de forma segura.
    • Leer y modificar valores mediante punteros
      Utiliza el operador de desreferenciación para leer y escribir el objeto al que apunta un puntero, sigue la ejecución del código con diagramas de memoria y evita desreferenciar punteros no válidos.
  2. Escribe funciones que modifiquen las variables de la función llamadora mediante parámetros de tipo puntero, protege los datos de solo lectura con const y devuelve punteros que sigan siendo válidos.
    • Pasar variables por referencia mediante parámetros de tipo puntero
      Explica por qué las funciones de C reciben copias de sus argumentos y utiliza parámetros de tipo puntero para modificar variables de la función llamadora, intercambiar valores, devolver varios resultados y leer la entrada con scanf.
    • Proteger los datos con const y devolver punteros de forma segura
      Marca los datos de solo lectura con const, distingue un puntero a datos constantes de un puntero constante y devuelve punteros que sigan siendo válidos después de que termine la función.
  3. Desplaza punteros por arrays y cadenas, explica cómo se relacionan los nombres de arrays con los punteros y mantén cada puntero dentro del array al que pertenece.
    • Realizar operaciones aritméticas con punteros
      Suma enteros a punteros, resta y compara punteros a elementos del mismo array y reconoce qué resultados son válidos.
    • Explicar la relación entre arrays y punteros
      Utiliza la conversión del nombre de un array a puntero, la identidad entre a[i] y *(a + i) y las diferencias reales entre un array y un puntero.
    • Recorrer arrays y cadenas con punteros
      Escribe bucles que recorran arrays y cadenas terminadas en nulo con punteros, utiliza correctamente expresiones habituales como *p++ y mantente dentro de los límites.
  4. Sigue varios niveles de indirección, permite que las funciones actualicen un puntero de la función llamadora y utiliza arrays de punteros como los argumentos de la línea de comandos.
    • Utilizar punteros a punteros
      Declara y desreferencia un puntero a puntero, sigue su recorrido en un diagrama de memoria y utilízalo para permitir que una función modifique un puntero de la función llamadora.
    • Utilizar arrays de punteros y argumentos de la línea de comandos
      Construye arrays de punteros a cadenas, procesa argc y argv como punteros y distingue un array de punteros de un array bidimensional.
  5. Almacena punteros a funciones y llama a las funciones mediante ellos, pasa funciones a otras funciones y ordena datos de cualquier tipo con la función estándar qsort y una función de comparación.
    • Declarar punteros a funciones, asignarles valores y llamar a funciones mediante ellos
      Escribe e interpreta declaraciones de punteros a funciones, llama a funciones mediante ellos y construye tablas de funciones que eligen el comportamiento durante la ejecución del programa.
    • Escribir funciones de devolución de llamada y ordenar con qsort
      Pasa funciones como argumentos, utiliza punteros void para manejar datos de cualquier tipo y escribe funciones de comparación correctas para qsort.

Guía de estudio de Punteros y direcciones de memoria en C

5 capítulos

1

Memoria, direcciones y valores apuntados

Una variable tiene un valor, ocupa almacenamiento y tiene un tiempo de vida. Un puntero te permite acceder a esa variable mediante su dirección, por lo que leer código ahora implica seguir tanto la dirección contenida en un puntero como el valor que se encuentra allí. Comienza por la distribución de los bytes y después sigue los efectos de obtener una dirección, redirigir un puntero y escribir mediante él. Los diagramas de memoria pequeños hacen visibles estas operaciones antes de que aparezcan dentro de funciones y bucles.

1.1 Bytes, tamaños y tiempos de vida

La memoria puede representarse como una secuencia de bytes numerados. Cada número es una dirección. Un objeto como un int ocupa bytes consecutivos y su dirección identifica su primer byte. El nombre que utilizas en el código fuente permite al compilador encontrar ese objeto; el lenguaje no exige que se almacene una etiqueta con el nombre junto a su valor.

Medir el almacenamiento con sizeof

sizeof indica los bytes y produce un valor size_t. Utiliza %zu para imprimirlo:

int scores[4] = {12, 18, 15, 21};
printf("%zu %zu\n", sizeof scores, sizeof scores[0]);

Con valores int de 4 bytes, esto imprime 16 4. El primer resultado mide los cuatro elementos; el segundo mide uno. sizeof(char) siempre es 1. Una disposición habitual en equipos de escritorio de 64 bits utiliza valores int de 4 bytes, valores double de 8 bytes y punteros a objetos de 8 bytes. Otros sistemas pueden diferir. Incluso entre sistemas de 64 bits, long suele ocupar 8 bytes en Linux y macOS, pero 4 en Windows.

El tamaño del almacenamiento y la longitud del texto responden a preguntas distintas. Para char word[20] = "hi";, sizeof word es 20, mientras que strlen(word) es 2. El array sigue conteniendo 20 elementos aunque su cadena utilice solo dos letras y un terminador. Salvo en los arrays de longitud variable, estos tamaños de objeto se fijan en tiempo de compilación.

Seguir las direcciones de un array

Las direcciones hexadecimales utilizan los dígitos de 0 a 9 y luego de a a f. El prefijo 0x indica la base 16. Para un array ilustrativo que comienza en 0x1000, cuatro elementos de 4 bytes comienzan en:

ElementoPrimer byteÚltimo byte
scores[0]0x10000x1003
scores[1]0x10040x1007
scores[2]0x10080x100B
scores[3]0x100C0x100F

0x100C es 4108 en decimal, doce bytes después de 0x1000. Los elementos de un array no tienen espacios entre ellos. No se garantiza el orden ni la separación de las variables declaradas por separado; las direcciones reales impresas también pueden cambiar entre ejecuciones. Un compilador puede mantener un valor en un registro del procesador cuando no se necesita almacenamiento con dirección.

Alineación y orden de los bytes

La alineación restringe dónde pueden comenzar los objetos de un tipo. Una disposición habitual sitúa los valores int de 4 bytes en múltiplos de 4 y los valores double de 8 bytes en múltiplos de 8. El compilador proporciona una alineación adecuada para los objetos declarados.

El orden de los bytes es el orden de los bytes dentro de un valor de varios bytes. Para un unsigned int de 4 bytes que contiene 0x12345678, el almacenamiento con el byte menos significativo primero tiene los bytes 78 56 34 12 de las direcciones menores a las mayores; el almacenamiento con el byte más significativo primero tiene 12 34 56 78. Un unsigned char * puede examinar la representación de un objeto byte a byte. El orden de los bytes no cambia el orden de los índices del array.

Mantener el objeto activo

Una variable local ordinaria tiene duración automática del almacenamiento: su tiempo de vida dura mientras se ejecuta su bloque. Las implementaciones típicas mantienen esos objetos en el marco de pila de una llamada y reutilizan ese almacenamiento después. Las variables globales, las variables static y los literales de cadena duran todo el programa. Guardar una dirección no prolonga el tiempo de vida de su objeto.

Puntos clave

  • sizeof mide bytes; strlen cuenta los caracteres anteriores al terminador de una cadena.
  • Los elementos de un array ocupan almacenamiento consecutivo en orden de índice.
  • No se garantizan las direcciones relativas de variables independientes.
  • La alineación determina las direcciones iniciales adecuadas; el orden de los bytes determina su orden dentro de un valor.
  • Una dirección guardada solo puede utilizarse mientras exista su objeto.

1.2 Almacenar y mostrar una dirección

Un puntero almacena una dirección y tiene un tipo que describe el objeto al que puede acceder. Declarar un int * te proporciona almacenamiento para un puntero; no crea un int que ese puntero pueda utilizar. Da al puntero un valor conocido cuando lo declares.

Obtener la dirección de un objeto
int count = 42;
int *p = &count;

El operador de obtención de dirección unario & obtiene la dirección de count. Si count comienza en la dirección ilustrativa 0x1000, p contiene 0x1000, mientras que count contiene 42. El puntero también es un objeto: &p es la dirección del almacenamiento independiente que contiene ese puntero.

Puedes obtener la dirección de un elemento de un array, como &scores[2]. Una constante numérica o el resultado de un cálculo no es un objeto almacenado cuya dirección puedas obtener de esta forma: &42 y &(count + 1) no son válidos. La expresión binaria a & b utiliza el mismo símbolo para la operación AND a nivel de bits; el número de operandos indica de qué operación se trata.

Interpretar cada declarador
DeclaraciónSignificado
int *p, q;Un puntero y un int ordinario
int *p, *q;Dos punteros a int
double *r;Un puntero a double

Los caracteres en blanco no cambian ninguno de estos tipos. int* p e int *p son equivalentes. Declarar un puntero por línea hace más difícil pasar por alto su asterisco. En equipos de escritorio habituales de 64 bits, un puntero a objeto ocupa 8 bytes, pero su propio tamaño no indica el tamaño del objeto al que apunta.

El tipo apuntado importa. Para double price = 9.5;, utiliza double *r = &price;. Inicializar un int * con &price produce un mensaje de diagnóstico por punteros incompatibles. Una conversión explícita que oculte el mensaje no hace válido el acceso a ese double como int: el tipo determina cómo se accede a los bytes almacenados.

Redirigir un puntero
int a = 5, b = 8;
int *p = &a;
p = &b;

La última asignación reemplaza la dirección en p. No mueve ninguno de los enteros ni copia el valor de uno en el otro. Ahora p apunta a b; a sigue siendo 5 y b sigue siendo 8. Varias variables de tipo puntero también pueden almacenar la misma dirección.

Utilizar un estado vacío conocido

Un puntero local sin inicializar tiene un valor indeterminado, y a veces se llama puntero indeterminado. No lo examines ni lo desreferencies. Inicialízalo con la dirección de un objeto válido o con NULL, disponible en cabeceras como <stddef.h> y <stdio.h>:

int *pending = NULL;
if (pending == NULL) {
    pending = &count;
}

Un puntero nulo no designa ningún objeto. !pending es otra prueba de nulidad. Estas pruebas solo tienen sentido para punteros inicializados; no validan una dirección arbitraria ni un puntero cuyo objeto ha dejado de existir.

Imprimir un puntero a objeto
printf("%p\n", (void *)p);

%p espera un void *, así que proporciona la conversión explícita cuando utilices un puntero a objeto con un tipo concreto. La implementación elige su representación impresa, que suele ser hexadecimal. %d y %x esperan argumentos enteros de tipos determinados y no pueden utilizarse como sustitutos de %p.

Puntos clave

  • Inicializa un puntero con una dirección válida o con NULL.
  • &count es una dirección; count es el entero almacenado.
  • Cada nombre de puntero necesita su propio * en una declaración.
  • Los tipos de puntero deben ser adecuados para los objetos a los que se accede mediante ellos.
  • p = &b cambia el puntero y deja los enteros sin modificar.
  • Imprime un puntero a objeto con %p y un argumento void *.

1.3 Seguir un puntero para leer o escribir

El operador de desreferenciación unario * sigue un puntero hasta su objeto. Si p apunta a x, *p designa el propio x. Leer *p lee ese objeto; asignar un valor a *p lo modifica.

Seguir una escritura mediante una dirección
int x = 10;
int *p = &x;
*p = *p + 5;
int y = *p * 2;
printf("%d %d %d\n", x, *p, y);

Primero p almacena la dirección de x. En el lado derecho de la asignación, *p lee 10. Al sumar 5 se obtiene 15, y el *p del lado izquierdo almacena 15 en x. La siguiente línea multiplica el valor actualizado por 2, por lo que y pasa a ser 30. La salida es 15 15 30.

El contexto distingue los asteriscos: int *p declara un puntero, el *p unario accede a un objeto y *p * 2 también contiene una multiplicación binaria. Para un objeto válido x, *&x designa x; para un puntero a un objeto válido, &*p devuelve el valor del puntero.

Distinguir las direcciones del contenido

Comienza cada traza con dos filas de información: qué dirección contiene cada puntero y qué valor contiene cada entero. Supón que p apunta a a, que contiene 3, y q apunta a b, que contiene 8.

Sentencia considerada desde ese estado inicialEfecto
*p = *q;Copia 8 en a; los punteros conservan sus destinos
p = q;Hace que p apunte a b; los enteros conservan sus valores

Un diagrama de memoria utiliza una caja para cada objeto. Etiqueta una caja con su nombre y dirección, coloca dentro su valor actual y dibuja una flecha desde un puntero hasta su destino. Una asignación de dirección mueve una flecha; una asignación al objeto apuntado cambia el contenido de la caja de destino.

Después de p = q, los punteros son alias de b. Si luego ejecutas *p = 20;, leer *q o b da 20. Copiar una dirección crea otra vía hacia el mismo objeto, no otro entero.

Comprobar que una desreferenciación es válida

Un puntero utilizado para acceder debe designar un objeto existente de un tipo adecuado, y escribir también requiere un objeto modificable. Los punteros nulos no designan ningún objeto. Los punteros sin inicializar no tienen un destino conocido que se pueda utilizar. Un puntero queda colgante cuando termina el tiempo de vida de su destino.

Acceder mediante un puntero no válido es comportamiento indefinido: C no impone requisitos sobre el resultado. El programa puede detenerse con un fallo de segmentación, corromper datos o parecer que se ejecuta normalmente. Una ejecución aparentemente correcta no demuestra que el acceso fuera válido.

Para un puntero que puede ser nulo legítimamente, compruébalo antes de acceder:

if (p != NULL) {
    *p = 0;
}

Asignar NULL a un puntero que ya no necesitas puede evitar que esa variable se utilice por accidente más adelante. No restablece otros alias. Lleva por separado el control del tiempo de vida del objeto.

Poner paréntesis alrededor del valor deseado

El ++ posfijo tiene mayor precedencia que el * unario. Por tanto, *p++ significa *(p++): utiliza el valor anterior del puntero para acceder a un valor y avanza el puntero. (*p)++ incrementa el valor apuntado y deja el puntero en su lugar. *p + 1 lee el valor y suma 1 sin almacenar el resultado. Son tres acciones distintas.

Puntos clave

  • *p designa el objeto al que se accede mediante p.
  • Copiar una dirección crea alias; copiar *p copia un valor.
  • Una desreferenciación requiere un objeto existente y un tipo de acceso adecuado.
  • El comportamiento indefinido no garantiza ningún resultado, ni siquiera un cierre inesperado.
  • *p++ avanza el puntero; (*p)++ incrementa su destino.
2

Permitir que las funciones trabajen con datos de la función llamadora

Una función recibe una copia de cada argumento, incluidos los argumentos de tipo puntero. Esa dirección copiada aún puede acceder a un objeto que pertenece a la función llamadora, lo que permite intercambiar valores, utilizar funciones de entrada y obtener varios resultados. La interfaz de la función también debe mostrar si modifica los datos. Sigue la dirección dentro de la llamada, distingue los cambios locales de las escrituras mediante ella y comprueba qué objetos seguirán existiendo cuando se utilice un puntero devuelto.

2.1 Modificar variables de la función llamadora mediante parámetros

C pasa todos los argumentos por valor. Un parámetro es una variable local inicializada a partir del argumento. Pasar una dirección no cambia esa regla: la función recibe una copia de la dirección y esa copia puede acceder al objeto de la función llamadora.

Comparar dos funciones de restablecimiento
void reset_copy(int n) {
    n = 0;
}

void reset(int *n) {
    *n = 0;
}

Para int score = 12;, reset_copy(score) cambia solo su n local. score sigue siendo 12. La llamada reset(&score) copia la dirección de score en el parámetro de tipo puntero n; asignar después un valor a *n cambia score a 0. Esta técnica suele llamarse paso por referencia, aunque el paso real de argumentos del lenguaje sigue siendo por valor.

La dirección en la llamada y la desreferenciación en el cuerpo cumplen funciones distintas. &score indica a la función dónde está el objeto. *n = 0 le indica qué almacenar allí. Llamar a reset(score) proporciona un entero donde se requiere un puntero y provoca un mensaje de diagnóstico del compilador.

Analizar un intercambio
void swap(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}

/* Inside a calling function: */
int x = 3, y = 8;
swap(&x, &y);
  1. a apunta a x y b apunta a y.
  2. t guarda el valor original de x, que es 3.
  3. *a = *b escribe 8 en x.
  4. *b = t escribe el 3 guardado en y.

Ambas variables de la función llamadora cambian. Una versión que recibe int a, int b normales solo intercambia copias. Una versión con punteros que hace int *t = a; a = b; b = t; tampoco intercambia los enteros: solo redirige los parámetros locales de tipo puntero.

Entregar más de un resultado

Una sentencia de retorno proporciona un valor, pero los parámetros de tipo puntero pueden llevar resultados adicionales:

void divide(int total, int size, int *q, int *r) {
    *q = total / size;
    *r = total % size;
}

/* size must be positive for these example inputs. */
int q, r;
divide(17, 5, &q, &r);

La división entera da q igual a 3 y r igual a 2. La función llamadora proporciona dos objetos modificables distintos. Otra interfaz útil devuelve un estado de éxito y escribe un resultado solo si tiene éxito; las funciones llamadoras comprueban ese estado antes de utilizar el resultado.

Reconocer el mismo patrón en llamadas a la biblioteca

scanf("%d", &age) necesita la dirección de age porque almacena allí el entero leído. Pasar age proporciona un valor en lugar del int * requerido, lo que produce comportamiento indefinido. Comprueba que scanf devuelva 1 antes de utilizar el valor recién leído.

Para texto, char name[20]; scanf("%19s", name); pasa un puntero al primer carácter del array. El ancho permite como máximo 19 caracteres de entrada y deja espacio para '\0'. La expresión del array ya proporciona el puntero a carácter requerido.

Un parámetro de tipo array también recibe una dirección, no un bloque copiado de elementos. En void zero(int *a, size_t n), las asignaciones a a[i] afectan al array de la función llamadora. Pasar la dirección solo copia un puntero, lo que también permite que funciones de solo lectura examinen arrays grandes sin copiar cada elemento.

Puntos clave

  • C copia todos los argumentos, incluidas las direcciones.
  • Una escritura mediante un parámetro de tipo puntero puede modificar el objeto de la función llamadora.
  • Un intercambio de enteros que funciona intercambia *a y *b utilizando un valor guardado.
  • Los parámetros de salida necesitan objetos válidos y modificables proporcionados por la función llamadora.
  • scanf recibe direcciones e indica cuántas conversiones tuvieron éxito.
  • Los parámetros de tipo array comparten el acceso a los elementos de la función llamadora.

2.2 Acceso con const y valores de retorno seguros

Una interfaz con punteros debe dejar claras dos cosas: si la función puede escribir mediante el puntero y durante cuánto tiempo sigue siendo válido el almacenamiento apuntado. const responde a la primera pregunta. El tiempo de vida del objeto responde a la segunda.

Identificar la restricción de const
Declaración¿Puede redirigirse p?¿Puede asignarse un valor mediante *p?
const int *p = &x;SíNo
int *const p = &x;NoSí
const int *const p = &x;NoNo

Un puntero a datos constantes limita esta vía de acceso. Si x se definió como un int x = 5; ordinario, x = 6; sigue estando permitido aunque un const int * apunte a él. Si el propio objeto se definió con const, intentar modificarlo mediante cualquier puntero es comportamiento indefinido.

Un puntero constante conserva una dirección. Debe inicializarse y la dirección no puede sustituirse después. El objeto en esa dirección aún puede cambiar cuando su tipo lo permite. Lee hacia fuera desde el nombre: int *const p hace que p sea constante; const int *p protege el int al que se accede mediante p.

Utiliza un parámetro de solo lectura como const char *s cuando una función solo examina una cadena. Puede pasarse un char * ordinario a ese parámetro sin conversión explícita. La función puede avanzar su s local, pero se rechaza la asignación mediante *s. Volver a convertir a un tipo modificable requiere más que eliminar una advertencia del compilador: el objeto subyacente debe ser realmente modificable.

Devolver una posición en almacenamiento de la función llamadora
int *find(int *a, size_t n, int target) {
    for (size_t i = 0; i < n; i++) {
        if (a[i] == target) return &a[i];
    }
    return NULL;
}

Para int v[] = {4, 9, 2, 9};, find(v, 4, 9) devuelve &v[1]. La función termina, pero v pertenece a la función llamadora y sigue existiendo allí. Después de comprobar el resultado frente a NULL, la función llamadora puede calcular found - v para obtener el índice 1 o asignar *found = 0 para actualizar el elemento. Una búsqueda de 7 devuelve NULL, que no debe desreferenciarse. strchr y strstr usan la misma convención de devolver un puntero nulo cuando no se encuentra el resultado.

Rechazar direcciones de variables locales cuyo tiempo de vida ha terminado
int *bad(void) {
    int result = 42;
    return &result;
}

result tiene duración automática del almacenamiento. Su tiempo de vida termina cuando la llamada retorna, lo que deja a la función llamadora con un puntero colgante. El compilador puede advertir de ello y cualquier acceso posterior mediante ese puntero tiene comportamiento indefinido. Que los bytes antiguos sean visibles en una ejecución no mantiene vivo el objeto.

El mismo problema puede ocurrir en la llave de cierre de un bloque interior. Un puntero exterior no puede prolongar el tiempo de vida de una variable interior cuya dirección guardó.

Elegir almacenamiento que dure lo suficiente

Devuelve un valor int cuando la función llamadora solo necesite un resultado entero. Para varios resultados, deja que la función llamadora proporcione el almacenamiento que se va a rellenar. Un puntero devuelto también puede referirse al array de la función llamadora, a un literal de cadena expuesto mediante const char * o a un objeto static.

Una variable local estática se inicializa una vez y dura todo el programa, pero todas las llamadas la comparten. Si una llamada posterior almacena allí un nuevo valor, todos los punteros devueltos anteriormente ven ese cambio. El almacenamiento estático resuelve el problema del tiempo de vida sin proporcionar a cada llamada su propio objeto de resultado independiente.

Puntos clave

  • const int * restringe las escrituras mediante ese puntero, mientras que int *const fija la dirección.
  • Un objeto definido con const no puede modificarse de forma válida.
  • Los punteros a elementos de un array existente de la función llamadora pueden seguir siendo válidos después de que retorne una función.
  • Comprueba un resultado de búsqueda que pueda ser nulo antes de acceder a él o restarlo.
  • Devolver la dirección de una variable local automática deja un puntero colgante.
  • Un objeto estático de resultado se comparte entre todas las llamadas.
3

Recorrer arrays y cadenas

La aritmética de punteros relaciona la dirección de un elemento con su posición en un array. El tipo apuntado proporciona el tamaño del paso, mientras que el array proporciona el rango permitido. Utiliza ese modelo para interpretar índices, pasar un segmento a una función y construir bucles que se detengan antes de un acceso no válido. Las cadenas añaden una segunda regla de parada: su carácter nulo de terminación marca el final del texto, incluso cuando el almacenamiento contiene más bytes.

3.1 Pasos entre elementos y límites del array

Sumar un entero a un puntero lo desplaza por elementos del tipo apuntado. Si p apunta al elemento i de un array, p + n apunta al elemento i + n, siempre que la posición resultante esté dentro del array o inmediatamente después de su último elemento.

Ajustar la dirección una sola vez

En la dirección ilustrativa 0x1000, sumar 3 da estos resultados:

Tipo de punteroTamaño de un elementoDirección después de p + 3
int *4 bytes0x100C
double *8 bytes0x1018
char *1 byte0x1003

El compilador realiza el ajuste según el tamaño. Escribir p + 3 * sizeof *p lo aplica dos veces y generalmente llega al elemento equivocado. Utiliza unsigned char * cuando el paso deseado sea realmente un byte de la representación de un objeto.

Para int a[6] = {10, 20, 30, 40, 50, 60}; e int *p = &a[1];, *(p + 2) lee a[3], que es 40. La asignación p += 3; desplaza el propio puntero a a[4], que contiene 50. Desde ahí, *(p - 1) lee 40. p++, p--, p += n y p -= n se desplazan por elementos.

Restar posiciones para contar elementos

&a[5] - &a[1] es 4, e invertir los operandos da -4. El resultado ya cuenta elementos; no lo dividas por sizeof(int). Su tipo con signo es ptrdiff_t, declarado en <stddef.h>, y %td lo imprime.

Resta solo punteros al mismo array, incluida la posición inmediatamente posterior a su último elemento, y con un resultado representable en ptrdiff_t. Esta operación describe posiciones dentro de ese array. Restar direcciones de variables no relacionadas es comportamiento indefinido, aunque los números impresos estén casualmente próximos.

Utilizar la posición posterior al final como límite

Para seis elementos, a + 6 es un puntero a la posición inmediatamente posterior al último elemento válido. Puede servir como límite y participar en una resta válida dentro del mismo array, pero no puede desreferenciarse. a + 7 y a - 1 ya tienen comportamiento indefinido al formarse; evitar una desreferenciación posterior no los corrige.

for (int *p = a; p < a + 6; p++) {
    printf("%d\n", *p);
}

El cuerpo se ejecuta para los índices de 0 a 5. Después del último incremento, p es igual a a + 6 y la siguiente prueba detiene el bucle. Utilizar <= permitiría que el cuerpo accediera a esa posición que no es un elemento.

Para las reglas de la aritmética, un objeto individual actúa como un array de un elemento. Puedes formar &x + 1; no puedes utilizarlo para leer una supuesta variable siguiente.

Comparar solo posiciones pertinentes

Los operadores de orden como < comparan posiciones dentro de un array. No ordenes punteros a objetos no relacionados. La igualdad y la desigualdad pueden comparar tipos de puntero compatibles y comprobar si un puntero es nulo.

No puedes sumar dos punteros, multiplicar punteros ni realizar aritmética de C estándar con un void * o un puntero a función. Para encontrar el punto medio de un intervalo [p, q) dentro de un mismo array, utiliza p + (q - p) / 2: resta las posiciones para obtener una cantidad, divide esa cantidad y luego avanza desde p.

Puntos clave

  • Sumar un entero desplaza un puntero por elementos de su tipo apuntado.
  • Una diferencia entre punteros al mismo array ya cuenta elementos y tiene tipo ptrdiff_t.
  • Los punteros a la posición posterior al final pueden marcar un límite, pero no pueden desreferenciarse.
  • Formar un puntero anterior al array o más allá de la posición posterior al final no es válido.
  • Utiliza posiciones del mismo array para las comparaciones de orden.
  • Resta las posiciones antes de calcular un punto medio.

3.2 Expresiones de array y parámetros de tipo puntero

Un array es un bloque de elementos. Un puntero es un objeto independiente que contiene una dirección. Su estrecha relación procede de una conversión automática que ocurre en la mayoría de las expresiones, no de que los dos objetos sean del mismo tipo.

Convertir una expresión de array
int a[5] = {3, 1, 4, 1, 5};
int *p = a;

Aquí a se convierte en un puntero a a[0]. Esta conversión de array a puntero hace que el inicializador sea equivalente a &a[0]. Tanto *p como a[0] leen 3.

C define el acceso mediante índices a través de la aritmética: a[i] significa *(a + i). Por tanto, p[3] también funciona y lee 1. &a[i] y a + i identifican el mismo elemento. Incluso 3[a] tiene el mismo significado definido que a[3], aunque la forma habitual es mucho más clara.

Saber cuándo el array completo sigue siendo un array

La conversión no ocurre para los operandos de sizeof o del & unario. Tampoco ocurre para typeof y typeof_unqual de C23, ni cuando un literal de cadena inicializa un array de caracteres.

Expresión, con valores int de 4 bytes y un puntero de 8 bytesResultado
sizeof a20 bytes para los cinco elementos
sizeof p8 bytes para el objeto de tipo puntero
sizeof a / sizeof a[0]5 elementos

No puedes asignar una nueva dirección a un array ni incrementar su nombre: a = p; y a++; no son válidos. Puedes redirigir p, por ejemplo, con p = a + 2;. Copia el contenido del array con un bucle sobre los elementos o con una llamada adecuada a memcpy.

Seguir el tipo ajustado del parámetro
int sum(const int *values, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) total += values[i];
    return total;
}

La función llamadora proporciona tanto una dirección como una cantidad. Para el array anterior, sum(a + 2, 3) visita los índices originales 2, 3 y 4, y suma 4, 1 y 5 para devolver 10. Dentro de la función, el índice 0 es relativo al puntero inicial proporcionado.

En una lista de parámetros de función, int a[], int a[10] e int *a declaran un parámetro de tipo puntero. La forma simple [10] no proporciona un límite comprobado ni transmite una longitud. Dentro de una función así, sizeof a mide un puntero. La expresión sizeof a / sizeof a[0] daría 2 en un sistema con punteros de 8 bytes y valores int de 4 bytes, independientemente de la longitud real del array de la función llamadora.

Distinguir un puntero a elemento de un puntero a array

a se convierte en int *, mientras que &a tiene el tipo int (*)[5], un puntero al array completo de cinco elementos. Ambos identifican el comienzo del mismo almacenamiento, pero la aritmética sigue sus distintos tipos. a + 1 avanza un int, o 4 bytes en esta disposición. &a + 1 avanza un array completo, o 20 bytes, hasta su posición inmediatamente posterior al final.

Una dirección inicial impresa no puede revelar esa diferencia de tipos. Lee la declaración y mantén el tipo apuntado junto al puntero al seguir una expresión.

Puntos clave

  • La mayoría de las expresiones de array se convierten en un puntero al primer elemento.
  • a[i] se define como *(a + i).
  • sizeof a mide el array completo solo donde a es realmente un array.
  • Un parámetro de tipo array se ajusta a un puntero; pasa su longitud utilizable por separado.
  • &a apunta a un array completo, por lo que su aritmética tiene un paso mayor.

3.3 Bucles con punteros sobre números y texto

Un bucle con puntero necesita una posición actual y una regla de parada. Un array con un número conocido de elementos utiliza un puntero al final; una cadena utiliza su '\0' de terminación. Elige la forma que permita ver con mayor facilidad los elementos visitados.

Recorrer un intervalo de longitud conocida
int a[] = {3, 1, 4, 1, 5};
int total = 0;
for (const int *p = a; p < a + 5; p++) {
    total += *p;
}

Los totales acumulados son 3, 4, 8, 9 y 14. Un bucle con índice que suma a[i] para índices menores que 5 da el mismo resultado. Los compiladores optimizadores modernos suelen generar código similar para estas formas; la sintaxis de punteros por sí sola no garantiza ninguna ventaja de velocidad.

Leer antes o después de avanzar

Con p en el 3 de {3, 1, 4}, int v = *p++; asigna 3 a v y deja p en el 1. El incremento posfijo proporciona el valor anterior del puntero a * y actualiza el puntero. *++p primero avanza al siguiente elemento y luego lo lee. (*p)++ modifica el elemento actual y no desplaza el puntero.

Detenerse en el terminador de una cadena
size_t my_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0') p++;
    return (size_t)(p - s);
}

Para "pointer", p se detiene en el terminador, siete posiciones desde el inicio. El resultado excluye ese terminador. Un recuento de caracteres utiliza el mismo recorrido: examinar "mississippi" e incrementar un contador cada vez que *p == 's' cuenta cuatro coincidencias entre once letras.

Un puntero al interior de una cadena da acceso a su sufijo. Con const char *msg = "hello world";, puts(msg + 6) imprime "world". Los caracteres anteriores permanecen donde estaban.

Copiar también el terminador
char destination[4];
const char *src = "cat";
char *dst = destination;
while ((*dst++ = *src++) != '\0') {
}

Cada iteración asigna un carácter, comprueba el valor asignado y avanza ambos punteros. La última iteración copia '\0', por lo que el destino es una cadena completa. Ambos punteros quedan entonces justo después de sus terminadores. El almacenamiento necesita la longitud del texto más un byte; omitir ese byte adicional provoca un desbordamiento de búfer. Este patrón de copia también requiere que las regiones de origen y destino no se solapen.

Invertir desde ambos extremos

Para una cadena modificable, obtén primero su longitud. Si la longitud es cero, detente antes de formar un puntero al último carácter. De lo contrario, establece left = s y right = s + length - 1. Mientras left < right, intercambia los caracteres, incrementa left y decrementa right. Para "stressed", cuatro intercambios producen "desserts"; el terminador permanece en el extremo original.

La comprobación de cadena vacía importa porque restar 1 de su puntero inicial formaría un puntero anterior al array.

Elegir almacenamiento de caracteres modificable

char word[] = "hello"; crea un array modificable de seis bytes, incluido el terminador. const char *word = "hello"; apunta a un literal de cadena con duración estática del almacenamiento. Modificar un literal de cadena es comportamiento indefinido, incluso si una declaración utiliza char * sin const. Utiliza const char * para que las escrituras accidentales sean un error de compilación.

Un bucle sobre un array debe detenerse antes de la posición inmediatamente posterior a su final, y un recorrido de cadena debe encontrar realmente un terminador dentro del almacenamiento accesible. Un puntero no lleva consigo ni una longitud ni una garantía de que los bytes formen una cadena.

Puntos clave

  • Un intervalo de longitud conocida termina en un puntero a la posición posterior al final; una cadena termina en su carácter nulo.
  • *p++ utiliza la posición anterior y avanza el puntero.
  • La copia de cadenas incluye el terminador y necesita tantos bytes como la longitud más uno.
  • Comprueba si una cadena está vacía antes de formar su puntero al último carácter.
  • Los arrays de caracteres inicializados con texto son modificables; los literales de cadena no lo son.
  • Un puntero por sí solo no proporciona una longitud ni garantiza la terminación.
4

Seguir y actualizar otro puntero

Un puntero puede ser a su vez el objeto al que se accede mediante una dirección. Seguir dos niveles permite que una función sustituya el puntero de la función llamadora y que un programa recorra un array de direcciones de cadenas. Ten presente el tipo de cada nivel: un puntero a un entero, un puntero a ese puntero y un puntero a una fila completa representan disposiciones distintas. Sigue el objeto al que llega cada desreferenciación antes de determinar qué modificará una asignación.

4.1 Dos niveles de indirección

Un puntero es un objeto almacenado, por lo que tiene una dirección que otro puntero puede contener. Un puntero a puntero añade un paso más entre la expresión y los datos finales.

Seguir las flechas de una en una
int x = 7;
int *p = &x;
int **pp = &p;

Utiliza direcciones ilustrativas: x en 0x2000 contiene 7; p en 0x3000 contiene 0x2000; pp en 0x4000 contiene 0x3000. Al evaluar *pp se llega a p, cuyo valor tiene tipo int *. Al evaluar **pp se sigue ese puntero hasta x, un int.

ExpresiónTipoObjeto o valor alcanzado
ppint **Dirección de p
*ppint *El puntero almacenado en p
**ppintEl entero almacenado en x

&p tiene un nivel de puntero más que p; cada desreferenciación elimina un nivel. Inicializar int **pp con &x produce una incompatibilidad porque &x tiene tipo int *.

Elegir el nivel de asignación

Con otro entero existente y, **pp = 9; modifica x. La asignación *pp = &y; modifica el puntero p, por lo que los usos posteriores de **pp llegan a y. Deja x con su valor actual. Si q es otro int *, pp = &q; modifica solo el puntero exterior, haciendo que su primera flecha llegue a q en lugar de p.

No cuentes los asteriscos sin comprobar también el objeto de cada nivel. Antes de evaluar **pp, tanto la ruta hasta el objeto de tipo puntero como la ruta desde ese objeto hasta el entero deben ser válidas.

Desplazar un puntero a cadena de la función llamadora
void skip_spaces(const char **s) {
    while (**s == ' ') {
        (*s)++;
    }
}

/* In the caller: */
const char *text = "   hi";
skip_spaces(&text);
puts(text);

La función llamadora pasa la dirección de su variable de tipo puntero. Por tanto, *s designa text; **s lee el carácter actual. Cada (*s)++ avanza text un carácter. Después de tres iteraciones apunta a 'h' y puts imprime "hi". La función requiere un puntero válido a un puntero que apunte a una cadena terminada en nulo.

Una versión que recibe const char *s e incrementa s modifica solo una copia local. Los paréntesis en (*s)++ también son imprescindibles: *s++ incrementa el puntero local exterior en lugar del puntero a carácter de la función llamadora. En const char **s, los caracteres son de solo lectura mediante esta vía de acceso, pero el puntero de la función llamadora puede desplazarse.

Leer un puntero de salida de la biblioteca

strtol de <stdlib.h> utiliza un parámetro de salida de tipo char **:

char *end;
long number = strtol("42abc", &end, 10);

Devuelve 42 y almacena en end un puntero al primer carácter no utilizado, que ahora apunta a "abc". Si el puntero al final es igual al puntero inicial, no se convirtió ningún dígito. Un analizador numérico que realice todas las comprobaciones también gestiona los errores de rango; aquí la función del puntero es indicar la posición de parada.

Puntos clave

  • int **pp puede contener la dirección de un objeto de tipo int *.
  • Cada desreferenciación sigue un nivel de puntero.
  • **pp modifica el entero final; *pp puede modificar el puntero intermedio.
  • Actualizar un puntero de la función llamadora requiere la dirección de ese puntero.
  • strtol utiliza char ** para devolver la posición del primer carácter no utilizado.

4.2 Arrays de punteros a cadenas y arrays rectangulares

Un array puede almacenar punteros como elementos. Un array bidimensional, en cambio, almacena filas completas como elementos. Ambos admiten dos índices, pero su almacenamiento y los tipos de puntero a los que se convierten son distintos.

Almacenar direcciones de cadenas independientes
const char *days[] = {"Mon", "Tue", "Wed"};

days contiene tres punteros a caracteres. Cada uno apunta a un literal de cadena independiente. days[1] es el puntero a "Tue", y days[1][0] lee 'T'. Con punteros a objetos de 8 bytes, sizeof days es 24, independientemente de las longitudes de las cadenas. Las cadenas ocupan almacenamiento en otro lugar.

Las longitudes pueden diferir, lo que produce una colección irregular. Reordenar la lista puede intercambiar elementos de tipo puntero sin copiar el texto. Asignar days[0] = "Thursday"; está permitido: el elemento de tipo puntero puede cambiar aunque los caracteres a los que se accede mediante él sean de solo lectura.

En la mayoría de las expresiones, days se convierte en un puntero a su primer elemento. Como un elemento es un const char *, el tipo convertido es const char **:

for (const char **d = days; d < days + 3; d++) {
    puts(*d);
}

El bucle avanza un elemento de tipo puntero por iteración. *d obtiene el puntero a carácter que necesita puts.

Leer cadenas de la línea de comandos

int main(int argc, char *argv[]) recibe el mismo tipo de parámetro que char **argv. argc no es negativo y cuenta las cadenas disponibles. Cuando es positivo, argv[0] representa el nombre del programa o una cadena vacía si ese nombre no está disponible. Los argumentos del usuario ocupan los índices desde 1 hasta argc - 1, y argv[argc] es un puntero nulo.

En una invocación típica ./greet Ada 42, argc es 3, argv[1] apunta a "Ada" y argv[2] apunta a "42". El último argumento es texto hasta que lo lee una conversión como strtol. argv[1][0] es 'A'. Comprueba argc > 2 antes de utilizar argv[2].

Un bucle que imprime los argumentos del usuario puede utilizar for (int i = 1; i < argc; i++) puts(argv[i]);. Para recorrer desde argv + 1 hasta un puntero nulo, comprueba primero argc > 0; si argc es cero, no hay un intervalo de argumentos del usuario que pueda comenzarse de ese modo. Las cadenas de argumentos son modificables, a diferencia de los literales de cadena.

Seguir filas completas

int grid[3][4] contiene doce enteros en orden por filas: toda la fila 0, luego la fila 1 y después la fila 2. Con valores int de 4 bytes, cada fila ocupa 16 bytes. grid[2][1] está 36 bytes después de grid[0][0]: dos filas completas aportan 32 bytes y la columna aporta 4.

Declaración o expresiónSignificado
int (*rows)[4]Puntero a una fila de cuatro enteros
int *ptrs[4]Array de cuatro punteros a enteros
grid convertidoint (*)[4]

grid + 1 avanza una fila. Una función que lo recibe puede utilizar int g[][4] o int (*g)[4], con un número de filas separado. No puede utilizar int **: la tabla no tiene punteros a filas almacenados que puedan desreferenciarse. Una conversión explícita no puede crear esos punteros inexistentes.

Del mismo modo, char names[3][6] dispone de dieciocho posiciones para caracteres en filas de tamaño fijo. const char *names[3] dispone de tres posiciones para punteros que pueden referirse a cadenas de distintas longitudes.

Puntos clave

  • Un array de punteros a cadenas almacena direcciones, con el texto en otro lugar.
  • Un array de punteros se convierte en un puntero a su primer elemento de tipo puntero.
  • argv[argc] es nulo; comprueba el número de argumentos antes de leer una cadena concreta.
  • Un array bidimensional almacena filas contiguas y no contiene objetos de tipo puntero a fila.
  • int (*)[4] e int ** describen disposiciones distintas de almacenamiento.
  • Dos índices por sí solos no demuestran que dos objetos tengan el mismo tipo.
5

Elegir funciones y proporcionar funciones de devolución de llamada

Un puntero a función permite que un programa elija una operación durante su ejecución. Su tipo recoge la llamada permitida y un parámetro de función de devolución de llamada permite que otra función solicite esa operación cuando la necesite. Utiliza estas ideas primero en una pequeña tabla de funciones aritméticas y después en qsort, donde la función de devolución de llamada debe interpretar correctamente las direcciones de los elementos y devolver un orden coherente. Ordenar punteros a cadenas reúne tanto los punteros a funciones como dos niveles de punteros a objetos.

5.1 Declaraciones y llamadas de punteros a funciones

Un puntero a función almacena una referencia a una función a la que se puede llamar más adelante. Su tipo incluye el tipo de retorno y los tipos de los parámetros. Esos tipos deben coincidir con los de la función utilizada mediante el puntero.

Poner paréntesis alrededor del nombre del puntero
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

int (*op)(int, int) = add;

Lee desde op: es un puntero, aquello a lo que apunta es una función que recibe dos valores int, y esa función devuelve un int. Sin los paréntesis, int *op(int, int); declara una función que devuelve un puntero a entero. Los paréntesis cambian el significado de la declaración.

El nombre de una función se convierte en un puntero a función en este inicializador. op = &add; es equivalente a op = add;. Tanto op(6, 3) como (*op)(6, 3) llaman a add y devuelven 9. Asignar op = sub; hace que la misma llamada devuelva 3. En la expresión de llamada no ha cambiado nada salvo la función almacenada en el puntero.

Mantener la firma compatible

Un puntero de tipo int (*)(int, int) no puede llamar de forma segura a una función que recibe y devuelve double. Por ejemplo, asignarle sqrt produce un mensaje de diagnóstico por tipos incompatibles. Convertir explícitamente el puntero a función para eliminar un mensaje de diagnóstico no hace válida una llamada incompatible; el comportamiento es indefinido.

C estándar trata por separado los punteros a objetos y los punteros a funciones. void * es el tipo de puntero genérico a objeto, no un contenedor portable para un puntero a función. Los punteros a funciones admiten pruebas de igualdad y desigualdad, pero no aritmética de punteros.

Un puntero nulo a función significa que no hay ninguna función a la que llamar. Cuando una interfaz permite ese estado, compruébalo:

if (op != NULL) {
    int result = op(6, 3);
    printf("%d\n", result);
}

Llamar mediante un puntero nulo a función es comportamiento indefinido.

Dar nombre al tipo una vez
typedef int (*binop)(int, int);
binop ops[] = {add, sub, mul};

El typedef da nombre a un tipo de puntero. binop selected = mul; declara un puntero; binop pick(char symbol); declara una función que devuelve uno. Sin el typedef, esa declaración de retorno sería int (*pick(char symbol))(int, int);.

Un array de punteros a funciones es una tabla de despacho. ops[2](6, 3) selecciona mul y devuelve 18. Para un índice de selección con signo i, comprueba i >= 0 && i < 3 antes de evaluar ops[i]. Acceder fuera de los límites de la tabla no es válido aunque se pueda llamar a todas las funciones almacenadas.

Interpretar declaraciones más amplias hacia fuera

Comienza por el nombre y resuelve los paréntesis y las partes posfijas antes de avanzar más hacia fuera. En int (*table[3])(int, int), table[3] forma un array de tres elementos, * hace que cada elemento sea un puntero y (int, int) hace que su destino sea una función que devuelve int.

En char *(*f)(const char *), f es un puntero a una función que recibe un const char *; esa función devuelve un char *. Mantén separados los requisitos de tiempo de vida del puntero devuelto y los requisitos de compatibilidad de llamada del puntero a función.

Puntos clave

  • Los paréntesis distinguen un puntero a función de una función que devuelve un puntero.
  • Un puntero a función recoge los tipos de retorno y de parámetros de las llamadas válidas.
  • El nombre de una función puede inicializar un puntero a función compatible sin &.
  • Un typedef puede hacer más fáciles de leer las tablas y los tipos de retorno de punteros a funciones.
  • Valida el índice de la tabla y cualquier puntero que pueda ser nulo antes de llamar.

5.2 Funciones de devolución de llamada y comparaciones correctas para qsort

Una función de devolución de llamada es una función proporcionada a otra función, que la llama como parte de su trabajo. Esto separa la operación general de recorrido u ordenación de la acción o el orden concreto que elijas.

Pasar una operación sin llamarla
void twice(int *x) { *x *= 2; }

void for_each(int *a, size_t n, void (*fn)(int *)) {
    for (size_t i = 0; i < n; i++) fn(&a[i]);
}

Para int v[] = {1, 2, 3};, for_each(v, 3, twice); produce {2, 4, 6}. El nombre de la función proporciona un puntero. Escribir twice() intentaría llamarla de inmediato, sin el argumento requerido, en lugar de proporcionar una operación para llamadas posteriores.

Recuperar un tipo desde un puntero genérico a objeto

Un void * puede contener un puntero a objeto y convertirse de nuevo al tipo original. No puede desreferenciarse ni utilizarse en aritmética de punteros de C estándar porque no proporciona un tipo de elemento. Conserva los calificadores: los datos de solo lectura pasan por const void * y después por un puntero adecuado a datos constantes.

qsort de <stdlib.h> recibe una dirección base, un número de elementos, un tamaño de elemento en bytes y una función de comparación. La función de devolución de llamada tiene tipo int (*)(const void *, const void *). Cada argumento apunta a un elemento del array y el signo devuelto indica dónde corresponde el primer elemento:

Signo devueltoOrden
NegativoEl primero antes del segundo
CeroEquivalentes para este orden
PositivoEl primero después del segundo
Ordenar enteros sin desbordamiento
int cmp_int(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);
}

/* In the caller: */
int v[] = {42, -7, 19, 0, 3};
qsort(v, 5, sizeof v[0], cmp_int);

El array pasa a ser {-7, 0, 3, 19, 42}. Las comparaciones producen 0 o 1, por lo que la expresión de retorno solo produce -1, 0 o 1. Invertir la expresión de orden a (y > x) - (y < x) ordena en sentido descendente.

No utilices return x - y; para enteros con signo arbitrarios. Para un int de 32 bits, restar -2 000 000 000 de 2 000 000 000 supera INT_MAX. El desbordamiento con signo tiene comportamiento indefinido, por lo que no se garantiza un resultado reducido modularmente ni un signo de comparación fiable.

Ordenar punteros a cadenas
int cmp_str(const void *a, const void *b) {
    return strcmp(*(const char *const *)a,
                  *(const char *const *)b);
}

const char *names[] = {"Lin", "Ada", "Grace"};
qsort(names, 3, sizeof names[0], cmp_str);

Incluye <string.h> para strcmp. Aquí un elemento del array es a su vez un puntero. La función de devolución de llamada recibe una dirección de ese elemento de tipo puntero, por lo que desreferenciar una vez obtiene la dirección de la cadena. El const interior añadido impide modificar el elemento mediante esta vía de acceso. Después de ordenar, el orden es "Ada", "Grace", "Lin".

strcmp(a, b) leería los bytes de las posiciones de puntero como texto. Utiliza también sizeof names[0], el tamaño de un elemento de tipo puntero, no sizeof(char) ni la longitud de un nombre.

Las funciones de comparación deben dejar los elementos sin cambios y responder de forma coherente, incluido el orden transitivo. qsort no garantiza conservar el orden de los elementos que se consideran iguales al compararlos. bsearch acepta el mismo tipo de función de comparación para buscar en un array ya ordenado según ese criterio.

Puntos clave

  • Pasa el nombre de una función para proporcionar una función de devolución de llamada; los paréntesis llaman a una función inmediatamente.
  • Recupera el tipo de elemento a partir de cada argumento genérico de comparación antes de acceder.
  • qsort necesita un número de elementos, el tamaño de un elemento y una función de comparación compatible.
  • Utiliza los signos de las comparaciones sin una resta que pueda desbordarse.
  • Las funciones de comparación de punteros a cadenas reciben direcciones de posiciones de puntero.
  • Los resultados de comparación iguales no garantizan un orden estable después de qsort.

Ejemplos de Punteros y direcciones de memoria en C

5 Lecciones Interactivas

1

Seguir una dirección y el valor al que llega

13 pasos
Construye una representación de la memoria, redirige punteros y predice qué objeto modifica cada asignación.
  1. 1
    IntroducciónDistinguir tres cosas

    Un entero almacenado tiene un valor, una ubicación y un tiempo de vida. Un puntero añade otro objeto cuyo valor es una dirección. Sigue un programa pequeño registrando ambos tipos de valores: enteros como 10 y direcciones como 0x1000.

    La pregunta útil en cada línea es concreta: ¿esta sentencia cambia una dirección, cambia el objeto al que se llega mediante esa dirección o simplemente lee un valor?

  2. 2
    ExplicaciónDistribuir cuatro enteros

    Imagina que int scores[4] comienza en 0x1000 en un sistema con enteros de 4 bytes. El primer elemento utiliza los bytes de 0x1000 a 0x1003. El siguiente comienza en 0x1004, seguido de 0x1008 y 0x100C.

    Los elementos ocupan almacenamiento consecutivo. Esta regla de disposición pertenece al array; no garantiza ningún orden para dos enteros declarados por separado.

  3. 3
    EjemploMedir el array y después su contenido

    Para char word[20] = "hi";, dos mediciones dan resultados distintos:

    • sizeof word es 20 bytes de almacenamiento del array.
    • strlen(word) es 2 caracteres antes de '\0'.

    El terminador ocupa otro elemento del array y los elementos restantes siguen perteneciendo al array. sizeof devuelve size_t, que se imprime con %zu; no cuenta cuántos elementos utiliza actualmente el programa.

  4. 4
    ConexiónDistinguir la disposición del tiempo de vida

    La alineación determina las direcciones iniciales adecuadas, mientras que el orden de los bytes determina su orden dentro de un valor. Ninguno de los dos indica cuánto tiempo existe un objeto.

    Una variable local ordinaria dura mientras se ejecuta su bloque. Una variable local estática o global dura todo el programa. Guardar una dirección registra una ruta hacia el almacenamiento; no puede mantener activa una variable local automática después de que termine el bloque.

  5. 5
    DesafíoEncontrar la dirección de un elemento

    Un array de enteros de seis elementos comienza en 0x1800, con 4 bytes por elemento. ¿Dónde comienza el elemento 3 y cuál es el tamaño de todo el array?

    ---reveal---

    El elemento 3 comienza doce bytes después del inicio, en 0x180C. Seis elementos de 4 bytes ocupan 24 bytes. El índice da un desplazamiento en elementos; multiplicarlo por el tamaño del elemento da el desplazamiento en bytes.

  6. 6
    ExplicaciónDar un destino al puntero

    Considera int count = 42; int *p = &count;. El operador & obtiene la dirección de count y p almacena esa dirección. Si count está en la dirección ilustrativa 0x2000, el valor del puntero es 0x2000; el valor del entero sigue siendo 42.

    El puntero también ocupa su propio almacenamiento. Su dirección es &p, una información distinta de la dirección contenida en p.

  7. 7
    ConsejoInterpretar el asterisco junto a cada nombre

    int *p, q; declara un puntero y un entero ordinario. Para declarar dos punteros, utiliza int *p, *q; o coloca las declaraciones en líneas separadas.

    Inicializa un puntero con la dirección de un objeto adecuado o con NULL. Un puntero nulo es un estado vacío conocido; un puntero local sin inicializar tiene un valor indeterminado y no puede comprobarse de forma segura como si ya fuera nulo.

  8. 8
    DesafíoRedirigir sin modificar los datos

    Comienza con int a = 9, b = 16; int *p = &a;. Después ejecuta p = &b;. ¿Qué valores enteros cambian y qué contiene ahora p?

    ---reveal---

    Ninguno de los enteros cambia: a sigue siendo 9 y b sigue siendo 16. Solo cambia p, que almacena la dirección de b. La asignación de un puntero sustituye una dirección; no copia el contenido del destino anterior ni del nuevo.

  9. 9
    EjemploLeer, sumar y escribir mediante p

    Continúa con int x = 10; int *p = &x; y ejecuta:

    *p = *p + 5;
    

    El *p del lado derecho lee 10 de x. Al sumar 5 se obtiene 15. El *p del lado izquierdo designa el mismo objeto, por lo que la asignación escribe 15 en x. La dirección de p no ha cambiado. Después, int y = *p * 2; lee 15 y crea un entero independiente y que contiene 30.

  10. 10
    Idea claveUna copia de la dirección crea otra ruta

    Para int n = 6; int *p = &n; int *q = p;, ambos punteros llegan a n. La asignación copió una dirección, no el objeto entero.

    Después de *q = 14;, leer n, *p o *q da 14. Dibuja una caja para el entero y dos flechas hacia ella. Crear dos cajas para enteros sugeriría incorrectamente que son datos independientes.

  11. 11
    ConsejoComprobar el destino antes de acceder

    El acceso necesita un puntero a un objeto existente de un tipo adecuado. Un puntero nulo no designa ningún objeto y un puntero colgante ha perdido su destino existente. Una prueba de nulidad gestiona el estado vacío opcional de un puntero inicializado; no demuestra que un destino no nulo siga existiendo.

    Un acceso no válido tiene comportamiento indefinido. C no garantiza ni un valor concreto ni un cierre inesperado, incluso cuando una ejecución parece normal.

  12. 12
    DesafíoSeguir una dirección copiada

    ¿Cuáles son los valores finales de a y b?

    int a = 6, b = 19;
    int *p = &a, *q = p;
    p = &b;
    *q = *p + 1;
    

    ---reveal---

    a pasa a ser 20 y b sigue siendo 19. Redirigir p deja q apuntando a a. La última línea lee b mediante p y escribe la suma en a mediante q.

  13. 13
    RepasoSeguir el objeto indicado por el lado izquierdo

    Lee el lado izquierdo antes de predecir un cambio. Asignar un valor a p cambia una dirección; asignar un valor a *p modifica su destino. Mantén los punteros copiados como flechas separadas, comprueba el tiempo de vida del destino y utiliza el tipo apuntado para interpretar cada acceso.

2

Hacer que una función modifique datos de la función llamadora

11 pasos
Sigue un intercambio, utiliza parámetros de salida y determina si los punteros devueltos siguen siendo válidos.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónSeguir la dirección dentro de una llamada
  2. 2
    ExplicaciónUna dirección copiada sigue llegando a la función llamadora
  3. 3
    EjemploGuardar el valor antes de reemplazarlo
  4. 4
    EjemploEscribir el segundo valor en la primera posición
  5. 5
    EjemploCompletar el intercambio
  6. 6
    DesafíoEntregar dos resultados
  7. 7
    ExplicaciónColocar const en el nivel deseado
  8. 8
    EjemploDevolver una posición válida
  9. 9
    DesafíoExplicar el fallo de tiempo de vida
  10. 10
    DesafíoElegir una interfaz de búsqueda que permita escribir
  11. 11
    RepasoComprobar la escritura y el tiempo de vida
3

Recorrer un array sin cruzar sus límites

15 pasos
Relaciona la aritmética, la conversión de arrays y los bucles sobre cadenas siguiendo cada posición visitada.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónEncontrar el inicio, el paso y la parada
  2. 2
    ExplicaciónDesplazarse por elementos
  3. 3
    EjemploRestar dos posiciones
  4. 4
    Idea claveUtilizar un límite que no es un elemento
  5. 5
    DesafíoLocalizar el punto medio
  6. 6
    ExplicaciónUna expresión de array proporciona un puntero inicial
  7. 7
    EjemploAplicar sizeof en el lugar correcto
  8. 8
    DesafíoPasar un intervalo más pequeño
  9. 9
    EjemploLeer y luego avanzar, o avanzar y luego leer
  10. 10
    ExplicaciónEncontrar el final de una cadena
  11. 11
    EjemploCopiar un carácter cada vez
  12. 12
    ConsejoReservar la posición del terminador
  13. 13
    DesafíoCorregir la última visita del bucle
  14. 14
    DesafíoElegir una preparación segura para invertir
  15. 15
    RepasoIdentificar cada posición visitada
4

Modificar un puntero y leer un array de punteros

12 pasos
Sigue dos niveles de indirección y después distingue las posiciones de punteros a cadenas de las filas completas de un array.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónHacer que el propio puntero sea un destino
  2. 2
    EjemploDibujar dos flechas
  3. 3
    ExplicaciónElegir qué nivel cambia
  4. 4
    EjemploPasar por dirección la posición de la función llamadora
  5. 5
    DesafíoPredecir un cambio solo en el puntero
  6. 6
    ConexiónLeer la posición de parada de una conversión
  7. 7
    ExplicaciónUn array puede contener posiciones de puntero
  8. 8
    EjemploAvanzar por las direcciones
  9. 9
    ConsejoComprobar el número de argumentos antes de acceder a cadenas
  10. 10
    DesafíoInterpretar el tipo de una fila completa
  11. 11
    DesafíoAdaptar una función al almacenamiento proporcionado
  12. 12
    RepasoNombrar el objeto de cada nivel
5

Elegir una operación y ordenar mediante una función de devolución de llamada

15 pasos
Interpreta tipos de punteros a funciones, selecciona una operación de una tabla y construye funciones de comparación de enteros y de punteros a cadenas.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónSeparar una tarea general de la operación elegida
  2. 2
    ExplicaciónInterpretar el tipo de la función a la que se llama
  3. 3
    EjemploSeleccionar una función distinta
  4. 4
    ConsejoDar nombre a un tipo de puntero para una tabla
  5. 5
    DesafíoInterpretar una declaración relacionada
  6. 6
    ExplicaciónProporcionar una acción como función de devolución de llamada
  7. 7
    EjemploProporcionar los cuatro argumentos de ordenación
  8. 8
    ExplicaciónRecuperar el tipo de elemento entero
  9. 9
    EjemploDevolver el signo del orden
  10. 10
    DesafíoEvitar un atajo que puede desbordarse
  11. 11
    ConexiónOrdenar punteros a cadenas, no filas de caracteres
  12. 12
    DesafíoElegir el tamaño correcto para ordenar
  13. 13
    Idea claveLa igualdad no garantiza estabilidad
  14. 14
    DesafíoSeguir una función de comparación seleccionada
  15. 15
    RepasoHacer que coincidan la llamada, la disposición y el orden

Ejercicios de Punteros y direcciones de memoria en C

40 Preguntas de Práctica en 4 sets

Set 1: Seguir direcciones y llamadas a funciones10 Preguntas de Práctica

Pregunta 1 de 10

Un int ocupa 4 bytes. Un array int levels[7] comienza en la dirección ilustrativa 0x2400. ¿En qué dirección comienza levels[4]?
  1. 0x2404
  2. 0x2410
  3. 0x2414
  4. 0x241C
Mostrar respuestaOcultar respuesta
Respuesta: B
El índice 4 está cuatro elementos después del inicio. Cada uno ocupa 4 bytes, por lo que el desplazamiento es de 16 bytes, escrito 0x10, y la dirección es 0x2410. Sumar solo 4 a la dirección trataría el índice como un número de bytes y llegaría al segundo elemento.

Pregunta 2 de 10

Dado int items = 16;, ¿qué declaración crea un puntero a items?
  1. int *p = items;
  2. int p = &items;
  3. int *p = *items;
  4. int *p = &items;
Mostrar respuestaOcultar respuesta
Respuesta: D
&items obtiene la dirección del entero e int *p declara el tipo de puntero correspondiente. Inicializar el puntero con items proporcionaría el entero almacenado 16 en lugar de la dirección de un objeto entero. Obtener una dirección deja el valor de items sin cambios.

Pregunta 3 de 10

¿Qué valor contiene n después de este código?

int n = 14;
int *p = &n;
*p = 23;
  1. 23
  2. 14
  3. 37
  4. 0
Mostrar respuestaOcultar respuesta
Respuesta: A
p contiene la dirección de n, por lo que *p designa ese mismo entero. Asignar 23 mediante *p reemplaza el valor de n. Mantener 14 supondría confundir una escritura mediante el puntero con un cambio de la dirección almacenada en el propio puntero.

Pregunta 4 de 10

¿Cuál es el valor de score después de que retorne clear(score)?

void clear(int value) { value = 0; }
/* In the caller: */
int score = 27;
clear(score);
  1. 0
  2. 1
  3. 27
  4. Comportamiento indefinido
Mostrar respuestaOcultar respuesta
Respuesta: C
La función recibe una copia local del argumento entero. Asignar cero a value modifica ese parámetro, por lo que score de la función llamadora sigue siendo 27. Para obtener cero haría falta una función que recibiera &score mediante un puntero a entero y escribiera en el objeto apuntado.

Pregunta 5 de 10

En un sistema con punteros a objetos de 8 bytes, ¿qué imprime esto?

char label[12] = "oak";
char *p = label;
printf("%zu %zu
", sizeof label, sizeof p);
  1. 4 8
  2. 12 8
  3. 12 12
  4. 3 8
Mostrar respuestaOcultar respuesta
Respuesta: B
label es un array de doce caracteres, por lo que sizeof label es 12 aunque el texto almacenado solo tenga tres letras. p es un objeto de tipo puntero independiente y ocupa 8 bytes en este sistema. Contar el terminador da la parte inicial ocupada por la cadena, no el tamaño declarado del array.

Pregunta 6 de 10

¿Cuáles son los valores de left y right después de la llamada?

void exchange(int *a, int *b) {
    int *t = a;
    a = b;
    b = t;
}
/* In the caller: */
int left = 11, right = 29;
exchange(&left, &right);
  1. 29, 11
  2. 29, 29
  3. 11, 11
  4. 11, 29
Mostrar respuestaOcultar respuesta
Respuesta: D
La función intercambia sus copias locales de los punteros sin asignar valores a *a ni a *b. No se escribe en ningún objeto entero, por lo que left sigue siendo 11 y right sigue siendo 29. Esperar que los valores se intercambien confunde cambiar los valores de las direcciones locales con copiar los enteros a los que se llega mediante esas direcciones.

Pregunta 7 de 10

Una función se declara int read_count(int *out);. Devuelve 1 si tiene éxito y escribe el recuento solo en ese caso. ¿Qué código de la función llamadora imprime de forma segura un recuento recién leído?
  1. int n; if (read_count(&n)) printf("%d", n);
  2. int n; read_count(&n); printf("%d", n);
  3. int n; if (read_count(n)) printf("%d", n);
  4. int *n; if (read_count(n)) printf("%d", *n);
Mostrar respuestaOcultar respuesta
Respuesta: A
La función llamadora proporciona un objeto entero mediante su dirección y utiliza su valor solo después de que el estado de éxito sea verdadero. Una impresión incondicional podría leer un n sin inicializar si la función falla sin escribir. Un puntero sin inicializar no proporciona un objeto de salida válido que la función pueda rellenar.

Pregunta 8 de 10

Dado int x = 6, y = 17; const int *p = &x;, ¿qué asignación está permitida?
  1. *p = y;
  2. (*p)++;
  3. p = &y;
  4. *p += 1;
Mostrar respuestaOcultar respuesta
Respuesta: C
p es un puntero a datos enteros constantes, por lo que puede redirigirse hacia y. La asignación mediante *p se rechaza porque esa vía de acceso es de solo lectura. La restricción afecta al acceso mediante este puntero; el propio x se definió sin const y aún puede modificarse por su nombre.

Pregunta 9 de 10

¿Cuáles son los valores finales de a y b?

int a = 4, b = 13;
int *p = &a, *q = p;
*p += 3;
p = &b;
*q = *p - 2;
*p = *q + 5;
  1. 7, 12
  2. 11, 13
  3. 7, 18
  4. 11, 16
Mostrar respuestaOcultar respuesta
Respuesta: D
La primera escritura hace que a sea igual a 7, y redirigir p deja q apuntando a a. La siguiente asignación establece a en 11 utilizando el valor 13 de b; después, la última escritura establece b en 16. Mantener a en 7 desplaza incorrectamente q cuando solo se redirigió p.

Pregunta 10 de 10

¿Qué par imprime la última línea?

int *next_value(void) {
    static int n = 20;
    n += 3;
    return &n;
}
/* In the caller: */
int *first = next_value();
int saved = *first;
int *second = next_value();
printf("%d %d
", saved, *first + *second);
  1. 23 49
  2. 23 52
  3. 26 52
  4. 23 46
Mostrar respuestaOcultar respuesta
Respuesta: B
La primera llamada almacena 23, que se copia en saved. La segunda llamada cambia el mismo entero estático a 26 y ambos punteros devueltos llegan a él, por lo que sus valores suman 52. El resultado 49 trata el primer puntero como si conservara una instantánea, pero solo saved contiene un entero copiado independiente.
Set 2: Razonar sobre intervalos de arrays y cadenas10 Preguntas de Práctica

Guarda esta unidad para ver todas las respuestas y practicar cada set.

Pregunta 1 de 10

¿Qué imprime puts(message + 5) para const char *message = "blue sky";?
  1. sky
  2. sky
  3. blue
  4. ky
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Para double values[8];, ¿cuánto es &values[6] - &values[2]?
  1. 32
  2. 8
  3. 4
  4. 2
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

¿Cuál es el valor de *(p - 2) después de estas sentencias?

int bins[] = {6, 12, 18, 24, 30, 36, 42};
int *p = bins + 1;
p += 4;
  1. 18
  2. 36
  3. 12
  4. 24
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

¿Qué devuelve total(data + 1, 3)?

int total(const int *p, size_t n) {
    int s = 0;
    for (size_t i = 0; i < n; i++) s += p[i];
    return s;
}
/* In the caller: */
int data[] = {9, 4, 7, 2, 8};
  1. 20
  2. 13
  3. 17
  4. 21
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

En un sistema con punteros de 8 bytes y valores int de 4 bytes, ¿qué devuelve count(values)?

size_t count(int a[9]) {
    return sizeof a / sizeof a[0];
}
/* In the caller: */
int values[9] = {0};
  1. 2
  2. 9
  3. 8
  4. 4
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

¿Cuáles son los valores de taken y a[0] después?

int a[] = {8, 15, 22};
int *p = a;
int taken = *p++;
(*p)++;
  1. 15, 9
  2. 8, 9
  3. 8, 8
  4. 9, 8
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Después de este código, ¿cuál es el mayor entero no negativo k para el que puede formarse p + k dentro del rango permitido para el puntero?

int a[8];
int *p = a + 1, *end = a + 8;
p += (end - p) / 2;
end -= 2;
p += end - p;
  1. 3
  2. 2
  3. 1
  4. 4
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Una cadena modificable terminada en nulo se invierte con esta preparación. ¿Qué entrada pone de manifiesto un cálculo de puntero no válido antes de que comience el bucle?

size_t n = strlen(s);
char *left = s;
char *right = s + n - 1;
while (left < right) {
    char t = *left; *left++ = *right; *right-- = t;
}
  1. Una cadena de dos caracteres
  2. Una cadena de un carácter
  3. Una cadena de longitud impar
  4. Una cadena vacía
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Un array int a[6] comienza en la dirección ilustrativa 0x3000 y cada entero ocupa 4 bytes. ¿Qué direcciones representan a + 1 y &a + 1?
  1. 0x3004 y 0x3018
  2. 0x3004 y 0x3004
  3. 0x3018 y 0x3018
  4. 0x3001 y 0x3006
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

¿Cuáles son los valores de count y p - text después de este bucle?

char text[] = "abaca";
char *p = text;
int count = 0;
while (*p != '\0') {
    if (*p++ == 'a') count++;
}
  1. 3, 6
  2. 2, 5
  3. 3, 5
  4. 2, 6
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Set 3: Seguir niveles de punteros y disposiciones de almacenamiento10 Preguntas de Práctica

Guarda esta unidad para ver todas las respuestas y practicar cada set.

Pregunta 1 de 10

¿Cuánto vale *p después?

int a = 18, b = 25;
int *p = &a;
int **pp = &p;
*pp = &b;
**pp -= 4;
  1. 21
  2. 14
  3. 25
  4. 18
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Después de int n = 31; int *p = &n; int **pp = &p;, ¿cuál es el tipo de *pp?
  1. int
  2. int **
  3. int ***
  4. int *
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Con punteros a objetos de 8 bytes, ¿cuánto es sizeof words?

const char *words[] = {"red", "violet", "gold", "teal"};
  1. 20
  2. 32
  3. 24
  4. 8
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Para char *end; long n = strtol("57kg", &end, 10);, ¿a qué apunta end después de la conversión?
  1. Al primer carácter de "57kg"
  2. Al carácter 7 de "57kg"
  3. Al carácter k de "57kg"
  4. Al terminador después de "57kg"
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Un programa recibe la línea de comandos ./paint amber 6 en el entorno alojado habitual. ¿Cuáles son los valores de argc y argv[2][0]?
  1. 2 y 6
  2. 3 y 6
  3. 3 y '6'
  4. 2 y '6'
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

¿Qué tipo de parámetro corresponde al int board[2][5] de una función llamadora después de su conversión habitual de array?
  1. int **board
  2. int (*board)[5]
  3. int *board[5]
  4. int (*board)[2]
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

¿Cuáles son los valores de a, b y *p después de la llamada?

void shift(int **slot, int *next) {
    **slot += 2;
    *slot = next;
    **slot += 3;
}
/* In the caller: */
int a = 5, b = 12;
int *p = &a;
shift(&p, &b);
  1. 7, 15, 15
  2. 10, 12, 12
  3. 7, 15, 7
  4. 5, 15, 15
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

¿Cuál es el desplazamiento en bytes de map[2][3] respecto de map[0][0] y cuál es el tipo de map + 1, para int map[4][6] con enteros de 4 bytes?
  1. 36 bytes e int **
  2. 60 bytes e int **
  3. 36 bytes e int (*)[6]
  4. 60 bytes e int (*)[6]
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

¿Cuál es el valor de word y cuál es el índice de p después?

char word[] = "abcd";
char *p = word;
char saved = *p++;
*p = saved;
p += 2;
*p = 'z';
  1. "aacz", índice 3
  2. "bbcz", índice 3
  3. "aazd", índice 2
  4. "aacd", índice 4
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Una función auxiliar debe devolver un puntero al elemento mayor del array no vacío de la función llamadora, permitiendo que esta lo modifique. ¿Qué diseño cumple ambos requisitos?
  1. Copiar el valor mayor en un int local y devolver su dirección.
  2. Guardar el valor mayor en un int estático y devolver su dirección.
  3. Seguir el índice del elemento mayor y devolver &a[index].
  4. Seguir el valor mayor y devolverlo mediante un const int *.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Set 4: Elegir operaciones y construir funciones de comparación para ordenar10 Preguntas de Práctica

Guarda esta unidad para ver todas las respuestas y practicar cada set.

Pregunta 1 de 10

¿Qué declara int (*filter)(int);?
  1. Una función que recibe int y devuelve int *.
  2. Un puntero a una función que recibe int y devuelve int.
  3. Una función que recibe int * y devuelve int.
  4. Un puntero a una función que recibe int * y devuelve int.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Una función tiene la firma void visit(int *a, size_t n, void (*action)(int *)); y está disponible void mark(int *x);. ¿Cómo proporciona una función llamadora mark para un array data de cinco elementos?
  1. visit(data, 5, mark());
  2. visit(data, 5, *mark(data));
  3. visit(data, 5, mark(data));
  4. visit(data, 5, mark);
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

¿Qué valor se asigna a result?

int difference(int a, int b) { return a - b; }
int product(int a, int b) { return a * b; }
int (*op)(int, int) = difference;
op = product;
int result = (*op)(7, 4);
  1. 28
  2. 3
  3. 11
  4. -3
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Para int readings[8];, ¿qué llamada ordena todos los elementos utilizando una función de comparación compare compatible?
  1. qsort(readings, sizeof readings, 8, compare);
  2. qsort(readings, 8, sizeof readings, compare);
  3. qsort(readings, 8, sizeof readings[0], compare);
  4. qsort(readings, 8, sizeof readings[0], compare());
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

¿Cuáles son los valores de result y calls después de este código?

int calls = 0;
int add_one(int x) { calls++; return x + 1; }
int twice(int x) { calls++; return x * 2; }
/* In a function: */
int (*ops[])(int) = {twice, add_one};
int result = ops[1](ops[0](5));
  1. 12, 2
  2. 11, 1
  3. 12, 1
  4. 11, 2
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Una función de comparación de enteros debe funcionar para todos los valores int representables en orden ascendente. ¿Qué expresión de retorno cumple ese requisito después de leer los elementos en x e y?
  1. (x > y) - (x < y)
  2. (x < y) - (x > y)
  3. x - y
  4. x > y
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Para un array const char *labels[], ¿qué expresión obtiene la cadena correspondiente al parámetro de comparación const void *a antes de llamar a strcmp?
  1. (const char *)a
  2. (const char *const *)a
  3. *(const char *const *)a
  4. **(const char *const *)a
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Una tabla int (*ops[3])(int, int) tiene funciones válidas en las posiciones 0 y 2, y NULL en la posición 1. Para un entero con signo choice, ¿qué condición permite ejecutar ops[choice](8, 2) de forma segura?
  1. choice < 3 && ops[choice] != NULL
  2. choice >= 0 && choice < 3 && ops[choice] != NULL
  3. ops[choice] != NULL && choice >= 0 && choice < 3
  4. choice >= 0 && choice < 3
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Una función de comparación ordena enteros por valor absoluto y devuelve cero para valores opuestos de igual magnitud. Ordena {4, -4, 2, -2} con qsort. ¿Qué resultado está garantizado?
  1. {-2, 2, -4, 4} exactamente en ese orden.
  2. {2, -2, 4, -4} exactamente en ese orden.
  3. {-4, 4, -2, 2} exactamente en ese orden.
  4. Los dos valores de magnitud 2 preceden a los de magnitud 4.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Una lista se ordena por longitud creciente de cadenas mediante este cuerpo de función de devolución de llamada. ¿Cuál es el defecto?

const char *x = *(const char *const *)a;
const char *y = *(const char *const *)b;
size_t nx = strlen(x), ny = strlen(y);
return nx < ny;
  1. Devuelve un valor positivo para texto más corto y cero para texto más largo.
  2. Lee las longitudes de las cadenas de los bytes de las posiciones de puntero.
  3. Modifica las cadenas mientras mide sus longitudes.
  4. Compara direcciones de caracteres en lugar de cantidades de caracteres.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

¿Te atascas con una pregunta tuya? Ayuda con Tareas

Términos Clave

60 Términos Clave, cada uno con su definición

A

Ajuste de un parámetro de tipo array a puntero
El ajuste de un parámetro de tipo array a puntero transforma una declaración de array en una lista de parámetros de función en un parámetro de tipo puntero, sin copiar la longitud del array.
Un parámetro escrito int a[10] es un puntero dentro de la función, por lo que sizeof a mide ese puntero.
Alineación
La alineación es la restricción de un tipo sobre las direcciones iniciales adecuadas para sus objetos, que se gestiona automáticamente en las declaraciones ordinarias.
Una disposición habitual con enteros alineados a cuatro bytes sitúa los elementos sucesivos de un array en direcciones divisibles entre cuatro.
argc
argc es el número no negativo de argumentos que se pasa a main en un entorno alojado; cuenta las cadenas proporcionadas mediante argv, incluido el nombre del programa cuando está presente.
Antes de leer argv[2], comprueba que argc > 2 para que la cadena de argumento solicitada exista realmente.
argv
argv es la lista de punteros a caracteres que se pasa a main en un entorno alojado, con un puntero nulo en el índice argc y cadenas de argumentos del usuario que comienzan en el índice 1.
Para una invocación habitual ./sum 8 13, argv[2] llega a "13"; el argumento es texto antes de la conversión numérica.
Aritmética de punteros
La aritmética de punteros desplaza un puntero por cantidades de elementos de su tipo, con resultados limitados a su array y a la posición inmediatamente posterior al último elemento.
Sumar 2 a un puntero a entero lo desplaza dos posiciones de entero; no requiere que la función llamadora ajuste el desplazamiento con sizeof.
Array bidimensional
Un array bidimensional es un array cuyos elementos son arrays de tamaño fijo, que almacena filas completas contiguas en lugar de punteros a filas separados.
int grid[2][5] se convierte en int (*)[5]; pasarlo como int ** describiría una disposición distinta.
Array de punteros
Un array de punteros almacena valores de dirección independientes como elementos, mientras que los objetos apuntados se mantienen en otras ubicaciones de almacenamiento.
Una lista de cuatro punteros a cadenas tiene cuatro posiciones de dirección, incluso cuando las cadenas tienen longitudes muy distintas.
Array irregular
Un array irregular es una colección cuyas filas pueden tener distintas longitudes, representada habitualmente en C mediante punteros a filas almacenadas por separado.
Los punteros a cadenas pueden referirse a "red" y "violet" sin reservar filas de caracteres del mismo tamaño para ambas palabras.

B

Byte
Un byte es la unidad de almacenamiento medida por sizeof; un char de C siempre ocupa un byte, aunque los tamaños de otros tipos dependen del sistema.
En un sistema de destino con enteros de cuatro bytes, un array de ocho enteros ocupa 32 bytes antes de declarar cualquier puntero.

C

Carácter nulo
El carácter nulo es el carácter de valor cero, escrito '\0', que termina una cadena de C dentro de su almacenamiento accesible de caracteres.
Un bucle de longitud se detiene en el terminador, mientras que un bucle de copia completa también debe escribir ese carácter final.
Comportamiento indefinido
El comportamiento indefinido significa que el estándar de C no impone requisitos sobre el resultado de una operación, incluido si el programa se detiene o continúa.
Desreferenciar un puntero nulo tiene comportamiento indefinido aunque una máquina concreta informe de un fallo de segmentación.
Conversión de array a puntero
La conversión de array a puntero convierte una expresión de array en un puntero a su primer elemento en la mayoría de los contextos de expresión, sin copiar sus elementos.
int *p = values; inicializa un puntero a values[0]; el array original sigue conteniendo todos sus elementos.

D

Declarador de puntero
Un declarador de puntero utiliza * con un nombre declarado individual para indicar que el objeto nombrado tiene un tipo de puntero.
En int *left, right;, solo left es un puntero; declarar right como puntero necesita otro asterisco.
Desbordamiento de búfer
Un desbordamiento de búfer escribe más allá de los límites de un objeto de destino, por ejemplo, cuando una copia de cadena no tiene espacio para su carácter nulo final.
Una palabra de tres letras requiere cuatro posiciones de caracteres; un destino de tres bytes no puede contener su terminador.
Desbordamiento de enteros con signo
El desbordamiento de enteros con signo ocurre cuando un resultado aritmético con signo está fuera del rango representable de su tipo, lo que produce comportamiento indefinido en C.
Una función de comparación de enteros que utiliza x - y puede desbordarse aunque cada entrada por separado quepa en un int.
Desreferenciación
La desreferenciación aplica el * unario a un puntero para designar el objeto apuntado, lo que permite una lectura o escritura adecuada mediante esa ruta.
Después de int *p = &score;, asignar *p = 28; modifica el mismo entero que después se lee mediante score.
Diferencia entre punteros
Una diferencia entre punteros es el número de elementos con signo obtenido al restar dos posiciones de un mismo array, incluida la posición válida inmediatamente posterior al último elemento.
&a[5] - &a[2] da 3 elementos, independientemente de cuántos bytes ocupe un elemento en ese sistema.
Dirección de memoria
Una dirección de memoria identifica un byte de almacenamiento; la dirección de un objeto identifica el primer byte del almacenamiento que ocupa.
Si un entero de cuatro bytes comienza en la dirección ilustrativa 0x2000, sus bytes restantes siguen hasta 0x2003.
Duración automática del almacenamiento
La duración automática del almacenamiento hace que un objeto local ordinario exista durante la ejecución de su bloque, y su almacenamiento termina cuando la ejecución sale de él.
Un resultado local ordinario de una función auxiliar deja de existir al retornar, aunque la función haya copiado su dirección en un puntero.
Duración del almacenamiento
La duración del almacenamiento describe cuánto dura el almacenamiento de un objeto, como una ejecución de bloque para una variable local automática ordinaria.
Un puntero guardado de un bloque interior no puede mantener activo después el entero local ordinario de ese bloque.
Duración estática del almacenamiento
La duración estática del almacenamiento hace que el almacenamiento de un objeto dure todo el programa; una variable local estática conserva su valor entre llamadas.
Dos punteros devueltos al mismo objeto estático de resultado ven el mismo valor actualizado después de que otra llamada escriba en él.

F

Fallo de segmentación
Un fallo de segmentación es un acceso no válido a memoria señalado por el sistema que suele detener un programa, aunque un acceso no válido en C no tiene por qué producirlo.
Una desreferenciación nula puede detenerse con un fallo de segmentación; otra escritura mediante un puntero no válido puede corromper datos sin avisar.
Firma de función
Una firma de función describe los tipos de retorno y parámetros que deben ser compatibles con el tipo de puntero utilizado para llamar a la función.
Un puntero para dos parámetros enteros no puede llamar de forma segura a una función que espera un double, ni siquiera mediante una conversión explícita.
Función de comparación
Una función de comparación ordena un par de elementos mediante un resultado negativo, cero o positivo para indicar que el primero va antes, es equivalente o va después.
Devolver (x > y) - (x < y) ordena enteros de forma ascendente sin restar los propios valores.
Función de devolución de llamada
Una función de devolución de llamada es una función proporcionada a otra para que la invoque cuando su trabajo general requiera una acción o decisión elegida.
Un recorrido acepta un puntero a una acción y lo llama con la dirección de cada elemento para actualizarlo.

I

Incremento posfijo
El incremento posfijo proporciona el valor anterior de un objeto a la expresión que lo contiene e incrementa ese objeto, como en el avance del puntero de *p++.
int x = *p++; lee el elemento de la posición anterior y deja p apuntando a la posición siguiente.

L

Límites de un array
Los límites de un array determinan qué índices de elementos existen y por dónde puede desplazarse un puntero; solo se permite una posición adicional para marcar el final.
Formar a - 1 para un puntero al inicio de un array no es válido, aunque el programa nunca lea mediante él.
Literal de cadena
Un literal de cadena es texto entre comillas en C que proporciona un array con duración estática del almacenamiento cuyos caracteres no deben modificarse.
const char *p = "oak"; expone el literal mediante una vía de solo lectura, mientras que char a[] = "oak"; crea una copia modificable.

M

Marco de pila
Un marco de pila es el almacenamiento que una implementación típica utiliza para los datos de trabajo de una llamada a función, incluidos muchos objetos locales automáticos.
Retornar de una función permite reutilizar el almacenamiento de su marco anterior; una dirección local guardada no puede conservarlo.

N

Nivel de indirección
Un nivel de indirección es un paso de puntero en una cadena de direcciones, que se sigue mediante una desreferenciación y se añade al obtener la dirección de un objeto.
Para int **pp, la primera desreferenciación da un int * y la segunda llega a un objeto entero.

O

Operador de indexación
El operador de indexación accede a un elemento al definir a[i] como *(a + i), utilizando el tipo de elemento del puntero para ajustar la posición.
Si p apunta al elemento original 2, entonces p[1] accede al elemento original 3 del mismo array.
Operador de obtención de dirección
El operador de obtención de dirección unario & obtiene la dirección de un objeto cuya dirección puede obtenerse o de una función, y produce un tipo de puntero correspondiente.
Para un array de enteros, &values[2] proporciona la dirección del tercer elemento sin modificar su contenido.
Orden de los bytes
El orden de los bytes es el orden de los bytes dentro de un valor de varios bytes; los sistemas con el byte menos significativo primero lo almacenan en primer lugar.
Un valor 0x12345678 de cuatro bytes tiene los bytes 78 56 34 12 en orden de direcciones menores a mayores en un sistema con el byte menos significativo primero.
Orden por filas
El orden por filas almacena todos los elementos de una fila antes de la siguiente en un array rectangular, y el ancho fijo de fila determina los pasos entre filas.
Para int a[3][4], sumar 1 al a convertido avanza una fila completa de cuatro enteros, no un entero.
Ordenación estable
Una ordenación estable conserva el orden relativo original de los elementos que su función de comparación considera iguales; qsort no ofrece esa garantía.
Ordenar nombres solo por longitud puede cambiar el orden de dos nombres de igual longitud aunque su resultado de comparación sea cero.

P

Parámetro de salida
Un parámetro de salida es un parámetro de tipo puntero utilizado para almacenar un resultado en almacenamiento modificable proporcionado por la función llamadora, además de cualquier valor de retorno.
Una función auxiliar de división puede rellenar enteros separados para el cociente y el resto mientras devuelve un estado de éxito.
Parámetro de tipo puntero
Un parámetro de tipo puntero es un puntero local de una función inicializado a partir de un argumento, que da a la función acceso a un objeto mediante su dirección.
Una función auxiliar de restablecimiento recibe &score mediante int *out y escribe cero en el entero de la función llamadora utilizando *out.
Paso por referencia
El paso por referencia en C suele nombrar la técnica de pasar una dirección y modificar su destino mientras la propia dirección se copia.
Un intercambio llamado con &x y &y modifica variables de la función llamadora escribiendo mediante los dos parámetros locales de tipo puntero.
Paso por valor
El paso por valor proporciona a un parámetro de función una copia del valor de su argumento, incluso cuando ese valor es un puntero al almacenamiento de la función llamadora.
Asignar un valor a un parámetro local int deja sin cambios el entero de la función llamadora; escribir mediante una dirección copiada puede modificarlo.
Precedencia de operadores
La precedencia de operadores determina cómo se agrupan los operadores cuando los paréntesis no especifican la agrupación, antes de evaluar la expresión resultante.
El ++ posfijo tiene mayor precedencia que el * unario, por lo que *p++ se agrupa como *(p++), no como (*p)++.
ptrdiff_t
ptrdiff_t es el tipo entero con signo utilizado para la resta de punteros; una resta definida debe tener un resultado representable en ese tipo.
Incluye <stddef.h> para ptrdiff_t y utiliza %td para imprimir una diferencia entre posiciones válidas del mismo array.
Puntero
Un puntero es un valor con tipo que puede designar un objeto o una función; un puntero a objeto también puede contener un valor nulo o apuntar a la posición inmediatamente posterior al último elemento.
En int *p = &count;, la variable de tipo puntero almacena una dirección mientras que el entero conserva su propio valor.
Puntero a datos constantes
Un puntero a datos constantes permite leer su destino, pero rechaza las escrituras mediante ese puntero; el propio puntero aún puede redirigirse.
const int *p = &x; permite p = &y;, mientras que una asignación como *p = 3; se rechaza.
Puntero a función
Un puntero a función designa una función a la que se puede llamar y recoge los tipos compatibles de retorno y parámetros requeridos al llamar mediante él.
int (*op)(int, int) = add; almacena una función de suma a la que después se puede llamar como op(4, 7).
Puntero a la posición inmediatamente posterior al último elemento
Un puntero a la posición inmediatamente posterior al último elemento marca la posición justo después del último elemento de un array y puede servir como límite, pero no desreferenciarse.
Para int a[5], un recorrido puede detenerse cuando su puntero actual sea igual a a + 5, antes de acceder a otro elemento.
Puntero a puntero
Un puntero a puntero designa un objeto de tipo puntero, lo que añade un nivel de indirección antes de que el acceso llegue a los datos finales apuntados.
Para int **pp = &p;, asignar *pp = &y; redirige p, mientras que asignar **pp = 8; modifica su destino.
Puntero a un array
Un puntero a un array designa un array completo como tipo de elemento, por lo que sumar 1 avanza el tamaño de almacenamiento de ese array completo.
Para int a[4], &a tiene tipo int (*)[4]; su paso aritmético es distinto del de &a[0].
Puntero al final de la conversión
Un puntero al final de la conversión registra dónde se detuvo una conversión de texto, lo que permite a la función llamadora examinar el sufijo no consumido independientemente del resultado numérico.
Convertir "73ms" con strtol puede devolver 73 y hacer que el puntero al final de la función llamadora apunte a la m.
Puntero colgante
Un puntero colgante es un puntero cuyo destino anterior ha terminado su tiempo de vida, por lo que no puede utilizarse para acceder posteriormente a ese objeto.
Devolver la dirección de una variable local automática deja un puntero colgante cuando termina la llamada a la función.
Puntero constante
Un puntero constante es un objeto de tipo puntero cuya dirección almacenada no puede cambiarse después de la inicialización, aunque su destino siga siendo modificable.
Con int *const p = &x;, *p = 3; está permitido, pero p = &y; no puede redirigir el puntero.
Puntero genérico a objeto
Un puntero genérico a objeto utiliza void *, o una forma con calificador, para contener una dirección de objeto que debe recuperar un tipo adecuado antes del acceso.
Una función de comparación de enteros convierte const void * en const int * antes de leer el elemento del array cuya dirección recibió.
Puntero nulo
Un puntero nulo es un valor de puntero que no designa ningún objeto ni función y puede representar un resultado vacío opcional o una función de devolución de llamada ausente.
Una búsqueda de elementos devuelve NULL cuando no hay coincidencia, por lo que la función llamadora comprueba el resultado antes de desreferenciarlo.

Q

qsort
qsort es la función de biblioteca estándar que reorganiza un array utilizando su número de elementos, el tamaño de cada elemento y una función de comparación proporcionada.
Ordenar elementos de tipo puntero a cadena requiere sizeof names[0] para el tamaño de la posición, no el número de caracteres de una cadena.

S

Segmento de un array
Un segmento de un array puede proporcionarse en C como un puntero inicial más una cantidad que describe una parte consecutiva seleccionada de un array existente.
Pasar data + 2 con un número de elementos de 3 selecciona los índices originales del 2 al 4 sin copiarlos.
sizeof
El operador sizeof indica el tamaño de almacenamiento de un tipo u objeto en bytes y devuelve un valor del tipo entero sin signo size_t.
Para char code[10] = "go";, sizeof code indica 10 aunque el texto solo tenga dos caracteres.
strtol
strtol es la función de biblioteca estándar que convierte texto numérico inicial a long y puede indicar su posición de parada mediante char **.
Pasar &end permite que strtol reemplace el puntero a carácter de la función llamadora por la dirección del primer carácter no utilizado.

T

Tabla de despacho
Una tabla de despacho es un array de punteros a funciones que selecciona una operación mediante un índice elegido durante la ejecución del programa.
Un menú de operaciones comprueba su índice de selección antes de utilizarlo para obtener una entrada de la tabla y llamarla.
typedef
Una declaración typedef introduce un nombre para un tipo existente, lo que facilita reutilizar un tipo complejo de puntero a función en declaraciones.
Después de typedef int (*operation)(int);, operation chosen; declara un puntero a función, no el cuerpo de una función.

U

Uso de alias
El uso de alias ocurre cuando distintas expresiones o punteros designan el mismo objeto, de modo que una escritura por una ruta es visible por otra.
Después de int *q = p;, ambos punteros llegan al mismo entero hasta que una asignación posterior redirige uno de ellos.

V

Valor indeterminado de un puntero
Un valor indeterminado de un puntero es el estado sin inicializar de un puntero local ordinario; no proporciona ningún destino conocido que pueda utilizarse.
La declaración int *p; por sí sola no permite que el programa compruebe, siga o escriba mediante ese puntero de forma segura.

Sobre esta unidad

Por StudeliInformar de un error

Crea tus propios Materiales de Estudio

Convierte tus notas, diapositivas y documentos en Guías de Estudio, Preguntas de Práctica, Flashcards y 6 herramientas más.

Comenzar