¿Sabemos todo sobre los operadores nuevos y eliminados? Usando una nueva eliminación para implementar matrices

Como sabe, en el lenguaje C, las funciones malloc() y free() se utilizan para asignar y liberar memoria dinámicamente. Sin embargo, C++ contiene dos operadores que realizan la asignación y desasignación de memoria de manera más eficiente y sencilla. Estos operadores son nuevos y eliminados. Su forma general es:

variable de puntero = nuevo tipo_variable;

eliminar variable de puntero;

Aquí, variable de puntero es un puntero de tipo tipo variable. El nuevo operador asigna memoria para almacenar un valor de tipo tipo_variable y devuelve su dirección. Cualquier tipo de dato se puede colocar con nuevo. El operador de eliminación libera la memoria a la que apunta pointer_variable.

Si no se puede realizar la operación de asignación de memoria, el nuevo operador genera una excepción de tipo xalloc. Si el programa no detecta esta excepción, su ejecución finalizará. Si bien este comportamiento predeterminado es satisfactorio para programas cortos, los programas de aplicaciones reales normalmente necesitan detectar la excepción y manejarla adecuadamente. Para detectar esta excepción, debe incluir el archivo de encabezado except.h.

El operador de eliminación solo debe usarse para punteros a la memoria asignada usando el nuevo operador. Usar el operador de eliminación con otros tipos de direcciones puede causar serios problemas.

Hay una serie de ventajas al usar new en lugar de malloc(). Primero, el nuevo operador calcula automáticamente el tamaño de la memoria requerida. No es necesario utilizar el operador sizeof(). Más importante aún, evita que usted asigne accidentalmente una cantidad incorrecta de memoria. En segundo lugar, el nuevo operador devuelve automáticamente un puntero al tipo requerido, por lo que no es necesario utilizar un operador de conversión de tipo. En tercer lugar, como se describirá en breve, es posible inicializar un objeto utilizando el nuevo operador. Finalmente, es posible sobrecargar el operador nuevo y el operador eliminar de forma global o en relación a la clase que se está creando.

A continuación se muestra un ejemplo sencillo del uso de los operadores nuevo y eliminar. Tenga en cuenta el uso de un bloque try/catch para realizar un seguimiento de los errores de asignación de memoria.

#incluir
#incluir
int principal()
{
entero *p;
intentar (
p = nuevo int; // asigna memoria para int
) atrapar (xalloc xa) (
corte<< "Allocation failure.\n";
devolver 1;
}
*p = 20; // asignando a esta ubicación de memoria el valor 20
corte<< *р; // демонстрация работы путем вывода значения
eliminar p; // liberando memoria
devolver 0;
}

Este programa asigna a la variable p la dirección de un bloque de memoria lo suficientemente grande como para contener un número entero. A continuación, a esta memoria se le asigna un valor y el contenido de la memoria se muestra en la pantalla. Finalmente, se libera la memoria asignada dinámicamente.

Como se señaló, puede inicializar la memoria usando el nuevo operador. Para hacer esto, debe especificar el valor de inicialización entre paréntesis después del nombre del tipo. Por ejemplo, en el siguiente ejemplo, la memoria a la que apunta p se inicializa a 99:

#incluir
#incluir
int principal()
{
entero *p;
intentar (
p = nuevo int(99); // inicialización 99
) atrapar (xalloc xa) (
corte<< "Allocation failure.\n";
devolver 1;
}
corte<< *p;
eliminar p;
devolver 0;
}

Puede utilizar new para asignar matrices. La forma general de una matriz unidimensional es:

puntero_variable = nuevo tipo_variable [tamaño];

Aquí el tamaño determina la cantidad de elementos en la matriz. Hay una limitación importante que se debe recordar al colocar una matriz: no se puede inicializar.

Para liberar una matriz asignada dinámicamente, debe utilizar la siguiente forma del operador de eliminación:

eliminar variable de puntero;

Aquí los paréntesis informan al operador de eliminación que libere la memoria asignada para la matriz.

El siguiente programa asigna memoria para una matriz de 10 elementos flotantes. A los elementos de la matriz se les asignan valores del 100 al 109, y luego el contenido de la matriz se imprime en la pantalla:

#incluir
#incluir
int principal()
{
flotante *p;
ent i;
intentar (
p = nuevo flotador; //obteniendo el décimo elemento de la matriz
) atrapar(xalloc xa) (
corte<< "Allocation failure.\n";
devolver 1;
}
//asignando valores del 100 al 109
para (i=0; yo<10; i + +) p[i] = 100.00 + i;
// genera el contenido de la matriz
para (i=0; yo<10; i++) cout << p[i] << " ";
eliminar p; // eliminando toda la matriz
devolver 0;
}

Las matrices y los punteros en realidad están estrechamente relacionados. El nombre de la matriz es puntero constante, cuyo valor es la dirección del primer elemento de la matriz (&arr). Por lo tanto, el nombre de la matriz puede ser un inicializador de puntero al que se aplicarán todas las reglas de aritmética de direcciones asociadas con los punteros. Programa de ejemplo:
Programa 11.1

#incluir usando el espacio de nombres estándar; int main() ( const int k = 10; int arr[k]; int *p = arr; // el puntero apunta al primer elemento de la matriz for (int i = 0; i< 10; i++){ *p = i; p++; // указатель указывает на следующий элемент } p = arr; // возвращаем указатель на первый элемент for (int i = 0; i < 10; i++){ cout << *p++ << " "; } cout << endl; // аналогично: for (int i = 0; i < 10; i++){ cout << *(arr + i) << " "; } cout << endl; p = arr; // выводим адреса элементов: for (int i = 0; i < 10; i++){ cout << "arr[" << i << "] => " << p++ << endl; } return 0; }

Salida del programa:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 arr => 0xbffc8f00 arr => 0xbffc8f04 arr => 0xbffc8f08 arr => 0xbffc8f0c arr => 0xbffc8f10 arr => bffc8f1 4 arreglo => 0xbffc8f18 arreglo = > 0xbffc8f1c arreglo => 0xbffc8f20 arreglo => 0xbffc8f24

La expresión arr[i] – acceder a un elemento por índice corresponde a la expresión *(arr + i) , que se llama desplazamiento del puntero(línea 22). Esta expresión ilustra más claramente cómo funciona realmente C++ con elementos de matriz. Variable de contador i indica cuántos elementos deben compensarse desde el primer elemento. La línea 17 imprime el valor del elemento de la matriz después de eliminar la referencia al puntero.

