C++ Estándar 
Programación con el Estándar ISO y la Biblioteca de Plantillas (STL)
© Paraninfo Thomson Learning 2001
 

Ejercicios resueltos por capítulos.
Aquí encontrará los ejercicios resueltos por capítulos
(También puede descargarse un fichero comprimido con todos los fuentes de los ejercicios)



PARTE I

1. Introducción

No tiene ejercicios.

2. Conceptos básicos

2.1) Quedaría del siguiente modo:

    ,

    hay otros tipos de comentarios como los de C++:
    empezar un comentario tipo C y ahora lo acabo. */
        / * Que comentario más precioso * /
    / / Este es más precioso todavía.

2.2) Si en un programa no ponemos la función main(), el programa no se podría enlazar. Si ponemos dos funciones main() ocurre lo mismo, el compilador da un error de redefinición de una función.

2.3) El programa puede parecer a primera vista muy sencillo. En primer lugar vamos a leer y escribir una cadena. La primera solución intuitiva:

    #include <iostream> // USAMOS: cin, cout

    void main() {

        char s[20]; // Cadena de hasta 19 caracteres
        cin >> s; // Leer la primera palabra
        cout << endl << s // Escribir en nueva línea la cadena
             << endl; // Y pasar a la línea siguiente
        }

El problema es que esto únicamente nos lee la primera palabra de una cadena (esto se explicará en el capítulo de entrada/salida). Aunque no se comprenda de momento, la solución se encuentra en el fichero EJ02_03.CPP

2.4) Aquí está el programa:

    #include <iostream> // USAMOS: cin, cout

    void main() {
        double d1, d2;
        out << "Introduce dos reales: ";
        cin >> d1 >> d2;
        cout << "La suma es: " << d1 + d2 << endl
    }

2.5) El programa correcto es éste:

    #include <iostream.h> // USAMOS: cout
    void main() {
        cout << "Hola mundo";
    }

2.6) Sí es correcta.

2.7) Este comentario es erróneo. Hemos dicho que los comentarios no se detectan en las cadenas. Pues no es completamente cierto. No se detecta su apertura pero sí su clausura. Por ello, las sentencias se convertirían en:

    ";
    */

La solución sería definir de nuevo las sentencias como:

    /*
    cout << "/* Me pillaste *""/"; // Concatenación de cadenas
    */

3. Tipos de datos

3.1) La función es:

    int Convierte(char c) {
        return int(c - '0');
    }
        // Usando int() en vez de (int) se ahorra un par de paréntesis

3.2) Sí que es válido ya que en C++ todos los tipos integrales son compatibles. Aunque sería mucho mejor explicitar las conversiones:

    b= (byte)w;
    w= (word)l;
        d= (dword)w;

3.3) Su longitud es 9 tomando sizeof(int) == 2.

3.4) La primera dará error ya que 8 no es un dígito octal y por tanto 08 es un error. La segunda dará 24 porque 014 está en octal que es 12 en decimal.

4. Control de Flujo

4.1) El listado es funcionalmente correcto pero sintácticamente no. Faltan los puntos y comas de las cuatro sentencias de asignación.

    if (a < b)
        if (a < c)
                            min= a;
        else
              min= c;
    else
        if (b > c)
            min= c;
        else
            min= b;

4.2) Programa que cuenta el número de ocurrencias en una cadena de las 5 vocales en 5 variables diferentes: a, e, i, o, u. Usaremos la función Leer_Cadena() del ejercicio 2.3. El programa está en EJ04_02.CPP

4.3) Una función que calcule el M.C.D. de dos números:

    int Mcd(int a, int b) {
        if (a <= 0 || b <= 0)
            return -1; // Código de error
        while (a != b)
            if (a < b)
                b= b - a;
                // b-= a; // Igual que la anterior. Ya se verá
            else
                a= a - b;
                // a-= b; // Igual que la anterior
        return a; // Al final el mcd está en a y en b (a == b)
    }

Un ejemplo de uso de la función está en EJ04_03.CPP

4.4) Función que compara dos cadenas:

    int StrCmp(char *s1, char *s2) {
        int i= 0;
        while (s1[i] || s2[i]) { // Hasta terminar las dos
            if (s1[i] < s2[i])
                return -1; // La cadena 1 es menor que la 2
            else if (s1[i] > s2[i])
                return 1; // La cadena 1 es mayor que la 2
            i++;
        }
        return 0; // Las cadenas son iguales
    }

Esta función es similar a strcmp() de la librería estándar <cstring> del C++.
Un ejemplo de uso de la función está en EJ04_04.CPP

4.5) Sólo la b) es correcta. Recordemos que la sentencia:

    for(a; b; c);

se convierte en:

    {
     a;
     while(b) {
       d;
       c;
     }
    }

Por tanto, las sentencias anteriores son:

    a) int i= 0, int j= 0; // Incorrecto
                           // Dos declaraciones separadas por una coma
       while ..

    b) int i= 0, j= 0; // Correcto
                       // Dos declaraciones de tipo entero
       while ..

    c) int i= 0, long j= 0; // Incorrecto
       while ..

    d) (int i = 0), (long j = 0) // Incorrecto
                                 // Lástima porque era una buena idea

