[C++] Explicação detalhada do conhecimento essencial para começar a usar C++

Primeiro de tudo, precisamos saber que C++ é baseado em C, incorpora ideias de programação orientada a objetos e adiciona muitas bibliotecas úteis. Este capítulo ajudará você a entender que C++ complementa as deficiências da gramática da linguagem C e como C++ otimiza o design irracional da linguagem C.

1. Espaço para nome

1. espaço para nome

Em C/C++, há um grande número de variáveis, funções, etc., e os nomes dessas variáveis, funções e classes existirão no escopo global, o que pode causar muitos conflitos. O objectivo da utilização de espaços de nomes é localizar nomes de identificadores para evitar conflitos de nomenclatura.A palavra-chave namespace parece resolver este problema.

Por exemplo, se quisermos definir uma variável sqrt, ela pode ser definida diretamente em uma variável global e depois compilada, conforme mostra a figura abaixo:

Insira a descrição da imagem aqui
No entanto, sabemos que sqrt é na verdade uma função de biblioteca incluída no arquivo de cabeçalho de math.h. Se adicionarmos o arquivo de cabeçalho de math.h, ele ainda poderá ser compilado? A resposta é não, pois eles têm o mesmo nome. Se o arquivo de cabeçalho de math.h for incluído, a compilação não passará e será relatado o erro na figura abaixo:
Insira a descrição da imagem aqui

Então existe uma boa solução? A resposta é sim. Palavras-chave como namespace foram adicionadas ao C++ para resolver esses problemas. Por exemplo, podemos colocar as variáveis ​​que precisamos definir no namespace do namespace e, em seguida, usá-lo para permitir que o compilador pesquise no namespace especificado; se o compilador não for especificado, o compilador primeiro procurará variáveis ​​no domínio global ; uso de namespace:

		#include <stdio.h>
		#include <math.h>
		
		// 命名空间的名字
		namespace Young
		{
			int sqrt = 10;
		}
		
		int main()
		{
			printf("%d\n", Young::sqrt);
			return 0;
		}

O uso do código acima é permitir que o compilador encontre a variável sqrt no namespace especificado Young e então use essa variável, para que não haja conflito de nomenclatura com a função sqrt na função da biblioteca; Young é um namespace que pode ser nomeado por si só Nome, você pode usar qualquer nome, não necessariamente Jovem.

Na printf("%d\n", Young::sqrt);imagem, o símbolo :: na frente de sqrt é chamado de qualificador de escopo , o que significa que o compilador usa coisas definidas no namespace antes do qualificador de escopo.

2. Cenários de uso de namespace

Além do acima, usamos namespace para definir variáveis ​​no namespace, também podemos definir funções, estruturas, etc.; além disso, elas também podem ser aninhadas. Por exemplo o seguinte código:

		namespace Young
		{
			//变量
			int sqrt = 10;
		
			// 函数
			int Add(int a, int b)
			{
				return a + b;
			}
		
			// 结构体
			struct ListNode
			{
				int data;
				struct ListNode* next;
			};
		
			// 嵌套使用 
			namespace Y
			{
				int a = 10;
			}
		
		}
		
		int main()
		{
			int ret = Young::Add(1, 2);
			printf("%d\n", ret);
		
			struct Young::ListNode node;
		
			printf("%d\n", Young::Y::a);
		
			return 0;
		}

Na parte da função principal do código acima, o qualificador de domínio na estrutura deve ser usado antes de ListNode, não antes de struct; ao usar namespace aninhado, olhe da direita para a esquerda e pesquise no namespace especificado;

Embora esse método possa efetivamente evitar problemas de conflito de nomenclatura, é problemático adicionar um qualificador de domínio na frente dele sempre que for usado? Na verdade, mas há outra maneira de resolver isso, expanda o namespace ; tome o namespace acima como exemplo, como o seguinte código:

		// 将命名空间展开
		using namespace Young;
		using namespace Y;
		
		int main()
		{
			int ret = Add(1, 2);
			printf("%d\n", ret);
		
			struct ListNode node;
		
			printf("%d\n",a);
		
			return 0;
		}

