C++ de notas de estudio

C++

concepto basico

función en línea

  • Las funciones en línea se expanden automáticamente cuando se compila la función y no existen después de la compilación, similar a la expansión de macros.
  • Las funciones en línea se pueden definir en archivos de encabezado y no se producirán errores de definición repetidos incluso si se incluyen varias veces.
  • Las funciones en línea son como macros en tiempo de compilación, no existen en el momento del enlace después de la compilación .
  • Es necesario agregar una palabra clave en línea cuando se define la función , y no es válida cuando se usa cuando se declara la función .
  • Las funciones en línea no deben tener una declaración, pero la declaración no informará un error.
  • Poner la definición y declaración de la función en línea en diferentes archivos provocará un error, para ser precisos, se informará un error al vincular. Por lo tanto, la definición de funciones en línea generalmente se coloca en el archivo de encabezado.
  • Las funciones en línea tienen dos funciones, la primera esEliminar la sobrecarga de llamadas a funciones, el segundo esreemplazar macros con parámetros

parámetros predeterminados

Al crear una función, puede establecer un parámetro predeterminado para el parámetro formal. El parámetro predeterminado también puede usar una expresión. El parámetro predeterminado solo puede estar al final de la lista de parámetros formales . Una vez que un determinado parámetro formal tiene un valor predeterminado, entonces los parámetros formales detrás de esto también serán Debe haber un valor predeterminado. C++ estipula que en un ámbito determinado, los parámetros predeterminados solo se pueden especificar una vez.

Si los parámetros predeterminados en la función de definición y la función de declaración son diferentes, entonces los parámetros predeterminados del alcance actual se utilizan en el momento de la compilación. La misma función se puede declarar varias veces.

sobrecarga de funciones

C++ permite que varias funciones tengan el mismo nombre, siempre que las listas de parámetros sean diferentes, lo cual essobrecarga de funciones(Sobrecarga de funciones)lista de parámetrosTambién se llama firma de parámetro, incluido el tipo de parámetro , el número de parámetros y el orden de los parámetros . Siempre que haya una diferencia, se denomina lista de parámetros diferente.

Reglas para la sobrecarga de funciones:

  1. Los nombres de las funciones deben ser los mismos.
  2. Las listas de argumentos deben ser diferentes.
  3. Los tipos de retorno de funciones pueden ser iguales o diferentes
  4. No se pueden utilizar tipos de retorno simplemente diferentes como base para la sobrecarga de funciones

C ++ cambiará el nombre de la función de acuerdo con la lista de parámetros al compilar, por ejemplo, void Swap(int a, int b)se cambiará el nombre a Swap_int_int, void Swap(float x, float y)se cambiará el nombre a Swap_float_float. Cuando se produce una llamada a una función, el compilador hará coincidir los parámetros reales entrantes uno por uno y seleccionará la función correspondiente. Si la coincidencia falla, el compilador informará un error, lo que se denomina resolución de sobrecarga.

La sobrecarga de funciones se produce solo en el nivel gramatical, pero funciones esencialmente diferentes ocupan memoria diferente . La sobrecarga de funciones y muy pocos o demasiados tipos de parámetros pueden provocar errores de ambigüedad.

“C” externaHay aproximadamente 2 usos:

  1. Cuando solo modifique una oración de código C++, simplemente agréguela directamente al comienzo de la línea de código;
  2. Si se usa para decorar un fragmento de código C ++, simplemente agregue un par de llaves {} para externar "C" y envuelva el código que se va a decorar entre corchetes.

clases y objetos

concepto

De manera similar a la plantilla para crear objetos, una clase puede crear múltiples objetos y cada objeto es una variable del tipo de clase; el proceso de creación de un objeto también se denomina creación de instancias de la clase . Cada objeto es una instancia concreta de una clase, con variables miembro y funciones miembro de la clase.

Al igual que las estructuras, las clases son solo declaraciones de tipos de datos complejos y no ocupan espacio en la memoria .

Utilice la palabra clave class para definir una clase. La primera letra del nombre de la clase generalmente se escribe en mayúscula para distinguirla. Dentro de {} están las variables miembro y las funciones miembro de la clase, que se denominan colectivamente miembros de la clase . Las definiciones de clase van seguidas de un punto y coma.

Una clase es sólo una plantilla y no ocupa espacio en la memoria después de la compilación, por lo queLas variables miembro no se pueden inicializar al definir una clase, porque no hay ningún lugar donde almacenar los datos . Solo después de crear el objeto se asignará memoria a la variable miembro y luego se podrá asignar el valor.

Cuando una función se define directamente en una clase, no es necesario agregar el nombre de la clase antes del nombre de la función, pero cuando la función se define fuera de la clase, es necesario agregar el nombre de la clase al nombre de la función, que se llama un solucionador de dominio ::.

Las funciones miembro definidas en el cuerpo de la clase se convertirán automáticamente en funciones en línea , mientras que las definidas fuera de la clase no. La función en línea reemplazará automáticamente la llamada a la función con el cuerpo de la función en el momento de la compilación, lo cual no es lo que esperamos, por lo que se recomienda declarar la función dentro del cuerpo de la clase y definirla fuera del cuerpo de la clase .

derechos de acceso a clase

Pase C ++públicoprotegidoprivadoTres palabras clave para controlar el acceso a variables miembro y funciones miembro. Son públicos, protegidos y privados, respectivamente, y se denominan calificadores de acceso de miembros . El llamado derecho de acceso es si puede utilizar los miembros de esta clase.

Dentro de la clase, todas las funciones miembro y variables miembro pueden acceder entre sí sin restricciones en los derechos de acceso.

Fuera de la clase, solo se puede acceder a los miembros a través del objeto , y solo se puede acceder a los miembros de propiedades públicas a través del objeto.

La mayoría de las variables miembro m_comienzan con, que es una forma habitual de escribir. El comienzo de m_la variable miembro se puede ver de un vistazo y se pueden distinguir los nombres de los parámetros formales en la función miembro. La asignación de variables miembro generalmente utiliza funciones públicas, set y get, y el nombre de la variable puede ir seguido de la función. Si público y privado no están escritos, el valor predeterminado es privado . En un cuerpo de clase, privado y público pueden aparecer varias veces y no hay un alcance válido hasta que ocurre otro calificador de acceso o el final del cuerpo de clase.

Además de las funciones set y get, también puede llamar al constructor para inicializar cada variable miembro al crear un objeto, pero el constructor solo puede inicializar el valor de cada variable miembro y no puede asignar valores a las variables miembro. .

Una clase es una plantilla para crear un objeto, no ocupa espacio de memoria y no existe en el archivo ejecutable compilado, pero el objeto son datos reales que requieren memoria para almacenarse. Cuando se crea un objeto, se asigna memoria en la pila o montón. Los valores de las variables miembro de diferentes objetos pueden ser diferentes, por lo que se requiere memoria separada para el almacenamiento.

== Los códigos de las funciones miembro de diferentes objetos son los mismos y los fragmentos de código se pueden comprimir en una copia. ==El compilador almacenará las variables miembro y las funciones miembro del objeto por separado,Las variables miembro asignan memoria por separado, pero las funciones miembro comparten un fragmento de código de función. Las variables miembro asignan memoria en el área de la pila o en el área del montón,Las funciones miembro asignan memoria en el área de código. El valor de tamaño de un objeto solo se ve afectado por las variables miembro y no tiene nada que ver con las funciones miembro . La distribución de la memoria de la clase también tendrá problemas de alineación de la memoria.

Compilación de funciones de C++.

La función del lenguaje C simplemente se agrega con un guión bajo al compilar (las diferentes implementaciones del compilador son diferentes).

Cuando se compila C++, se cambiará el nombre de la función según el espacio de nombres , la clase a la que pertenece , la lista de parámetros (firma del parámetro) y otra información. Forme un nuevo nombre de función, este nuevo nombre de función solo lo conoce el compilador, este proceso de cambiar el nombre de la función se llamacódigo de nombre. El proceso de codificación de nombres es reversible y conocer uno de los nombres puede inferir el otro.

Nota : Si desea ver el nuevo nombre de función generado por el compilador, solo puede declarar la función sin definir la función, de modo que aparecerá un error al llamar a la función y podrá ver el nuevo nombre de función en el mensaje de error. .

llamada a función miembro

La función miembro finalmente se compila en una función que no tiene nada que ver con el objeto. Si la variable miembro no se usa en la función, se puede llamar directamente.

Si se usa una variable miembro en la función miembro, se debe agregar un parámetro adicional al compilar la función miembro, y el objeto actual se pasa como un puntero y se accede a la variable miembro a través del puntero. Esto es contrario a lo que imaginamos cuando usamos objetos para encontrar funciones, pero en realidad busca objetos a través de funciones.

Constructor

Hay una función miembro especial cuyo nombre es el mismo que el nombre de la clase , no tiene valor de retorno , no necesita ser llamado explícitamente por el usuario y el usuario no puede llamarlo, pero es una función que se ejecuta automáticamente cuando el Se crea el objeto . Esta función miembro especial esConstructor(Constructor)。

Si desea llamar al constructor, debe pasar los parámetros reales mientras crea el objeto, y los parámetros reales están ()rodeados por , que es muy similar a una llamada de función normal. Al crear un objeto en la pila , el parámetro real se encuentra después del nombre del objeto Student stu("小明", 15, 92.5f);,. Cuando se crea un objeto en el montón , la implementación se coloca después del nombre de la clase new Student("李华", 16, 96). El constructor es un atributo público ; de lo contrario, no se llamará al crear un objeto.

El constructor no tiene valor de retorno , porque no hay ninguna variable para recibir el valor de retorno, por lo que ya sea que esté definido o declarado, el tipo de valor de retorno no puede aparecer antes del nombre de la función y la declaración de retorno no se puede usar en el cuerpo de la función . Se permite la sobrecarga del constructor. Una clase puede tener múltiples constructores sobrecargados. Al crear un objeto, se juzga a qué constructor llamar de acuerdo con los parámetros pasados.

La llamada al constructor esobligatorioSí, una vez que el constructor está definido en la clase, se llamará cuando se cree el objeto. Si hay varios constructores sobrecargados, los parámetros reales proporcionados deben coincidir con uno de los constructores cuando se crea el objeto. Sólo se llama a un constructor cuando se crea un objeto. Los constructores generalmente se utilizan para realizar algunos trabajos de inicialización en proyectos. Si no se define ningún constructor, el compilador generará automáticamente un constructor , que está vacío dentro del constructor predeterminado. Una clase debe tener un constructor, que lo define el usuario o lo genera automáticamente el compilador. Si el usuario define el constructor, el compilador no lo generará automáticamente. Los paréntesis se pueden omitir al llamar a un constructor sin parámetros .

El uso de la inicialización del constructor también puede usarlista de inicialización, es decir, al definir un constructor, no es necesario asignar valores a las variables en el cuerpo de la función, sino utilizar dos puntos entre el encabezado de la función y el cuerpo de la función, seguido de una declaración, que es equivalente al interior del cuerpom_name(name), m_age(age), m_score(score) funcional . Por ejemplo:m_name = name; m_age = age; m_score = score

Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
		//TODO:
	}

No hay ninguna ventaja de eficiencia al usar una lista de inicializadores, solo para comodidad de escritura y lectura. El orden de inicialización de las variables miembro no tiene nada que ver con el orden de las variables enumeradas en la lista de inicialización , sólo con el orden en que se declaran las variables miembro en la clase . Para asignar memoria en la pila, el valor inicial es incierto; para asignar memoria en el montón, el valor inicial es 0.

Otra función importante de la lista de inicialización del constructor es inicializar variables miembro constantes, y esta es la única forma de inicializar variables constantes.

incinerador de basuras

Al destruir un objeto, el sistema llamará automáticamente a una función para limpiar, como liberar memoria, cerrar archivos abiertos, etc. Esta función esincinerador de basurasincinerador de basuras(Incinerador de basuras) también es una función miembro especial, no tiene valor de retorno, no es necesario llamarla explícitamente y no se puede llamar, y se llamará automáticamente cuando se destruya el objeto.

El nombre del constructor es el mismo que el nombre de la clase y el nombre del destructor se agrega delante del nombre de la clase~ . Los destructores no tienen parámetros y no se pueden sobrecargar, por lo que una clase sólo puede tener un destructor .

C ++ usa new y delete para asignar y liberar memoria. La mayor diferencia con malloc y free es que se llamará al constructor cuando se use new para asignar memoria, y se llamará al destructor cuando se use eliminar para liberar memoria .

objeto globalEl área de datos globales se almacena en el área de memoria y se llamará al destructor de estos objetos al final de la ejecución del programa.objeto localEl área de la pila almacenada en la memoria, el destructor de estos objetos se llamará cuando finalice la ejecución de la función. El objeto creado por nuevo se encuentra en el área del montón en la memoria, y cuando se llama a eliminar, se llama al destructor. Si no se elimina, el destructor no se ejecutará.

matriz de objetos C++

Cada elemento de una matriz es un objeto, y dicha matriz se llama matriz de objetos . Cuando el constructor tiene más de un elemento, la lista de inicializadores de matriz contiene explícitamente la llamada al constructor .

Objeto miembro de C++