4.6) La solución se encuentra en el fichero EJ04_06.CPP

4.7) En este ejercicio se ha pretendido aumentar la atención del lector en este error común y sutil pero difícil de detectar. La condición del bucle está formada por el operador de asignación (=) y no el operador de comparación (==), con lo que el resultado del programa es que sólo muestra un 0, ya que el resultado de la asignación i=10, además de asignar 10 a la variable i, es que devuelve el valor 10, que es un valor cierto, al ser no nulo. Si después lo negamos con el operador ! obtenemos falso, con lo que el bucle sale después de la primera iteración.

4.8) La solución se encuentra en el fichero EJ04_08.CPP

4.9) La solución se encuentra en el fichero EJ04_09.CPP

4.10) La solución se encuentra en el fichero EJ04_10.CPP

4.11) La solución se encuentra en el fichero EJ04_11.CPP

5. Operadores

5.1) Es simple:

    x & (x - 1)

5.3) Se supone que tenemos dos valores enteros almacenados en dos variables reales. Yo lo haría así:

    float Resto(float a, float b) {
        return float((long)a % (long)b);
    }

5.4) NO ES VÁLIDO PORQUE EL OPERADOR COMA NO SE PUEDE UTILIZAR EN ESE PARTE DEL FOR. Si cogemos uno de los dos incementos y lo ponemos al final del bucle sí que funciona. En este caso invierte el vector de caracteres s (no es una cadena porque no acaba en '\0'). El resultado en s será ACABATOR.

5.5) Con algo parecido a esto sería suficiente para que pareciera aleatorio. Si además hacemos coincidir la llamada a Rand() con un factor externo (tiempo, preferiblemente), esta función es casi impredecible. El programa se encuentra en EJ05_05.CPP

6. Funciones

6.1) La solución se encuentra en el fichero EJ06_01.CPP

6.2) La solución se encuentra en el fichero EJ06_02.CPP

6.3) Es sintácticamente correcto. El compilador crea variables temporales para almacenar estas constantes y así ya puede tomar la dirección. De todas formas no es un buen estilo de programación pasar constantes por referencia porque aunque la función modifique su valor no nos podemos dar cuenta.

6.4) La llamada f(25) es ambigua. El compilador no sabe si llamar a la función con un argumento o llamar a la segunda usando parámetros por defecto. La llamada f(17, 42) es completamente correcta ya que no hay ambigüedad.

6.5) Sí que es correcto y sería equivalente a:

    void f(int a= 1, int b= 2, int c= 3) {
            // ..
    }

6.6) La solución se encuentra en el fichero EJ06_06.CPP

7. Variables

7.1) Las variables estáticas se inicializan a 0. Las variables automáticas no. Por tanto a valdrá 0 y b tendrá un valor indefinido dependiendo del compilador. No se recomienda usar la declaración de 'a' de ese modo. Es mejor explicitar:

    int a= 0;

7.2) Este sería un programa que volvería loco al propio Bjarne Stroustrup:

    void Funcion(float f); // Decl1. Campo prototipo
    float f;
            // Decl 2. Campo global. Se almacena en el seg. de datos. f vale 0
    void Funcion(float f) {
            // Decl. 2. Campo local automático. Se almacena en pila
        float f;
            // Error: parámetros y var. locales tienen el mismo campo

        auto float a; // Este auto es opcional
            // Decl.3.Campo local automático. Se almacena en pila. a vale ?

        static float f;
            // Error: mismo campo.

        static float s;
            // Decl.4.Campo local estático. Se almac. en el s. de datos. s vale 0
        {
            float f;
                // Decl. 5. Campo de bloque. Se almacena en la pila
            f= 2; // Accedo a la 'f' de la decl. 5
            ::f= 3; // Accedo a la 'f' de la decl. 1
            s= 4; // Accedo a la 's' de la decl. 4
            a= 5.5; // Accedo a la 'a' de la decl. 3
            // No hay forma de acceder al parámetro 'f' de la función (Decl. 2)
        }
    }

    float f; // Error! Redefinimos la variable global.

7.3) Como hemos visto en el caso anterior, no es correcto ya que los dos tienen el mismo campo local automático.

7.4) Dará un error en la definición const int a ya que las constantes se deben inicializar en el momento de la definición. Las otras dos también darían error.

7.5) No sería equivalente a:

    const char * Var;

sino a:

    char * const Var;

porque typedef no es una macro.

7.6) El programa A funciona correctamente. El programa B da error porque no sabemos cómo es la estructura, por tanto, no podemos definir una variable de ella. El programa C funcionaría si no se tratara de una estructura. Ya se vio que extern sólo es aplicable a variables de tipos no compuestos.