O código acima expande o conteúdo nos namespaces Young e Y, portanto não há necessidade de usar qualificadores de domínio; além disso, também podemos expandir o conteúdo em alguns namespaces, por exemplo, eu apenas expando a função Add para sair:

		// 展开部分
		using Young::Add;
		
		int main()
		{
			int ret = Add(1, 2);
			printf("%d\n", ret);
		
			struct Young::ListNode node;
		
			printf("%d\n", Young::Y::a);
		
			return 0;
		}

O acima é o namespace da parte expandida. Normalmente, ao trabalhar em projetos, não expandiremos o namespace, porque a expansão se tornará insegura; mas quando normalmente escrevemos exercícios de código, podemos expandir o namespace. É mais propício à nossa prática .

2. Compreender entrada e saída em C++

Primeiro de tudo, precisamos saber que C++ introduz entrada e saída diferentes da linguagem C. Na linguagem C, usamos scanf e printf como entrada e saída, mas em C++, cout objeto de saída padrão (console) e objeto de entrada padrão cin ( teclado); vejamos primeiro seu uso:

Insira a descrição da imagem aqui

Podemos entender que cout e cin no código acima são chamados de operador de inserção de fluxo e operador de extração de fluxo , respectivamente . Apresentaremos mais sobre esses dois em estudos futuros; cout e cin devem incluir o cabeçalho <iostream>. Arquivos e uso std de acordo com o método de uso do namespace, onde std é o nome do namespace da biblioteca padrão C++.C++ coloca a definição e implementação da biblioteca padrão neste namespace. Portanto, podemos expandir o namespace std:

		#include <iostream>
		using namespace std;
		
		int main()
		{
			int input;
			double d;
			// 自动识别类型
			cin >> input >> d;
		
			cout << input << endl << d << endl;
			return 0;
		}

Além disso, cin e cout também podem identificar automaticamente o tipo de variável, como o código acima, e sua saída é a seguinte:

Insira a descrição da imagem aqui

3. Parâmetros padrão

Os parâmetros padrão especificam um valor padrão para os parâmetros da função ao declarar ou definir uma função. Ao chamar esta função, se nenhum parâmetro real for especificado, o valor padrão do parâmetro formal será usado, caso contrário, o parâmetro real especificado será usado. Vejamos primeiro o uso de parâmetros padrão:

Insira a descrição da imagem aqui
No uso acima, a função Add usa parâmetros padrão. Na definição da função Add, ela especifica a = 100, b = 200, o que significa que quando a função Add for chamada, se nenhum parâmetro for passado, ela será usada. Variáveis ​​definidas por você; ao passar parâmetros, use os parâmetros reais especificados, conforme mostrado abaixo:

Insira a descrição da imagem aqui
Claro, você também pode passar apenas parte dos parâmetros, mas quando vários parâmetros aparecem, os parâmetros devem ser dados sequencialmente da direita para a esquerda e não podem ser dados em intervalos; por exemplo:

		#include <iostream>
		using namespace std;
		
		int Add(int a = 100, int b = 200, int c = 300)
		{
			return a + b + c;
		}
		
		int main()
		{
			int a = 10, b = 20, c = 30;
		
			int ret = Add(a);
			cout << ret << endl;
			return 0;
		}

O resultado de saída do código acima é 510, portanto, por exemplo, int ret = Add(a,,c);esse tipo de passagem de parâmetro não é permitido.

Então podemos classificar os parâmetros padrão . Como no código acima, Add()aqueles que não passam nada são chamados de parâmetros padrão completos ; Add(a)aqueles Add(a,b)que passam apenas parte deles são chamados de parâmetros semi-padrão .

Finalmente, devemos observar que os parâmetros padrão não podem aparecer na declaração e na definição da função ao mesmo tempo. Se aparecerem na declaração da função e na função ao mesmo tempo, só precisamos fornecer os valores padrão na declaração.

4. Sobrecarga de funções

1. O conceito de sobrecarga de função

Sobrecarga de funções : É um caso especial de funções. C++ permite que várias funções de mesmo nome com funções semelhantes sejam declaradas no mesmo escopo . Essas funções com o mesmo nome possuem listas de parâmetros formais diferentes (número de parâmetros ou tipos ou ordem de tipo ) Isso geralmente é usado para lidar com funções de implementação semelhantes ao problema de diferentes tipos de dados. Vejamos como usá-lo primeiro:

		#include <iostream>
		using namespace std;
		
		void Add(int a ,double b)
		{
			// 打印数据方便观察
			cout << "void Add(int a ,double b)" << endl;
		}
		
		
		void Add(double a, int b)
		{
			// 打印数据方便观察
			cout << "void Add(double a, int b)" << endl;
		}
		
		
		int main()
		{
		
			Add(3, 3.14);
			Add(3.14, 3);
			
			return 0;
		}

