Historia de los lenguajes de programación y algunos sistemas operativos. ¿Qué es una computadora? ¿Por qué hay interés en programarlas?. Relación entre C, C++, Java, Objective-C y el compilador de Objective-C++.
Cadena de compilación en C/C++
Proceso de compilación de código fuente en C/C++
Introducción a la programación en C
Hola mundo en C
El libro de Kernighan y Ritchie sobre el lenguaje de programación en C, introdujo la práctica de que el primer ejemplo sea un programa minimalista, normalmente imprimir algún texto simple, como "hola mundo" en la pantalla:
int main()
{
printf("Hola mundo\n");
return 0;
}
]]>
Hola mundo en C
Para crear un programa en C, se debe crear un archivo de texto, por convención con extensión .c, por ejemplo, hola.c. Luego se debe invocar el compilador en línea de comandos. Por ejemplo, si se utiliza el GNU Compiler Coleccion (GCC):
gcc -Wall -std=c99 -o hola hola.c
El comando anterior indica al compilador de C, gcc, que compile el archivo fuente hola.c y genere un ejecutable hola. Por convención en Unix los ejecutables no tienen extensión, en Microsoft Windows se querrá hola.exe. El parámetro -Wall indica al compilador que despliegue todas las advetencias (warnings) que pueda encontrar. Un buen programa debe generar 0 advertencias. El parámetro -std=c99 indica al compilador que habilite los cambios que se hicieron al estándar C en el año 1999.
Tanto C como C++ son lenguajes multi-paradigma. El paradigma más influyente en C es la programación procedimental, mientras que en C++ la programación orientada a objetos. En el paradigma procedimental, un programa consta de muchas funciones que se llaman entre ellas. No hay objetos ni clases, las funciones son "métodos libres", también llamadas "funciones libres".
La estructura de un programa en C consta de funciones, declaraciones de variables globales y declaraciones de tipos de datos. Pueden aparecer en cualquier orden, mientras se cumpla la regla de que algo puede ser usado sólo si está declarado antes. Un prototipo puede utilizarse cuando se necesita evadir esta regla. Un prototipo de una función consiste en la declaración de la función sin su cuerpo. Sirve para decirle al compilador que la función existe en algún otro lugar del código fuente, además de los parámetros que necesita para ser invocada.
La función libre que lleva por nombre main() es especial. Ella inicia la ejecución de un programa ejecutable. Debe retornar un entero, que indica al sistema operativo si la ejecución de nuestro programa fue exitosa (con un 0), o que hubo un error (con un número distinto a 0).
Programa en C que muestra algunas características de este lenguaje. Obtener código fuente.
Todo símbolo debe estar declarado. ¿Dónde está declarado printf()? Lo está en el archivo stdio.h. Al encerrarlo entre paréntesis angulares #include <stdio.h>, le indica al preprocesador que debe encontrar este archivo en las carpetas donde está instalado el compilador.
El uso de variables globales es una práctica común en C, sin embargo, se considera una mala práctica de programación. Más adelante se introducirán formas de evitar su uso. La línea 25 utiliza una constante literal real (-0.0001) en una condición, lo cual es completamente válido en C, ya que cualquier valor distinto de 0 se considera verdadero.
La línea 27 hace un recordatorio de que el programador siempre debe tener cuidado con el operador de división. Si sus dos operandos son enteros, el resultado será el cociente de la división entera.
Tipos de datos en C/C++
El tamaño y capacidad de los tipos de datos primitivos en C/C++ dependen de la arquitectura a la cual el compilador esté generando código:
Tipos de datos disponibles en C/C++. 1 Disponible únicamente en C++.
Operadores de (pre|pos)(in|de)cremento
El preincremento o predecremento se realiza antes de ejecutar la sentencia. El posincremento o posdecremento se realiza después de haber ejecutado la sentencia. Si hay dos o más (pre|pos)(in|de)cremento se ejecutarán en el orden que aparecen. Siempre que se pueda se debe prefer el preincremento y el predecremento sobre el posincremento y posdecremento, por razones de eficiencia.
int main()
{
int a = 1;
std::cout << "++a + a = " << ++a + a << std::endl;
int c = 1;
std::cout << "c + ++c + ++c = " << c + ++c + ++c << std::endl;
int b = 1;
std::cout << "b++ + b = " << b++ + b << std::endl;
return 0;
}
]]>
Los operadores de (pre|pos)(in|de)cremento deben utilizarse con cuidado. Obtener código fuente.
Entrada y salida en C
En Unix (el sistema operativo más influyente en todos los sistemas operativos modernos), todo recurso es un archivo. Para Unix el teclado, la pantalla, la impresora, la red, son archivos; como también lo son los archivos tradicionales almacenados en memoria secundaria (disco duro, unidades de estado sólido (SSD, solid state drive), discos ópticos (CD, DVD, blue-ray), cintas magnéticas, etc.). Por esta razón, cuando un programa en C está imprimiendo en la pantalla, realmente lo está haciendo en un archivo que está conectado con la pantalla. Si en lugar de la pantalla se quiere imprimir en un archivo de texto en disco, basta cambiar el archivo destino; el programador sigue utilizando las mismas funciones como lo haría con la pantalla.
Las funciones para entrada y salida de C se encuentran en el encabezado <stdio.h>. Para imprimir texto con formato, se utilizan las funciones printf que varían el destino donde se quiere imprimir: printf(fmt,...) imprime en la salida estándar, fprintf(file,fmt,...) imprime en un archivo cualquiera, sprintf(str,fmt,...) imprime en un string en memoria. Todos reciben un parámetro fmt que es una cadena con formato, que dentro puede tener cero o más especificadores de formato (iniciados por un % y terminados en una letra) que son puntos en la cadena donde se insertarán los parámetros opcionales que reemplazan los puntos suspensivos (...) (que en C son un operador que indican una cantidad arbitraria de parámetros).
Las funciones análogas a las printf son las scanf, utilizadas para leer de un archivo: sea el teclado scanf(fmt,...), un archivo tradicional fscanf(file,fmt,...), o un string en memoria sscanf(str,fmt,...). Existe una diferencia importante: las funciones printf reciben los valores en sus parámetros en modo sólo-lectura con el fin de imprimir copias en el archivo destino, mientras que las funciones scanf requieren punteros a las variables con el fin de modificarlas, por esto, hay que anteponer un ampersand (&) a los tipos de datos primitivos, para pasar la dirección de memoria donde está la variable y no una copia del valor de la variable.
int main()
{
int DD, MM, AAAA;
int hh, mm;
printf("Su fecha de nacimiento [DD/MM/AAAA hh:mm]? ");
scanf("%i/%i/%i %i:%i", &DD, &MM, &AAAA, &hh, &mm);
printf("En formato universal: %04i-%02i-%02iT%02i:%02i:%02i\n"
, AAAA, MM, DD, hh, mm, 0);
return 0;
}
]]>
Lee una fecha/hora del teclado y la imprime en formato de fecha/hora internacional de acuerdo al estándar ISO-8601. Al correr el programa note como scanf ignora los caracteres separadores / y : al leer las fechas y horas. Note como printf inserta ceros a la izquierda cuando los números son menores a 10. Obtener código fuente.
Ejercicio. Modifique el programa de fecha y hora, para que imprima la cantidad años cumplidos que tiene la persona; y la cantidad de días que faltan para el próximo cumpleaños, a menos de que la persona esté cumpliendo años y en tal caso, la felicita. Estudie las funciones del encabezado <time.h>.
El formateo de los parámetros se hace entre el % y la letra que indica el tipo de datos del parámetro. Se pueden escribir números que indican a la función hacer un campo de tantos caracteres como indicados por ese número. Esto es especialmente útil para imprimir tablas. El siguiente ejemplo muestra las tablas de multiplicar de un número mínimo a un máximo indicados por el usuario en incrementos de 1.
int main()
{
int minimo = 0;
printf("Minimo: ");
scanf("%i", &minimo);
int maximo = 0;
printf("Maximo: ");
scanf("%i", &maximo);
for ( int i = minimo; i <= maximo; ++i )
{
for ( int j = minimo; j <= maximo; ++j )
printf("%5i ", i * j);
printf("\n");
}
return 0;
}
]]>
Imprime las tablas de multiplicar entre dos números en incrementos de 1. Obtener código fuente.
Ejercicio. Modifique el programa de tablas de multiplicar para que estime automáticamente el ancho ideal para cada valor de la tabla. Indague sobre el especificador de ancho * en la cadena de formato. Probablemente deba implementar una función que dado un número retorne la cantidad de dígitos que éste ocupa.
Ejercicio. Modifique el programa de tablas de multiplicar para que permita al usuario indicar no sólo el mínimo y el máximo, sino también los incrementos. Estos tres valores pueden ser números reales. Utilice los números de mayor precisión que provea su compilador. Permita al usuario también escoger el operador. Trate de dar un estilo profesional al formateo de la tabla resultado como se solicita en este enunciado de Principios de informática.
Ejemplo: comando Estadísticas
#include
typedef long double real;
int main()
{
FILE* archivo = fopen("gr08.txt", "r");
if ( ! archivo )
{
fprintf(stderr, "No se pudo abrir %s\n", "gr08.txt");
return 1;
}
real minimo = DBL_MAX;
real maximo = -DBL_MAX;
real valor = 0.0;
real suma = 0.0;
unsigned long long contador = 0;
while ( fscanf(archivo, "%Lf", &valor) != EOF )
{
++contador;
if ( valor < minimo ) minimo = valor;
if ( valor > maximo ) maximo = valor;
suma += valor;
}
fclose(archivo);
if ( contador > 0 )
{
printf("Minimo: %.2Lf\n", minimo);
printf("Maximo: %.2Lf\n", maximo);
printf("Promedio: %.2Lf\n", suma / contador);
}
return 0;
}
]]>
Programa en C que lee números reales de la entrada estándar o archivos por parámetro e imprime estadísticas como el mínimo, máximo y promedio. Obtener código fuente. Descargar datos de ejemplo.
Operadores de manejo de bits (bitwise operators)
#include
// La cantidad de veces que se va a invocar cada versión de la función para hacer el benchmark
const size_t ITERACIONES = 50000000;
// Crea un tipo de datos nuevo: entero_t que equivale a unsinged long int
typedef unsigned long int entero_t;
// Retorna true si valor es una potencia de 2. El cálculo se hace con divisiones enteras
bool es_potencia_de_2_v1(entero_t valor)
{
// Divide el valor original sucesivamente por 2 y si todos los módulos fueron 0 es porque
// el número era una potencia de 2. Apenas no sea divisible por 2 retorna false
for ( ; valor > 1; valor /= 2 )
if ( valor %2 != 0 )
return false;
return true;
}
// Retorna true si valor es una potencia de 2. El cálculo se hace con operadores de bits
bool es_potencia_de_2_v2(entero_t valor)
{
// Si el resultado de un AND binario de un número y su predecesor es 0, es una potencia
// de 2: http://www.cprogramming.com/tutorial/bitwise_operators.html
return !(valor & (valor - 1));
}
// Defínase esta macro al invocar el compilador para generar un ejecutable con benchmark. Ej:
// g++ -DBENCHMARK potencia2.cpp -o potencia2_benchmark
#if defined(BENCHMARK)
// Invoca ITERACIONES veces cada una de las dos versiones de la función es_potencia_de_2 y
// cronometriza cuanto tarda cada una y finalmente imprime sus duraciones en segundos; con
// el fin de determinar cuál de las dos versiones es más eficiente
int main()
{
// [1] Primera versión de es_potencia_de_2: con divisiones enteras
// Se registra el tick del reloj del procesador en que la primera versión inició su ejecución
clock_t inicio = clock();
// Se invoca es_potencia_de_2_v1() ITERACIONES veces y se cuenta cuántas potencias encuentra
// entre 1 e ITERACIONES, sólo con fines ilustrativos, lo que importa es correr la función
// muchas veces y cronometrizarla
size_t conteo_potencias_2 = 0;
for ( size_t i = 0; i < ITERACIONES; ++i )
if ( es_potencia_de_2_v1(i) )
++conteo_potencias_2;
// Imprimir lo que tardó la versión 1 de la función es_potencia_de_2 con divisiones
// la diferencia 'clock() - inicio' indica la cantidad de ticks del reloj que transcurrieron
// mientras se hicieron las ITERACIONES invocaciones. Para convertirlas a segundos se debe
// hacer división flotante entre la macro CLOCKS_PER_SEC
std::cout << conteo_potencias_2 << " potencias encontradas con divisiones en "
<< (static_cast(clock() - inicio) / CLOCKS_PER_SEC) << " segundos" << std::endl;
// [2] Se repite el proceso anterior con la segunda versión de es_potencia_de_2: con
// operadores de bits
inicio = clock();
conteo_potencias_2 = 0;
for ( size_t i = 0; i < ITERACIONES; ++i )
if ( es_potencia_de_2_v2(i) )
++conteo_potencias_2;
std::cout << conteo_potencias_2 << " potencias encontradas con operadores de bits "
<< (static_cast(clock() - inicio) / CLOCKS_PER_SEC) << " segundos" << std::endl;
return 0;
}
#else // defined(BENCHMARK)
// Permite al usuario ingresar números interactivamente, y muestra cuáles de ellos son
// potencias de 2
int main()
{
while ( true )
{
// Lee un entero de la entrada estándar (usualmente el teclado)
entero_t valor = 0;
std::cout << "Número: ";
std::cin >> valor;
// Si se ingresó 0, se termina la invocación de main() retornando 0 al sistema operativo
if ( valor == 0 ) return 0;
// Si es otro valor, se indica en la salida estándar si es o no potencia de 2
// Los paréntesis redondos aquí son obligatorios, dado que el operador << tiene
// precedencia sobre el operador ternario ?:
std::cout << valor << (es_potencia_de_2_v2(valor) ? " " : " no ") << "es potencia de dos"
<< std::endl << std::endl;
}
return 0;
}
#endif // defined(BENCHMARK)
]]>
Operadores de bits pueden utilizarse para mejorar la eficiencia de un programa. Obtener código fuente.
Punteros
void swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
int main()
{
int a = 1;
int b = 2;
printf("a=%i, b=%i\n", a, b);
swap(&a, &b);
printf("a=%i, b=%i\n", a, b);
return 0;
}
]]>
Función en C que intercambia dos valores dado que recibe punteros hacia los valores originales en lugar de copias de esos valores. Obtener código fuente.
Introducción a la programación en C++
Hola mundo en C/C++
using namespace std;
int main()
{
cout << "Hola mundo" << " C++" << endl;
return 0;
}
]]>
Hola mundo en C++
Uso de namespaces.
Espaciado en blanco e indentación
Ejemplo de una clase
using std::cin;
using std::cout;
using std::endl;
class Entero
{
protected:
long valor;
public:
Entero(long valor)
: valor(valor)
{
}
bool esPar()
{
return valor % 2 == 0;
}
void imprimirPropiedades()
{
cout << valor << " es par: " << esPar() << endl;
}
};
int main()
{
long valor = 0;
cout << "Número: ";
cin >> valor;
Entero entero(valor);
entero.imprimirPropiedades();
return 0;
}
]]>
Una clase de propiedades de enteros. Obtener código fuente.
Espacios de nombres (namespaces)
Entre más software se escribe en una aplicación extensa, bibliotecas o módulos, mayor probabilidad de que un identificador colisione. Un namespace agrupa identificadores en un espacio o ámbito con un nombre. Tras definir todos los identificadores en un namespace, éste los absorbe, y el nombre del namespace se convierte en el único identificador global que podría colisionar con otros símbolos globales. Antes de la introducción de los namespaces, era común que los programadores introdujeran prefijos en sus clases y funciones globales (como CString, QString o wxString) para evitar colisiones. Ejemplo de uso:
// Todo lo que se defina dentro de ucr se debe acceder como ucr::algo
namespace ucr
{
// una variable global dentro del espacio de nombres ucr
double cout = 0.0;
// Las clases y estructuras son otros espacios de nombres
struct Impresora
{
static bool cout;
};
bool Impresora::cout = false;
} // namespace ucr
// Una varable global accesible como ::cout desde cualquier contexto
char cout = '\n';
int main()
{
// Una variable local tapa a la variable global homonima
int cout = 0;
cout = 1;
std::cout << "cout = " << cout << std::endl;
// Accede a la variable global, que se encuentra en el contexto global ::
::cout = 'z';
std::cout << "::cout = " << ::cout << std::endl;
// Accede a una variable global dentro de un espacio de nombres llamado ucr
ucr::cout = 3.14;
std::cout << "ucr::cout = " << ucr::cout << std::endl;
// Accede a un miembro de una clase o estructura que esta dentro de un namespace
ucr::Impresora::cout = true;
std::cout << "ucr::Impresora::cout = " << ucr::Impresora::cout << std::endl;
// C++ sabe cual es el cout de la std, imprimir su tamanno en bytes
std::cout << "sizeof(std::cout) = " << sizeof(std::cout) << std::endl;
return 0;
}
]]>
Programa que muestra cómo acceder a elementos que se encuentran en diferentes espacios de nombres y cómo crear uno. Obtener código fuente.
Relación entre estructuras y clases
// Por defecto los miembros de una estructura son publicos en C++
struct Fecha
{
int dia;
int mes;
int anno;
// Las estructuras y las clases pueden tener metodos
// Los constructores son metodos especiales que se invocan siempre que
// un objeto es creado con el fin de inicializar los miembros de datos
// Note la sintaxis para inicializar Constructor() : miembro(valorIni)
Fecha(int d = 0, int m = 0, int a = 0) : dia(d), mes(m), anno(a) { }
};
// Las enumeraciones declaran un tipo de datos nuevo en el programa: un
// numero entero que solo puede tomar los valores declarados en el cuerpo
// de la enumeracion, que son constantes
enum Sexo
{
desconocido, // 0
hombre, // 1
mujer // 2
};
// Textos equivalentes a las constantes de la enumeracion Sexo
const char* const SexoStr[] =
{
"desconocido",
"hombre",
"mujer"
};
// Por defecto los miembros de una clase son privados en C++
class Persona
{
private:
long cedula;
Fecha fechaNacimiento; // una estructura dentro de otra
Sexo sexo;
char* nombre;
static long totalPersonas;
public:
static long obtenerTotalPersonas() { return totalPersonas; }
public:
// Constructor. Los miembros de datos se deben inicializar en el
// mismo orden en que fueron declarados. El constructor Fecha() se
// invoca automaticamente cuando llegue el turno de inicializar
// la estructura fechaNacimiento
Persona(long cedula = 0, const Fecha& fn = Fecha(), Sexo sexo = desconocido, const char* nombre = "");
~Persona();
// Un metodo: una funcion que esta dentro de una clase o estructura
int calcularEdad() const;
inline long obtenerCedula() const { return this->cedula; }
inline const char* obtenerNombre() const { return nombre; }
void imprimir() const;
}; // nunca olvidar los dos puntos
#endif
]]>
#include
#include
// Las variables estaticas son variables globales, hay que darles memoria
long Persona::totalPersonas = 0;
// Constructor. Los miembros de datos se deben inicializar en el mismo orden en
// que fueron declarados. El constructor Fecha() se invoca automaticamente cuando
// llegue el turno de inicializar la estructura fechaNacimiento
Persona::Persona(long cedula, const Fecha& fn, Sexo sexo, const char* nombre)
: cedula(cedula)
, fechaNacimiento(fn)
, sexo(sexo)
, nombre(strdup(nombre))
{
++totalPersonas;
}
Persona::~Persona()
{
free(nombre);
}
// Un metodo: una funcion que esta dentro de una clase o estructura
int Persona::calcularEdad() const
{
// El parametro Persona* this es automaticamente agregado por
// el compilador de C++, es buena practica acceder a los miembros
// a traves de this->miembro, pero no es necesario
return 2013 - /*this->*/fechaNacimiento.anno;
}
void Persona::imprimir() const
{
std::cout << cedula << ": " << nombre << ". " << SexoStr[sexo];
//fechaNacimiento.imprimir();
static long invocaciones = 0;
++invocaciones;
std::cout << "\nimprimir ha sido invocado " << invocaciones << " veces\n";
}
]]>
//using std::cout;
//using std::endl;
//using namespace std;
int main()
{
// Un metodo estatico solo puede acceder a variables estaticas
std::cout << "total personas: " << Persona::obtenerTotalPersonas() << std::endl;
// esto crea un objeto completo de ~104 bytes, no un puntero de 8 bytes ni una
// refeferencia como ocurre en Java, y luego llama al constructor por defecto
Persona persona;
// Crea un objeto automatico persona1 utilizando el constructor mas general
Persona persona1(101929328, Fecha(7,5,2000), mujer, "Ana Soto");
persona1.imprimir();
// Crea un ojeto en memoria dinamica con el operador new, el cual llama al constructor
Persona* persona2 = new Persona(191990212, Fecha(), hombre);
persona2->imprimir();
// Crea un arreglo de objetos, a cada uno se le invoca el constructor por defecto
Persona* familia = new Persona[3];
for ( size_t i = 0; i < 3; ++i )
familia[i].imprimir();
for ( size_t i = 0; i < 3; ++i )
familia[i].imprimir();
// Imprime el total de objetos de la clase Persona creados hasta el momento
std::cout << "total personas: " << Persona::obtenerTotalPersonas() << std::endl;
#if 0
// persona es un objeto, no un puntero, por lo que se usa el operador .
// para acceder a los miembros publicos
std::cout << "Edad 1 en años cumplidos: " << persona1.calcularEdad() << std::endl;
std::cout << "Edad 2 en años cumplidos: " << persona2->calcularEdad() << std::endl;
std::cout << "Edad f.2 en años cumplidos: " << familia[1].calcularEdad() << std::endl;
#endif
// persona2 es un objeto alojado en memoria dinamica, evitar fugas
delete persona2;
persona2 = NULL; // innecesario, pero es una buena practica
// Eliminar un arreglo requiere el operador delete[] y no delete
delete [] familia;
return 0;
}
]]>
El siguiente ejemplo agrega a la clase Fraccion la posibilidad de leerse del teclado e imprimirse en la pantalla con los objetos cin y cout. Permite hacer sumas conmutativamente con enteros, para lo cual el operador de suma debe implementarse como una función libre. La clase Fraccion otorga amistad a estas funciones libres para que puedan acceder a los miembros privados y protegidos. Sin embargo, si en la clase se tienen métodos get y set, se podría hacer innecesaria la amistad.
#include "Fraccion.h"
using namespace std;
int main()
{
cout << "Ingrese una fracción en formato a/b: ";
Fraccion f1;
cin >> f1;
Fraccion f2;
f2 = 2;
cout << "Ingrese una tercera fracción en formato a/b: ";
Fraccion f3;
cin >> f3;
cout << "-1 + " << f1 << " + " << f2 << " + " << f3 << " = " << -1 + f1 + f2 + f3 << endl;
return 0;
}
]]>
typedef long long entero;
class Fraccion
{
private:
entero numerador;
entero denominador;
public:
/// Constructor por defecto y constructor de conversion
Fraccion(entero numerador = 0, entero denominador = 1);
/// Destructor
~Fraccion();
// La relacion de amistad permite a los operadores sobrecargados como funciones libres acceder
// a los miembros privados y protegidos de los objetos Fraccion
friend std::ostream& operator<<(std::ostream& salida, const Fraccion& fraccion);
friend std::istream& operator>>(std::istream& entrada, Fraccion& fraccion);
friend Fraccion operator+(const Fraccion& a, const Fraccion& b);
};
/// Permite cout << f1 << f2 << ...
std::ostream& operator<<(std::ostream& salida, const Fraccion& fraccion);
/// Permite cin >> f1 >> f2 >> ...
std::istream& operator>>(std::istream& entrada, Fraccion& fraccion);
/// Permite f1 + f2
/// Permite f1 + 13
/// Permite 13 + f2
/// Nunca se invoca con dos tipos primitivos como: 13 + 2
Fraccion operator+(const Fraccion& a, const Fraccion& b);
#endif // FRACCION_H
]]>
Una clase para representar fracciones. Obtener código fuente.
Global.h
Un archivo de encabezado con definiciones comunes y útiles para la mayoría de clases que se harán de ahora en adelante en los ejemplos
#include
int eprintf(int errorcode, const char* format, ...)
{
va_list list;
va_start(list, format);
vfprintf(stderr, format, list);
va_end(list);
return errorcode;
}
]]>
Un encabezado con definiciones comunes para la mayoría de clases. Obtener código fuente: Global.h, Global.cpp.
Clase String
Clase en C++ para facilitar el manejo de cadenas de caracteres de C.
#include
#include "String.h"
using namespace std;
int main()
{
// Llama al constructor de conversion. Equivale a String s1 = String("Hola mundo");
String s1 = "Hola mundo";
// Llama al constructor por defecto
String s2;
// Llama al operador de asignacion
s2 = s1;
// Verifica el contenido de cada objeto
std::cout << s1 << endl;
std::cout << s2 << endl;
// Concatena un String con el operador +
// El String temporal resultado de filename + ".idx" se envia como primer parametro de
// fopen(const char*, const char*), es decir, un objeto String esta en un contexto
// donde se requiere un const char*, el operador de conversion es invocado entonces
String filename = "horoscopo.txt";
FILE* index = fopen(filename + ".idx", "r");
if ( ! index )
cerr << "No se pudo abrir " << (filename + ".idx") << endl;
else
fclose(index);
return 0;
}
]]>
#include
const int null = 0;
#define implicit
/** @class String
@brief Facilita el trabajo con cadenas de caracteres de C
*/
class String
{
private:
/// La cadena de C terminada en nulo. Siempre tiene memoria alojada en segmento de heap
char* str;
/// La longitud actual de la cadena apuntada por @a str
size_t len;
public:
/// Constructor por defecto y de conversion
/// ToDo: recibir un parametro 'size_t len = (size_t)-1' por si el llamador conoce el length
implicit String(const char* str = "");
/// Crea un buffer con la capacidad dada
explicit String(size_t capacity);
/// Constructor de copia. Se invoca cuando
/// String str2 = str1;
/// String str3 = String("algo");
/// String str4 = "algo";
/// void foo(String str5); foo("algo");
String(const String& other);
/// Operador de asignacion. Se invoca cuando
/// String str1;
/// str1 = str0;
/// str1 = String("algo");
/// str1 = "algo";
const String& operator=(const String& other);
/// Destructor
~String();
/// Retorna la cantidad de caracteres almacenados en el String
inline size_t length() const { return len; }
/// Permite acceder al string de C solo para lectura
inline const char* c_str() const { return str; }
/// Permite utilizar un objeto String en un contexto donde se requiere un const char*
inline operator const char*() const { return str; }
/// Permite acceder al i-esimo caracter almacenado en el String
/// El caracter puede ser modificado por el llamador, lo cual afectara al objeto String
/// @remarks No se verifica que @a i este fuera de rango
inline char& operator[](size_t i) { return str[i]; }
/// Permite acceder al caracter i-simo del String en modo solo lectura
/// @remarks No se verifica que @a i este fuera de rango
inline const char& operator[](size_t i) const { return str[i]; }
/// Obtiene el sub-string de length caracteres a partir del caracter start
String substr(size_t start, size_t length = (size_t)-1) const;
/// Obtiene el sub-string de length caracteres a partir del caracter start
String operator()(size_t start, size_t length = (size_t)-1) const;
// Amistad con los operadores sobrecargados para que puedan acceder a los miembros de datos
friend std::ostream& operator<<(std::ostream& output, const String& str);
friend String operator+(const String& a, const String& b);
};
/// Permite cout << str1 << str2 << ...
std::ostream& operator<<(std::ostream& output, const String& str);
/// Permite str1 + str2
String operator+(const String& a, const String& b);
#endif // STRING_H
]]>
Una clase para representar cadenas de caracteres de C. Obtener código fuente.
Plantillas (templates)
using namespace std;
// Esta macro permite obtener el minimo pero no verifica tipos lo que es inadecuado
// Las macros del preprocesador permite hacer metaprogramacion en C/C++
#define min(a,b) ((a) < (b) ? (a) : (b))
// Esta es una funcion libre plantilla. No es realmente una funcion, sino un molde
// para construir funciones. C++ utilizara esta plantilla para construir funciones
// a demanda conforme se usen en el programa; esto en tiempo de compilacion
template
TipoDato minimo(TipoDato a, TipoDato b)
{
return a < b ? a : b;
}
// El 'template ' declara TipoDato como un tipo de datos
// generico. C++ lo reemplazara por el tipo adecuado cuando se utilice la
// plantilla para construir una funcion particular. Notese que la declaracion
// de TipoDato solo esta disponible para la funcion que le sigue. Es decir el
// TipoDato declarado anteriormente solo esta disponible para la plantilla
// minimo() y maximo() no puede utilizarla; sino que maximo debe definir su
// propio TipoDato
template
TipoDato maximo(TipoDato a, TipoDato b)
{
return a > b ? a : b;
}
// Prueba una de las plantillas para construir funciones
int main()
{
long long a = 0;
cout << "a: ";
cin >> a;
long long b = 0;
cout << "b: ";
cin >> b;
// Cuando C++ ve minimo(a,b) no puede invocar a la plantilla
// minimo(TipoDato, TipoDato). Una plantilla no es una funcion. Es solo una
// plantilla para construir funciones con diferentes tipos de datos. C++
// entonces genera una funcion libre minimo(double, double) con esa plantilla
// automaticamente y la utiliza en la siguiente invocacion
cout << "minimo(" << a << ", " << b << ") = " << minimo(a,b) << endl;
// Si usamos la plantilla con otro tipo de datos (double) C++ creara otra
// funcion, en este caso, minimo(double, double) e invoca esa funcion generada
// en la siguiente linea
cout << "minimo(" << a << ", " << b << ") = " << minimo((double)a,(double)b) << endl;
// Esto no generara una nueva funcion minimo(double, double), sino que C++
// reutilizara la que ya se creo previamente
cout << "minimo(-0.01, -0.1) = " << minimo(-0.01, -0.1) << endl;
// A este punto nunca se utilizo la funcion maximo(), por lo que C++ nunca
// expandio la plantilla maximo(TipoDato, TipoDato)
return 0;
}
]]>
Funciones libres plantilla. C++ las expandirá con cualquier tipo de datos con que se usen en tiempo de compilación. Obtener código fuente.
Clase Array
#include "Array.h"
using std::cout;
using std::endl;
using std::cin;
int main()
{
Array arr;
Array arr2;
char value = char();
while ( cin >> value )
arr.add(value);
for (size_t i = 0; i < arr.count(); ++i )
cout << i << ": " << arr[i] << endl;
return 0;
}
]]>
const size_t incrementFactor = 2;
const size_t initialCapacity = 10;
template
class Array
{
private:
/// The count of elements stored in the array
size_t size;
/// The quantity of elements that can be stored in the array
/// without asking for more memory
size_t capacity;
/// A C-type array of elements
DataType* arr;
public:
/// Default constructor and conversion constructor
explicit Array(size_t capacity = initialCapacity);
/// Destructor
~Array();
/// Adds a copy of the given value to the ending position of the array
/// @return true if the element was added, false if no enough memory is available
bool add(const DataType& element);
/// Returns the number of elements stored in the array
inline size_t count() const { return size; }
/// Gets read and write access to the i-th element
inline DataType& operator[](size_t i) { return arr[i]; }
/// Gets read-only access to the i-th element
inline const DataType& operator[](size_t i) const { return arr[i]; }
private:
/// Copy constructor
Array(const Array& other);
/// Assignment operator
const Array& operator=(const Array& other);
/// Makes room for more values
bool incrementArray();
};
#include
template
Array::Array(size_t capacity)
: size(0)
, capacity(capacity ? capacity : initialCapacity)
, arr( new DataType[capacity] )
{
assert(arr);
}
template
Array::~Array()
{
delete [] arr;
}
template
bool Array::add(const DataType &element)
{
if ( size == capacity )
if ( ! incrementArray() )
return false;
arr[size++] = element;
return true;
}
template
bool Array::incrementArray()
{
size_t newCapacity = capacity * incrementFactor;
DataType* newArr = new DataType[newCapacity];
if ( ! newArr ) return false;
for ( size_t i = 0; i < size; ++i )
newArr[i] = arr[i];
capacity = newCapacity;
delete [] arr;
arr = newArr;
return true;
}
#endif // ARRAY_H
]]>
Una clase plantilla que implementa un arreglo dinámico. Obtener código fuente.
Clase Queue
El siguiente listado implementa un contenedor cola simplemente enlazada. Las inserciones al inicio o final de la cola son muy eficientes. Pero para acceder a un valor, se requiere recorrer todos los elementos previos. Se provee una clase Iterator para poder hacer este recorrido.
#include "Queue.h"
using namespace std;
int main()
{
Queue cola;
double data = 0.0;
while ( cin >> data )
cola.add(data);
cout << cola << endl;
for ( Queue::ConstIterator itr = cola.begin(); itr != cola.end(); ++itr)
cout << *itr << ", ";
cout << endl;
#if 0
cin.clear();
Queue colaStr;
string text;
while ( cin >> text )
colaStr.add(text);
cout << colaStr << endl;
#endif
return 0;
}
]]>
const int null = 0;
template
class Queue
{
private:
/// For each element stored in the queue, a Node is created to keep pointers to
/// the next element. Note Node is for Queue class use exclusively (private)
struct Node
{
/// A copy of the original element to be stored in the queue
DataType data;
/// A pointer to the next node in the queue
Node* next;
public:
/// Conversion constructor
explicit Node(const DataType& data) : data(data), next(null) { }
};
private:
/// A pointer to the first node in the queue. Null if queue is empty
Node* head;
/// A pointer to the last node in the queue. Null if queue is empty
Node* tail;
/// The count of elements currently stored in the queue
size_t size;
public:
/// Default constructor
Queue() : head(null), tail(null), size(0) { }
/// Destructor: when the queue is destroyed, all its values are deleted also
~Queue()
{
clear();
}
/// Returns true if queue is empty
inline bool empty() const { return head == null; }
/// Adds a copy of the given element into the queue
/// @return true on success, false if not enough memory
bool add(const DataType& data)
{
Node* node = new Node(data);
if ( ! node ) return false;
if ( empty() )
tail = head = node;
else
tail = tail->next = node;
return ++size;
}
/// Remove all elements from the queue
void clear()
{
for ( Node* node = head; head; node = head)
{
head = head->next;
delete node;
}
tail = null;
size = 0;
}
/// Prints all elements of the queue to the given output stream
/// @param output File or stream to print
/// @param separator Each element will be separated from another with this text
std::ostream& print(std::ostream& output, const char* separator = " ") const
{
for ( Node* node = head; node; node = node->next)
output << node->data << separator;
return output;
}
public:
/// Allows traversing the queue or pointing to a specific value in the queue
class ConstIterator
{
private:
/// An iterator mimics a pointer to this element which is stored in the queue
const Node* node;
public:
/// Default constructor and conversion constructor
explicit ConstIterator(const Node* node = null) : node(node) { }
/// Returns true if this iterator points to the same element than other iterator
inline bool operator!=(const ConstIterator& other) const { return node != other.node; }
/// Get read-only access to the pointed element in array
inline const DataType& operator*() const { return node->data; }
/// Pre-increment operator: ++itr
const ConstIterator& operator++()
{
node = node->next;
return *this;
}
/// Post-increment operator: itr++
ConstIterator operator++(int)
{
ConstIterator previous(node);
node = node->next;
return previous;
}
};
/// Returns an iterator to the first element in the queue
inline ConstIterator begin() const { return ConstIterator(head); }
/// Returns an iterator to an invalid position in the queue
inline ConstIterator end() const { return ConstIterator(); }
private:
Queue(const Queue& other);
const Queue& operator=(const Queue& other);
};
/// Allows std::cout << myqueue1 << myqueue2 << ... ;
template
std::ostream& operator<<(std::ostream& output, const Queue& queue)
{
return queue.print(output);
}
#endif // QUEUE_H
]]>
Una clase plantilla que implementa una cola enlazada. Obtener código fuente.
Clase BinarySearchTree
En un árbol binario de búsqueda cada nodo conoce cuales son sus nodos hijos: uno a la izquierda y otro a la derecha, de tal forma que cualquier nodo que se encuentre a la izquierda tiene un valor menor que el nodo y a la derech a un valor mayor o igual al nodo. El siguiente implementa algunos métodos de un árbol binario de búsqueda no balanceado.
Una clase plantilla que implementa un árbol binario de búsqueda no balanceado. Obtener código fuente.
Mapas (Arreglos asociativos, Hash o Diccionarios)
Un mapa, también llamado arreglo asociativo, hash o diccionario; es una estructura de datos que permite asociar una llave con un valor. Tanto las llaves como sus valores asociados pueden ser de cualquier tipo de datos. Por ejemplo, se puede asociar una palabra con su definición, o una palabra con su frecuencia de aparición en un texto, o un nivel de educación (un número) contra las personas que la han alcanzado (una lista enlazada de objetos), etc. El siguiente ejemplo muestra el contenedor std::map<Key, Value> de la Biblioteca Estándar de Plantillas (STL) de C++.
#include
Ejemplo minimalista de implementación de un diccionario de definiciones. Obtener código fuente.
El siguiente ejemplo es un contador de palabras. Recibe una lista de nombres de archivo por parámetro. Cada palabra del archivo es cargada a un mapa (implementado como un árbol binario de búsqueda balanceado). Cada palabra es asociada con un entero largo, el cual indica la cantidad de veces que se ha encontrado la palabra en el archivo. El operador [] del mapa recibe una llave y retorna una referencia a su valor asociado. Cada vez que se encuentra una palabra, se incrementa su contador (con el operador ++). Finalmente el programa imprime cada una de las palabras y su conteo en la salida estándar.
#include
#include
Un eficiente contador de palabras únicas en archivos de texto. Obtener código fuente. Puede probar con el texto completo del Quijote de La Mancha.
Ejercicio. Modifique el ejemplo del contador de palabras únicas para que ignore caracteres especiales que se cargan al inicio o final de las palabras. Indague sobre los métodos de la clase std::string.
Ejercicio. Modifique el programa anterior, de tal forma que para cada palabra almacene las posiciones en el archivo donde la palabra aparece. Es decir, en su mapa cada palabra debe estar asociada con una lista enlazada de enteros (streampos). Indague sobre el método ifstream::tellg(). Su programa debe imprimir cada palabra única, la cantidad de veces que aparece en el archivo, y la lista de posiciones donde se encuentra.
Ejercicio. Modifique el contador de palabras únicas para que imprima las palabras en orden descendente de aparición. Sugerencia: cree otro mapa donde asocie la frecuencia de aparición con una lista de palabras. Recorra el mapa original para llenar el segundo. Luego imprima el segundo mapa de tal forma que las palabras más frecuentes aparezcan primero.
Herencia y polimorfismo
En este capítulo se construirá parcialmente una jerarquía de preguntas para un juego de trivia, las cuales se muestra en el siguiente diagrama:
Diagrama UML con una jerarquía de diversos tipos de preguntas para un juego de Trivia
Herencia
El siguiente código muestra la clase base Pregunta y la clase hija PreguntaSeleccionUnica. Ambas son capaces de cargarse de un archivo de texto.
Archivo de texto con preguntas de seleccion unica
#include
#include "PreguntaSeleccionUnica.h"
using namespace std;
int main()
{
vector preguntas;
PreguntaSeleccionUnica pregunta;
ifstream archivo("preguntas.txt");
while ( archivo >> pregunta )
preguntas.push_back(pregunta);
archivo.close();
for ( size_t i = 0; i < preguntas.size(); ++i )
cout << preguntas[i];
return 0;
}
]]>
>(std::istream& in, PreguntaSeleccionUnica& pregunta)
{
const size_t bufferSize = 2048;
char buffer[bufferSize];
in.getline(buffer, bufferSize);
pregunta.texto = buffer;
in >> pregunta.respuesta; in.ignore();
pregunta.opciones.clear();
for ( size_t i = 0; i < 4; ++i )
{
in.getline(buffer, bufferSize);
pregunta.opciones.push_back(buffer);
}
in.ignore();
return in;
}
std::ostream& operator<<(std::ostream& out, const PreguntaSeleccionUnica& pregunta)
{
for ( size_t i = 0; i < pregunta.opciones.size(); ++i )
out << '\t' << i + 1 << ": " << pregunta.opciones[i] << std::endl;
return out << std::endl;
}
]]>
Programa que carga preguntas de selección única de un archivo de texto. Las preguntas utilizan herencia. Obtener código fuente.
Polimorfismo
La función main() en la sección anterior contenía un arreglo que únicamente podía tener preguntas de selección única. Pero lo esperable es que las preguntas sean de diferente naturaleza. Para poder mantener en un contenedor objetos de distinta clase, se debe utilizar herencia. El contenedor tendría punteros o referencias a la clase base. Con estos punteros o referencias, sólo se puede acceder a la parte "base" de cada objeto. El polimorfismo es una funcionalidad provista por los compiladores de C++ para hacer que un método, al ser invocado con un puntero o referencia a la clase base, se invoque la versión de la clase hija (con la que fue construida el objeto) y no con la clase base. Los métodos que tienen este comportamiento se llaman métodos virtuales y C++ crea tablas de punteros a funciones virtuales (vtable) para implementar este mecanismo.
Los siguientes listados agregan la clase PreguntaAbierta a la jerarquía y modifican el método imprimir() para que sea virtual. Si se retira el comentario del método virtual puro aplicar(), la clase Pregunta se convierte en abstracta. Este método será necesario para aplicar una pregunta al jugador.
Archivo de texto con preguntas abiertas y de selección única
#include
#include "PreguntaSeleccionUnica.h"
#include "PreguntaAbierta.h"
using namespace std;
int main()
{
vector preguntas;
string tipo;
ifstream archivo("preguntas.txt");
while ( archivo >> tipo )
{
archivo.ignore();
if ( tipo == "abierta" )
{
PreguntaAbierta* pregunta = new PreguntaAbierta();
archivo >> *pregunta;
preguntas.push_back(pregunta);
}
else if ( tipo == "seleccion_unica" )
{
PreguntaSeleccionUnica* pregunta = new PreguntaSeleccionUnica();
archivo >> *pregunta;
preguntas.push_back(pregunta);
}
}
archivo.close();
for ( size_t i = 0; i < preguntas.size(); ++i )
cout << * preguntas[i];
size_t contador = 0;
for ( size_t i = 0; i < preguntas.size(); ++i )
{
PreguntaSeleccionUnica* psu = dynamic_cast(preguntas[i]);
if ( psu )
++contador;
delete preguntas[i];
}
cout << endl << contador << " preguntas de seleccion unica eliminadas" << endl;
return 0;
}
]]>
Introducción de polimorfismo en la jerarquía de preguntas. Obtener código fuente.
Si se hace el método leer() virtual en la clase Pregunta, se puede simplificar ligeramente la creación de objetos pregunta en el main(). Una función crearPregunta() se encarga de crear un objeto Pregunta de acuerdo al tipo encontrado en el archivo de texto. Esta función libre lleva sobre sí la parte menos polimórfica del programa. En caso de agregarse una nueva pregunta, se debe modificar crearPregunta() para considerar el nuevo tipo. El resto del programa debería mantenerse inalterado.
#include
#include "PreguntaSeleccionUnica.h"
#include "PreguntaAbierta.h"
using namespace std;
Pregunta* crearPregunta(const string& tipo)
{
if ( tipo == "abierta" ) return new PreguntaAbierta();
if ( tipo == "seleccion_unica" ) return new PreguntaSeleccionUnica();
return 0;
}
int main()
{
vector preguntas;
string tipo;
ifstream archivo("preguntas.txt");
while ( archivo >> tipo )
{
archivo.ignore();
Pregunta* pregunta = crearPregunta(tipo);
archivo >> *pregunta;
preguntas.push_back(pregunta);
}
archivo.close();
for ( size_t i = 0; i < preguntas.size(); ++i )
cout << * preguntas[i] << endl;
for ( size_t i = 0; i < preguntas.size(); ++i )
delete preguntas[i];
return 0;
}
]]>
#include
#include
class Pregunta
{
protected:
std::string tipo;
std::string texto;
std::string respuesta;
public:
Pregunta(const std::string& tipo);
virtual ~Pregunta() { }
virtual std::istream& leer(std::istream& in);
virtual std::ostream& imprimir(std::ostream& out) const;
/// Aplica la pregunta al jugador. ES decir, le hace la pregunta y espera la respuesta.
/// Retorna la cantidad de puntos obtenidos por el jugador, lo cual depende de si
/// contesto o no correctamente la pregunta.
virtual int aplicar() const = 0;
};
// cin >> pregunta
inline std::istream& operator>>(std::istream& in, Pregunta& pregunta)
{
return pregunta.leer(in);
}
// cout << pregunta
inline std::ostream& operator<<(std::ostream& out, const Pregunta& pregunta)
{
return pregunta.imprimir(out);
}
#endif // PREGUNTA_H
]]>
La lectura de las preguntas se puede hacer también polimórifica. Obtener código fuente.
Ejemplo: Población universitaria
Ejemplo de una jerarquía de personas que conforman la población universitaria:
#include
#include
#include "Student.h"
#include "Employee.h"
void print_population(const vector& population)
{
for (size_t i = 0; i < population.size(); ++i)
population[i]->print(cout);
}
// Factory method. It could be a class
Person* createPerson(const string& role)
{
if ( role == "estudiante" )
return new Student();
if ( role == "administrativo" )
return new Employee();
// ...
return NULL;
}
int main()
{
ifstream source("poblacion.txt");
vector population;
string role;
while ( source >> role )
{
Person* person = createPerson(role);
if ( person )
{
person->load(source);
population.push_back(person);
#if 0
Student* temp = dynamic_cast(person);
if ( temp )
temp->specificMethodOfStudent();
#endif
}
}
source.close();
print_population(population);
for (size_t i = 0; i < population.size(); ++i)
delete population[i];
return 0;
}
]]>
Jerarquía de clases que representan los roles de las personas involucradas con una universidad. Obtener código fuente.
Programación de interfaces gráficas (Qt)
La programación de interfaces gráficas es dependiente de cada arquitectura: Windows, MacOSX, iOS, Android, Gnome, KDE, Xcfe, X.org, etc. El desarrollador tendrá que programar la misma lógica en cada plataforma, lo cual consume recursos cuantiosos. Algunos desarrolladores crean un conjunto de código (clases, funciones libres, etc.) que aíslan a la lógica del programa de la interfaz de cada sistema operativo. Algunos de ellos publican este código como una biblioteca de programación multiplataforma, con diferentes licencias. Ejemplos son wxWidgets y Qt. En este capítulo se introducirá Qt, cuyo aprendizaje serio requiere trabajo adicional del estudiante.
Para hacer un hello world, cree un main.cpp con el código de abajo {Sugerencia: escríbalo y no lo copie/pegue}. Los includes tienen el mismo nombre de la clase que se quiere usar. Qt hace las conversiones para encontrar el .h correspondiente. El main() instancia un objeto QApplication que luego entrará en el ciclo de eventos (event loop) cuando se le invoque exec(). El main() crea un QLabel invisible hasta que se le invoque el método show(). Como es la única ventana, esta se convierte en el main window y al cerrarse, invocará automáticamente el método QApplication::quit() que terminará el programa.
La forma de compilar el programa anterior varía dependiendo del OS y el compilador que se quiera usar. Trolltech provee un mecanismo independiente de la plataforma: QMake. QMake recibe un project file (.pro) en una notación independiente del OS y del compilador y genera un Makefile dependiente de ambos.
Usted puede crear el archivo .pro manualmente o pedirle a QMake que haga uno por usted con qmake -project. QMake tomará el nombre de la carpeta donde se invoque como el nombre del proyecto y automáticamente incluirá todos los fuentes (.cpp) que encuentre en esa carpeta:
El archivo .pro generado tiene el aspecto de abajo. La directiva TEMPLATE indica si se quiere generar una aplicación (app) o una biblioteca (lib). SOURCES indica los archivos que forman parte del proyecto. HEADERS los encabezados (.h) del proyecto. Las demás son opcionales. INCLUDEPATH y DEPENDPATH indican los directorios en los que el compilador buscará los include files. CONFIG = -moc indica que el proyecto no necesita uar el MetaObject Compiler.
Para generar un Makefile simplemente corra qmake en el directorio donde está el archivo .pro. Los archivos generados dependen del OS donde se invoque qmake. Finalmente para compilar el proyecto emita make ó make release ó make debug. Si usa MinGW cambie make por mingw32-make o si usa VC++ por nmake. Esto generará la biblioteca o el ejecutable de su programa.
Layouts, jerarquía de objetos y manejo de memoria
El siguiente ejemplo ubica un label bajo el otro, indiferentemente del tamaño de la ventana. En este caso un QWidget se emplea como ventana principal. ¿Cómo ocurre esto? Cuando un QWidget es creado y no recibe un objeto parent por parámetro en el constructor, crea una nueva jerarquía de widgets. Cuando se le invoca su método show() y aún no tiene un parent, se convierte en una ventana independiente y se registra con el objeto QApplication. Varias ventanas independientes pueden estar flotando y pertenecer a la misma aplicación. Cada vez que una ventana independiente se cierra avisa al QApplication, y éste termina su ejecución cuando la última de ellas se cierra.
#include
#include
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QWidget window;
QLabel* label1 = new QLabel("This is the first paragraph");
QLabel* label2 = new QLabel("The history continues here");
QVBoxLayout* mainLayout = new QVBoxLayout(& window);
mainLayout->addWidget(label1);
mainLayout->addWidget(label2);
window.show();
return app.exec();
}
]]>
El objecto QVBoxLayout se encarga del ordenamiento de los widgets hijos del widget que recibe por parámetro en su constructor. Es decir, que ese QVBoxLayout se encargará de ordenar los hijos que tenga window, automáticamente de acuerdo al tamaño de la ventana y de los labels.
Cuando los QLabel se crean, no reciben un parent en su constructor, es decir, crean una nueva jerarquía de widgets momentáneamente. Sin embargo, cuando se agregan al QVBoxLayout, éste se encarga de asignarles el window como su padre. Cuando el window se muestra, lo hará con todos sus hijos automáticamente.
En el código anterior se crea tres objetos con el operador new ¿por qué no se les invoca delete? Qt ayuda en parte del manejo de memoria. Todos los objetos heredados de QObject pueden tener otros QObject como hijos. Cuando un QObject se destruye, automáticamente destruye todos sus hijos y así recursivamente. En el código de arriba se crea esta jerarquía:
El QVBoxLayout se crea como hijo de window al recibirlo en su contructor, pero cuando se le agregan los labels, éste los hace hijos de su parent widget y no hijos propios, esto porque los widgets necesitan tener como padre el widget donde se dibujan.
El mecanismo de padres e hijos de QObject funciona siempre y cuando los hijos hayan sido creados en el heap con el operador new para poderles hacer delete, como ocurre en la jerarquía de arriba. Si alguno de los labels o el QVBoxLayout fuese creado como una variable en la pila, el compilador la eliminaría automáticamente y luego el mecanismo de QObject trataría de eliminarla por segunda vez haciendo que el programa se caiga. Nótese que es seguro construir QObjects en la pila cuando no tienen un parent QObject, como ocurre con window.
QVBoxLayout distribuye elemento tras elemento en forma vertical, QHBoxLayout en horizontal y QGridLayout en una cuadrícula los elementos que se le agreguen con addWidget(widget, row, col).
#include
#include
const size_t rows = 13;
const size_t cols = 5;
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QWidget window;
QGridLayout* mainLayout = new QGridLayout(& window);
for (size_t row = 0; row < rows; ++row)
for (size_t col = 0; col < cols; ++col)
mainLayout->addWidget( new QLabel(
QString("Label (%1,%2)").arg(row + 1).arg(col + 1) ), row, col );
window.show();
return app.exec();
}
]]>
El siguiente ejemplo, muestra la creación de algunos widgets (controles) típicos de las interfaces gráficas, seleccionados aleatoriamente en cada invocación del programa. Nótese que se puede formatear texto dentro de un QString utilizando secuencias con números "%1", "%2", etc. en lugar de letras para indicar tipos de datos como ocurre con las funciones printf(). Qt reemplaza estos secuencias con el i-ésimo argumento, indicado con el método arg(), lo cual permite cambiar de posición estas secuencias a la hora de traducir la aplicación de un idioma a otro. La línea 44 provoca que al presionarse el botón Salir, se cierre el mainWindow como se explica en la próxima sección.
#include
#include
#include
#include
#include
#include
#include
#include
#include
QWidget* crearControl(int i, int j)
{
static int tipo = rand() % 5;
switch ( tipo )
{
case 0: return new QLabel( QString("Label (%1, %2)").arg(i + 1).arg(j + 1));
case 1: return new QLineEdit( QString("LineEdit (%1, %2)").arg(i + 1).arg(j + 1));
case 2: return new QTextEdit( QString("TextEdit (%1, %2)").arg(i + 1).arg(j + 1));
case 3: return new QCheckBox( QString("Checkbox (%1, %2)").arg(i + 1).arg(j + 1));
case 4: return new QRadioButton( QString("RadioButton (%1, %2)").arg(i + 1).arg(j + 1));
}
return 0;
}
int main(int argc, char* argv[])
{
srand( time(0) );
QApplication app(argc, argv);
QWidget mainWindow;
QVBoxLayout* mainLayout = new QVBoxLayout(&mainWindow);
QGridLayout* gridLayout = new QGridLayout();
mainLayout->addLayout( gridLayout );
for ( int i = 0; i < 5; ++i )
for ( int j = 0; j < 4; ++j )
gridLayout->addWidget( crearControl(i,j), i, j );
QPushButton* botonSalir = new QPushButton("Salir");
mainLayout->addWidget( botonSalir );
QObject::connect( botonSalir, SIGNAL(clicked()), &mainWindow, SLOT(close()) );
mainWindow.show();
return app.exec();
}
]]>
En cada ejecución crea controles escogidos aleatoriamente y ordenados en una cuadrícula. Obtener código fuente.
Signals and slots
Si se quiere manejar inputs del usuario, habrá que comunicar objetos. Qt provee un mecanismo llamado Signals and Slots que comunica dos o más objetos entre sí y además Qt se encarga de romper la comunicación automáticamente cuando un objeto participante es eliminado, evitando que el programa se caiga.
El siguiente ejemplo muestra un push button como ventana principal. La invocación al método estático connect() de QObject, hace una conexión entre un objeto que emite una señal y el objeto que la recibe. Esta conexión se lee: cada vez que el usuario presiona el botón, button emitirá la señal clicked() y en su respuesta, Qt invocará el método quit() en el objeto app que interrumpe el event loop y por consiguiente la ejecución de la aplicación.
Cualquier método puede ser un signal o un slot si es marcado como tal con macros de Qt. Hay una relación N:M entre ellas. Una señal puede conectarse con M slots, incluso de diferentes objetos; y un mismo slot puede invocarse para N señales distintas o de distintos objetos. Cada vez que una señal es emitida, Qt invocará todos los slots conectados con dicha señal, en cualquier orden. Siempre deben usarse las macros SIGNAL y SLOT en los parámetros indicados ya que connect() espera strings generados de forma consistente a partir de los métodos.
Una conexión puede transportar valores. El siguiente ejemplo muestra un valor en label, el cual puede ser cambiado con un spinbox o un slider. Es decir, cuando cualquiera de los dos widgets que permiten entrada (el spinbox o el slider) es cambiado por el usuario, se debe actualizar los otros dos widgets: el label y el otro control.
Note que todos los signals y todos los slots transportan un entero como parámetro: el nuevo valor en el control modificado por el usuario. Esto funciona si no se tiene que hacer conversiones. Por ejemplo, ninguna de las señales emitidas por el spinbox o el slider pueden conectarse con setText(QString&) de QLabel. Si una conversión es ineludible, usted deberá heredar la clase e implementar un SLOT que hace la conversión.
Sin embargo una señal que se emite con varios parámetros, puede ser conectada con un slot que recibe menos. Los parámetros adicionales son simplemente ignorados. Así por ejemplo, el QSlider::valueChanged(int) podría ser conectado con QApplication.quit(). foo(int, double) puede se conectado con bar(), bar(int) y bar(int, double); pero no con bar(double) o bar(int,double,int).
Si usted hace una conexión inválida, ni el compilador ni el linker se quejarán; lo hará la aplicación cuando corra, con un warning message.
Módulos de Qt
Qt se compone de varias bibliotecas (o módulos) y varias herramientas, como QMake. En la versión 4.7 son las siguientes:
QtCore provee tipos de datos básicos (QString, QByteArray), estructuras de datos básicas (QList, QVector, QHash), input/output (QIODevice, QTextStream, QFile), hilos (QThread, QWaitCondition), clases abstractas (QObject, QCoreApplication).
QtGui provee widgets (QWidget, QLabel, QPushButton), layouts (QVBoxLayout, QGridLayout), menu-based windows (QMainWindow, QMenu), clases para dibujar (QPainter, QPen, QBrush), dialogs (QFileDialog, QPrintDialog), y la clase QApplication {debería haberse llamado QGuiApplication}.
QtOpenGL provee QGLWidget, un widget donde puede dibujar usando comandos de OpenGL. Requiere QtGui y esta a QtCore.
QtSql provee acceso a bases de datos a través de SQL. Clases para establecer conexiones, consultas y modificar datos, que trabajan transparentemente con varios motores populares como PostgreSQL, MySQL, SQLite; Oracle y SQLServer.
QtXml provee un simple nonvalidating XML parser a través de SAX2 o DOM. El segundo permite manipular la estructura de elementos del documento.
Qt3Support facilita migrar programas de Qt3 a Qt4. No debe usarse para programas nuevos.
QtSvg soporta SVG Basic y SVG Tiny para desplegar archivos SVG y animaciones. Todavía no permite manipular su DOM tree.
QtAssitantClient permite controlar remotamente la aplicación QtAssistant, útil para desplegar ayuda al usuario. Vea la clase QAssistantClient.
QTestLib contiene facilidades para escribir unit tests, similar a JUnit de Java.
QtDBus es el estandar de comunicación en *nix OS, reemplazando el Interprocess communication (IPC). Sólo funciona en Unix aunque podría ser portado a Win/Mac.
ActiveQt disponible sólo en Windows y en commercial Qt, permite implementar ActiveX componentes o usarlos desde programas con Qt. Qt provee migration solutions for MFC-, Motif- y Xt-based applications, también en la versión comercial.
Por defecto su aplicación es enlazada (linked) contra QtCore y QtGui. Si usted necesita usar alguna otra biblioteca, deberá indicarlo en la variable QT de su archivo .pro, en el cual también puede quitarlas. Por ejemplo, para hacer una aplicación en línea de comandos con acceso a la red y XML:
También puede separarlas por espacio. Lo siguiente incluye todas las bibliotecas de Qt 4.0:
Interfaz multi-lenguaje
QtLinguist es la herramienta para traducir su programa de un idioma a otro. Funciona con dos command-line tools: lupdate y lrelease. El primero extrae strings a partir de su código fuente y genera archivos de tradución .ts si hay una regla en el .pro que lo solicite:
QtLinguist abre esos archivos .ts y permite traducirlos en forma gráfica. Finalmente lrelease toma las traduciones hechas y genera un archivo binario que la aplicación carga cuando es ejecutada. De esta forma, no es necesario recompilar código fuente para agregar una traducción.
Para que una aplicación pueda ser traducida su código fuente debe seguir ciertas convenciones. Los strings que serán traducidos deben ser pasados a QObject::tr() o QApplication::translate(). Estos métodos reciben el string, buscarán su correspondiente traducción y la retornarán; por lo que el usuario podrá ver el mensaje en su idioma de elección. Además lupdate levantará la lista de strings a traducir buscando esas funciones en el código fuente. Para que nuestro hello world sea traducible, debemos hacer un cambio como el siguiente:
QApplication::translate(context, str) recibe dos textos: str es el string a traducir, pero este puede ser el mismo en varios lugares y sus traduciones podrían ser distintas. Pej: "label" puede traducirse como "etiqueta" en un contexto XML y como "rótulo" en una aplicación sobre publicidad. QtLinguist puede encontrar traducciones diferentes de acuerdo al contexto en que está el str. En el caso de QObject::tr(str), el contexto se asume como el nombre de la clase donde se invoca tr().
Si usted quiere que su programa esté en varios idiomas, empiece a usar tr() desde el inicio, ya que es muy tedioso agregarlo en una etapa posterior.
Diálogos
Esta sección construirá un conversor de Decimal/Hexadecimal/Binario. Un número insertado en cualquiera de esos tres campos, se convertirá y actualizará en los otros automáticamente. Ningún widget de Qt tiene esos tres campos, es necesario crear uno combinado. Por simplicidad se adaptará un diálogo.
QDialog está diseñado para transferir información entre un main window y el usuario, no para ser el main window, por eso, cuando un QDialog se cierra, por defecto no solicita al QApplication terminar. Esto se puede cambiar asignándole el atributo Qt::WA_QuitOnClose. El diálogo luce así:
class QLineEdit;
class BaseConverterDialog : public QDialog
{
Q_OBJECT
private:
QLineEdit* decimalEdit;
QLineEdit* hexadecimalEdit;
QLineEdit* binaryEdit;
public:
explicit BaseConverterDialog(QWidget *parent = 0);
};
#endif // BASECONVERTERDIALOG_H
]]>
Se debe usar forward declarations (ej: QLineEdit) siempre que sea posible para optimizar el tiempo de compilación. Todas las clases que hereden de QObject, aunque sea indirectamente, deben tener la macro Q_OBJECT para que el Meta Object Compiler (moc) genere código que hace funcionar los signal/slots y otras características.
Note que Q_OBJECT no se terminó en punto y coma, ya que en algunos compiladores puede generar errores. Si por error se omite esta macro en un descendiente de QObject, ni el compilador ni el linker se quejarán, el signal/slots y otros mecanismos simplemente no funcionarán. Lo más que puede obtener es un warning en runtime en la terminal si corre su programa con debugging information, quejándose de que el signal o el slot no existe, que tristemente es el mismo mensaje si escribe mal el nombre de un signal o un slot o un parámetro de ellos: No such signal/slot...
Todo archivo que tenga la macro Q_OBJECT debe pasar por el moc, el cual no modifica sus archivos, sino que implementa los signal/slots en archivos separados (moc_MyClass), los cuales deben agregarse al Makefile, lo cual es hecho por QMake. QMake rastrea todos los archivos que referencie desde su .pro y en aquellos que encuentre la macro Q_OBJECT, generará reglas en los Makefile para llamar al moc. Por esta razón, se debe referenciar no sólo los .cpp sino también los .h en la variable HEADERS:
Regresando al diálogo, es necesario crear los widgets que lo componen y alinearlos de tal forma que se vean bien incluso aunque se redimensione el dialog. Es por esto que la creación de controles y layouts se hace simultáneamente. Es conveniente que haga un dibujo antes de empezar a programar.
addLayout( editLayout );
mainLayout->addStretch();
mainLayout->addLayout( buttonLayout );
// Create each label and field into the grid
editLayout->addWidget( new QLabel(tr("Decimal")), 0, 0 );
editLayout->addWidget( decimalEdit = new QLineEdit(), 0, 1 );
editLayout->addWidget( new QLabel(tr("Hexadecimal")), 1, 0 );
editLayout->addWidget( hexadecimalEdit = new QLineEdit(), 1, 1 );
editLayout->addWidget( new QLabel(tr("Binary")), 2, 0 );
editLayout->addWidget( binaryEdit = new QLineEdit(), 2, 1 );
// Create the button
QPushButton* quitButton = new QPushButton(tr("Quit"));
buttonLayout->addStretch();
buttonLayout->addWidget(quitButton);
[...]
}
]]>
Note que los widgets se agregan a un layout con addWidget() y un layout a otro layout con addLayout(). El layout contenedor debe estar asociado a un widget para poder recibir a otros widgets; de lo contrario poduciría un un error en tiempo de ejecución. Por eso es buena práctica crear y asociar los layouts al inicio de su constructor.
Una tercera función de los layouts addStrecth() agrega un Stretch, que se encarga de ocupar el espacio no requerido por los widgets. Note que los métodos addWidget() y addLayout() se encargan de establecer la jerarquía de widgets y por ende, no tenemos que preocuparnos de eliminar su memoria, sólo la del BaseConverterDialog que es el padre de todos.
Mejoras: El título del diálogo usa el nombre de la aplicación y debería ser algo más descriptivo y traducible. El Quit button debería reaccionar cuando se presione Enter {¿o Escape?}, es decir, hacerlo el Default button. Evitar que el usuario ingrese texto o números decimales mayores a 255, hexadecimales mayores a 0xFF y binarios de 8 bits. El código siguiente soluciona estos inconvenientes.
setDefault(true);
// Limit input to valid values
decimalEdit->setValidator( new QIntValidator(0, 255, decimalEdit) );
hexadecimalEdit->setValidator(
new QRegExpValidator(QRegExp("[0-9A-Fa-f]{1,2}"), hexadecimalEdit) );
binaryEdit->setValidator( new QRegExpValidator(QRegExp("[01]{1,8}"), binaryEdit) );
setWindowTitle(tr("Base converter"));
]]>
Cada validador es asociado a un text field en dos formas. Primero el validador recibe un parent en su constructor, el cual se encargará de eliminar su memoria automáticamente a través de la jerarquía de QObjects, por lo que los validadores siempre deben construirse en heap. El método setValidator() hace que el text field pida ayuda al validador para saber si aceptar cada carácter recién ingresado por el usuario.
Para que el botón Quit tenga efecto, se conecta con el slot accept() del diálogo, por una convención. Los diálogos generalmente proveen dos botones: Accept y Cancel, conectados a los slots accept() y reject() respectivamente. Ambos cierran el diálogo, el primero retorna un valor positivo y el segundo uno negativo.
Ahora queremos hacer conversiones cada vez que un text field cambia su valor, es decir, conectar la señal textChanged() con qué? No hay ningún slot que haga conversiones de base, es responsabilidad nuestra proveerlos. El autor decide crearlos en el diálogo mismo {Aunque es mejor un controlador, de acuerdo al modelo MVC}. Se declaran en la sección especial {public|protected|private} slots de la clase:
Los slots son métodos normales; se declaran, implementan e invocan como cualquier otro método en cualquier momento. La única diferencia es su declaración en una sección especial y la posibilidad de conectarse con signals. Abajo la implementación de decimalChanged()
setText( ok ? QString::number(num, 16) : QString() );
binaryEdit->setText( ok ? QString::number(num, 2) : QString() );
}
]]>
El parámetro newValue contendrá el texto que ha introducido el usuario en el campo decimalEdit, es decir, un string con el número en decimal que debe convertirse a hexadecimal y binario. Las conversiones entre strings y números ya vienen incorporadas en la clase QString: de string a número (toInt, toLong, toLongLong...), de número a string (number, setNum, sprintf, arg).
La implementación anterior asigna el valor hexadecimal y binario a los otros campos sólo si el valor en el campo decimal representa un valor entero válido. Para obtener strings en esas bases se debe convertir de nuevo el número, se hizo con el método estático number(), cuyo segundo parámetro indica la base en la que se quiere generar el número en el string.
Si el método toInt() de QString no puede convertir a un entero, se limpiarán los otros campos de texto. Gracias al validador que sólo permite valores decimales entre 0 y 255, sólo habrá una situación en que lo anterior pase: cuando el usuario borra el campo decimal completamente. Los otros métodos se implementan igual, cambiando nada más las bases y los los otros text fields:
setText( ok ? QString::number(num, 10) : QString() );
binaryEdit->setText( ok ? QString::number(num, 2) : QString() );
}
]]>
Para que esos slots sean invocados, se deben conectar con las señales textChanged() de cada text field al final del constructor:
Separación de la vista del modelo
En el ejemplo actual, la clase BaseConverterDialog se ocupa tanto de la interfaz gráfica como de la lógica de procesamiento. Esto hace al programa más difícil de mantener. Por ejemplo, si después se quisiera cambiar la interfaz gráfica por una web o hacer un refactoring, es difícil no afectar el código de procesamiento. Lo conveniente es separarlos para reducir este retrabajo, así la clase BaseConverterDialog se encarga únicamente de la interfaz, y una clase nueva, BaseConverter se encarga únicamente de hacer conversiones entre números.
La clase BaseConverter tendrá signals y slots de tal forma que el diálogo instancia un BaseConverter y conecta sus text fields con dichos signals/slots. Por ejemplo, cuando el usuario cambia el texto en el decimal text field, se invoca el slot BaseConverter::setDecimal(), quien hará conversiones y emite dos señales: hexadecimalChanged() y binaryChanged(), los cuales el diálogo asociará con los respectivos text fields. De esta forma, la clase BaseConverter no conoce en absoluto la interfaz, y además podrá reutilizarse en otras circunstancias, quizá programación web.
La clase BaseConverter tiene signals/slots, por ende debe heredar de QObject y todas sus implicaciones, como incluir la macro Q_OBJECT y ser compilada con moc (agregarla a HEADERS y SOURCES en el .pro). Los slots deberán ser invocados desde el diálogo, por lo que se declaran públicos.
Las señales se declaran en la sección signals: y no tienen un modo de acceso, siempre son públicas ya que de otro modo serían inútiles para comunicación entre objetos. Además nunca se implementan por el programador, es el moc quien lo hace en los archivos moc_MyClass.cpp y su implementación consiste simplemente en invocar los métodos que se les haya conectado.
La implementación de los slots es muy similar a las del diálogo: recibir el nuevo valor en un string, cambiarlo de base y en lugar de asignarlo a otro text field, emitir la señal correspondiente, lo cual es una invocación normal pero antecedida por la macro emit de Qt. Ej:
Ahora la nueva clase liberará responsabilidad en el diálogo. Elimine los private slots, tanto su declaración como implementación. Ahora haga las nuevas conexiones en el constructor:
Note que el objeto BaseConverter es creado en heap y no es eliminado explícitamente. La jerarquía de QObject se encarga de ello cuando el diálogo es destruido. Esto libera al programador de esta responsabilidad y además, asegura que el BaseConverter estará disponible para el diálogo mientras éste tenga existencia.