7.7) Este sería un programa que volvería loco al propio Bjarne Stroustrup:

    void Funcion(float f); // Decl1. Campo prototipo

    float f;
            // Decl 2. Campo global. Se almacena en el seg. de datos. f vale 0

    void Funcion(float f) {
           // Decl. 2. Campo local automático. Se almacena en pila
        float f;
                // Error: parámetros y var. locales tienen el mismo campo
        auto float a; // Este auto es opcional
                // Decl.3.Campo local automático. Se almacena en pila. a vale ?
        static float f;
                // Error: mismo campo.
        static float s;
                // Decl.4.Campo local estático.Se almac. en el s. de datos. s vale 0
        {
            float f;
                // Decl. 5. Campo de bloque. Se almacena en la pila
            f= 2; // Accedo a la 'f' de la decl. 5
            ::f= 3; // Accedo a la 'f' de la decl. 1
            s= 4; // Accedo a la 's' de la decl. 4
            a= 5.5; // Accedo a la 'a' de la decl. 3
            // No hay forma de acceder al parámetro 'f' de la función (Decl. 2)
        }
    }

    float f; // Error! Redefinimos la variable global.

8. Sobrecarga y conversiones

8.1) En C++, las constantes tienen tipo por lo que el compilador asignará:

- la primera es un int . Coincidencia exacta con Print(int ).

- la segunda es un double. No hay coincidencia exacta ni trivial. No hay promoción. Hay conversión estándar. Pero las conversiones estándar de un tipo aritmético puede ser a cualquier otro tipo aritmético. Por tanto, fallará porque hay una ambigüedad. Podríamos haberlo solventado poniendo 2.2F.

- la tercera es un char. No hay coincidencia exacta ni trivial. Pero hay promoción con int; por tanto, se llama a Print(int ).

En general, las posibles soluciones a los problemas que aparecen (como el de la segunda llamada) son:

a) Declarar variables auxiliares del tipo que se desee.

b) Forzar que las constantes sean del tipo requerido.

c) Utilizar conversiones explícitas (cast)

8.2) Sí, no hay coincidencia exacta o trivial, no hay promociones, pero hay conversión estándar aritmética. Por tanto, se llama sin ningún problema.

8.3) No porque tomará f() como float y no como una función. Concretamente, dará un error de llamada a no-función ("call of non-function") ya que estamos intentando llamar a un float como si fuera una función.

8.4) La solución se encuentra en el fichero EJ08_04.CPP

8.5) La solución se encuentra en el fichero EJ08_05.CPP

8.6) Daría error al haber conversión trivial entre const T y T.

8.7) Como no hay conversión trivial, se podría definir perfectamente.

8.8) Las dos primeras llamadas invocan a sus funciones correspondientes sin ningún problema. La tercera sigue estos pasos: Primero: no hay coincidencia exacta. Segundo: no hay promoción. Tercero: conversión estándar, pero la hay a los dos, no le damos preferencia a la que no tiene signo. Por tanto daría error de ambigüedad.

8.9) La solución se encuentra en el fichero EJ08_09.CPP

8.10) La solución se encuentra en el fichero EJ08_10.CPP

8.11) Son correctas. Se trata de conversiones de línea.

8.12) En C++, typedef no crea tipos nuevos distintos, sólo les da un nombre diferente.

8.13) Para las cinco llamadas, el proceso es bien diferente:

1.- El literal 0.0 es un double. Pasos: Primero: coincidencia exacta. Por tanto se llama a f(double ).

2.- El literal 0 es un int. Pasos: Primero: no hay coincidencia exacta. Segundo: no hay promoción posible. Tercero: hay conversión estándar de int a char y de int a double. Además, dijimos que la constante 0 tiene conversión estándar con cualquier puntero. Por tanto habrá error de ambigüedad al no poder elegir ninguna de las tres funciones.

3.- El literal 0F da error de sintaxis, ya que F sólo se puede aplicar a constantes reales.

4.- El literal 0.0F es un float. Pasos: Primero: no hay coincidencia exacta. Segundo: hay promoción de float a double. Por tanto se llama a f(double ).

5.- El literal cadena es un char *. Pasos: Primero: no hay coincidencia exacta. Segundo: no hay promoción. Tercero: hay conversión estándar entre char * y void *. Por tanto se llama a f(void *).
 

8.14) Para la primera combinación, la segunda llamda es correcta (mismo tipo), pero la primera no, porque no hay conversión estándar desde int a enum. Como si está permitido lo contrario, la combinación dos es perfectamente correcta. La combinación tercera también lo es, llamando cada una a su correspondiente función.

8.15) El compilador da un error de ambigüedad, ya que no sabe si llamar a ff(fc) sin signo o ff(fc) con signo. ¡Qué complicados son los complicadores!

9. Punteros

9.1) El primero carga en p la dirección de la variable a (p= &a), pero al cerrarse el bloque la variable a se destruye con lo que el acceso posterior de (*p= 10) puede ser catastrófico.

El segundo programa, en cambio, funciona correctamente ya que el carácter a tratar se almacena en el 'heap' y no en la pila, así al cerrar el bloque no destruimos ninguna variable ya que no hemos definido ninguna tampoco. El acceso (*p= 10) será válido hasta que pongamos (delete p;).

