Publi

Operador coma. Cómo incorporarlo a nuestro día a día con muchos ejemplos en C

El operador coma. ¿Cómo empezar a usarlo?
Seguro que lo has visto cientos de veces por ahí, pasando desapercibido en multitud de programas hechos en C. Es más, incluso el primer día que empezaste a ver programación os visteis las caras y casi no te diste cuenta. Incluso puede que lo hayas utilizado sin siquiera ser consciente de lo que hace en realidad.

Otros muchos sí que sabréis de qué va este operador, y me gustaría que los que sí sabéis de qué va, sugirierais más ejemplos en los comentarios.

Para situarnos

Seguramente hayas utilizado la coma en varias situaciones como por ejemplo la creación de variables del mismo tipo:

1
2
3
4
5
int main ()
{
  int a=2, b=1, c, d, e=4;
  int f, g, h=f=g=2;
}

O en declaraciones y llamadas a funciones:

1
2
3
4
5
6
7
8
9
10
11
int funcion(int a, int b)
{
  ...
}

int main()
{
  int a=2, b=3;
  funcion(a, b);
  return 0;
}

Pero este símbolo tiene muchos más usos en C, C++ y otros lenguajes derivados (Java, Perl o Javascript, por ejemplo) en los que podemos utilizarlo como algo más que un simple separador y con el que podemos ahorrar muchas líneas de código.

¿Cómo probar estos programas?

Si queréis probar todos los ejemplos que muestro en el post, no tenéis más que copiar y pegar en vuestro editor favorito y compilar con vuestro compilador preferido. Yo utilizo GCC (no hace falta ninguna biblioteca ni nada):

gcc -o compilado fuente.c

¿ Para qué vale el operador coma ?

Este operador se utiliza generalmente para evaluar varias expresiones dentro de la misma línea de código. Es, además, el operador con menor preferencia de todos. Esto último quiere decir que asignaciones, operaciones matemáticas, lógicas, direccionamiento, ternarios, etc; se ejecutarán antes que la coma y esto nos puede llevar a pensar que no funciona bien, pero tiene su lógica. Además, una vez que hemos ejecutado todas las expresiones que están separadas por coma de izquierda a derecha, devolveremos como resultado el valor de la última (la que más a la derecha estará).

1
2
3
4
int main()
{
   expresión1, expresión2, expresión3;
}

Como todos los elementos de un lenguaje, pueden ser utilizados con buenos o males fines. Por un lado, podemos hacer un código muy críptico con este operador, y hacer que el próximo que se siente a analizar nuestro código lo pase mal para entender qué estamos haciendo, y todo para ahorrar unas cuántas líneas de código. Aunque también puede utilizarse con buenos fines, y hacer que todo se entienda mucho mejor.

Como consejo para beneficiarnos en la legibilidad y el mantenimiento del código, debemos utilizar el operador coma para ejecutar expresiones que estén relacionadas, incluso que para ejecutar las expresiones de la derecha sea necesario que las de la izquierda se hayan ejecutado antes, ya que si vamos a ejecutar cosas que no tienen nada que ver, es mejor separarlas con punto y coma (;) y ponerlas en otra línea, por el bien de los que vayan a leer nuestro código en el futuro.

Aunque la mejor forma de conocer el funcionamiento es este operador es viendo ejemplos y jugando con ellos. Así que vamos a ver una buena selección de ejemplos para empezar a poner en práctica ya. He de decir que la coma siempre hace lo mismo, todos estos son diferentes escenarios donde podemos hacerlo, no estamos inventando nada en cada uno de los ejemplos.

La coma en asignaciones

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main()
{
  int a;
  a = 1, 2, 3, 4;
  printf ("a = %d\n", a);
  return 0;
}

Este código puede parecer (y de hecho lo es) una tontería y no sirve para nada. Pero nos ayuda a comprender la preferencia de los operadores. En realidad, si hacemos un programa y ponemos un 7;, éste compilará bien, aunque será cosa del compilador ignorar lo que hemos hecho, ya que no estamos haciendo nada con ese valor. En realidad, en este código estamos evaluando las expresiones:

  • a = 1
  • 2
  • 3
  • 4

Por lo que el resultado será:

a = 1

¿No dije yo antes que el valor devuelto era el de la derecha? Y, ¿el de la derecha no es el 4? Sí, pero si vemos las expresiones, la primera es a=1, por lo que se realiza dicha asignación. Sería distinto decir lo siguiente:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main()
{
  int a;
  a = (1, 2, 3, 4);
  printf ("a = %d\n", a);
  return 0;
}