Si una variable miembro de una clase es un objeto de una clase, se llama objeto miembro . Una clase que contiene objetos miembro se llama clase envolvente . Al crear un objeto de clase cerrada, también es necesario crear los objetos miembro que contiene, lo que activará la llamada del constructor del objeto miembro.

Cuando se genera el objeto de clase cerrada, primero se ejecutan los constructores de todos los objetos miembros y luego se ejecuta el propio constructor de la clase cerrada . El orden de ejecución de los constructores de objetos miembro es coherente con el orden de los objetos miembro en la definición de clase, independientemente del orden en que aparecen en la lista de inicialización del constructor.

Cuando el objeto de la clase cerrada muere, primero se ejecuta el destructor de la clase cerrada y luego se ejecuta el destructor del miembro del objeto. El orden de ejecución del destructor del objeto miembro es opuesto al orden de ejecución del destructor, es decir, primero la destrucción construida y luego la ejecutada.

este puntero

esteEs una palabra clave de C++ y también un puntero constante .apuntar al objeto actual, a través del cual se puede acceder a todos los miembros del objeto actual .esto solo se puede usar dentro de la clase, a través de esto se puede acceder a todos los miembros de la clase, incluidos los atributos públicos, protegidos y privados.

Aunque esto está dentro de la clase, solo se le asignará un valor después de que se cree el objeto , y el compilador completa automáticamente este proceso de asignación.Este es en realidad un parámetro formal de una función miembro. Al llamar a una función miembro, la dirección del objeto se pasa a esta como un parámetro real, pero el propio compilador lo agrega a la lista de parámetros durante la fase de compilación.

Esto como parámetro es esencialmente una variable local de la función miembro, que solo se puede usar dentro de la función miembro y solo se asigna cuando la función se llama a través del objeto.

Las funciones miembro se compilarán en funciones ordinarias que no tienen nada que ver con el objeto durante la compilación. Excepto las variables miembro, toda la información se perderá. Por lo tanto, al compilar, se debe agregar un parámetro adicional a la función miembro para pasar el objeto actual. como puntero Para asociar variables miembro y funciones miembro, este parámetro adicional es este .

variable miembro estática

A veces desea compartir datos entre varios objetos, puede utilizar variables miembro estáticas para lograr el objetivo de que varios objetos compartan datos . Una variable miembro estática es una variable miembro especial que se modifica con la palabra clave estática.Las variables miembro estáticas pertenecen a una clase y no a un objeto específico. Incluso si se crean varios objetos, se asigna una parte de la memoria y todos los objetos usan los datos de esta memoria.

Las variables miembro estáticas deben inicializarse fuera de la declaración de clase . Las variables miembro estáticas no se pueden agregar estáticas al inicializar, pero deben tener un tipo de datos .

La memoria de la variable miembro estática no se asigna cuando se declara la clase ni cuando se crea el objeto, pero se asigna cuando se inicializa fuera de la clase.. Por lo tanto, no se pueden utilizar variables miembro estáticas que no se inicialicen fuera de la clase .Se puede acceder a las variables miembro estáticas a través de la clase o mediante variables miembro. Las variables miembro estáticas no ocupan la memoria del objeto, pero abren memoria fuera de todos los objetos y se puede acceder a ellas incluso si el objeto no se crea .

Al igual que las variables estáticas ordinarias, a las variables miembro estáticas se les asigna memoria en el área de datos globales en la partición de memoria y no se liberan hasta el final del programa. Las variables miembro estáticas deben inicializarse e inicializarse fuera de la clase. La inicialización se puede asignar o no y la asignación predeterminada es 0 .

Se puede acceder a las variables miembro estáticas por nombre de objeto o por nombre de clase . Requiere restricciones de acceso a palabras clave.

función miembro estática

Las funciones de miembros ordinarios pueden acceder a todos los miembros y las funciones de miembros estáticos solo pueden acceder a miembros estáticos . Cuando el compilador compila una función miembro ordinaria, agregará un parámetro formal this a la función y le pasará la dirección del objeto actual, por lo que las funciones miembro ordinarias solo se pueden llamar a través del objeto después de que se crea el objeto.

Las funciones miembro estáticas se pueden llamar directamente a través de la clase, el compilador no le agregará este parámetro y no necesita la dirección del objeto actual. Las variables miembro ordinarias ocupan la memoria del objeto, pero los miembros estáticos no tienen este puntero, por lo que no saben a qué objeto apunta y no pueden acceder a las variables miembro del objeto . Por lo tanto, los miembros estáticos no pueden acceder a variables miembro ordinarias, solo a variables miembro estáticas.

En C ++, la función principal de las funciones miembro estáticas es acceder a miembros estáticos. Las funciones miembro estáticas se pueden llamar a través de objetos y clases, pero generalmente se llaman a través de clases.

funciones miembro constantes y variables miembro

variable miembro constanteLa inicialización se realiza únicamente a través de la lista de inicializadores del constructor .

función miembro constanteSe pueden utilizar todas las variables miembro de la clase, pero sus valores no se pueden modificar. Las funciones miembro constantes también se llamanfunción miembro constante, generalmente establece la función get en una función constante.

Las funciones miembro const y las variables miembro const deben agregar la palabra clave const en la definición y declaración, por ejemplo: int getage() const.

La constante al comienzo de la función se usa para modificar el valor de retorno de la función, lo que indica que el valor de retorno es de tipo constante. La constante al final del encabezado de la función significa una función miembro constante,Esta función solo puede leer el valor de la variable miembro, pero no puede modificar el valor de la variable miembro.

objeto constante

El objeto modificado constantemente se llamaobjeto constante, el objeto constante solo puede llamar al miembro constante de la clase ,

Funciones de amigos y clases de amigos.

con la ayuda deTomomoto(amigo), permitiendo que funciones miembro en otras clases y funciones en el ámbito global accedan a los miembros privados de la clase actual.

función amigo, Las funciones que están definidas fuera de la clase actual y no pertenecen a la clase actual también se pueden declarar en la clase actual, pero la palabra clave amigo debe agregarse delante de ella, formando así una función amiga, a la que no puede pertenecer. cualquier clase Las funciones que no son miembros también pueden ser funciones miembro de otras clases.

Las funciones amigas pueden acceder a todos los miembros de la clase actual, incluidos los atributos públicos, protegidos y privados. La función amiga es diferente de la función miembro de la clase: en la función amiga, no se puede acceder directamente a los miembros de la clase y se debe utilizar el objeto para acceder a ella.

En algunos casos, siempre que se realice una declaración temprana, la clase se puede probar primero, pero el alcance de la declaración temprana de la clase es limitado y solo se puede usar para crear objetos después de una declaración formal. Debido a que la creación de un objeto requiere asignar memoria, pero no hay una declaración formal, el compilador no sabe cuánta memoria asignar.

Varias clases pueden declarar una función como función amiga, de modo que se pueda acceder a miembros privados en varias clases.

clase de amigosTodas las funciones miembro son funciones amigas de otra clase.

Nota:Las relaciones de amistad son unidireccionales, no bidireccionales. Las relaciones de amistad no se pueden pasar.

Una clase también es un tipo de alcance. Cada clase define su propio alcance. Fuera del alcance de una clase, solo se puede acceder a los miembros ordinarios a través de objetos. Se puede acceder a los miembros estáticos a través de objetos y clases. Sin embargo, typedef define el tipo de Sólo es accesible a través de la clase.

La diferencia entre estructura y clase.

  1. Los miembros de la clase son privados de forma predeterminada y los miembros de la estructura son públicos.
  2. La herencia de clases por defecto es herencia privada y la herencia de estructuras por defecto es herencia pública.
  3. La clase puede usar plantillas, pero la estructura no puede usar plantillas

cadena de cuerda

String es una clase en C++ y los archivos de encabezado deben incluirse al usarlo <string>. Si la cadena de definición no se inicializa, el compilador utilizará de forma predeterminada una cadena vacía .

No hay una marca de final "\0" al final de la cadena en C++, si desea saber la longitud de la cadena, puede usar la proporcionada en cadena length(), y la cadena devuelve la longitud oficial en lugar de longitud+1 .

En la programación real, a veces es necesario utilizar cadenas de estilo C (como la ruta al abrir un archivo), para ello,clase de cadenanos proporciona unfunción de conversiónc_str(), esta función puede convertir la cadena en una cadena de estilo C y devolver el puntero constante de la cadena .

string path = "D:\\demo.txt";
FILE *fp = fopen(path.c_str(), "rt");

La clase de cadena sobrecarga los operadores de entrada y salida., puede tratar las variables de cadena como variables ordinarias, es decir, usarlas >>como entrada y usarlas <<como entrada. Y para la clase de cadena, puede usar el operador +y +=para concatenar cadenas directamente, lo cual es muy conveniente y no necesita usar strcat(),, para concatenar cadenas.strcpy()malloc()

Adición, eliminación, modificación y consulta de cadenas de caracteres.

insertar: la función puede insertar otra cadenainsert() en la posición especificada en la cadena : .string& insert(size_t pos, const string& str);

  • pos indica la posición a insertar, es decir, el subíndice; str indica la cadena a insertar.

borrar: erase()la función puede eliminar una subcadena en la cadena: string& erase(size_t pos = 0, size_t len = npos);.

  • pos indica el subíndice inicial de la subcadena que se eliminará; len indica la longitud de la subcadena que se eliminará.

extracto: substr()la función puede extraer la subcadena de la cadena cadena string substr (size_t pos = 0, size_t len = npos) const;:.

  • pos es el subíndice real de la cadena que se va a extraer; len es la longitud de la cadena que se va a extraer

buscar: find()La función puede encontrar la posición donde aparece la subcadena en la cadena cadena: Size_t find (const string& str, size_t pos = 0) const;.

  • El primer parámetro es la cadena a buscar; el segundo parámetro es el subíndice para comenzar a buscar.
  • findLa función finalmente devuelve el subíndice real donde la subcadena aparece por primera vez en la cadena, o un número infinito si no se encuentra.

rfindLa diferencia entre una función y finduna función es que rfindla función solo encuentra la posición del segundo parámetro. find_first_of()Función para encontrar la primera aparición en una cadena de un carácter que tanto una subcadena como una cadena tienen en común.

En lenguaje C, hay dos formas de representar cadenas:

  1. Utilice matrices de caracteres para contener cadenas; dichas cadenas se pueden leer y escribir.
  2. Representada por una constante de cadena, dicha cadena solo se puede leer, no escribir .

En C++, la cadena encapsula información relacionada con la memoria y la capacidad internamente, por lo que la cadena conoce su posición inicial en la memoria , la secuencia de cadenas que contiene y la longitud de la secuencia de caracteres .Cuando el espacio de memoria es insuficiente, la cadena también ajustará automáticamente el tamaño de la memoria.

cita

concepto

Se denominan tipos como char, int y float.tipo básico, datos, estructuras, clases, etc. se llamantipo de agregación

En C++, existe una forma más rápida de pasar datos de tipo agregado que los punteros , es decircita. Una referencia puede considerarse como un alias de datos y los datos se pueden encontrar a través de este alias y el nombre original.

Las referencias se definen como punteros, utilizando el símbolo & : type &name = data;.

  • type es el tipo de datos al que se hace referencia.
  • nombre es el nombre de la referencia.
  • Los datos son los datos referenciados.

Las referencias deben inicializarse en el momento de la definición., Será consistente en el futuro y no se podrá hacer referencia a otros datos, lo cual es un poco similar a una constante (variable constante).

Al definir una referencia, se debe agregar &, pero no se puede agregar al usarla , y agregar & significa tomar la dirección al usarla. Dado que tanto la referencia como la variable original apuntan a la misma dirección, la referencia también puede modificar los datos almacenados en la variable original.

Si no desea modificar los datos originales por referencia, puede agregar una constante al definir, const type &name = valueo type const &name = valueeste método de referencia esA menudo citado

Al definir o declarar una función, puede especificar el parámetro formal de la función como referencia , de modo que cuando se llame a la función, el parámetro real y el parámetro formal se unirán para que todos hagan referencia al mismo dato. Si los datos del parámetro formal se modifican en el cuerpo de la función, los datos del parámetro real también se modificarán , para afectar los datos fuera de la función dentro de la función. Pasar parámetros por referencia es más intuitivo que los punteros en términos de uso y fomenta el uso de referencias en lugar de punteros.

El problema al que se debe prestar atención al utilizar referencias como valores de retorno de funciones es que no se pueden devolver datos locales , porque los datos locales se destruirán una vez finalizada la función.

Una referencia simplemente encapsula un puntero, y su capa inferior todavía se implementa a través de un puntero. La memoria ocupada por una referencia es la misma que la memoria ocupada por un puntero, que es de 4 bytes en un entorno de 32 bits y de 8 bytes en un Entorno de 64 bits.bytes , la razón por la que no se puede obtener la dirección de referencia es porque el compilador realiza una conversión interna. De lo contrario, el compilador obtiene la dirección de la referencia y la referencia seguirá ocupando memoria.

La diferencia entre referencias y punteros.:

  1. Al definir una referencia se debe inicializar y no se puede cambiar posteriormente, es decir, no puede apuntar a otros datos, el puntero no tiene esta restricción.
  2. Los punteros pueden tener varios niveles, pero las referencias solo pueden tener un nivel.