Una mejor solución sería:

    void main() {
        char *p;
        int a;
        {
            p= &a;
        }
        *p= 10;
    }

9.2) Invierte una cadena. La solución se encuentra en el fichero EJ09_02.CPP

9.3) La solución se encuentra en el fichero EJ09_03.CPP

9.4) La solución se encuentra en el fichero EJ09_04.CPP

9.5) La solución se encuentra en el fichero EJ09_05.CPP

9.6) Ese programa es muy peligroso. Leemos una cadena en s, pero s apunta a una dirección indefinida; por ello, podemos estar estropeando código, datos de nuestro o de otro programa. Además no se puede asegurar que la salida sea igual que la entrada. En fin, que este es uno de los errores más graves y típicos del C++. Además, puede que en un primer momento funcione. Más tarde el error aparecerá inesperadamente de forma catastrófica. La solución es reservar la memoria que vamos a usar:

    #include <iostream.h>

    void main() {
        char s[100];
        // Suponemos que con 100 caracteres es suficiente
        cin >> s;
        cout << s;
    }

También podríamos haber usado:

    #include <iostream.h>

    void main() {
        char *s;
        s= new int[100];
        cin >> s;
        cout << s;
        delete []s;
    }

9.7) No ocurre nada, al final del programa el compilador se encarga de hacer todos los delete que falten. De todas formas, es muy recomendable no olvidarse de ponerlo porque si es en una función que se llama 1000 veces acabaremos con el 'heap' lleno!. Tampoco es muy recomendable hacer lo que se ha hecho en el ejercicio 1, pero a veces como en ese ejercicio, es necesario.

9.8) Para hacer lo que se nos pide en el ejercicio habría que hacer uso de punteros:

    float f;
    int *pi= (int *)&f;
    char *pc= (char *)&f;

Y con f, *pi, *pc accederíamos a lo mismo que con la unión: f, i, c. Claramente, usar una unión anónima es más limpio aunque con punteros se ve físicamente que comparten la misma memoria. En este caso, trabajar con punteros puede ser peligroso, ya que si tenemos:

    char c;
    int *pi= (int *)&c;
    float *pf= (float *)&c;

un acceso a (*pi) a (*pf) excedería del tamaño del carácter, estropeando lo que hay después en memoria, que en este caso es el puntero que accede. Aquí, se puede decir, que está casi asegurado que el sistema se quede bloqueado o lo que en el argot se conoce como "colgado".

9.9) El programa compara los punteros, no donde apuntan. Si lo sustituyéramos por(*s == *t) tampoco ya que sólo compararía el primer elemento. Queda como ejercicio hacer una función que compare cadenas. En el siguiente capítulo también se verán algunas funciones de comparación.

9.10) No es correcto porque hemos definido p como un puntero a enteros constantes sobre los cuales nos podemos hacer un delete. Además, delete p sólo borraría el primer elemento, en el caso de que no fuera const.

9.11) Los dos son, obviamente, equivalentes y ninguno de ellos da error. El puntero retornado en p es indefinido y la dirección a la que apunte no está reservada. No retorna NULL como podríamos imaginar en un principio, del mismo modo que delete no modifica el puntero, sino simplemente libera la memoria.

9.12) Intentar borrar sólo una parte del vector reservado es una barbaridad, no porque sea ilógico pensarlo, sino porque el C++ no lo detecta como error y dependiendo de la implementación, puede ser que no ocurra nada o se convierta en un desastre. Lo único que sabemos con seguridad es que si hacemos lo correcto, no tendremos ningún problema.

10. Eficiencia y Optimización

10.1) La solución se encuentra en el fichero EJ10_01.CPP

10.2) La solución se encuentra en el fichero EJ10_02.CPP

10.3) Tenemos un tipo reloj y tres funciones:

    reloj Start_Timer();
            // Crea un reloj y lo pone en marcha
    double Get_Timer(reloj &);
            // Retorna el tiempo en segundos desde que se creo este reloj
    double Stop_Timer(reloj &);
           // Igual que Get_Timer() pero además destruye el reloj

Además se ha implementado una función de retardo Delay(unsigned long ) que tarda tantos milisegundos como se le pasen en su parámetro. Además tenemos una función Calibrar(int ) para calibrar durante unos segundos la función Delay(). El listado de la implementación es EJ10_03.CPP

10.4) Usando las funciones necesarias del ejercicio anterior veamos EJ10_04.CPP. En muchas máquinas saldrá Fact3() la más rápida y Fact2() la más lenta, completamente al contrario de lo que podríamos pensar en un principio. Esto depende de cómo estén orientados los procesadores, si tienen antememorias (cachés), si son máquinas RISC o CISC, etc.

10.5) Se prueba en el siguiente ejercicio.

10.6) La solución se encuentra en el fichero EJ10_06.CPP