Os resultados da operação são os seguintes:

Insira a descrição da imagem aqui
No código acima, imprimimos dados na função para mostrar que o compilador chamou essa função; definimos duas funções com o mesmo nome, mas seus tipos de parâmetros são diferentes, e quando usamos essas duas funções, passamos Os parâmetros também são diferentes, então eles chamarão suas funções correspondentes;

2. O princípio da sobrecarga de função de suporte ao C++

A razão pela qual o C++ suporta sobrecarga de funções é porque o C++ tem suas próprias regras para modificar nomes de funções .
Sabemos que arquivos .cpp ou .c devem passar pelo processo de pré-processamento, compilação, montagem e vinculação antes de gerar um programa executável. Revise especificamente os blogs anteriores: Pré-processamento e ambiente de programação ;

Entre eles, durante o processo de compilação da linguagem C, o resumo do símbolo resume os nomes das funções de todos os arquivos .c . Observe que é o nome da função. Portanto, na linguagem C, nomes de funções com o mesmo nome entrarão em conflito durante o processo de compilação , e a compilação não será bem-sucedida.

No entanto, nas regras de modificação de nomes de funções em C++, C++ não é resumido com nomes de funções, mas tem suas próprias regras de modificação.As regras de modificação específicas têm diferentes regras de modificação em diferentes compiladores, por exemplo:

		void func(int i, double d)
		{}
		
		void func(double d, int i)
		{}

Essas duas funções, após a modificação da função pelo compilador g++, tornam-se [_Z+comprimento da função+nome da função+primeira letra do tipo], conforme mostrado na figura:

Insira a descrição da imagem aqui
Insira a descrição da imagem aqui
Portanto, eles podem ser distinguidos ao compilar e resumir.

5. Citação

1. O conceito de citação

Uma referência não é uma nova definição de uma variável, mas um alias para uma variável existente. O compilador não abrirá espaço de memória para a variável de referência e compartilha o mesmo espaço de memória com a variável a que se refere. Vejamos primeiro um exemplo simples:

		#include <iostream>
		using namespace std;
		
		int main()
		{
			int a = 10;
			int& b = a;
			return 0;
		}

O código acima int& b = a;está definindo o tipo de referência, b é um alias de a, a e b na verdade apontam para o mesmo espaço, a mudança de a afetará b, e a mudança de b também afetará a.

2. Características de citação

  1. As referências devem ser inicializadas quando definidas

  2. Uma variável pode ter múltiplas referências

  3. Uma vez que uma referência se refere a uma entidade, ela não pode se referir a outras entidades.

     	void Test()
     	{
     		int a = 10;
     		// int& ra;   // 该语句编译时会出错
     		int& ra = a;
     		int& rra = a;
     	}
    

int& ra; O erro de compilação ocorre porque não há inicialização no momento da definição; no código acima, rra é um alias de ra e a, essas três variáveis ​​usam o mesmo espaço, e mudanças mútuas entre elas afetarão uma à outra.

3. Frequentemente citado

Devemos respeitar uma regra ao usar citações, ou seja, no processo de citação, a autoridade pode ser alterada, e a autoridade também pode ser reduzida, mas a autoridade não pode ser ampliada. Por exemplo:

		int main()
		{
			const int a = 0;
			// 权限的放大,不允许
			//int& b = a;
		
			// 不算权限的放大,因为这里是赋值拷贝,b修改不影响a
			//int b = a; 
		
			// 权限的平移,允许
			const int& c = a;
		
			// 权限的缩小,允许
			int x = 0;
			const int& y = x;
		
			return 0;
		}

No código acima, a ampliação das permissões significa que const int a = 0;a variável a modificada por const é constante, não modificável e somente leitura, mas o int& b = a;valor de b pode ser modificado, e a modificação do valor de b afetará a, e b é legível e gravável, mas a é somente leitura, então isso é uma ampliação de permissões; mas int b = a; não conta como uma ampliação de permissões, porque é uma cópia de atribuição e a modificação de b não afeta a.