Un puntero es la dirección de los datos o el código en la memoria, y una variable de puntero apunta a los datos o el código en la memoria. El puntero sólo puede apuntar a la memoria, no a registros ni discos duros, porque no se pueden direccionar registros ni discos duros . Algunos datos, como el resultado de una expresión, el valor de retorno de una función, etc., pueden almacenarse en un registro. Una vez colocados en un registro, no se puede acceder a sus direcciones a través de &, por lo que no se pueden utilizar punteros para señalar a ellos.

El tipo de puntero corresponde estrictamente al tipo de datos al que apunta.

expresión constante

Una expresión que no contiene variables se llamaexpresión constanteDebido a que la expresión constante no contiene variables, se puede evaluar en la etapa de compilación . El compilador no asignará una memoria separada para almacenar el valor de la expresión constante, sino que combinará el valor de la expresión constante con el código y lo colocará. en una región de código en el espacio de direcciones virtuales . Por lo tanto, la expresión constante es un valor inmediato desde la perspectiva del ensamblador , que estará codificado en la instrucción y no podrá abordarse .

Por lo tanto, aunque una expresión constante se almacena en la memoria, no se puede direccionar, por lo que no se puede usar & para obtener su dirección, y mucho menos usar un puntero para señalarla .

El compilador creará una variable temporal para la referencia constante, haciendo que la referencia sea más flexible y versátil. Después de agregar una calificación constante a la referencia, no solo la referencia se puede vincular a datos temporales, sino que también se puede vincular a datos de tipo similar, lo que hace que la referencia sea más flexible y general, y los mecanismos detrás de ellas son todas variables temporales. . Por lo tanto, el parámetro de función del tipo de referencia debe usar coonst tanto como sea posible. Cuando la referencia se usa como parámetro de función, si el valor de la referencia no se modificará dentro de la función, intente usar la restricción const.

En resumen, hay tres razones para agregar un tipo restringido constantemente a un parámetro de un tipo de referencia:

  1. El uso de const evita errores de programación que modifican los datos sin darse cuenta .
  2. const permite que la función reciba argumentos de tipo constante y no constante; de ​​lo contrario, solo recibirá argumentos de tipo no constante .
  3. El uso de referencias constantes permite que las funciones generen y utilicen correctamente variables temporales.

Herencia y Derivación

concepto

heredar(herencia) es el proceso mediante el cual una clase obtiene variables miembro y funciones miembro de otra clase .

derivación(derivar) y heredar un concepto, sólo que desde un punto de vista diferente.

La clase heredada se llama clase padre o clase base , y la clase heredada se llama subclase o clase derivada . Las clases derivadas tienen miembros de la clase base y también pueden definir sus propios miembros nuevos para mejorar la funcionalidad de la clase. Se puede acceder a los miembros heredados a través de objetos de subclase como los suyos.

Sintaxis general para herencia: class 派生类 : 继承方式 基类{派生类新增加的成员};. Los métodos de herencia incluyen public,,, si no privatese protectedescriben, el valor predeterminado es privado.

Los miembros protegidos son similares a las funciones privadas y no se puede acceder a ellos a través de objetos. Pero cuando existe una relación de herencia, los miembros protegidos de la clase base se pueden usar en la clase derivada, pero los miembros privados de la clase base no se pueden usar.

Derechos de acceso de diferentes métodos de herencia en clases derivadas.

herencia publica

  • Los miembros de public en la clase base de la clase derivada también son propiedades públicas.
  • Las propiedades de los miembros protegidos en la clase base también están protegidas en la clase derivada .
  • Los miembros privados de una clase base no se pueden utilizar en clases derivadas.

herencia protegida

  • Los miembros públicos de la clase base están protegidos en el atributo de clase derivada .
  • El miembro protegido en la clase base tiene la propiedad protegida en la clase derivada .
  • Todos los miembros privados de la clase base no se pueden utilizar en la clase derivada.

herencia privada

  • Todos los miembros públicos de la clase base son propiedades privadas de la clase derivada .
  • Todos los miembros protegidos de la clase base son privados en la clase derivada .
  • Todos los miembros privados de la clase base no se pueden utilizar en la clase derivada.

Los miembros de la clase base no deben tener derechos de acceso superiores a los especificados en Herencia en las clases derivadas . Independientemente del método de herencia, los miembros privados de la clase base no siempre se pueden utilizar en la clase derivada. Los miembros privados de la clase base no pueden ser utilizados por las clases derivadas, por no decir que no pueden heredarse. En el desarrollo, generalmente se utiliza la herencia pública.

La única forma de acceder a miembros privados de una clase base en una clase derivada es a través de funciones de miembros no privados de la clase base.

El uso de la palabra clave de uso puede modificar los derechos de acceso de los miembros públicos y protegidos en la clase base, pero no puede cambiar los derechos de acceso de los miembros privados.

	//派生类Student
	class Student : public People {
	public:
		void learning();
	public:
		using People::m_name;  //将protected改为public
		using People::m_age;  //将protected改为public
		float m_score;
	private:
		using People::show;  //将public改为private
	};

Los miembros de la clase derivada y los miembros de la clase base tienen el mismo nombre y los miembros heredados de la clase base serán sombreados . La función miembro de la clase base causará sombreado siempre que el nombre sea el mismo independientemente de los parámetros, por lo que la función miembro de la clase base y la función miembro de la clase derivada no constituirán una sobrecarga .

El alcance de la clase derivada está anidado en el alcance de la clase base . Si no se puede encontrar un nombre en el alcance de la clase derivada, el compilador continuará buscando la definición del nombre en el alcance de la clase base externa. Si un nombre se define o declara en el ámbito externo, entonces el ámbito interno puede acceder a este nombre. La búsqueda de nombres se busca gradualmente desde la capa interna a la capa externa. Si se encuentra la capa interna, no se buscará. Este proceso se llamabúsqueda de nombre

El compilador solo usará funciones con el mismo nombre en el mismo alcance que las opciones para funciones sobrecargadas.

Modelo de memoria al heredar

Al heredar, la memoria de la clase derivada se puede considerar como la suma de los miembros de la clase base y las variables miembro recién agregadas .Las funciones miembro todavía se almacenan en el área de código y son compartidas por todos los objetos.. En el modelo de objetos de la clase derivada, se incluirán todas las variables miembro de la clase base. Este método puede acceder directamente a las variables miembro de la clase base sin pasar por varias capas de cálculos indirectos.

Constructores para clases base y derivadas.

El constructor de la clase no se puede heredar , y la inicialización de la función miembro heredada por la clase derivada también necesita que se complete el constructor de la clase derivada, pero la variable privada en la clase base no se puede inicializar con el constructor de la clase derivada. , porque no se puede acceder a la clase derivada.

En la lista de inicialización de la clase derivada, puede llamar al constructor de la clase base para inicializar las variables. La clase derivada siempre llama primero al constructor de la clase base y luego ejecuta otro código. El siguiente código es que el constructor de estudiante llama al constructor de personas para inicializar.

Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }

Cabe señalar que el constructor no se heredará, por lo que el constructor de la clase base solo se puede colocar en el encabezado de la función, no en el cuerpo de la función, y no se puede usar como una función ordinaria. Aquí, el encabezado de la función es una llamada al constructor, no una declaración, por lo que lo que se pasa aquí es el parámetro real, por lo que lo que se pasa aquí no solo pueden ser los parámetros en la lista de parámetros del constructor de la clase derivada, sino también los locales. variables, constantes, etc.

El orden en que se llaman los constructores es de arriba hacia abajo, de acuerdo con la jerarquía de herencia, desde la clase base hasta la clase derivada. Las clases derivadas solo pueden llamar a constructores de clases base directas, no a clases base indirectas.

Al crear un objeto a partir de una clase derivada, se debe llamar al constructor de la clase base.

Destructores para clases base y derivadas

El destructor tampoco se puede heredar.En el destructor de la clase derivada, no es necesario llamar explícitamente al destructor de la clase base, porque cada clase tiene un destructor y el compilador sabe cómo elegir.Los destructores se ejecutan en el orden opuesto a los constructores.

herencia múltiple

Una clase derivada con múltiples clases base se llama herencia múltiple . La herencia múltiple puede complicar fácilmente la lógica del código, por lo que la herencia múltiple se cancela en Java, C# y PHP.

class D: public A, private B, protected C {
    //类D新增加的成员
}

El orden en que se llaman los constructores de la clase base no tiene nada que ver con el orden en que aparecen en los constructores de la clase derivada, sino con el orden en que aparecen las clases base cuando se declara la clase derivada.

Cuando hay miembros con el mismo nombre en varias clases base, el acceso directo provocará conflictos de nombres y el compilador no sabe qué miembro de la clase base usar. En este caso, se deben agregar el nombre de la clase y el solucionador de dominio .::

Modelo de memoria de objetos en herencia múltiple

Los objetos de la clase base se enumeran en el mismo orden en que se declararon cuando se heredaron.

Acceda a miembros privados con punteros

C ++ no permite el acceso a miembros privados a través de objetos, ya sea a través de variables de objeto o punteros de objeto. Sin embargo, esta restricción está en el nivel gramatical y aún se puede acceder a las variables miembro privadas mediante punteros.

usar compensación

Existe una cierta distancia entre la variable miembro y el comienzo del objeto. Siempre que conozca el desplazamiento de la variable miembro desde el encabezado del objeto, puede acceder a él a través del puntero. Entonces, si conoce la dirección inicial del objeto y luego agrega el desplazamiento de la variable, puede conocer la dirección de la variable, y si conoce la dirección y el tipo de la variable miembro, puede conocer su valor.

De hecho, al acceder a variables miembro a través de punteros de objeto, el compilador obtiene valores de esta manera, como se muestra en el siguiente código.

class A{
public:
    A(int a, int b, int c);
public:
    int m_a;
    int m_b;
    int m_c;
};

int b = p->m_b;
//上一句在编译器内部会转换为下面这样
int b = *(int*)( (int)p + sizeof(int) );
//其中p 是对象 obj 的指针,(int)p将指针转换为一个整数,这样才能进行加法运算;sizeof(int)用来计算 m_b 的偏移;(int)p + sizeof(int)得到的就是 m_b 的地址,不过因为此时是int类型,所以还需要强制转换为int *类型;开头的*用来获取地址上的数据

//如果使用对象指针访问变量m_a的话如下
int a = p -> m_a;
//在编译器中转换为
int a = * (int*) ( (int)p + 0 );

Romper las restricciones de los derechos de acceso requiere que realicemos la conversión manualmente, en lugar de utilizar el compilador para la conversión automática. Siempre que el desplazamiento se pueda calcular correctamente.

El siguiente código es el desplazamiento del puntero para acceder a la variable privada.

#include <iostream>
using namespace std;
class A{
public:
    A(int a, int b, int c);
private:
    int m_a;
    int m_b;
    int m_c;
};
A::A(int a, int b, int c): m_a(a), m_b(b), m_c(c){ }
int main(){
    A obj(10, 20, 30);
    int a1 = *(int*)&obj;
    int b = *(int*)( (int)&obj + sizeof(int) );
    A *p = new A(40, 50, 60);
    int a2 = *(int*)p;
    int c = *(int*)( (int)p + sizeof(int)*2 );
   
    cout<<"a1="<<a1<<", a2="<<a2<<", b="<<b<<", c="<<c<<endl;
    return 0;
}

El resultado de la operación es que se puede ver que C++ no permite el acceso a variables privadas solo a nivel gramatical, lo que significa que los derechos de acceso solo funcionan a1=10, a2=40, b=20, c=60en la suma de los operadores miembros . Pero no hay forma de impedir el acceso directo mediante punteros..->

Herencia virtual y clases base virtuales

Para resolver el problema de los conflictos de nombres y los datos redundantes en herencia múltiple, elherencia virtual, de modo que solo se reserva un miembro de la clase base indirecta en la clase derivada. Agregar una palabra clave delante del método de herencia virtuales herencia virtual.

class B: virtual public A{  //虚继承
 //todo
};

El propósito de la herencia virtual es permitir que una clase haga una declaración de que está dispuesta a compartir su clase base. Las clases base compartidas se denominan clases base virtuales . No importa cuántas veces aparezca la clase base virtual en el sistema de herencia, solo se incluye un miembro de la clase base virtual en la clase derivada.

Por lo tanto, la operación de derivación virtual debe completarse antes de que aparezca la demanda real de derivación virtual. La derivación virtual solo afecta a las clases que se derivan de la clase derivada de la clase base virtual y no afecta a la clase derivada en sí.

Visibilidad de los miembros virtuales

Solo un miembro de la clase base virtual se guarda en la clase derivada final de herencia virtual, por lo que se puede acceder directamente a este miembro sin ambigüedad. Si un miembro de una clase base virtual está cubierto por una sola ruta de derivación, aún se puede acceder al miembro cubierto. Si el miembro está cubierto por dos o más rutas, no se podrá acceder directamente a él durante mucho tiempo y es necesario indicar a qué clase pertenece el miembro.

Se desaconseja el uso de herencia múltiple .

Constructores con herencia virtual

En la herencia virtual, la clase base virtual es inicializada por la clase derivada final, por lo que el constructor de la clase derivada final debe llamar al constructor de la clase base virtual. Para la clase derivada definitiva, la clase base virtual es una clase base indirecta, no una clase base directa. En herencia normal, el constructor de la clase derivada solo puede llamar al constructor de la clase base directa, pero no a la clase base indirecta.

