C | |
---|---|
clase de idioma | procesal |
tipo de ejecución | compilado |
Apareció en | 1972 |
Autor | dennis ritchie |
Desarrollador | Bell Labs , Dennis Ritchie [1] , Instituto Nacional de Normas de EE . UU. , ISO y Ken Thompson |
extensión de archivo | .c— para archivos de código, .h— para archivos de cabecera |
Liberar | ISO/IEC 9899:2018 ( 5 de julio de 2018 ) |
sistema de tipos | estática débil |
Implementaciones principales | GCC , Clang , TCC , Turbo C , Watcom , Oracle Solaris Studio C, Pelles C |
Dialectos |
"K&R" C ( 1978 ) ANSI C ( 1989 ) C99 ( 1999 ) C11 ( 2011 ) |
sido influenciado | BCPL , B |
influenciado | C++ , Objective-C , C# , Java , Nim |
sistema operativo | Sistema operativo similar a Microsoft Windows y Unix |
Archivos multimedia en Wikimedia Commons |
ISO/CEI 9899 | |
Tecnología de la información — Lenguajes de programación — C | |
Editor | Organización Internacional de Normalización (ISO) |
Sitio web | www.iso.org |
Comité (desarrollador) | ISO/CEI JTC 1/SC 22 |
sitio web del comité | Lenguajes de programación, sus entornos e interfaces de software del sistema. |
Estación Espacial Internacional (ICS) | 35.060 |
Edición actual | ISO/CEI 9899:2018 |
Ediciones anteriores | ISO/CEI 9899:1990/COR2:1996 ISO/CEI 9899:1999/COR3:2007 ISO/CEI 9899:2011/COR1:2012 |
C (de la letra latina C , idioma inglés ) es un lenguaje de programación tipado estáticamente compilado de propósito general desarrollado en 1969-1973 por el empleado de Bell Labs Dennis Ritchie como un desarrollo del lenguaje Bee . Originalmente fue desarrollado para implementar el sistema operativo UNIX , pero desde entonces ha sido portado a muchas otras plataformas. Por diseño, el lenguaje se corresponde estrechamente con las instrucciones típicas de la máquina y ha encontrado uso en proyectos que eran nativos del lenguaje ensamblador , incluidos los sistemas operativos y varias aplicaciones de software para una variedad de dispositivos, desde supercomputadoras hasta sistemas integrados . El lenguaje de programación C ha tenido un impacto significativo en el desarrollo de la industria del software, y su sintaxis se convirtió en la base de lenguajes de programación como C++ , C# , Java y Objective-C .
El lenguaje de programación C se desarrolló entre 1969 y 1973 en Bell Labs , y en 1973 la mayor parte del kernel de UNIX , originalmente escrito en ensamblador PDP-11 /20, había sido reescrito en este lenguaje. El nombre del idioma se convirtió en una continuación lógica del antiguo idioma " Bi " [a] , del cual se tomaron muchas características como base.
A medida que se desarrolló el lenguaje, primero se estandarizó como ANSI C , y luego este estándar fue adoptado por el comité de estandarización internacional ISO como ISO C, también conocido como C90. El estándar C99 agregó nuevas funciones al lenguaje, como matrices de longitud variable y funciones en línea. Y en el estándar C11 , se agregó al lenguaje la implementación de flujos y soporte para tipos atómicos. Desde entonces, sin embargo, el lenguaje ha evolucionado lentamente y solo las correcciones de errores del estándar C11 se convirtieron en el estándar C18.
El lenguaje C fue diseñado como un lenguaje de programación de sistemas para el cual se podía crear un compilador de un solo paso . La biblioteca estándar también es pequeña. Como consecuencia de estos factores, los compiladores son relativamente fáciles de desarrollar [2] . Por lo tanto, este lenguaje está disponible en una variedad de plataformas. Además, a pesar de su naturaleza de bajo nivel, el lenguaje se centra en la portabilidad. Los programas que se ajustan al estándar del lenguaje se pueden compilar para varias arquitecturas informáticas.
El objetivo del lenguaje era facilitar la escritura de programas grandes con errores mínimos en comparación con el ensamblador, siguiendo los principios de la programación procedimental , pero evitando cualquier cosa que introdujera una sobrecarga adicional específica de los lenguajes de alto nivel.
Características principales de C:
Al mismo tiempo, C carece de:
Algunas de las funciones que faltan se pueden simular con herramientas integradas (por ejemplo, las corrutinas se pueden simular usando las funciones setjmpylongjmp ), algunas se agregan usando bibliotecas de terceros (por ejemplo, para admitir funciones multitarea y de red, puede usar la bibliotecas pthreads , sockets y similares; hay bibliotecas para admitir la recolección automática de basura [3] ), parte se implementa en algunos compiladores como extensiones de lenguaje (por ejemplo, funciones anidadas en GCC ). Existe una técnica algo engorrosa, pero bastante funcional, que permite implementar mecanismos OOP en C [4] , basados en el polimorfismo real de punteros en C y el soporte de punteros a funciones en este lenguaje. Los mecanismos OOP basados en este modelo se implementan en la biblioteca GLib y se utilizan activamente en el marco GTK+ . GLib proporciona una clase base GObject, la capacidad de heredar de una sola clase [5] e implementar múltiples interfaces [6] .
Tras su introducción, el lenguaje fue bien recibido porque permitió la creación rápida de compiladores para nuevas plataformas y también permitió a los programadores ser bastante precisos en la forma en que se ejecutaban sus programas. Debido a su proximidad a los lenguajes de bajo nivel, los programas en C se ejecutaron de manera más eficiente que los escritos en muchos otros lenguajes de alto nivel, y solo el código de lenguaje ensamblador optimizado a mano podría ejecutarse aún más rápido, ya que otorgaba control total sobre la máquina. Hasta la fecha, el desarrollo de los compiladores y la complicación de los procesadores ha hecho que el código ensamblador escrito a mano (salvo quizás en programas muy cortos) no tenga prácticamente ninguna ventaja sobre el código generado por compiladores, mientras que C sigue siendo uno de los más lenguajes de alto nivel eficientes.
El idioma utiliza todos los caracteres del alfabeto latino , números y algunos caracteres especiales [7] .
Caracteres del alfabeto latino |
A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, B_ C_ D_ E_ F_ G_ H_ I_ J_ K_ L_ M_ N_ O_ P_ Q_ R_ S_ T_ U_ V_ W_ X_ Y_ Z |
Números | 0, 1, 2, 3, 4, 5, 6, 7, 8,9 |
Símbolos especiales | , (coma) , ;, . (punto) , +, -, *, ^, & (ampersand) , =, ~ (tilde) , !, /, <, >, (, ), {, }, [, ], |, %, ?, ' (apóstrofe) , " (comillas) , : (dos puntos) , _ (guion bajo ) ) , \,# |
Los tokens se forman a partir de caracteres válidos : constantes predefinidas , identificadores y signos de operación . A su vez, los lexemas forman parte de las expresiones ; y las declaraciones y los operadores se componen de expresiones .
Cuando se traduce un programa a C, se extraen del código del programa lexemas de máxima longitud que contengan caracteres válidos. Si un programa contiene un carácter no válido, el analizador léxico (o compilador) generará un error y la traducción del programa será imposible.
El símbolo #no puede formar parte de ningún token y se utiliza en el preprocesador .
IdentificadoresUn identificador válido es una palabra que puede incluir caracteres latinos, números y guiones bajos [8] . Los identificadores se dan a los operadores, constantes, variables, tipos y funciones.
Los identificadores de palabras clave y los identificadores integrados no se pueden utilizar como identificadores de objetos de programa. También hay identificadores reservados, para los cuales el compilador no dará errores, pero que en el futuro pueden convertirse en palabras clave, lo que conducirá a la incompatibilidad.
Solo hay un identificador integrado - __func__, que se define como una cadena constante declarada implícitamente en cada función y que contiene su nombre [8] .
Constantes literalesLos literales con formato especial en C se denominan constantes. Las constantes literales pueden ser enteros, reales, caracteres [9] y cadenas [10] .
Los números enteros se establecen en decimal de forma predeterminada . Si se especifica un prefijo 0x, entonces está en hexadecimal . El prefijo 0 indica que el número está en octal . El sufijo especifica el tamaño mínimo del tipo de constante y también determina si el número tiene signo o no. El tipo final se toma como el más pequeño posible en el que se puede representar la constante dada [11] .
Sufijo | para decimales | Para octal y hexadecimal |
---|---|---|
No | int
long long long |
int
unsigned int long unsigned long long long unsigned long long |
uoU | unsigned int
unsigned long unsigned long long |
unsigned int
unsigned long unsigned long long |
loL | long
long long |
long
unsigned long long long unsigned long long |
uo Ujunto con loL | unsigned long
unsigned long long |
unsigned long
unsigned long long |
lloLL | long long | long long
unsigned long long |
uo Ujunto con lloLL | unsigned long long | unsigned long long |
Decimal
formato |
Con exponente | hexadecimal
formato |
---|---|---|
1.5 | 1.5e+0 | 0x1.8p+0 |
15e-1 | 0x3.0p-1 | |
0.15e+1 | 0x0.cp+1 |
Las constantes de números reales son de tipo por defecto double. Al especificar un sufijo , fel tipo se asigna a la constante float, y al especificar lo L - long double. Una constante se considerará real si contiene un signo de punto, o una letra, po Pen el caso de una notación hexadecimal con un prefijo 0x. La notación decimal puede incluir un exponente después de las letras eo E. En el caso de la notación hexadecimal, el exponente se especifica después de las letras po Pes obligatorio, lo que distingue las constantes hexadecimales reales de los números enteros. En hexadecimal, el exponente es una potencia de 2 [12] .
Las constantes de carácter se encierran entre comillas simples ( '), y el prefijo especifica tanto el tipo de datos de la constante de carácter como la codificación en la que se representará el carácter. En C, una constante de carácter sin prefijo es de tipo int[13] , a diferencia de C++ , donde una constante de carácter es char.
Prefijo | Tipo de datos | Codificación |
---|---|---|
No | int | ASCII |
u | char16_t | Codificación de cadenas multibyte de 16 bits |
U | char32_t | Codificación de cadena multibyte de 32 bits |
L | wchar_t | Codificación de cadena ancha |
Los literales de cadena están encerrados entre comillas dobles y pueden tener como prefijo el tipo de datos y la codificación de la cadena. Los literales de cadena son matrices simples. Sin embargo, en codificaciones multibyte como UTF-8 , un carácter puede ocupar más de un elemento de matriz. De hecho, los literales de cadena son const [14] , pero a diferencia de C++, sus tipos de datos no contienen el modificador const.
Prefijo | Tipo de datos | Codificación |
---|---|---|
No | char * | Codificación ASCII o multibyte |
u8 | char * | UTF-8 |
u | char16_t * | Codificación multibyte de 16 bits |
U | char32_t * | codificación multibyte de 32 bits |
L | wchar_t * | Codificación de cadena ancha |
Varias constantes de cadena consecutivas separadas por espacios en blanco o líneas nuevas se combinan en una sola cadena en la compilación, que a menudo se usa para diseñar el código de una cadena separando partes de una constante de cadena en diferentes líneas para mejorar la legibilidad [16] .
Constantes con nombreMacro | #define BUFFER_SIZE 1024 |
enumeración anónima |
enumeración { BUFFER_SIZE = 1024 }; |
Variable como una constante |
const int tamaño_búfer = 1024 ; constante externa interna tamaño_búfer ; |
En el lenguaje C, para definir constantes, se acostumbra utilizar definiciones de macros declaradas mediante la directiva del preprocesador [17] : #define
#define nombre constante [ valor ]Una constante introducida de esta manera tendrá efecto en su ámbito, desde el momento en que se establece la constante y hasta el final del código del programa, o hasta que el efecto de la constante dada sea cancelado por la directiva #undef:
#undef nombre constanteAl igual que con cualquier macro, para una constante con nombre, el valor de la constante se sustituye automáticamente en el código del programa siempre que se use el nombre de la constante. Por lo tanto, cuando se declaran enteros o números reales dentro de una macro, puede ser necesario especificar explícitamente el tipo de datos usando el sufijo literal apropiado; de lo contrario, el número será un tipo predeterminado inten el caso de un número entero o un tipo double en el caso de un real.
Para números enteros, hay otra forma de crear constantes con nombre: a través de enumeraciones de operadores enum[17] . Sin embargo, este método solo es adecuado para tipos más pequeños o iguales que type y no se usa en la biblioteca estándar [18] . int
También es posible crear constantes como variables con el calificador const, pero a diferencia de los otros dos métodos, tales constantes consumen memoria, se pueden señalar y no se pueden usar en tiempo de compilación [17] :
Las palabras clave son identificadores diseñados para realizar una tarea particular en la etapa de compilación, o para sugerencias e instrucciones para el compilador.
Palabras clave | Objetivo | Estándar |
---|---|---|
sizeof | Obtener el tamaño de un objeto en tiempo de compilación | C89 |
typedef | Especificación de un nombre alternativo para un tipo | |
auto,register | Sugerencias del compilador sobre dónde se almacenan las variables | |
extern | Diciéndole al compilador que busque un objeto fuera del archivo actual | |
static | Declarar un objeto estático | |
void | Sin marcador de valor; en punteros significa datos arbitrarios | |
char. short. int.long | Tipos enteros y sus modificadores de tamaño | |
signed,unsigned | Modificadores de tipo entero que los definen como firmados o sin firmar | |
float,double | Tipos de datos reales | |
const | Un modificador de tipo de datos que le dice al compilador que las variables de ese tipo son de solo lectura | |
volatile | Instruir al compilador para cambiar el valor de una variable desde el exterior | |
struct | Tipo de datos, especificado como una estructura con un conjunto de campos | |
enum | Un tipo de datos que almacena uno de un conjunto de valores enteros | |
union | Un tipo de datos que puede almacenar datos en representaciones de diferentes tipos de datos | |
do. for.while | Declaraciones de bucle | |
if,else | operador condicional | |
switch. case.default | Operador de selección por parámetro entero | |
break,continue | Declaraciones de ruptura de bucle | |
goto | Operador de salto incondicional | |
return | Retorno de una función | |
inline | Declaración de función en línea | C99 [20] |
restrict | Declarar un puntero que hace referencia a un bloque de memoria al que no hace referencia ningún otro puntero | |
_Bool[b] | tipo de datos booleano | |
_Complex[c] ,_Imaginary [d] | Tipos utilizados para cálculos de números complejos | |
_Atomic | Un modificador de tipo que lo hace atómico. | C11 |
_Alignas[mi] | Especificar explícitamente la alineación de bytes para un tipo de datos | |
_Alignof[F] | Obtener alineación para un tipo de datos determinado en tiempo de compilación | |
_Generic | Seleccionar uno de un conjunto de valores en tiempo de compilación, según el tipo de datos controlado | |
_Noreturn[gramo] | Indicar al compilador que la función no puede terminar normalmente (es decir, por return) | |
_Static_assert[h] | Especificación de aserciones para verificar en tiempo de compilación | |
_Thread_local[i] | Declarar una variable local de subproceso |
Además de las palabras clave, el estándar del idioma define identificadores reservados, cuyo uso puede generar incompatibilidad con futuras versiones del estándar. Se reservan todas las palabras clave excepto las que comienzan con un guión bajo ( _) seguido de una letra mayúscula ( A- Z) u otro guión bajo [21] . En los estándares C99 y C11, algunos de estos identificadores se utilizaron para palabras clave de nuevos idiomas.
En el ámbito del archivo, se reserva el uso de cualquier nombre que comience con un guión bajo ( _) [21] , es decir, se permite nombrar tipos, constantes y variables declaradas dentro de un bloque de instrucciones, por ejemplo, dentro de funciones, con un guión bajo.
También los identificadores reservados son todas las macros de la biblioteca estándar y los nombres de ella vinculados en la etapa de vinculación [21] .
El uso de identificadores reservados en los programas está definido por el estándar como un comportamiento indefinido . Intentar cancelar cualquier macro estándar #undeftambién dará como resultado un comportamiento indefinido [21] .
El texto de un programa C puede contener fragmentos que no forman parte de los comentarios del código del programa . Los comentarios se marcan de forma especial en el texto del programa y se omiten durante la compilación.
Inicialmente, en el estándar C89 , los comentarios en línea estaban disponibles y podían colocarse entre secuencias de caracteres /*y */. En este caso, es imposible anidar un comentario en otro, ya que la primera secuencia encontrada */terminará el comentario, y el */compilador percibirá el texto que sigue inmediatamente a la notación como el código fuente del programa.
El siguiente estándar, C99 , introdujo otra forma de marcar los comentarios: se considera que un comentario es un texto que comienza con una secuencia de caracteres //y termina al final de una línea [20] .
Los comentarios a menudo se usan para autodocumentar el código fuente, explicando partes complejas, describiendo el propósito de ciertos archivos y describiendo las reglas para usar y trabajar ciertas funciones, macros, tipos de datos y variables. Hay postprocesadores que pueden convertir comentarios con formato especial en documentación. Entre tales posprocesadores con lenguaje C, puede funcionar el sistema de documentación Doxygen .
Los operadores que se usan en las expresiones son algunas operaciones que se realizan en los operandos y que devuelven un valor calculado: el resultado de la operación. El operando puede ser una llamada constante, variable, expresión o función. Un operador puede ser un carácter especial, un conjunto de caracteres especiales o una palabra especial. Los operadores se distinguen por el número de operandos involucrados, es decir, distinguen entre operadores unarios, operadores binarios y operadores ternarios.
Operadores unariosLos operadores unarios realizan una operación en un solo argumento y tienen el siguiente formato de operación:
[ operador ] [ operando ]Las operaciones de incremento y decremento de postfijo tienen el formato inverso:
[ operando ] [ operador ]+ | más unario | ~ | Tomando el código de retorno | & | Tomando una dirección | ++ | Incremento de prefijo o posfijo | sizeof | Obtener el número de bytes ocupados por un objeto en la memoria; se puede utilizar como operación y como operador |
- | menos unario | ! | negación lógica | * | Eliminación de referencias de puntero | -- | Decremento de prefijo o posfijo | _Alignof | Obtener alineación para un tipo de datos dado |
Los operadores de incremento y decremento, a diferencia de los otros operadores unarios, cambian el valor de su operando. El operador de prefijo primero modifica el valor y luego lo devuelve. Postfix primero devuelve el valor y solo luego lo cambia.
Operadores binariosLos operadores binarios se ubican entre dos argumentos y realizan una operación sobre ellos:
[ operando ] [ operador ] [ operando ]+ | Suma | % | Tomando el resto de una división | << | Desplazamiento a la izquierda bit a bit | > | Más | == | igual |
- | Sustracción | & | Y bit a bit | >> | Desplazamiento de bits a la derecha | < | Menos | != | No es igual |
* | Multiplicación | | | O bit a bit | && | Y lógico | >= | Mayor que o igual | ||
/ | División | ^ | XOR bit a bit | || | O lógico | <= | Menor o igual |
Además, los operadores binarios en C incluyen operadores de asignación a la izquierda que realizan una operación en los argumentos izquierdo y derecho y colocan el resultado en el argumento izquierdo.
= | Asignar el valor del argumento de la derecha al de la izquierda | %= | Resto de dividir el operando izquierdo por el derecho | ^= | XOR bit a bit del operando derecho al operando izquierdo |
+= | Adición al operando izquierdo del derecho | /= | División del operando izquierdo por el derecho | <<= | Desplazamiento bit a bit del operando izquierdo hacia la izquierda por el número de bits dado por el operando derecho |
-= | Resta del operando izquierdo del derecho | &= | Bitwise Y el operando derecho a la izquierda | >>= | Desplazamiento bit a bit del operando izquierdo a la derecha por el número de bits especificado por el operando derecho |
*= | Multiplicación del operando izquierdo por el derecho | |= | OR bit a bit del operando derecho a la izquierda |
Solo hay un operador ternario en C, el operador condicional abreviado, que tiene la siguiente forma:
[ condición ] ?[ expresión1 ] :[ expresión2 ]El operador condicional abreviado tiene tres operandos:
El operador en este caso es una combinación de signos ?y :.
Una expresión es un conjunto ordenado de operaciones sobre constantes, variables y funciones. Las expresiones contienen operaciones que consisten en operandos y operadores . El orden en que se realizan las operaciones depende del formulario de registro y de la prioridad de las operaciones. Cada expresión tiene un valor , el resultado de realizar todas las operaciones incluidas en la expresión. Durante la evaluación de una expresión, dependiendo de las operaciones, los valores de las variables pueden cambiar y las funciones también pueden ejecutarse si sus llamadas están presentes en la expresión.
Entre las expresiones, se distingue una clase de expresiones admisibles por la izquierda : expresiones que pueden estar presentes a la izquierda del signo de asignación.
Prioridad de ejecución de las operacionesLa prioridad de las operaciones está definida por el estándar y especifica el orden en que se realizarán las operaciones. Las operaciones en C se realizan de acuerdo con la tabla de precedencia a continuación [25] [26] .
Una prioridad | fichas | Operación | Clase | Asociatividad |
---|---|---|---|---|
una | a[índice] | Referencias por índice | sufijo | de izquierda a derecha → |
f(argumentos) | Llamada de función | |||
. | Acceso al campo | |||
-> | Acceso al campo por puntero | |||
++ -- | Incremento positivo y negativo | |||
() {inicializador de nombre de tipo} | Literal compuesto (C99) | |||
() {inicializador de nombre de tipo ,} | ||||
2 | ++ -- | Incrementos de prefijos positivos y negativos | unario | ← de derecha a izquierda |
sizeof | Obtener el tamaño | |||
_Alignof[F] | Obtener alineación ( C11 ) | |||
~ | bit a bit NO | |||
! | NO lógico | |||
- + | Indicación de signo (menos o más) | |||
& | Obtener una dirección | |||
* | Referencia de puntero (desreferencia) | |||
(escribe un nombre) | Tipo de fundición | |||
3 | * / % | Multiplicación, división y resto | binario | de izquierda a derecha → |
cuatro | + - | Adición y sustracción | ||
5 | << >> | Desplazamiento a la izquierda y a la derecha | ||
6 | < > <= >= | Operaciones de comparación | ||
7 | == != | Comprobar la igualdad o la desigualdad | ||
ocho | & | Y bit a bit | ||
9 | ^ | XOR bit a bit | ||
diez | | | O bit a bit | ||
once | && | Y lógico | ||
12 | || | O lógico | ||
13 | ? : | Condición | ternario | ← de derecha a izquierda |
catorce | = | Asignación de valor | binario | |
+= -= *= /= %= <<= >>= &= ^= |= | Operaciones para cambiar el valor de la izquierda | |||
quince | , | Computación Secuencial | de izquierda a derecha → |
Las prioridades de los operadores en C no siempre se justifican y, a veces, conducen a resultados intuitivamente difíciles de predecir. Por ejemplo, dado que los operadores unarios tienen asociatividad de derecha a izquierda, la evaluación de la expresión *p++dará como resultado un incremento de puntero seguido de una desreferencia ( *(p++)), en lugar de un incremento de puntero ( (*p)++). Por lo tanto, en caso de situaciones difíciles de entender, se recomienda agrupar explícitamente las expresiones usando corchetes [26] .
Otra característica importante del lenguaje C es que la evaluación de los valores de los argumentos pasados a una llamada de función no es secuencial [27] , es decir, la coma que separa los argumentos no corresponde a la evaluación secuencial de la tabla de precedencia. En el siguiente ejemplo, las llamadas a funciones dadas como argumentos para otra función pueden estar en cualquier orden:
intx ; _ x = computar ( get_arg1 (), get_arg2 ()); // llama primero a get_arg2()Además, no puede confiar en la precedencia de las operaciones en caso de efectos secundarios que aparecen durante la evaluación de la expresión, ya que esto conducirá a un comportamiento indefinido [27] .
Puntos de secuencia y efectos secundariosEl Apéndice C del estándar de lenguaje define un conjunto de puntos de secuencia que se garantiza que no tendrán efectos secundarios continuos de los cálculos. Es decir, el punto de secuencia es una etapa de cálculos que separa la evaluación de expresiones entre sí, de modo que los cálculos que ocurrieron antes del punto de secuencia, incluidos los efectos secundarios, ya han terminado, y después del punto de secuencia aún no han comenzado [28]. ] . Un efecto secundario puede ser un cambio en el valor de una variable durante la evaluación de una expresión. Cambiar el valor involucrado en el cálculo, junto con el efecto secundario de cambiar el mismo valor al siguiente punto de secuencia, conducirá a un comportamiento indefinido. Lo mismo sucederá si hay dos o más cambios de lado del mismo valor involucrados en el cálculo [27] .
punto de ruta | Evento antes | Evento después |
---|---|---|
Llamada de función | Cálculo de un puntero a una función y sus argumentos | Llamada de función |
Operadores lógicos AND ( &&), OR ( ||) y cálculo secuencial ( ,) | Cálculo del primer operando | Cálculo del segundo operando |
Operador de condición abreviado ( ?:) | Cálculo del operando que sirve como condición | Cálculo del 2º o 3er operando |
Entre dos expresiones completas (no anidadas) | Una expresión completa | La siguiente expresión completa |
Descriptor completo completado | ||
Justo antes de regresar de una función de biblioteca | ||
Después de cada conversión asociada con un especificador de E/S formateado | ||
Inmediatamente antes e inmediatamente después de cada llamada a la función de comparación, y entre la llamada a la función de comparación y cualquier movimiento realizado en los argumentos pasados a la función de comparación |
Las expresiones completas son [27] :
En el siguiente ejemplo, la variable se cambia tres veces entre puntos de secuencia, lo que genera un resultado indefinido:
int i = 1 ; // El descriptor es el primer punto de secuencia, la expresión completa es el segundo i += ++ i + 1 ; // Expresión completa: tercer punto de secuencia printf ( "%d \n " , i ); // Puede generar 4 o 5Otros ejemplos simples de comportamiento indefinido para evitar:
yo = yo ++ + 1 ; // comportamiento indefinido i = ++ i + 1 ; // también comportamiento indefinido printf ( "%d, %d \n " , --i , ++ i ) ; // comportamiento indefinido printf ( "%d, %d \n " , ++ i , ++ i ); // también comportamiento indefinido printf ( "%d, %d \n " , i = 0 , i = 1 ); // comportamiento indefinido printf ( "%d, %d \n " , i = 0 , i = 0 ); // también comportamiento indefinido un [ yo ] = yo ++ ; // comportamiento indefinido a [ i ++ ] = i ; // también comportamiento indefinidoLas declaraciones de control están diseñadas para realizar acciones y controlar el flujo de ejecución del programa. Varias sentencias consecutivas forman una secuencia de sentencias .
Declaración vacíaLa construcción de lenguaje más simple es una expresión vacía llamada declaración vacía [29] :
;Una declaración vacía no hace nada y se puede colocar en cualquier parte del programa. Comúnmente utilizado en bucles con cuerpo faltante [30] .
InstruccionesUna instrucción es un tipo de acción elemental:
( expresión );La acción de este operador es ejecutar la expresión especificada en el cuerpo del operador.
Varias instrucciones consecutivas forman una secuencia de instrucciones .
Bloque de instruccionesLas instrucciones se pueden agrupar en bloques especiales de la siguiente forma:
{
( secuencia de instrucciones )},
Un bloque de declaraciones, también llamado a veces declaración compuesta, está delimitado por una llave izquierda ( {) al principio y una llave derecha ( }) al final.
En funciones , un bloque de instrucciones denota el cuerpo de la función y es parte de la definición de la función. La declaración compuesta también se puede usar en declaraciones de bucle, condición y elección.
Sentencias condicionalesHay dos operadores condicionales en el lenguaje que implementan la bifurcación del programa:
La forma más simple del operador.if
if(( condición ) )( operador ) ( siguiente declaración )El operador iffunciona así:
En particular, el siguiente código, si se cumple la condición especificada, no realizará ninguna acción, ya que, de hecho, se ejecuta una sentencia vacía:
if(( condición )) ;Una forma más compleja del operador ifcontiene la palabra clave else:
if(( condición ) )( operador ) else( operador alternativo ) ( siguiente declaración )Aquí, si no se cumple la condición especificada entre paréntesis, se ejecuta la declaración especificada después de la palabra clave else.
Aunque el estándar permite que las declaraciones se especifiquen en una línea ifo como elseuna sola línea, esto se considera de mal estilo y reduce la legibilidad del código. Se recomienda que siempre especifique un bloque de sentencias utilizando llaves como cuerpo [31] .
Sentencias de ejecución de bucleUn bucle es una pieza de código que contiene
En consecuencia, hay dos tipos de ciclos:
Un bucle poscondicional garantiza que el cuerpo del bucle se ejecutará al menos una vez.
El lenguaje C proporciona dos variantes de bucles con una condición previa: whiley for.
while(condición) [ cuerpo del bucle ] for( instrucción de ;condición de bloque de inicialización [ cuerpo del bucle ],;)El bucle fortambién se llama paramétrico, es equivalente al siguiente bloque de sentencias:
[ bloque de inicialización ] while(condición) { [ cuerpo del bucle ] [ operador ] }En una situación normal, el bloque de inicialización contiene el establecimiento del valor inicial de una variable, que se denomina variable de bucle, y la declaración que se ejecuta inmediatamente después de que el cuerpo del bucle cambie los valores de la variable utilizada, la condición contiene una comparación del valor de la variable de bucle utilizada con algún valor predefinido, y tan pronto como la comparación deja de ejecutarse, el bucle se interrumpe y el código del programa que sigue inmediatamente a la instrucción del bucle comienza a ejecutarse.
Para un bucle do-while, la condición se especifica después del cuerpo del bucle:
do[ cuerpo del bucle ] while( condición)La condición de bucle es una expresión booleana. Sin embargo, la conversión implícita de tipos le permite usar una expresión aritmética como condición de bucle. Esto le permite organizar el llamado "bucle infinito":
while(1);Lo mismo se puede hacer con el operador for:
for(;;);En la práctica, estos bucles infinitos suelen utilizarse junto con break, gotoo return, que interrumpen el bucle de diferentes formas.
Al igual que con una declaración condicional, usar un cuerpo de una sola línea sin encerrarlo en un bloque de declaración con llaves se considera de mal estilo, lo que reduce la legibilidad del código [31] .
Operadores de salto incondicionalesLos operadores de rama incondicionales le permiten interrumpir la ejecución de cualquier bloque de cálculos e ir a otro lugar del programa dentro de la función actual. Los operadores de salto incondicionales generalmente se usan junto con los operadores condicionales.
goto[ etiqueta ],Una etiqueta es un identificador que transfiere el control al operador que está marcado en el programa con la etiqueta especificada:
[ etiqueta ] :[ operador ]Si la etiqueta especificada no está presente en el programa, o si hay varias declaraciones con la misma etiqueta, el compilador informa un error.
La transferencia de control solo es posible dentro de la función donde se usa el operador de transición, por lo tanto, usar el operador gotono puede transferir el control a otra función.
Otras declaraciones de salto están relacionadas con los bucles y le permiten interrumpir la ejecución del cuerpo del bucle:
La instrucción breaktambién puede interrumpir la operación de la instrucción switch, por lo que dentro de la instrucción que se switchejecuta en el ciclo, la instrucción breakno podrá interrumpir el ciclo. Especificado en el cuerpo del ciclo, interrumpe el trabajo del ciclo anidado más cercano.
El operador continuesolo se puede usar dentro de los operadores do, whiley for. Para bucles whiley do-whileel operador continueprovoca la prueba de la condición del bucle, y en el caso de un bucle for , la ejecución del operador especificado en el 3er parámetro del bucle, antes de comprobar la condición para continuar el bucle.
Declaración de retorno de funciónEl operador returninterrumpe la ejecución de la función en la que se utiliza. Si la función no debe devolver un valor, entonces se utiliza una llamada sin valor de retorno:
return;Si la función debe devolver un valor, el valor devuelto se indica después del operador:
return[ valor ];Si hay otras declaraciones después de la declaración de retorno en el cuerpo de la función, estas declaraciones nunca se ejecutarán, en cuyo caso el compilador puede emitir una advertencia. Sin embargo, después del operador return, se pueden indicar instrucciones para la terminación alternativa de la función, por ejemplo, por error, y la transición a estos operadores se puede realizar utilizando el operador gotosegún cualquier condición .
Al declarar una variable, se especifica su tipo y nombre, y también se puede especificar el valor inicial:
[descriptor] [nombre];o
[descriptor] [nombre] =[inicializador] ;,dónde
Si a la variable no se le asigna un valor inicial, entonces en el caso de una variable global, su valor se llena con ceros, y para una variable local, el valor inicial será indefinido.
En un descriptor de variable, puede designar una variable como global, pero limitada al alcance de un archivo o función, utilizando la palabra clave static. Si una variable se declara global sin la palabra clave static, también se puede acceder a ella desde otros archivos, donde se requiere declarar esta variable sin un inicializador, pero con la palabra clave extern. Las direcciones de dichas variables se determinan en el momento del enlace .
Una función es una pieza independiente de código de programa que se puede reutilizar en un programa. Las funciones pueden tomar argumentos y devolver valores. Las funciones también pueden tener efectos secundarios durante su ejecución: cambiar variables globales, trabajar con archivos, interactuar con el sistema operativo o el hardware [28] .
Para definir una función en C, debe declararla:
También es necesario proporcionar una definición de función que contenga un bloque de instrucciones que implementen el comportamiento de la función.
No declarar una función en particular es un error si la función se usa fuera del alcance de la definición, lo que, según la implementación, genera mensajes o advertencias.
Para llamar a una función, basta con especificar su nombre con los parámetros especificados entre paréntesis. En este caso, la dirección del punto de llamada se coloca en la pila, las variables responsables de los parámetros de la función se crean e inicializan y el control se transfiere al código que implementa la función llamada. Después de que se ejecuta la función, se libera la memoria asignada durante la llamada a la función, se regresa al punto de llamada y, si la llamada a la función es parte de alguna expresión, el valor calculado dentro de la función se pasa al punto de retorno.
Si no se especifican paréntesis después de la función, el compilador interpreta esto como obtener la dirección de la función. La dirección de una función se puede ingresar en un puntero y luego llamar a la función usando un puntero hacia ella, que se usa activamente, por ejemplo, en sistemas de complementos [32] .
Usando la palabra clave, inlinepuede marcar funciones cuyas llamadas desea ejecutar lo más rápido posible. El compilador puede sustituir el código de tales funciones directamente en el punto de su llamada [33] . Por un lado, esto aumenta la cantidad de código ejecutable, pero, por otro lado, ahorra el tiempo de su ejecución, ya que no se utiliza la operación de llamada de función que consume mucho tiempo. Sin embargo, debido a la arquitectura de las computadoras, las funciones integradas pueden acelerar o ralentizar la aplicación en su conjunto. Sin embargo, en muchos casos, las funciones en línea son el reemplazo preferido de las macros [34] .
Declaración de funciónUna declaración de función tiene el siguiente formato:
[descriptor] [nombre] ([lista] );,dónde
El signo de una declaración de función es el ;símbolo “ ”, por lo que una declaración de función es una instrucción.
En el caso más simple, [declarator] contiene una indicación de un tipo específico de valor de retorno. Una función que no debe devolver ningún valor se declara de tipo void.
Si es necesario, el descriptor puede contener modificadores especificados mediante palabras clave:
La lista de parámetros de función define la firma de la función.
C no permite declarar varias funciones con el mismo nombre, no se admite la sobrecarga de funciones [36] .
Definición de funciónLa definición de la función tiene el siguiente formato:
[descriptor] [nombre] ([lista] )[cuerpo]Donde [declarador], [nombre] y [lista] son los mismos que en la declaración, y [cuerpo] es una declaración compuesta que representa una implementación concreta de la función. El compilador distingue entre definiciones de funciones del mismo nombre por su firma, y así (por firma) se establece una conexión entre la definición y la declaración correspondiente.
El cuerpo de la función se ve así:
{ [secuencia de declaraciones] return([valor de retorno]); }El retorno de la función se lleva a cabo mediante el operador , que especifica el valor de retorno o no lo especifica, dependiendo del tipo de datos devuelto por la función. En casos excepcionales, una función se puede marcar como que no realiza una devolución utilizando una macro de un archivo de encabezado , en cuyo caso no se requiere ninguna declaración. Por ejemplo, las funciones que llaman incondicionalmente dentro de sí mismas pueden marcarse de esta manera [33] . returnnoreturnstdnoreturn.hreturnabort()
Llamada de funciónLa llamada de función es para realizar las siguientes acciones:
Dependiendo de la implementación, el compilador se asegura estrictamente de que el tipo del parámetro real coincida con el tipo del parámetro formal o, si es posible, realiza una conversión de tipo implícita, lo que obviamente genera efectos secundarios.
Si se pasa una variable a la función, cuando se llama a la función, se crea una copia ( la memoria se asigna en la pila y el valor se copia). Por ejemplo, pasar una estructura a una función hará que se copie toda la estructura. Si se pasa un puntero a una estructura, solo se copia el valor del puntero. Pasar una matriz a una función también solo hace que se copie un puntero a su primer elemento. En este caso, para indicar explícitamente que la dirección del comienzo de la matriz se toma como entrada para la función, y no como un puntero a una sola variable, en lugar de declarar un puntero después del nombre de la variable, puede poner corchetes, por ejemplo ejemplo:
void ejemplo_func ( matriz int []); // array es un puntero al primer elemento de un array de tipo intC permite llamadas anidadas. La profundidad de anidamiento de las llamadas tiene una limitación obvia relacionada con el tamaño de la pila asignada al programa. Por lo tanto, las implementaciones de C establecen un límite en la profundidad de anidamiento.
Un caso especial de una llamada anidada es una llamada de función dentro del cuerpo de la función llamada. Tal llamada se llama recursiva y se usa para organizar cálculos uniformes. Dada la restricción natural de las llamadas anidadas, la implementación recursiva se reemplaza por una implementación que usa bucles.
Los tipos de datos enteros varían en tamaño desde al menos 8 hasta al menos 32 bits. El estándar C99 aumenta el tamaño máximo de un número entero a al menos 64 bits. Los tipos de datos enteros se usan para almacenar números enteros (el tipo chartambién se usa para almacenar caracteres ASCII). Todos los tamaños de rango de los tipos de datos a continuación son mínimos y pueden ser mayores en una plataforma determinada [37] .
Como consecuencia de los tamaños mínimos de los tipos, la norma exige que los tamaños de los tipos integrales cumplan la condición:
1= ≤ ≤ ≤ ≤ . sizeof(char)sizeof(short)sizeof(int)sizeof(long)sizeof(long long)
Por lo tanto, los tamaños de algunos tipos en términos de número de bytes pueden coincidir si se cumple la condición del número mínimo de bits. Incluso chary longpuede tener el mismo tamaño si un byte ocupará 32 bits o más, pero tales plataformas serán muy raras o no existirán. El estándar garantiza que el tipo sea char siempre de 1 byte. El tamaño de un byte en bits está determinado por una constante CHAR_BITen el archivo de encabezado limits.h, que es de 8 bits en sistemas compatibles con POSIX [38] .
El rango de valores mínimos de tipos enteros según el estándar se define de a para tipos con signo y de a para tipos sin signo, donde N es la profundidad de bits del tipo. Las implementaciones del compilador pueden expandir este rango a su discreción. En la práctica, el rango de a se usa más comúnmente para tipos firmados . Los valores mínimos y máximos de cada tipo se especifican en el archivo como definiciones de macro. -(2N-1-1)2N-1-102N-2N-12N-1-1limits.h
Se debe prestar especial atención al tipo char. Formalmente, este es un tipo separado, pero en realidad es charequivalente a signed char, o unsigned char, dependiendo del compilador [39] .
Para evitar confusiones entre los tamaños de letra, el estándar C99 introdujo nuevos tipos de datos, descritos en stdint.h. Entre ellos se encuentran tipos como: , , , donde = 8, 16, 32 o 64. El prefijo indica el tipo mínimo que puede acomodar bits, el prefijo indica un tipo de al menos 16 bits, que es el más rápido en esta plataforma. Los tipos sin prefijos indican tipos con un tamaño fijo de bits. intN_tint_leastN_tint_fastN_tNleast-Nfast-N
Los tipos con prefijos least-y fast-pueden considerarse un reemplazo de los tipos int, short, long, con la única diferencia de que los primeros dan al programador la posibilidad de elegir entre velocidad y tamaño.
Tipo de datos | El tamaño | Rango de valor mínimo | Estándar |
---|---|---|---|
signed char | mínimo 8 bits | de −127 [40] (= -(2 7 −1)) a 127 | C90 [j] |
int_least8_t | C99 | ||
int_fast8_t | |||
unsigned char | mínimo 8 bits | 0 a 255 (=2 8 −1) | C90 [j] |
uint_least8_t | C99 | ||
uint_fast8_t | |||
char | mínimo 8 bits | −127 a 127 o 0 a 255 dependiendo del compilador | C90 [j] |
short int | mínimo 16 bits | de -32.767 (= -(2 15 -1)) a 32.767 | C90 [j] |
int | |||
int_least16_t | C99 | ||
int_fast16_t | |||
unsigned short int | mínimo 16 bits | 0 a 65,535 (= 2 16 −1) | C90 [j] |
unsigned int | |||
uint_least16_t | C99 | ||
uint_fast16_t | |||
long int | mínimo 32 bits | −2.147.483.647 a 2.147.483.647 | C90 [j] |
int_least32_t | C99 | ||
int_fast32_t | |||
unsigned long int | mínimo 32 bits | 0 a 4,294,967,295 (= 2 32 −1) | C90 [j] |
uint_least32_t | C99 | ||
uint_fast32_t | |||
long long int | mínimo 64 bits | -9.223.372.036.854.775.807 a 9.223.372.036.854.775.807 | C99 |
int_least64_t | |||
int_fast64_t | |||
unsigned long long int | mínimo 64 bits | 0 a 18 446 744 073 709 551 615 (= 264 −1 ) | |
uint_least64_t | |||
uint_fast64_t | |||
int8_t | 8 bits | -127 a 127 | |
uint8_t | 8 bits | 0 a 255 (=2 8 −1) | |
int16_t | 16 bits | -32.767 a 32.767 | |
uint16_t | 16 bits | 0 a 65,535 (= 2 16 −1) | |
int32_t | 32 bits | −2.147.483.647 a 2.147.483.647 | |
uint32_t | 32 bits | 0 a 4,294,967,295 (= 2 32 −1) | |
int64_t | 64 bits | -9.223.372.036.854.775.807 a 9.223.372.036.854.775.807 | |
uint64_t | 64 bits | 0 a 18 446 744 073 709 551 615 (= 264 −1 ) | |
La tabla muestra el rango mínimo de valores según el estándar de idioma. Los compiladores de C pueden expandir el rango de valores. |
Además, desde el estándar C99, se han agregado los tipos intmax_ty uintmax_t, correspondientes a los tipos más grandes firmados y sin firmar, respectivamente. Estos tipos son convenientes cuando se usan en macros para almacenar valores intermedios o temporales durante operaciones con argumentos enteros, ya que permiten ajustar valores de cualquier tipo. Por ejemplo, estos tipos se utilizan en las macros de comparación de enteros de la biblioteca de pruebas de unidades Check para C [41] .
En C, existen varios tipos de enteros adicionales para el manejo seguro del tipo de datos de puntero: intptr_t, uintptr_ty ptrdiff_t. Los tipos intptr_ty uintptr_tdel estándar C99 están diseñados para almacenar valores firmados y sin firmar, respectivamente, que pueden ajustarse al tamaño de un puntero. Estos tipos se utilizan a menudo para almacenar un número entero arbitrario en un puntero, por ejemplo, como una forma de deshacerse de la asignación de memoria innecesaria al registrar funciones de retroalimentación [42] o al usar listas vinculadas de terceros, matrices asociativas y otras estructuras en las que los datos se almacenan por puntero. El tipo ptrdiff_tdel archivo de encabezado stddef.hestá diseñado para almacenar de forma segura la diferencia de dos punteros.
Para almacenar el tamaño, se proporciona un tipo sin firmar size_tdel archivo de encabezado stddef.h. Este tipo es capaz de contener la cantidad máxima posible de bytes disponibles en el puntero y, por lo general, se usa para almacenar el tamaño en bytes. sizeofEl operador [43] devuelve el valor de este tipo .
Conversión de tipo enteroLas conversiones de tipo entero pueden ocurrir explícitamente, utilizando un operador de conversión, o implícitamente. Los valores de tipos menores que int, cuando participan en cualquier operación o cuando se pasan a una llamada de función, se convierten automáticamente en el tipo inty, si la conversión es imposible, en el tipo unsigned int. A menudo, tales lanzamientos implícitos son necesarios para que el resultado del cálculo sea correcto, pero a veces conducen a errores intuitivamente incomprensibles en los cálculos. Por ejemplo, si la operación involucra números de tipo inty unsigned int, y el valor con signo es negativo, convertir un número negativo en un tipo sin signo generará un desbordamiento y un valor positivo muy grande, lo que puede conducir a un resultado incorrecto de las operaciones de comparación. [44] .
Los tipos firmados y sin firmar son menores queint | Firmado es menor que sin firmar, y sin firmar no es menosint |
---|---|
#incluir <stdio.h> caracter firmado x = -1 ; carácter sin signo y = 0 ; if ( x > y ) { // la condición es falsa printf ( "No se mostrará el mensaje. \n " ); } si ( x == UCHAR_MAX ) { // la condición es falsa printf ( "No se mostrará el mensaje. \n " ); } | #incluir <stdio.h> caracter firmado x = -1 ; int sin signo y = 0 ; if ( x > y ) { // la condición es verdadera printf ( "Desbordamiento en la variable x. \n " ); } si (( x == UINT_MAX ) && ( x == ULONG_MAX )) { // la condición siempre será verdadera printf ( "Desbordamiento en la variable x. \n " ); } |
En este ejemplo, ambos tipos, firmado y sin firmar, se convertirán en firmado int, porque permite que se ajusten los rangos de ambos tipos. Por tanto, la comparación en el operador condicional será correcta. | Un tipo con signo se convertirá en sin signo porque el tipo sin signo tiene un tamaño mayor o igual que int, pero se producirá un desbordamiento porque es imposible representar un valor negativo en un tipo sin signo. |
Además, la conversión automática de tipos funcionará si se utilizan dos o más tipos enteros diferentes en la expresión. El estándar define un conjunto de reglas según las cuales se elige un tipo de conversión que puede dar el resultado correcto del cálculo. A los diferentes tipos se les asignan diferentes rangos dentro de la transformación, y los rangos mismos se basan en el tamaño del tipo. Cuando diferentes tipos están involucrados en una expresión, generalmente se elige convertir estos valores a un tipo de rango superior [44] .
Números realesLos números de punto flotante en C están representados por tres tipos básicos: float, doubley long double.
Los números reales tienen una representación muy diferente a la de los enteros. Las constantes de números reales de diferentes tipos, escritas en notación decimal, pueden no ser iguales entre sí. Por ejemplo, la condición 0.1 == 0.1fserá falsa debido a la pérdida de precisión en el tipo float, mientras que la condición 0.5 == 0.5fserá verdadera porque estos números son finitos en representación binaria. Sin embargo, la condición (float) 0.1 == 0.1fde conversión también será cierta, porque la conversión a un tipo menos preciso pierde los bits que hacen que las dos constantes sean diferentes.
Las operaciones aritméticas con números reales también son inexactas y suelen tener algún error flotante [45] . El mayor error ocurrirá cuando se opere sobre valores que estén cerca del mínimo posible para un tipo en particular. Además, el error puede resultar grande al calcular simultáneamente números muy pequeños (≪ 1) y muy grandes (≫ 1). En algunos casos, el error se puede reducir cambiando los algoritmos y métodos de cálculo. Por ejemplo, al reemplazar la suma múltiple con la multiplicación, el error puede disminuir tantas veces como originalmente había operaciones de suma.
También en el archivo de encabezado math.hhay dos tipos adicionales float_ty double_t, que corresponden al menos a los tipos floaty doublerespectivamente, pero pueden ser diferentes de ellos. Los tipos float_ty double_tse agregan en el estándar C99 , y su correspondencia con los tipos básicos está determinada por el valor de la macro FLT_EVAL_METHOD.
Tipo de datos | El tamaño | Estándar |
---|---|---|
float | 32 bits | IEC 60559 ( IEEE 754 ), extensión F del estándar C [46] [k] , número de precisión simple |
double | 64 bits | IEC 60559 (IEEE 754), extensión F del estándar C [46] [k] , número de doble precisión |
long double | mínimo 64 bits | dependiente de la implementación |
float_t(C99) | mínimo 32 bits | depende del tipo de base |
double_t(C99) | mínimo 64 bits | depende del tipo de base |
FLT_EVAL_METHOD | float_t | double_t |
---|---|---|
una | float | double |
2 | double | double |
3 | long double | long double |
Aunque no hay un tipo especial para cadenas en C como tal, las cadenas terminadas en nulo se usan mucho en el lenguaje. Las cadenas ASCII se declaran como una matriz de tipo char, cuyo último elemento debe ser el código de carácter 0( '\0'). Es habitual almacenar cadenas UTF-8 en el mismo formato . Sin embargo, todas las funciones que trabajan con cadenas ASCII consideran cada carácter como un byte, lo que limita el uso de funciones estándar al usar esta codificación.
A pesar del uso generalizado de la idea de cadenas terminadas en cero y la conveniencia de usarlas en algunos algoritmos, tienen varios inconvenientes serios.
En las condiciones modernas, cuando se prioriza el rendimiento del código sobre el consumo de memoria, puede ser más eficiente y fácil usar estructuras que contengan tanto la cadena como su tamaño [48] , por ejemplo:
estructura cadena_t { char * cadena ; // puntero a cadena size_t str_size ; // tamaño de cadena }; typedef estructura cadena_t cadena_t ; // nombre alternativo para simplificar el códigoUn enfoque alternativo de almacenamiento de tamaño de cadena de poca memoria sería prefijar la cadena con su tamaño en un formato de tamaño de longitud variable .. Un enfoque similar se usa en los búferes de protocolo , sin embargo, solo en la etapa de transferencia de datos, pero no en su almacenamiento.
Literales de cadenaLos literales de cadena en C son inherentemente constantes [10] . Al declarar, se encierran entre comillas dobles y 0el compilador agrega automáticamente el terminador. Hay dos formas de asignar un literal de cadena: por puntero y por valor. Al asignar por puntero, char *se ingresa un puntero a una cadena inmutable en la variable de tipo, es decir, se forma una cadena constante. Si ingresa un literal de cadena en una matriz, la cadena se copia en el área de la pila.
#incluir <stdio.h> #incluir <cadena.h> int principal ( vacío ) { const char * s1 = "Const string" ; char s2 [] = "Cadena que se puede cambiar" ; memcpy ( s2 , "c" , strlen ( "c" )); // cambia la primera letra a minúscula pone ( s2 ); // se mostrará el texto de la línea memcpy (( char * ) s1 , "a" , strlen ( "a" )); // error de segmentación pone ( s1 ); // la línea no se ejecutará }Dado que las cadenas son matrices regulares de caracteres, se pueden usar inicializadores en lugar de literales, siempre que cada carácter quepa en 1 byte:
char s [] = { 'I' , 'n' , 'i' , 't' , 'i' , 'a' , 'l' , 'i' , 'z' , 'e' , 'r' , '\0' };Sin embargo, en la práctica, este enfoque solo tiene sentido en casos extremadamente raros cuando se requiere no agregar un cero final a una cadena ASCII.
Líneas anchasPlataforma | Codificación |
---|---|
GNU/Linux | USC-4 [49] |
Mac OS | |
ventanas | USC-2 [50] |
AIX | |
FreeBSD | Depende de la localidad
no documentado [50] |
Solaris |
Una alternativa a las cadenas normales son las cadenas anchas, en las que cada carácter se almacena en un tipo especial wchar_t. El tipo dado por el estándar debe ser capaz de contener en sí mismo todos los caracteres de la mayor de las configuraciones regionales existentes . Las funciones para trabajar con cadenas anchas se describen en el archivo de encabezado wchar.hy las funciones para trabajar con caracteres anchos se describen en el archivo de encabezado wctype.h.
Al declarar literales de cadena para cadenas anchas, se usa el modificador L:
const wchar_t * wide_str = L "Cadena ancha" ;La salida formateada usa el especificador %ls, pero el especificador de tamaño, si se proporciona, se especifica en bytes, no en caracteres [51] .
El tipo wchar_tse concibió para que cualquier carácter pudiera caber en él, y cadenas anchas, para almacenar cadenas de cualquier configuración regional, pero como resultado, la API resultó ser un inconveniente y las implementaciones dependían de la plataforma. Entonces, en la plataforma Windows , se eligieron 16 bits como el tamaño del tipo wchar_t, y luego apareció el estándar UTF-32, por lo que el tipo wchar_ten la plataforma Windows ya no puede adaptarse a todos los caracteres de la codificación UTF-32. como resultado de lo cual se pierde el significado de este tipo [ 50] . Al mismo tiempo, en las plataformas Linux [49] y macOS, este tipo ocupa 32 bits, por lo que el tipo no es adecuado para implementar tareas multiplataforma .wchar_t
Cadenas multibyteHay muchas codificaciones diferentes en las que un solo carácter se puede programar con un número diferente de bytes. Tales codificaciones se denominan multibyte. UTF-8 también se aplica a ellos . C tiene un conjunto de funciones para convertir cadenas de varios bytes dentro de la configuración regional actual a ancho y viceversa. Las funciones para trabajar con caracteres multibyte tienen un prefijo o sufijo mby se describen en el archivo de encabezado stdlib.h. Para admitir cadenas multibyte en programas C, dichas cadenas deben admitirse en el nivel de configuración regional actual . Para establecer explícitamente la codificación, puede cambiar la configuración regional actual mediante una función setlocale()del archivo locale.h. Sin embargo, la especificación de una codificación para una configuración regional debe ser compatible con la biblioteca estándar que se utiliza. Por ejemplo, la biblioteca estándar Glibc es totalmente compatible con la codificación UTF-8 y es capaz de convertir texto a muchas otras codificaciones [52] .
A partir del estándar C11, el lenguaje también admite cadenas multibyte de 16 bits y 32 bits de ancho con los tipos de caracteres apropiados char16_ty char32_tdesde un archivo de encabezado uchar.h, así como la declaración de cadenas literales UTF-8 mediante la extensión u8. Las cadenas de 16 y 32 bits se pueden usar para almacenar codificaciones UTF-16 y UTF-32 si las uchar.hdefiniciones de macro __STDC_UTF_16__y se especifican en el archivo de encabezado __STDC_UTF_32__, respectivamente. Para especificar literales de cadena en estos formatos, se utilizan modificadores: upara cadenas de 16 bits y Upara cadenas de 32 bits. Ejemplos de declaración de literales de cadena para cadenas multibyte:
const char * s8 = u8 "Cadena multibyte UTF-8" ; const char16_t * s16 = u "cadena multibyte de 16 bits" ; const char32_t * s32 = U "Cadena multibyte de 32 bits" ;Tenga en cuenta que la función c16rtomb()para convertir una cadena de 16 bits a una cadena de varios bytes no funciona según lo previsto, y en el estándar C11 se descubrió que no podía traducir de UTF-16 a UTF-8 [53] . La corrección de esta función puede depender de la implementación específica del compilador.
Las enumeraciones son un conjunto de constantes enteras con nombre y se indican con la palabra clave enum. Si una constante no está asociada con un número, se establece automáticamente 0para la primera constante de la lista o para un número mayor que el especificado en la constante anterior. En este caso, el tipo de datos de enumeración en sí mismo, de hecho, puede corresponder a cualquier tipo primitivo con o sin signo, en cuyo rango caben todos los valores de enumeración; El compilador decide qué tipo usar. Sin embargo, los valores explícitos para las constantes deben ser expresiones como int[18] .
Un tipo de enumeración también puede ser anónimo si no se especifica el nombre de la enumeración. Las constantes especificadas en dos enumeraciones diferentes son de dos tipos de datos diferentes, independientemente de si las enumeraciones tienen nombre o son anónimas.
En la práctica, las enumeraciones se usan a menudo para indicar estados de autómatas finitos , para establecer opciones para modos operativos o valores de parámetros [54] , para crear constantes enteras y también para enumerar cualquier objeto o propiedad único [55] .
EstructurasLas estructuras son una combinación de variables de diferentes tipos de datos dentro de la misma área de memoria; indicado por la palabra clave struct. Las variables dentro de una estructura se denominan campos de la estructura. Desde el punto de vista del espacio de direcciones, los campos siempre se suceden en el mismo orden en que se especifican, pero los compiladores pueden alinear las direcciones de los campos para optimizar una arquitectura en particular. Así, de hecho, el campo puede tomar un tamaño mayor que el especificado en el programa.
Cada campo tiene un cierto desplazamiento relativo a la dirección de la estructura y un tamaño. El desplazamiento se puede obtener mediante una macro offsetof()del archivo de encabezado stddef.h. En este caso, el desplazamiento dependerá de la alineación y el tamaño de los campos anteriores. El tamaño del campo suele estar determinado por la alineación de la estructura: si el tamaño de la alineación del tipo de datos del campo es menor que el valor de alineación de la estructura, entonces el tamaño del campo está determinado por la alineación de la estructura. La alineación del tipo de datos se puede obtener utilizando la macro alignof()[f] del archivo de encabezado stdalign.h. El tamaño de la estructura en sí es el tamaño total de todos sus campos, incluida la alineación. Al mismo tiempo, algunos compiladores proporcionan atributos especiales que le permiten empaquetar estructuras, eliminando alineaciones de ellas [56] .
Los campos de estructura se pueden establecer explícitamente en tamaño en bits separados por dos puntos después de la definición del campo y el número de bits, lo que limita el rango de sus posibles valores, independientemente del tipo de campo. Este enfoque se puede utilizar como una alternativa a las banderas y máscaras de bits para acceder a ellos. Sin embargo, especificar el número de bits no cancela la posible alineación de los campos de estructuras en la memoria. Trabajar con campos de bits tiene una serie de limitaciones: es imposible aplicarles un operador sizeofo macro alignof(), es imposible obtener un puntero hacia ellos.
AsociacionesLas uniones son necesarias cuando desea hacer referencia a la misma variable como diferentes tipos de datos; indicado por la palabra clave union. Se puede declarar un número arbitrario de campos que se cruzan dentro de la unión, lo que de hecho brinda acceso a la misma área de memoria que diferentes tipos de datos. El compilador elige el tamaño de la unión en función del tamaño del campo más grande de la unión. Debe tenerse en cuenta que cambiar un campo de la unión lleva a un cambio en todos los demás campos, pero solo se garantiza que el valor del campo que ha cambiado es correcto.
Las uniones pueden servir como una alternativa más conveniente para convertir un puntero en un tipo arbitrario. Por ejemplo, al usar una unión colocada en una estructura, puede crear objetos con un tipo de datos que cambia dinámicamente:
Código de estructura para cambiar el tipo de datos sobre la marcha #incluir <stddef.h> enumerar valor_tipo_t { VALUE_TYPE_LONG , // entero VALUE_TYPE_DOUBLE , // número real VALUE_TYPE_STRING , // cadena VALUE_TYPE_BINARY , // datos arbitrarios }; estructura binaria_t { vacío * datos ; // puntero a datos tamaño_t tamaño_datos ; // tamaño de datos }; estructura cadena_t { char * cadena ; // puntero a cadena tamaño_t tamaño_cadena ; // tamaño de la cadena }; unión value_contents_t { tan largo como_largo ; // valor como un entero doble como_doble ; // valor como número real estructura cadena_t como_cadena ; // valor como cadena estructura binary_t como_binario ; // valor como datos arbitrarios }; estructura valor_t { enumerar value_type_t tipo ; // tipo de valor union value_contents_t contenidos ; // contenido del valor }; MatricesLas matrices en C son primitivas y son solo una abstracción sintáctica sobre la aritmética de punteros . Una matriz en sí misma es un puntero a un área de memoria, por lo que solo se puede acceder a toda la información sobre la dimensión de la matriz y sus límites en tiempo de compilación de acuerdo con la declaración de tipo. Los arreglos pueden ser unidimensionales o multidimensionales, pero el acceso a un elemento del arreglo se reduce a simplemente calcular el desplazamiento relativo a la dirección del comienzo del arreglo. Dado que las matrices se basan en la aritmética de direcciones, es posible trabajar con ellas sin usar índices [57] . Entonces, por ejemplo, los siguientes dos ejemplos de lectura de 10 números del flujo de entrada son idénticos entre sí:
Comparación del trabajo a través de índices con el trabajo a través de la aritmética de direccionesCódigo de ejemplo para trabajar con índices | Código de ejemplo para trabajar con aritmética de direcciones |
---|---|
#incluir <stdio.h> int a [ 10 ] = { 0 }; // Inicialización cero sin firmar int count = sizeof ( a ) / sizeof ( a [ 0 ]); for ( int i = 0 ; i < cuenta ; ++ i ) { int * ptr = &a [ yo ]; // Puntero al elemento de matriz actual int n = scanf ( "%8d" , ptr ); si ( norte != 1 ) { perror ( "Error al leer el valor" ); // Manejando el error break ; } } | #incluir <stdio.h> int a [ 10 ] = { 0 }; // Inicialización cero sin firmar int count = sizeof ( a ) / sizeof ( a [ 0 ]); int * a_end = a + cuenta ; // Puntero al elemento que sigue al último for ( int * ptr = a ; ptr != a_end ; ++ ptr ) { int n = scanf ( "%8d" , ptr ); si ( norte != 1 ) { perror ( "Error al leer el valor" ); // Manejando el error break ; } } |
La longitud de las matrices con un tamaño conocido se calcula en tiempo de compilación. El estándar C99 introdujo la capacidad de declarar matrices de longitud variable, cuya longitud se puede establecer en tiempo de ejecución. A tales matrices se les asigna memoria desde el área de la pila, por lo que deben usarse con cuidado si su tamaño se puede establecer desde fuera del programa. A diferencia de la asignación de memoria dinámica, exceder el tamaño permitido en el área de la pila puede tener consecuencias impredecibles, y una longitud de matriz negativa es un comportamiento indefinido . A partir de C11 , las matrices de longitud variable son opcionales para los compiladores, y la falta de soporte está determinada por la presencia de una macro __STDC_NO_VLA__[58] .
Los arreglos de tamaño fijo declarados como variables locales o globales se pueden inicializar dándoles un valor inicial usando llaves y enumerando los elementos del arreglo separados por comas. Los inicializadores de matrices globales solo pueden usar expresiones que se evalúan en tiempo de compilación [59] . Las variables utilizadas en dichas expresiones deben declararse como constantes, con el modificador const. Para matrices locales, los inicializadores pueden contener expresiones con llamadas a funciones y el uso de otras variables, incluido un puntero a la propia matriz declarada.
Desde el estándar C99, se permite declarar una matriz de longitud arbitraria como el último elemento de las estructuras, lo que se usa ampliamente en la práctica y es compatible con varios compiladores. El tamaño de dicha matriz depende de la cantidad de memoria asignada a la estructura. En este caso, no puede declarar una matriz de tales estructuras y no puede colocarlas en otras estructuras. En las operaciones en una estructura de este tipo, una matriz de longitud arbitraria generalmente se ignora, incluso cuando se calcula el tamaño de la estructura, e ir más allá de la matriz implica un comportamiento indefinido [60] .
El lenguaje C no proporciona ningún control sobre los arreglos fuera de los límites, por lo que el propio programador debe monitorear el trabajo con los arreglos. Los errores en el procesamiento de matrices no siempre afectan directamente la ejecución del programa, pero pueden generar errores de segmentación y vulnerabilidades .
Sinónimos de tipoEl lenguaje C le permite crear sus propios nombres de tipo con el typedef. Se pueden dar nombres alternativos tanto a los tipos de sistema como a los definidos por el usuario. Dichos nombres se declaran en el espacio de nombres global y no entran en conflicto con los nombres de los tipos de estructura, enumeración y unión.
Se pueden usar nombres alternativos tanto para simplificar el código como para crear niveles de abstracción. Por ejemplo, algunos tipos de sistemas se pueden acortar para que el código sea más legible o para que sea más uniforme en el código de usuario:
#incluir <stdint.h> definición de tipo int32_t i32_t ; typedef int_fast32_t i32fast_t ; typedef int_least32_t i32least_t ; typedef uint32_t u32_t ; typedef uint_fast32_t u32fast_t ; typedef uint_least32_t u32least_t ;Un ejemplo de abstracción son los nombres de tipo en los archivos de encabezado de los sistemas operativos. Por ejemplo, el estándar POSIX define un tipo pid_tpara almacenar un ID de proceso numérico. De hecho, este tipo es un nombre alternativo para algún tipo primitivo, por ejemplo:
typedef int __kernel_pid_t ; typedef __kernel_pid_t __pid_t typedef __pid_t pid_t ;Dado que los tipos con nombres alternativos son solo sinónimos de los tipos originales, se conserva la plena compatibilidad e intercambiabilidad entre ellos.
El preprocesador trabaja antes de la compilación y transforma el texto del archivo del programa según las directivas encontradas en él o pasadas al preprocesador . Técnicamente, el preprocesador se puede implementar de diferentes maneras, pero lógicamente es conveniente pensar en él como un módulo separado que procesa cada archivo destinado a la compilación y forma el texto que luego ingresa a la entrada del compilador. El preprocesador busca líneas en el texto que comiencen con un carácter #, seguido de directivas de preprocesador. Todo lo que no pertenece a las directivas del preprocesador y no está excluido de la compilación de acuerdo con las directivas se pasa a la entrada del compilador sin cambios.
Las características del preprocesador incluyen:
Es importante comprender que el preprocesador solo proporciona sustitución de texto, sin tener en cuenta la sintaxis y la semántica del lenguaje. Entonces, por ejemplo, las definiciones de macros #definepueden ocurrir dentro de funciones o definiciones de tipos, y las directivas de compilación condicional pueden conducir a la exclusión de cualquier parte del código del texto compilado del programa, sin importar la gramática del lenguaje. Llamar a una macro paramétrica también es diferente de llamar a una función porque la semántica de los argumentos separados por comas no se analiza. Entonces, por ejemplo, es imposible pasar la inicialización de una matriz a los argumentos de una macro paramétrica, ya que sus elementos también están separados por una coma:
#define matriz_de(tipo, matriz) (((tipo) []) (matriz)) int * a ; a = array_of ( int , { 1 , 2 , 3 }); // error de compilación: // macro "array_of" pasó 4 argumentos, pero solo toma 2Las definiciones de macro se usan a menudo para garantizar la compatibilidad con diferentes versiones de bibliotecas que han cambiado las API , incluidas ciertas secciones de código según la versión de la biblioteca. Para estos fines, las bibliotecas a menudo proporcionan definiciones de macros que describen su versión [61] y, a veces, macros con parámetros para comparar la versión actual con la especificada dentro del preprocesador [62] . Las definiciones de macros también se utilizan para la compilación condicional de partes individuales del programa, por ejemplo, para habilitar la compatibilidad con alguna funcionalidad adicional.
Las definiciones de macros con parámetros se utilizan ampliamente en los programas C para crear funciones análogas a las genéricas . Anteriormente, también se usaban para implementar funciones en línea, pero desde el estándar C99, esta necesidad se eliminó debido a la adición de inlinefunciones. Sin embargo, debido al hecho de que las definiciones de macro con parámetros no son funciones, sino que se llaman de manera similar, pueden ocurrir problemas inesperados debido a un error del programador, incluido el procesamiento de solo una parte del código de la definición de macro [63] y prioridades incorrectas para realizar operaciones [64] . Un ejemplo de código erróneo es la macro al cuadrado:
#incluir <stdio.h> int principal ( vacío ) { #define SQR(x) x * x printf ( "%d" , SQR ( 5 )); // todo es correcto, 5*5=25 printf ( "%d" , SQR ( 5 + 0 )); // se supone que es 25, pero generará 5 (5+0*5+0) printf ( "%d" , SQR ( 4/3 ) ) ; // todo correcto, 1 (porque 4/3=1, 1*4=4, 4/3=1) printf ( "%d" , SQR ( 5/2 ) ) ; // se supone que es 4 (2*2), pero generará 5 (5/2*5/2) devolver 0 ; }En el ejemplo anterior, el error es que el contenido del argumento macro se sustituye en el texto tal cual, sin tener en cuenta la precedencia de las operaciones. En tales casos, debe usar inlinefunciones - o priorizar operadores explícitamente en expresiones que usan parámetros de macro usando paréntesis:
#incluir <stdio.h> int principal ( vacío ) { #define SQR(x) ((x) * (x)) printf ( "%d" , SQR ( 4 + 1 )); // cierto, 25 devolver 0 ; }Un programa es un conjunto de archivos C que se pueden compilar en archivos objeto . Luego, los archivos de objeto pasan por un paso de vinculación entre sí, así como con bibliotecas externas, lo que da como resultado el archivo ejecutable o biblioteca final . La vinculación de archivos entre sí, así como con bibliotecas, requiere una descripción de los prototipos de las funciones utilizadas, las variables externas y los tipos de datos necesarios en cada archivo. Es habitual colocar dichos datos en archivos de encabezado separados , que se conectan mediante una directiva #include en aquellos archivos donde se requiere esta o aquella funcionalidad, y le permiten organizar un sistema similar a un sistema de módulos. En este caso, el módulo puede ser:
Dado que la directiva #includesolo sustituye el texto de otro archivo en la etapa de preprocesador , incluir el mismo archivo varias veces puede generar errores en tiempo de compilación. Por lo tanto, dichos archivos utilizan protección contra la reactivación mediante macros #define y #ifndef[65] .
Archivos de código fuenteEl cuerpo de un archivo de código fuente C consta de un conjunto de funciones, tipos y definiciones de datos globales. Las variables y funciones globales declaradas con los especificadores y staticestán inlinedisponibles solo dentro del archivo en el que se declaran, o cuando un archivo se incluye en otro a través de #include. En este caso, las funciones y variables declaradas en el archivo de cabecera con la palabra staticse crearán de nuevo cada vez que se conecte el archivo de cabecera con el siguiente archivo con el código fuente. Las variables globales y los prototipos de funciones declarados con el especificador externo se consideran incluidos de otros archivos. Es decir, se permite su uso de acuerdo con la descripción; se supone que después de compilar el programa, el enlazador los vinculará con los objetos y funciones originales descritos en sus archivos.
Se puede acceder a las variables y funciones globales, a excepción de staticy , desde otros archivos siempre que se declaren correctamente allí con el especificador . También se puede acceder a las variables y funciones declaradas con el modificador en otros archivos, pero solo cuando su dirección se pasa por puntero. Escriba declaraciones y no se puede importar en otros archivos. Si es necesario utilizarlos en otros archivos, deben duplicarse allí o colocarse en un archivo de encabezado separado. Lo mismo se aplica a las funciones -. inlineexternstatictypedefstructunioninline
Punto de entrada del programaPara un programa ejecutable, el punto de entrada estándar es una función denominada main, que no puede ser estática y debe ser la única en el programa. La ejecución del programa comienza desde la primera declaración de la función main()y continúa hasta que sale, después de lo cual el programa termina y devuelve al sistema operativo un código entero abstracto del resultado de su trabajo.
sin argumentos | Con argumentos de línea de comando |
---|---|
int principal ( vacío ); | int principal ( int argc , char ** argv ); |
Cuando se llama, se pasa a la variable argcel número de argumentos pasados al programa, incluida la ruta al programa en sí, por lo que la variable argc normalmente contiene un valor no inferior a 1. La argvlínea de inicio del programa se pasa a la variable como una matriz de cadenas de texto, cuyo último elemento es NULL. El compilador garantiza que main()todas las variables globales del programa se inicializarán cuando se ejecute la función [67] .
Como resultado, la función main()puede devolver cualquier número entero en el rango de valores de tipo int, que se pasará al sistema operativo u otro entorno como el código de retorno del programa [66] . El lenguaje estándar no define el significado de los códigos de retorno [68] . Por lo general, el sistema operativo donde se ejecutan los programas tiene algún medio para obtener el valor del código de retorno y analizarlo. A veces existen ciertas convenciones sobre los significados de estos códigos. La convención general es que un código de retorno de cero indica la finalización exitosa del programa, mientras que un valor distinto de cero representa un código de error. El archivo de encabezado stdlib.hdefine dos definiciones generales de macro EXIT_SUCCESSy EXIT_FAILURE, que corresponden a la finalización exitosa y no exitosa del programa [68] . Los códigos de retorno también se pueden usar dentro de aplicaciones que incluyen varios procesos para proporcionar comunicación entre estos procesos, en cuyo caso la propia aplicación determina el significado semántico de cada código de retorno.
C proporciona 4 formas de asignar memoria, que determinan la vida útil de una variable y el momento en que se inicializa [67] .
Método de selección | Objetivos | tiempo de selección | tiempo de liberación | Gastos generales |
---|---|---|---|---|
Asignación de memoria estática | Variables globales y variables marcadas con palabra clave static(pero sin _Thread_local) | Al inicio del programa | Al final del programa | Perdido |
Asignación de memoria a nivel de hilo | Variables marcadas con palabra clave_Thread_local | Cuando empieza el hilo | Al final de la corriente | Al crear un hilo |
Asignación automática de memoria | Argumentos de función y valores devueltos, variables locales de funciones, incluidos registros y matrices de longitud variable | Al llamar a funciones en el nivel de la pila . | Automático al completar las funciones | Insignificante, ya que solo cambia el puntero a la parte superior de la pila |
Asignación de memoria dinámica | Memoria asignada a través de funciones malloc(), calloc()yrealloc() | Manualmente desde el montón en el momento de llamar a la función utilizada. | Manualmente usando la funciónfree() | Grande tanto para la asignación como para la liberación |
Todos estos métodos de almacenamiento de datos son adecuados en diferentes situaciones y tienen sus propias ventajas y desventajas. Las variables globales no le permiten escribir algoritmos de reentrada y la asignación automática de memoria no le permite devolver un área arbitraria de memoria de una llamada de función. La asignación automática tampoco es adecuada para asignar grandes cantidades de memoria, ya que puede provocar daños en la pila o en el montón [69] . La memoria dinámica no tiene estas deficiencias, pero tiene una gran sobrecarga cuando se usa y es más difícil de usar.
Siempre que sea posible, se prefiere la asignación de memoria automática o estática: el compilador controla esta forma de almacenar objetos , lo que libera al programador de la molestia de asignar y liberar memoria manualmente, que suele ser la fuente de fugas de memoria difíciles de encontrar. errores de segmentación y errores de liberación en el programa . Desafortunadamente, muchas estructuras de datos tienen un tamaño variable en el tiempo de ejecución, por lo que debido a que las áreas asignadas de forma automática y estática deben tener un tamaño fijo conocido en el momento de la compilación, es muy común usar la asignación dinámica.
Para las variables asignadas automáticamente, registerse puede usar un modificador para indicarle al compilador que acceda rápidamente a ellas. Estas variables se pueden colocar en los registros del procesador. Debido al número limitado de registros y posibles optimizaciones del compilador, las variables pueden terminar en la memoria ordinaria, pero sin embargo no será posible obtener un puntero hacia ellas desde el programa [70] . El modificador registeres el único que se puede especificar en los argumentos de función [71] .
Direccionamiento de memoriaEl lenguaje C heredó el direccionamiento de memoria lineal al trabajar con estructuras, matrices y áreas de memoria asignadas. El estándar del lenguaje también permite que se realicen operaciones de comparación en punteros nulos y en direcciones dentro de matrices, estructuras y áreas de memoria asignadas. También se permite trabajar con la dirección del elemento del array que sigue al último, lo que se hace para facilitar la escritura de algoritmos. Sin embargo, no se debe realizar la comparación de punteros de dirección obtenidos para diferentes variables (o áreas de memoria), ya que el resultado dependerá de la implementación de un compilador en particular [72] .
Representación de la memoriaLa representación en memoria de un programa depende de la arquitectura del hardware, del sistema operativo y del compilador. Entonces, por ejemplo, en la mayoría de las arquitecturas, la pila crece hacia abajo, pero hay arquitecturas en las que la pila crece [73] . El límite entre la pila y el montón se puede proteger parcialmente del desbordamiento de la pila mediante un área de memoria especial [74] . Y la ubicación de los datos y el código de las bibliotecas puede depender de las opciones de compilación [75] . El estándar C se abstrae de la implementación y le permite escribir código portátil, pero comprender la estructura de memoria de un proceso ayuda a depurar y escribir aplicaciones seguras y tolerantes a fallas.
Representación típica de la memoria de proceso en sistemas operativos similares a UnixCuando se inicia un programa desde un archivo ejecutable, las instrucciones del procesador (código de máquina) y los datos inicializados se importan a la RAM. main()Al mismo tiempo, los argumentos de la línea de comandos (disponibles en funciones con la siguiente firma en el segundo argumento int argc, char ** argv) y las variables de entorno se importan a direcciones superiores .
El área de datos no inicializados contiene variables globales (incluidas las declaradas como static) que no se han inicializado en el código del programa. Dichas variables se inicializan por defecto a cero después de que se inicia el programa. El área de datos inicializados, el segmento de datos, también contiene variables globales, pero esta área incluye aquellas variables a las que se les ha dado un valor inicial. Los datos inmutables, incluidas las variables declaradas con el modificador const, los literales de cadena y otros literales compuestos, se colocan en el segmento de texto del programa. El segmento de texto del programa también contiene código ejecutable y es de solo lectura, por lo que un intento de modificar los datos de este segmento dará como resultado un comportamiento indefinido en forma de falla de segmentación .
El área de la pila está destinada a contener datos asociados con llamadas a funciones y variables locales. Antes de la ejecución de cada función, la pila se expande para acomodar los argumentos pasados a la función. En el curso de su trabajo, la función puede asignar variables locales en la pila y asignar memoria en ella para arreglos de longitud variable, y algunos compiladores también proporcionan medios para asignar memoria dentro de la pila a través de una llamada alloca()que no está incluida en el estándar del lenguaje. . Una vez que finaliza la función, la pila se reduce al valor que tenía antes de la llamada, pero es posible que esto no suceda si la pila se maneja incorrectamente. La memoria asignada dinámicamente se proporciona desde el montón .
Un detalle importante es la presencia de relleno aleatorio entre la pila y el área superior [77] , así como entre el área de datos inicializados y el montón . Esto se hace por motivos de seguridad, como evitar que se acumulen otras funciones.
Las bibliotecas de vínculos dinámicos y las asignaciones de archivos del sistema de archivos se ubican entre la pila y el montón [78] .
C no tiene ningún mecanismo de control de errores incorporado, pero existen varias formas generalmente aceptadas de manejar los errores usando el lenguaje. En general, la práctica de manejar errores C en código tolerante a fallas obliga a escribir construcciones engorrosas, a menudo repetitivas, en las que el algoritmo se combina con el manejo de errores .
Marcadores de error y errnoEl lenguaje C usa activamente una variable especial errnodel archivo de encabezado errno.h, en la que las funciones ingresan el código de error, mientras devuelven un valor que es el marcador de error. Para verificar el resultado en busca de errores, el resultado se compara con el marcador de error y, si coinciden, puede analizar el código de error almacenado errnopara corregir el programa o mostrar un mensaje de depuración. En la biblioteca estándar, el estándar a menudo solo define los marcadores de error devueltos, y la configuración errnodepende de la implementación [79] .
Los siguientes valores suelen actuar como marcadores de error:
La práctica de devolver un marcador de error en lugar de un código de error, aunque guarda la cantidad de argumentos pasados a la función, en algunos casos conduce a errores como resultado de un factor humano. Por ejemplo, es común que los programadores ignoren la verificación de un resultado de tipo ssize_t, y el resultado en sí mismo se usa más en los cálculos, lo que genera errores sutiles si se devuelve -1[82] .
Devolver el valor correcto como un marcador de error [82] contribuye aún más a la aparición de errores , lo que también obliga al programador a realizar más comprobaciones y, en consecuencia, a escribir más del mismo tipo de código repetitivo. Este enfoque se practica en funciones de flujo que funcionan con objetos de tipo FILE *: el marcador de error es el valor EOF, que también es el marcador de fin de archivo. Por lo tanto, EOFa veces debe verificar el flujo de caracteres tanto para el final del archivo usando la función feof()como para detectar la presencia de un error usando ferror()[83] . Al mismo tiempo, no es necesario configurar algunas funciones que pueden regresar EOFde acuerdo con el estándar errno[79] .
La falta de una práctica unificada de manejo de errores en la biblioteca estándar conduce a la aparición de métodos personalizados de manejo de errores y la combinación de métodos de uso común en proyectos de terceros. Por ejemplo, en el proyecto systemd , se combinaron las ideas de devolver un código de error y un número -1como marcador: se devuelve un código de error negativo [84] . Y la biblioteca GLib introdujo la práctica de devolver un valor booleano como un marcador de error , mientras que los detalles del error se colocan en una estructura especial, cuyo puntero se devuelve a través del último argumento de la función [85] . El proyecto Enlightenment usa una solución similar , que también usa un tipo booleano como marcador, pero devuelve información de error similar a la biblioteca estándar, a través de una función separada [86] que debe verificarse si se devolvió un marcador.
Devolviendo un código de errorUna alternativa a los marcadores de error es devolver el código de error directamente y devolver el resultado de la función a través de argumentos de puntero. Los desarrolladores del estándar POSIX tomaron este camino, en cuyas funciones se acostumbra devolver un código de error como un número de tipo int. Sin embargo, devolver un valor de tipo intno aclara explícitamente que es el código de error lo que se devuelve, y no el token, lo que puede generar errores si el resultado de dichas funciones se compara con el valor -1. La extensión K del estándar C11 introduce un tipo especial errno_tpara almacenar un código de error. Hay recomendaciones para usar este tipo en el código de usuario para devolver errores, y si la biblioteca estándar no lo proporciona, entonces declararlo usted mismo [87] :
#ifndef __STDC_LIB_EXT1__ typedef int errno_t ; #terminara siEste enfoque, además de mejorar la calidad del código, elimina la necesidad de usar errno, lo que le permite crear bibliotecas con funciones reentrantes sin necesidad de incluir bibliotecas adicionales, como POSIX Threads , para definir correctamente errno.
Errores en funciones matemáticasMás complejo es el manejo de errores en funciones matemáticas desde el archivo de cabecera math.h, en el que pueden ocurrir 3 tipos de errores [88] :
La prevención de dos de los tres tipos de errores se reduce a verificar los datos de entrada para el rango de valores válidos. Sin embargo, es extremadamente difícil predecir la salida del resultado más allá de los límites del tipo. Por lo tanto, el lenguaje estándar prevé la posibilidad de analizar funciones matemáticas en busca de errores. A partir del estándar C99, este análisis es posible de dos formas, dependiendo del valor almacenado en el archivo math_errhandling.
En este caso, el método de manejo de errores está determinado por la implementación específica de la biblioteca estándar y puede estar completamente ausente. Por lo tanto, en el código independiente de la plataforma, puede ser necesario comprobar el resultado de dos formas a la vez, según el valor de math_errhandling[88] .
Liberación de recursosNormalmente, la aparición de un error requiere que la función salga y devuelva un indicador de error. Si en una función puede ocurrir un error en diferentes partes de la misma, se requiere liberar los recursos asignados durante su funcionamiento para evitar fugas. Es una buena práctica liberar recursos en orden inverso antes de regresar de la función y, en caso de errores, en orden inverso después del principal return. En partes separadas de dicho lanzamiento, puede saltar usando el operador goto[89] . Este enfoque le permite mover secciones de código que no están relacionadas con el algoritmo que se está implementando fuera del propio algoritmo, lo que aumenta la legibilidad del código y es similar al trabajo de un operador deferdel lenguaje de programación Go . A continuación se muestra un ejemplo de liberación de recursos, en la sección de ejemplos .
Para liberar recursos dentro del programa, se proporciona un mecanismo de controlador de salida del programa. Los controladores se asignan mediante una función atexit()y se ejecutan al final de la función main()a través de una instrucción returny al ejecutar la función exit(). En este caso, las funciones abort()y _Exit()[90] no ejecutan los controladores .
Un ejemplo de liberación de recursos al final de un programa es la liberación de memoria asignada para variables globales. A pesar de que la memoria se libera de una forma u otra después de que el programa termina por el sistema operativo, y se permite no liberar la memoria que se requiere durante la operación del programa [91] , es preferible la desasignación explícita, ya que hace que sea es más fácil encontrar fugas de memoria con herramientas de terceros y reduce la posibilidad de fugas de memoria como resultado de un error:
Ejemplo de código de programa con liberación de recursos #incluir <stdio.h> #incluir <stdlib.h> int numeros_cuenta ; int * números ; void free_numbers ( void ) { libre ( números ); } int principal ( int argc , char ** argv ) { si ( argumento < 2 ) { salir ( EXIT_FAILURE ); } numeros_cuenta = atoi ( argv [ 1 ]); si ( números_cuenta <= 0 ) { salir ( EXIT_FAILURE ); } numeros = calloc ( numeros_cuenta , tamano de ( * numeros )); si ( ! numeros ) { perror ( "Error al asignar memoria para el arreglo" ); salir ( EXIT_FAILURE ); } atexit ( números_libres ); // ... trabajar con matriz de números // El controlador free_numbers() se llamará automáticamente aquí devuelve SALIR_ÉXITO ; }La desventaja de este enfoque es que el formato de los controladores asignables no permite pasar datos arbitrarios a la función, lo que le permite crear controladores solo para variables globales.
Un programa C mínimo que no requiere procesamiento de argumentos es el siguiente:
int principal ( vacío ){}Se permite no escribir un operador returnpara la función main(). En este caso, según el estándar, la función main()devuelve 0, ejecutando todos los controladores asignados a la función exit(). Esto supone que el programa se ha completado con éxito [40] .
¡Hola Mundo!¡Hola , mundo! se da en la primera edición del libro " El lenguaje de programación C " de Kernighan y Ritchie:
#incluir <stdio.h> int main ( void ) // No acepta argumentos { printf ( "¡Hola, mundo! \n " ); // '\n' - nueva línea devuelve 0 ; // Terminación exitosa del programa }Este programa imprime el mensaje Hello, world! ' en la salida estándar .
Manejo de errores utilizando la lectura de archivos como ejemploMuchas funciones de C pueden devolver un error sin hacer lo que se suponía que debían hacer. Los errores deben verificarse y responderse correctamente, incluida a menudo la necesidad de enviar un error de una función a un nivel superior para su análisis. Al mismo tiempo, la función en la que ocurrió un error se puede hacer reentrante , en cuyo caso, por error, la función no debería cambiar los datos de entrada o salida, lo que le permite reiniciarla de manera segura después de corregir la situación de error.
El ejemplo implementa la función para leer un archivo en C, pero requiere que las funciones fopen()y el fread()estándar POSIX cumplan , de lo contrario, es posible que no establezcan la variable errno, lo que complica mucho tanto la depuración como la escritura de código universal y seguro. En plataformas que no sean POSIX, el comportamiento de este programa será indefinido en caso de un error . La desasignación de recursos en errores está detrás del algoritmo principal para mejorar la legibilidad, y la transición se realiza usando [89] . goto
Código de ejemplo del lector de archivos con manejo de errores #include <errno.h> #incluir <stdio.h> #incluir <stdlib.h> // Definir el tipo para almacenar el código de error si no está definido #ifndef __STDC_LIB_EXT1__ typedef int errno_t ; #terminara si enumeración { EOK = 0 , // valor de errno_t en caso de éxito }; // Función para leer el contenido del archivo errno_t get_file_contents ( const char * filename , vacío ** contenido_ptr , tamaño_t * contenido_tamaño_ptr ) { ARCHIVO * f ; f = fopen ( nombre de archivo , "rb" ); si ( ! f ) { // En POSIX, fopen() establece errno por error devuelve errno ; } // Obtener el tamaño del archivo fbuscar ( f , 0 , SEEK_END ); largo contenido_tamaño = ftell ( f ); if ( contenido_tamaño == 0 ) { * contenido_ptr = NULL ; * contenido_tamaño_ptr = 0 ; ir a limpieza_fopen ; } rebobinar ( f ); // Variable para almacenar el código de error devuelto errno_t guardado_errno ; vacío * contenido ; contenido = malloc ( contenido_tamaño ); si ( ! contenidos ) { guardado_errno = errno ; ir a abortar_fopen ; } // Leer todo el contenido del archivo en el puntero de contenido tamaño_tn ; _ n = fread ( contenido , contenido_tamaño , 1 , f ); si ( norte == 0 ) { // No verifique feof() porque se almacena en búfer después de fseek() // POSIX fread() establece errno por error guardado_errno = errno ; ir a abortar_contenidos ; } // Devuelve la memoria asignada y su tamaño * contenido_ptr = contenido ; * tamaño_contenido_ptr = tamaño_contenido ; // Sección de liberación de recursos sobre el éxito limpieza_fopen : fcerrar ( f ); devolver EOK ; // Sección separada para liberar recursos por error abortar_contenidos : gratis ( contenidos ); abortando_fopen : fcerrar ( f ); volver guardado_errno ; } int principal ( int argc , char ** argv ) { si ( argumento < 2 ) { devuelve EXIT_FAILURE ; } const char * nombre de archivo = argv [ 1 ]; errno_t errnum ; vacío * contenido ; tamaño_t contenido_tamaño ; errnum = get_file_contents ( nombre de archivo , & contenido , & contenido_tamaño ); si ( número de error ) { charbuf [ 1024 ] ; const char * error_text = strerror_r ( errnum , buf , sizeof ( buf )); fprintf ( stderr , "%s \n " , texto_error ); salir ( EXIT_FAILURE ); } printf ( "%.*s" , ( int ) contenido_tamaño , contenido ); gratis ( contenidos ); devuelve SALIR_ÉXITO ; }Algunos compiladores se incluyen con compiladores para otros lenguajes de programación (incluido C++ ) o forman parte del entorno de desarrollo de software .
|
A pesar de que la biblioteca estándar es parte del estándar del lenguaje, sus implementaciones están separadas de los compiladores. Por lo tanto, los estándares de idioma admitidos por el compilador y la biblioteca pueden diferir.
Dado que el lenguaje C no proporciona un medio para escribir código de manera segura, y muchos elementos del lenguaje contribuyen a los errores, la escritura de código tolerante a fallas y de alta calidad solo puede garantizarse mediante la escritura de pruebas automatizadas. Para facilitar dichas pruebas, existen varias implementaciones de bibliotecas de pruebas unitarias de terceros .
También hay muchos otros sistemas para probar código C, como AceUnit, GNU Autounit, cUnit y otros, pero no prueban en entornos aislados, proporcionan pocas funciones [100] o ya no se están desarrollando.
Herramientas de depuraciónPor las manifestaciones de errores, no siempre es posible sacar una conclusión inequívoca sobre el área problemática en el código; sin embargo, varias herramientas de depuración a menudo ayudan a localizar el problema.
A veces, para trasladar ciertas bibliotecas, funciones y herramientas escritas en C a otro entorno, es necesario compilar el código C en un lenguaje de nivel superior o en el código de una máquina virtual diseñada para dicho lenguaje. Los siguientes proyectos están diseñados para este propósito:
También para C existen otras herramientas que facilitan y complementan el desarrollo, incluyendo analizadores estáticos y utilidades para el formateo de código. El análisis estático ayuda a identificar posibles errores y vulnerabilidades. Y el formato de código automático simplifica la organización de la colaboración en los sistemas de control de versiones, minimizando los conflictos debido a cambios de estilo.
El lenguaje se usa ampliamente en el desarrollo de sistemas operativos, en el nivel de API del sistema operativo, en sistemas integrados y para escribir código de alto rendimiento o de error crítico. Una de las razones de la adopción generalizada de la programación de bajo nivel es la capacidad de escribir código multiplataforma que se puede manejar de manera diferente en diferentes sistemas operativos y hardware.
La capacidad de escribir código de alto rendimiento se produce a expensas de la total libertad de acción del programador y la ausencia de un control estricto por parte del compilador. Por ejemplo, las primeras implementaciones de Java , Python , Perl y PHP se escribieron en C. Al mismo tiempo, en muchos programas, las partes que más recursos demandan suelen estar escritas en C. El núcleo de Mathematica [109] está escrito en C, mientras que MATLAB , originalmente escrito en Fortran , fue reescrito en C en 1984 [110] .
C también se usa a veces como un lenguaje intermedio al compilar lenguajes de nivel superior. Por ejemplo, las primeras implementaciones de los lenguajes C++ , Objective-C y Go funcionaron de acuerdo con este principio : el código escrito en estos lenguajes se tradujo a una representación intermedia en el lenguaje C. Los lenguajes modernos que funcionan con el mismo principio son Vala y Nim .
Otra área de aplicación del lenguaje C son las aplicaciones en tiempo real , las cuales son exigentes en cuanto a la capacidad de respuesta del código y su tiempo de ejecución. Dichas aplicaciones deben comenzar la ejecución de acciones dentro de un marco de tiempo estrictamente limitado, y las acciones en sí deben encajar dentro de un período de tiempo determinado. En particular, el estándar POSIX.1 proporciona un conjunto de funciones y capacidades para crear aplicaciones en tiempo real [111] [112] [113] , pero el sistema operativo también debe implementar soporte duro en tiempo real [114] .
El lenguaje C ha sido y sigue siendo uno de los lenguajes de programación más utilizados desde hace más de cuarenta años. Naturalmente, su influencia se puede rastrear hasta cierto punto en muchos idiomas posteriores. Sin embargo, entre las lenguas que han alcanzado cierta distribución, son pocas las descendientes directas de C.
Algunos lenguajes descendientes se basan en C con herramientas y mecanismos adicionales que agregan soporte para nuevos paradigmas de programación ( OOP , programación funcional , programación genérica , etc.). Estos lenguajes incluyen principalmente C++ y Objective-C , e indirectamente sus descendientes Swift y D. También se conocen intentos de mejorar C corrigiendo sus defectos más significativos, pero conservando sus características atractivas. Entre ellos podemos mencionar el lenguaje de investigación Cyclone (y su descendiente Rust ). A veces ambas direcciones de desarrollo se combinan en un solo idioma, Go es un ejemplo .
Por separado, es necesario mencionar todo un grupo de lenguajes que, en mayor o menor medida, heredaron la sintaxis básica de C (el uso de llaves como delimitadores de bloques de código, declaración de variables, formas características de operadores for, while, if, switchcon parámetros entre paréntesis, operaciones combinadas ++, --, +=, -=y otros), por lo que los programas en estos lenguajes tienen un aspecto característico asociado específicamente a C. Estos son lenguajes como Java , JavaScript , PHP , Perl , AWK , C# . De hecho, la estructura y la semántica de estos lenguajes son muy diferentes a las de C, y generalmente están destinados a aplicaciones donde nunca se usó el C original.
El lenguaje de programación C++ se creó a partir de C y heredó su sintaxis, complementándola con nuevas construcciones en el espíritu de Simula-67, Smalltalk, Modula-2, Ada, Mesa y Clu [116] . Las principales adiciones fueron soporte para POO (descripción de clases, herencia múltiple, polimorfismo basado en funciones virtuales) y programación genérica (motor de plantillas). Pero además de esto, se han hecho muchas adiciones diferentes al lenguaje. Actualmente, C++ es uno de los lenguajes de programación más utilizados en el mundo y se posiciona como un lenguaje de propósito general con énfasis en la programación de sistemas [117] .
Inicialmente, C ++ mantuvo la compatibilidad con C, lo que se declaró como una de las ventajas del nuevo lenguaje. Las primeras implementaciones de C++ simplemente tradujeron nuevas construcciones a C puro, después de lo cual un compilador de C normal procesó el código. Para mantener la compatibilidad, los creadores de C++ se negaron a excluir algunas de las características de C a menudo criticadas, y en su lugar crearon nuevos mecanismos "paralelos" que se recomiendan al desarrollar un nuevo código C++ (plantillas en lugar de macros, conversión de tipos explícita en lugar de automática). , contenedores de biblioteca estándar en lugar de asignación de memoria dinámica manual, etc.). Sin embargo, desde entonces los lenguajes han evolucionado de forma independiente, y ahora C y C++ de los últimos estándares publicados son solo parcialmente compatibles: no hay garantía de que un compilador de C++ compile con éxito un programa C, y si tiene éxito, no hay garantía de que el programa compilado se ejecutará correctamente. Particularmente molestas son algunas diferencias semánticas sutiles que pueden conducir a un comportamiento diferente del mismo código que es sintácticamente correcto para ambos lenguajes. Por ejemplo, las constantes de caracteres (caracteres entre comillas simples) tienen un tipo inten C y un tipo charen C++ , por lo que la cantidad de memoria ocupada por dichas constantes varía de un idioma a otro. [118] Si un programa es sensible al tamaño de una constante de carácter, se comportará de manera diferente cuando se compile con los compiladores C y C++.
Diferencias como estas dificultan la escritura de programas y bibliotecas que puedan compilar y funcionar de la misma manera tanto en C como en C++ , lo que, por supuesto, confunde a quienes programan en ambos lenguajes. Entre los desarrolladores y usuarios tanto de C como de C++, hay defensores de minimizar las diferencias entre lenguajes, lo que objetivamente traería beneficios tangibles. Existe, sin embargo, un punto de vista opuesto, según el cual la compatibilidad no es especialmente importante, aunque sí útil, y los esfuerzos por reducir la incompatibilidad no deben impedir la mejora de cada idioma individualmente.
Otra opción para extender C con herramientas basadas en objetos es el lenguaje Objective-C , creado en 1983. El subsistema de objetos se tomó prestado de Smalltalk , y todos los elementos asociados con este subsistema se implementan en su propia sintaxis, que es bastante diferente de la sintaxis de C (hasta el hecho de que en las descripciones de clase, la sintaxis para declarar campos es opuesta a la sintaxis para declarar variables en C: primero se escribe el nombre del campo, luego su tipo). A diferencia de C++, Objective-C es un superconjunto de C clásico, es decir, conserva la compatibilidad con el lenguaje de origen; un programa C correcto es un programa Objective-C correcto. Otra diferencia significativa con la ideología de C++ es que Objective-C implementa la interacción de objetos mediante el intercambio de mensajes completos, mientras que C++ implementa el concepto de "enviar un mensaje como una llamada de método". El procesamiento completo de mensajes es mucho más flexible y se adapta naturalmente a la computación paralela. Objective-C, así como su descendiente directo Swift , se encuentran entre los más populares en las plataformas compatibles con Apple .
El lenguaje C es único en el sentido de que fue el primer lenguaje de alto nivel que suplantó seriamente al ensamblador en el desarrollo del software del sistema . Sigue siendo el lenguaje implementado en la mayor cantidad de plataformas de hardware y uno de los lenguajes de programación más populares , especialmente en el mundo del software libre [119] . Sin embargo, el lenguaje tiene muchas deficiencias, desde sus inicios ha sido criticado por muchos expertos.
El lenguaje es muy complejo y está lleno de elementos peligrosos que son muy fáciles de usar mal. Con su estructura y reglas, no soporta programación destinada a crear código de programa confiable y mantenible; por el contrario, nacido en la era de la programación directa para varios procesadores, el lenguaje contribuye a escribir código inseguro y confuso [119] . Muchos programadores profesionales tienden a pensar que el lenguaje C es una herramienta poderosa para crear programas elegantes, pero al mismo tiempo puede usarse para crear soluciones de muy mala calidad [120] [121] .
Debido a varias suposiciones en el lenguaje, los programas pueden compilarse con múltiples errores, lo que a menudo resulta en un comportamiento impredecible del programa. Los compiladores modernos brindan opciones para el análisis de código estático [122] [123] , pero incluso ellos no pueden detectar todos los errores posibles. La programación en C analfabeta puede resultar en vulnerabilidades de software , lo que puede afectar la seguridad de su uso.
Xi tiene un alto umbral de entrada [119] . Su especificación ocupa más de 500 páginas de texto, que deben estudiarse en su totalidad, ya que para crear un código sin errores y de alta calidad, se deben tener en cuenta muchas características no obvias del lenguaje. Por ejemplo, la conversión automática de operandos de expresiones enteras al tipo intpuede dar resultados predecibles difíciles cuando se usan operadores binarios [44] :
carácter sin signo x = 0xFF ; carácter sin firmar y = ( ~ x | 0x1 ) >> 1 ; // Intuitivamente, aquí se espera 0x00 printf ( "y = 0x%hhX \n " , y ); // Imprimirá 0x80 si sizeof(int) > sizeof(char)La falta de comprensión de tales matices puede conducir a numerosos errores y vulnerabilidades. Otro factor que aumenta la complejidad del dominio de C es la falta de retroalimentación del compilador: el lenguaje le da al programador total libertad de acción y permite compilar programas con errores lógicos evidentes. Todo esto dificulta el uso de C en la docencia como primer lenguaje de programación [119]
Finalmente, durante más de 40 años de existencia, el lenguaje se ha vuelto algo obsoleto y es bastante problemático usar muchas técnicas y paradigmas de programación modernos en él .
No existen módulos y mecanismos para su interacción en la sintaxis C. Los archivos de código fuente se compilan por separado y deben incluir prototipos de variables, funciones y tipos de datos importados de otros archivos. Esto se hace mediante la inclusión de archivos de encabezado a través de la sustitución de macros . En el caso de una violación de la correspondencia entre los archivos de código y los archivos de encabezado, pueden ocurrir errores de tiempo de enlace y todo tipo de errores de tiempo de ejecución: desde corrupción de pila y montón hasta errores de segmentación . Dado que la directiva solo sustituye el texto de un archivo por otro, la inclusión de una gran cantidad de archivos de encabezado conduce al hecho de que la cantidad real de código que se compila aumenta muchas veces, lo cual es la razón del rendimiento relativamente lento de compiladores de C. La necesidad de coordinar las descripciones en el módulo principal y los archivos de encabezado dificulta el mantenimiento del programa. #include#include
Advertencias en lugar de erroresEl lenguaje estándar le da al programador más libertad de acción y, por lo tanto, una alta probabilidad de cometer errores. Gran parte de lo que no se permite con mayor frecuencia lo permite el lenguaje y, en el mejor de los casos, el compilador emite advertencias. Aunque los compiladores modernos permiten que todas las advertencias se conviertan en errores, esta característica rara vez se usa y, en la mayoría de los casos, las advertencias se ignoran si el programa se ejecuta satisfactoriamente.
Entonces, por ejemplo, antes del estándar C99, llamar a una función mallocsin incluir un archivo de encabezado stdlib.hpodría provocar daños en la pila, porque en ausencia de un prototipo, la función se llamaba como si devolviera un tipo int, mientras que de hecho devolvía un tipo void*(un se produjo un error cuando los tamaños de los tipos en la plataforma de destino diferían). Aun así, era sólo una advertencia.
Falta de control sobre la inicialización de variablesLos objetos creados de forma automática y dinámica no se inicializan de forma predeterminada y, una vez creados, contienen los valores que quedan en la memoria de los objetos que estaban allí anteriormente. Tal valor es completamente impredecible, varía de una máquina a otra, de ejecución a ejecución, de llamada de función a llamada. Si el programa utiliza dicho valor debido a una omisión accidental de la inicialización, el resultado será impredecible y es posible que no aparezca de inmediato. Los compiladores modernos intentan diagnosticar este problema mediante análisis estático del código fuente, aunque en general es extremadamente difícil resolver este problema mediante análisis estático. Se pueden usar herramientas adicionales para identificar estos problemas en la etapa de prueba durante la ejecución del programa: Valgrind y MemorySanitizer [124] .
Falta de control sobre la aritmética de direccionesLa fuente de situaciones peligrosas es la compatibilidad de punteros con tipos numéricos y la posibilidad de utilizar aritmética de direcciones sin control estricto en las etapas de compilación y ejecución. Esto hace posible obtener un puntero a cualquier objeto, incluido el código ejecutable, y hacer referencia a este puntero, a menos que el mecanismo de protección de la memoria del sistema lo impida .
El uso incorrecto de punteros puede provocar un comportamiento indefinido del programa y tener consecuencias graves. Por ejemplo, un puntero puede no inicializarse o, como resultado de operaciones aritméticas incorrectas, apuntar a una ubicación de memoria arbitraria. En algunas plataformas, trabajar con dicho puntero puede obligar a que el programa se detenga, en otras, puede corromper datos arbitrarios en la memoria; El último error es peligroso porque sus consecuencias son impredecibles y pueden manifestarse en cualquier momento, incluso mucho más tarde que el momento de la acción errónea real.
El acceso a las matrices en C también se implementa mediante la aritmética de direcciones y no implica un medio para verificar la exactitud del acceso a los elementos de la matriz por índice. Por ejemplo, las expresiones a[i]y i[a]son idénticas y simplemente se traducen a la forma *(a + i), y no se realiza la verificación de matriz fuera de los límites. El acceso a un índice mayor que el límite superior de la matriz da como resultado el acceso a los datos ubicados en la memoria después de la matriz, lo que se denomina desbordamiento de búfer . Cuando una llamada de este tipo es errónea, puede conducir a un comportamiento impredecible del programa [57] . A menudo, esta característica se usa en exploits para acceder ilegalmente a la memoria de otra aplicación o a la memoria del kernel del sistema operativo.
Memoria dinámica propensa a erroresLas funciones del sistema para trabajar con la memoria asignada dinámicamente no brindan control sobre la corrección y la puntualidad de su asignación y liberación, la observancia del orden correcto de trabajo con la memoria dinámica es responsabilidad exclusiva del programador. Sus errores, respectivamente, pueden conducir al acceso a direcciones incorrectas, a la liberación prematura o a una fuga de memoria (esto último es posible, por ejemplo, si el desarrollador olvidó llamar free()o llamar a la free()función de llamada cuando fue necesario) [125] .
Uno de los errores comunes es no verificar el resultado de las funciones de asignación de memoria ( malloc(), calloc()y otras) en NULL, mientras que la memoria puede no asignarse si no hay suficiente o si se solicitó demasiado, por ejemplo, debido a la reducción del número -1recibido como resultado de cualquier operación matemática errónea, a un tipo sin signo size_t, con operaciones posteriores sobre él . Otro problema con las funciones de memoria del sistema es el comportamiento no especificado cuando se solicita una asignación de bloque de tamaño cero: las funciones pueden devolver un valor de puntero real o bien, dependiendo de la implementación específica [126] . NULL
Algunas implementaciones específicas y bibliotecas de terceros brindan características como el conteo de referencias y referencias débiles [127] , punteros inteligentes [128] y formas limitadas de recolección de basura [129] , pero todas estas características no son estándar, lo que naturalmente limita su aplicación. .
Cadenas ineficientes e insegurasPara el lenguaje, las cadenas terminadas en cero son estándar, por lo que todas las funciones estándar funcionan con ellas. Esta solución conduce a una pérdida significativa de eficiencia debido al ahorro de memoria insignificante (en comparación con el almacenamiento explícito del tamaño): calcular la longitud de una cadena (función ) requiere recorrer toda la cadena de principio a fin, copiar cadenas también es difícil de optimizar debido a la presencia de un cero terminal [ 48] . Debido a la necesidad de agregar un valor nulo de terminación a los datos de la cadena, se vuelve imposible obtener subcadenas de manera eficiente como segmentos y trabajar con ellas como si fueran cadenas ordinarias; la asignación y manipulación de porciones de cadenas generalmente requiere asignación y desasignación manual de memoria, lo que aumenta aún más la posibilidad de error. strlen()
Las cadenas terminadas en nulo son una fuente común de errores [130] . Incluso las funciones estándar generalmente no verifican el tamaño del búfer de destino [130] y es posible que no agreguen un carácter nulo [131] al final de la cadena , sin mencionar que es posible que no se agregue o sobrescriba debido a un error del programador. [132] .
Implementación insegura de funciones variádicasSi bien admite funciones con un número variable de argumentos , C no proporciona un medio para determinar el número y los tipos de parámetros reales pasados a dicha función, ni un mecanismo para acceder a ellos de forma segura [133] . Informar a la función sobre la composición de los parámetros reales recae en el programador, y para acceder a sus valores, es necesario contar el número correcto de bytes desde la dirección del último parámetro fijo en la pila, ya sea manualmente o usando un conjunto de macros va_argdel archivo de encabezado stdarg.h. Al mismo tiempo, es necesario tener en cuenta el funcionamiento del mecanismo de promoción implícita automática de tipos al llamar a funciones [134] , según el cual los tipos enteros de argumentos menores que intse convierten en int(o unsigned int), pero se floatconvierten en double. Un error en la llamada o en el trabajo con parámetros dentro de la función solo aparecerá durante la ejecución del programa, lo que tendrá consecuencias impredecibles, desde leer datos incorrectos hasta corromper la pila.
printf()Al mismo tiempo, las funciones con un número variable de parámetros ( scanf()y otros) que no pueden verificar si la lista de argumentos coincide con la cadena de formato son los medios estándar de E/S formateada . Muchos compiladores modernos realizan esta verificación para cada llamada, generando advertencias si encuentran una discrepancia, pero en general esta verificación no es posible porque cada función variable maneja esta lista de manera diferente. Es imposible controlar estáticamente incluso todas las llamadas a funciones printf()porque la cadena de formato se puede crear dinámicamente en el programa.
Falta de unificación del manejo de erroresLa sintaxis C no incluye un mecanismo especial de manejo de errores. La biblioteca estándar solo admite los medios más simples: una variable (en el caso de POSIX , una macro) errnodel archivo de encabezado errno.hpara establecer el último código de error y funciones para obtener mensajes de error de acuerdo con los códigos. Este enfoque lleva a la necesidad de escribir una gran cantidad de código repetitivo, mezclando el algoritmo principal con el manejo de errores y, además, no es seguro para subprocesos. Además, incluso en este mecanismo no existe un único orden:
En la biblioteca estándar, los códigos se errnodesignan a través de definiciones de macro y pueden tener los mismos valores, lo que hace imposible analizar los códigos de error a través del operador switch. El idioma no tiene un tipo de datos especial para banderas y códigos de error, se pasan como valores de tipo int. Un tipo separado errno_tpara almacenar el código de error apareció solo en la extensión K del estándar C11 y es posible que los compiladores no lo admitan [87] .
Las deficiencias de C son bien conocidas desde hace mucho tiempo, y desde el inicio del lenguaje ha habido muchos intentos de mejorar la calidad y la seguridad del código C sin sacrificar sus capacidades.
Medios de análisis de corrección de códigoCasi todos los compiladores de C modernos permiten un análisis de código estático limitado con advertencias sobre posibles errores. También se admiten opciones para incrustar comprobaciones de matriz fuera de los límites, destrucción de pilas, fuera de los límites del montón, lectura de variables no inicializadas, comportamiento indefinido, etc. en el código. Sin embargo, las comprobaciones adicionales pueden afectar el rendimiento de la aplicación final, por lo que son se usa con mayor frecuencia solo en la etapa de depuración.
Existen herramientas de software especiales para el análisis estático del código C para detectar errores que no son de sintaxis. Su uso no garantiza los programas libres de errores, pero le permite identificar una parte significativa de los errores típicos y las vulnerabilidades potenciales. El efecto máximo de estas herramientas no se logra con un uso ocasional, sino cuando se usan como parte de un sistema bien establecido de control constante de la calidad del código, por ejemplo, en sistemas de implementación e integración continua. También puede ser necesario anotar el código con comentarios especiales para excluir falsas alarmas del analizador en las secciones correctas del código que formalmente caen bajo los criterios de las erróneas.
Estándares de programación segurosSe ha publicado una cantidad significativa de investigaciones sobre la programación adecuada en C, que van desde pequeños artículos hasta libros extensos. Se adoptan estándares corporativos y de la industria para mantener la calidad del código C. En particular:
El conjunto de estándares POSIX contribuye a compensar algunas de las deficiencias del lenguaje . La instalación está estandarizada errnopor muchas funciones, lo que permite manejar los errores que ocurren, por ejemplo, en las operaciones de archivos, y se introducen análogos seguros para subprocesos de algunas funciones de la biblioteca estándar, cuyas versiones seguras están presentes en el lenguaje estándar solo en la extensión K [137] .
diccionarios y enciclopedias | ||||
---|---|---|---|---|
|
Lenguajes de programación | |
---|---|
|
lenguaje de programación c | |
---|---|
compiladores |
|
bibliotecas | |
Peculiaridades | |
algunos descendientes | |
C y otros lenguajes |
|
Categoría:Lenguaje de programación C |