10.7) La función al ser inline haría que cualquier aparición de alias(i) fuera equivalente en sentido y eficiencia a i.

10.8) La solución se encuentra en el fichero EJ10_08.CPP

10.9) Una forma de implementar el algoritmo quicksort() está en EJ10_09.CPP

10.10) La multiplicación por potencias de dos se puede realizar por medio de desplazamientos de bit. Ejemplos para 2, 4 y 8 serían:

    inline
    int Mult2(int a) {
        return a << 1;
    }

    inline
    int Mult4(int a) {
        return a << 2;
    }

    inline
    int Mult8(int a) {
        return a << 3
    }

que por las pruebas que se han realizado son ligeramente más rápidas que la multiplicación normal. Esto depende mucho de la máquina.
Las funciones para la divisiones son similares pero utilizando el operador >>.

10.11) Se trataría de lo siguiente:

    inline
    int Mult3(int a) {
        return Mult2(a) + a;
    }

    inline
    int Mult5(int a) {
        return Mult4(a) + a;
    }

    inline
    int Mult6(int a) {
        return Mult4(a) + Mult2(a);
    }

    inline
    int Mult7(int a) {
        return Mult(6) + a;
    }

    inline
    int Mult9(int a) {
        return Mult8(a) + a;
    }

Según vamos aumentando iremos perdiendo en eficiencia. La reutilización de unas funciones en otras no ralentiza ya que son inline. En general:

    int Mult(int a, int b) {
        int r= 0;
        while (b) {
            if ((b | 1) == b) // bit es 1
                r+= a;
            b >>= 1;
            a <<= 1;
        }
        return r;
    }

que ya no es eficiente.
Esta solución y medidas de tiempo se encuentran en el fichero EJ10_11.CPP

10.12) Es correcto ya que en los macros los comentarios no son expandidos. Esto se verá mejor cuando se vea preprocesamiento.

10.13) El algoritmo se encuentra en EJ10_13.CPP.

10.14) La solución se encuentra en el fichero EJ10_14.CPP
 

PARTE II

11. Clases

11.1) Al hacer delete this estamos liberando la memoria que ocupa el objeto actual por lo que el siguiente this= Ultimo ya no es válido porque el Ultimo puede haber perdido su valor. Para la gente que empieza puede quedar más claro poniendo:

    delete this;
    this= this->Ultimo;

que es lo mismo que antes pero ahora se ve que el puntero this al que hemos hecho un delete, lo utilizamos como fuente en la siguiente sentencia. Por ello, se suele utilizar el puntero this para acceder a los atributos de una clase cuando queda comprometida la claridad.

En segundo lugar como this es un puntero constante ni se puede hacer un delete sobre él ni se puede poner como destino en una asignación.

11.2) El programa es completamente correcto. El primer objeto Obj1 llama al Pon de la clase c1 con el parámetro 3. Por tanto Obj1.Valor se pone a 3. El segundo objeto Obj2 llama al Pon de la clase c2 sin parámetros por lo que se toma el parámetro 1 por defecto que es lo que se almacena en Obj2.Valor. Al hacer Obj3.Valor= 10 no modificamos ningún otro objeto ni de c1 y mucho menos de c2 que no tiene nada que ver.

11.3) Las dos son inline.

11.5) La solución se encuentra en el fichero EJ11_05.CPP

11.6) La solución se encuentra en el fichero EJ11_06.CPP

11.7) La solución se encuentra en el fichero EJ11_07.CPP

11.9) En primer lugar no funcionaría porque hemos definido los métodos privados. Solventando este problema no funcionaría tampoco porque cuando se llama a una función inline debe tener su implementación ya definida. En este caso la solución sería cambiar de orden f1() y f2().

    class clase {
        ..
    public:
        void f1();
        void f2();
            ..
    };

    inline void clase::f2() {
        ..
    }

    void clase::f1() {
        ..
        f2();
        ..
    }

    void main() {
        clase o;
        o.f1();
    }

Una curiosa solución es poner las dos funciones inline, así las funciones no son evaluadas hasta que se expanden, que en este caso ocurrirá cuando lleguemos a main(), pasadas ya las definiciones de f1() y f2(). Otra solución, evidentemente, es no definir ninguna inline.

11.10) La solución se encuentra en el fichero EJ11_10.CPP

11.11) Si hacemos la implementación de los complejos en forma polar, no quita para que definamos exactamente los mismos métodos, incluso los constructores. Por tanto, si sólo viésemos las declaraciones de los métodos, no podemos saber si están implementados en forma rectangular o en forma polar.

11.12) La primera sentencia modifica el parámetro c1. La segunda modifica el miembro c2. La tercera modifica la variable global c3. La cuarta sentencia, modifica el miembro c1 al usar this. La quinta sentencia también al utilizar el operador de campo de clase.

11.13) No se puede. Deberemos hacer:

    class clase {
        static int Estatuto;
        ..
    };
    int clase::Estatuto= 23;

o definir un método estático para acceder a Estatuto. De todas formas la sentencia:

    int clase::Estatuto;