¿Qué significa la expresión *p++? El operador * tiene menor prioridad, mientras que el incremento de sufijo es asociativo de izquierda a derecha. Por lo tanto, en esta expresión compleja, primero se realizará el direccionamiento indirecto (acceder al valor de un elemento de matriz) y luego se incrementará el puntero. De lo contrario, esta expresión podría representarse como: cout Nota. El operador sizeof() aplicado al nombre de una matriz devolverá el tamaño de toda la matriz (no el primer elemento).
Nota. El operador de dirección (&) se utiliza para elementos de matriz de la misma manera que para variables regulares (los elementos de matriz a veces se denominan variables indexadas). Por ejemplo, &arr. Por lo tanto, siempre puede obtener un puntero a cualquier elemento de la matriz. Sin embargo, la operación &arr (donde arr es el nombre de la matriz) devolverá la dirección de la matriz completa y, por ejemplo, una operación (&arr + 1) significará un paso del tamaño de una matriz, es decir, obtener un puntero al elemento al lado del último.

Ventajas de utilizar punteros al trabajar con elementos de matriz

Veamos dos ejemplos de programas que conducen al mismo resultado: se asignan nuevos valores de 0 a 1999999 a los elementos de la matriz y se generan.
Programa 11.2

#incluir usando el espacio de nombres estándar; int main() ( const int n = 2000000; int masa[n] (); for (int i = 0; i< n; i++) { mass[i] = i; cout << mass[i]; } return 0; }

Programa 11.3

#incluir usando el espacio de nombres estándar; int main() ( const int n = 2000000; int masa[n] (); int *p = masa; for (int i = 0; i< n; i++) { *p = i; cout << *p++; } return 0; }

¡El programa 11.3 se ejecutará más rápido que el programa 11.2 (a medida que aumente el número de elementos, el programa 11.3 será más eficiente)! La razón es que el Programa 11.2 recalcula la ubicación (dirección) del elemento actual de la matriz en relación con el primero cada vez (11.2, líneas 12 y 13). En el Programa 11.3, se accede a la dirección del primer elemento una vez cuando se inicializa el puntero (11.3, línea 11).

Matriz fuera de límites

Observemos otro aspecto importante del trabajo con matrices C en C++. No disponible en C++ monitorear el cumplimiento de los límites de la matriz C. Eso. La responsabilidad de observar el modo de procesamiento de los elementos dentro de los límites de la matriz recae enteramente en el desarrollador del algoritmo. Veamos un ejemplo.
Programa 11.4

#incluir #incluir #incluir usando el espacio de nombres estándar; int main() ( int mas; default_random_engine rnd(tiempo(0)); uniform_int_distribution < 10; i++) mas[i] = d(rnd); cout << "Элементы массива:" << endl; for (int i = 0; i < 10; i++) cout << mas[i] << endl; return 0; }

El programa generará algo como esto:

Elementos de la matriz: 21 58 38 91 23 5 38 -1219324996 -1074960992 0

Hubo un error intencional en el programa 11.4. Pero el compilador no informará un error: la matriz tiene cinco elementos declarados, ¡pero los bucles suponen que hay 10 elementos! Como resultado, sólo cinco elementos se inicializarán correctamente (es posible que se produzcan más daños en los datos) y se eliminarán junto con la "basura". C++ proporciona la capacidad de controlar los límites utilizando las funciones de biblioteca comenzar() y finalizar() (debe incluir el archivo de encabezado del iterador). Modificando el programa 11.4
Programa 11.5

#incluir #incluir #incluir #incluir usando el espacio de nombres estándar; int main() ( int mas; int *primero = comenzar(mas); int *último = final(mas); default_random_engine rnd(time(0)); uniform_int_distribution d(10, 99);<< "Элементы массива:" << endl; while(first != last) { cout << *first++ << " "; } return 0; }

while(primero != último) ( *primero = d(rnd); primero++; ) primero = comenzar(mas);
corte

Las funciones comenzar() y finalizar() devuelven . Cubriremos el concepto de iteradores más adelante, pero por ahora diremos que se comportan como punteros que apuntan al primer elemento (primero) y al elemento que sigue al último (último). En el programa 11.5, por compacidad y conveniencia, reemplazamos el bucle for con un bucle while (ya que aquí ya no necesitamos un contador, usamos aritmética de punteros). Al tener dos punteros, podemos formular fácilmente una condición para salir del bucle, ya que en cada paso del bucle se incrementa el primer puntero.

Otra forma de hacer que atravesar elementos de matriz sea más seguro es utilizar el bucle for basado en rango que mencionamos en el tema ()
Operaciones nuevas y eliminar

Antes de familiarizarse con los punteros, conocía la única forma de escribir datos mutables en la memoria a través de variables. Una variable es un área de memoria con nombre. Los bloques de memoria para las variables correspondientes se asignan cuando se inicia el programa y se utilizan hasta que finaliza. Usando punteros, puede crear bloques de memoria sin nombre de un cierto tipo y tamaño (y también liberarlos) mientras el programa se está ejecutando. Esto revela una característica notable de los punteros, que se revela más plenamente en la programación orientada a objetos al crear clases.

La asignación de memoria dinámica se realiza mediante la nueva operación. Sintaxis:

tipo_datos *nombre_puntero = nuevo tipo_datos;

El lado derecho de la expresión dice que new solicita un bloque de memoria para almacenar datos de tipo int. Si se encuentra la memoria, se devuelve la dirección, que se asigna a una variable de puntero de tipo int. ¡Ahora solo puedes acceder a la memoria creada dinámicamente usando punteros! En el Programa 3 se muestra un ejemplo de cómo trabajar con memoria dinámica.
Programa 11.6

#incluir usando el espacio de nombres estándar; int main() ( int *a = nuevo int(5); int *b = nuevo int(4); int *c = nuevo int; *c = *a + *b; cout<< *c << endl; delete a; delete b; delete c; return 0; }

Después de trabajar con la memoria asignada, se debe liberar (devolver, dejar disponible para otros datos) mediante la operación de eliminación. Controlar el consumo de memoria es un aspecto importante del desarrollo de aplicaciones. Los errores en los que no se libera la memoria dan como resultado " pérdidas de memoria", lo que a su vez puede provocar que el programa se bloquee. La operación de eliminación se puede aplicar a un puntero nulo (nullptr) o uno creado con nuevo (es decir, nuevo y eliminar se utilizan en pares).

matrices dinámicas

