Sei sulla pagina 1di 11

práctica 1

introducción a la programación en c++


estructuras de datos y algoritmos
facultad de informática
curso 2009-2010

Objetivos
Conocer el entorno de programación y familiarizarse con algunos aspectos del lenguaje C++ de cara a poder
realizar con él el resto de prácticas de la asignatura. Crear la clase imagen que utilizaremos en prácticas
posteriores.

1. Compilando un programa C++


Las prácticas se realizan en el entorno Linux que dispone del compilador g++. Si tenemos el fichero
hola.cc siguiente:
1 # include < iostream >
2 using namespace std ;
3 int main () {
4 cout << " Hola mundo !\ n " ;
5 return 0;
6 }
podemos ejecutar el comando:
prompt> g++ -o hola hola.cc
donde la opción -o sirve para indicar el nombre del ejecutable producido. Se recomienda utilizar siempre
las siguientes opciones de compilación:
prompt> g++ -o hola hola.cc -ansi -pedantic -Wall
Entre otras cosas, el compilador g++ tiene extensiones no estándar del lenguaje C++ que vamos a considerar
incorrectas a todos los efectos a lo largo de la asignatura (por ejemplo, reservar un vector dee talla no
conocida en tiempo de compilación).
Otras opciones de compilación permiten modificar el nivel de optimización (para lograr ejecutables más
eficientes se aconseja utilizar -O3, observa que la letra “o” tiene significados distintos en mayúscula y en
minúscula). Ejecuta:
prompt> man g++
o también1
prompt> info g++
para conocer con detalle éstas y otras opciones.
Para que el compilador produzca un ejecutable es imprescindible que esté definida la función main.
También es posible compilar ficheros sin la función main y producir lo que se denominan ficheros objeto2 que
suelen tener extensión .o
Ası́, si tenemos el fichero rectangulo.cc siguiente:
1 # include < iostream >
2 using namespace std ;
3 void rectangulo ( int alto , int ancho , char letra = ’* ’) {
4 for ( int i = 0; i < alto ; i ++) {
5 for ( int j = 0; j < ancho ; j ++)
6 cout << letra ;
7 cout << endl ;
8 }
9 }
1 Lo puedes llamar desde Emacs con M-x info.
2 Esta denominación no tiene nada que ver con la programación orientada a objetos.

1
podemos compilarlo ası́:
prompt> g++ -c rectangulo.cc -Wall -ansi -pedantic
y nos generará el fichero rectangulo.o que podremos enlazar (link ) posteriormente para formar un fichero
ejecutable. Para poder utilizar las funciones de este fichero objeto en nuestros programas basta con decla-
rar las cabeceras de las funciones, no es necesario el cuerpo. De esta manera, podemos tener un fichero
principal.cc siguiente:
1 void rectangulo ( int alto , int ancho , char letra = ’* ’ );
2 int main () {
3 rectangulo (4 ,5);
4 return 0;
5 }
que se podrá compilar con el comando
prompt> g++ -o principal principal.cc rectangulo.o -Wall -ansi -pedantic
para obtener un ejecutable de nombre principal
Normalmente, en lugar de declarar las funciones en el fuente que las vaya a utilizar, se escribe un
fichero de cabecera (header ) que suele tener extensión .h, en tal caso escribirı́amos un fichero denominado3
rectangulo.h que contiene simplemente:
1 void rectangulo ( int alto , int ancho , char letra = ’* ’ );
de manera que ahora la función principal queda:
1 # include " rectangulo . h "
2 int main () {
3 rectangulo (4 ,5);
4 }

2. Paso de parámetros en la lı́nea de comandos