Esto sí que devolverá 4

Modificando los valores dentro de la igualación

Podemos intentar cosas como:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main()
{
    int a=3, b=4;
    int c=(a*=a, b*=b, a+b);

    printf ("a=%d\nb=%d\nc=%d\n", a, b, c);
    return 0;
}

Es decir, aqui, hemos cogido a y la hemos multiplicado por sí misma, b también, y luego en c hemos metido a y b (que ahora son los cuadrados de los originales). Como resultado, hemos modificado las tres variables.

Intercambiando valores en una sola línea

Tenemos dos valores (a y b) y queremos que b sea a y a sea b. (Podemos ver algunas técnicas aquí). Aunque esto podemos resumirlo en una línea de esta manera:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main()
{
    int temp;
    int a=33, b=22;

    a=(temp=a,b),b=temp;

    printf ("a = %d\nb = %d\n", a, b);
}

Necesitaríamos una variable temporal, declarada (temp), pero lo demás sería ejecutar las expresiones:

  • temp = a
  • a=b
  • b=temp

En un orden determinado y con una preferencia determinada.

Un ejemplo para explotarnos la cabeza

Veamos ahora:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main()
{
    int a=1;
    int b=(a++, a++, a++);

    printf ("a=%d\nb=%d\n", a, b);
    return 0;
}

¿Cuánto debe valer a y b? La lógica nos diría que a y b valen lo mismo, 4. Pero no es así. A termina valiendo 4, es cierto, porque inicialmente vale 1 y lo incrementamos 3 veces (1 + 1 + 1 + 1 = 4), bien. Pero ¿b? En realidad, las expresiones se evalúan todas, pero a++ en realidad es un post-incremento, esto es, que primero devolverá el valor y luego incrementará, por lo que antes del tercer incremento a vale 4 y es lo que llega a b. Después de esto se incrementará.

Operación pop_first() en listas enlazadas

Una de las posibles operaciones es la extracción del primer elemento de la lista. Es decir, devolvemos el valor del primer elemento y lo quitamos al mismo tiempo de la lista. Imaginemos que nuestra lista está formada por (TNodo)s y éstos tienen un TDato (que podrá ser un entero, una cadena, un struct, etc):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct TNodo
{
  TDato dato;
  struct TNodo* sig;
}

TDato pop_first(TLista** lista)
{
  TDato dato;
  TLista _lista = *lista;
  TNodo sig = _lista->sig;

// Aquí a dato le asignamos el dato que hay actualmente en la lista, luego liberamos la lista y apuntamos lista al siguiente nodo.
  dato = _lista->dato, free(*lista), *lista=sig;
  return dato;
}

Cálculo de salarios

Imaginémonos que vamos a calcular el salario de un empleado. Éste es el salario base + 0.1 * años de antigüedad * salario base + bonificación. Eso sí, cada una de las variables (salario base, antigüedad y bonificación debe obtenerse con llamadas a varias funciones), podríamos hacer lo siguiente:

1
2
3
4
5
6
int main()
{
  double base, bonificacion;
  unsigned years;
  double salario = (base = get_salario_base(), years=get_antiguedad(), bonificacion = get_bonificacion(), base + base*years*0.1 + bonificacion);
}

Otro ejemplo curioso más

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main()
{
   int i, j;
   
   j = 10;
   i = (j++, j+100, 999+j);

   printf ("i=%d\nj=%d\n", i, j);
   return 0;
}

¿ Y el resultado ?

i=1010
j=11