se debe seguir poniendo.

11.14) Perfectamente. Aunque no tiene mucho sentido.

11.15) No podemos acceder a a porque f() es una función estática y por tanto no tenemos el parámetro implícito this para poder acceder a los miembros.

11.16) La solución se encuentra en el fichero EJ11_16.CPP

12. Creación de objetos

12.1) Los métodos son:

    a) constructor normal
    b) constructor por defecto
    c) exactamente igual a lo anterior
    d) constructor copia
    e) constructor por defecto (todos los argumentos por defecto) y constructor de conversión de (int *) a c1
    f) constructor de conversión de float a cl si se toma el último argumento por defecto, si no, constructor normal
    g) operador suma
    h) operador de conversión de cl a int. No se pone retorno
    i) operador de asignación
    j) destructor
    k) Error! Los destructores no tienen parámetros

12.2) No es correcta ya que punto(c.RE(), c.IM()) crea un objeto temporal en el cuerpo de la función punto(complejo c) y no modifica 'x' e 'y'. La solución sería:

    class punto {
    private:
        double x, y;
    public:
        void Pon(double xx, double yy) {
            x= xx; y= yy;
        }
        punto(double xx, double yy) {
            Pon(xx, yy);
        }
        punto(const complejo & c) {
            Pon(c.RE(), c.IM());
        }
    };

12.3) Tiene dos problemas, el primero es que como no hemos puesto ningún modificador de acceso y se trata de 'class', el método f() será privado con lo que no lo podremos llamar. En segundo lugar, no podemos llamar a funciones no constantes desde objetos constantes. En este caso, esta última restricción es una garantía de seguridad de que el objeto si es constante no va a ser modificado.

12.4) Dependerá del orden en que están definidos dentro de una clase. Lo único que sabemos con seguridad es que x pasará a valer a antes de que y pase a valer b. Esto es debido a que los dos se construyen en la lista de inicialización, como 'x' si que está en la lista se construye y toma su valor a la vez mientras que 'y' tiene que esperar a valer 'b' al cuerpo de la función.

12.5) Para la primera no, ya que al haber otro constructor, ya no está definido el constructor por defecto. En la segunda sí será posible (en la versión 2.0 no). Las dos últimas son perfectamente válidas.

12.6) Se creará un objeto temporal por lo que el objeto constante no puede ser modificado por mucha referencia de que se trate.

12.8) Ver EJ12_08.CPP

12.10) Sí se pueden definir miembros (atributos y métodos) volatile.

12.11) El operador sizeof no puede sobrecargarse.

12.12) En primer lugar, falta el punto y coma final de la clase. En segundo lugar, nunca deberemos hacer una asignación a this y mucho menos un delete.

12.13) Es correcto aunque es más recomendable definir la unión dentro de una clase.

12.14) Genera ambigüedad. No sabemos si llamar a

    f(A(Objeto));

o a:

    f(Objeto.operator A ());

12.15) No funcionaría porque en C++ no se buscan conversiones multinivel. Y no hay ninguna conversión en un solo paso para hacer coincidir los parámetros.

12.16) Al llamarse a la función Nada() que parece que no hace nada, se crea un objeto temporal usando el constructor copia. El constructor copia que tenemos definido sólo copia los atributos. Al llegar al final del cuerpo de la función (en seguida porque no hace nada), se retornaría llamando al destructor del objeto temporal, que de la forma que tenemos definida la cadena haría un delete s; liberando la memoria. Cuando hiciéramos cout << c1; probablemente salga la cadena por pantalla, pero no se puede asegurar, ya que hemos liberado la memoria que ocupa y puede ser utilizada por cualquier otro. Lo peor no es esto, sino que al llegar al final de la función se destruiría c1 volviendo a llamar a delete s que ya está borrado. Esto lo suele avisar el compilador por medio de un error al final del programa del tipo "Null pointer assignment"

12.17) La solución se encuentra en el fichero EJ12_17.CPP

13. Herencia y Polimorfismo

13.1) La asignación e) es incorrecta ya que no podemos asignar un clase derivada con una clase base. La asignación f) nos muestra que esto es imposible incluso utilizando casts. La asignación h) es incorrecta por el mismo motivo que la e). Pero la i) es correcta porque siempre podemos pasar de un puntero de un tipo a un puntero de otro tipo utilizando casts.

13.2) No, no tiene sentido heredar dos veces ya se virtual o no virtual. Si se quiere incluir dos veces una clase se hace precisamente eso, incluir (composición).

13.3) La primera crea un objeto dinámico de la clase cuadrado y toma su dirección en c1 que es un puntero a cuadrilatero. La segunda sentencia es incorrecta ya que no se puede asignar un puntero a un cuadrilatero a un puntero a un cuadrado. En el segundo lugar podríamos usar un cast pero si los métodos no son virtuales puede ser peligroso.

    cuadrado *c2= (cuadrado *)new cuadrilatero;