Observa que la función main devuelve un valor int. En el entorno Unix y Linux los programas tienen la
función main definida como en el ejemplo siguiente (fichero muestraargumentos.cc)
1 # include < iostream >
2 using namespace std ;
3 int main ( int argc , char * argv []) {
4 int i ;
5 for ( i = 0; i < argc ; i ++) {
6 cout << " Parámetro " << i << " - esimo es : \" " << argv [ i ] << " \" " << endl ;
7 }
8 return 0;
9 }
Para que el programa pueda acceder a los distintos parámetros que le pasamos al invocarlo. Si ejecutamos
este programa ası́:
prompt> ./muestraargumentos uno dos y tres
aparecerá por pantalla lo siguiente:
Parámetro 0-esimo es: "./muestraparametros"
Parámetro 1-esimo es: "uno"
Parámetro 2-esimo es: "dos"
Parámetro 3-esimo es: "y"
Parámetro 4-esimo es: "tres"
Observa que los argumentos o parámetros son cadenas de caracteres. Si uno de los argumentos corresponde
a un número introducido por el usuario y queremos conocer su valor, debemos utilizar alguna función para
convertirlo de cadena a número, como las funciones atoi, atof, strtod, strtof, strtold, strtol, etc. de
la biblioteca estándar cstdlib, o incluso con un sstream o con la función sscanf. Por ejemplo, el siguiente
programa asume que todos los argumentos recibidos por la lı́nea de comandos son números enteros y devuelve
la suma de los mismos:

3 No es imprescindible, aunque sı́ aconsejable, utilizar el mismo nombre para los .h y los .cc asociados.
1 # include < iostream >
2 # include < cstdlib >
3 using namespace std ;
4 int main ( int argc , char * argv []) {
5 int i , suma =0;
6 for ( i = 1; i < argc ; i ++)
7 suma += atoi ( argv [ i ]);
8 cout << " La suma de los argumentos vale " << suma << endl ;
9 return 0;
10 }
Por ejemplo:
prompt> ./sumaargumentos 10 11 12
La suma de los argumentos vale 33
La función atoi no permite darse cuenta de que la cadena no es un número válido, tal y como muestra el
siguiente ejemplo:
prompt> ./sumaargumentos casa telefono
La suma de los argumentos vale 0
Ejercicio Para detectar estos casos, se recomienda utilizar adecuadamente la función strtol, búscala en
las páginas del manual o en la red y modifica el programa adecuadamente.

3. Entrada y salida con streams


En los ejemplos anteriores hemos visto la manera de mostrar texto por la salida estándar utilizando
los streams. Una alternativa consiste en seguir utilizando las funciones de la biblioteca estándar ISO-C
denominada <stdio.h> que en C++ se conoce también como <cstdio> (las funciones printf, scanf, etc.
ya conocidas por todos).
Para trabajar con streams incluimos la biblioteca <iostream> que nos ofrece, entre otras cosas, tres
objetos de tipo stream: cout, cerr y cin que corresponden, respectivamente, a salida estándar, salida de
error y entrada estándar. Podemos leer de entrada estándar de la manera siguiente:
1 # include < iostream >
2 using namespace std ;
3 int main () {
4 int i , j ;
5 cout << " Dame dos enteros separados por espacios : " ;
6 cin >> i >> j ;
7 cout << " El estado de cin es " << cin . fail () << endl ;
8 cout << " Los enteros son : " << i << " , " << j << endl ;
9 }
Observa que además de mostrar por pantalla los valores introducidos, también mostramos el valor
cin.fail(). Si ejecutas este programa puede resultar interesante ver qué ocurre con este valor cuando
no introduces los números correctamente. Las funciones scanf, fscanf, sscanf tienen la ventaja de retornar
el número de argumentos correctamente leı́dos.

3.1. Leer un fichero