Esto es así porque el j++ se ejecuta (por lo que j vale 11 ya. La segunta expresión j+100 no hace nada, se ejecuta, pero no se guarda en ningún lado (seguramente un compilador listo la ignore), la última expresión 999+j será la que de verdad se guarde en i, por tanto i=999+11 = 1010.

Resolviendo una ecuación de segundo grado

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <math.h>

int main()
{
  double a = 1;
  double b = -5;
  double c = 6;
  ddouble sq, x1, x2;

  x1 = (sq = sqrt(b*b-4*a*c), a=a*2, (-b+sq)/a);
  x2 = (-b-sq)/a;

  printf ("x1 = %lf\n", x1);
  printf ("x2 = %lf\n", x2);
  return 0;
}

En este código vemos cómo para calcular la primera solución, evaluamos varias expresiones seguidas:

  • sq = sqrt(b*b-4*a*c)
  • a=a*2
  • (-b+sq)/a

Y a la variable x1 le asignamos este último valor. En este caso, vemos que las tres expresiones deben ejecutarse en este orden ya que hay dependencias de unas sobre otras. Si cambiamos el orden puede ser fatal y no se ejecutará bien nuestro código.
Para compilar esto, se debe incluir la biblioteca matemática (en gcc es utilizando -lm).

Inicializando estructuras

Este ejemplo está pensado como algo más complejo. Imaginemos que hemos creado una lista enlazada, o una lectura desde archivo (primero tenemos que abrir el archivo antes de leer). Es decir, antes de poder obtener el valor, tenemos que llamar a una función:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int estructura = 20;

void inicializa(int **a)
{
    /* Aquí iría un malloc(), metemos datos en la estructura, etc */
    *a = &estructura;
}

int main()
{
    int *a = NULL;

    int valor = (inicializa(&a), *a);

    printf ("valor = %d\n", valor);
  return 0;
}

Es más, fijaos que inicializa() es de tipo void y no devuelve ningún valor. El compilador podía quejarse al querer dar ese valor a una variable de tipo entero, pero como en realidad el valor que se le da es el de *a, no hay ningún problema.

O con ficheros:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main()
{
    FILE *f;
    int valor;
    int ok = (f = fopen("valor.txt", "r"), fscanf(f, "%d", &valor), fclose(f));
   
    printf ("valor = %d\n", valor);
  return 0;
}

Hay que tener en cuenta que esto son sólo ejemplos, en la práctica puede que todo no sea tan fantástico y debamos hacer comprobaciones de error. Por ejemplo, en el ejemplo anterior (de ficheros), sería más conveniente utilizar && en lugar de , así cuando falla la expresión de más a la izquierda, no se ejecutan las siguientes.

Comas dentro de un bucle for

Muchos de los que empiezan a programar en C, separan las expresiones del bucle for con comas, cuando en realidad es con punto y coma. Es decir:

1
2
3
4
for (inicialización ; finalización ; seguimiento)
{
  ...
}

Pero utilizar comas aquí tiene utilidades bien distintas, por ejemplo cuando intervienen varias variables en el bucle, cuando debemos inicializar dos variables, en lugar de inicializar una variable y luego hacer el for, podemos hacer:

1
2
3
4
5
  int i, j;
  for (i=0, j=0 ; i< 10; ++i)
  {
     ...
  }

Para complicar esto, podemos contar 20 con dos variables, una desde 0 a 19 y otra desde 19 hasta 0:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main()
{
    int i, j, total=20;

    for (j=total-1, i=0; i<total; ++i, --j)
        printf ("i = %d - j = %d\n", i, j);

  return 0;
}

Comas dentro de un bucle while

Vamos, con otro pequeño ejemplo. Un típico ejercicio de adivinar un número:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main()
{
    srand(time(NULL));
    int numero=1+rand()%10;
    int input=0;

    while (input!=numero)
        {
            printf ("Introduce un número: ");
            scanf("%d", &input);
        }

    printf("Has acertado!\n");
  return 0;
}

Que podemos escribir de la siguiente forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main()
{
  int numero=(srand(time(NULL)), rand()%10), input=0;

  while (printf ("Introduce un número: "), scanf("%d", &input), input!=numero);

  printf("Has acertado!\n");
  return 0;
}

En este caso, dentro del while, hemos incluido varias expresiones:

  • printf (“Introduce un número: “)
  • scanf(“%d”, &input)
  • input!=numero

Siendo la última de éstas la que en realidad sirve como condición del while.

Leyendo un archivo (Unix)

Una lectura de fichero utilizando la biblioteca (unistd), podemos escribirla así:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int f;
    char c;
    int ok;
    f = open("coma.c", O_RDONLY);
    while ((ok = read(f, &c, 1))>0)
        printf ("%c", c);
   
  return 0;
}

Aunque podemos escribirla también así, utilizando el operador coma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int f;
    char c;
    int ok;
    f = open("coma.c", O_RDONLY);
    while (ok = read(f, &c, 1), ok>0)
        printf ("%c", c);
   
  return 0;
}

Pero, por ejemplo, si queremos que el bucle pare cuando encontremos un carácter punto y coma ‘;’, podemos hacer esto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int f;
    char c;
    int ok;
    f = open("coma.c", O_RDONLY);
    while (ok = read(f, &c, 1), ok>0 && c!=';')
        printf ("%c", c);
   
  return 0;
}