El orden de ejecución del constructor de herencia virtual es diferente del de la herencia ordinaria. En la lista de llamadas al constructor de la clase derivada final, independientemente del orden en que aparecen los constructores, el compilador siempre llama primero al constructor de la clase base virtual. y luego llama a otros constructores en orden Constructor. En herencia normal, el orden en que se llaman los constructores está de acuerdo con el orden en que aparecen las clases base cuando se declara la clase.

Modelo de memoria cuando la herencia virtual

Para la herencia ordinaria, el objeto de clase base siempre se ubica frente al objeto de clase derivada; no importa cuán profunda sea la jerarquía de herencia, su desplazamiento con respecto a la parte superior del objeto de clase derivada es fijo.

Para la herencia virtual, a diferencia de la herencia ordinaria, la mayoría de los compiladores colocarán las variables miembro de la clase base detrás de las variables miembro de la clase derivada, de modo que a medida que aumenta el nivel de herencia, el desplazamiento de las variables miembro de la clase base cambiará, de modo que Ya no es Es posible utilizar compensaciones fijas para acceder a los miembros de la clase base.

La herencia virtual divide la clase derivada en una parte fija y una parte compartida , y coloca la parte compartida al final.

Asignar clase derivada a clase base (upcast)

Una clase también es una estructura de datos y puede ocurrir una conversión de tipo de datos, pero esta conversión solo es significativa entre la clase base y la clase derivada , y solo la clase derivada se puede asignar a la clase base , incluida laAsignación de objeto de clase derivada al objeto de clase base,VoluntadAsignación de puntero de clase derivada al puntero de clase base,VoluntadAsignación de referencia de clase derivada a referencia de clase base, que en C++ se llamaTransformación ascendente. Por supuesto, asignar la clase base a la clase derivada se llamaTransformación a la baja

La conversión ascendente es muy segura y el compilador puede realizarla automáticamente; la conversión ascendente es riesgosa y requiere intervención manual.

La esencia de la asignación es escribir los datos existentes en la memoria asignada. La memoria del objeto solo contiene variables miembro, por lo que la asignación entre objetos es la asignación de variables miembro y no hay problemas de asignación en las funciones miembro .

La asignación de un objeto de clase derivada a un objeto de clase base descartará los nuevos miembros de la clase derivada. Esta relación de conversión es irreversible. Solo los objetos de clase derivada se pueden usar para asignar valores a los objetos de clase base, y los objetos de clase base no se pueden asignar. a objetos de clase derivados. Porque el objeto de clase base no contiene los miembros recién agregados en el objeto de clase derivada.

Asignar puntero de clase derivada al puntero de clase base

Se puede asignar un puntero de clase derivada a un puntero de clase base. A diferencia de la asignación de variables de objeto, la asignación de punteros de objeto no necesita copiar las variables miembro del objeto, ni modifica los datos del objeto, solo cambia la punta del puntero.

Acceda a miembros de clase derivados a través de punteros de clase base

Cuando el puntero de clase derivada se asigna al puntero de clase base, solo las variables miembro de la clase derivada se pueden usar a través del puntero de clase base, y las funciones miembro de la clase derivada no se pueden usar. Las funciones miembro utilizadas siguen siendo de la clase base .

Esto se debe a que después de que el puntero de la clase derivada se asigna al puntero de la clase base, el puntero de la clase base apunta al objeto de la clase derivada, de modo que este puntero cambia y apunta al objeto de la clase derivada . , por lo que la variable miembro a la que se accede es la categoría derivada. Pero el compilador accede a las variables miembro a través de punteros de clase base, pero no a través de punteros a funciones miembro.El compilador accede a las funciones miembro a través del tipo de puntero.. Entonces, incluso si el puntero de la clase derivada se asigna al puntero de la clase base, el tipo del puntero de la clase base no cambia.

Entonces en resumen:El compilador accede a las variables miembro a través de punteros, y los datos de ese objeto se usan cuando el puntero apunta al objeto; el compilador accede a las funciones miembro a través del tipo de puntero, y la variable miembro de la clase se usa para el tipo de clase a la que pertenece el puntero

Asignar referencia de clase derivada a referencia de clase base

Las referencias se implementan esencialmente mediante punteros, por lo que los efectos de los punteros y las referencias son los mismos.

Asignar puntero de clase derivada al procedimiento de puntero de clase base

Después de asignar el puntero de clase derivada al puntero de clase base, encontrará que sus valores pueden ser iguales o no.

El compilador puede realizar algún procesamiento en el valor existente antes de la asignación, como asignar datos de tipo doble a una variable de tipo int, el compilador borrará la parte decimal. Lo mismo ocurre cuando se asigna un puntero de una clase derivada a un puntero de una clase base. El compilador puede realizar algún procesamiento en el valor antes de la asignación.

Polimorfismo y funciones virtuales.

concepto

Porque el puntero de la clase base solo puede acceder a las variables miembro de la clase derivada, pero no puede acceder a las funciones miembro de la clase derivada. C++ agregadofunción virtual, para permitir que el puntero de la clase base acceda a la función miembro de la clase derivada , y la función virtual solo necesita agregar palabras clave delante de la declaración de la función virtual.

Al usar funciones virtuales, los miembros de la clase base se usan cuando el puntero de la clase base apunta al objeto de la clase base, es decir, se pueden usar variables miembro y funciones miembro. Cuando se apunta a una clase derivada, se utiliza un miembro de la clase derivada. Entonces, el puntero de la clase base puede hacer cosas a la manera de la clase base, y también puede hacer cosas a la manera de la clase derivada. Hay muchas formas o formas de expresión. Este fenómeno se llamapolimorfismo

La única función de las funciones virtuales es formar polimorfismo .

El propósito de proporcionar polimorfismo es: a través del puntero de clase base, se puede acceder a las variables miembro y funciones miembro de la clase derivada (directamente derivadas e indirectamente derivadas) en todas las direcciones. Si no hay polimorfismo, solo se puede acceder a las variables miembro de las clases derivadas. La función virtual se llama según el punto del puntero y la función de qué clase se llama cuando el puntero apunta al objeto de la clase .

Las referencias son esencialmente punteros, por lo que también pueden lograr polimorfismo, pero debido a que las referencias no se pueden cambiar después de la inicialización, carecen de expresividad en términos de polimorfismo.

Para programas grandes y medianos con relaciones de herencia complejas, el polimorfismo puede aumentar su flexibilidad y hacer que el código sea más expresivo. Si hay muchas clases derivadas en un programa, es necesario definir varios punteros si no se usa polimorfismo, pero si se usa polimorfismo, solo se necesita una variable de puntero para llamar a las funciones virtuales de todas las clases derivadas.

función virtual

función virtualSolo necesita agregar la palabra clave cuando se declara la funciónvirtual , y se puede agregar o no cuando se define la función. Solo puede declarar las funciones en la clase base como funciones virtuales, de modo que todas las funciones con el mismo nombre en la clase derivada que tengan una relación de sombra se convertirán automáticamente en funciones virtuales.

Cuando se define una función virtual en la clase base, si ninguna clase derivada define una nueva función virtual para seguir esta función, entonces se utilizará la función virtual de la clase base. Solo cuando la función virtual de la clase derivada anula la función virtual de la clase base se puede formar polimorfismo, es decir, se accede a la función de la clase derivada a través del puntero de la clase base.

El constructor no puede ser virtual.

Los destructores pueden, y en ocasiones deben, declararse virtuales.

Encapsulación, herencia y polimorfismo: las tres características principales de la orientación a objetos

condiciones que constituyen polimorfismo

  1. Debe existir una relación de herencia.
  2. Debe haber una función virtual con el mismo nombre en la relación de herencia y debe haber una relación de superposición entre ellas.
  3. Hay un puntero a la clase base y la función virtual se llama a través del puntero.
  4. El puntero de la clase base solo puede acceder a los miembros heredados de la clase base y no puede acceder a los miembros recién agregados de la clase derivada.
  5. Si una función miembro de una clase quiere cambiar su función después de la herencia, generalmente se declara como una función virtual.

destructor virtual

Los constructores no pueden ser virtuales porque:

  1. Las clases derivadas no pueden heredar el constructor de la clase base y no tiene sentido declarar el constructor como una función virtual.
  2. El constructor en C ++ se utiliza para inicializar el objeto cuando se crea. El objeto no se ha creado antes de que se ejecute el constructor y la tabla de funciones virtuales no existe.

Los destructores generalmente se declaran virtuales por defecto :

Cuando el destructor se escribe de forma predeterminada, si no se define como una función virtual, es fácil provocar una pérdida de memoria. Porque al eliminar el puntero del objeto, si es una clase derivada a la que apunta el puntero de la clase base, al eliminar la clase derivada, se llamará al destructor. Porque al llamar a una función miembro a través de un puntero, el compilador llamará a la función miembro según el tipo de puntero en lugar del objeto al que apunta el puntero . Entonces esto hará que se llame al destructor de la clase base en lugar del destructor de la clase derivada. Si la memoria se aplica en el constructor de la clase derivada, no se liberará, lo que provocará una pérdida de memoria.

El destructor de la clase derivada siempre llama al destructor de la clase base. Declarar el destructor de la clase base como una función virtual automáticamente convierte al destructor de la clase derivada en virtual. En este momento, el compilador no seleccionará la función miembro según el tipo de puntero, sino según el objeto señalado por el puntero.

En la mayoría de los casos, el destructor de la clase base se declara como una función virtual .

Funciones virtuales puras y clases abstractas.

Una función virtual se puede declarar comofunción virtual pura:

virtual 返回值类型 函数名 (函数参数) = 0;

Una función virtual pura no tiene cuerpo de función, solo una declaración de función, que se agrega al final de la declaración de función virtual =0para indicar que es una función virtual pura.

最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。

Una clase que contiene funciones virtuales puras se llamaclase abstracta, la razón por la que se llama clase abstracta es porqueno se puede instanciar, eso esNo se pudo crear el objeto. Una función virtual pura no tiene cuerpo de función, no es una función completa, no se puede llamar y no se le puede asignar espacio de memoria.

Las clases abstractas generalmente se usan como clases base para permitir que las clases derivadas implementen funciones virtuales puras , y las clases derivadas deben implementar funciones virtuales puras antes de que se puedan crear instancias . Defina una clase base abstracta, que solo completa parte de las funciones, y las funciones no terminadas se entregan a clases derivadas para su implementación. Estas funciones a menudo no son requeridas por la clase base o no se pueden implementar en la clase base.

Aunque la función virtual pura declarada por la clase abstracta no está implementada, es obligatoria que la clase derivada la implemente; de ​​lo contrario, no se puede completar la creación de instancias .

Además de restringir las funciones de las clases derivadas, las clases abstractas también pueden implementar polimorfismo. Porque si no hay una declaración en la clase base, se producirá un error al usar el puntero de la clase base para llamar a la función miembro en la clase derivada.

Sólo las funciones virtuales pueden declararse puramente virtuales.

Mecanismo de implementación polimórfico

Si la función es una función virtual y está cubierta por una función del mismo nombre de una clase derivada, el compilador encontrará la función según el puntero, es decir, la función de esa clase será llamada cuando el objeto A qué clase apunta el puntero pertenece: esto es polimorfismo .

La razón por la que el compilador puede encontrar la función virtual a través del objeto señalado por el puntero es porque un adicionaltabla de funciones virtuales

Si una clase contiene una función virtual, se agregará una matriz adicional al crear un objeto de esta clase, y cada elemento de la matriz es la dirección de entrada de la función virtual. Sin embargo, la matriz y el objeto se almacenan por separado. Para asociar el objeto con la matriz, el compilador también necesita insertar un puntero en el objeto, que apunte a la dirección inicial de la matriz. Esta matriz es la tabla de funciones virtuales, abreviada como vtable .

Hay un puntero al comienzo del objeto, que apunta a la tabla de funciones virtuales, y este puntero siempre está ubicado al comienzo del objeto .

En la tabla de funciones virtuales, el subíndice de la función virtual de la clase base en la vtable es fijo y no cambiará con el aumento del nivel de herencia.La nueva función virtual de la clase derivada se coloca al final de la vtable. Si hay Si la función virtual del mismo nombre oculta la función virtual de la clase base, entonces la función virtual de la clase derivada se utilizará para reemplazar la función virtual de la clase base. Dicha función virtual con una relación de sombra solo aparece una vez en la vtable .

Al llamar a una función virtual a través de un puntero, primero busque la dirección de entrada de la función virtual de acuerdo con el puntero vfptr.

p -> display()
( *( *(p+0) + 0 ) )(p);//上面的调用在编译器中会转换成下面这样

Se puede encontrar que la expresión convertida es fija. Siempre que se llame a la función virtual, esta expresión se usará sin importar en qué clase se encuentre el valor. En otras palabras, al compilador no le importa hacia dónde apunta el puntero de la clase base. Convierta siempre a la misma expresión.

La expresión convertida no utiliza información relacionada con el tipo de p, y la función se puede llamar siempre que se conozca el puntero, lo cual es fundamentalmente diferente del algoritmo de codificación de nombres.

operador typeid

typeid se utiliza para obtener la información de tipo de una expresión. Para los tipos de datos básicos, la información de tipo es relativamente simple y se refiere principalmente al tipo de datos. Para datos de tipo clase, es decir, objetos, la información de tipo se refiere a la clase a la que pertenece el objeto, los miembros que contiene, la relación de herencia en la que se encuentra, etc.