Ya sabemos más o menos leer de entrada estándar. Ahora vamos a ver cómo leer y escribir en ficheros.
Para ello utilizamos la biblioteca <fstream>. Veamos un ejemplo de programa (leecadenas.cc) que espera
como único argumento el nombre de un fichero, lo abre en modo lectura y muestra su contenido cadena a
cadena. Esto nos servirá para ver cómo actúa el operador >> de un stream de entrada cuando ponemos un
vector de caracteres.
Este programa comprueba que el fichero ha sido correctamente abierto y utiliza el manipulador setw(int)
de la biblioteca <iomanip> para ajustar el ancho del campo al mostrar el entero cuenta. Prueba a ejecutar
este programa de la manera siguiente:
g++ -o leecadenas leecadenas.cc
leecadenas
leecadenas uno dos y tres
leecadenas nosoynombrefichero
leecadenas leecadenas.cc | less
1 # include < iostream >
2 # include < iomanip >
3 # include < fstream >
4 using namespace std ;
5 int main ( int argc , char * argv []) {
6 int cuenta ;
7 const int longcadena = 1000;
8 char cadena [ longcadena ];
9 if ( argc != 2) {
10 cerr << " Uso del programa : " << argv [0] << " nombre_de_fichero \ n " ;
11 } else {
12 fstream fich ;
13 fich . open ( argv [1] , ios :: in ); // abrimos en modo lectura
14 if (! fich ) {
15 cerr << " He tenido problemas para abrir el fichero \" "
16 << argv [1] << " \"\ n " ;
17 } else { // mostramos cadena por cadena lo que nos ofrece el fichero
18 cuenta = 1;
19 while ( fich >> cadena ) {
20 cout << " La cadena " << setw (3) << cuenta
21 << " es : \" " << cadena << " \"\ n " ;
22 cuenta ++;
23 }
24 }
25 fich . close ();
26 }
27 return 0;
28 }
Puede resultar de interés leer lı́nea a lı́nea. Para ello utilizamos el método getline que recibe tres
parámetros: un puntero char* al principio de la cadena donde guardar el resultado, el tamaño máximo de
esta cadena y, por último, el caracter que delimita la lı́nea. Este último argumento tiene un valor por defecto
igual a ’\n’, es decir, no hace falta ponerlo cuando lo que queremos es leer lı́neas. Veamos una versión del
programa anterior para leer lı́neas:
1 # include < iostream >
2 # include < iomanip >
3 # include < fstream >
4 using namespace std ;
5 int main ( int argc , char * argv []) {
6 int cuenta ;
7 const int longlinea = 1000;
8 char linea [ longlinea ];
9 if ( argc != 2) {
10 cerr << " Uso del programa : " << argv [0] << " nombre_de_fichero \ n " ;
11 } else {
12 fstream fich ;
13 fich . open ( argv [1] , ios :: in ); // abrimos en modo lectura
14 if (! fich ) {
15 cerr << " He tenido problemas para abrir el fichero \" "
16 << argv [1] << " \"\ n " ;
17 } else { // mostramos linea por linea lo que nos ofrece el fichero
18 cuenta = 1;
19 while ( fich . getline ( linea , longlinea )) {
20 cout << " La lı́nea " << setw (3) << cuenta
21 << " es : \" " << linea << " \"\ n " ;
22 cuenta ++;
23 }
24 }
25 fich . close ();
26 }
27 return 0;
28 }
3.2. Escribir en un fichero
El siguiente ejemplo muestra un programa que abre el fichero hola.txt en modo escritura. Si dicho
fichero no existe, se crea. Si el fichero ya existı́a, se borra su contenido. Una vez abierto, se escribe en él la
cadena ya conocida por todos:
1 # include < fstream >
2 # include < iomanip >
3 using namespace std ;
4 int main () {
5 fstream fsalida ;
6 fsalida . open ( " hola . txt " , ios :: out ); // abrimos en modo escritura
7 fsalida << " Hola mundo !\ n " ;
8 fsalida . close ();
9 return 0;
10 }
Ejercicio Realiza un programa denominado micat que recibe un número indeterminado de parámetros que
interpretará como nombres de fichero.
Si el número de argumentos argc es 1 (el propio nombre del programa) debe finalizar sin más.
Todos los argumentos aparte del propio nombre del programa se refieren a nombres de fichero:
• El primer fichero (argumento argv[1]) es un fichero destino que debes abrir en modo escritura.
• El resto de ficheros son ficheros origen y se deben abrir en modo lectura.
El comportamiento del programa consiste en concatenar los ficheros origen en el fichero destino lı́nea
a lı́nea poniendo el nombre del fichero origen al principio de cada lı́nea tal y como muestra el siguiente
ejemplo:
prompt> cat a.txt
linea uno
linea dos
prompt> cat b.txt
linea uno
linea dos
linea tres
prompt> ./micat c.txt a.txt b.txt
prompt> cat c.txt
a.txt:linea uno
a.txt:linea dos
b.txt:linea uno
b.txt:linea dos
b.txt:linea tres

