C (lenguaje de programación)

La versión actual de la página aún no ha sido revisada por colaboradores experimentados y puede diferir significativamente de la versión revisada el 4 de agosto de 2022; las comprobaciones requieren 3 ediciones .
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 .

Historia

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.

Información general

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.

Sintaxis y semántica

Fichas

Alfabeto del idioma

El idioma utiliza todos los caracteres del alfabeto latino , números y algunos caracteres especiales [7] .

La composición del alfabeto [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
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 .

Identificadores

Un 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 literales

Los 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] .

Orden de asignación de tipos de datos a constantes enteras según su valor [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
Ejemplos de escribir un número real 1.5
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.

Prefijos constantes de caracteres [13]
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.

Prefijos constantes de cadena [15]
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 nombre Comparación de métodos para establecer constantes [17]
Macro #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 constante

Al 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] :

  • para especificar el tamaño de los campos de bits,
  • para establecer el tamaño de una matriz (excepto las matrices de longitud variable),
  • para establecer el valor de un elemento de enumeración,
  • como el valor del operador case.
Palabras clave

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 del lenguaje C [19]
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
Identificadores reservados

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] .

Comentarios

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 .

Operadores

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 unarios

Los 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 ] Operadores C unarios [22]
+ 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 binarios

Los operadores binarios se ubican entre dos argumentos y realizan una operación sobre ellos:

[ operando ] [ operador ] [ operando ] Operadores binarios básicos [23]
+ 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.

Operadores binarios de asignación a la izquierda [24]
= 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
Operadores ternarios

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:

  • [ condición ] - una condición lógica que se verifica para la verdad,
  • [ expresión1 ] - expresión, cuyo valor se devuelve como resultado de la operación, si la condición es verdadera;
  • [ expresión2 ] es la expresión cuyo valor se devuelve como resultado de la operación si la condición es falsa.

El operador en este caso es una combinación de signos ?y :.

Expresiones

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 operaciones

La 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 secundarios

El 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] .

Puntos de secuencia definidos por el estándar [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] :

  • un inicializador que no forma parte de un literal compuesto;
  • expresión aislada;
  • una expresión especificada como la condición de una sentencia condicional ( if) o una sentencia de selección ( switch);
  • una expresión especificada como una condición de bucle whilecon una precondición o una poscondición;
  • cada uno de los parámetros del bucle for, si los hay;
  • expresión de operador return, si se especifica una.

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 5

Otros 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 indefinido

Sentencias de control

Las 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ía

La 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] .

Instrucciones

Una 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 instrucciones

Las 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 condicionales

Hay dos operadores condicionales en el lenguaje que implementan la bifurcación del programa:

  • declaración ifque contiene una sola prueba de condición,
  • y una declaración switchque contiene múltiples condiciones para ser verificadas.

La forma más simple del operador.if

if(( condición ) )( operador ) ( siguiente declaración )

El operador iffunciona así:

  • si la condición entre paréntesis es verdadera, entonces se ejecuta la primera instrucción y luego se ejecuta la instrucción posterior a la instrucción if.
  • si la condición especificada entre paréntesis no se cumple, la declaración especificada después de la declaración se ejecuta inmediatamente if.

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 bucle

Un bucle es una pieza de código que contiene

  • condición de ejecución de bucle: una condición que se verifica constantemente;
  • y el cuerpo del ciclo es una declaración simple o compuesta cuya ejecución depende de la condición del ciclo.

En consecuencia, hay dos tipos de ciclos:

  • un ciclo con una condición previa , donde primero se verifica la condición de ejecución del ciclo y, si se cumple la condición, se ejecuta el cuerpo del ciclo;
  • un ciclo con una condición posterior, donde la condición de continuación del ciclo se verifica después de la ejecución del cuerpo del ciclo.

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 incondicionales

Los 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 breakinterrumpe inmediatamente la ejecución del cuerpo del bucle y el control se transfiere a la instrucción que sigue inmediatamente al bucle;
  • el operador continueinterrumpe la ejecución de la iteración actual del bucle e inicia un intento de pasar a la siguiente.

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ón

El 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 .

Variables

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

  • [descriptor] - tipo de variable y modificadores opcionales que preceden al tipo;
  • [nombre] — nombre de la variable;
  • [inicializador]: el valor inicial de la variable asignada cuando se crea.

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 .

Funciones

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:

  • informar el nombre (identificador) de la función,
  • enumerar los parámetros de entrada (argumentos)
  • y especifique el tipo de devolución.

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ón

Una declaración de función tiene el siguiente formato:

[descriptor] [nombre] ([lista] );,

dónde

  • [descriptor] — descriptor de tipo del valor devuelto por la función;
  • [nombre] - nombre de la función (identificador único de la función);
  • [lista] - una lista de parámetros (formales) de la función o voiden su ausencia [35] .

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:

  • externindica que la definición de la función está en otro módulo ;
  • staticdefine una función estática que solo se puede usar en el módulo actual.

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ón

La 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ón

La llamada de función es para realizar las siguientes acciones:

  • guardar el punto de llamada en la pila;
  • asignación automática de memoria para variables correspondientes a los parámetros formales de la función;
  • inicialización de variables con los valores de las variables (parámetros reales de la función) pasados ​​​​a la función cuando se llama, así como la inicialización de aquellas variables para las cuales los valores predeterminados se especifican en la declaración de la función, pero para los cuales los parámetros reales que les corresponden no se especificaron durante la llamada;
  • pasando el control al cuerpo de la función.

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 int

C 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.

Tipos de datos

Tipos primitivos

Enteros

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.

Tipos de datos básicos para almacenar números enteros
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.
Tipos enteros auxiliares

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 entero

Las 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] .

Comparación de tipos automáticos correctos e incorrectos.
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 reales

Los 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.

Tipos de datos reales
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
Cumplimiento de tipos adicionales con los básicos [47]
FLT_EVAL_METHOD float_t double_t
una float double
2 double double
3 long double long double