Comas en condicionales

En este caso, estaremos evaluando alguna expresión o ejecutando una función por ejemplo, antes de preguntar por la condición. Por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>

int pregunta_edad()
{
    int res = 0;

    while (printf ("¿Cuántos años tienes? "),
                 scanf("%d", &res),
                 res<1)
        {
            if (res<1)
                printf ("No me mientas!!\n");
        }
   
    return res;
}

int main()
{
    int edad;

    if (edad=pregunta_edad(), edad<18)
        printf ("Espera %d años antes de volver\n", 18-edad);
    else if (edad==18)
        printf ("Perfecto, tienes 18\n");
    else
        printf ("Sabes que podias haber entrado hace %d años?\n", edad-18);
  return 0;
}

Aunque es verdad que el condicional también podía haberse expresado como:

1
  if ((edad=pregunta_edad())<18)

evitando así repetir la variable. Aunque con la coma podríamos incluir más expresiones para ejecutar, incluyendo operaciones y varias asignaciones:

1
  if (precio = calcula_coste_pedido(&pedido), financiacion = calcula_financiacion(precio, usuario), financiacion.ok)

En este ejemplo, podemos ver cómo evaluar si el usuario de una tienda online puede optar por financiación de su pedido. Para ello, utilizamos la función calcula_coste_pedido, a la que le pasamos un struct (por ejemplo) con información del pedido y calcula su precio total. Luego la variable financiacion (que es otro struct) se rellenará con datos gracias a calcula_financiacion() a la que le pasamos el precio y el usuario (nuestra tienda ofrece condiciones especiales a ciertos usuarios) y luego devolvemos financiacion.ok (un campo de nuestro struct).

Dentro de un return

Utilizar este operador dentro de un return es de los usos más comunes, y es que muchas veces, antes de salir de una función debemos realizar pequeñas tareas, llamar a otras funciones, liberar memoria y cosas del estilo.
En este ejemplo, vamos a utilizar strtok(), una función conocida por arruinar las cadenas originales que le pasamos, es decir, cuando utilizamos strtok() la cadena original se modifica. Por tanto, si queremos que ésta no cambie, debemos hacer una copia y aplicar strtok() a la cadena copiada, luego podremos hacer lo que queramos con ella:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

int cuenta_palabras(char* cadena)
{
   const char s[2] = " ";
   char *token;
     char *copia=(char*)malloc(strlen(cadena)+1);
     strcpy(copia, cadena);
     
     int tokens=0;
   token = strtok(copia, s);
   
   while( token != NULL )
   {
         tokens++;    
         token = strtok(NULL, s);
   }
     
     return free(copia), tokens;
}

int main()
{
   char str[] = "Poesía Binaria. https://poesiabinaria.net Aprendiendo a utilizar el operador coma.";
     printf ("Cuenta palabras: %d\n", cuenta_palabras(str));

     printf ("Str: %s\n", str);
   return 0;
}

Como vemos, en el mismo return de la función cuenta_palabras(), hacemos free(copia) para liberar memoria de la cadena copiada y luego devolvemos tokens. Podíamos hacerlo en dos líneas, pero lo hacemos en una. Es más free() es void.

Para evitar tener que abrir bloques

Esto es más pereza que otra cosa. Mientras en varios manuales de estilo (depende de las aplicaciones o del grupo que vaya a programar) se recomienda abrir y cerrar llaves siempre que estemos ante un bloque aunque sólo sea de una línea (cosa que en C es opcional), otros prefieren no usar llaves si no es necesario para así evitar que el número de líneas crezca.
Aunque hay veces que vamos a realizar acciones comunes, y podemos pensar que son un poco tontas, como por ejemplo mostrar un mensaje de error en pantalla y devolver un código (de error):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int analiza_datos(int a, int b)
{
    if (a==b)
        return printf("Son iguales\n"), 0;
    else if (a<b)
        return printf("a<b\n"), 1;
    else
        return printf ("a>b\n"), 2;
}

int main()
{
    printf ("=%d\n", analiza_datos(1, 2));
    printf ("=%d\n", analiza_datos(2, 2));
    printf ("=%d\n",analiza_datos(2, 1));
    return 0;
}

Si miramos la función analiza_datos(), vemos que en lugar de abrir llaves y poner un printf() y return x lo ponemos todo en la misma línea.

Llamadas a funciones

