¡¡¡Problema de programación en lenguaje C muy problemático!!!!!!
1. El concepto básico de punteros
Al igual que otros tipos básicos de variables, un puntero también es una variable, pero es una variable que utiliza una dirección de memoria como valor. Debido a que un puntero generalmente contiene la dirección de una variable que contiene un valor específico, puede hacer referencia a un valor indirectamente.
2. Declaración, inicialización y operadores de variables de puntero
Declaración de declaración
- class="code" width="100%">int *ptra , a; ->
Declara una variable entera a y un puntero ptra que apunta a un valor entero Es decir, usar * (llamado "operador de referencia indirecta") en la declaración de declaración significa La variable es. declarado es un puntero. Se pueden declarar punteros para que apunten a cualquier tipo de datos. Debe enfatizarse que la variable a solo se declara como una variable entera en esta declaración. Esto se debe a que el operador de referencia indirecta * no apunta a todas las variables en una declaración, por lo que cada puntero debe ir precedido de su nombre Prefijo *. El puntero debe inicializarse con una declaración de declaración o una declaración de asignación. El puntero se puede inicializar a 0, NULL o una dirección. Un puntero con un valor de 0 o NULL no apunta a ningún valor. puntero, debe utilizar un único operador & (llamado "operador de dirección").
Por ejemplo, el programa ha utilizado declaraciones de declaración
- class="code" width="100%">int *ptra, a=3 ->
;Declare la variable entera a (valor 3) y el puntero a que apunte al valor entero, luego use la declaración de asignación
- width="100%">ptra=&a; p>
Puede asignar la dirección de la variable a a la variable de puntero ptra. Cabe señalar que el operador & no se puede utilizar en constantes, expresiones o variables cuya clase de almacenamiento esté declarada como registro. El puntero asignado puede obtener el valor del objeto al que apunta a través del operador *. Esto se denomina "referencia múltiple del puntero", como la declaración de impresión
- width="100%">printf. ("% d", *ptra); ->
imprimirá el valor del objeto señalado por la variable puntero ptra (es decir, el valor de a) 3. Si el puntero al que se hace referencia no se inicializa correctamente o no apunta a una ubicación de memoria específica, puede provocar errores de ejecución fatales o modificaciones inesperadas de datos importantes. El especificador de conversión de Printf %p genera la dirección de memoria en forma de un entero hexadecimal. Por ejemplo, después de la asignación anterior, la declaración de impresión
- width="100%">printf("%p", &a );
printf("%p", ptra); ->
imprimirá la dirección de la variable a.
3. La relación entre expresiones de puntero y operaciones aritméticas, así como matrices, cadenas y punteros.
En expresiones aritméticas, expresiones de asignación y expresiones de comparación, los punteros son números de operaciones legales, pero no todos los operadores son legales cuando se usan con variables de puntero. Las operaciones aritméticas limitadas que se pueden realizar en punteros incluyen operaciones de incremento (++), operaciones de decremento (--) y suma de un número entero (+, +=), resta. entero (- o -=) y restando otro puntero.
Cada elemento de la matriz se almacena continuamente en la memoria, que es la base de la aritmética de punteros.
Ahora supongamos que en una máquina donde el número entero ocupa 4 bytes, el puntero ptr se inicializa para apuntar al elemento a[0] de la matriz de enteros a (*** tiene tres elementos), y la dirección de a[0] es 40000, entonces la dirección de cada variable será como se muestra en la siguiente tabla:
- width="89">Expression-> - width="54" bgcolor="#ffffff">ptra -> - ancho="64 " bgcolor="#ffffff">&a[0] -> - ancho="55" bgcolor="#ffffff">&a[1] -> - ancho="93" bgcolor="#ffffff" >&a[2] ->
- width="89">El significado de la expresión-> - width="54" bgcolor="#ffffff">El valor del puntero ptra-> - width="64" bgcolor=" #ffffff">La dirección del elemento a[0]-> - width="55" bgcolor="#ffffff">La dirección del elemento a[1]-> - width="93 " bgcolor="#ffffff">Elemento a La dirección de [2] ->
- width="89">El valor de la expresión-> - width="54" bgcolor="#ffffff ">40000 -> - ancho="64" bgcolor ="#ffffff">40000 -> - ancho="55" bgcolor="#ffffff">40004 -> - ancho="93" bgcolor="#ffffff"> 40008 ->
Debe prestar atención, la aritmética de punteros es diferente de las operaciones aritméticas convencionales. Generalmente, el resultado de 40002 es 40002, pero cuando un puntero suma o resta un número entero, el puntero no simplemente suma. o restar el valor entero, pero suma El producto del número entero y el tamaño del objeto al que hace referencia el puntero, y el tamaño del objeto está relacionado con la máquina y el tipo de datos del objeto. Por ejemplo, en el caso anterior, el resultado de la declaración
- width="100%">ptra+=2 ->
es 40004*2=40008; y ptra también sigue. Apunta al elemento a[2], de manera similar, como la declaración
- width="100%">ptra-=2
ptra++; >
++ ptra;
ptra--;
ptra--; ->
Los principios de operación son los mismos. de punteros, luego obtendrá la cantidad de elementos de la matriz contenidos entre las dos direcciones. Por ejemplo, ptra1 contiene la unidad de almacenamiento 40008, ptra2 contiene la unidad de almacenamiento 40000 y luego la declaración
- width="100%" >x = ptra1 - ptra2; ->
El resultado obtenido es 2 (aún suponiendo que el entero ocupa 4 bytes en memoria). Debido a que no podemos pensar que dos variables del mismo tipo estén almacenadas de forma contigua en la memoria, excepto para los elementos de una matriz, la aritmética de punteros tiene poco significado excepto para las matrices.
Si los dos punteros tienen el mismo tipo, puede asignar un puntero al otro. De lo contrario, debe utilizar un operador de conversión para convertir el tipo del puntero en el lado derecho del operador de asignación al. tipo del puntero en el lado izquierdo del tipo de asignación.
Por ejemplo, ptr1 es un puntero a un número entero y ptr2 es un puntero a un número de punto flotante. Para asignar el valor de ptr2 a ptr1, debe utilizar la instrucción
- width="100%" >ptr1 = (int *) ptr2; ->
para implementar. La única excepción es un puntero al tipo void (es decir, void *), ya que puede representar un puntero de cualquier tipo. Se puede asignar un puntero de cualquier tipo a un puntero para anular, y se puede asignar un puntero a anular a un puntero de cualquier tipo. En ambos casos, no se requiere conversión.
Sin embargo, debido a que el compilador no puede determinar a cuántos bytes se refiere un puntero de tipo void * según el tipo, la referencia múltiple a un puntero void * es un error de sintaxis.
Puede utilizar operadores de prueba de igualdad y operadores relacionales para comparar dos punteros, pero a menos que apunten a elementos en la misma matriz, esta comparación generalmente no tiene sentido. El operador de prueba de igualdad se usa generalmente para determinar si un puntero es NULL, que tiene ciertos usos en las operaciones de memoria que se mencionan más adelante en este artículo.
Las matrices y los punteros en lenguaje C están estrechamente relacionados. Son casi intercambiables. De hecho, el nombre de la matriz puede considerarse como un puntero constante. la declaración de asignación
- width="100%">ptra = a;
para asignar la dirección del primer elemento al puntero ptra que apunta al número entero, luego el siguiente Un conjunto de expresiones son equivalentes:
a[3] ptra[3] *(ptra+3) *(a+3)
Todas representan la cuarta posición en el valor de matriz de elementos.
Y debido a que la cadena en lenguaje C es una matriz de caracteres terminada en un carácter nulo ('\0'), de hecho, la cadena es un puntero a su primer carácter. Pero todavía tengo que recordarles a todos que los nombres de matrices y cadenas son punteros constantes y sus valores no se pueden cambiar, como segmentos de programa
- width="100%">char s[]= "esto es una prueba.";
for (;*s='\0';s++)
printf("%c", *s); p>
es incorrecto porque intenta cambiar el valor de s en el bucle, mientras que s es en realidad un puntero constante.
Al final de esta sección, hablemos de punteros a funciones. El puntero a la función contiene la dirección de la función en la memoria. El nombre de la función es en realidad la dirección inicial en la memoria del código que completa la tarea de la función. Los punteros de función se utilizan comúnmente en sistemas controlados por menús.
4. Aplicación de punteros, operaciones de memoria y estructuras de datos simples
De hecho, cuando los principiantes aprenden punteros, la dificultad a menudo no está en comprender los conceptos básicos, sino en no entenderlos. No sé cuál es el uso de los punteros y cuándo deben usarse para resolver problemas prácticos. A continuación daré una breve introducción a las funciones principales de los punteros.
A——Al llamar a una función, se pueden modificar dos o más valores y devolverlos a la función que llama.
Hemos cubierto el uso de funciones antes de aprender sobre los punteros. La declaración de retorno en una función puede devolver un valor de la función llamada a la función que llama (o devolver el control de la función llamada sin devolver un valor), pero en aplicaciones prácticas a menudo es necesario poder modificar varios valores en la función llamada. función de llamada, que requiere el uso de punteros.
Hay dos formas de pasar parámetros a una función, a saber, pasar por valor y pasar por referencia. Todas las llamadas a funciones en lenguaje C son llamadas por valor, pero puede utilizar punteros y operadores de referencia indirectos para simular llamadas. por referencia. Al llamar a una función, si necesita modificar el valor del parámetro en la función llamada, debe pasar la dirección del parámetro a la función. Al pasar la dirección de una variable a una función, puede utilizar el operador de referencia indirecta * en la función para modificar el valor de la variable en la unidad de memoria en la función que llama.
Por ejemplo, no podemos completar el intercambio de dos números en una función usando solo una llamada por valor, porque requiere modificar los valores de los dos parámetros, pero esto se puede lograr fácilmente pasando una dirección para simular una llamada. por referencia, como la función:
- width="100%">void swap( int *a, int *b)
{
int temperatura;
temp=*a;
*a=*b;
*b=temp; /p>
Simplemente implementa el intercambio de dos números enteros. Al llamar a la función swap, necesita usar dos direcciones (o dos punteros a números enteros) como parámetros, como
- width="100. %">swap(&num1 , &num2); ->
donde num1 y num2 son dos variables enteras, por lo que sus valores se intercambian llamando a la función swap. Cabe señalar que no es necesario utilizar el operador & al pasar una matriz, porque el nombre de la matriz es en realidad un puntero constante. Sin embargo, es diferente al pasar elementos de la matriz. Aún necesita usar la dirección del elemento. como parámetro para modificar el valor del elemento en la función y devolverlo. Llame a la función.
B——Puede manejar cadenas y matrices de manera más conveniente
Como se mencionó anteriormente, los nombres de matrices y cadenas son punteros constantes y, a veces, los punteros y los nombres de matrices pueden ser alternativos. El uso de punteros para escribir expresiones de subíndices de matriz puede ahorrar tiempo de compilación y, a veces, es más conveniente expresar compensaciones relativas. Este artículo no presentará más técnicas en esta área.
C: puede asignar espacio dinámicamente y manejar direcciones de memoria directamente
A diferencia de otros lenguajes de alto nivel como BASIC, el lenguaje C requiere que las declaraciones de declaración en el mismo nivel no puedan colocarse en cualquier ejecución Después de la declaración, esto determina que las matrices en C son completamente estáticas, porque la longitud de la matriz debe determinarse cuando se declara, lo que significa que el tamaño de la matriz no se puede determinar temporalmente mediante métodos como la entrada declaraciones Si desea establecer y mantener una estructura de datos dinámica, debe implementar una asignación de memoria dinámica y utilizar punteros para operar.
Ahora la lista de varias funciones importantes sobre las operaciones de memoria es la siguiente:
- width="22%">
Funciones
-> - ancho="78%">
Función
->
- ancho="22%" bgcolor="#ffffff">void * calloc(size_t nmemb, size_t size); -> - width="78%" bgcolor="#ffffff">Asignar espacio para una matriz que contiene objetos nmemb El tamaño de cada objeto es el tamaño. ceros. La función calloc devuelve un puntero nulo o un puntero al espacio asignado. ->
- width="22%" bgcolor="#ffffff">void free(void *ptr -> - width="78%" bgcolor="#ffffff">Reciclar ptr El espacio señalado, incluso si ese espacio está disponible para reasignación. ->
- width="22%" bgcolor="#ffffff">void malloc(size_t size); -> - width="78%" bgcolor="#ffffff">La asignación se especifica por tamaño tamaño del espacio del objeto. La función devuelve un puntero nulo o un puntero al espacio asignado.
->
- ancho="22%" bgcolor="#ffffff">void *realloc(void *ptr, size_t tamaño); -> - ancho="78%" bgcolor="#ffffff" > Cambie el tamaño del objeto señalado por ptr al tamaño especificado por tamaño. El contenido de los objetos en el espacio más pequeño de los espacios antiguo y nuevo no cambia. Si el nuevo espacio es grande, el valor de la parte del objeto recién asignada no está definido. Si ptr es un puntero nulo, la función realloc se comporta como la función malloc para especificar el espacio. Si el tamaño es cero y ptr no es un puntero nulo, libera el objeto al que apunta. La función realloc devuelve un puntero nulo o un puntero al espacio asignado que puede haberse movido. ->
- width="100%" bgcolor="#ffffff" colspan="2">Nota: Los prototipos de estas funciones se encuentran en el archivo de encabezado de la herramienta general. ->
Usando estas funciones, podemos procesar estructuras de datos dinámicas. Para un ejemplo simple, si queremos simular la creación de una matriz de longitud variable, solo necesitamos usar la siguiente declaración:
- ancho ="100%">int n, *ptr
scanf("%d", &n
ptr=malloc(n*sizeof(int); )); ->
De esta manera, el sistema asigna el espacio requerido para almacenar una matriz de n elementos. ptr almacena la primera dirección de la memoria asignada, y luego se pueden realizar la asignación y otras operaciones. múltiples referencias de punteros. Debe recordarse que si no hay memoria disponible, se devolverá el puntero NULL cuando se llame a la función malloc, por lo que en este momento, generalmente debe determinar si el puntero es NULL antes de continuar con el siguiente paso.
Se puede ver que aunque un puntero suele utilizar la dirección de una variable como valor, a veces la dirección a la que apunta no tiene ningún nombre de variable. En este caso, se pueden realizar una serie de operaciones. sólo se puede realizar volviendo a hacer referencia al puntero.
D——Puede representar eficazmente estructuras de datos complejas (se recomienda a los principiantes que no lean esta parte hasta que comprendan todos los conceptos básicos)
Cuando los principiantes entran en contacto con este fascinante Antes de comprender Para comprender la desconcertante estructura de datos, primero debe comprender dos conceptos nuevos: "puntero a puntero" (también llamado puntero secundario) y "estructura autorreferencial".
El puntero de primer nivel contiene una dirección que almacena un valor específico, mientras que la dirección contenida en el puntero de segundo nivel almacena otra dirección. Por tanto, también se puede decir que el puntero de segundo nivel es un. puntero a un puntero.
Para declarar un puntero secundario, utilice **, por ejemplo:
- width="100%">int **ptr ->
Declarado; Un puntero secundario a un número entero. ¿Para qué sirven los punteros secundarios? De hecho, al igual que pasar una dirección a una función, podemos modificar un valor específico. Al pasar un puntero secundario a una función, podemos modificar la dirección y devolverla a la función que llama. Esto es muy importante en la gestión de datos. estructuras.
Las estructuras generales contienen varios miembros, y las estructuras autorreferenciales deben contener un miembro puntero que apunte a una estructura del mismo tipo que ellas mismas. Por ejemplo, la siguiente definición de estructura define una estructura autorreferencial:
- width="100%">struct node{
int data; node *nextptr;
}; ->
Una estructura de tipo nodo struct se puede vincular con otra estructura del mismo tipo a través del enlace nextptr. Podemos construir estructuras de datos útiles como listas enlazadas, colas, pilas y árboles a través de estructuras autorreferenciales. Teniendo en cuenta la relación de longitud y dificultad, el autor solo proporciona el programa fuente del "Tutorial de programación en C" (segunda edición) de Machinery Industry Press como una lista vinculada unidireccional relativamente básica.
No recomiendo a los principiantes que se adentren en las estructuras de datos demasiado pronto, porque implica una gran cantidad de aplicaciones de punteros secundarios y estructuras de autorreferencia. Antes de sentar una base sólida, es básicamente imposible comprender completamente el programa, y mucho menos usarlo. . Los estudiantes que estén interesados en estructuras de datos pueden comunicarse conmigo nuevamente.
5. Algunas sugerencias para aprender los punteros y la memoria
Muchas personas pueden sentir que los punteros son abstractos cuando entran en contacto con ellos por primera vez, por lo que siempre evitan su aplicación cuando practican. e intente Cuanto más utilice el método original para resolver problemas, más difícil será progresar. Sugiero que todos intenten usar punteros incluso si se enfrentan a problemas que pueden resolverse sin punteros, especialmente llamadas de paso por referencia para simular funciones. Siempre que practique mucho, en realidad es muy fácil dominarlos. conceptos básicos. El siguiente paso es pensar más en cuándo usar punteros es más conveniente y práctico, y gradualmente dominar el verdadero significado de cómo usar punteros.