Cuerdas

Cadenas terminadas en cero

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.

  1. La necesidad de agregar un carácter terminal al final de la cadena no permite obtener una subcadena sin necesidad de copiarla, y el lenguaje no proporciona funciones para trabajar con un puntero a una subcadena y su longitud.
  2. Si se requiere asignar memoria por adelantado para el resultado de un algoritmo basado en datos de entrada, cada vez que se requiere recorrer toda la cadena para calcular su longitud.
  3. Cuando se trabaja con grandes cantidades de texto, el cálculo de la longitud puede ser un cuello de botella .
  4. Trabajar con una cadena que no termina en nulo por error puede provocar un comportamiento indefinido del programa, incluidos errores de segmentación , errores de desbordamiento de búfer y vulnerabilidades .

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ódigo

Un 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 cadena

Los 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 anchas Tipo de codificación wchar_tdependiendo de la plataforma
Plataforma 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 multibyte

Hay 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.

Tipos personalizados

Enumeraciones

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] .

Estructuras

Las 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.

Asociaciones

Las 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 }; Matrices

Las 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 direcciones
Có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 tipo

El 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.

Preprocesador

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:

  • sustitución de un lexema dado con texto usando la directiva #define, incluida la capacidad de crear plantillas de texto parametrizadas (llamadas de manera similar a las funciones), así como cancelar dichas sustituciones, lo que hace posible realizar la sustitución en áreas limitadas del texto del programa;
  • incrustación condicional y eliminación de partes del texto, incluidas las propias directivas, utilizando los comandos condicionales #ifdef, #ifndef, y #if;#else#endif
  • incrustar texto de otro archivo en el archivo actual usando la extensión #include.

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 2

Las 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 ; }

Programación en C

Estructura del programa

Módulos

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:

  • un conjunto de archivos individuales con código fuente, cuya interfaz se presenta en forma de archivos de cabecera;
  • una biblioteca de objetos o parte de ella, con los archivos de encabezado apropiados;
  • un conjunto autónomo de uno o más archivos de encabezado (biblioteca de interfaz);
  • biblioteca estática o parte de ella con archivos de encabezado apropiados;
  • biblioteca dinámica o parte de ella con archivos de encabezado apropiados.

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 fuente

El 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 programa

Para 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.

Prototipos de funciones válidas main()[66]
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.

Trabajando con la memoria

Modelo de memoria

C proporciona 4 formas de asignar memoria, que determinan la vida útil de una variable y el momento en que se inicializa [67] .

Métodos de asignación de memoria [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 memoria

El 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 memoria

La 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 Unix

Cuando 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] .

Manejo de errores

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 errno

El 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:

  • -1para el tipo inten los casos en que no se utiliza un rango de resultado negativo [80] ;
  • -1para tipo ssize_t(POSIX) [81] ;
  • (size_t) -1para tipo size_t[80] ;
  • (time_t) -1al usar algunas funciones para trabajar con el tiempo [80] ;
  • NULLpara punteros [80] ;
  • EOFcuando se transmiten archivos [80] ;
  • código de error distinto de cero [80] .

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 error

Una 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 si

Este 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áticas

Má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] :

  • ir más allá del rango de valores de entrada;
  • obtener un resultado infinito para datos de entrada finitos;
  • el resultado está fuera del rango del tipo de datos que se está utilizando.

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.

  1. Si el bit está establecido , primero se debe restablecer MATH_ERRNOla variable a y, después de llamar a la función matemática, verificar si hay errores y .errno0EDOMERANGE
  2. Si el bit está establecido MATH_ERREXCEPT, los posibles errores matemáticos se restablecen previamente mediante la función feclearexcept()del archivo de encabezado fenv.hy, después de llamar a la función matemática, se prueban utilizando la función fetestexcept().

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 recursos

Normalmente, 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.

Ejemplos de programas en C

Programa mínimo en C

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 ejemplo

Muchas 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 ; }

Herramientas de desarrollo

Compiladores

Algunos compiladores se incluyen con compiladores para otros lenguajes de programación (incluido C++ ) o forman parte del entorno de desarrollo de software .

  • La colección de compiladores GNU (GCC) es totalmente compatible con los estándares C99 y C17 ( C11 con correcciones) [92] . También admite extensiones GNU, protección de código con desinfectantes y una gran cantidad de características adicionales, incluidos los atributos.
  • Clang también es totalmente compatible con los estándares C99 [93] y C17 [94] . Desarrollado para ser en gran parte compatible con el compilador GCC, incluido el soporte para extensiones GNU y protección de desinfectante de código.
Implementaciones de la biblioteca estándar

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.

  • La biblioteca de código abierto glibc es el núcleo de muchas distribuciones de GNU/Linux , es compatible con los estándares C11 y POSIX.1-2008 [95] y proporciona un conjunto de correcciones y mejoras de GNU .
  • La biblioteca de código abierto musl fue concebida como un reemplazo liviano para glibc, utilizada como la biblioteca predeterminada en la distribución Alpine Linux [96] , Void Linux [97] .
  • La biblioteca CRT de Microsoft es compatible con el estándar C99 y se envía como un componente de Windows 10 [98] .
Entornos de Desarrollo Integrado
  • CLion es totalmente compatible con C99, pero la compatibilidad con C11 es parcial [99] , la compilación se basa en CMake.
  • Code::Blocks  es un entorno de desarrollo integrado multiplataforma gratuito para lenguajes C, C++, D, Fortran. Admite más de dos docenas de compiladores. Con el compilador GCC , todas las versiones C desde C90 a C17 están disponibles.
  • Eclipse  es un IDE gratuito que admite el lenguaje C estándar C99. Tiene una arquitectura modular, lo que permite conectar soporte para diferentes lenguajes de programación y características adicionales. Hay disponible un módulo de integración de Git , pero no hay integración de CMake .
  • KDevelop  es un IDE gratuito que admite algunas de las funciones del lenguaje C del estándar C11. Le permite administrar proyectos utilizando diferentes lenguajes de programación, incluidos C ++ y Python , es compatible con el sistema de compilación CMake. Tiene soporte incorporado para Git a nivel de archivo y formato de código fuente personalizable para diferentes idiomas.
  • Microsoft Visual Studio solo admite parcialmente los estándares C99 y C11, ya que se centra en el desarrollo de C++, pero tiene compatibilidad integrada con CMake.