matriz dinámica es una matriz cuyo tamaño se determina durante la ejecución del programa. Estrictamente hablando, una matriz C no es dinámica en C++. Es decir, solo puede determinar el tamaño de la matriz y aún es imposible cambiar el tamaño de la matriz mientras el programa se está ejecutando. Para obtener una matriz del tamaño requerido, debe asignar memoria para una nueva matriz y copiar datos de la original en ella, y luego liberar la memoria previamente asignada para la matriz original. El verdadero tipo de matriz dinámica en C++ es , que veremos más adelante. Para asignar memoria para una matriz, se utiliza la nueva operación. La sintaxis para asignar memoria para una matriz es:
puntero = nuevo tipo[tamaño] . Por ejemplo:

Int n = 10; int *arr = nuevo int[n];

La memoria se libera utilizando el operador de eliminación:

Eliminar llegada;

En este caso, no se especifica el tamaño de la matriz.
Programa de ejemplo. Llene la matriz de enteros dinámicos arr1 con números aleatorios. Mostrar matriz de origen. Vuelva a escribir todos los elementos con números de secuencia impares (1, 3, ...) en una nueva matriz de enteros dinámica arr2. Imprime el contenido de la matriz arr2.
Programa 11.7

#incluir #incluir #incluir usando el espacio de nombres estándar; int principal() ( int n; cout<< "n = "; cin >>norte; int *arr1 = nuevo int[n];< n; i++) { arr1[i] = d(rnd); cout << arr1[i] << " "; } cout << endl; int *arr2 = new int; for (int i = 0; i < n / 2; i++) { arr2[i] = arr1; cout << arr2[i] << " "; } delete arr1; delete arr2; return 0; } n = 10 73 94 17 52 11 76 22 70 57 68 94 52 76 70 68

Sabemos que en C++, una matriz bidimensional es una matriz de matrices. Por lo tanto, para crear una matriz dinámica bidimensional, es necesario asignar memoria en un bucle para cada matriz entrante, habiendo determinado previamente el número de matrices a crear. Para este fin se utiliza puntero a puntero, en otras palabras, una descripción de una serie de punteros:

Int **arr = nuevo int *[m];

donde m es el número de dichas matrices (filas de una matriz bidimensional).
Tarea de ejemplo. Complete con números aleatorios y genere los elementos de una matriz dinámica bidimensional.
Programa 11.8

#incluir #incluir #incluir #incluir usando el espacio de nombres estándar; int main() ( int n, m; default_random_engine rnd(tiempo(0)); uniform_int_distribution d(10, 99);<< "Введите количество строк:" << endl; cout << "m = "; cin >corte<< "введите количество столбцов:" << endl; cout << "n = "; cin >> metro;< m; i++) { arr[i] = new int[n]; for (int j = 0; j < n; j++) { arr[i][j] = d(rnd); } } // вывод массива: for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { cout << arr[i][j] << setw(3); } cout << "\n"; } // освобождение памяти выделенной для каждой // строки: for (int i = 0; i < m; i++) delete arr[i]; // освобождение памяти выделенной под массив: delete arr; return 0; } Введите количество строк: m = 5 введите количество столбцов: n = 10 66 99 17 47 90 70 74 37 97 39 28 67 60 15 76 64 42 65 87 75 17 38 40 81 66 36 15 67 82 48 73 10 47 42 47 90 64 22 79 61 13 98 28 25 13 94 41 98 21 28

corte
  1. >norte;
  2. int **arr = nuevo int *[m];
  3. // llenando la matriz: for (int i = 0; i
  4. Preguntas
  5. ¿Cuál es la relación entre punteros y matrices?
  6. ¿Por qué usar punteros al iterar sobre elementos de una matriz es más eficiente que usar la operación de índice?
¿Cuál es la esencia del concepto de "pérdida de memoria"?
¿Enumere formas de evitar que los límites de la matriz vayan más allá de los límites?

¿Qué es una matriz dinámica? ¿Por qué una matriz C no es inherentemente dinámica en C++?

Describir el proceso de creación de una matriz bidimensional dinámica.

Presentación para la lección.

Tarea
  1. Usando matrices dinámicas, resuelva el siguiente problema: Dada una matriz entera A de tamaño N. Vuelva a escribir todos los números pares de la matriz original (en el mismo orden) en una nueva matriz de enteros B e imprima el tamaño de la matriz B resultante y su contenido.
  2. Libro de texto
  3. §62 (10) §40 (11)
  4. Literatura
  5. Lafore R. Programación orientada a objetos en C++ (4ª ed.). Pedro: 2004

Prata, Esteban. Lenguaje de programación C++. Conferencias y ejercicios, 6ª ed.: Trans. del ingles - M.: LLC “I.D. Guillermo", 2012