13.4) La longitud es 4 + 4 + 4 = 12 suponiendo 4 la longitud de int, 4 la longitud de float y 4 la longitud del puntero a la tabla de métodos virtuales (suponiendo punteros de 32 bits).

13.5) Las funciones f() darán error ya que tienen los mismos parámetros y distinto tipo de retorno. En cambio las funciones g() son funciones totalmente diferentes ya que tienen parámetros distintos. Por tanto B heredará g(int, double) y tendrá además g(double, int).

13.6) Son los dos virtuales ya que si definimos un destructor como virtual en una clase base, los destructores en las clases heredadas también serán virtuales. En estos casos se recomienda poner la palabra virtual para dejarlo más claro. Se deja como ejercicio averiguar si teniendo dos clases A y B, una con destructor virtual y la otra normal, si heredamos las dos en una clase C, ¿el destructor de C será virtual?

13.7) La solución se encuentra en el fichero EJ13_07.CPP

14. Plantillas

14.1) Sí que podemos compilar ese programa, pero en el momento que usemos la función f() dará error. La solución es simplemente borrar la primera declaración ya que la segunda la incluye.

14.2) Porque el tipo A no está incluido en los parámetros de la función. Ya sabemos que el retorno no cuenta.

14.3) Ver EJ14_03.CPP. Se han definido algunos métodos internos y otros externos para mostrar el acceso a vector. Sería preferible todos externos.

14.4) Que no se puede hacer coincidir (C *) con (int). Si hubiera sido (C) no habría problema, C valdría int.

14.5) La solución vale para cualquier tipo:

    template <class T>
    int SizeOf_EnBits(T v) {
        return sizeof(T) * 8;
        //return sizeof v * 8; // También válido
    }

12.6) No, pero la solución es simple:

    typedef clase1<int> clase1_int;
    clase2 <clase1_int> a;

15. Errores y Excepciones

16. Modularidad

16.1) Porque ya vienen provistas del método de protección contra redefiniciones que hemos explicado.

16.2) Funcionaría muy mal ya que no hemos definido el constructor copia y el operador de asignación. Al tratarse de una estructura dinámica, cada vez que llamemos implícitamente al constructor copia (por ejemplo con objetos temporales), deberíamos copiar toda la estructura y sólo copiamos la dirección. Pero cuando destruimos los objetos temporales, sí que destruimos toda la estructura. En resumidas cuentas, que vamos a destruir más veces que a construir.

16.3) Aquí viene la solución al ejercicio anterior. Se compone de tres ficheros EJ16_03.H, EJ16_03.H1, EJ16_03.H2. Además tenemos un fichero EJ16_03.CPP que nos lo prueba todo.

El fichero EJ16_03.H como vemos, queda limpio de toda implementación. En primer lugar incluye EJ16_03.H1 en la parte privada de la clase y después incluye fuera a EJ16_03.H2. Como se observa lo único que ve el usuario son los métodos de la lista. No se puede saber si está implementada dinámica o estáticamente, no se sabe nada de sus miembros privados, ni estáticos, sólo lo imprescindible que debe conocer el que usa esta clase. La parte privada de la clase (los atributos) está en el fichero EJ16_03.H1.

Aquí se definen los miembros privados. Es de resaltar la presencia interna de nodo. Se podría haber definido como clase amiga de clista, pero como en este caso sólo la utilizamos aquí, la incluimos dentro del campo de la función. Pasemos ahora a la implementación de los métodos. Están en el fichero EJ16_03.H2.

El último método (Alias) se suele incluir para hacer referencias. Esto sirve para que tengamos varias listas operando sobre los mismos datos. Esto suele ser peligroso por los objetos temporales y porque la destrucción de uno implica que se ha liberado el espacio al que apuntan todos. Por eso no lo vamos a usar. Por último, hay un fichero que lo prueba todo; este fichero sólo debe incluir la especificación. Es el fichero EJ16_03.CPP.

COMENTARIOS: Los operadores de postincremento y postdecremento retornan por valor. Así, operaciones como la siguiente, estarían permitidas pero no funcionarían de manera correcta:

    ++l1++;

Sólo incrementaría una vez l1, el otro operador actuaría sobre un objeto temporal retornado por el primero.

En resumen, este artificio de estructura modular es un poco largo de realizar y difícil de entender para el que lo desarrolla. Pero nadie puede dudar que el fichero EJ16_03.H está claro como el agua.

16.5) Sí que compilaría y enlazaría. Al tener los dos el atributo const tienen acceso privado al módulo, por lo que no son visibles externamente y no habría conflicto entre ellas. De todas formas, sería mucho más conveniente, poner una sola declaración en una cabecera e incluirla en los dos módulos.
 
 

PARTE III

17. Introducción a las Librerías Estándar

No tiene ejercicios.

18. Entrada y salida

18.1) La solución se encuentra en el fichero EJ18_01.CPP

18.2) En cada caso saldría:

    a) 00103
    b) 10 // Todavía no había hecho efecto
    c) a