Herramientas de pruebas unitarias

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 .

  • La biblioteca Check proporciona un marco para probar el código C en el estilo xUnit común . Entre las posibilidades, podemos mencionar la ejecución de pruebas en procesos separados a través fork()de , que le permite reconocer errores de segmentación en las pruebas [100] y también permite establecer el tiempo máximo de ejecución para pruebas individuales.
  • La biblioteca Google Test también proporciona pruebas de estilo xUnit, pero está diseñada para probar código C++ , lo que permite que también se use para probar código C. También admite pruebas aisladas de partes individuales del programa. Una de las ventajas de la biblioteca es la separación de las macros de prueba en aserciones y errores, lo que puede facilitar la depuración del código.

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ón

Por 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.

  • Gdb  es un depurador de consola interactivo para varios lenguajes, incluido C.
  • Valgrind es una herramienta de análisis de código dinámico que puede detectar errores en el código directamente durante la ejecución del programa. Admite la detección de: fugas, accesos a memoria no inicializada, accesos a direcciones no válidas (incluidos desbordamientos de búfer). También es compatible con la ejecución en modo de creación de perfiles mediante callgrind [101] profiler .
  • KCacheGrind  es una interfaz gráfica para visualizar los resultados de creación de perfiles obtenidos utilizando el generador de perfiles callgrind [102] .
Compiladores para lenguajes y plataformas dinámicas

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:

Herramientas adicionales

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.

  • Cppcheck es un  analizador de código estático de código abierto para C y C++ que a veces da falsos positivos que pueden ser suprimidos por comentarios con formato especial en el código.
  • Clang-format  es una utilidad de línea de comandos para formatear el código fuente según un estilo determinado, que se puede especificar en un archivo de configuración especialmente diseñado. Tiene muchas opciones y varios estilos incorporados. Desarrollado como parte del proyecto Clang [107] .
  • Las utilidades Indent y GNU Indent también brindan formato de código, pero las opciones de formato se especifican como opciones de línea de comandos [108] .

Alcance

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] .

Idiomas descendientes

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.

C++

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.

Objetivo-C

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 .

Problemas y críticas

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.

Críticas generales

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 .

Inconvenientes de ciertos elementos del lenguaje

Soporte de modularidad primitiva

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 errores

El 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 variables

Los 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 direcciones

La 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 errores

Las 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 inseguras

Para 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ádicas

Si 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 errores

La 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 caso de error , y el código en sí debe obtenerse si la función lo expone;-1errno
  • es habitual en POSIX devolver un código de error directamente, pero no todas las funciones de POSIX lo hacen;
  • en muchas funciones, por ejemplo, fopen()y fread(), fwrite()la configuración errnono está estandarizada y puede diferir en diferentes implementaciones [79] (en POSIX, los requisitos son más estrictos y se especifican algunas de las opciones para posibles errores );
  • hay funciones en las que el marcador de error es uno de los valores de retorno permitidos y, antes de llamarlos, debe establecerlos en cero errnopara asegurarse de que el código de error fue establecido por esta función [79] .

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] .

Formas de superar las carencias del lenguaje

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ódigo

Casi 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 seguros

Se 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:

  • MISRA C  es un estándar desarrollado por Motor Industry Software Reliability Association para el uso de C en el desarrollo de sistemas integrados de vehículos. Ahora MISRA C se utiliza en muchas industrias, incluidas la militar, la médica y la aeroespacial. La edición de 2013 contiene 16 directivas y 143 reglas, incluidos los requisitos del código y las restricciones sobre el uso de ciertas características del lenguaje (por ejemplo, está prohibido el uso de funciones con un número variable de parámetros). Hay alrededor de una docena de herramientas de verificación de código MISRA C en el mercado y varios compiladores con verificación de restricciones MISRA C incorporada.
  • El estándar de codificación CERT C  es un estándar que está desarrollando el Centro de Coordinación CERT [135] . También tiene como objetivo proporcionar una programación C confiable y segura. Incluye reglas y pautas para desarrolladores, incluidos ejemplos de código incorrecto y correcto caso por caso. El estándar es utilizado en el desarrollo de productos por empresas como Cisco y Oracle [136] .
Estándares POSIX

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] .

Véase también

Notas

Comentarios

  1. B es la segunda letra del alfabeto inglés y C es la tercera letra del alfabeto inglés .
  2. La macro booldel archivo de encabezado stdbool.hes un contenedor sobre la palabra clave _Bool.
  3. La macro complexdel archivo de encabezado complex.hes un contenedor sobre la palabra clave _Complex.
  4. La macro imaginarydel archivo de encabezado complex.hes un contenedor sobre la palabra clave _Imaginary.
  5. La macro alignasdel archivo de encabezado stdalign.hes un contenedor sobre la palabra clave _Alignas.
  6. 1 2 3 La macro alignofdel archivo de encabezado stdalign.hes un contenedor sobre la palabra clave _Alignof.
  7. La macro noreturndel archivo de encabezado stdnoreturn.hes un contenedor sobre la palabra clave _Noreturn.
  8. La macro static_assertdel archivo de encabezado assert.hes un contenedor sobre la palabra clave _Static_assert.
  9. La macro thread_localdel archivo de encabezado threads.hes un contenedor sobre la palabra clave _Thread_local.
  10. 1 2 3 4 5 6 7 La primera aparición de los tipos firmados y sin firmar , y charfue shorten K&R C.intlong
  11. 1 2 La conformidad del formato de tipo con floatel doubleestándar IEC 60559 se define mediante la extensión C F, por lo que el formato puede diferir en plataformas o compiladores individuales.