La información de tipo es la plantilla para crear datos. La cantidad de memoria que ocupan los datos y las operaciones que se pueden realizar están determinadas por la información de tipo. El objeto de operación de typeid puede ser una expresión o un tipo de datos .

typeid( dataType )
typeid( expression )

typeid debe estar entre paréntesis, y typeid guardará la información de tipo obtenida en un objeto de tipo type_info , ydevuelve una referencia constante al objeto. Varias funciones miembro de la clase type_info devuelta :

  • name()El nombre utilizado para devolver el tipo.
  • raw_name()Se utiliza para devolver el nuevo nombre generado por el algoritmo de codificación de nombres.
  • hash_code()Se utiliza para devolver el valor hash correspondiente al tipo actual.

El estándar C++ estipula que la clase type_info debe tener al menos las siguientes cuatro funciones públicas:

  1. const char* name() const;: Devuelve una cadena que representa el nombre del tipo.
  2. bool before (const type_info& rhs) const;: Para determinar si un tipo está delante de otro tipo, el parámetro rhs es una referencia a un objeto type_info.
  3. bool operator== (const type_info& rhs) const;: Operador sobrecargado ==para determinar si dos tipos son iguales, el parámetro rhs es una referencia a un objeto type_info.
  4. bool operator!= (const type_info& rhs) const;: Operador sobrecargado !=para determinar si dos tipos son diferentes, el parámetro rhs es una referencia a un objeto type_info.

El operador typeid se utiliza a menudo para determinar si dos tipos son iguales.No importa cuántas veces se use un tipo, el compilador solo creará un objeto para él y todos los typeids devolverán una referencia a este objeto.

Pero para reducir el tamaño del archivo compilado, el compilador no creará objetos type_info para todos los tipos, solo para los tipos que usan el operador typeid. Pero para las clases con funciones virtuales, independientemente de si se utiliza el operador typeid, el compilador creará un objeto type_info para la clase con funciones virtuales.

mecanismo de reconocimiento de tipo

Generalmente, el tipo de expresión se puede determinar durante la compilación, pero cuando hay polimorfismo, el tipo de algunas expresiones no se puede determinar durante la compilación y debe determinarse de acuerdo con el entorno real después de que se ejecuta el programa.

modelo de memoria de objetos:

  • Si no hay funciones virtuales ni herencia virtual, entonces solo hay variables miembro en el modelo de memoria de objetos.
  • Si la clase contiene funciones virtuales , se agregará una tabla de funciones virtuales adicional y se insertará un puntero en la memoria del objeto para apuntar a esta tabla de funciones virtuales. Al mismo tiempo, si la memoria de objetos de esta clase también agregará información de tipo adicional, es decir, el objeto type_info.
  • Si la clase contiene herencia virtual , se agregará una tabla de clases base virtual adicional y se insertará un puntero en la memoria del objeto para apuntar a esta tabla de clases base virtual.

Si la clase contiene funciones virtuales, el compilador no solo creará una tabla de funciones virtuales al comienzo del objeto, sino que también insertará un puntero al comienzo de la tabla de funciones virtuales, apuntando al objeto type_info correspondiente en el actual. Cuando el programa actual obtiene la información de tipo en la etapa de ejecución, puede encontrar el puntero de la tabla de funciones virtuales a través del puntero del objeto, y luego encontrar el puntero del objeto type_info a través del puntero de la tabla de funciones virtuales, y luego obtener la información de tipo.

El compilador no puede determinar a qué objeto apunta el puntero de la clase base durante la fase de compilación, por lo que no puede obtener la información de tipo del objeto señalado por el puntero, pero el compilador puede hacer varios preparativos durante la fase de compilación, para que el programa pueda utilice estos datos preparados después de ejecutar para obtener información de tipo, que incluye:

  • Cree un objeto type_info e inserte un puntero al objeto type_info al principio de la tabla de funciones virtuales.
  • Transforme las operaciones que obtienen información de tipo en declaraciones similares a punteros. **(p->vfptr - 1).

Este mecanismo para determinar el tipo de información de un objeto después de que se ejecuta el programa se llamareconocimiento de tipo de tiempo de ejecución(RTTI). En C++, el mecanismo RTTI solo se usa cuando se incluyen funciones virtuales y la información de tipo se puede determinar en la etapa de compilación en todos los demás casos.

El polimorfismo es una característica importante de la programación orientada a objetos, que aumenta en gran medida la flexibilidad del programa, pero el costo de admitir el polimorfismo también es muy alto. Parte de la información no se puede determinar de antemano en la etapa de compilación, lo que consumirá más memoria y CPU. recursos.

Lo que la CPU necesita para acceder a la memoria es la dirección, y se llama a la operación de hacer coincidir el nombre de la variable, el nombre de la función y la dirección.enlace de símbolo. En circunstancias normales, la dirección correspondiente al nombre de la función se puede encontrar durante la compilación, se completa la vinculación de la función y el programa se puede usar directamente después de ejecutarse.enlace estático. A veces no es posible determinar qué función llamar durante la compilación y debe decidirse de acuerdo con el entorno u operación específica después de que se ejecute el programa.enlace dinámico

lenguaje estático: El tipo se especifica explícitamente en el momento de la definición y no se puede cambiar después de especificar el tipo, por lo que el compilador puede conocer el tipo de la mayoría de las expresiones durante la compilación. Este lenguaje se denomina lenguaje estático.

lenguaje dinámico: No es necesario especificar el tipo al definir una variable, y el tipo de la variable se puede cambiar en cualquier momento. El compilador no puede determinar la información del tipo de la expresión durante la compilación. Solo se puede obtener después de que se esté ejecutando el programa. Se llama lenguaje dinámico.

Para ser flexibles y fáciles de implementar, los lenguajes dinámicos generalmente se compilan y ejecutan al mismo tiempo, desdibujando el proceso tradicional de compilación y ejecución.

sobrecarga del operador

concepto

sobrecarga del operadorEs para permitir que un mismo operador tenga diferentes funciones. En aplicaciones prácticas, +la operación de suma de diferentes tipos de datos <<se puede utilizar como operador de turno o como símbolo de cout a salida, estos son sobrecargas de operadores.

La sobrecarga del operador consiste en definir una función e implementar la función deseada dentro del cuerpo de la función. Cuando se necesita un operador, el compilador llamará automáticamente a esta función. entoncesLa sobrecarga del operador se logra mediante funciones., que es esencialmente una sobrecarga de funciones. El formato de sobrecarga de operadores es:

返回值类型 operator 运算符名称(形参列表){
    //TO DO
}
operator是关键字,专门用于定义重载运算符的函数。

Las funciones de sobrecarga de operadores no son diferentes de las funciones ordinarias excepto que el nombre de la función tiene un formato específico . Las funciones de sobrecarga de operadores se pueden utilizar no solo como funciones miembro de clases, sino también como funciones globales.

  1. No todos los operadores se pueden sobrecargar , los operadores de longitud sizeof, los operadores condicionales :?, los operadores de miembros .y los operadores de resolución de dominio ::no se pueden sobrecargar.
  2. La sobrecarga no puede cambiar la precedencia y asociatividad de los operadores.
  3. La sobrecarga no cambia el uso del operador , cuántos operandos hay, si los operandos están a la izquierda o a la derecha, estos no cambian.
  4. Las funciones de sobrecarga de operadores no pueden tener parámetros predeterminados ; de lo contrario, se cambiará el número de operandos del operador.
  5. Las funciones de sobrecarga de operadores se pueden utilizar como funciones miembro de una clase o como funciones globales.
  6. Los operadores de flecha ->, operadores de subíndice [], operadores de llamada a función ()y operadores de asignación =solo se pueden sobrecargar como funciones miembro.

Cuando la función de sobrecarga del operador se utiliza como función miembro de una clase, el operador binario tiene solo un parámetro y el operador unario no necesita ningún parámetro. Porque cuando se usa como función miembro de una clase, un parámetro está implícito. Por ejemplo, cuando está sobrecargado +, se convertirá para acceder implícitamente a la variable miembro de c1 a través del puntero this c3 = c1 + c2durante la compilación .c3 = c1.operator+(c2)

Cuando la función de sobrecarga del operador se usa como una función global, el operador binario necesita dos parámetros, y el operador unario necesita un parámetro, y uno de los parámetros debe ser un objeto, para que el compilador sepa que este es un operador definido por programador para evitar Modificar los operadores utilizados para los tipos integrados.

Cuando una función sobrecargada de operador se utiliza como función global, generalmente es necesario declarar la función como función amiga en la clase.

sobrecarga <<y>>

Los operadores de desplazamiento a la izquierda y a la derecha se han sobrecargado en la biblioteca estándar para que puedan usarse para diferentes entradas y salidas de datos. Pero los objetos de entrada y salida solo pueden ser tipos de datos integrados en C++ y tipos de datos contenidos en la biblioteca estándar. Si define un tipo de datos usted mismo, deberá sobrecargarlo usted mismo si desea utilizar <<y >>.

cout es un objeto de la clase ostream y cin es un objeto de la clase istream . Si desea sobrecargar <<y , >>debe sobrecargar y en forma de función global , de lo contrario debe modificar la clase en la biblioteca estándar.<<>>

Sobrecarga del operador de entrada >>:

istream & operator>>(istream &in, complex &A){
	in >> A.m_real >> A.m_imag;
  return in;
}

Entre ellos, istream representa el flujo de entrada y cin es un objeto de la clase istream, pero este objeto está definido en la biblioteca estándar.El propósito de devolver referencias es poder leer números complejos continuamente, si no devuelve referencias, solo podrá leerlas una por una.. Y la función de sobrecarga del operador utiliza la variable privada en el complejo, que debe declararse como una función amiga en el complejo.

Operador de salida sobrecargado <<:

ostream & operator<<(ostream &out, complex &A){
	out << A.m_real << " + " << A.m_imag << "i";
  return out;
}

ostream representa el flujo de salida y cout es un objeto de la clase ostream.