18.3) El programa debe incluir algunos manipuladores y flags para que no se ignoren los caracteres blancos.

    #include <iostream.h>
    #include <iomanip.h>

    void main() {
        cin >> resetiosflags(ios::skipws);
        while (1) {
            char c;
            cin >> c;
            if (!cin)
                break;
            cout << c;
        }
    }

También se podía haber hecho así:

    while (1) {
        char c;
        cin.get(c);
        if (cin.eof())
            break; // Fin copia
        cout.put(c);
    }

que no utiliza manipuladores.

18.4) No ya que el setw(100) actúa sobre un stream (cin) y el setw(5) actúa sobre otro (cout). Además, la salida sería:

    00069

18.5) No ya que ya está sobrecargado en <iostream.h> en las clases istream y ostream.

18.6) Tenemos aquí un programa que produce un volcado hexadecimal de un fichero de entrada a otro de salida. Si se omiten estos ficheros se cogerán por defecto la entrada y salida por pantalla estándar. Ver EJ18_06.CPP

18.7) La solución es:

    int i= 42;
    const char *fn= "test.dat"
    const int Largo = 7;
    {
        fstream f(fn, ios::out | ios::binary);
        f.seekp(Largo, ios::beg);
        f.write((const char *)&i, 2);
    } // Al destruirse se cierra
    {
        fstream f(fn, ios::in | ios::binary);
        f.seekg(Largo, ios::beg);
        f.read((char *)&i, 2);
    } // Al destruirse se cierra

También podíamos haber llamado a los destructores explícitamente. Ya que estamos con la programación orientada a objetos, es más lógico utilizar constructores y destructores. Principalmente, lo que no hay que hacer es mezclar los dos métodos.

18.8) Simplemente hay que saber el código del descriptor de la impresora (que suele ser 4). Utilizamos entonces el constructor ofstream(int fh):

    fstream Stream_de_la_impresora(4);

Si queremos utilizar buffer:

    char *Buffer= new char [1024];
    ofstream Str_de_la_impr(4, Buffer, 1024);

18.9) La función Leer_Cadena() la definimos así:

    #include <iomanip.h> // resetiosflags

    void Leer_Cadena(char *s) {
        cin >> resetiosflags(ios::skipws);
        // No pasar los caracteres blancos
        for(int i= 0; cin >> s[i]; i++) // Leer hasta '\0'
            if (s[i] == '\n') // Se ha pulsado INTRO
                break;
        s[i]= '\0'; // Poner caracter nulo de terminación de cadena
    }

En primer lugar hacemos que no se quiten los caracteres blancos. Luego leemos hasta encontrar uno de estos dos caracteres '\0', '\n'. Éste último se producirá cuando pulsemos la tecla de retorno. Al final deberemos poner el carácter nulo ya que el stream no lo inserta.

19. Cadenas y Numéricos

19.1) La solución se encuentra en el fichero EJ19_01.CPP

19.2) La solución se encuentra en el fichero EJ19_02.CPP

19.4) La solución se encuentra en el fichero EJ19_04.CPP

19.5) La solución se encuentra en el fichero EJ19_05.CPP

20. Introducción a la STL

20.1-2) La solución se encuentra en el fichero EJ20_01.CPP

20.3) La solución se encuentra en los ficheros EJ20_03a.CPP, EJ20_03b.CPP y EJ20_03c.CPP.

20.4) La solución se encuentra en el fichero EJ20_04.CPP

20.5) La solución se encuentra en el fichero EJ20_05.CPP

21. Contenedores y adaptadores

21.1) La solución se encuentra en el fichero EJ21_01.CPP

21.2) La solución se encuentra en el fichero EJ21_02.CPP

21.3) La solución se encuentra en los ficheros EJ21_03a.CPP y EJ21_03b.CPP.

21.4) La solución se encuentra en  los ficheros EJ21_04a.CPP y EJ21_04b.CPP.

21.5) La solución se encuentra en el fichero EJ21_05.CPP

21.6) La solución se encuentra en el fichero EJ21_06.CPP

21.7) La solución se encuentra en el fichero EJ21_07.CPP

21.8) La solución se encuentra en el fichero EJ21_08.CPP

21.9) La solución se encuentra en el fichero EJ21_09.CPP

22. Objetos-funciones y algoritmos

22.1) La solución se encuentra en el fichero EJ22_01.CPP

22.2) La solución se encuentra en el fichero EJ22_02.CPP

22.3) La solución se encuentra en el fichero EJ22_03.CPP

22.4) La solución se encuentra en el fichero EJ22_04.CPP

22.5) La solución se encuentra en el fichero EJ22_05.CPP

22.6) La solución se encuentra en el fichero EJ22_06.CPP

22.7) La solución se encuentra en el fichero EJ22_07.CPP

23. El Proceso de Desarrollo con C++

23.1) Está en INTERPRETE.CPP



 
Última actualización: 
12 de noviembre de 2001.
Ir a la página principal
© 2001 Paraninfo Thomson Learning y los autores:
Enrique Hernández Orallo, José Hernández Orallo y Mª Carmen Juan Lizandra