A tradução de permissões significa que todos têm as mesmas permissões.Por exemplo, const int& c = a;c e a no código acima são modificados por const, e todos são constantes, portanto é uma tradução de permissões, o que é possível.

O estreitamento de permissões no código acima int x = 0; const int& y = x;significa que x é legível e gravável, mas y é modificado por const, somente leitura, mas é permitido mudar de legível e gravável para somente leitura, isso é chamado de estreitamento de permissões .

Então, vamos dar uma olhada a que pertencem as seguintes afirmações?

		void test()
		{
			int i = 0;
			double& d = i;
		}

Primeiro de tudo, devemos entender claramente que é int i = 0; double d = i;possível se assim for, porque a promoção inteira ocorrerá entre eles; então devemos deixar claro que durante esse processo de promoção inteira, ocorrerá um processo de cópia e d fará uma cópia temporária de i, Conforme mostrado na figura abaixo, e esta cópia temporária é permanente e não pode ser modificada, então aqui está a ampliação das permissões, o que não é permitido.
Insira a descrição da imagem aqui
Portanto, a afirmação correta deveria ser a seguinte:

		void test()
		{
			int i = 0;
			const double& d = i;
		}

Se os atributos de d também se tornarem inalteráveis, então haverá um relacionamento translacional entre eles.

4. Cenários de uso de referência

(1) Criação de parâmetros (passagem de parâmetros por referência)

Nosso método comum de passar parâmetros por referência é a função de troca. Escreva uma função de troca comumente usada da seguinte forma:

		#include <iostream>
		using namespace std;
		
		void Swap(int* p1, int* p2)
		{
			int tmp = *p1;
			*p1 = *p2;
			*p2 = tmp;
		}
		
		int main()
		{
			int a = 10, b = 20;
		
			Swap(&a, &b);
			
			return 0;
		}

Nesta função de troca, precisamos passar o endereço de a e o endereço de b para alterar os valores de a e b; em C++, podemos usar referências para completar a mesma troca, o código é o seguinte:

		void Swap(int& p1, int& p2)
		{
			int tmp = p1;
			p1 = p2;
			p2 = tmp;
		}
		
		int main()
		{
			int a = 10, b = 20;
		
			Swap(a, b);
		
			return 0;
		}

Depois de usar referências, o código geral parece muito confortável. Não há necessidade de passar endereços e desreferências como ponteiros. Ao mesmo tempo, passar referências e parâmetros também pode melhorar a eficiência da passagem de parâmetros, porque toda vez que o endereço ou valor é passado , é uma cópia. É muito ineficiente copiar mais uma vez por vez; enquanto a referência não precisa ser copiada, porque o parâmetro formal é um alias do parâmetro real, portanto não há necessidade de copiar.

Além disso, o local mais confortável para passar referências e parâmetros é na lista vinculada individualmente que aprendemos antes.Por exemplo, na lista vinculada individualmente em blogs anteriores, seja inserção de cabeça ou inserção de cauda, ​​etc., você precisa passe um ponteiro secundário. Altere a estrutura geral da lista vinculada e, após C++ introduzir a referência , não há necessidade de passar o ponteiro secundário, o seguinte código:

		void SLTPushBack(SLTNode*& phead, SLTDateType x)
		{
		    // ...
		    if (phead == NULL)
		    {
		        phead = newnode;
		    }
		    else
		    {
		        //...
		    }
		}
		
		int main()
		{
		    SLTNode* plist = NULL;
		
		    SLTPushBack(plist, 1);
		    SLTPushBack(plist, 2);
		    SLTPushBack(plist, 3);
		
		    return 0;
		}

(2) Valor de retorno (retorno por referência)

Você precisa prestar atenção ao usar o retorno por referência. Ao contrário da passagem de parâmetros por referência, o retorno por referência só pode ser usado se o objeto do escopo da função ainda estiver lá. Se o objeto do escopo da função estiver fora do escopo da função, ele não poderá ser usado; como o seguinte código:

		int& func()
		{
			int n = 0;
			n = 10;
		
			return n;
		}
		
		int main()
		{
			int ret = func();
			
			return 0;
		}

Neste código, uma variável n é definida na função func, mas seu ciclo de vida é apenas dentro desta função. Seu espaço será destruído quando sair do escopo da função. Faça um desenho para entender melhor:

Insira a descrição da imagem aqui
Conforme mostrado na figura acima, após a destruição de func, n também será destruído e o espaço será retornado ao sistema operacional. No entanto, na função principal, ret é na verdade equivalente a acessar o n destruído. Isso é estritamente equivalente a um problema de ponteiro selvagem, ou seja, acesso transfronteiriço.

Porém, em diferentes compiladores, os resultados são diferentes. No vs2019, o valor de n pode ser obtido, conforme mostrado abaixo:
Insira a descrição da imagem aqui

No compilador gcc/g++, um erro é relatado, conforme mostrado abaixo:
Insira a descrição da imagem aqui
O motivo é porque depende se o compilador inicializará o espaço destruído após a destruição do quadro de pilha. Se o espaço destruído for inicializado e Continuando a acessá-lo está fora dos limites. Compiladores como gcc/g++ obviamente inicializarão o espaço durante a reciclagem do espaço, causando assim um fora dos limites; enquanto o vs2019 não tem verificações rigorosas.

Extensão : Se o código for alterado para o seguinte, ele ainda poderá ser compilado?

		int& func()
		{
			int n = 0;
			n = 10;
		
			return n;
		}
		
		int main()
		{
			int& ret = func();
			cout << ret << endl;
			cout << ret << endl;
		
			return 0;
		
		}

Aqui a recepção de ret é alterada para uma referência, ou seja, ret é o alias do n retornado. Vejamos os resultados da execução:

Insira a descrição da imagem aqui
A segunda execução é um valor aleatório, por quê? A razão é que ret é um alias de n, e eles compartilham o mesmo espaço. Quando a instrução cout é executada, uma série de quadros de pilha de funções também será criada, de modo que o novo espaço substituirá o espaço onde a função anterior está localizada , isto é, n O espaço de é coberto, ou seja, o espaço de ret é coberto, então o valor de n se torna um valor aleatório; a razão pela qual a primeira vez é 10 é que o espaço original não é coberto.

Então outro tópico é introduzido: se o espaço de n não for coberto, ainda será 10? Em seguida, modificamos o código para o seguinte código:

		int& func()
		{
			int a[1000];
			int n = 0;
			n = 10;
		
			return n;
		}
		
		int main()
		{
			int& ret = func();
			
			cout << ret << endl;
			cout << ret << endl;
		
			return 0;
		}

Dentro da função func, adicionamos um array com comprimento de 1000. Vejamos primeiro os resultados da execução:

Insira a descrição da imagem aqui

Neste momento, torna-se 10 novamente. Isso ocorre porque o espaço no quadro de pilha da função é criado para baixo, portanto, na função func, 1000 espaços são criados primeiro e, em seguida, o espaço é criado para n. A posição de n em este tempo está abaixo de ; se func for destruído, se houver nova cobertura de espaço, depende se esse espaço é maior que o espaço original de func. Se esse espaço for grande e cobrir n, então n se tornará um valor aleatório. Caso contrário , n ainda é o valor original.

Então, quais são os cenários de aplicação para retorno por referência? Nosso retorno comum por referência pode ser usado para modificar o objeto retornado. Por exemplo, em uma lista vinculada individualmente, a função de pesquisa e a função de modificação podem ser escritas juntas, e o retorno por referência pode ser usado, para que você possa encontrar o dados que você deseja encontrar e modificá-los. O valor que você deseja modificar. Por exemplo o seguinte código:

		int& SLFindOrModify(struct SeqList& ps, int i)
		{
			assert(i < ps.size);
			// ...
			return (ps.a[i]);
		}
		
		int main()
		{
			// 定义对象
			struct SeqList s;
			
			// 查找 10 这个数据,并将它修改成 20
			SLFindOrModify(s, 10) = 20;
		
			return 0;
		}

(3) A diferença entre referências e ponteiros

Agora que aprendemos ponteiros e referências, podemos descobrir que referências e ponteiros são muito semelhantes. Em muitos usos, os ponteiros podem substituir referências, e as referências também podem substituir ponteiros. Então, qual é a diferença entre eles? Vamos analisá-los um por um:

A diferença entre referências e ponteiros:

  1. Uma referência define conceitualmente um alias para uma variável e um ponteiro armazena um endereço de variável.
  2. As referências devem ser inicializadas quando definidas, ponteiros não são necessários

  3. Depois que uma referência se refere a uma entidade durante a inicialização, ela não pode mais se referir a outras entidades e um ponteiro pode apontar para qualquer entidade do mesmo tipo a qualquer momento.
  4. Nenhuma referência NULL, mas ponteiro NULL
  5. O significado em sizeof é diferente: o resultado da referência é o tamanho do tipo de referência, mas o ponteiro é sempre o número de bytes ocupados pelo espaço de endereço (
    4 bytes em uma plataforma de 32 bits)
  6. O autoincremento de referência significa que a entidade referenciada é aumentada em 1, e o autoincremento do ponteiro significa que o ponteiro é deslocado para trás pelo tamanho do tipo.
  7. Existem ponteiros multinível, mas não há referências multinível
  8. A forma de acessar entidades é diferente. O ponteiro precisa ser explicitamente desreferenciado e o compilador de referência lida com isso sozinho.
  9. As referências são relativamente mais seguras de usar do que os ponteiros

6. Funções embutidas

1. #define define macros

Já aprendemos #define a definir macros antes, como no blog anterior #defineDefine macros , as macros nos trazem muitos benefícios, como para pequenas funções que são chamadas com frequência, não há necessidade de criar um stack frame, o que melhora a eficiência; tal como o seguinte código:

		#define ADD(a,b) ((a)+(b))
		
		int main()
		{
			int ret = ADD(10, 20);
		
			cout << ret << endl;
			return 0;
		}

A macro acima define a adição de dois números. Observe que a definição da macro aqui não pode ((a)+(b))escrita(a+b)serADD(1 | 2 + 1 & 2)

A definição de macro acima é expandida e substituída diretamente no estágio de pré-processamento, de modo que nenhum quadro de pilha é criado, o que melhora muito a eficiência.

No entanto, embora as macros nos tragam benefícios, elas inevitavelmente trarão inconvenientes. Se você usar definições de macro, será fácil cometer erros. Assim como a macro para somar dois números acima, ela não funcionará sem sequer um parênteses, então existem muitas armadilhas de sintaxe em macros.

Finalmente, vamos resumir as vantagens e desvantagens das macros:

vantagem:

  1. Não há restrições estritas sobre os tipos.
  2. Não há estabelecimento de estrutura de pilha de funções, o que melhora a eficiência.

deficiência:

  1. Não é conveniente depurar macros. (Por causa da substituição durante a fase de pré-compilação)
  2. Isso resulta em baixa legibilidade do código.
  3. Não há verificações de segurança de tipo.
  4. É fácil cometer erros e tem muitas armadilhas gramaticais.

2. O conceito de funções inline

Portanto, C++ introduziu funções inline . Funções modificadas com inline são chamadas de funções inline. Ao compilar, o compilador C++ expandirá a função inline onde ela é chamada. Não há sobrecarga de chamadas de função para estabelecer quadros de pilha. As funções inline melhoram a eficiência de operação do programa. .

Por exemplo, a seguinte função embutida para adicionar dois números:

		inline int Add(int a, int b)
		{
			return a + b;
		}
		
		int main()
		{
			int ret = Add(10, 20);
		
			cout << ret << endl;
			return 0;
		}

No código acima, a função inline para adicionar dois números não cria um quadro de pilha de funções, que mostra bom desempenho, nem precisa adicionar muitos parênteses devido a problemas do operador, portanto, a função inline combina as vantagens e desvantagens de macros e funções.Projetado.

2. Características das funções embutidas

(1) Inline é um método de troca de espaço por tempo. Se o compilador tratar uma função como uma função inline, ele substituirá a chamada de função pelo corpo da função durante a fase de compilação. Desvantagem: pode tornar o arquivo de destino maior. Vantagens : menos Reduza a sobrecarga de chamadas e melhore a eficiência da execução do programa.

(2) Inline é apenas uma sugestão para o compilador. Diferentes compiladores podem ter diferentes mecanismos de implementação inline. A sugestão geral é: torne a função menor (ou seja, a função não seja muito longa). Não há uma declaração precisa e é depende da implementação interna do compilador. ), funções não recursivas e frequentemente chamadas devem ser modificadas em linha, caso contrário, o compilador ignorará o recurso em linha.