Otra biblioteca que nos podrı́a ser útil es <sstream> que nos permite utilizar una cadena de caracteres
como un stream. Veamos un ejemplo. Vamos a combinar dos programas anteriores para que, dado un fichero,
nos muestre cada lı́nea y las palabras que contiene:
1 # include < iostream >
2 # include < iomanip >
3 # include < fstream >
4 # include < sstream >
5 using namespace std ;
6 int main ( int argc , char * argv []) {
7 int cuenta ;
8 const int longlinea = 1000;
9 char linea [ longlinea ];
10 const int longcadena = 100;
11 char cadena [ longcadena ];
12 if ( argc != 2) {
13 cerr << " Uso del programa : " << argv [0] << " nombre_de_fichero \ n " ;
14 } else {
15 fstream fich ;
16 fich . open ( argv [1] , ios :: in ); // abrimos en modo lectura
17 if (! fich ) {
18 cerr << " He tenido problemas para abrir el fichero \" "
19 << argv [1] << " \"\ n " ;
20 } else {
21 // mostramos linea por linea lo que nos ofrece el fichero
22 cuenta = 1;
23 while ( fich . getline ( linea , longlinea )) {
24 cout << " Lı́nea " << setw (3) << cuenta
25 << " : \" " << linea << " \"\ n " ;
26 cuenta ++;
27 // ahora mostramos las palabras de lı́nea
28 istringstream fichlinea ( linea ); // creamos un stream cadena
29 int cuenta_cadena = 1;
30 while ( fichlinea >> cadena ) {
31 cout << " Cadena " << cuenta_cadena << " - esima : \" "
32 << cadena << " \"\ n " ;
33 cuenta_cadena ++;
34 }
35 }
36 }
37 fich . close ();
38 }
39 }

4. Trabajando con clases


Antes de aprender a crear nuestras propias clases vamos a ver cómo utilizar una clase ya existente. Para
ello debemos saber cómo se declaran instancias de una clase (denominadas “objetos”) llamando a un de
los métodos constructores (pueden haber varios). Una vez creado un objeto, podemos utilizarlo invocando
métodos sobre él. Para probar estos conceptos os ofrecemos un ejemplo de definición de una clase, denominada
muestra, que nos permite hallar distintos parámetros estadı́sticos asociados a una muestra. Vamos a suponer
que dicha muestra está formada por un conjunto de enteros cuyos valores están entre 0 y un valor máximo.
1 class muestra {
2 public :
3 muestra ( int tamanyo ); // constructor
4 ~ muestra (); // destructor
5 void insertar ( int numero );
6 int moda ();
7 private :
8 int tam ,* vect ;
9 };
10 muestra :: muestra ( int tamanyo ) { // valores a insertar entre 0 y tam -1
11 int i ;
12 tam = tamanyo ;
13 vect = new int [ tamanyo ];
14 for ( i =0; i < tam ; i ++) vect [ i ] = 0;
15 }
16 muestra ::~ muestra () {
17 delete [] vect ;
18 }
19 void muestra :: insertar ( int numero ) {
20 vect [ numero ]++;
21 }
22 int muestra :: moda () {
23 int i , max , imax ;
24 max = vect [0]; imax = 0;
25 for ( i =1; i < tam ; i ++)
26 if ( vect [ i ] > max ) {
27 max = vect [ i ];
28 imax = i ;
29 }
30 return imax ;
31 }
Ejercicio Escribe un programa que, utilizando la clase muestra, lea por la entrada estándar una secuencia
de número enteros en el rango [0, 9999] –hasta fin de fichero– y halle la moda. Para ello has de crear un
objeto de tipo muestra y utilizar los métodos disponibles. Recuerda que para invocar un método se utiliza
la sintaxis:
vobjeto.nombre_método(parámetros); // si es un objeto
pobjeto->nombre_método(parámetros); // si es un puntero a objeto

4.1. Creando una clase, una estructura FIFO


