Cadena, iterador vectorial

  • El lenguaje C++ también define una rica biblioteca de tipos de datos abstractos. Entre ellos, cadena y vector son los dos tipos de bibliotecas estándar más importantes: la primera admite cadenas de longitud variable y la segunda representa colecciones de longitud variable. Otro tipo de biblioteca estándar es el iterador, que es un tipo de soporte para cadenas y vectores y se utiliza a menudo para acceder a caracteres en cadenas o elementos en vectores. La matriz incorporada es un tipo más básico, y la cadena y el vector l son algunas abstracciones de la misma . Este capítulo presentará matrices y tipos de biblioteca estándar de cadena y vector, respectivamente.

  • Los tipos integrados están definidos directamente por el lenguaje C++. Estos tipos, como números y caracteres, representan las capacidades inherentes a la mayoría del hardware informático . La biblioteca estándar define un conjunto adicional de tipos con propiedades más avanzadas que aún no se implementan directamente en el hardware de la computadora . La cadena representa una secuencia de caracteres de longitud variable y el vector almacena una secuencia de objetos de un tipo determinado de longitud variable. También presentaremos los tipos de matrices integradas. Al igual que otros tipos integrados, la implementación de las matrices está estrechamente relacionada con el hardware. Por lo tanto, en comparación con los tipos de biblioteca estándar cadena y vector, las matrices son un poco menos flexibles.

  • Las funciones de biblioteca utilizadas pertenecen básicamente al espacio de nombres std, y el programa también marca explícitamente este punto. Por ejemplo, std::cin significa leer desde la entrada estándar. El significado de usar el operador de alcance ( :: ) aquí es que el compilador debe buscar el nombre en el lado derecho del alcance indicado por el nombre en el lado izquierdo del operador. Por lo tanto, std: :cin significa usar el nombre cin en el espacio de nombres std. Los miembros del espacio de nombres también se pueden utilizar de una forma más sencilla. En esta sección, aprenderemos uno de los métodos más seguros: usar una declaración de uso. Con la declaración de uso, se puede utilizar el nombre deseado sin un prefijo especial (como espacio de nombres: :). La declaración de uso tiene la siguiente forma:

    • using namespace::name;
      
  • Una vez que declare la declaración anterior, podrá acceder directamente a los nombres en el espacio de nombres:

    • Insertar descripción de la imagen aquí
  • La forma del lenguaje C++ es relativamente libre, por lo que puede colocar solo una declaración de uso en una línea, o puede colocar varias declaraciones de uso en una línea. Sin embargo, tenga en cuenta que cada nombre utilizado debe tener su propia declaración y cada declaración debe terminar con un punto y coma .

  • El código ubicado en los archivos de encabezado generalmente no debe utilizar instrucciones de uso . Esto se debe a que el contenido del archivo de encabezadoCopiarVaya a todos los archivos que hacen referencia a él. Si hay una declaración de uso en el archivo de encabezado, entonces cada archivo que use el archivo de encabezado tendrá esta declaración. Para algunos programas, pueden ocurrir conflictos de nombres inesperados debido a la inclusión involuntaria de algunos nombres.

  • La cadena de tipo de biblioteca estándar representa una secuencia de caracteres de longitud variable. Para utilizar el tipo de cadena, primero debe incluir el archivo de encabezado de cadena. Como parte de la biblioteca estándar, la cadena se define en el espacio de nombres std. Los siguientes ejemplos suponen que se incluye el siguiente código:

    • #include <string>
      using std::string
      
  • Por un lado, el estándar C++ especifica en detalle las operaciones proporcionadas por los tipos de biblioteca y, por otro lado, también impone algunos requisitos de rendimiento a los implementadores de bibliotecas. Por lo tanto, los tipos de biblioteca estándar son lo suficientemente eficientes para aplicaciones generales.

  • La forma en que se inicializa un objeto de una clase está determinada por la propia clase. Una clase puede definir muchas formas de inicializar objetos, pero estos métodos deben ser diferentes: o el número de valores iniciales es diferente o el tipo de valores iniciales es diferente. Se enumeran algunas de las formas más comunes de inicializar objetos de cadena. A continuación se muestran algunos ejemplos:

    • 	string sl;//默认初始化,s1是一个空字符串
      	string s2 = sl;// s2是s1的副本
      	string s3 = "hiya";// s3是该字符串字面值的副本
      	string s4(10, 'c');// s4的内容是cccccccccc
      
  • Puede inicializar un objeto de cadena de forma predeterminada, por lo que obtendrá una cadena vacía, es decir, no hay caracteres en el objeto de cadena . Si se proporciona un literal de cadena, todos los caracteres del literal, excepto el último carácter nulo, se copian en el objeto de cadena recién creado. Si se proporcionan un número y un carácter, el contenido del objeto de cadena es la secuencia obtenida repitiendo el carácter dado varias veces seguidas.

  • El lenguaje C ++ tiene varios métodos de inicialización diferentes y a través de cadenas podemos ver claramente las diferencias y conexiones entre estos métodos de inicialización. Si utiliza el signo igual (=) para inicializar una variable, en realidad se realiza la inicialización de copia. El compilador copia el valor inicial en el lado derecho del signo igual al objeto recién creado . Por el contrario, si no se utiliza el signo igual, se realiza la inicialización directa.

    • 	string s5 = "hiya";//拷贝初始化
      	string s6("hiya");//直接初始化
      	string s7(10, 'c');//直接初始化,s7的内容是cccccccccc
      	string s8 = string(10, 'c');//拷贝初始化,s8的内容是cccccccccc
      
  • El valor inicial de s8 es cadena (10, 'c'), que en realidad es un objeto de cadena creado con dos parámetros: el número 10 y el carácter c, y luego este objeto de cadena se copia a s8.

  • Además de especificar la forma de inicializar sus objetos, una clase también debe definir las operaciones que se pueden realizar sobre los objetos. Entre ellos, la clase no solo puede definir operaciones llamadas a través de nombres de funciones, al igual que la función isbn de la clase sales_item, sino que también puede definir nuevos significados de varios operadores como << y + en objetos de esta clase.

    • os<<s Escribe s en el flujo de salida os y devuelve os
      es >> s Lea la cadena de is y asígnela a s. La cadena está separada por espacios en blanco y el retorno es
      obtener línea (es, s) Lea una fila de is y asígnela a s, regresando is
      en.vacío() Devuelve verdadero si s está vacío, en caso contrario devuelve falso
      s.tamaño() Devuelve el número de caracteres en s.
      s[n] Devuelve una referencia al enésimo carácter en s, la posición n comienza desde 0
      s1 + s2 Devuelve el resultado después de conectar s1 y s2
      sl = s2 Reemplace los caracteres originales en s1 con copias de s2
      s1 == s2 Si los caracteres contenidos en s1 y s2 son exactamente iguales, son iguales; los objetos de cadena son iguales.
      s1 != s2 El juicio de igualdad es sensible al caso de las letras
      <, <= ,>, >= Las comparaciones se realizan utilizando el orden de los caracteres en el diccionario y son sensibles al caso de las letras.
  • Al igual que las operaciones de entrada y salida de tipos integrados, dichas operaciones en objetos de cadena también devuelven el operando en el lado izquierdo del operador como resultado. Por lo tanto, se pueden escribir varias entradas o varias salidas juntas:

    • string s1,s2;
      cin>>s1>>s2;
      cout<<s1<<s2<<endl;
      
  • A veces queremos conservar los espacios en blanco durante la entrada en la cadena final. En este caso, deberíamos usar la función getline en lugar del operador >> original . Los parámetros de la función getline son un flujo de entrada y un objeto de cadena. La función lee el contenido del flujo de entrada dado hasta que encuentra un carácter de nueva línea (tenga en cuenta que el carácter de nueva línea también se lee) y luego almacena el contenido leído en Go. a ese objeto de cadena (tenga en cuenta que el carácter de nueva línea no se almacena) . obtener línea solamenteFinalice la operación de lectura tan pronto como se encuentre un carácter de nueva líneaY devuelve el resultado, incluso si la entrada comienza con un carácter de nueva línea. Si la entrada realmente comienza con un carácter de nueva línea, el resultado es una cadena vacía .

  • La función vacía devuelve un valor booleano correspondiente en función de si el objeto de cadena está vacío. Al igual que el miembro isbn de la clase sales_item, vacío también es una función miembro de cadena. El método para llamar a esta función es muy simple, simplemente use el operador de punto para indicar qué objeto ejecutó la función vacía . La función de tamaño devuelve la longitud del objeto de cadena (es decir, el número de caracteres en el objeto de cadena). Puede usar la función de tamaño para generar solo líneas de más de 80 caracteres:

    • 	string line;
      	while (getline(cin, line))
      		if (!line.empty())
      			if(line.size()>80)
      				cout << line << endl;
      
  • La clase de cadena y la mayoría de los demás tipos de bibliotecas estándar definen varios tipos de soporte. Estos tipos de soporte encarnan la naturaleza independiente de la máquina de los tipos de biblioteca estándar, y el tipo size_type es uno de ellos. Cuando se usa específicamente, el operador de alcance se usa para indicar que el nombre tipo_tamaño está definido en la cadena de clase.

  • Aunque no sabemos mucho sobre los detalles del tipo string : :size_type, una cosa es segura: es un valor sin signo y puede contener el tamaño de cualquier objeto de cadena . Todas las variables utilizadas para almacenar el valor de retorno de la función de tamaño de la clase de cadena deben ser del tipo cadena:: tamaño_tipo.

  • Dado que la función de tamaño devuelve un número entero sin signo, recuerde que mezclar números con y sin signo en una expresión puede producir resultados inesperados. Por ejemplo, suponiendo que n es un int con un valor negativo, es casi seguro que la expresión s.size () < n se evaluará como verdadera. Esto es porqueUn valor negativo n se convierte automáticamente en un valor sin signo mayor. Si ya existe una función size() en una expresión, no utilice int. Esto puede evitar posibles problemas causados ​​por mezclar int y unsigned .

  • Los operadores de igualdad (== y !=) prueban respectivamente si dos objetos de cadena son iguales o no. La igualdad de objetos de cadena significa que tienen la misma longitud y contienen los mismos caracteres. Los operadores relacionales <, <=, > y >= prueban respectivamente si un objeto de cadena es menor, menor o igual, mayor o mayor o igual que otro objeto de cadena. Los operadores anteriores están todos en orden lexicográfico (distingue entre mayúsculas y minúsculas):

    • Si las longitudes de los dos objetos de cadena son diferentes y cada carácter del objeto de cadena más corto es el mismo que el carácter correspondiente del objeto de cadena más largo, se dice que el objeto de cadena más corto es más pequeño que el objeto de cadena más largo.

    • Si dos objetos de cadena son inconsistentes en algunas posiciones correspondientes, el resultado de la comparación del objeto de cadena es en realidad el resultado de la comparación del primer par de caracteres diferentes en el objeto de cadena .

  • En términos generales, al diseñar tipos de bibliotecas estándar, nos esforzamos por estar en línea con los tipos integrados en términos de facilidad de uso, por lo que la mayoría de los tipos de bibliotecas admiten operaciones de asignación . Para la clase string, se permite asignar el valor de un objeto a otro objeto:

    • string st1(10,"c"),st2;
      st1=st2;//此时均为空
      
  • Agregar dos objetos de cadena da como resultado un nuevo objeto de cadena, cuyo contenido se forma concatenando el operando de la izquierda y el operando de la derecha . Es decir, el resultado de usar el operador de suma (+) en un objeto de cadena es un nuevo objeto de cadena, y los caracteres que contiene se componen de dos partes: la primera mitad son los caracteres contenidos en el objeto de cadena de la izquierda lado del signo más, y la segunda mitad es el carácter contenido en el objeto de cadena en el lado derecho del signo más. Además, el operador de asignación compuesta (+=) es responsable de anexar el contenido del objeto de cadena de la derecha al objeto de cadena de la izquierda :

    • 	string s1 = "hello, ", s2 = "world\n";
      	string s3 = s1 + s2; // s3的内容是hello, world\n
      	sl += s2;//等价于sl = s1 + s2
      
  • Debido a que la biblioteca estándar permite convertir literales de caracteres y literales de cadena en objetos de cadena, estos dos literales se pueden usar en su lugar cuando se requieren objetos de cadena . Al mezclar objetos de cadena con literales de caracteres y literales de cadena en una declaración, debe asegurarse de que al menos uno de los operandos en ambos lados de cada operador de suma (+) sea una cadena.

  • Funciones en el archivo de encabezado cctype

    • isalno(c ) Verdadero cuando c es una letra o un número
      isalfa(c) Verdadero cuando c es una letra
      control remoto(c) Verdadero cuando c es un carácter de control
      es dígito (c) Verdadero cuando c es un número
      isgrafo(c) Verdadero cuando c no es un espacio pero es imprimible
      islower(c) Verdadero cuando c es una letra minúscula
      carrera de velocidad (c) Verdadero cuando c es un carácter imprimible (es decir, c es un espacio o c tiene una forma visual)
      puntuado(c) Verdadero cuando c es un signo de puntuación (es decir, c no es un carácter de control, un número, una letra o un espacio en blanco imprimible)
      isespacio(c) Verdadero cuando c es un espacio en blanco (es decir, c es uno de los caracteres de espacio, tabulación horizontal, tabulación vertical, retorno de carro, avance de línea y avance de línea)
      issuperior(c) Verdadero cuando c es una letra mayúscula
      esxdígito(c) Verdadero cuando c es un número hexadecimal
      torre( c) Si c es una letra mayúscula, genere la letra minúscula correspondiente; de ​​lo contrario, genere c tal como está.
      superior(c) Si c es una letra minúscula, genera la letra mayúscula correspondiente; de ​​lo contrario, genera c tal como está.
  • Además de definir funciones exclusivas del lenguaje C+, la biblioteca estándar de C++ también es compatible con la biblioteca estándar del lenguaje C.. El archivo de encabezado del lenguaje C tiene el formato name.h, y C++ nombra estos archivos cname. Es decir, se elimina el sufijo .h y se agrega la letra c antes del nombre del archivo. La c aquí indica que se trata de un archivo de encabezado que pertenece a la biblioteca estándar del lenguaje C.

  • Por lo tanto, el contenido del archivo de encabezado cctype y el archivo de encabezado ctype.h son los mismos, pero están más en línea con los requisitos del lenguaje C ++ en términos de convenciones de nomenclatura. En particular, los nombres definidos en un archivo de encabezado llamado cname pertenecen al espacio de nombres std, mientras que los nombres definidos en un archivo de encabezado llamado .h no .

  • En términos generales, los programas C++ deberían usar el archivo de encabezado llamado cname en lugar de nombre.h. Los nombres en la biblioteca estándar siempre se pueden encontrar en el espacio de nombres std. Si utiliza archivos de encabezado en formato .h, los programadores deben tener en cuenta cuáles se heredan del lenguaje C y cuáles son exclusivos del lenguaje C++.

  • Si desea hacer algo con cada carácter en el objeto de cadena, la mejor manera en este momento es usar una declaración proporcionada por el nuevo estándar C++ 11: la declaración rango para (rango para). Este tipo de declaración itera a través de cada elemento en una secuencia dada y realiza alguna operación en cada valor de la secuencia. Su sintaxis es:

    • for (declaration : expression)
      statement
      
  • Entre ellos, la parte de expresión es un objeto utilizado para representar una secuencia. La parte de declaración es responsable de definir una variable que se utilizará para acceder a los elementos básicos de la secuencia. En cada iteración, las variables en la parte de declaración se inicializan con el valor del siguiente elemento en la parte de expresión .

  • Un objeto de cadena representa una secuencia de caracteres, por lo que un objeto de cadena se puede utilizar como parte de expresión de un rango para una declaración. Para dar un ejemplo simple, podemos usar la instrucción range for para generar los caracteres en el objeto de cadena uno por línea:

    • 	string str(" some string");//每行输出str中的一个字符。
      	for (auto c : str)//对于str中的每个字符
      		cout << c << endl;//输出当前字符,后面紧跟一个换行符
      
  • El bucle for conecta las variables cy str. La forma en que definimos la variable de control del bucle es la misma que definir cualquier variable ordinaria. En este ejemplo, el compilador determina el tipo de variable c utilizando la palabra clave auto, donde el tipo de c es char . En cada iteración, el siguiente carácter de str se copia a c, por lo que el bucle se puede leer como "para cada carácter c en la cadena str", se realiza tal o cual operación. La "operación XXX" en este ejemplo genera un carácter y luego rompe la línea.

  • Si desea cambiar el valor de los caracteres en el objeto de cadena, debe definir la variable de bucle como tipo de referencia. Recuerde, una referencia es solo un alias para un objeto determinado, por lo que cuando usa una referencia como variable de control de bucle, la variable en realidad está vinculada a cada elemento de la secuencia por turno . Usando esta referencia, podemos cambiar el carácter al que está vinculado.

  • El nuevo ejemplo ya no cuenta el número de signos de puntuación. Supongamos que queremos reescribir la cadena en letras mayúsculas. Para hacer esto, puede usar la función de biblioteca estándar toupper, que toma un carácter y genera su correspondiente forma en mayúscula. De esta manera, para convertir todo el objeto de cadena a mayúsculas, simplemente llame a la función toupper en cada carácter y asigne el resultado al carácter original:

    • 	string s("Hello world!!!");//转换成大写形式。
      	for (auto& c : s)
      		//对于s 中的每个字符(注意:c是引用)
      		c = toupper(c);
      	//c是一个引用,因此赋值语句将改变s中字符的值
      	cout << s << endl;
      //输出:HELLO WORLD !!!
      
  • Si desea procesar cada carácter en un objeto de cadena, es una buena idea utilizar un rango para la declaración. Sin embargo, a veces necesitamos acceder solo a uno de los personajes, o acceder a varios personajes pero detenernos cuando encontramos una determinada condición. Por ejemplo, el mismo carácter se cambia a mayúsculas, pero el nuevo requisito ya no es hacer esto para toda la cadena, sino solo poner en mayúscula la primera letra o la primera palabra en el objeto de cadena.

  • Hay dos formas de acceder a un solo carácter en un objeto de cadena: una es usar un subíndice y la otra es usar un iterador . El parámetro de entrada recibido por el operador de subíndice ([ ]) es un valor de tipo cadena: :size_type. Este parámetro indica la posición del carácter al que se accederá; el valor de retorno es una referencia al carácter en esa posición. Los subíndices de los objetos de cadena comienzan desde 0. Si el objeto de cadena s contiene al menos dos caracteres, entonces s[0] es el primer carácter, s[1] es el segundo carácter y s[s.size()-1] es el último carácter.

  • El subíndice del objeto de cadena debe ser mayor o igual a 0 y menor que s.size(). El uso de un subíndice más allá de este rango provocará resultados impredecibles. Se puede inferir que el uso de subíndices para acceder a una cadena vacía también provocará resultados impredecibles . resultados . El valor del subíndice se denomina "subíndice" o "índice", y cualquier expresión puede usarse como índice siempre que su valor sea un valor entero. Sin embargo, si un índice es de tipo con signo, el valor se convertirá automáticamente al tipo sin signo representado por cadena::tipo_tamaño.

  • Antes de acceder al carácter especificado, primero verifique si s está vacío. De hecho, siempre que utilice un subíndice en un objeto de cadena, debe confirmar que efectivamente hay un valor en esa posición. Si s está vacío, el resultado de s[0] no estará definido.

  • Operador lógico AND (&&). Si ambos operandos involucrados en la operación son verdaderos, el resultado lógico AND es verdadero; de lo contrario, el resultado es falso. Lo más importante de este operador es que el lenguaje C++ estipula que la condición del operando de la derecha solo se verificará si el operando de la izquierda es verdadero . Como se muestra en este ejemplo, esta regla garantiza que solo cuando el valor del subíndice esté dentro de un rango razonable, el subíndice realmente se utilizará para acceder a la cadena. Es decir, s[index] no se ejecutará hasta que el índice alcance s.size(). A medida que el índice aumenta, nunca puede exceder el valor de s.size (), por lo que se garantiza que el índice sea menor que s.size ().

  • El vector de tipo de biblioteca estándar representa una colección de objetos, todos los cuales son del mismo tipo. Cada objeto de la colección tiene un índice correspondiente, que se utiliza para acceder al objeto . Debido a que el vector "contiene" otros objetos, a menudo se le llama contenedor. Para utilizar vectores, se deben incluir los archivos de encabezado adecuados. En ejemplos posteriores, se supondrá que se realiza la siguiente declaración de uso:

    • 	#include <vector>
      	using std::vector;
      
  • El lenguaje C++ tiene plantillas de clases y plantillas de funciones, donde vector es una plantilla de clase. Sólo con un conocimiento bastante profundo de C++ se pueden escribir plantillas. Afortunadamente, incluso si aún no sabes cómo crear una plantilla, puedes intentar usarla primero. La plantilla en sí no es una clase o función, sino que puedes considerarla como una instrucción para escribir una clase o función generada por el compilador . El proceso mediante el cual el compilador crea una clase o función basada en una plantilla se llama creación de instancias. Cuando se utiliza una plantilla, es necesario indicar en qué tipo el compilador debe crear una instancia de la clase o función.

  • Para las plantillas de clase, especificamos de qué tipo de clase se crea una instancia de la plantilla proporcionando información adicional. La información que se debe proporcionar está determinada por la plantilla . La forma de proporcionar información siempre es así: sigue el nombre de la plantilla con un par de corchetes angulares y coloca la información dentro de los corchetes.

    • 	vector<int> ivec;// ivec保存int类型的对象
      	vector<sales_item> sales_vec;//保存sales_item类型的对象
      	vector<vector<string>> file;//该向量的元素是vector对象
      
  • Vector puede acomodar la mayoría de los tipos de objetos como elementos, pero debido a que las referencias no son objetos, no hay ningún vector que contenga referencias . Además, la mayoría de los demás tipos integrados (sin referencia) y tipos de clases pueden formar objetos vectoriales, e incluso los elementos que componen un vector también pueden ser vectores.

  • Cabe señalar que en versiones anteriores del estándar C++, si los elementos del vector todavía eran vectores (u otros tipos de plantillas), su forma de definición era ligeramente diferente del nuevo estándar C++11 actual. En el pasado, tenías que agregar un espacio entre el corchete angular derecho del objeto vectorial externo y su tipo de elemento ; por ejemplo, debería escribirse vector<vector> en lugar de vector<vector>.

    • vector v1 v1 es un vector vacío, sus elementos potenciales son de tipo T y se realiza la inicialización predeterminada.
      vector v2(v1) v2 contiene copias de todos los elementos de v1
      vector v2 = v1 Equivalente a v2 (v1), v2 contiene copias de todos los elementos de v1
      vector v3(n, valor) v3 contiene n elementos repetidos, el valor de cada elemento es val
      vector v4(n) v4 contiene n objetos que realizan repetidamente la inicialización de valores
      vector v5{a,b,c…} v5 contiene la cantidad de elementos con valores iniciales y a cada elemento se le asigna un valor inicial correspondiente.
      vector v5 = {a, b, c… .} Equivalente a v5{ a,b,c… }
  • Por supuesto, también puede especificar el valor inicial del elemento al definir el objeto vectorial. Por ejemplo, se permite copiar los elementos de un objeto vectorial a otro objeto vectorial. En este momento, los elementos del nuevo objeto vectorial son copias de los elementos correspondientes del objeto vectorial original. Tenga en cuenta que los dos objetos vectoriales deben ser del mismo tipo :

    • vector<int> ivec;//初始状态为空
      //在此处给ivec添加一些值
      vector<int> ivec2(ivec);//把ivec的元素拷贝给ivec2
      vector<int> ivec3 = ivec; // 把ivec的元素拷贝给ivec3
      vector<string> svec(ivec2);//错误: svec的元素是string对象,不是int
      
  • Por lo general, solo puede proporcionar la cantidad de elementos que el objeto vectorial puede contener sin omitir el valor inicial. En este punto, la biblioteca creará un valor inicial de elemento inicializado y lo asignará a todos los elementos del contenedor. Este valor inicial está determinado por el tipo de elementos en el objeto vectorial.

  • Si los elementos del objeto vectorial son tipos integrados, como int, el valor inicial del elemento se establece automáticamente en 0. Si el elemento es de un determinado tipo de clase, como una cadena, la clase inicializa el elemento de forma predeterminada:

    • vector<int> ivec(10);//10个元素,每个都初始化为0
      vector<string> svec(10);//10个元素,每个都是空string对象
      
  • Hay dos restricciones especiales en este método de inicialización: primero, algunas clases requieren que el valor inicial se proporcione explícitamente. Si el tipo de elementos en el objeto vectorial no admite la inicialización predeterminada, debemos proporcionar el valor del elemento inicial . Para este tipo de objeto, la inicialización no se puede completar simplemente proporcionando el número de elementos sin establecer un valor inicial.

    • vector<int> vi = 10;//错误:必须使用直接初始化的形式指定向量大小
      
  • El 10 aquí se usa para ilustrar cómo inicializar el objeto vectorial. Nuestra intención original al usarlo es crear un objeto vectorial que contenga 10 elementos con valores inicializados, en lugar de "copiar" el número 10 en el vector. Por lo tanto, no es apropiado utilizar la inicialización de copia en este momento.

  • En algunos casos,El verdadero significado de inicialización depende de si se utilizan llaves o paréntesis para pasar el valor inicial.. Por ejemplo, al inicializar un vector con un número entero, el significado del número entero puede ser la capacidad del objeto vectorial o el valor del elemento. De manera similar, cuando se usan dos números enteros para inicializar un vector, uno de los dos números enteros puede ser la capacidad del objeto vectorial y el otro es el valor inicial del elemento, o pueden ser el valor inicial de los dos elementos en el vector. objeto con capacidad para 2. Estos significados se pueden distinguir mediante el uso de rizados o paréntesis:

    • vector<int> v1(10);// v1有10个元素,每个的值都是О
      vector<int> v2{
              
               10 };// v2有1个元素,该元素的值是10
      vector<int> v3(10, 1); // v3有10个元素,每个的值都是1
      vector<int> v4{
              
               101 }; // v4有2个元素,值分别是10和1
      
  • Si se utilizan paréntesis, se puede decir que el valor proporcionado se utiliza para construir el objeto vectorial. Por ejemplo, el valor inicial de v1 ilustra la capacidad del objeto vectorial; los dos valores iniciales de v3 ilustran la capacidad del objeto vectorial y el valor inicial del elemento respectivamente.

  • Si se utilizan llaves, se puede afirmar que queremos inicializar el objeto vectorial en la lista. En otras palabras, el proceso de inicialización tratará los valores entre llaves como una lista de valores iniciales de los elementos tanto como sea posible, y otros métodos de inicialización solo se considerarán cuando no se pueda realizar la inicialización de la lista . En el ejemplo anterior, los valores iniciales proporcionados a v2 y v4 se pueden usar como valores de elementos, por lo que ambos realizarán la inicialización de la lista.El objeto vectorial v2 contiene un elemento y el objeto vectorial v4 contiene dos elementos .

  • Por otro lado, si se usa la forma de llave durante la inicialización pero el valor proporcionado no se puede usar para la inicialización de la lista, considere usar dichos valores para construir el objeto vectorial. Por ejemplo, si desea inicializar en lista un objeto vectorial que contiene un objeto de cadena, debe proporcionar un valor inicial que pueda asignarse al objeto de cadena. En este punto, no es difícil distinguir si inicializar los elementos del objeto vectorial con una lista o construir el objeto vectorial con un valor de capacidad dado:

    • vector<string> v5{
              
               "hi" }; //列表初始化:v5有一个元素
      vector<string> v6("hi");//错误:不能使用字符串字面值构建vector对象
      vector<string> v7{
              
              10};//v7有10个默认初始化的元素
      vector<string> v8{
              
               10,"hi" };// v8有10个值为"hi"的元素
      
  • Aunque en el ejemplo anterior se utilizan llaves, excepto en la segunda declaración, solo la versión 5 es en realidad una inicialización de lista. Para inicializar en lista un objeto vectorial, el valor entre llaves debe ser del mismo tipo que el elemento. Obviamente, los objetos de cadena no se pueden inicializar con int, por lo que los valores proporcionados por v7 y v8 no se pueden usar como valor inicial del elemento. Después de confirmar que no se puede realizar la inicialización de la lista, el compilador intentará inicializar el objeto vectorial con los valores predeterminados.

  • Para objetos vectoriales, la inicialización directa es adecuada para tres situaciones: se conoce el valor inicial y el número es pequeño, el valor inicial es una copia de otro objeto vectorial y el valor inicial de todos los elementos es el mismo . Sin embargo, una situación más común es que al crear un objeto vectorial, no se conoce el número real de elementos necesarios y los valores de los elementos suelen ser inciertos . A veces, incluso si se conocen los valores iniciales de los elementos, si el número total de estos valores es grande y diferente, será demasiado engorroso realizar operaciones de inicialización al crear el objeto vectorial.

  • Un mejor enfoque es crear primero un vector vacío y luego usar las funciones miembro del vector en tiempo de ejecución.hacer retrocederAgrégale elementos . push_back es responsable de "empujar" un valor al "final (atrás)" del objeto vectorial como elemento de cola del objeto vectorial. Por ejemplo:

    • vector<int> v2;//空vector对象
      for (int i = 0; i != 100; ++i)
      	v2.push_back(i); //依次把整数值放到v2尾端//循环结束后v2有100个元素,值从0到99
      
  • De manera similar, si no se conoce el número exacto de elementos en el objeto vectorial hasta el tiempo de ejecución, debe usar el método que se acaba de describir para crear el objeto vectorial y asignarle valores. Por ejemplo, a veces es necesario leer datos en tiempo real y asignarlos a un objeto vectorial:

    • // 从标准输入中读取单词,将其作为vector对象的元素存储string word;
      vector<string> text;// 空vector对象
      while (cin >> word) {
              
              
      	text.push_back(word); // 把 word添加到text后面
      
  • El estándar C++ requiere que los vectores puedan agregar elementos de manera eficiente y rápida en tiempo de ejecución. Entonces, dado que los objetos vectoriales pueden crecer de manera eficiente, no es necesario establecer el tamaño al definir el objeto vectorial. De hecho, el rendimiento puede ser peor si lo hace. La única excepción es que todos los elementos tienen el mismo valor. Una vez que los valores de los elementos son diferentes, es más eficiente definir primero un objeto vectorial vacío y luego agregarle valores específicos en tiempo de ejecución. Vector también proporciona métodos que nos permiten mejorar aún más el rendimiento de agregar elementos dinámicamente. Crear un objeto vectorial vacío al principio y agregar elementos dinámicamente en tiempo de ejecución es diferente del uso de tipos de matrices integrados en C y la mayoría de los otros lenguajes. Especialmente si está acostumbrado a C o Java, puede esperar que sea mejor especificar la capacidad del objeto vectorial cuando lo crea. Sin embargo, en realidad suele ocurrir lo contrario.

  • Debido a que se pueden agregar elementos a objetos vectoriales de manera eficiente y conveniente, muchas tareas de programación se simplifican enormemente. Sin embargo, esta simplicidad también conlleva algunos requisitos más elevados para escribir programas: uno de ellos es garantizar que los bucles escritos sean correctos, especialmente cuando el bucle puede cambiar la capacidad del objeto vectorial. A medida que usemos más el vector, aprenderemos gradualmente sobre algunos otros requisitos implícitos, uno de los cuales debe señalarse ahora: si el cuerpo del bucle contiene declaraciones que agregan elementos al objeto vectorial,No puedes usar un rango para bucle

  • Además de push_back, vector también proporciona varias otras operaciones, la mayoría de las cuales son similares a las operaciones relacionadas con cadenas. Algunas de las más importantes se enumeran a continuación.

    • v.vacío() Devuelve verdadero si v no contiene ningún elemento; en caso contrario, devuelve falso
      v.tamaño() Devuelve el número de elementos en v
      v.push_back(t) Agrega un elemento con valor t al final de v
      v[n] Devuelve una referencia al elemento en la posición n en v
      v1 = v2 Reemplazar elementos en v1 con copias de elementos en v2
      v1 = { a, b, c… } Reemplace los elementos en v1 con copias de los elementos en la lista
      v1==v2 v1 y v2 son iguales si y solo si tienen el mismo número de elementos y los valores de los elementos en las posiciones correspondientes son los mismos.
      v1 != v2
      <, <=, >, >= Como sugiere el nombre, las comparaciones se realizan en orden de diccionario.
  • 访问vector对象中元素的方法和访问string 对象中字符的方法差不多,也是通过元素在 vector对象中的位置。例如,可以使用范围for语句处理vector对象中的所有元素:

    • vector<int> v{
              
               1,2,3,4,5,6,7,8,9 }; 
      	for (auto& i : v)//对于v中的每个元素(注意:i是一个引用)
      		i *= i;//求元素值的平方
      	for (auto i : v)//对于v中的每个元素
      		cout << i << " ";//输出该元素
      	cout << endl;
      
  • 第一个循环把控制变量 i 定义成引用类型,这样就能通过 i 给 v 的元素赋值,其中 i 的类型由auto关键字指定。这里用到了一种新的复合赋值运算符。如我们所知,+=把左侧运算对象和右侧运算对象相加,结果存入左侧运算对象;类似的,*=把左侧运算对象和右侧运算对象相乘,结果存入左侧运算对象。最后,第二个循环输出所有元素。

  • vector的 empty和 size两个成员与string的同名成员功能完全一致: empty检查vector对象是否包含元素然后返回一个布尔值; size则返回vector对象中元素的个数,返回值的类型是由vector定义的size_type类型

  • 各个相等性运算符和关系运算符也与string 的相应运算符(参见3.2.2节,第79页)功能一致。两个vector对象相等当且仅当它们所含的元素个数相同,而且对应位置的元素值也相同。关系运算符依照字典顺序进行比较:如果两个vector对象的容量不同,但是在相同位置上的元素值都一样,则元素较少的vector对象小于元素较多的vector对象;若元素的值有区别,则 vector对象的大小关系由第一对相异的元素值的大小关系决定

  • 刚接触C++语言的程序员也许会认为可以通过vector对象的下标形式来添加元素,事实并非如此。下面的代码试图为vector对象ivec添加10个元素:

    • vector<int> ivec; // 空vector对象
      for (decltype (ivec.size()) ix = 0; ix != 10; ++ix)
      	ivec[ix] = ix; //严重错误:ivec不包含任何元素
      
  • 然而,这段代码是错误的: ivec是一个空vector,根本不包含任何元素,当然也就不能通过下标去访问任何元素!如前所述,正确的方法是使用push_back:

    • for (decltype (ivec.size()) ix = 0; ix != 10; ++ix)
      	ivec.push_back(ix); //正确:添加一个新元素,该元素的值是ix
      
  • 关于下标必须明确的一点是:只能对确知已存在的元素执行下标操作。例如,

    • vector<int> ivec;// 空vector对象
      cout << ivec[0];// 错误:ivec不包含任何元素
      vector<int> ivec2(10); // 含有10个元素的vector对象
      cout << ivec2[10];//错误:ivec2元素的合法索引是从0到9
      
  • 试图用下标的形式去访问一个不存在的元素将引发错误, 不过这种错误不会被编译器发现,而是在运行时产生一个不可预知的值。不幸的是, 这种通过下标访问不存在的元素的行为非常常见, 而且会产生很严重的后果。所谓的缓冲区溢出(buffer overflow)指的就是这类错误,这也是导致PC及其他设备上应用程序出现安全问题的一个重要原因

  • 我们已经知道可以使用下标运算符来访问 string对象的字符或vector对象的元素,还有另外一种更通用的机制也可以实现同样的目的,这就是迭代器(iterator)。除了vector之外,标准库还定义了其他几种容器。所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。严格来说,string对象不属于容器类型,但是string支持很多与容器类型类似的操作。vector支持下标运算符,这点和 string一样; string支持迭代器,这也和vector是一样的。

  • 类似于指针类型,迭代器也提供了对对象的间接访问。就迭代器而言,其对象是容器中的元素或者string对象中的字符。使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另外一个元素。迭代器有有效和无效之分,这一点和指针差不多。有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置:其他所有情况都属于无效。

  • 和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为begin和 end的成员,其中 begin 成员负责返回指向第一个元素(或第一个字符)的迭代器。如有下述语句:

    • //由编译器决定b和e的类型. b表示v的第一个元素,e表示v尾元素的下一位置
      auto b = v.begin(), e = v.end(); //b和e的类型相同
      
  • end成员则负责返回指向容器(或string对象)“尾元素的下一位置(one past the end)”的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素。这样的迭代器没什么实际含义,仅是个标记而已,表示我们已经处理完了容器中的所有元素。end 成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。特殊情况下如果容器为空,则 begin和 end返回的是同一个迭代器

  • 表列举了迭代器支持的一些运算。使用==和!=来比较两个合法的迭代器是否相等,如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等;否则就说这两个迭代器不相等。

    • *iter 返回迭代器iter所指元素的引用
      iter->mem 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
      ++iter 令iter指示容器中的下一个元素
      –iter 令iter指示容器中的上一个元素
      iter1 == iter2 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等; 反之,不相等
      iter1 != iter2
  • 和指针类似,也能通过解引用迭代器来获取它所指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素。试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。

  • 举个例子,利用下标运算符把string对象的第一个字母改为了大写形式,下面利用迭代器实现同样的功能:

    • string s(""some string" ) ;
      if (s.begin() != s.end()) {
              
              // 确保s 非空
      	auto it = s.begin();// it表示s 的第一个字符
      	*it = toupper(*it);//将当前字符改成大写形式
      }
      //输出:Some string
      
  • 迭代器使用递增(++)运算符来从一个元素移动到下一个元素。从逻辑上来说,迭代器的递增和整数的递增类似,整数的递增是在整数值上“加1”,迭代器的递增则是将迭代器“向前移动一个位置”。

  • 因为 end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作

  • //依次处理s的字符直至我们处理完全部字符或者遇到空白
    for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
    	*it = toupper(*it); //将当前字符改成大写形式
    
  • 和上文的那个程序一样,上面的循环也是遍历s的字符直到遇到空白字符为止,只不过之前的程序用的是下标运算符,现在这个程序用的是迭代器。循环首先用s.begin的返回值来初始化it,意味着it指示的是s 中的第一个字符(如果有的话)。条件部分检查是否已到达s 的尾部,如果尚未到达,则将it解引用的结果传入isspace函数检查是否遇到了空白。每次迭代的最后,执行++it令迭代器前移一个位置以访问s的下一个字符。循环体内部和上一个程序if语句内的最后一句话一样,先解引用it,然后将结果传入toupper函数得到该字母对应的大写形式,再把这个大写字母重新赋值给it所指示的字符。

  • 原来使用C或Java的程序员在转而使用C++语言之后,会对for循环中使用!=而非<进行判断有点儿奇怪。C++程序员习惯性地使用!=,其原因和他们更愿意使用迭代器而非下标的原因一样:因为这种编程风格在标准库提供的所有容器上都有效。之前已经说过,只有string和 vector等一些标准库类型有下标运算符,而并非全都如此。与之类似,所有标准库容器的迭代器都定义了==和!-,但是它们中的大多数都没有定义<运算符。因此,只要我们养成使用迭代器和!=的习惯,就不用太在意用的到底是哪种容器类型。

  • 就像不知道string和 vector的size_type成员到底是什么类型一样,一般来说我们也不知道(其实是无须知道)迭代器的精确类型。而实际上,那些拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型:

    • vector<int>:: iterator it;// it能读写vector<int>的元素
      string :: iterator it2;// it2能读写string对象中的字符
      vector<int> :: const_iterator it3; // it3只能读元素,不能写元素
      string :: const_iterator it4; // it4 只能读字符,不能写字符
      
  • const_iterator和常量指针差不多,能读取但不能修改它所指的元素值。相反,iterator的对象可读可写。如果vector对象或string对象是一个常量,只能使用const_iterator;如果vector对象或string对象不是常量,那么既能使用iterator也能使用const_iterator。

  • 迭代器这个名词有三种不同的含义:可能是迭代器概念本身,也可能是指容器定义的迭代器类型,还可能是指某个迭代器对象。重点是理解存在一组概念上相关的类型,我们认定某个类型是迭代器当且仅当它支持一套操作,这套操作使得我们能访问容器的元素或者从某个元素移动到另外一个元素。每个容器类定义了一个名为 iterator 的类型,该类型支持迭代器概念所规定的一套操作。

  • begin和 end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator:

    • vector<int> v;
      const vector<int> cv;
      auto it1 = v.begin(); // it1的类型是vector<int> : : iterator
      auto it2 = cv.begin(); // it2的类型是vector<int> : : const_iterator
      
  • 有时候这种默认的行为并非我们所要。如果对象只需读操作而无须写操作的话最好使用常量类型(比如 const_iterator)。为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegin和 cend:

    • auto it3 = v.cbegin(); // it3的类型是vector<int> : : const_iterator
      
  • 解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。例如,对于一个由字符串组成的vector对象来说,要想检查其元素是否为空,令it是该vector对象的迭代器,只需检查it所指字符串是否为空就可以了

    • (*it).empty()//解引用it,然后调用结果对象的empty成员
      * it.empty()//错误:试图访问it的名为empty的成员,但it是个迭代器,//没有empty成员
      
  • 上面第二个表达式的含义是从名为it的对象中寻找其empty成员,显然it是一个迭代器,它没有哪个成员是叫empty的,所以第二个表达式将发生错误。

  • 为了简化上述表达式,C++语言定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it->mem和(*it).mem表达的意思相同。

  • 例如,假设用一个名为text的字符串向量存放文本文件中的数据,其中的元素或者是一句话或者是一个用于表示段落分隔的空字符串。如果要输出text中第一段的内容,可以利用迭代器写一个循环令其遍历text,直到遇到空字符串的元素为止:

    • //依次输出text的每一行直至遇到第一个空白行为止
      for (auto it = text.cbegin ();it != text.cend() && !it->empty0); ++it)
      	cout << *it << endl;
      
  • 我们首先初始化it令其指向text的第一个元素,循环重复执行直至处理完了text的所有元素或者发现某个元素为空。每次迭代时只要发现还有元素并且尚未遇到空元素,就输出当前正在处理的元素。值得注意的是,因为循环从头到尾只是读取text的元素而未向其中写值,所以使用了cbegin和 cend来控制整个迭代过程。

  • 虽然vector对象可以动态地增长,但是也会有一些副作用。已知的一个限制是不能在范围for循环中向vector对象添加元素。另外一个限制是任何一种可能改变vector对象容量的操作,比如 push_back,都会使该vector对象的迭代器失效。谨记,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素

  • 迭代器的递增运算令迭代器每次移动一个元素,所有的标准库容器都有支持递增运算的迭代器。类似的,也能用==和!=对任意标准库类型的两个有效迭代器进行比较。string和 vector的迭代器提供了更多额外的运算符,一方面可使得迭代器的每次移动跨过多个元素,另外也支持迭代器进行关系运算。所有这些运算被称作迭代器运算( iterator arithmetic)。

  • 要访问顺序容器和关联容器中的元素,需要通过“迭代器(iterator)”进行。迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。从这一点上看,迭代器和指针类似。不同容器的迭代器,其功能强弱有所不同。容器的迭代器的功能强弱,决定了该容器是否支持 STL 中的某种算法。例如,排序算法需要通过随机访问迭代器来访问容器中的元素,因此有的容器就不支持排序算法。

  • 不同容器的迭代器的功能

    • 容器 迭代器功能
      vector 随机访问
      deque 随机访问
      list 双向
      set / multiset 双向
      map / multimap 双向
      stack 不支持迭代器
      queue 不支持迭代器
      priority_queue 不支持迭代器
  • 可以令迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器。执行这样的操作时,结果迭代器或者指示原vector对象(或string对象)内的一个元素,或者指示原vector对象(或string对象)尾元素的下一位置。

    • // 计算得到最接近vi中间元素的一个迭代器
      auto mid = vi.begin() + vi.size() / 2;
      
  • 如果vi有20个元素,vi.size()/2得10,此例中即令mid等于vi.begin ( )+10。已知下标从О开始,则迭代器所指的元素是vi[10],也就是从首元素开始向前相隔10个位置的那个元素。对于string或vector的迭代器来说,除了判断是否相等,还能使用关系运算符(<、<=、>、>=)对其进行比较。参与比较的两个迭代器必须合法而且指向的是同一个容器的元素(或者尾元素的下一位置)

  • 只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离。所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为difference_type 的带符号整型数。string 和vector都定义了difference_type ,因为这个距离可正可负,所以difference_type是带符号类型的

  • 使用迭代器运算的一个经典算法是二分搜索。二分搜索从有序序列中寻找某个给定的值。二分搜索从序列中间的位置开始搜索,如果中间位置的元素正好就是要找的元素,搜索完成;如果不是,假如该元素小于要找的元素,则在序列的后半部分继续搜素;假如该元素大于要找的元素,则在序列的前半部分继续搜索。在缩小的范围中计算一个新的中间元素并重复之前的过程,直至最终找到目标或者没有元素可供继续搜索。

    • // text必须是有序的
      // beg 和end表示我们搜索的范围
      auto beg - text.begin(), end = text.end();
      auto mid = text.begin() + (end - beg) / 2; // 初始状态下的中间点
      //当还有元素尚未检查并且我们还没有找到sought时执行循环
      while (mid != end & &*mid != sought) {
              
              
      	if (sought < *mid)//我们要找的元素在前半部分吗 ?
      		end = mid;// 如果是,调整搜索范围使得忽略掉后半部分
      	else//我们要找的元素在后半部分
      		beg = mid + 1; // 在mid之后寻找
      	mid = beg + (end - beg) / 2;//新的中间点
      
  • 程序的一开始定义了三个迭代器: beg 指向搜索范围内的第一个元素、end指向尾元素的下一位置、mid指向中间的那个元素。初始状态下,搜索范围是名为text 的vector的全部范围。

  • 循环部分先检查搜索范围是否为空,如果mid和end 的当前值相等,说明已经找遍了所有元素。此时条件不满足,循环终止。当搜索范围不为空时,可知 mid指向了某个元素,检查该元素是否就是我们所要搜索的,如果是,也终止循环。

  • 当进入到循环体内部后,程序通过某种规则移动beg 或者end来缩小搜索的范围。如果mid所指的元素比要找的元素sought大,可推测若text含有sought,则必出现在mid所指元素的前面。此时,可以忽略mid后面的元素不再查找,并把mid赋给end即可。另一种情况,如果*mid 比 sought小,则要找的元素必出现在mid所指元素的后面。此时,通过令 beg 指向mid 的下一个位置即可改变搜索范围。因为已经验证过mid不是我们要找的对象,所以在接下来的搜索中不必考虑它。

  • 循环过程终止时,mid或者等于end或者指向要找的元素。如果mid等于end,说明text中没有我们要找的元素。

  • 按照迭代器的功能强弱,可以把迭代器分为以下几种类型:

    • 输入迭代器 (input iterator)

    • 输出迭代器 (output iterator)

    • 前向迭代器 (forward iterator)

    • 双向迭代器 (bidirectional iterator)

    • iterador de acceso aleatorio

  • El iterador es uno de los componentes de C++ STL. Se utiliza para atravesar el contenedor y es una forma universal de atravesar los elementos del contenedor. No importa en qué estructura de datos se base el contenedor, aunque diferentes estructuras de datos tienen diferentes formas de atravesar elementos. , pero el código para usar iteradores para atravesar diferentes contenedores es exactamente el mismo.

Supongo que te gusta

Origin blog.csdn.net/weixin_43424450/article/details/132344639
Recomendado
Clasificación