Em outras palavras, supondo que você use inline, o compilador pode não necessariamente considerar esta função como uma função inline, porque se a função for grande e a quantidade de código for grande, causará expansão do código. Portanto, considerando o desempenho geral, se usarmos funções inline e tentarmos simplificar seu código.

(3) Inline não recomenda a separação entre declaração e definição, o que levará a erros de link. Como o in-line é expandido, não há endereço de função e o link não será encontrado.

Por exemplo, defino um Test.harquivo de cabeçalho que contém a declaração da função Add:

		inline int Add(int a, int b);

Defina outro Test.cpparquivo contendo a implementação da função Add:

		#include "Test.h"
		int Add(int a, int b)
		{
			return a + b;
		}

Em seguida, main.cppchame a função Add na função:

		#include "Test.h"

		int main()
		{
			int ret = Add(10, 20);
		
			cout << ret << endl;
			return 0;
		}

Ocorreu o erro final de compilação, conforme mostrado abaixo:

Insira a descrição da imagem aqui
Qual é a razão para isso? A razão é que o arquivo de cabeçalho #include "Test.h" será expandido no arquivo main.cpp durante o estágio de pré-processamento. Após a expansão, haverá uma declaração da função Add, e inline será adicionado antes da função Add. O compilador pensará que é uma função inline e pensar que é uma função inline. será expandida diretamente, portanto não recebe um endereço válido durante a fase de compilação e não entra na tabela de símbolos; e quando a função Add é chamada na função principal , ele não encontra o endereço de sua função correspondente na tabela de símbolos, portanto, Ocorreu um erro de link.

7. palavra-chave automática

No C++ 11, auto significa que as variáveis ​​declaradas com auto devem ser deduzidas pelo compilador em tempo de compilação. Em outras palavras, auto é uma palavra-chave que deduz automaticamente o tipo com base na variável.

Por exemplo:

8. Loop for baseado em intervalo (C++ 11)

Quando precisamos percorrer um array, geralmente usamos o seguinte método:

		int main()
		{
			int arr[] = { 1,2,3,4,5,6,7,8 };
		
			for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
			{
				cout << arr[i] << " ";
			}
		
			return 0;
		}

Para uma coleção variada, é redundante e às vezes sujeito a erros que o programador especifique o intervalo do loop. Portanto, os loops for baseados em intervalo foram introduzidos no C++ 11. Os parênteses após o loop for são divididos em duas partes pelos dois pontos ":": a primeira parte é a variável no intervalo usado para iteração e a segunda parte representa o intervalo que está sendo iterado. Usando o escopo for podemos usá-lo em combinação com a palavra-chave auto que aprendemos acima, como o seguinte código:

Insira a descrição da imagem aqui
Se precisarmos alterar os valores do array, devemos usá-lo como no código a seguir?

Insira a descrição da imagem aqui

Obviamente, a resposta é não, porque e é apenas uma cópia temporária dos dados no array, e alterar o valor da cópia temporária não afeta o valor original no array, então precisamos adicionar uma referência:

		int main()
		{
			int arr[] = { 1,2,3,4,5,6,7,8 };
		
			for (auto& e : arr)
			{
				e *= 2;
			}
		
			for (auto e : arr)
			{
				cout << e << " ";
			}
			return 0;
		}

Depois de adicionar a referência, e é o alias dos dados no array. Alterar e significa alterar o conteúdo do array.

9. Valor nulo do ponteiro nullptr

Quando o ponteiro NULL foi projetado nos primeiros dias, NULL era na verdade 0, portanto, usar NULL em alguns lugares causaria chamadas de função ambíguas, por exemplo:

Insira a descrição da imagem aqui
No código acima, func constitui uma sobrecarga de função, esperamos que NULL chame void func(int*)a função, mas ele chama outra, então isso causa uma chamada de função ambígua.

Portanto, em C++ 11, nullptr foi introduzido e seu tipo é um ponteiro não digitado (void*), o que efetivamente evita a situação acima. Por exemplo, na figura abaixo, nullptr chama uma função com um tipo de ponteiro:

Insira a descrição da imagem aqui

Por fim, todo o conteúdo de Introdução ao C++ foi compartilhado. Amigos que acham que é útil para vocês, curtam e salvem ~ Obrigado pelo seu apoio!

Acho que você gosta

Origin blog.csdn.net/YoungMLet/article/details/131802354
Recomendado
Clasificación