Lippman B. Stanley, Josie Lajoie, Barbara E. Mu. Lenguaje de programación C++. Curso básico. Ed. 5to. M: LLC "I.D. Williams", 2014
struct A ( std::string str; // Objeto automático, se elimina implícitamente en el destructor de A (que se genera // automáticamente). El búfer de cadena en sí es un objeto dinámico (*), se // eliminará explícitamente en el destructor de std::string, que se llamará implícitamente en el destructor de A. // (*) A menos que la cadena sea demasiado corta, la optimización de cadenas pequeñas funcionará y el // buffer dinámico no se asignará en absoluto); vacío foo() ( std::vector v; // Objeto automático, eliminado implícitamente al salir de la función. v.push_back(10); // El contenido del vector, un objeto dinámico (matriz), se eliminará explícitamente en el // destructor del vector, que se llamará implícitamente al salir de la función.
Una una; // Objeto automático de clase A, eliminado implícitamente al salir de la función.

A* pa = nueva A; // El puntero pa es un objeto automático que se elimina implícitamente cuando la función sale, // pero apunta a un objeto dinámico de clase A que debe eliminarse explícitamente.

borrar pa; // Eliminar explícitamente un objeto dinámico.

auto upa = // El puntero inteligente upa es un objeto automático, eliminado implícitamente al salir de la función, std::make_unique

(); // pero apunta a un objeto dinámico de clase A, que se eliminará explícitamente // en el destructor de puntero inteligente. )

Normalmente los objetos dinámicos están en el montón, aunque en general no es así. Los objetos automáticos se pueden ubicar en la pila o en el montón. En el ejemplo anterior, el objeto automático upa->str está en el montón porque es parte del objeto dinámico *upa. Aquellos. las propiedades dinámicas/automáticas determinan la vida útil, pero no el lugar de vida, de un objeto.

Desde su invención, los operadores nuevo y eliminar se han utilizado en exceso. Los mayores problemas se relacionan con el operador de eliminación:
  • Puede olvidarse por completo de llamar a eliminar (pérdida de memoria).
  • Puede olvidarse de llamar a eliminar en caso de una excepción o un retorno anticipado de una función (también una pérdida de memoria).
  • Puede llamar a eliminar dos veces (doble eliminación).
  • Es posible llamar al operador de forma incorrecta: eliminar en lugar de eliminar o viceversa (comportamiento indefinido).
  • Puede utilizar el objeto después de llamar a eliminar (puntero colgante).
Todas estas situaciones conducen, en el mejor de los casos, a fallos del programa y, en el peor, a pérdidas de memoria y demonios nasales.

Por lo tanto, la gente ha descubierto desde hace mucho tiempo cómo ocultar el operador de eliminación en las profundidades de los contenedores y punteros inteligentes, eliminándolo así del código del cliente. Sin embargo, también hay problemas asociados con el nuevo operador, pero las soluciones para ellos no aparecieron de inmediato y, de hecho, muchos desarrolladores todavía se avergüenzan de utilizar estas soluciones. Hablaremos más sobre esto cuando lleguemos a crear funciones.

Ahora pasemos a los casos de uso para nuevos y eliminar. Permítanme recordarles que veremos varios escenarios y mostraremos sistemáticamente que en la mayoría de ellos el código será mejor si abandonamos el uso de nuevo y eliminamos.

Comencemos con algo simple: con matrices dinámicas.

matrices dinámicas

Una matriz dinámica es una matriz con elementos asignados en la memoria dinámica. Es necesario si se desconoce el tamaño en el momento de la compilación, o si el tamaño es lo suficientemente grande como para no querer asignar una matriz en la pila, que suele tener un tamaño muy limitado.

Para asignar matrices dinámicas, C++ proporciona una forma vectorial de bajo nivel de los operadores nuevo y eliminar: nuevo y eliminar. Como ejemplo, considere alguna función que funcione con un búfer externo:
void DoWork(int* buffer, size_t bufSize);
A menudo se encuentran funciones similares en bibliotecas con API en C puro. A continuación se muestra un ejemplo de cómo se vería el código que lo utiliza. Este es un código incorrecto porque... utiliza explícitamente eliminar y ya hemos descrito los problemas asociados con él anteriormente.
void Call(size_t n) ( int* p = new int[n]; DoWork(p, n); eliminar p; // ¡Malo! )
Aquí todo es simple y la mayoría de la gente sabe que para tales propósitos en C++ se debe usar el contenedor estándar std::vector. Asignará memoria en el constructor y la liberará en el destructor. Además, todavía puede cambiar su tamaño durante su vida, pero para nosotros esto ya no importa. Usando un vector, el código se vería así:
llamada vacía(size_t n) ( std::vector v(norte); // Mejor. DoWork(v.datos(), v.tamaño()); )
Por lo tanto, solucionamos todos los problemas asociados con la llamada a eliminar y, además, en lugar del par sin rostro de puntero + número, tenemos un contenedor explícito con una interfaz conveniente.

Al mismo tiempo, no se pueden crear ni eliminar. No entraré en más detalles sobre este escenario. En mi experiencia, la mayoría de los desarrolladores ya saben qué hacer en este caso y por qué.

* En C++, dicha interfaz debe implementarse utilizando el tipo span . Proporciona una interfaz unificada compatible con STL para acceder a secuencias contiguas de elementos sin afectar su vida útil de ninguna manera (semántica no propietaria).

** Dado que los programadores de C++ están leyendo este artículo, estoy bastante seguro de que alguien pensará: "¡Ja! std::vector almacena hasta tres (!) punteros, cuando el viejo int* es, por definición, solo un puntero. ¡Hay un uso excesivo de la memoria y de varias instrucciones de máquina para su inicialización! ¡Esto es inaceptable! Myers comentó excelentemente esta propiedad de los programadores de C++ en su informe. Por qué C++ navega cuando el Vasa se hundió. Si esto es para ti en realidad problema, puedo recomendar std::unique_ptr , y en el futuro el estándar puede darnos dinarray.

Objetos dinámicos

Los objetos dinámicos se suelen utilizar cuando es imposible vincular la vida útil de un objeto a un ámbito específico. Si esto se puede hacer, probablemente debería utilizar la memoria automática (vea por qué no debería abusar de los objetos dinámicos). Pero este es tema de un artículo aparte.

Cuando se crea un objeto dinámico, alguien debe eliminarlo, y los tipos de objetos se pueden dividir en dos grupos: los que no tienen conocimiento del proceso de su eliminación y los que sospechan algo. Diremos que los primeros tienen un modelo de gestión de memoria estándar, y los segundos uno no estándar.

Los tipos con un modelo de gestión de memoria estándar incluyen todos los tipos estándar, incluidos los contenedores. De hecho, el contenedor gestiona la memoria que se ha asignado. No le importa quién lo creó ni cómo será eliminado.

Los tipos con un modelo de gestión de memoria no estándar incluyen, por ejemplo, objetos Qt. Aquí, cada objeto tiene un padre que es responsable de eliminarlo. Y el objeto lo sabe, porque hereda de la clase QObject. Esto también incluye tipos con un recuento de referencias, por ejemplo, aquellos diseñados para funcionar con boost::intrusive_ptr .

En otras palabras, un tipo con un modelo de gestión de memoria estándar no proporciona ningún mecanismo adicional para gestionar su vida útil. Esto debe ser manejado completamente por el lado del usuario. Pero el tipo con un modelo no estándar proporciona tales mecanismos. Por ejemplo, QObject tiene los métodos setParent() y Children() y contiene una lista de hijos, y el tipo boost::intrusive_ptr se basa en las funciones intrusive_ptr_add_ref e intrusive_ptr_release y contiene un contador de referencia.

Si un tipo de objeto tiene un modelo de gestión de memoria estándar, por brevedad diremos que es un objeto con gestión de memoria estándar. De manera similar, si un tipo de objeto tiene un modelo de gestión de memoria no estándar, entonces diremos que es un objeto con gestión de memoria no estándar.

A continuación, veamos los objetos de ambos modelos. De cara al futuro, vale la pena decir que para objetos con administración de memoria estándar definitivamente no deberías usar new y delete en el código del cliente, y para objetos con administración de memoria no estándar depende del modelo específico.

* Algunas excepciones: modismo pimpl; un objeto muy grande (por ejemplo, un búfer de memoria).

** La excepción es std::locale::facet (ver más abajo).

Objetos dinámicos con gestión de memoria estándar.

Estos se encuentran con mayor frecuencia en la práctica. Y son ellos quienes deberían intentar utilizarlos en el C++ moderno, porque los enfoques estándar, utilizados en particular en los punteros inteligentes, funcionan con ellos.

En realidad, los consejos inteligentes, sí, son la respuesta. Se les debe dar control sobre la vida útil de los objetos dinámicos. Hay dos de ellos en C++: std::shared_ptr y std::unique_ptr . No resaltaremos std::weak_ptr aquí, porque es solo una ayuda para std::shared_ptr en ciertos casos de uso.

En cuanto a std::auto_ptr, se eliminó oficialmente de C++ a partir de C++ 17. ¡Que descanse en paz!

No me detendré aquí en el diseño y uso de punteros inteligentes, porque... esto está más allá del alcance del artículo. Permítanme recordarles de inmediato que vienen con las maravillosas funciones std::make_shared y std::make_unique, y deben usarse para crear punteros inteligentes.

Aquellos. en lugar de esto:
std::único_ptr galleta(nueva galleta(masa, azúcar, canela));
debería escribirse así:
cookie automática = std::make_unique (masa, azúcar, canela);
Las ventajas de las funciones make sobre la creación explícita de punteros inteligentes están bellamente descritas por Herb Sutter en su GotW #89 y por Scott Myers en su Effective Modern C++, Item 21. No me repetiré, pero solo daré una breve lista de puntos aquí:

  • Para ambas funciones make:
    • Seguridad en términos de excepciones.
    • No hay ningún nombre de tipo duplicado.
  • Para std::make_shared:
    • Ganar en productividad, porque el bloque de control se asigna al lado del objeto en sí, lo que reduce la cantidad de llamadas al administrador de memoria y aumenta la localidad de los datos. Mejoramiento.
Las funciones Make también tienen una serie de limitaciones, descritas en detalle en las mismas fuentes:
  • Para ambas funciones make:
    • No puedes pasar tu propio eliminador. Esto es bastante lógico, porque Internamente, las funciones make, por definición, utilizan el estándar new .
    • No puede utilizar un inicializador entre llaves, ni todas las demás sutilezas asociadas con el reenvío perfecto (consulte C++ moderno efectivo, elemento 30).
  • Para std::make_shared:
    • Consumo potencial de memoria para objetos grandes con referencias débiles de larga duración (std::weak_pointer).
    • Problemas con los operadores nuevos y de eliminación anulados a nivel de clase.
    • Posible intercambio falso entre un objeto y un bloque de control (consulte la pregunta en StackOverflow).
En la práctica, estas restricciones son raras y no restan valor a las ventajas. Resulta que los punteros inteligentes nos ocultaron la llamada a eliminar y las funciones de creación nos ocultaron la llamada a nuevo. Como resultado, obtuvimos un código más confiable, que no contiene ni nuevo ni eliminado.

Por cierto, la estructura de las funciones make se revela seriamente en sus informes de Stefan Lavavey (alias STL). Aquí hay una diapositiva elocuente de su informe No ayude al compilador:

Objetos dinámicos con gestión de memoria no estándar.

Además del enfoque estándar para la gestión de la memoria mediante punteros inteligentes, existen otros modelos. Por ejemplo, recuento de referencias y relaciones entre padres e hijos.

Objetos dinámicos con recuento de referencias.


Una técnica muy común utilizada en muchas bibliotecas. Tomemos como ejemplo la biblioteca OpenSceneGraph. Es un motor 3D multiplataforma abierto escrito en C++ y OpenGL.

La mayoría de las clases que contiene heredan de la clase osg::Referenced, que lleva a cabo el recuento de referencias internamente. El método ref() incrementa el contador, el método unref() lo disminuye y elimina el objeto cuando el contador llega a cero.

El kit también incluye un puntero inteligente osg::ref_ptr , que llama al método T::ref() en el objeto almacenado en su constructor y al método T::unref() en su destructor. El mismo enfoque se utiliza en boost::intrusive_ptr, sólo que allí, en lugar de los métodos ref() y unref(), hay funciones externas.

Veamos un fragmento de código que se proporciona en el OpenSceneGraph 3.0 oficial: Guía para principiantes:
osg::ref_ptr vértices = nuevo osg::Vec3Array; // ... osg::ref_ptr normales = nuevo osg::Vec3Array; // ... osg::ref_ptr geom = nuevo osg::Geometría; geom->setVertexArray(vértices.get()); geom->
Construcciones muy familiares como osg::ref_ptr p = nueva T . Exactamente de la misma manera que las funciones std::make_unique y std::make_shared se usan para crear las clases std::unique_ptr y std::shared_ptr, podemos escribir la función osg::make_ref para crear la clase osg::ref_ptr . Esto se hace de forma muy sencilla, por analogía con la función std::make_unique:
espacio de nombres osg (plantilla osg::ref_ptr make_ref(Args&&... args) ( devuelve nueva T(std::forward (argumentos)...);
) )
Reescribamos este fragmento de código armados con nuestra nueva función: vértices automáticos = osg::make_ref (); // ... auto normales = osg::make_ref (); // ... auto geom = osg::make_ref
(); geom->setVertexArray(vértices.get()); geom->setNormalArray(normals.get()); //...

Los cambios son triviales y se pueden realizar fácilmente de forma automática. De esta forma sencilla, obtenemos una seguridad excepcional, sin nombres de tipos duplicados y un excelente cumplimiento del estilo estándar. La llamada de eliminación ya estaba oculta en el método osg::Referenced::unref(), y ahora hemos ocultado la nueva llamada en la función osg::make_ref.

Así que no hay novedades ni eliminaciones.

* Técnicamente en este fragmento no hay situaciones que sean inseguras en cuanto a excepciones, pero en configuraciones más complejas sí podrían haber algunas.


Objetos dinámicos para diálogos no modal en MFC

Veamos un ejemplo específico de la biblioteca MFC. Este es un contenedor de clases de C++ a través de la API de Windows. Se utiliza para simplificar el desarrollo de GUI en Windows.

En el siguiente ejemplo, se crea un cuadro de diálogo cuando se hace clic en un botón en el método CMainFrame::OnBnClickedCreate() y se elimina en el método CMyDialog::PostNcDestroy() anulado.
void CMainFrame::OnBnClickedCreate() ( auto* pDialog = new CMyDialog(this); pDialog->ShowWindow(SW_SHOW); ) clase CMyDialog: CDialog público ( público: CMyDialog(CWnd* pParent) ( Create(IDD_MY_DIALOG, pParent); ) protegido: anular la anulación de PostNcDestroy() ( CDialog::PostNcDestroy(); eliminar esto; ) );
Aquí no tenemos ocultas ni la llamada nueva ni la de eliminación. Hay muchas maneras de pegarse un tiro en el pie. Además de los problemas habituales con los punteros, puedes olvidarte de anular el método PostNcDestroy() en tu cuadro de diálogo, lo que provoca una pérdida de memoria. Cuando veas la llamada a nuevo, es posible que quieras llamar a eliminarte en un momento determinado, lo que resultará en una doble eliminación. Puede crear accidentalmente un objeto de diálogo en la memoria automática, nuevamente obtenemos una doble eliminación.

Intentemos ocultar las llamadas a nuevo y eliminar dentro de la clase intermedia CModelessDialog y la fábrica CreateModelessDialog, que será responsable de los diálogos no modal en nuestra aplicación:
clase CModelessDialog: CDialog público (público: CModelessDialog(UINT nIDTemplate, CWnd* pParent) ( Create(nIDTemplate, pParent); ) protegido: invalidación de PostNcDestroy() ( CDialog::PostNcDestroy(); eliminar esto; ) ); // Fábrica para crear plantilla de diálogos modales Derived* CreateModelessDialog(Args&&... args) ( // En lugar de static_assert en el cuerpo de la función, podemos usar std::enable_if en su encabezado, lo que nos permitirá usar SFINAE. // Pero dado que otras sobrecargas de esta función son Es poco probable que sea de esperar. Parece razonable utilizar una solución más simple y visual static_assert(std::is_base_of). ::value, "Se debe llamar a CreateModelessDialog para los descendientes de CModelessDialog"); auto* pDialog = nuevo Derivado(std::forward
(argumentos)...);
pDialog->Mostrar ventana (SW_SHOW); devolver pDialog; )
Por supuesto, no hemos solucionado todos los problemas de esta manera. Por ejemplo, un objeto aún se puede asignar en la pila y eliminarlo dos veces. Puede evitar la asignación de un objeto en la pila únicamente modificando la clase del objeto en sí, por ejemplo agregando un constructor privado. Pero no hay manera de que podamos hacer esto desde la clase base CModelessDialog. Por supuesto, puede ocultar la clase CMyDialog por completo y hacer que la fábrica no sea una plantilla, sino una más clásica, aceptando un determinado identificador de clase. Pero todo esto escapa al alcance del artículo.

De todos modos, hemos facilitado la creación de un diálogo a partir del código del cliente y la escritura de una nueva clase de diálogo. Y al mismo tiempo, eliminamos las llamadas nuevas y eliminamos del código del cliente.

Objetos dinámicos con una relación padre-hijo.



Ocurren con bastante frecuencia, especialmente en bibliotecas para el desarrollo de GUI. Como ejemplo, considere Qt, una biblioteca muy conocida para el desarrollo de aplicaciones y UI.

La mayoría de las clases heredan de QObject. Almacena una lista de niños y los elimina cuando se elimina a sí mismo. Almacena un puntero al padre (puede ser nulo) y puede cambiar el padre durante la vida.

Un excelente ejemplo de una situación en la que deshacerse de lo nuevo y eliminarlo no será tan fácil. La biblioteca fue diseñada de tal manera que estos operadores pueden y deben usarse en muchos casos. Propuse un contenedor para crear objetos con un padre no nulo, pero la idea no funcionó (ver discusión en la lista de correo de Qt).

Así que no conozco una buena manera de deshacerme de lo nuevo y eliminarlo en Qt.

Objetos dinámicos std::locale::facet


Para controlar la salida de datos a secuencias en C++, se utilizan objetos std::locale. Una configuración regional es un conjunto de facetas que determinan cómo se muestran ciertos datos. Las facetas tienen su propio contador de referencia y al copiar configuraciones regionales, las facetas no se copian, solo se copia el puntero y se incrementa el contador de referencia.

La configuración regional en sí es responsable de eliminar facetas cuando el recuento de referencias llega a cero, pero el usuario debe crear facetas usando el nuevo operador (consulte la sección Notas en la descripción del constructor std::locale):
std::locale predeterminado; std::locale myLocale(predeterminado, nuevo std::codecvt_utf8 );
Este mecanismo se implementó incluso antes de la introducción de los punteros inteligentes estándar y se destaca de las reglas generales para el uso de clases en la biblioteca estándar.

Puede crear un contenedor simple que cree una configuración regional para eliminar elementos nuevos del código del cliente. Sin embargo, esta es una excepción bastante conocida a las reglas generales y quizás no tenga sentido hacerle un jardín.

Conclusión

Entonces, primero analizamos escenarios como la creación de matrices dinámicas y objetos dinámicos con administración de memoria estándar. En lugar de crear y eliminar, utilizamos contenedores estándar y funciones de creación y obtuvimos un código más simple y confiable.

Luego analizamos una serie de ejemplos de administración de memoria no estándar y vimos cómo podríamos mejorar el código eliminando elementos nuevos y eliminando en contenedores adecuados. También encontramos un ejemplo en el que este enfoque no funciona.

Sin embargo, en la mayoría de los casos esta recomendación produce resultados excelentes y puede utilizarse como principio predeterminado. Ahora podemos considerar que si el código usa new o delete, este es un caso especial que requiere especial atención. Si ve estas llamadas en el código del cliente, piense si realmente están justificadas.

  • Evite usar nuevo y eliminar en su código. Piense en ellas como operaciones manuales de gestión de montón de bajo nivel.
  • Utilice contenedores estándar para estructuras de datos dinámicas.
  • Utilice funciones make para crear objetos dinámicos siempre que sea posible.
  • Cree contenedores para objetos con un modelo de memoria no estándar.

Del autor

Personalmente, me he encontrado con muchos casos de pérdidas de memoria y fallas debido al uso excesivo de elementos nuevos y eliminados. Sí, la mayor parte de este código se escribió hace muchos años, pero luego los programadores jóvenes comienzan a trabajar con él y piensan que así es como debería escribirse.

Espero que este artículo sirva como guía práctica a la que se pueda enviar a un joven desarrollador para que no se extravíe.

Hace poco más de un año hice una presentación sobre este tema en la conferencia C++ Rusia. Después de mi discurso, el público se dividió en dos grupos: aquellos para quienes todo era obvio y aquellos que hicieron un descubrimiento maravilloso por sí mismos. Creo que a las conferencias tienden a asistir desarrolladores más experimentados, por lo que incluso si hubiera muchas personas nuevas en esta información, espero que este artículo sea útil para la comunidad.

PD En el proceso de discusión del artículo, mis colegas y yo tuvimos todo un debate sobre cuál es correcto: "Myers" o "Meyers". Por un lado, “Meyers” suena más familiar a los oídos rusos, y nosotros mismos parecemos haber hablado siempre así. Por otro lado, en la wiki se utiliza "Myers". Si nos fijamos en los libros localizados, en general hay muchas cosas: a estas dos opciones también se suma "Meyers". En conferencias diferente Gente representar ello de diferentes maneras. En última instancia nosotros logró descubrir, que se hace llamar “Myers”, que es lo que decidieron.

Campo de golf

  1. Sutter de hierbas Solución GotW n.º 89: punteros inteligentes.
  2. Scott Meyers C++ moderno efectivo, Artículo 21, pág. 139.
  3. Stephan T. Lavavej, No ayude al compilador.
  4. Bjarne Stroustrup, El lenguaje de programación C++, 11.2.1, pág. 281.
  5. Cinco mitos populares sobre C++.,Parte 2
  6. Mijail Matrosov, C++ sin nuevo y eliminar.

Etiquetas:

Agregar etiquetas

Comentarios 134

  • Tutorial

¡Hola! A continuación hablaremos de operadores conocidos. nuevo Y borrar, o más bien sobre lo que no está escrito en los libros (al menos en los libros para principiantes).
Me impulsó a escribir este artículo una idea errónea común sobre nuevo Y borrar, que veo constantemente en foros e incluso (!!!) en algunos libros.
¿Sabemos todos qué es realmente? nuevo Y borrar? ¿O simplemente creemos que lo sabemos?
Este artículo te ayudará a resolverlo (bueno, los que saben pueden criticar :))