Bueno, llegamos a un punto peliagudo, porque tal vez tomemos las cosas muy a la ligera en ocasiones. Tenemos que tener especial cuidado en este punto, cuando toque pasar valores a funciones y queramos meter el operador coma para modificar ciertos valores. Lo que está claro es que cuando vayamos a utilizar el operador, debemos poner las expresiones entre paréntesis , porque de otro modo la coma actuará como separador. Por ejemplo si tengo esta función:

1
2
3
4
int funcion(int a, int b)
{
    printf ("a = %d\nb = %d\n", a, b);
}

No puedo hacer lo siguiente cuando vaya a llamarla:

1
funcion(a, b+=3, b);

Porque estaré diciendo que hay tres argumentos, y la función sólo admite dos.

Lo que sí podemos hacer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int funcion(int a, int b)
{
    printf ("a = %d\nb = %d\n", a, b);
}

int main()
{
    int a=3;
    int b=2;
    funcion (a, (b+=a, b));
    printf ("a en main = %d\n", a);
    printf ("b en main = %d\n", b);
}

En este caso:

a = 3
b = 5
a en main = 3
b en main = 5

Si hemos estado atentos no habrá sido muy difícil intuir el resultado.

Cuidado con este caso!!

Aunque, ¿qué pasaría si utilizo a en todos los valores?

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int funcion(int a, int b)
{
    printf ("a = %d\nb = %d\n", a, b);
}

int main()
{
    int a=3;

    funcion (a, (a+=3, a));
    printf ("a en main = %d\n", a);
}

Podemos pensar que se llamará a la función con los valores 3 y 6, pero depende del compilador, porque los dos argumentos, al fin y al cabo son a. Si el compilador, a medida que va haciendo las operaciones va recopilando los argumentos, sí sucederá así, se llamará a la función con 3 y 6. Pero si por el contrario primero se hacen las operaciones, y luego se van enviando los resultados como los diferentes argumentos, terminaremos enviando dos 6, ya que primero se hace el a+=3 y luego se llama a mi_funcion(a, a). Es cierto que la coma tiene la menor preferencia posible, pero cuando encontramos la coma (separadora de argumentos) normalmente estaremos hablando de cosas distintas, por lo que contarían como instrucciones aparte y en cierto modo se reinician las preferencias.

¿Se te ocurren más ejemplos?

Si tienes más ejemplos, o alguna cuestión relacionada, ¡no dudes en poner un comentario!

Actualización 03/05/2017: He arreglado un ejemplo de código que no se veía bien.

Foto: Providence Doucet

También podría interesarte....

There are 5 comments left Ir a comentario

  1. Pingback: Operador coma. Cómo incorporarlo a nuestro día a día con muchos ejemplos en C | PlanetaLibre /

  2. nasciiboy /
    Usando Mozilla Firefox Mozilla Firefox 50.0 en Fedora Linux Fedora Linux

    lo habia visto solo en asignaciones, en while o evaluacion dentro de parentesis es totalmente novedoso para mi persona… esto forma parte del estandar? o es un “truco” dependiente de alguna implementacion especifica?

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 50.0 en Ubuntu Linux Ubuntu Linux

      Es totalmente estándar. Lo podemos encontrar en el libro de ANSI C de Kernighan y Ritchie de 1989.

      Lo que ya no estoy seguro si forma parte del estándar o no es la’ultima parte de interpretación de los argumentos de funciones. Ya que algunos compiladores (he probado VC y GCC) primero hacen las operaciones y luego pasan los argumentos, con lo que si pasamos a dos veces, dicha a se pasará una vez hechas las operaciones. Y por el momento CLang ha sido el único que va recopilando argumentos a medida que vamos haciendo las operaciones.

      Gracias por tu comentario!

  3. nasciiboy /
    Usando Mozilla Firefox Mozilla Firefox 50.0 en Fedora Linux Fedora Linux

    (tengo que releer el k&r), con lo vago y poco especifico que es el estandar de c es mejor ser cauto,

    no creo utilizarlo en demasia, pero las agrupaciones con comas tienen estilo, muy buen post

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 50.0 en Ubuntu Linux Ubuntu Linux

      Hay muchas cosas que pasan desapercibidas. A mí lo que me gusta de este operador es sobre todo el tema de los bloques. Más de una vez tienes que hacer un return con un valor y poner un mensaje o hacer un log de lo que has hecho y da pereza abrir un montón de llaves para algo así…

Leave a Reply