Fuentes

  1. 1 2 http://www.bell-labs.com/usr/dmr/www/chist.html
  2. Rui Ueyama. Cómo escribí un compilador de C autohospedado en 40 días  . www.sigbus.info (diciembre de 2015). Consultado el 18 de febrero de 2019. Archivado desde el original el 23 de marzo de 2019.
  3. Un recolector de basura para C y C++ Archivado el 13 de octubre de 2005 en Wayback Machine . 
  4. Programación orientada a objetos con ANSI-C Archivado el 6 de marzo de 2016 en Wayback Machine . 
  5. Tipos clasificados instanciables:  objetos . Manual de referencia de GObject . desarrollador.gnome.org. Consultado el 27 de mayo de 2019. Archivado desde el original el 27 de mayo de 2019.
  6. Tipos clasificados no instanciables:  interfaces . Manual de referencia de GObject . desarrollador.gnome.org. Consultado el 27 de mayo de 2019. Archivado desde el original el 27 de mayo de 2019.
  7. 1 2 Borrador del estándar C17 , 5.2.1 Conjuntos de caracteres, p. 17
  8. 12 Borrador del estándar C17 , 6.4.2 Identificadores, p. 43-44.
  9. Borrador del estándar C17 , 6.4.4 Constantes, p. 45-50.
  10. 1 2 Podbelsky, Fomin, 2012 , p. 19
  11. 12 Borrador del estándar C17 , 6.4.4.1 Constantes enteras, p. 46.
  12. Borrador del estándar C17 , 6.4.4.2 Constantes flotantes, p. 47-48.
  13. 1 2 Borrador del estándar C17 , 6.4.4.4 Constantes de caracteres, p. 49-50.
  14. STR30-C.  No intente modificar los literales de cadena - SEI CERT C Coding Standard - Confluence . wiki.sei.cmu.edu. Consultado el 27 de mayo de 2019. Archivado desde el original el 27 de mayo de 2019.
  15. Borrador del estándar C17 , 6.4.5 Literales de cadena, p. 50-52.
  16. Opciones de estilo de formato de Clang:  documentación de Clang 9 . clang.llvm.org. Consultado el 19 de mayo de 2019. Archivado desde el original el 20 de mayo de 2019.
  17. ↑ 1 2 3 4 DCL06-C. Use constantes simbólicas significativas para representar valores literales - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 6 de febrero de 2019. Archivado desde el original el 7 de febrero de 2019.
  18. 1 2 Borrador del estándar C17 , p. 84.
  19. Borrador del estándar C17 , 6.4.1 Palabras clave, p. 42.
  20. ↑ 1 2 Fundación de Software Libre (FSF). Estado de las características de C99 en GCC  . Proyecto GNU . gcc.gnu.org. Consultado el 31 de mayo de 2019. Archivado desde el original el 3 de junio de 2019.
  21. 1 2 3 4 Borrador del estándar C17 , 7.1.3 Identificadores reservados, p. 132.
  22. Borrador del estándar C17 , 6.5.3 Operadores unarios, p. 63-65.
  23. Borrador del estándar C17 , 6.5 Expresiones, p. 66-72.
  24. Borrador del estándar C17 , 6.5.16 Operadores de asignación, p. 72-74.
  25. Borrador del estándar C17 , p. 55-75.
  26. ↑ 1 2 El Manual de referencia de GNU C . 3.19  Precedencia de operadores . www.gnu.org . Consultado el 13 de febrero de 2019. Archivado desde el original el 7 de febrero de 2019.
  27. ↑ 1 2 3 4 5 EXP30-C. No dependa del orden de evaluación de los efectos secundarios - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 14 de febrero de 2019. Archivado desde el original el 15 de febrero de 2019.
  28. ↑ 12BB._ _ _ Definiciones - Estándar de codificación SEI CERT C -  Confluencia . wiki.sei.cmu.edu. Consultado el 16 de febrero de 2019. Archivado desde el original el 16 de febrero de 2019.
  29. Podbelsky, Fomin, 2012 , 1.4. Operaciones, pág. 42.
  30. Podbelsky, Fomin, 2012 , 2.3. Declaraciones de bucle, pág. 78.
  31. ↑ 12 EXP19- C. Use llaves para el cuerpo de una declaración if, for o while - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 2 de junio de 2019. Archivado desde el original el 2 de junio de 2019.
  32. Bibliotecas cargadas dinámicamente (DL)  . tldp.org. Consultado el 18 de febrero de 2019. Archivado desde el original el 12 de noviembre de 2020.
  33. 1 2 Borrador del estándar C17, 6.7.4 Especificadores de funciones , p. 90-91.
  34. PRE00-C. Prefiere funciones en línea o estáticas a macros similares a funciones - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 4 de junio de 2019. Archivado desde el original el 7 de agosto de 2021.
  35. Borrador del estándar C17 , 6.11 Direcciones futuras del lenguaje, p. 130.
  36. ¿Admite C la sobrecarga de funciones? | Geeks para Geeks . Fecha de acceso: 15 de diciembre de 2013. Archivado desde el original el 15 de diciembre de 2013.
  37. El Manual de referencia de GNU C. www.gnu.org. Consultado el 21 de mayo de 2017. Archivado desde el original el 27 de abril de 2021.
  38. Ancho de tipo (La biblioteca GNU C  ) . www.gnu.org. Consultado el 7 de diciembre de 2018. Archivado desde el original el 9 de diciembre de 2018.
  39. Borrador del estándar C17 , 6.2.5 Tipos, p. 31
  40. ↑ 1 2 Comité técnico conjunto ISO/IEC JTC 1. ISO/IEC 9899:201x. Lenguajes de programación - C. - ISO/IEC, 2011. - P. 14. - 678 p. Archivado el 30 de mayo de 2017 en Wayback Machine .
  41. Verifique 0.10.0: 4.  Funciones avanzadas . comprobar _ comprobar.sourceforge.net. Consultado el 11 de febrero de 2019. Archivado desde el original el 18 de mayo de 2018.
  42. Macros de conversión de tipos:  Manual de referencia de GLib . desarrollador.gnome.org. Consultado el 14 de enero de 2019. Archivado desde el original el 14 de enero de 2019.
  43. INT01-C. Use rsize_t o size_t para todos los valores enteros que representan el tamaño de un objeto - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 22 de febrero de 2019. Archivado desde el original el 7 de agosto de 2021.
  44. ↑ 1 2 3 INT02-C. Comprender las reglas de conversión de enteros - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Fecha de acceso: 22 de febrero de 2019. Archivado desde el original el 22 de febrero de 2019.
  45. FLP02-C. Evite el uso de números de punto flotante cuando se necesita un cálculo preciso - Estándar de codificación SEI CERT C -  Confluencia . wiki.sei.cmu.edu. Consultado el 21 de mayo de 2019. Archivado desde el original el 7 de agosto de 2021.
  46. 1 2 Borrador del estándar C17 , aritmética de punto flotante IEC 60559, p. 370.
  47. Borrador del estándar C17 , 7.12 Matemáticas <math.h>, p. 169-170.
  48. ↑ 1 2 Campamento de Poul-Henning. El error de un byte más costoso: la cola ACM  . queue.acm.org (25 de julio de 2011). Consultado el 28 de mayo de 2019. Archivado desde el original el 30 de abril de 2019.
  49. ↑ 1 2 unicode(7) - Página del manual de Linux  . man7.org. Consultado el 24 de febrero de 2019. Archivado desde el original el 25 de febrero de 2019.
  50. ↑ 1 2 3 El wchar_t mess - GNU libunistring  . www.gnu.org. Consultado el 2 de enero de 2019. Archivado desde el original el 17 de septiembre de 2019.
  51. ↑ Programación con caracteres  anchos . linux.com | La fuente de información de Linux (11 de febrero de 2006). Consultado el 7 de junio de 2019. Archivado desde el original el 7 de junio de 2019.
  52. Markus Kuhn . Preguntas frecuentes sobre UTF-8 y Unicode  . www.cl.cam.ac.uk. Consultado el 25 de febrero de 2019. Archivado desde el original el 27 de febrero de 2019.
  53. Resumen del informe de defectos para C11 . www.open-std.org. Consultado el 2 de enero de 2019. Archivado desde el original el 1 de enero de 2019.
  54. ↑ Enumeraciones estándar : Manual de referencia de GTK+ 3  . desarrollador.gnome.org. Consultado el 15 de enero de 2019. Archivado desde el original el 14 de enero de 2019.
  55. ↑ Propiedades de objeto : Manual de referencia de GObject  . desarrollador.gnome.org. Consultado el 15 de enero de 2019. Archivado desde el original el 16 de enero de 2019.
  56. Uso de la colección de compiladores GNU (GCC):  atributos de tipo común . gcc.gnu.org. Fecha de acceso: 19 de enero de 2019. Archivado desde el original el 16 de enero de 2019.
  57. ↑ 12 ARR00- C.  Comprender cómo funcionan las matrices - SEI CERT C Coding Standard - Confluence . wiki.sei.cmu.edu. Consultado el 30 de mayo de 2019. Archivado desde el original el 30 de mayo de 2019.
  58. ARR32-C. Asegúrese de que los argumentos de tamaño para las matrices de longitud variable estén en un rango válido: estándar de codificación SEI CERT C:  confluencia . wiki.sei.cmu.edu. Consultado el 18 de febrero de 2019. Archivado desde el original el 19 de febrero de 2019.
  59. Borrador del estándar C17 , 6.7.9 Inicialización, p. 101.
  60. DCL38-C. Utilice la sintaxis correcta al declarar un miembro de matriz flexible - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 21 de febrero de 2019. Archivado desde el original el 22 de febrero de 2019.
  61. OpenSSL_versión  . _ www.openssl.org. Consultado el 9 de diciembre de 2018. Archivado desde el original el 9 de diciembre de 2018.
  62. ↑ Información de la versión : Manual de referencia de GTK+ 3  . desarrollador.gnome.org. Consultado el 9 de diciembre de 2018. Archivado desde el original el 16 de noviembre de 2018.
  63. PRE10-C. Envuelva macros de instrucciones múltiples en un ciclo do-while - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 9 de diciembre de 2018. Archivado desde el original el 9 de diciembre de 2018.
  64. PRE01-C.  Use paréntesis dentro de las macros alrededor de los nombres de los parámetros - SEI CERT C Coding Standard - Confluence . wiki.sei.cmu.edu. Consultado el 9 de diciembre de 2018. Archivado desde el original el 9 de diciembre de 2018.
  65. PRE06-C. Adjunte los archivos de encabezado en un include guard-SEI CERT C Coding Standard-  Confluence . wiki.sei.cmu.edu. Consultado el 25 de mayo de 2019. Archivado desde el original el 25 de mayo de 2019.
  66. 1 2 Borrador C17, 5.1.2.2 Entorno alojado , p. 10-11.
  67. 1 2 3 Borrador del estándar C17 , 6.2.4 Duraciones de almacenamiento de objetos, p. treinta.
  68. 1 2 Borrador C17, 7.22.4.4 La función de salida , p. 256.
  69. MEM05-C. Evite las asignaciones de pilas grandes - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 24 de mayo de 2019. Archivado desde el original el 24 de mayo de 2019.
  70. Borrador C17 , 6.7.1 Especificadores de clase de almacenamiento, p. 79.
  71. Borrador del estándar C17 , 6.7.6.3 Declaradores de funciones (incluidos los prototipos), p. 96.
  72. Los punteros en C son más abstractos de lo que piensas . www.viva64.com. Consultado el 30 de diciembre de 2018. Archivado desde el original el 30 de diciembre de 2018.
  73. Tanenbaum Andrew S, Bos Herbert. sistemas operativos modernos. 4ª ed . - San Petersburgo. : Editorial Piter, 2019. - S. 828. - 1120 p. — (Clásicos "Informática"). — ISBN 9785446111558 . Archivado el 7 de agosto de 2021 en Wayback Machine .
  74. Jonathan Corbet. Ondas de Stack  Clash . lwn.net (28 de junio de 2017). Consultado el 25 de mayo de 2019. Archivado desde el original el 25 de mayo de 2019.
  75. Fortalecimiento de archivos binarios ELF mediante Reubicación de solo lectura (RELRO  ) . www.redhat.com. Consultado el 25 de mayo de 2019. Archivado desde el original el 25 de mayo de 2019.
  76. Espacio de direcciones de proceso tradicional -  Programa estático . www.openbsd.org. Consultado el 4 de marzo de 2019. Archivado desde el original el 8 de diciembre de 2019.
  77. Dra. Thabang Mokoteli. ICMLG 2017 5ta Conferencia Internacional sobre Liderazgo Gerencial y Gobernanza . - Conferencias Académicas y editorial limitada, 2017-03. - S. 42. - 567 pág. — ISBN 9781911218289 . Archivado el 7 de agosto de 2021 en Wayback Machine .
  78. Espacio de direcciones de proceso tradicional - Programa con  bibliotecas compartidas . www.openbsd.org. Consultado el 4 de marzo de 2019. Archivado desde el original el 8 de diciembre de 2019.
  79. ↑ 1 2 3 4 ERROR30-C. Establezca errno en cero antes de llamar a una función de biblioteca conocida por establecer errno, y verifique errno solo después de que la función devuelva un valor que indique falla: SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 23 de mayo de 2019. Archivado desde el original el 19 de noviembre de 2018.
  80. ↑ 1 2 3 4 5 6 ERR33-C. Detectar y manejar errores de la biblioteca estándar - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 23 de mayo de 2019. Archivado desde el original el 23 de mayo de 2019.
  81. sys_types.h.0p -  Página del manual de Linux . man7.org. Consultado el 23 de mayo de 2019. Archivado desde el original el 23 de mayo de 2019.
  82. ↑ 12 ERROR02- C. Evite los indicadores de error en banda - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 4 de enero de 2019. Archivado desde el original el 5 de enero de 2019.
  83. FIO34-C. Distinguir entre caracteres leídos de un archivo y EOF o WEOF-SEI CERT C Coding Standard-  Confluence . wiki.sei.cmu.edu. Fecha de acceso: 4 de enero de 2019. Archivado desde el original el 4 de enero de 2019.
  84. Estilo  de codificación . El administrador de sistemas y servicios de systemd . github.com. Consultado el 1 de febrero de 2019. Archivado desde el original el 31 de diciembre de 2020.
  85. ↑ Informe de errores : Manual de referencia de GLib  . desarrollador.gnome.org. Consultado el 1 de febrero de 2019. Archivado desde el original el 2 de febrero de 2019.
  86. ↑ Eina : Error  . docs.enlightenment.org. Consultado el 1 de febrero de 2019. Archivado desde el original el 2 de febrero de 2019.
  87. ↑ 1 2 DCL09-C. Declare funciones que devuelvan errno con un tipo de retorno de errno_t-SEI CERT C Coding Standard-Confluence . wiki.sei.cmu.edu. Consultado el 21 de diciembre de 2018. Archivado desde el original el 21 de diciembre de 2018.
  88. ↑ 1 2 FLP32-C. Prevenga o detecte errores de dominio y rango en funciones matemáticas - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 5 de enero de 2019. Archivado desde el original el 5 de enero de 2019.
  89. ↑ 12 MEM12- C. Considere el uso de una cadena goto cuando deje una función con error al usar y liberar recursos: SEI CERT C Coding Standard-  Confluence . wiki.sei.cmu.edu. Consultado el 4 de enero de 2019. Archivado desde el original el 5 de enero de 2019.
  90. ERROR04-C.  Elija una estrategia de terminación adecuada - SEI CERT C Coding Standard - Confluence . wiki.sei.cmu.edu. Consultado el 4 de enero de 2019. Archivado desde el original el 5 de enero de 2019.
  91. MEM31-C. Libere memoria asignada dinámicamente cuando ya no la necesite: estándar de codificación SEI CERT C:  confluencia . wiki.sei.cmu.edu. Consultado el 6 de enero de 2019. Archivado desde el original el 6 de enero de 2019.
  92. Uso de la colección de compiladores GNU (GCC):  estándares . gcc.gnu.org. Consultado el 23 de febrero de 2019. Archivado desde el original el 17 de junio de 2012.
  93. Compatibilidad  de idiomas . clang.llvm.org. Consultado el 23 de febrero de 2019. Archivado desde el original el 19 de febrero de 2019.
  94. Notas de la versión de Clang 6.0.0: documentación de Clang 6 . releases.llvm.org. Consultado el 23 de febrero de 2019. Archivado desde el original el 23 de febrero de 2019.
  95. Siddhesh Poyarekar - La versión 2.29 de la Biblioteca C de GNU ya está  disponible . sourceware.org. Consultado el 2 de febrero de 2019. Archivado desde el original el 2 de febrero de 2019.
  96. Alpine Linux ha cambiado a musl libc | Linux  alpino . alpinelinux.org. Consultado el 2 de febrero de 2019. Archivado desde el original el 3 de febrero de 2019.
  97. Manual de Musl-Void Linux . docs.voidlinux.org . Consultado el 29 de enero de 2022. Archivado desde el original el 9 de diciembre de 2021.
  98. Características de la biblioteca CRT . docs.microsoft.com. Consultado el 2 de febrero de 2019. Archivado desde el original el 7 de agosto de 2021.
  99. Idiomas admitidos - Características | CLión  (inglés) . cerebros de chorro Consultado el 23 de febrero de 2019. Archivado desde el original el 25 de marzo de 2019.
  100. ↑ 1 2 Comprobar 0.10.0: 2. Pruebas unitarias en C  . comprobar.sourceforge.net. Consultado el 23 de febrero de 2019. Archivado desde el original el 5 de junio de 2018.
  101. ↑ 6. Callgrind : un generador de caché de gráficos de llamadas y un generador de perfiles de predicción de sucursales  . Documentación de Valgrind . valgrind.org. Consultado el 21 de mayo de 2019. Archivado desde el original el 23 de mayo de 2019.
  102. Kcachegrind . kcachegrind.sourceforge.net. Consultado el 21 de mayo de 2019. Archivado desde el original el 6 de abril de 2019.
  103. Compilador Emscripten LLVM a JavaScript . Consultado el 25 de septiembre de 2012. Archivado desde el original el 17 de diciembre de 2012.
  104. Compilador de Flash C++ . Consultado el 25 de enero de 2013. Archivado desde el original el 25 de mayo de 2013.
  105. Pista del proyecto en SourceForge.net
  106. ^ Soluciones axiomáticas Sdn Bhd . Fecha de acceso: 7 de marzo de 2009. Archivado desde el original el 23 de febrero de 2009.
  107. Documentación de ClangFormat-Clang 9  . clang.llvm.org. Consultado el 5 de marzo de 2019. Archivado desde el original el 6 de marzo de 2019.
  108. ↑ indent(1) - Página man de Linux  . linux.die.net. Consultado el 5 de marzo de 2019. Archivado desde el original el 13 de mayo de 2019.
  109. Wolfram Research, Inc. INTERFACES DE SISTEMAS Y  DESPLIEGUE . Colección de tutoriales de Wolfram Mathematica® 36-37. biblioteca.wolfram.com (2008). Consultado el 29 de mayo de 2019. Archivado desde el original el 6 de septiembre de 2015.
  110. Cleve Moler. El crecimiento de MATLAB y The MathWorks durante dos décadas . Noticias y notas de TheMathWorks . www.mathworks.com (enero de 2006). Consultado el 29 de mayo de 2019. Archivado desde el original el 4 de marzo de 2016.
  111. sched_setscheduler._  _ _ pubs.opengroup.org. Fecha de acceso: 4 de febrero de 2019. Archivado desde el original el 24 de febrero de 2019.
  112. clock_gettime  . _ pubs.opengroup.org. Fecha de acceso: 4 de febrero de 2019. Archivado desde el original el 24 de febrero de 2019.
  113. clock_nanosleep._  _ _ pubs.opengroup.org. Fecha de acceso: 4 de febrero de 2019. Archivado desde el original el 24 de febrero de 2019.
  114. M. Jones. Anatomía de las arquitecturas Linux en tiempo real . www.ibm.com (30 de octubre de 2008). Consultado el 4 de febrero de 2019. Archivado desde el original el 7 de febrero de 2019.
  115. Índice  TIOBE . www.tiobe.com . Consultado el 2 de febrero de 2019. Archivado desde el original el 25 de febrero de 2018.
  116. Stroustrup, Bjarne Evolución de un lenguaje en y para el mundo real: C++ 1991-2006 . Consultado el 9 de julio de 2018. Archivado desde el original el 20 de noviembre de 2007.
  117. Preguntas frecuentes sobre Stroustrup . www.stroustrup.com. Consultado el 3 de junio de 2019. Archivado desde el original el 6 de febrero de 2016.
  118. Anexo 0: Compatibilidad. 1.2. C++ e ISOC . Documento de Trabajo para el Proyecto de Norma Internacional Propuesta para Sistemas de Información - Lenguaje de Programación C++ (2 de diciembre de 1996). — ver 1.2.1p3 (párrafo 3 en la sección 1.2.1). Consultado el 6 de junio de 2009. Archivado desde el original el 22 de agosto de 2011.
  119. 1 2 3 4 Stolyarov, 2010 , 1. Prefacio, p. 79.
  120. Crónica de lenguas. si _ Editorial "Sistemas abiertos". Consultado el 8 de diciembre de 2018. Archivado desde el original el 9 de diciembre de 2018.
  121. Allen I. Holub. Suficiente cuerda para pegarse un tiro en el pie: reglas para la programación en C y C++ . - McGraw-Hill, 1995. - 214 págs. — ISBN 9780070296893 . Archivado el 9 de diciembre de 2018 en Wayback Machine .
  122. Uso de la colección de compiladores GNU (GCC): opciones de advertencia . gcc.gnu.org. Consultado el 8 de diciembre de 2018. Archivado desde el original el 5 de diciembre de 2018.
  123. ↑ Indicadores de diagnóstico en la documentación de Clang-Clang 8 . clang.llvm.org. Consultado el 8 de diciembre de 2018. Archivado desde el original el 9 de diciembre de 2018.
  124. Documentación de MemorySanitizer-Clang 8  . clang.llvm.org. Consultado el 8 de diciembre de 2018. Archivado desde el original el 1 de diciembre de 2018.
  125. MEM00-C. Asigne y libere memoria en el mismo módulo, al mismo nivel de abstracción - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 4 de junio de 2019. Archivado desde el original el 4 de junio de 2019.
  126. MEM04-C. Tenga cuidado con las asignaciones de longitud cero - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 11 de enero de 2019. Archivado desde el original el 12 de enero de 2019.
  127. Gestión de memoria de objetos: Manual de referencia de GObject . desarrollador.gnome.org. Consultado el 9 de diciembre de 2018. Archivado desde el original el 7 de septiembre de 2018.
  128. Por ejemplo, snai.pe c-smart-pointers . Archivado el 14 de agosto de 2018 en Wayback Machine .
  129. Recolección de basura en programas C. Consultado el 16 de mayo de 2019. Archivado desde el original el 27 de marzo de 2019.
  130. ↑ 1 2 Información de seguridad informática del CERN . seguridad.web.cern.ch. Consultado el 12 de enero de 2019. Archivado desde el original el 5 de enero de 2019.
  131. CWE - CWE-170: Terminación nula incorrecta (3.2  ) . cwe.mitre.org. Consultado el 12 de enero de 2019. Archivado desde el original el 13 de enero de 2019.
  132. STR32-C. No pase una secuencia de caracteres que no termine en nulo a una función de biblioteca que espera una cadena - SEI CERT C Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 12 de enero de 2019. Archivado desde el original el 13 de enero de 2019.
  133. DCL50-CPP. No defina una función variable de estilo C - SEI CERT C++ Coding Standard -  Confluence . wiki.sei.cmu.edu. Consultado el 25 de mayo de 2019. Archivado desde el original el 25 de mayo de 2019.
  134. EXP47-C. No llame a va_arg con un argumento del tipo incorrecto: SEI CERT C Coding Standard-  Confluence . wiki.sei.cmu.edu. Consultado el 8 de diciembre de 2018. Archivado desde el original el 9 de diciembre de 2018.
  135. Estándar de codificación SEI CERT C - Estándar de codificación SEI CERT C - Confluencia . wiki.sei.cmu.edu. Consultado el 9 de diciembre de 2018. Archivado desde el original el 8 de diciembre de 2018.
  136. Introducción - Estándar de codificación SEI CERT C - Confluencia . wiki.sei.cmu.edu. Consultado el 24 de mayo de 2019. Archivado desde el original el 24 de mayo de 2019.
  137. CON33-C.  Evite las condiciones de carrera cuando utilice las funciones de la biblioteca - SEI CERT C Coding Standard - Confluence . wiki.sei.cmu.edu. Consultado el 23 de enero de 2019. Archivado desde el original el 23 de enero de 2019.