Nota: a continuación hablaremos exclusivamente sobre el nuevo operador, para otras formas del nuevo operador y para todas las formas del operador de eliminación, todo lo escrito a continuación también es cierto y se aplica por analogía.

Entonces, comencemos con lo que suelen escribir en los libros para principiantes cuando describen nuevo(el texto fue sacado de la nada, pero es totalmente cierto):

Operador nuevo asigna memoria mayor o igual al tamaño requerido y, a diferencia de las funciones del lenguaje C, llama a los constructores para los objetos para los cuales se asigna la memoria... puede sobrecargar (en algún lugar escriben para implementar) el operador nuevo para satisfacer sus necesidades.

Y, por ejemplo, muestran una sobrecarga primitiva (implementación) del nuevo operador, cuyo prototipo se ve así
operador void* nuevo (std::size_t tamaño) throw (std::bad_alloc);

A qué quieres prestar atención:
1. No comparten en ningún lado nuevo palabra clave Lenguaje y operador C++ nuevo, en todas partes se habla de ellos como una sola entidad.
2. En todos lados escriben eso nuevo llama a los constructores de los objetos.
Tanto el primero como el segundo son conceptos erróneos comunes.

Pero no confiemos en los libros para principiantes, pasemos al Estándar, es decir, a las secciones 5.3.4 y 18.6.1, en las que el tema de este artículo se revela realmente (o mejor dicho, se revela ligeramente).

