Clases en C Creación de clases en C++ (I) • Convertir estructura InfoAlumno en una clase • Escribir una clase simple en C++, con constructores Programación Orientada a Objeto Ing. Civil en Telecomunicaciones Estructura de datos por alumno • Estructura de datos que almacena información de cada alumno struct InfoAlumno { std::string nombre; double examen1, examen2; std::vector<double> notas; }; • Problema: programadores acceden directamente a los elementos de los datos del tipo InfoAlumno sobrecarga de operadores Principio de ocultamiento de información (Information hiding) • Al programador se le debe Esconder los detalles de la implementación Podría ser cambiada o mejorada en el tiempo Esconder los datos Limitar fuentes de errores • Exponer sólo lo necesario para poder manipular la información (interfaz) Exponer los métodos mediante los cuales acceder a los datos La interfaz debe ser inmutable Interfaz para InfoAlumno Función miembro de lectura de datos • Agregar dos funciones miembros a estructura InfoAlumno • Función es parte de una estructura InfoAlumno struct InfoAlumno { std::string nombre; double examen1, examen2; std::vector<double> notas; std::istream& leeAlumnos(std::istream&); double nota() const; }; © 2015 Mario Medina C. Nombre debe ser InfoAlumno::leeAlumnos No necesita recibir la estructura como argumento Tiene acceso directo a datos de estructura istream& InfoAlumno::leeAlumnos(istream& in) { in >> nombre >> examen1 >> examen2; // Llama a funcion leeNotas() anterior leeNotas(in, notas); return in; } 1 Función miembro nota() • Invoca a función notas() que no es miembro de InfoAlumno Versión que recibe dos doubles y un vector<double> • Función miembro const No puede modificar datos de la estructura double InfoAlumno::nota() const { return ::nota(examen1, examen2, notas); } Clase InfoAlumno class InfoAlumno { public: // Define la interfaz std::istream& leeAlumnos(std::istream&); double nota() const; private: // Define la implementación std::string nombre; double examen1, examen2; std::vector<double> notas; }; Funciones de acceso • Ejemplo anterior tiene los datos privados Cómo se puede conocer sus valores? • Mediante funciones de acceso (accessor functions) • Ejemplo: se necesita acceso al nombre del alumno para imprimir el informe std::string nombre() const { return n; } © 2015 Mario Medina C. Protección de datos • Programadores pueden acceder a estructura a través de funciones miembros Pero también pueden acceder directamente a los datos • Solución: proteger los datos public: accesible a todo el código private: accesible sólo a funciones miembros de InfoAlumno Comandos class y struct • La única diferencia entre class y struct es la protección por omisión Protección por omisión en una clase es private En una estructura, la protección por omisión es public • En general, se prefiere usar class para clases de C++ y struct para estructuras de C Clase InfoAlumno class InfoAlumno { public: // Define la interfaz std::istream& leeAlumnos(std::istream&); double nota() const; std::string nombre() const { return n; } private: // Define la implementación std::string n; double examen1, examen2; std::vector<double> notas; }; 2 Función nombre() Funciones en línea (inline functions) • Nombre de variable cambiada para evitar conflictos entre el dato n y la función • Función miembro const • En C, comando inline declara una función en línea No puede modificar variable n Por eso, retorna copia de string en vez de referencia a string • Compilador expande funciones in situ Múltiples copias del código Evita costo de llamada • Función está definida dentro de la clase Función en línea (inline Sugerencias al compilador function) a función • Funciones miembro definidas dentro de la definición de la clase son funciones en línea Función compara() Función valido() • Función no-miembro utilizada para ordenar los registros InfoAlumno en forma alfabética • Acceso a nombre de alumno via función de acceso nombre() • Función en línea que verifica que exista al menos una nota de tarea bool compara(const InfoAlumno& x, const InfoAlumno& y) { return x.nombre() < y.nombre(); } Clase InfoAlumno class InfoAlumno { public: // Define la interfaz std::istream& leeAlumnos(std::istream&); double nota() const; std::string nombre() const { return n; } bool valido() const { return !notas.empty(); } private: // Define la implementación std::string n; double examen1, examen2; std::vector<double> notas; }; bool compara(const InfoAlumno&, const InfoAlumno&); © 2015 Mario Medina C. Usa función miembro de vector .empty() class InfoAlumno { public: bool valido() const { return !notas.empty(); } } Constructores • Cómo crear un nuevo objeto InfoAlumno? Mediante una función constructor Funciones miembro especiales que crean un objeto y (quizás) lo inicializan • No son invocadas directamente Se invocan al crear un nuevo objeto 3 Constructores Inicialización de objetos • Objetos de bibliotecas estándar incluyen constructores para objetos vacíos e inicializaciones • Compilador genera constructor vacío por omisión • Inicialización por omisión Inicializa variables miembros a un valor que depende del tipo de objeto creado Para objetos de una clase dada Clase debe incluir una función de inicialización por omisión Creación de objetos string invoca constructor por omisión de objetos string Crea un objeto string vacío Para primitivas básicas Variables son indefinidas Inicialización de objetos Constructor de InfoAlumno • Inicialización por valor • Algunos contenedores como map inicializan primitivas básicas por valor • Objeto InfoAlumno contiene 4 elementos: Al crear un nuevo elemento pair de map, se crea un entero de tipo int inicializado a cero std::string (inicializado std::vector (inicializado a string vacío) a vector vacío) Dos variables double Es necesario inicializarlas explícitamente Constructor de InfoAlumno • Constructor vacío InfoAlumno::InfoAlumno(): examen1(0), examen2(0) { } Compilador convierte int a double • Constructor a partir de flujo de entrada InfoAlumno::InfoAlumno(istream& in) { leeNotas(in); } © 2015 Mario Medina C. Clase InfoAlumno (I) // Clase que almacena la informacion de un alumno class InfoAlumno { public: // Define la interfaz InfoAlumno() : examen1(0), examen2(0) {} InfoAlumno(std::istream& in) { leeAlumnos(in); } std::istream& leeAlumnos(std::istream& in) { in >> n >> examen1 >> examen2; leeNotas(in, notas); return in; } 4 Clase InfoAlumno (II) double nota() const { return ::nota(examen1, examen2, notas); } std::string nombre() const { return n; } bool valido() const { return !notas.empty(); } Usando la clase InfoAlumno (I) int main() { vector<InfoAlumno> alumnos; InfoAlumno alumno; string::size_type maxlen = 0; // Lee los datos desde entrada estandar while(alumno.leeAlumnos(cin)) { maxlen = max(maxlen, alumno.nombre().size()); alumnos.push_back(alumno); } Usando la clase InfoAlumno (III) try { double nota_final = alumnos[i].nota(); streamsize prec = cout.precision(); cout << setprecision(3) << nota_final << setprecision(prec) << endl; } catch(domain_error e) { cout << e.what() << endl; } } return 0; Clase InfoAlumno (III) private: // Define la implementación std::string n; double examen1, examen2; std::vector<double> notas; istream& leeNotas(istream& in, vector<double>& notas) { if (in) { notas.clear(); double temp; while (in >> temp) { notas.push_back(temp); } in.clear(); } return in; } }; Usando la clase InfoAlumno (II) // Ordena los registros de alumnos sort(alumnos.begin(), alumnos.end(), compara); // Escribe los nombres y las notas for (vector<InfoAlumno>::size_type i = 0; i != alumnos.size(); ++i) { cout << alumnos[i].nombre() << string(maxlen + 1 – alumnos[i].nombre().size(), ' '); Ejemplo: la clase punto • La clase punto representa un punto en el espacio cartesiano Tiene dos variables miembro: x_coord y_coord // coordenada x // coordenada y Como primera idea, construir una estructura punto de lenguaje C, y convertirla en una clase de C++ } © 2015 Mario Medina C. 5 struct versus class // Estructura de C struct punto { double x_coord; double y_coord; }; // Clase de C++ class punto { double x_coord; double y_coord; }; Ahora, agregaremos funciones miembro a la clase Agregar funciones de acceso class punto { // Interface public: double x() const { return x_coord; } double y() const { return y_coord; } void setX(double x) { x_coord = x; } void setY(double y) { y_coord = y; } // Implementacion private: double x_coord; double y_coord; }; Definir constructor vacío Definir constructor con argumentos • Constructor vacío inicializa variables miembro a 0.0 • Constructor inicializa variables miembro a partir de los argumentos punto():x_coord(0), y_coord(0) { } • Otra forma de escribir el constructor vacío punto() { x_coord = 0.0; y_coord = 0.0; } punto(double x, double y) { x_coord = x; y_coord = y; } • Forma de uso punto p1, p2(1.0, 2.0); Operaciones con objetos punto Comparando puntos • Nos gustaría poder usar las operaciones aritméticas comunes con objetos puntos • Dos puntos son iguales si sus componentes son iguales y diferentes si sus componentes son diferentes Suma, resta, multiplicación, división por una constante Suma y resta de dos objetos punto Comparación de objetos punto • Cómo? Sobrecargando los operadores estándar bool operator==(punto& p) { return(x_coord == p.x() && y_coord == p.y()); } bool operator!=(punto& p) { return !(x_coord == p.x() && y_coord == p.y()); } • Uso: cout << (p1 == p2) << endl; © 2015 Mario Medina C. 6 Comparando puntos Multiplicando punto por constante • Ambas funciones anteriores son funciones miembro de la clase punto • Comparación de objetos punto p1 == p2 es interpretada por el compilador como p1.==(p2) • Multiplicar un punto por una constante k multiplica sus componentes por la constante Se llama a la función miembro correspondiente Operación debería retornar un nuevo objeto punto No modifica el objeto actual • Cómo crear un nuevo objeto punto? Solicitar creación de objeto al sistema via comando new Multiplicando punto por constante punto operator*(double k) const { punto *temp = new punto; temp->setX(x_coord*k); temp->setY(y_coord*k); return *temp; } • Lo mismo puede hacerse para la división Uso: Creación dinámica de objetos • La expresión new T crea un nuevo objeto del tipo ó clase T Expresión retorna un puntero al nuevo objeto Objeto inicializado por omisión También puede ser inicializado con argumentos InfoAlumno *ia = new InfoAlumno(); punto *p = new punto(); Objeto se elimina con delete delete ia; p2 = (p1/5.0)*9.0; Creación dinámica de vectores • La expresión new T[n] crea un vector de n objetos del tipo ó clase T Expresión retorna un puntero de tipo T* al primer elemento del vector Todos los objetos inicializados por omisión Inicializados a través del constructor por omisión Vector se elimina con delete[] Elementos son destruidos en orden inverso de creación © 2015 Mario Medina C. Suma punto con constante punto operator+(double k) { punto *temp = new punto; temp->setX(x_coord + k); temp->setY(y_coord + k); return *temp; } Pero, qué pasa al sumar un punto con otro punto? 7 Suma punto con otro punto punto operator+(const punto& p) { punto *temp = new punto; temp->setX(x_coord + p.x()); temp->setY(y_coord + p.y()); return *temp; } • Cómo sabe el sistema cuál función usar? Por los argumentos: p1 = p2 + 3; p3 = p1 + p2; Sobrecarga de operador += punto& operator+=(double k) { x_coord = x_coord + k; y_coord = y_coord + k; return *this; } punto& operator+=(const punto& p) { x_coord = x_coord + p.x(); y_coord = y_coord + p.y(); return *this; } Sobrecarga de operaciones += y -= • Operación A += B sobreescribe A con la suma de A + B • Sobrecarga de operador debe modificar objeto y retornar objeto modificado • Cómo puede un objeto retornarse a sí mismo? Comando this hace referencia al objeto Sobrecarga de operador *= punto& operator*=(double k) { x_coord = x_coord*k; y_coord = y_coord*k; return *this; } punto& operator*=(const punto& p) { } • Función debe retornar referencia al objeto actual, no una copia! • Pero, qué debería hacer esto? Sobrecarga de operador *= Reglas básicas de sobrecarga de operadores • Qué debería hacer la multiplicación de dos objetos punto p1*p2? Producto punto ó interno Producto cruz ó externo Multiplicación elemento-a-elemento • Sólo sobrecargar operadores si la operación a realizar sobre los objetos está claramente definida © 2015 Mario Medina C. 1. No sobrecargar un operador si la operación no es “obvia” 2. Siempre atenerse a la semántica “obvia” de la operación • A – B resta el valor de B de A 3. Siempre proveer las operaciones relacionadas Si se sobrecarga +, también sobrecargar += 8 Operadores que pueden sobrecargarse • Aritméticos: + - * / += -= *= /= ++ -+(unario) -(unario) • Manipulación de bits: & | ^ << >> &= |= ^= <<= >>= ~ • Comparación: == != < > <= >= || && ! (unario) • Manejo de memoria: new new[] delete delete[] • Otros: () , [] -> * &(prefijos) Sobrecarga de == usando función no-miembro • La siguiente función no-miembro compara dos objetos punto bool operator==(const punto& p1, const punto p2) { return(p1.x() == p2.x() && p2.x() == p2.y()); } • Cómo debería sobrecargarse el operador ==? © 2015 Mario Medina C. Sobrecarga mediante funciones nomiembro • Operadores pueden ser sobrecargados en funciones miembro ó no-miembro Funciones no-miembro reciben dos operandos Declaradas fuera de la clase Funciones miembro tienen un operando implícito: el objeto al cual pertenecen Declaradas dentro de la clase Funciones miembro vs. no-miembro • Operadores =, ->, [], (), sólo pueden sobrecargarse como funciones miembro • Operadores =<<, >> sólo pueden sobrecargarse en funciones no-miembro • Operadores unarios: recomendable sobrecargar en funciones miembro • Operadores binarios que tratan ambos argumentos de igual forma: recomendable sobrecargar en funciones no-miembro • Operadores binarios que modifican argumento: recomendable sobrecargar en funciones miembro 9