Literatura

  • ISO/CEI. ISO/IEC9899:2017 . Lenguajes de programación - C (enlace descendente) . www.open-std.org (2017) . Consultado el 3 de diciembre de 2018. Archivado desde el original el 24 de octubre de 2018. 
  • Kernigan B. , Ritchie D. El lenguaje de programación C = El lenguaje de programación C. - 2ª ed. - M .: Williams , 2007. - S. 304. - ISBN 0-13-110362-8 .
  • Gukin D. El lenguaje de programación C para tontos = C para tontos. - M. : Dialéctica , 2006. - S. 352. - ISBN 0-7645-7068-4 .
  • Podbelsky V. V., Fomin S. S. Curso de programación en lenguaje C: libro de texto . - M. : DMK Press, 2012. - 318 p. - ISBN 978-5-94074-449-8 .
  • Prata S. El lenguaje de programación C: conferencias y ejercicios = C Primer Plus. - M. : Williams, 2006. - S. 960. - ISBN 5-8459-0986-4 .
  • Prata S. El lenguaje de programación C (C11). Clases y ejercicios, 6.ª edición = C Primer Plus, 6.ª edición. - M. : Williams, 2015. - 928 p. - ISBN 978-5-8459-1950-2 .
  • Stolyarov A. V. El lenguaje C y la formación inicial en programación  // Colección de artículos de jóvenes científicos de la facultad de la CMC MSU. - Departamento de publicaciones de la facultad de la CMC de la Universidad Estatal de Moscú, 2010. - No. 7 . - S. 78-90 .
  • Schildt G. C: The Complete Reference, Classic Edition = C: The Complete Reference, 4th Edition. - M .: Williams , 2010. - S. 704. - ISBN 978-5-8459-1709-6 .
  • Lenguajes de programación Ada, C, Pascal = Comparación y evaluación de lenguajes de programación Ada, C y Pascal / A. Feuer, N. Jehani. - M. : Radio y Sayaz, 1989. - 368 p. — 50.000 copias.  — ISBN 5-256-00309-7 .