5.3.4
La nueva expresión intenta crear un objeto del tipo-id (8.1) o nuevo tipo-id al que se aplica. /*ya no estamos interesados*/
18.6.1
operador void* new(std::size_t size) throw(std::bad_alloc);
Efectos: La función de asignación llamada por una nueva expresión (5.3.4) para asignar bytes de tamaño de
almacenamiento adecuadamente alineado para representar cualquier objeto de ese tamaño /*no estamos interesados ​​más*/

Aquí ya vemos que en el primer caso nuevo referido como expresión, y en el segundo se declara como operador.¡Y estas son realmente 2 entidades diferentes!
Intentemos descubrir por qué esto es así, para ello necesitaremos listados de ensamblaje obtenidos después de compilar el código usando nuevo. Bueno, ahora hablemos de todo en orden.

nueva expresión es un operador de lenguaje, igual que si, mientras etc. (A pesar de si, mientras etc. todavía se les conoce como declaración, pero descartemos la letra) Eso es. Al encontrarlo en el listado, el compilador genera un código específico correspondiente a este operador. También nuevo- este es uno de palabras clave lenguaje C++, que una vez más confirma su similitud con si"amigo, para" amigos, etc. A operador nuevo() a su vez, es simplemente una función del lenguaje C++ del mismo nombre, cuyo comportamiento puede anularse. IMPORTANTE - operador nuevo() NO llama a los constructores de los objetos para los que se asigna memoria. Simplemente asigna memoria del tamaño requerido y listo. Su diferencia con las funciones C es que puede generar una excepción y puede redefinirse, además de convertirse en un operador para una clase separada, redefiniendo así solo para esta clase (recuerde el resto usted mismo :)).
Pero nueva expresión simplemente llama a los constructores de los objetos. Aunque sería más correcto decir que tampoco llama a nada simplemente, cuando lo encuentra, el compilador genera código para llamar a los constructores.