Vamos a ver ahora cómo realizar una estructura de datos tipo cola FIFO con una clase que denominamos
cola_float y que implementaremos utilizando listas simplemente enlazadas. El objetivo del ejercicio es
terminar la implementación de los métodos de la clase cola_float utilizando listas simplemente enlazadas
(con un puntero al último nodo para realizar las inserciones de forma eficiente).
Ejercicio Implementa los métodos de la clase siguiente:
1 struct nodo {
2 float valor ;
3 nodo * sig ;
4 };
5 class cola_float {
6 nodo * primero ,* ultimo ;
7 public :
8 cola_float (); // constructor
9 ~ cola_float (); // destructor
10 void insertar ( float valor );
11 float extraer ();
12 bool vacia () const ; // indica si la cola esta vacia
13 void mostrar ( ostream & str ) const ; // imprime el contenido por el
14 // stream str
15 };
16 cola_float :: cola_float () {
17 primero = ultimo = 0;
18 }
Ejercicio Tras terminar la implementación, deberás comprobar que funciona correctamente creando un
programa que reciba de entrada estándar una serie de lı́neas con los siguientes comandos:
inserta seguido de un número, lo inserta en la cola FIFO,
extrae saca el primer valor de la cola y lo muestra por salida estándar, si la cola está vacı́a da un error,
ver visualiza la cola por salida estándar.

Ejemplo
extrae
> no puedo, la cola esta vacı́a
inserta 3.5
inserta 2.4
ver
>cola[3.5 2.4]
extrae
>3.5
ver
>cola[2.4]
inserta 5.2
inserta 1.1
ver
>cola[1.1 5.2 2.4]
Los valores extraidos se mostrarán por salida estándar. Se debe controlar el caso en que se intenta extraer
de la cola vacı́a. SUGERENCIA! No intentes leer el segundo argumento del comando inserta sin antes
comprobar que efectivamente es una inserción, pues los métodos ver y extrae no reciben argumentos.

5. Sobrecarga de funciones y de operadores


Ahora que tenemos algo más de soltura, vamos a profundizar en alguna otra caracterı́stica del lenguaje
C++.
La sobrecarga de operadores se puede realizar de dos modos distintos. Si el primer operando es un objeto,
podemos sobrecargar el operador en la propia clase. En cualquier caso, podemos definir el operador como una
función, aunque algunos operadores sólo pueden definirse del primer modo (ver transparencias de teorı́a).
Sintácticamente, sobrecargar un operador es como declarar una función poniendo como nombre de la
misma la secuencia formada por la palabra reservada operator y el operador a sobrecargar. Por ejemplo,
vamos a sobrecargar el operador << para insertar elementos en la cola definida anteriormente:
1 cola_float & operator << ( float valor );
y su respectiva implementación:
1 cola_float & cola_float :: operator << ( float valor ) {
2 insertar ( valor );
3 return * this ;
4 }
Observa que el operador devuelve una referencia a un objeto del mismo tipo cola_float. La utilidad de
devolver este valor es que podemos enlazar esta operación de la manera siguiente:
1 cola_float cf ();
2 cf << 0.1 << 0.2 << 0.3;
3 float f1 , f2 , f3 ;
4 cf >> f1 >> f2 >> f3 ;

Ejercicio Se pide sobrecargar los operadores << y >> para las operaciones de introducir y extraer elementos
de la cola. Observa que al extraer datos con el operador >> no hay forma de saber si la cola estaba vacı́a.

5.1. Sobrecarga de los operadores para utilizar streams


Si sobrecargamos el operador << para la clase ostream y un objeto de tipo cola_float podremos hacer
cosas como esta:
1 cout << " La cola es : " << cf << endl ;
Supondremos que la forma de obtener la salida por pantalla es la siguiente: si tenemos una cola en la
que hemos insertado los valores 0,1, 0,2 y 0,3 la salida serı́a:
La cola es: cola[0.3,0.2,0.1]

Ejercicio Se pide sobrecargar el operador << tal y como se ha explicado. Una forma de realizar este ejercicio
consiste en implementar una función que se limite a llamar al método mostrar corrrespondiente de la clase.