sobrecargado[]`

C ++ estipula que el operador de subíndice debe sobrecargarse en forma de función miembro .

1.返回值类型 & operator[](参数);
或者是:
2.const 返回值类型 & operator[](参数) const;

Con la primera declaración, []no sólo puedes acceder a los elementos, sino también modificarlos.

Con el segundo método, []solo se puede acceder al elemento y no se puede modificar. En aplicaciones prácticas, debemos proporcionar dos formas para adaptarnos a los objetos constantes, porque solo se pueden llamar funciones miembro constantes a través de objetos constantes, y si no se proporciona la segunda forma, no se puede acceder a ningún elemento de los objetos constantes .

sobrecarga ++y--

Los operadores de autoincremento ++y autodecremento son operadores unarios y tanto sus formularios previos como posteriores pueden sobrecargarse.

1.返回值类型 operator++(); //++i,前置形式
2.返回值类型 operator++(int i); //i++,后置形式

operator++();la función implementa el incremento automáticoforma frontal, devuelve el resultado de la ejecución por sí solo.

operator++(int i);la función implementa el incremento automáticoformulario de publicación, el valor de retorno es el objeto en sí, pero cuando el objeto se usa nuevamente más tarde, el objeto se incrementa automáticamente, por lo que en el cuerpo de la función, primero guarde el objeto, luego llame a la función run () una vez y luego guarde el primer objeto guardado devuelto. En esta función el parámetro i no tiene significado, solo distingue si es una preposición o una posposición.

sobrecarga newydelete

Los operadores de memoria new, new[], también se pueden sobrecargar, y la forma sobrecargada puede ser una función miembro o una funcióndelete[] global .delete

Sobrecargar nuevo como función miembro:

void* classname::operator new(size_t size){
	//todo;
}

Sobrecargar nuevo como función global:

void* operator new(size_t size){
	//todo;
}

Al sobrecargar new y new[], ya sea como función miembro o función global,El primer parámetro debe ser de tipo size_t. size_t representa el tamaño del espacio a asignar.Para la función sobrecargada de new[], size_t representa la suma de todos los espacios que deben asignarse

size_t 在头文件<cstdio>中被定义为 typedef unsigned int size_t;就是无符号整形

Por supuesto, la función sobrecargada puede tener otros parámetros, pero todos deben tener un valor predeterminado y el tipo del primer parámetro debe ser size_t.

Sobrecargar eliminar como función miembro:

void* classname::operator delete(void *ptr){
	//todo;
}

Eliminar sobrecarga como función global:

void* operator delete(void *ptr){
	//todo;
}

Sobrecargar al operador de reparto()

En C++, el nombre del tipo (incluido el nombre de la clase) en sí también es un operador, es decir, el operador de conversión de tipo .

El operador de coerción de tipo es un operador unario y también se puede sobrecargar, pero solo se puede sobrecargar como una función miembro , no como una función global. Después de la sobrecarga, (类型名)对象la expresión que genera el objeto es equivalente a 对象.operator 类型名(), es decir, se convierte en una llamada a la función del operador.

Al sobrecargar el operador de conversión, no es necesario especificar el tipo de valor de retorno, porque se determina el tipo de valor de retorno, que es el tipo representado por el propio operador .

Precauciones

  • El significado del operador después de la sobrecarga debe ajustarse a los hábitos de uso originales.
  • sobrecarga del operadorNo cambia la precedencia del operador
  • .operadores , .*, ::, ?:, sizeofetc.no se puede sobrecargar
  • Al sobrecargar operadores (), [], ->,=Sólo se puede sobrecargar como función miembro., no se puede sobrecargar como una función global.
  • La sobrecarga del operador en realidad consiste en sobrecargar el operador en una función, y la expresión que utiliza el operador se interpreta como una llamada a la función sobrecargada.
  • Un operador se puede sobrecargar como una función global. En este momento, el número de parámetros de la función es el número de operandos del operador, y los operandos del operador se convierten en los parámetros reales de la función.
  • Un operador se puede sobrecargar como función miembro. En este momento, el número de parámetros de la función es el operando del operador menos uno. Uno de los operandos del operador se convierte en el objeto de acción y el resto se convierte en los parámetros reales del función.
  • El nombre del tipo se puede utilizar como operador de conversión o se puede sobrecargar como una función miembro de la clase, de modo que el objeto se convierta automáticamente a un determinado tipo.
  • Los operadores de autoincremento y autodecremento tienen cada uno dos modos de sobrecarga, que se utilizan para distinguir entre uso previo y posterior.

plantilla

concepto

En C++, el tipo de datos también se puede pasar a través de parámetros. Cuando se define la función, no se puede nombrar el tipo de datos específico. Cuando se llama a la función, el compilador puede inferir automáticamente el tipo de datos de acuerdo con los parámetros reales pasados. . Esto esparametrización de tipo. El valor y el tipo son las dos características principales de los datos, y ambas se pueden parametrizar en C++ .

plantilla de funciónes crear unfunción universal, los tipos de datos que se utilizarán (incluido el tipo de valor de retorno, el tipo de parámetro formal y el tipo de variable local) no se pueden especificar específicamente, sino que se utiliza un tipo virtual y, cuando se produce una llamada de función, se invierte de acuerdo con el parámetro real pasado Muestra el tipo real.

Una vez definida una plantilla de función, los parámetros de tipo se pueden utilizar en definiciones y declaraciones de funciones. Cuando originalmente se usaban tipos integrados como int, float, etc. char, en su lugar se pueden usar parámetros de función.

plantillaEs una palabra clave para definir una plantilla de función , seguida de corchetes angulares <>, y los corchetes angulares rodean los parámetros de tipo. typename es otra palabra clave utilizada para declarar parámetros de tipo específicos, template<typename T>llamadaencabezado de plantilla

template <typename 类型参数1, typename 类型参数2, ...> 返回值类型 函数名(形参列表){
    //在函数体中可以使用类型参数
}

Puede haber muchos parámetros de tipo, pero deben estar separados por comas, los parámetros de tipo están rodeados por <>y los parámetros formales están ()rodeados por. La palabra clave typename se puede reemplazar con la palabra clave class , porque las primeras versiones de C ++ no introdujeron nuevas palabras clave, sino que usaron la palabra clave class para indicar el parámetro de tipo.

Las plantillas de funciones se pueden declarar con anticipación, pero la declaración debe ir acompañada de un encabezado de plantilla, y el encabezado de la plantilla y la definición de función (declaración) son un todo inseparable , que se puede encapsular, pero no se pueden agregar puntos y comas.

Además de las plantillas de funciones, también se admiten plantillas de clases. Los parámetros de tipo definidos en plantillas de funciones se pueden usar en declaraciones de funciones y definiciones de funciones, y los parámetros de tipo definidos en plantillas de clases se pueden usar en declaraciones de clases e implementaciones de clases. El propósito de las plantillas de clases también es parametrizar tipos de datos.

template <typename 类型参数1, typename 类型参数2,...> class 类名{
  //todo
};

Las plantillas de clase son las mismas que las plantillas de funciones, los parámetros de tipo no pueden estar vacíos y varios parámetros de tipo están separados por comas.

Una vez que se declara una plantilla de clase, los parámetros de tipo se pueden usar en las funciones miembro y las variables miembro de la clase. Además de agregar un encabezado de plantilla a la declaración de clase, también debe agregar un encabezado de plantilla al definir funciones miembro fuera de la clase.

template <typename 类型参数1, typename 类型参数2, ...>
返回值类型 类名<类型参数1, 类型参数2, ...>::函数名(形参列表){
//todo
}
  • Nota: Además de la necesidad de especificar el parámetro de tipo después de la palabra clave de la plantilla, el parámetro de tipo también es necesario después del punto del nombre de la clase, pero no se agrega la palabra clave de nombre de tipo.

A diferencia de las plantillas de funciones, las plantillas de clases deben especificar el tipo de datos al crear instancias y el compilador no puede inferir el tipo de datos en función de los datos proporcionados. Cuando se utilizan punteros de objetos para la creación de instancias, también es necesario especificar tipos de datos específicos en ambos lados y mantenerlos coherentes.

Los tipos soportados por plantillas son amplios e ilimitados, pudiendo ser reemplazados por cualquier tipo, esta programación se llamaprogramación genérica. El parámetro T puede considerarse como un tipo genérico, e int, char, etc. pueden considerarse como un tipo específico.

C++ permite la sobrecarga de plantillas de funciones.

  • Nota: Los lenguajes de programación se pueden dividir en lenguajes fuertemente tipados y lenguajes débilmente tipados según si el lenguaje necesita indicar explícitamente el tipo de datos al definir variables . Un lenguaje fuertemente tipado necesita especificar el tipo de datos al definir una variable, y una vez que se especifica un determinado tipo de datos para la variable, a la variable no se le pueden asignar otros tipos de datos en el futuro, a menos que se someta a una conversión de tipo obligatoria y una conversión implícita.
int a = 100;  //不转换
a = 12.34;  //隐式转换(直接舍去小数部分,得到12)
a = (int)"http://c.biancheng.net";  //强制转换(得到字符串的地址) 

Un lenguaje débilmente tipado no necesita especificar explícitamente el tipo de datos al definir una variable. El compilador deducirá automáticamente el tipo de acuerdo con los datos asignados a la variable y puede asignar diferentes tipos de datos a la variable y puede asignar diferentes tipos. de datos a la variable.

var a = 100;  //赋给整数
a = 12.34;  //赋给小数
a = "http://c.biancheng.net";  //赋给字符串
a = new Array("JavaScript","React","JSON");  //赋给数组

Ya sea que se trate de un lenguaje fuertemente tipado o débilmente tipado, hay un sistema de tipos dentro del compilador para mantener diversa información sobre las variables.

Para un lenguaje fuertemente tipado, el compilador puede detectar si la operación de la variable es correcta durante la compilación, de modo que no es necesario mantener un conjunto de información de tipo cuando se ejecuta el programa, lo que reduce el uso de memoria y acelera la ejecución del programa. Sin embargo, los lenguajes fuertemente tipados también tienen algunos tipos que no se pueden determinar durante la compilación, como el polimorfismo en C ++. El compilador agregará tablas de funciones virtuales, type_info y otra información auxiliar al modelo de memoria de objetos durante la compilación para mantener una cadena de herencia completa. , espere hasta que el programa se esté ejecutando para determinar qué función llamar. La compilación tiene poca importancia para los lenguajes de tipo débil, porque incluso si se compila, hay muchas cosas que no se pueden determinar.

Los lenguajes de tipo débil generalmente se compilan y ejecutan una vez, por lo que se puede derivar mucha información útil según el contexto. Este tipo de lenguaje que se compila y ejecuta una vez se llama lenguaje interpretado, mientras que el tradicional que se compila primero y luego se ejecuta se le llama lenguaje compilado .

Los lenguajes fuertemente tipados son más rigurosos, se pueden encontrar muchos errores durante la compilación y son adecuados para desarrollar proyectos a gran escala, a nivel de sistema y a nivel industrial; mientras que los lenguajes débilmente tipados son más flexibles y tienen una alta codificación. Eficiencia, fácil implementación y bajos costos de aprendizaje. Demuestre sus habilidades. Además, los IDE para lenguajes fuertemente tipados son generalmente más potentes, con un buen conocimiento del código y una rica información de solicitud, mientras que para los lenguajes débilmente tipados, los códigos generalmente se escriben directamente en el editor.

inferencia argumental

Al crear un objeto usando una plantilla de clase, debe especificar explícitamente los parámetros reales. Por ejemplo, al crear un objeto a continuación, debe especificar el tipo de parámetro real. De esta manera, al compilar, el compilador no necesita inferirlo por sí mismo y puede usarlo directamente.

template<typename T1, typename T2> class Point;
Point<int, int> p1(10, 20);  //在栈上创建对象
Point<char*, char*> *p = new Point<char*, char*>("东京180度", "北纬210度");  //在堆上创建对象

Para las plantillas de funciones, los parámetros reales no se pueden especificar explícitamente al llamar a la función.

//函数声明
template<typename T> void Swap(T &a, T &b);
//函数调用
int n1 = 100, n2 = 200;
Swap(n1, n2);
float f1 = 12.5, f2 = 56.93;
Swap(f1, f2);

El compilador inferirá automáticamente el tipo de T de acuerdo con el tipo de parámetro real . Este proceso de determinar el argumento de la plantilla a través del argumento de la función se llamainferencia de argumentos de plantilla

La inferencia de parámetros reales de una plantilla de función se refiere al proceso de encontrar el tipo específico del parámetro de tipo de acuerdo con el tipo del parámetro real durante el proceso de llamada a la función. Esto funciona en la mayoría de los casos, pero cuando hay muchos parámetros de tipo, habrá be Los tipos individuales no se pueden inferir. En este caso, los parámetros reales deben especificarse explícitamente. En otras palabras, si el compilador no puede inferir todos los tipos en la plantilla basándose en los parámetros reales, se producirá un error de llamada.

El método para especificar parámetros reales para plantillas de funciones y plantillas de clases es el mismo, todos se agregan después del nombre de la función <>, incluido el tipo específico. Los argumentos de plantilla especificados explícitamente se comparan con los argumentos de plantilla correspondientes en orden de izquierda a derecha.

func<int, int>(10);

parámetro sin tipo

Plantilla es una tecnología genérica , el propósito es parametrizar el tipo de datos y mejorar la flexibilidad del lenguaje. Además de admitir parámetros de tipo, las plantillas también admiten parámetros que no son de tipo. Los parámetros que no son de tipo se utilizan para pasar parámetros, no tipos. Al igual que los parámetros formales ordinarios, es necesario especificar tipos específicos. Al llamar a una plantilla de función o crear un objeto a través de una plantilla de clase, los parámetros que no son de tipo serán proporcionados por el usuario o el compilador infiere.

El tipo de un parámetro que no es de tipo no se puede especificar arbitrariamente, solo puede ser un número entero o un puntero a un objeto o función. Cuando el parámetro que no es de tipo es un número entero, el parámetro real que se le pasa o el parámetro real deducido por el compilador debe ser una expresión constante.

Cuando el parámetro que no es de tipo es un puntero, el parámetro real vinculado al puntero debe tener una vida útil estática, por lo que el parámetro real debe almacenarse en el área de datos estáticos del espacio de direcciones virtuales. Las variables locales se encuentran en el área de la pila y los objetos creados dinámicamente se encuentran en el área del montón.

instanciar

Las plantillas no ocupan memoria, la función o clase final generada ocupará memoria,El proceso de generar una función o clase a partir de una plantilla se llama creación de instancias de plantilla., una versión específica de una función o clase generada para un tipo se denomina instancia de plantilla.

Una plantilla puede verse como un conjunto de instrucciones para el compilador, que le indica al compilador que genere el código que queremos.. La creación de instancias de la plantilla se realiza bajo demanda, cualquier tipo que se utilice generará una función o clase para ese tipo y no generará demasiado código por adelantado. El compilador genera una versión específica de la función o clase en función de los parámetros reales pasados ​​al parámetro de tipo, y el mismo tipo se genera solo una vez.

La creación de instancias de plantillas de clases no crea instancias de todas las funciones miembro cuando se crean objetos a través de plantillas de clases. Sólo se creará una instancia cuando realmente se llame. Si una función miembro nunca se llama, nunca se creará una instancia. Por lo tanto, la creación de instancias es vagamente local.

programación de varios archivos de referencia de plantilla

Independientemente de si es una clase o una función, la separación entre declaración y definición es la misma. La definición de la función se coloca en otros archivos. Al final, solo queda un problema por resolver, que es hacer coincidir la llamada a la función con Definición de función (busque la dirección de la definición de función y complétela en la llamada de función), la finalización de este trabajo es el conector.

Pero en las plantillas, no es correcto dividir la declaración y la definición de la plantilla en asuntos separados. Las plantillas no son funciones o clases reales, sino solo un dibujo para generar funciones o clases.

  • La instanciación de la plantilla se realiza bajo demanda , y se generará la función o clase para ese tipo para qué tipo se utiliza, y el código no se generará por adelantado.
  • La creación de instancias de plantillas la realiza el compilador, no el vinculador.
  • Durante el proceso de creación de instancias, necesita conocer los detalles de la plantilla, incluidas las declaraciones y definiciones.

Entonces la plantilla es solo una plantilla, que no ocupa memoria. Al compilar, el compilador genera el tipo de código correspondiente según las necesidades, por lo que la instanciación de la plantilla la realiza el compilador. Si está separada en dos archivos, está vinculado El compilador completa el trabajo de llenado de funciones, lo que puede provocar que no se encuentre la instancia correspondiente durante el enlace. Por lo tanto, la definición y declaración de la plantilla generalmente se colocan en el archivo de encabezado .

Las definiciones y declaraciones se pueden colocar en dos archivos mediante la creación de instancias explícita de la siguiente manera. La plantilla no se agrega después de la función <>y va seguida directamente del prototipo de función, es decir, la plantilla se crea una instancia en una versión específica correspondiente al prototipo de función.

template void Swap(double &a, double &a);

De la misma manera que se crea una instancia explícita de una plantilla de clase, es necesario agregar una clase. Cuando crea una instancia explícita de una plantilla de clase, se crean instancias de todos los miembros de la clase, incluidas las funciones miembro y las variables miembro, a la vez.

Plantillas de clases y herencia

  • Una plantilla de clase se deriva de una plantilla de clase.
  • Una plantilla de clase se deriva de una clase de plantilla.
  • Una plantilla de clase se deriva de una clase normal.
  • Las clases ordinarias se derivan de clases de plantilla.

Plantillas de clase y amigos.

  • Funciones, clases, funciones miembro de clases como amigos de plantillas de clases.
  • Plantillas de funciones como plantillas amigas de la clase.
  • Plantillas de funciones como amigos de clases.
  • Plantillas de clase como amigos de las plantillas de clase.

manejo de excepciones

Los errores de programa se dividen principalmente en tres tipos: errores de sintaxis , errores lógicos y errores de tiempo de ejecución . El mecanismo de excepción es poder detectar errores que ocurren durante el tiempo de ejecución, informar al usuario lo que sucedió y luego finalizar el programa.

La sintaxis para detectar excepciones es:

try{
    //可能抛出异常的语句
}catch(exceptionType variable){
    //处理异常的语句
}

try y catch son palabras clave, seguidas de un bloque de declaración. La variable tipo excepción detrás de la palabra clave catch indica el tipo de excepción que puede manejar catch. La variable variable recibe información de excepción. Cuando el programa lanza una excepción, se creará un dato. Estos datos contienen información de error y el programador puede juzgar la excepción en función de esta información.

Una vez que se detecta la excepción, se lanzará inmediatamente, se detectará mediante try inmediatamente y la declaración posterior a la excepción no se ejecutará. Es decir, cuando se detecta una excepción, saltará a la posición de captura y la declaración después del punto de excepción no se ejecutará nuevamente. La palabra clave throw se utiliza para generar una excepción, que será detectada por try y luego capturada por catch.

El tipo de excepción puede ser un tipo básico o un tipo agregado. Las excepciones lanzadas por el propio C ++ o la biblioteca estándar son excepciones de la clase de excepción. Entonces, cuando se lanza una excepción, se creará un objeto de la clase de excepción o su subclase.

Las excepciones se generan durante la fase de ejecución. Pueden ser de cualquier tipo y no se pueden predecir de antemano. Por lo tanto, no es posible juzgar si el tipo es correcto durante la fase de compilación. Solo después de que se ejecuta el programa y se lanza una excepción se puede el tipo de excepción lanzada coincide con el tipo manejado por la captura. Un intento puede ir seguido de múltiples capturas. Una vez que se encuentra un tipo de captura coincidente, no se ejecutarán otras declaraciones de captura.

Las excepciones deben lanzarse explícitamente antes de que puedan detectarse, y throw se puede utilizar para lanzar excepciones.

throw exceptionData;

La excepción son los datos anormales, que pueden contener cualquier información. Además de generar excepciones en el cuerpo de la función, la palabra clave throw también puede indicar el tipo de excepciones que la función actual puede generar entre el encabezado de la función y el cuerpo de la función, llamadaespecificación de excepción

double func (char param) throw (int);

Indica que el tipo de valor de retorno de la función func es doble, tiene un parámetro de tipo char y solo puede generar una excepción de tipo int. Si la función necesita generar varias excepciones, puede usar comas para distinguirlas.

double func (char param) throw (int, char, exception);

La clase de excepción se denomina excepción estándar y se encuentra en el archivo de encabezado, declarada como:

class exception{
public:
    exception () throw();  //构造函数
    exception (const exception&) throw();  //拷贝构造函数
    exception& operator= (const exception&) throw();  //运算符重载
    virtual ~exception() throw();  //虚析构函数
    virtual const char* what() const throw();  //虚函数
}

orientado a objetos

constructor de copias

La creación de objetos consta de dos partes, la primera esasignar espacio, entonces eninicialización

Al inicializar un objeto mediante copia , se llama a un constructor especial, que esconstructor de copias. El constructor de copia tiene solo un parámetro y el tipo generalmente es una referencia a la clase actual y generalmente es una referencia constante .

Una clase puede tener dos constructores de copia al mismo tiempo, uno cuyo parámetro sea una referencia constante y el otro cuyo parámetro sea una referencia no constante .

Si no se define explícitamente ningún constructor de copia, el compilador generará automáticamente un constructor de copia predeterminado, que utiliza las variables miembro del objeto antiguo para asignar valores a las variables miembro del nuevo objeto. Pero cuando la clase contiene otros recursos, como memoria asignada dinámicamente, archivos abiertos, punteros a otros datos, conexiones de red, etc., el constructor de copia predeterminado no puede copiar estos recursos, debemos definir explícitamente el constructor de copia para copiar completamente todos los datos del objeto.

El constructor de copia se llama cuando se inicializa una copia de un objeto . Inicializar un objeto se refiere a llenar la memoria con datos por primera vez después de asignar memoria para el objeto.Este proceso llama al constructor y el objeto debe inicializarse inmediatamente después de su creación.

Tanto la inicialización como la asignación escriben datos en la memoria, enLa asignación al mismo tiempo que la definición se llama inicialización.La asignación después de la definición se llama asignación, solo hay una inicialización y puede haber muchas asignaciones. Para los tipos básicos, no hay diferencia entre inicialización y asignación, pero para las clases, hay una diferencia, porque se llamará al constructor cuando se inicialice el objeto (se llamará al constructor de copia cuando se inicialice el objeto mediante copia), y la función de reasignación se llamará cuando se asigne el valor Operador de asignación sobrecargado .

Cuando se inicializa un objeto, se llama al constructor y diferentes métodos de inicialización llaman a diferentes constructores. Si el parámetro real pasado es para inicialización, se llamará al constructor normal; si el objeto se inicializa con los datos de otros objetos, se llamará al constructor de copia para completar la inicialización mediante la copia.

copia superficial y copia profunda

Copiar entre tipos primitivos y datos de objetos simples es copiar la memoria bit a bit , este método se llamacopia superficial, que es similar al efecto de llamar a la función memcpy.

Cuando una clase contiene recursos como memoria asignada dinámicamente y punteros a otros datos, la función de copia predeterminada no puede copiar estos recursos y es necesario definir explícitamente un constructor de copia. Defina explícitamente un constructor de copia. Además de copiar las variables miembro del objeto original al nuevo objeto, también asignará un bloque de memoria para el nuevo objeto y copiará la memoria que tiene el objeto original. De esta manera, el objeto original y el nuevo objeto no están asociados y son independientes entre sí, y cambiar los datos de un objeto no afectará al otro objeto. Este comportamiento de copiar los recursos retenidos por el objeto se llamacopia profunda, el constructor de copia debe definirse explícitamente.

En lugar de utilizar un constructor de copia explícito, cuando se utiliza el constructor de copia predeterminado para inicializar un objeto con recursos como punteros, hará que el puntero del objeto copiado apunte a la misma parte de memoria que el original.Por lo tanto, es necesario utilizar una copia profunda cuando se tiene una variable miembro de tipo puntero, de modo que el objeto original y el nuevo objeto sean independientes entre sí .

Cuando un objeto se inicializa mediante copia, se llama al constructor de copia y cuando se asigna un valor a un objeto, se llama al operador de asignación sobrecargado . Incluso si no hay una sobrecarga explícita del operador de asignación, el editor lo sobrecargará de la forma predeterminada. La forma predeterminada de sobrecargar el operador es asignar todas las variables miembro del objeto original al nuevo objeto, que es diferente del default El constructor de copias funciona de manera similar .

Conversión de constructores y funciones de conversión de tipos

Se pueden convertir diferentes tipos de datos entre sí sin que el usuario especifique cómo realizar la conversión.conversión automática de tipos, que debe ser especificado explícitamente por el usuario se llamaelenco. La coerción se aplica sólo a las clases, porque las reglas de conversión de tipos sólo pueden aparecer en forma de funciones miembro de las clases.

constructor de conversiónEs un constructor que convierte otros tipos al tipo de clase actual y el constructor de conversión tiene solo un parámetro. El constructor de conversión también es un constructor que se puede usar para convertir otros tipos al tipo de clase actual y también se puede usar para inicializar objetos, que es el significado original del constructor.

Al inicializar una copia de un objeto, el compilador llama al constructor de conversión antes de copiar los datos a la variable. El constructor debe inicializar el objeto cuando se crea, y el compilador coincidirá con diferentes constructores de acuerdo con el paso de diferentes parámetros reales.

  • Constructor predeterminado: un constructor generado automáticamente por el compilador.
  • Constructor ordinario: constructor definido por el usuario.
  • Constructor de copia: se llama cuando un objeto se inicializa mediante copia.
  • Constructor de conversión: se llama al convertir otros tipos al tipo de clase actual.

No importa qué tipo de constructor, se utiliza para inicializar el objeto. Además de inicializar el objeto al crear el objeto, también se llamará al constructor en otros casos. Por ejemplo, cuando el objeto se inicializa copiando, el constructor de copia será llamado El constructor de conversión se llama cuando se convierten otros tipos al tipo de clase actual.

Un constructor de conversión puede convertir otros tipos al tipo actual, pero no al revés.función de conversión de tipoEl tipo de clase actual se puede convertir a otros tipos y la función de conversión de tipo solo puede aparecer en la clase como una función miembro.

operator type(){
    //todo
    return data;
}

operador es una palabra clave de C++, tipo es el tipo de destino que se va a convertir y datos es el tipo de datos que se va a devolver.

Debido a que se sabe que los datos del tipo se van a devolver, no es necesario proporcionar el tipo de valor de retorno . La función de conversión de tipo no parece tener un tipo de valor de retorno, pero en realidad indica implícitamente el tipo de valor de retorno. La función de conversión de tipo tampoco tiene parámetros, porque para convertir el objeto de la clase actual a otro tipo, el compilador asignará la dirección del objeto actual a este puntero, de modo que el objeto actual pueda manipularse en el cuerpo de la función.

Las funciones de conversión de tipos son similares a la sobrecarga de operadores; ambas usan la palabra clave operador, por lo que las funciones de conversión de tipos también se denominan operadores de conversión de tipos .

Las funciones de conversión de tipos y los constructores de conversión funcionan a la inversa.: El constructor de conversión convertirá otros tipos al tipo de clase actual y la función de conversión de tipos convertirá el tipo de clase actual a otros tipos. Sin estas dos funciones, se escribirá una gran cantidad de funciones de sobrecarga de operadores para implementar operaciones y conversión de tipos.

conversión de tipo

Se pueden convertir diferentes tipos de datos entre sí sin que el usuario especifique cómo realizar la conversión.conversión automática de tipos(conversión de tipo implícita), que requiere que el usuario indique explícitamente cómo convertir se llamaelenco

Las conversiones de tipos implícitas utilizan las reglas de conversión internas del compilador o constructores de conversión definidos por el usuario y funciones de conversión de tipos.

Los datos se almacenan en la memoria en formato binario, variosEl tipo de datos se refiere a la forma en que se interpretan los datos., se debe determinar cómo deben interpretarse dichos datos antes de su uso. Este método de interpretación está determinado por el tipo de datos. Los tipos de datos se utilizan para describir el tipo de datos y determinar cómo se interpretan. Los tipos de datos incluyen tipos integrados y tipos definidos por el usuario .

La conversión de tipos de datos consiste en reinterpretar los bits binarios ocupados por los datos y, si es necesario, modificar los datos durante la reinterpretación. Conversión de tipo implícita, el compilador puede decidir si modifica los bits binarios de los datos de acuerdo con reglas de conversión conocidas; y para la conversión de tipo obligatoria, dado que no existe una regla de conversión correspondiente, todo lo que se puede hacer es reinterpretar los bits binarios del datos, pero los bits binarios de los datos no se pueden corregir, lo cual esLa diferencia más fundamental entre la conversión de tipos implícita y la conversión de tipos obligatoria

Modificar los bits binarios de los datos es muy importante para poder ajustar los datos convertidos al valor correcto.

La conversión de tipos implícita debe utilizar reglas de conversión conocidas. Aunque la flexibilidad es limitada, es más segura (casi sin riesgo) porque los datos se pueden ajustar adecuadamente. La conversión puede convertir entre una gama más amplia de tipos de datos, como conversiones entre diferentes tipos de punteros (referencias), conversiones de constante a no constante, conversiones de int a punteros (algunos compiladores también permiten lo contrario), etc. Aunque esto aumenta flexibilidad, también está lleno de riesgos porque los datos no se pueden ajustar adecuadamente y los programadores deben usarlos con cuidado.

La coerción no es una panacea. La conversión de tipos sólo puede ocurrir entre tipos relacionados o tipos similares. Dos tipos irrelevantes no se pueden convertir entre sí, incluso si se usa la coerción. Dos clases que no tienen una relación de herencia no se pueden convertir entre sí, una clase base no se puede convertir en una clase derivada (abatida), un tipo de clase no se puede convertir en un tipo básico y los punteros y los tipos de clase no se pueden convertir entre sí. .

flujo de E/S

C++ contiene muchas clases io, denominadas colectivamenteclase de flujo

  • istream: a menudo se utiliza para recibir datos ingresados ​​desde el teclado;
  • ostream: a menudo se usa para enviar datos a la pantalla;
  • ifstream: utilizado para leer los datos del archivo;
  • ofstream: utilizado para escribir datos en el archivo;
  • iostream: Heredado de las clases istream y ostream, porque las funciones de esta clase están en una, que puede usarse tanto para entrada como para salida;
  • fstream: combina las funciones de ifstream y ofstream, que no solo pueden leer los datos del archivo, sino también escribir datos en el archivo.

De hecho, cin es el objeto de la clase istream y cout es el objeto de ostream. Todos están declarados en <iostream>Además, este archivo de encabezado también declara objetos de clase ostream, que son cerr y clog respectivamente. cerr se usa para generar mensajes de advertencia y error, y clog se usa para generar información de registro durante la ejecución del programa.

cout admite la redirección, clog y cerr no admiten la redirección y solo pueden enviar datos a la pantalla. Este tipo de objeto creado de antemano en C++ se llamaobjeto incorporado, que se puede utilizar directamente.

Redirección de entrada y salida

Hay tres formas de redirigir

  • freopen()Redirección de implementación de función: freopen()definida en <stdio.h>, es una función en la biblioteca estándar, que se utiliza especialmente para redirigir el flujo de entrada y el flujo de salida.
    string name, url;
    //将标准输入流重定向到 in.txt 文件
    freopen("in.txt", "r", stdin);
    cin >> name >> url;
    //将标准输出重定向到 out.txt文件
    freopen("out.txt", "w", stdout); 
    cout << name << "\n" << url;
  • rdbuf()Función para lograr la redirección: rdbuf()la función se define en <ios>el archivo de encabezado y se utiliza para realizar la redirección de flujos de entrada y salida. iOS es la clase base de istream y ostream, por lo que cin y cout pueden llamar directamente a esta función para realizar la redirección.
streambuf * rdbuf() const;//仅是返回一个指向当前流缓冲区的指针
streambuf * rdbuf(streambuf * sb);//用于将 sb 指向的缓冲区设置为当前流的新缓冲区,并返回一个指向旧缓冲区的对象

ifstream fin("in.txt");//打开 in.txt 文件,等待读取
ofstream fout("out.txt");//打开 out.txt 文件,等待写入

oldcin = cin.rdbuf(fin.rdbuf());//用 rdbuf() 重新定向,返回旧输入流缓冲区指针
oldcout = cout.rdbuf(fout.rdbuf());//用 rdbuf() 重新定向,返回旧输出流缓冲区指针
  • Realice la redirección a través de la consola: cuando ejecute el programa, agregue <in.txt >out.txt, este parámetro <in.txtredirige el flujo de entrada cin en el programa y también >out.txtredirige el flujo de salida cout en el programa.

buffer de salida

Cada flujo gestiona un buffer, utilizado para guardar los datos leídos y escritos por el programa. Con el mecanismo de almacenamiento en búfer implementado, el sistema operativo puede combinar múltiples salidas de un programa en una única operación de escritura a nivel del sistema. Debido a que las operaciones de escritura consumen mucho tiempo, permitir que el sistema operativo combine múltiples operaciones de salida como una operación de escritura de un solo dispositivo puede proporcionar ganancias de rendimiento significativas.

Las razones para vaciar el búfer (donde los datos realmente se escriben en el dispositivo o archivo de salida) son las siguientes:

  • El programa finaliza normalmente y el vaciado del búfer se realiza como parte de la operación de retorno de la función principal.
  • Cuando el búfer está lleno , es necesario vaciarlo antes de que se puedan seguir escribiendo nuevos datos en el búfer.
  • Utilice operadores como endl para vaciar el búfer.
  • Después de cada operación de salida, el estado interno de la secuencia se puede configurar usando el operador unitbuf para borrar el búfer. De forma predeterminada, cerr se establece en unitbuf, por lo que cerr se vacía inmediatamente.
  • Un flujo de salida puede estar asociado con otro flujo , en cuyo caso el búfer del flujo asociado se vaciará al leer y escribir en el flujo asociado.

Además del operador endl que puede vaciar el búfer, hay enjuague y extremos. Flush vacía el búfer pero no genera ningún carácter. termina inserta un carácter nulo en el búfer y luego vacía el búfer.

Cuando un flujo de entrada está asociado con un flujo de salida, cualquier intento de leer datos del flujo de salida primero eliminará el flujo de salida asociado. La biblioteca estándar asocia cout y cin juntos. tie()la función puedeutilizado para vincular el flujo de salida, hay dos versiones sobrecargadas.

ostream* tie() const;//返回指向绑定的输出流的指针
ostream* tie(ostream* os);//将os指向的输出流绑定在该对象上,并返回上一个绑定的输出流指针

cin.tie(&cout);  //仅仅是用来展示,标准库已经将 cin 和 cout 关联在一起
//old_tie 指向当前关联到 cin 的流(如果有的话)
ostream *old_tie = cin.tie(nullptr);  // cin 不再与其他流关联
//将 cin 与 cerr 关联,这不是一个好主意,因为 cin 应该关联到 cout
cin.tie(&cerr);  //读取 cin 会刷新 cerr 而不是 cout
cin.tie(old_tie);  //重建 cin 和 cout 间的正常关联

leer un solo caracter: get()Es una función miembro de la clase istream. Lee un carácter del flujo de salida y cuando llega al final de la entrada, el valor de retorno esEOF. EOF es una constante entera definida en la clase iostream con un valor de -1.

leer una línea de cuerda: get()es una función miembro de la clase istream, hay dos versiones sobrecargadas:

istream & getline(char* buf, int bufSize);
istream & getline(char* buf, int bufSize, char delim);

La primera versión lee caracteres bufsize-1 del flujo de entrada en el búfer buf, o hasta que se encuentra un carácter de nueva línea, la función se agrega automáticamente al final de los datos leídos en buf \0.

La diferencia entre la segunda versión y la primera es que la primera versión lee \0hasta y la segunda versión lee hasta el carácter delimitador. \no delim no se leerán en buf, sino que se tomarán del flujo de salida.

Si el número de caracteres en el flujo de entrada \ny antes de la delimitación alcanza o excede el tamaño buf, se producirá un error de lectura. Como resultado, esta lectura se completó, pero las lecturas posteriores fallarán.

Cómo ignorar caracteres específicos: ignore()Es una función miembro de la clase istream, el prototipo de función es istream & ignore(int n =1, int delim = EOF);, esta función omite n caracteres en el flujo de entrada especificado y omite un carácter de forma predeterminada.

mira el siguiente carácter en el flujo de entrada: peek()Es una función miembro de la clase istream. El prototipo de la función es que int peek()la función peek devuelve el siguiente carácter en el flujo de entrada, pero no toma el carácter del flujo de entrada. En caso de que el flujo de entrada haya finalizado, peek devuelve EOF.

Fin de entrada de juicio: Después de redirigir la entrada estándar a un archivo, cin puede leer los datos del archivo. Cuando se desconoce la cantidad de datos de entrada y no hay una marca de final, puede ingresar un carácter especial en la consola para indicar el final. Ingrese ctrl+D en Linux para indicar el final, genere ctrl+Z en Windows y presione Enter para indicar el final.

Cómo juzga cin el final de la entrada: juzga el final de la lectura de la consola: ya sea el final del archivo, o ctrl+z o ctrl+D, es el signo de fin, cin devuelve verdadero cuando se lee normalmente y Devuelve falso cuando encuentra el signo de fin, por lo que puede juzgar si la lectura finalizó de acuerdo con el valor de retorno de cin.

operación de archivo

flujo de archivos

Desde la perspectiva del almacenamiento de datos, la esencia de todos los archivos es la misma y todos están compuestos de bytes. Además de los archivos de texto sin formato, las imágenes, los vídeos y los archivos ejecutables generalmente se denominanarchivo binario

La clase de flujo de archivos consta principalmente de tres clases proporcionadas en la biblioteca estándar para implementar operaciones de archivos. ifstream, ofstream, y se derivan de y , por lo que estas tres clases tienen todos los métodos miembros de y fstreamrespectivamente .ifstreamofstreamistreamostreamistreamostream

abierto cerrado

Antes de operar con el archivo, primero es necesario abrir el archivo. Esto garantiza que al especificar el nombre del archivo, se establece la asociación entre el archivo y el objeto de secuencia de archivos, y cuando el archivo se va a operar más adelante, se puede realizar a través del objeto de secuencia asociado con él. La segunda es que puedes especificar cómo abrir el archivo cuando lo abres.

Hay dos formas de abrir un archivo:

  • Abra el archivo usando la función miembro abierta del objeto de secuencia. void open(const char* szFileName, int mode), el primer parámetro es un puntero al nombre del archivo y el segundo parámetro es la forma de abrir el archivo. Al abrir un archivo a través de la función miembro abierta, se puede juzgar utilizando el valor de retorno y el éxito es verdadero.
  • Al definir un objeto de flujo de archivos, el archivo se abre a través del constructor. ifstream::ifstream (const char* szFileName, int mode = ios::in, int);, el primer parámetro es un puntero al nombre del archivo, el segundo parámetro es la forma de abrir el archivo y el tercer parámetro generalmente no se usa.

La diferencia entre texto de archivo y texto binario: desde un punto de vista físico, no existe diferencia entre archivos binarios y archivos de caracteres, y todos se almacenan en el disco en formato binario. Los archivos de texto utilizan codificaciones de caracteres como ASCII y UTF-8, y los editores de texto pueden reconocer estos formatos de codificación y convertir los valores de codificación en caracteres para su visualización. No existe una diferencia esencial entre el modo texto y el modo binario, pero el manejo de los saltos de línea es diferente. No hay diferencia entre abrir un archivo binario y un archivo de texto en la plataforma Linux. En la plataforma Windows, el archivo de texto se conectará como un salto de línea. Si el archivo se abre como un archivo de texto para leer el archivo, el El programa eliminará \r\ntodo lo que hay en el archivo .\r\n\r

Usar abrir para abrir un archivo es el proceso de establecer una asociación entre el objeto de secuencia de archivos y el archivo, y cerrar es cortar la asociación entre el objeto de secuencia de archivos y el archivo, pero la secuencia de archivos no se destruye.

Los archivos abiertos deben cerrarse con close. Flush puede actualizar el búfer, porque el búfer solo escribirá datos en el archivo cuando esté lleno o el archivo esté cerrado, pero usar Flush puede vaciar el búfer de flujo de salida y escribir datos en el archivo.

leer escribir

Leer y escribir datos en binario puede ahorrar espacio y también es fácil de encontrar, porque cada dato ocupa el mismo tamaño de espacio.

Para leer y escribir datos en binario, ya no puede usar <<y >>para leer y escribir datos, necesita usar lectura y escritura, donde el método de lectura se usa para leer datos del archivo en formato binario y se usa el método de escritura. para escribir datos en el archivo en formato binario.

ostream & write(char* buffer, int count);

istream & read(char* buffer, int count);

conseguir y poner

Para leer los caracteres almacenados en el archivo uno por uno o almacenar los caracteres en el archivo uno por uno, puede usar get y put.

Dado que los archivos se almacenan en el disco duro, la velocidad de acceso al disco duro es mucho menor que la de la memoria. Si tiene que acceder al disco duro cada vez que escribe un archivo, la velocidad de lectura y escritura será muy lenta. , por lo que el sistema operativo recibe la solicitud put u get, primero almacenará los caracteres especificados en una parte de la memoria o leerá un dato del disco duro y lo almacenará en una parte de la memoria (búfer de salida de flujo de archivos, archivo búfer de entrada de flujo), y cuando se actualiza el búfer, una parte de Los datos se escriben juntos en el disco duro o se obtienen directamente del búfer de entrada de flujo de archivos cuando los datos se obtendrán la próxima vez.

El método de uso getline()puede leer una línea de cadenas del búfer de flujo de entrada cin y también puede leer una línea de datos en el archivo especificado.

Mover y obtener el puntero de lectura del archivo.

Al leer y escribir archivos, a veces desea saltar directamente a un lugar determinado del archivo para comenzar a leer y escribir, debe mover el puntero de lectura y escritura del archivo y luego realizar operaciones de lectura y escritura.

ostream & seekp (int offset, int mode);//设置文件读指针的位置
istream & seekg (int offset, int mode);//设置文件写指针的位置

El modo tiene tres opciones:

  1. ios::beg apunta al byte de desplazamiento después del comienzo del archivo, el desplazamiento igual a 0 significa desde el principio del archivo; en este caso, el desplazamiento debe ser un número no negativo.
  2. ios::cur apunta al byte de desplazamiento de la posición actual.
  3. ios::end apunta a compensar bytes desde el final del archivo.

tellg()y tellp()se puede obtener la posición del puntero actual.

Supongo que te gusta

Origin blog.csdn.net/qq_41323475/article/details/127856347
Recomendado
Clasificación