Para completar el cuadro, considere el siguiente ejemplo:

#incluir clase Foo ( público: Foo() ( std::cout<< "Foo()" << std::endl; } }; int main () { Foo *bar = new Foo; }

Después de ejecutar este código, se imprimirá "Foo()", como se esperaba. Averigüemos por qué, para esto es necesario mirar el ensamblador, que comenté un poco por conveniencia.
(el código proviene del compilador cl usado en MSVS 2012, aunque yo uso principalmente gcc, pero eso no viene al caso)
/Foo *bar = nuevo Foo; empujar 1; tamaño en bytes para el operador de llamada de objeto Foo nuevo (02013D4h); llamar al operador nuevo pop ecx mov dword ptr, eax; escriba el puntero devuelto desde nuevo a la barra y dword ptr,0 cmp dword ptr,0; comprobamos si 0 se ha registrado en bar je main+69h (0204990h); si es 0, entonces dejamos aquí (tal vez incluso desde main o hacia algún controlador, en este caso no importa) mov ecx,dword ptr; coloque un puntero a la memoria asignada en ecx (MSVS siempre pasa esto a ecx(rcx)) llame a Foo::Foo (02011DBh); y llamar al constructor; sin más intereses
Para aquellos que no entendieron nada, aquí hay un (casi) análogo de lo que sucedió en el pseudocódigo tipo C (es decir, no es necesario intentar compilarlo :))
Foo *bar = operador nuevo (1); // donde 1 es el tamaño requerido bar->Foo(); // llama al constructor