6. La clase imagen
A continuación vamos a implementar una clase imagen que te será útil en práticas posteriores. Esta clase
representa una imagen en color (RGB con 256 valores de intensidad para cada componente) y dispondrá de
métodos para cargar y salvar una imagen en formato ppm ascii. Escribe man ppm en la consola (si no
encuentra ppm en las páginas de manual puedes buscarlo en la red) para ver una descripción detallada del
formato. A nosotros nos interesará el formato ppm ascii, aquı́ tienes el ejemplo que pone en el manual:
P3
# feep.ppm
4 4
15
0 0 0 0 0 0 0 0 0 15 0 15
0 0 0 0 15 7 0 0 0 0 0 0
0 0 0 0 0 0 0 15 7 0 0 0
15 0 15 0 0 0 0 0 0 0 0 0
Ejercicio Implementa una clase en c++ denominada ImageColor que permita leer o escribir una imagen
en color en formato pgm ascii basándote en estas cabeceras:
1 # ifndef PIXEL_H
2 # define PIXEL_H
3 struct Color {
4 unsigned char r ,g , b ;
5 Color ( unsigned char r =0 , unsigned char g =0 , unsigned char b =0)
6 : r ( r ) , g ( g ) , b ( b ) {}
7 void to_gray_level () { } // completar !
8 };
9 # endif // PIXEL_H
para definir un pixel y la siguiente para definir la clase ImageColor:
1 # ifndef IMAGE_H
2 # define IMAGE_H
3

4 # include < iostream >


5 # include " pixel . h "
6

7 class ImageColor {
8 public :
9 int getWidth () const { }
10 int getHeight () const { }
11 // coord2index returns a linear index for a pair of ( row , col ) coordinates
12 int coord2index ( int row , int col ) const { }
13

14 Color & operator ()( int row , int col );


15 bool read_ppm ( std :: istream & fich_in );
16 void write_ppm ( std :: ostream & fich_out ) const ;
17

18 ImageColor (): data (0) , width (0) , height (0) {} // null image , 0 x0 , no pixels
19 ImageColor ( int width , int height ); // creates an empty ( black ) image
20 ImageColor ( std :: istream & fich_in ); // reads a ppm image from a file
21 ~ ImageColor ();
22

23 private :
24 Color * data ;
25 int width , height ;
26 };
27

28 # endif // IMAGE_H
El formato pgm ascii es muy fácil de manipular. Tras una primera lı́nea con la palabra clave P3 vienen
3 valores correspondientes a las columnas, filas y valor máximo. El valor máximo suele ser 255 y se refiere al
valor del color blanco (el 0 es el color negro). Valores intermedios corresponden al gris. Tras estos valores,
tenemos, por cada pixel, tripletas con valores entre 0 y el valor máximo, recorriendo la imagen por filas.
Uno de los objetivos de este ejercicio es que implemente el operador de indexación para poder acceder a
los pixels de la imagen como si ésta fuese una matrix:
ImageColor img (10 ,20); // crea una imagen en negro de 10 x20
Obtén una imagen cualquiera, aquı́ tienes una imagen utilizada en muchos artı́culos de tratamiento de
imágenes:
wget http://upload.wikimedia.org/wikipedia/en/2/24/Lenna.png
Para poder leerlo en formato ppm ascii primero hay que convertir el fichero a ese formato, para ello
puedes ejecutar el comando:
convert -compress none Lenna.png Lenna.ppm
o bien:
convert -compress none Lenna.png ppm:-| programaQueLeeImagenDeEntradaEstandar
Ejercicio Escribe un programa que lea una imagen, la pase a a gris y la vuelva a escribir, todo en formato
RGB, para ello basta con transformar cada pixel a tres valores iguales de RGB con el valor de luminosidad
que viene dado por la fórmula: 0.3*r+0.59*g+0.11*b

7. Introducción a la depuración de programas


Cuando nuestro programa no funciona, existen herramientas que nos pueden ayudar a encontrar qué es
lo que hemos hecho mal. Vamos a presentar brevemente dos de ellas:

7.1. Valgrind
Valgrind es una potente herramienta que ejecuta un programa en una CPU emulada y ofrece varias
utilidades de depuración. Su uso más común es el de encontrar errores en el uso de memoria dinámica, como
por ejemplo accesos a bloques de memoria que no pertenecen a nuestro programa o bloques de memoria que
han sido reservados y no liberados. Veamos un ejemplo:
1 // leak . cc - Reserva un array de 100 enteros y no lo libera
2 int main ()
3 {
4 int * p = new int [100];
5

6 return 0;
7 }
A la hora de depurar es conveniente generar una version del ejecutable especı́fica para este fin, utilizando
la opción -g de gcc/g++ y desactivando las optimizaciones. Esto permite a las herramientas disponer de más
información de forma que pueden, por ejemplo, especificar en qué linea del código fuente se ha encontrado
un error. Ası́ pues, compilamos nuestro fichero fuente:
g++ -g -o leak leak.cc
y lo lanzamos con valgrind:
$ valgrind -q --leak-check=full ./leak
==29292==
==29292== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==29292== at 0x4022F14: operator new[](unsigned) (vg_replace_malloc.c:268)
==29292== by 0x8048490: main (leak.cc:4)
La opción -q ejecuta valgrind en modo silencioso. En este modo se omite gran parte de la salida y se
muestran sólo los errores. Podemos ver cómo valgrind detecta que se ha perdido un bloque de memoria, que
fue reservado por un operator new[] en la funcion main(), en la lı́nea 4 de leak.cc
Veamos otro ejemplo: cómo detectar un acceso fuera de un bloque reservado.
1 // overflow . cc - Reserva un vector de 100 enteros , y escribe fuera del mismo
2 int main ()
3 {
4 int * p = new int [100];
5 p [100]=1234; // error tı́pico , el vector va del 0 al 99 ;)
6 delete [] p ;
7

8 return 0;
9 }
Si compilamos y ejecutamos este programa probablemente no obtengamos ningún error de segmenta-
ción, ya que es posible que el espacio inmediatamente después del bloque todavı́a se encuentre dentro del
espacio de direcciones del proceso. No obstante, escribir fuera del bloque es un error ya que podemos estar
sobreescribiendo otros datos del programa. Valgrind también nos puede ayudar a detectar esta clase de
errores:
$ g++ -g -o overflow overflow.cc
$ valgrind -q --leak-check=full ./overflow
==29291== Invalid write of size 4
==29291== at 0x80484CC: main (overflow.cc:5)
==29291== Address 0x42ab1b8 is 0 bytes after a block of size 400 alloc’d
==29291== at 0x4022F14: operator new[](unsigned) (vg_replace_malloc.c:268)
==29291== by 0x80484C0: main (overflow.cc:4)
En este caso vemos que se ha realizado en la linea 5 de overflow.cc una escritura no válida de 4 bytes,
en una dirección que está justo después (0 bytes after) del bloque reservado en un new[] en la lı́nea 4 del
mismo fichero.
En general, cuando algo va mal o cuando creemos que hemos terminado el programa suele ser buena idea
dar una pasada con valgrind para ver si todo va bien :)

7.2. GDB
Para tareas de depuración más complejas podemos recurrir a gdb (GNU Debugger). gdb nos permite
hacer las mismas cosas que cualquier depurador tradicional: ejecución paso a paso, inspección de variables,
breakpoints, watchpoints...
Un ejemplo completo de uso de gdb serı́a demasiado largo para este boletı́n y además es muy fácil
encontrar tutoriales con una sencilla búsqueda. A continuación se incluye, a modo de referencia, una pequeña
lista con las órdenes más habituales:
help Muestra la ayuda.
run Ejecuta el programa.
break Establece un breakpoint. Ejemplos:
break fichero.cc:38 ←− breakpoint al llegar a una lı́nea de código
break main ←− breakpoint al llegar a una función
break *0x80484b5 ←− breakpoint en una dirección de memoria

list Muestra un trozo de código fuente alrededor de la lı́nea que se esté ejecutando en ese momento. También
permite especificar un nombre de función o un número de lı́nea igual que break.
bt (o backtrace) Muestra la pila de registros de activación de funciones.
up/down Seleccionan el registro anterior/siguiente de la pila.
info locals Muestra las variables locales de la función correspondiente al registro de activación actual.
cont Continúa la ejecución de un programa tras un breakpoint.
step Ejecuta una lı́nea de código. Si esta lı́nea contiene una llamada a función saltaremos a la primera lı́nea
de la función llamada.
next Ejecuta una lı́nea de código. Si la lı́nea contiene una llamada a función saltaremos a la lı́nea de después
de la llamada.
print expresión Muestra el valor de una expresión. Esta expresión puede hacer referencia a variables del
programa.
quit Sale de gdb.

Potrebbero piacerti anche