Enlaces

  •  Página oficial de ISO/IEC JTC1/ SC22 /WG14 . — Página oficial del grupo de trabajo internacional sobre la estandarización del lenguaje de programación C. Consultado el 20 de febrero de 2009. Archivado desde el original el 22 de agosto de 2011.
    • WG14 N1124  (inglés) . ISO/IEC 9899 - Lenguajes de programación - C - Estándares aprobados . ISO/IEC JTC1/SC22/WG14 (6 de mayo de 2005). — Norma ISO/IEC 9899:1999 (C99) + ISO/IEC 9899:1999 Cor. 1:2001(E) (TC1 - Corrección técnica 1 de 2001) + ISO/IEC 9899:1999 Cor. 2:2004(E) (TC2 - Corrección técnica 2 de 2004). Consultado el 20 de febrero de 2009. Archivado desde el original el 22 de agosto de 2011.
    • C - La Norma ISO - Justificación, Revisión 5.10  (inglés) (abril de 2004). — Justificación y explicaciones del estándar C99. Consultado el 20 de febrero de 2009. Archivado desde el original el 22 de agosto de 2011.
  • Cppreference.com  es un wiki de referencia respaldado por entusiastas con una gran colección de datos sobre los lenguajes C y C++ , sus estándares y material relacionado con estos lenguajes y su desarrollo.
  • SEI CERT C Coding Standard (o SEI CERT C Coding Standard, 2016 Edition ) es un estándar para la programación C segura .
  • Romanov E. C/C++. De aficionado a profesional . ermak.cs.nstu.ru. Consultado: 25 de mayo de 2015.