El código anterior confirma todo lo escrito anteriormente, a saber:
1. operador (idioma) nuevo Y operador nuevo()- esto NO es lo mismo.
2. operador nuevo() NO llama a constructor(es)
3. la llamada al constructor la genera el compilador cuando se encuentra en el código palabra clave "nuevo"

En pocas palabras: espero que este artículo te haya ayudado a comprender la diferencia entre nueva expresión Y operador nuevo() o incluso descubrir que (esta diferencia) existe, si alguien no lo supiera.

PD operador borrar Y operador eliminar() Tiene una diferencia similar, por eso al principio del artículo dije que no lo describiría. Creo que ahora entiendes por qué su descripción no tiene sentido y puedes comprobar de forma independiente la validez de lo que se escribió anteriormente. borrar.

Actualizar:
Habrazhitel con un apodo khim En correspondencia personal, sugirió el siguiente código, que demuestra claramente la esencia de lo escrito anteriormente.
#incluir clase Prueba ( pública: Prueba() ( std::cout<< "Test::Test()" << std::endl; } void* operator new (std::size_t size) throw (std::bad_alloc) { std::cout << "Test::operator new(" << size << ")" << std::endl; return::operator new(size); } }; int main() { Test *t = new Test(); void *p = Test::operator new(100); // 100 для различия в выводе }
Este código generará lo siguiente
Prueba::operador nuevo(1) Prueba::Prueba() Prueba::operador nuevo(100)
lo cual es de esperar.

El nuevo operador le permite asignar memoria para matrices. el regresa

puntero al primer elemento de la matriz entre corchetes. Al asignar memoria para matrices multidimensionales, todas las dimensiones excepto la más a la izquierda deben ser constantes. La primera dimensión se puede especificar mediante una variable cuyo valor es conocido por el usuario en el momento en que se utiliza new, por ejemplo:

int *p=nuevo int[k]; // el error no puede convertir de "int (*)" a "int *"

int (*p)=nuevo int[k]; // bien

Al asignar memoria para un objeto, su valor no estará definido. Sin embargo, a un objeto se le puede dar un valor inicial.

entero *a = nuevo entero (10234);

Esta opción no se puede utilizar para inicializar matrices. Sin embargo

en lugar del valor de inicialización, puede colocar una lista separada por comas

valores pasados ​​​​al constructor al asignar memoria para la matriz (masa

siv nuevos objetos especificados por el usuario). Memoria para una variedad de objetos.

sólo se puede asignar si la clase correspondiente tiene

hay un constructor predeterminado.

matr())(; // constructor predeterminado

matr(int i,flotante j): a(i),b(j) ()

( matr mt(3,.5);

matr *p1=nueva matr; // verdadero p1 - puntero a 2 objetos

matr *p2=nueva matr (2,3.4); // incorrecto, la inicialización no es posible

matr *p3=nueva matr (2,3.4); // verdadero p3 – objeto inicializado

( int i; // componente de datos de la clase A

A()() // constructor de la clase A

~A()() // destructor de la clase A

( A *a,*b; // descripción de punteros a un objeto de clase A

flotar *c,*d; // descripción de punteros a elementos de tipo flotante

a=nueva A; // asignando memoria para un objeto de clase A

b=nueva A; // asignando memoria para una matriz de objetos de clase A

c=nuevo flotador; // asigna memoria para un elemento flotante

d=nuevo flotador; // asigna memoria para una matriz de elementos flotantes

eliminar un; // liberando memoria ocupada por un objeto

eliminar b; // liberando memoria ocupada por una serie de objetos

eliminar c; // liberando la memoria de un elemento flotante

eliminar d; ) // liberando la memoria de una matriz de elementos flotantes

Organizar el acceso externo a los componentes locales de la clase (amigo)

Ya nos hemos familiarizado con la regla básica de la programación orientada a objetos: datos (internos

variables) del objeto están protegidos de influencias externas y el acceso a ellos puede ser

obtener solo usando las funciones (métodos) del objeto. Pero hay tales casos.

tés, cuando necesitamos organizar el acceso a los datos del objeto sin utilizar

aprendiendo su interfaz (funciones). Por supuesto, puedes agregar una nueva función pública.

a una clase para obtener acceso directo a las variables internas. Sin embargo, en

En la mayoría de los casos, la interfaz de un objeto implementa ciertas operaciones y

La nueva característica puede ser redundante. Al mismo tiempo, a veces hay una

la necesidad de organizar el acceso directo a los datos internos (locales)

dos objetos diferentes de una función. Al mismo tiempo, en C++ una función no puede

puede ser un componente de dos clases diferentes.

Para implementar esto, se introdujo el especificador de amigos en C++. si algunos

la función se define como una función amiga para alguna clase, entonces:

No es un componente funcional de esta clase;

Tiene acceso a todos los componentes de esta clase (privados, públicos y protegidos).

A continuación se muestra un ejemplo donde una función externa accede

datos de clase interna.

#incluir

usando el espacio de nombres estándar;

kls(int i,int J) : i(I),j(J) () // constructor

int max() (return i>j? i: j;) // función componente de la clase kls

amigo doble diversión(int, kls&); // declaración de amigo de función externa divertida

doble diversión (int i, kls &x) // función externa

(retorno (doble)i/x.i;

corte<< obj.max() << endl;

En C(C++), existen tres formas conocidas de pasar datos a una función: por valor

posible en algún objeto existente. Se pueden distinguir los siguientes tiempos:

Presencia de enlaces y punteros. En primer lugar, la imposibilidad de la existencia del cero.

enlaces significa que no es necesario comprobar su corrección. Y cuando utilice un puntero, debe verificar si tiene un valor no nulo. En segundo lugar, los punteros pueden apuntar a diferentes objetos, pero una referencia siempre apunta a un único objeto, especificado cuando se inicializa. Si desea permitir que una función cambie valores

parámetros que se le pasan, luego en el lenguaje C deben declararse

globalmente, o trabajar con ellos en funciones se lleva a cabo pasando a

Contiene punteros a estas variables. En C++, los argumentos se pueden pasar a una función.

el ron está marcado &.

diversión vacía1 (int, int);

void fun2(int &,int &);

( int i=1,j=2; // i y j son parámetros locales

corte<< "\n адрес переменных в main() i = "<<&i<<" j = "<<&j;

corte<< "\n i = "<


Arriba