Python Machine Learning Prediction com Flask REST API
Ciência De Dados E Bancos De Dados
Bjarne Stroustrup's A linguagem de programação C ++ tem um capítulo intitulado “Um tour pelo C ++: o básico” - C ++ padrão. Esse capítulo, em 2.2, menciona em meia página o processo de compilação e vinculação em C ++. Compilação e vinculação são dois processos muito básicos que acontecem o tempo todo durante o desenvolvimento de software C ++, mas, curiosamente, eles não são bem compreendidos por muitos desenvolvedores C ++.
Por que o código-fonte C ++ é dividido em arquivos de cabeçalho e de origem? Como cada parte é vista pelo compilador? Como isso afeta a compilação e a vinculação? Existem muitas outras perguntas como essas que você pode ter pensado, mas acabou aceitando como uma convenção.
Esteja você projetando um aplicativo C ++, implementando novos recursos para ele, tentando corrigir bugs (especialmente alguns bugs estranhos) ou tentando fazer o código C e C ++ funcionarem juntos, saber como funcionam a compilação e a vinculação economizará muito tempo e tornar essas tarefas muito mais agradáveis. Neste artigo, você aprenderá exatamente isso.
O artigo explicará como um compilador C ++ funciona com algumas das construções básicas de linguagem, responderá a algumas perguntas comuns relacionadas a seus processos e ajudará você a contornar alguns erros relacionados que os desenvolvedores costumam fazer no desenvolvimento C ++.
Observação: este artigo tem alguns exemplos de código-fonte que podem ser baixados de https://bitbucket.org/danielmunoz/cpp-article
Os exemplos foram compilados em uma máquina CentOS Linux:
$ uname -sr Linux 3.10.0-327.36.3.el7.x86_64
Usando a versão g ++:
$ g++ --version g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-11)
Os arquivos fonte fornecidos devem ser portáveis para outros sistemas operacionais, embora os Makefiles que os acompanham para o processo de construção automatizado devam ser portáveis apenas para sistemas do tipo Unix.
Cada arquivo de origem C ++ precisa ser compilado em um arquivo de objeto. Os arquivos objeto resultantes da compilação de vários arquivos de origem são então vinculados a um executável, uma biblioteca compartilhada ou uma biblioteca estática (a última delas sendo apenas um arquivo de arquivos de objeto). Os arquivos de origem C ++ geralmente têm os sufixos de extensão .cpp, .cxx ou .cc.
Um arquivo de origem C ++ pode incluir outros arquivos, conhecidos como arquivos de cabeçalho, com #include
diretiva. Os arquivos de cabeçalho têm extensões como .h, .hpp ou .hxx, ou não possuem nenhuma extensão como na biblioteca padrão C ++ e outros arquivos de cabeçalho de bibliotecas (como Qt). A extensão não importa para o pré-processador C ++, que irá substituir literalmente a linha que contém o #include
diretiva com todo o conteúdo do arquivo incluído.
A primeira etapa que o compilador fará em um arquivo de origem é executar o pré-processador nele. Apenas os arquivos de origem são passados para o compilador (para pré-processamento e compilação). Arquivos de cabeçalho não são passados para o compilador. Em vez disso, eles são incluídos nos arquivos de origem.
Cada arquivo de cabeçalho pode ser aberto várias vezes durante a fase de pré-processamento de todos os arquivos de origem, dependendo de quantos arquivos de origem os incluem ou de quantos outros arquivos de cabeçalho incluídos nos arquivos de origem também os incluem (pode haver muitos níveis de indireção) . Os arquivos fonte, por outro lado, são abertos apenas uma vez pelo compilador (e pré-processador), quando são passados para ele.
Para cada arquivo de origem C ++, o pré-processador construirá uma unidade de tradução inserindo conteúdo nela quando encontrar uma diretiva #include, ao mesmo tempo em que estará removendo o código do arquivo de origem e dos cabeçalhos quando encontrar compilação condicional blocos cuja diretiva é avaliada como false
. Também fará alguns outras tarefas como substituições de macro.
Depois que o pré-processador termina de criar aquela unidade de tradução (às vezes enorme), o compilador inicia a fase de compilação e produz o arquivo-objeto.
Para obter essa unidade de tradução (o código-fonte pré-processado), o -E
pode ser passada para o compilador g ++, junto com -o
opção para especificar o nome desejado do arquivo de origem pré-processado.
No cpp-article/hello-world
diretório, há um arquivo de exemplo “hello-world.cpp”:
#include int main(int argc, char* argv[]) { std::cout << 'Hello world' << std::endl; return 0; }
Crie o arquivo pré-processado por:
$ g++ -E hello-world.cpp -o hello-world.ii
E veja o número de linhas:
$ wc -l hello-world.ii 17558 hello-world.ii
Possui 17.588 linhas em minha máquina. Você também pode simplesmente executar make
nesse diretório e ele fará essas etapas para você.
Podemos ver que o compilador deve compilar um arquivo muito maior do que o arquivo fonte simples que vemos. Isso ocorre por causa dos cabeçalhos incluídos. E em nosso exemplo, incluímos apenas um cabeçalho. A unidade de tradução fica cada vez maior à medida que incluímos cabeçalhos.
Este processo de pré-processamento e compilação é semelhante para a linguagem C. Ele segue as regras C para compilar e a maneira como inclui arquivos de cabeçalho e produz código de objeto é quase a mesma.
Vamos ver agora os arquivos em cpp-article/symbols/c-vs-cpp-names
diretório.
ferramentas de sincronização de dados do servidor sql
Existe um arquivo fonte C simples (não C ++) chamado sum.c que exporta duas funções, uma para adicionar dois inteiros e outra para adicionar dois flutuantes:
int sumI(int a, int b) { return a + b; } float sumF(float a, float b) { return a + b; }
Compile-o (ou execute make
e todas as etapas para criar os dois aplicativos de exemplo a serem executados) para criar o arquivo de objeto sum.o:
$ gcc -c sum.c
Agora veja os símbolos exportados e importados por este arquivo de objeto:
$ nm sum.o 0000000000000014 T sumF 0000000000000000 T sumI
Nenhum símbolo é importado e dois símbolos são exportados: sumF
e sumI
. Esses símbolos são exportados como parte do segmento .text (T), portanto, são nomes de função, código executável.
Se outros arquivos de origem (C ou C ++) quiserem chamar essas funções, eles precisam declará-los antes de chamar.
A maneira padrão de fazer isso é criar um arquivo de cabeçalho que os declare e os inclua em qualquer arquivo de origem que desejamos chamá-los. O cabeçalho pode ter qualquer nome e extensão. Eu escolhi sum.h
:
#ifdef __cplusplus extern 'C' { #endif int sumI(int a, int b); float sumF(float a, float b); #ifdef __cplusplus } // end extern 'C' #endif
O que são esses ifdef
/ endif
blocos de compilação condicional? Se eu incluir este cabeçalho de um arquivo de origem C, quero que ele se torne:
int sumI(int a, int b); float sumF(float a, float b);
Mas se eu incluí-los de um arquivo de origem C ++, quero que se torne:
extern 'C' { int sumI(int a, int b); float sumF(float a, float b); } // end extern 'C'
A linguagem C não sabe nada sobre o extern 'C'
diretriz , mas o C ++ tem, e precisa dessa diretiva aplicada às declarações de função C. Isto é porque C ++ destrói nomes de funções (e métodos) porque suporta sobrecarga de função / método, enquanto C não.
Isso pode ser visto no arquivo de origem C ++ denominado print.cpp:
#include // std::cout, std::endl #include 'sum.h' // sumI, sumF void printSum(int a, int b) { std::cout << a << ' + ' << b << ' = ' << sumI(a, b) << std::endl; } void printSum(float a, float b) { std::cout << a << ' + ' << b << ' = ' << sumF(a, b) << std::endl; } extern 'C' void printSumInt(int a, int b) { printSum(a, b); } extern 'C' void printSumFloat(float a, float b) { printSum(a, b); }
Existem duas funções com o mesmo nome (printSum
) que diferem apenas no tipo de seus parâmetros: int
ou float
. A sobrecarga de funções é um recurso C ++ que não está presente em C. Para implementar esse recurso e diferenciar essas funções, C ++ destrói o nome da função, como podemos ver em seu nome de símbolo exportado (vou escolher apenas o que é relevante na saída de nm):
$ g++ -c print.cpp $ nm print.o 0000000000000132 T printSumFloat 0000000000000113 T printSumInt U sumF U sumI 0000000000000074 T _Z8printSumff 0000000000000000 T _Z8printSumii U _ZSt4cout
Essas funções são exportadas (em meu sistema) como _Z8printSumff
para a versão float e _Z8printSumii
para a versão int. Cada nome de função em C ++ é mutilado, a menos que seja declarado como extern 'C'
. Existem duas funções que foram declaradas com ligação C em print.cpp
: printSumInt
e printSumFloat
.
Portanto, eles não podem ser sobrecarregados, ou seus nomes exportados seriam os mesmos, uma vez que não estão mutilados. Eu tive que diferenciá-los um do outro fixando um Int ou um Float no final de seus nomes.
Como eles não estão mutilados, eles podem ser chamados a partir do código C, como veremos em breve.
Para ver os nomes mutilados como veríamos no código-fonte C ++, podemos usar o -C
(desmontar) opção na nm
comando. Novamente, copiarei apenas a mesma parte relevante da saída:
$ nm -C print.o 0000000000000132 T printSumFloat 0000000000000113 T printSumInt U sumF U sumI 0000000000000074 T printSum(float, float) 0000000000000000 T printSum(int, int) U std::cout
Com esta opção, em vez de _Z8printSumff
vemos printSum(float, float)
e em vez de _ZSt4cout
vemos std :: cout, que são nomes mais amigáveis para humanos.
Também vemos que nosso código C ++ está chamando o código C: print.cpp
está chamando sumI
e sumF
, que são funções C declaradas como tendo ligação C em sum.h
. Isso pode ser visto na saída nm de print.o acima, que informa sobre alguns símbolos indefinidos (U): sumF
, sumI
e std::cout
. Esses símbolos indefinidos devem ser fornecidos em um dos arquivos-objeto (ou bibliotecas) que serão vinculados com a saída desse arquivo-objeto na fase de vinculação.
Até agora, apenas compilamos o código-fonte em código-objeto, ainda não vinculamos. Se não vincularmos o arquivo de objeto que contém as definições para os símbolos importados junto com este arquivo de objeto, o vinculador irá parar com um erro de 'símbolo ausente'.
Observe também que, como print.cpp
é um arquivo fonte C ++, compilado com um compilador C ++ (g ++), todo o código nele é compilado como código C ++. Funções com ligação C como printSumInt
e printSumFloat
também são funções C ++ que podem usar recursos C ++. Apenas os nomes dos símbolos são compatíveis com C, mas o código é C ++, o que pode ser visto pelo fato de que ambas as funções estão chamando uma função sobrecarregada (printSum
), o que não poderia acontecer se printSumInt
ou printSumFloat
foram compilados em C.
Vamos ver agora print.hpp
, um arquivo de cabeçalho que pode ser incluído a partir de arquivos de origem C ou C ++, o que permitirá printSumInt
e printSumFloat
para ser chamado de C e C ++, e printSum
a ser chamado de C ++:
#ifdef __cplusplus void printSum(int a, int b); void printSum(float a, float b); extern 'C' { #endif void printSumInt(int a, int b); void printSumFloat(float a, float b); #ifdef __cplusplus } // end extern 'C' #endif
Se estivermos incluindo de um arquivo de origem C, queremos apenas ver:
void printSumInt(int a, int b); void printSumFloat(float a, float b);
printSum
não pode ser visto no código C, pois seu nome está mutilado, então não temos uma maneira (padrão e portátil) de declará-lo para o código C. Sim, posso declará-los como:
void _Z8printSumii(int a, int b); void _Z8printSumff(float a, float b);
E o vinculador não reclamará, pois esse é o nome exato que meu compilador atualmente instalado inventou para ele, mas não sei se funcionará para o seu vinculador (se o seu compilador gerar um nome mutilado diferente), ou mesmo para o próxima versão do meu vinculador. Eu nem sei se a ligação funcionará conforme o esperado devido à existência de diferentes convenções de chamada (como os parâmetros são passados e os valores de retorno são retornados) que são específicos do compilador e podem ser diferentes para chamadas C e C ++ (especialmente para funções C ++ que são funções de membro e recebem o ponteiro this como um parâmetro).
Seu compilador pode usar potencialmente uma convenção de chamada para funções regulares C ++ e outra diferente se forem declaradas como tendo ligação externa “C”. Portanto, enganar o compilador dizendo que uma função usa a convenção de chamada C enquanto na verdade usa C ++ para isso pode fornecer resultados inesperados se as convenções usadas para cada uma forem diferentes em sua cadeia de ferramentas de compilação.
tem maneiras padrão de misturar C e C ++ código e uma maneira padrão de chamar funções C ++ sobrecarregadas de C é envolva-os em funções com ligação C como fizemos envolvendo printSum
com printSumInt
e printSumFloat
.
Se incluirmos print.hpp
de um arquivo de origem C ++, o __cplusplus
a macro do pré-processador será definida e o arquivo será visto como:
void printSum(int a, int b); void printSum(float a, float b); extern 'C' { void printSumInt(int a, int b); void printSumFloat(float a, float b); } // end extern 'C'
Isso permitirá que o código C ++ chame a função sobrecarregada printSum ou seus invólucros printSumInt
e printSumFloat
.
Agora vamos criar um arquivo-fonte C contendo a função principal, que é o ponto de entrada para um programa. Esta função principal C irá chamar printSumInt
e printSumFloat
, ou seja, chamará ambas as funções C ++ com vinculação C. Lembre-se, essas são funções C ++ (seus corpos de função executam código C ++) que apenas não têm nomes mutilados em C ++. O arquivo é denominado c-main.c
:
#include 'print.hpp' int main(int argc, char* argv[]) { printSumInt(1, 2); printSumFloat(1.5f, 2.5f); return 0; }
Compile-o para gerar o arquivo objeto:
$ gcc -c c-main.c
E veja os símbolos importados / exportados:
$ nm c-main.o 0000000000000000 T main U printSumFloat U printSumInt
Exporta principais e importações printSumFloat
e printSumInt
, conforme esperado.
Para vincular tudo em um arquivo executável, precisamos usar o vinculador C ++ (g ++), já que pelo menos um arquivo que vincularemos, print.o
, foi compilado em C ++:
$ g++ -o c-app sum.o print.o c-main.o
A execução produz o resultado esperado:
$ ./c-app 1 + 2 = 3 1.5 + 2.5 = 4
Agora vamos tentar com um arquivo principal C ++, chamado cpp-main.cpp
:
#include 'print.hpp' int main(int argc, char* argv[]) { printSum(1, 2); printSum(1.5f, 2.5f); printSumInt(3, 4); printSumFloat(3.5f, 4.5f); return 0; }
Compile e veja os símbolos importados / exportados do cpp-main.o
arquivo objeto:
exemplo de arquivo de cabeçalho c ++
$ g++ -c cpp-main.cpp $ nm -C cpp-main.o 0000000000000000 T main U printSumFloat U printSumInt U printSum(float, float) U printSum(int, int)
Exporta ligação C principal e importa printSumFloat
e printSumInt
, e ambas as versões mutiladas de printSum
.
Você pode estar se perguntando por que o símbolo principal não é exportado como um símbolo mutilado como main(int, char**)
desta fonte C ++, pois é um arquivo fonte C ++ e não está definido como extern 'C'
. Bem, main
é uma função definida de implementação especial e minha implementação parece ter escolhido usar ligação C para isso, não importa se está definido em um arquivo de origem C ou C ++.
Vincular e executar o programa dá o resultado esperado:
$ g++ -o cpp-app sum.o print.o cpp-main.o $ ./cpp-app 1 + 2 = 3 1.5 + 2.5 = 4 3 + 4 = 7 3.5 + 4.5 = 8
Até agora, tive o cuidado de não incluir meus cabeçalhos duas vezes, direta ou indiretamente, do mesmo arquivo de origem. Mas, uma vez que um cabeçalho pode incluir outros cabeçalhos, o mesmo cabeçalho pode ser indiretamente incluído várias vezes. E uma vez que o conteúdo do cabeçalho é apenas inserido no local de onde foi incluído, é fácil terminar com declarações duplicadas.
Veja os arquivos de exemplo em cpp-article/header-guards
.
// unguarded.hpp class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; // guarded.hpp: #ifndef __GUARDED_HPP #define __GUARDED_HPP class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; #endif // __GUARDED_HPP
A diferença é que, em guarded.hpp, colocamos todo o cabeçalho em uma condicional que só será incluída se __GUARDED_HPP
macro do pré-processador não está definida. Na primeira vez que o pré-processador incluir este arquivo, ele não será definido. Mas, como a macro é definida dentro desse código protegido, da próxima vez que for incluída (do mesmo arquivo de origem, direta ou indiretamente), o pré-processador verá as linhas entre o #ifndef e o #endif e descartará todo o código entre eles.
Observe que este processo acontece para cada arquivo fonte que compilamos. Isso significa que este arquivo de cabeçalho pode ser incluído uma vez e apenas uma vez para cada arquivo de origem. O fato de ter sido incluído a partir de um arquivo de origem não impede que seja incluído a partir de um arquivo de origem diferente quando esse arquivo de origem for compilado. Isso apenas impedirá que seja incluído mais de uma vez no mesmo arquivo de origem.
O arquivo de exemplo main-guarded.cpp
inclui guarded.hpp
duas vezes:
#include 'guarded.hpp' #include 'guarded.hpp' int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }
Mas a saída pré-processada mostra apenas uma definição de classe A
:
$ g++ -E main-guarded.cpp # 1 'main-guarded.cpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'main-guarded.cpp' # 1 'guarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 2 'main-guarded.cpp' 2 int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }
Portanto, pode ser compilado sem problemas:
$ g++ -o guarded main-guarded.cpp
Mas o main-unguarded.cpp
o arquivo inclui unguarded.hpp
duas vezes:
#include 'unguarded.hpp' #include 'unguarded.hpp' int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }
E a saída pré-processada mostra duas definições de classe A:
$ g++ -E main-unguarded.cpp # 1 'main-unguarded.cpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'main-unguarded.cpp' # 1 'unguarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 2 'main-unguarded.cpp' 2 # 1 'unguarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 3 'main-unguarded.cpp' 2 int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }
Isso causará problemas ao compilar:
$ g++ -o unguarded main-unguarded.cpp
No arquivo incluído de main-unguarded.cpp:2:0
:
unguarded.hpp:1:7: error: redefinition of 'class A' class A { ^ In file included from main-unguarded.cpp:1:0: unguarded.hpp:1:7: error: previous definition of 'class A' class A { ^
Por uma questão de brevidade, não usarei cabeçalhos protegidos neste artigo se não for necessário, pois a maioria são exemplos curtos. Mas sempre proteja seus arquivos de cabeçalho. Não seus arquivos de origem, que não serão incluídos em nenhum lugar. Apenas arquivos de cabeçalho.
Observe by-value.cpp
arquivo em cpp-article/symbols/pass-by
:
#include #include #include // std::vector, std::accumulate, std::cout, std::endl using namespace std; int sum(int a, const int b) { cout << 'sum(int, const int)' << endl; const int c = a + b; ++a; // Possible, not const // ++b; // Not possible, this would result in a compilation error return c; } float sum(const float a, float b) { cout << 'sum(const float, float)' << endl; return a + b; } int sum(vector v) { cout << 'sum(vector)' << endl; return accumulate(v.begin(), v.end(), 0); } float sum(const vector v) { cout << 'sum(const vector)' << endl; return accumulate(v.begin(), v.end(), 0.0f); }
Como eu uso o using namespace std
diretiva, eu não tenho que qualificar os nomes dos símbolos (funções ou classes) dentro do namespace std no resto da unidade de tradução, que no meu caso é o resto do arquivo de origem. Se este fosse um arquivo de cabeçalho, eu não deveria ter inserido esta diretiva porque um arquivo de cabeçalho deve ser incluído de vários arquivos de origem; essa diretiva traria para o escopo global de cada arquivo de origem todo o namespace std a partir do ponto em que eles incluem meu cabeçalho.
Mesmo os cabeçalhos incluídos depois do meu nesses arquivos terão esses símbolos no escopo. Isso pode gerar conflitos de nomes, pois eles não esperavam que isso acontecesse. Portanto, não use esta diretiva em cabeçalhos. Use-o apenas nos arquivos de origem se desejar e somente depois de incluir todos os cabeçalhos.
Observe como alguns parâmetros são constantes. Isso significa que eles não podem ser alterados no corpo da função se tentarmos. Seria um erro de compilação. Além disso, observe que todos os parâmetros neste arquivo de origem são passados por valor, não por referência (&) ou por ponteiro (*). Isso significa que o chamador fará uma cópia deles e os passará para a função. Portanto, não importa para o chamador se eles são constantes ou não, porque se os modificarmos no corpo da função, estaremos apenas modificando a cópia, não o valor original que o chamador passou para a função.
Uma vez que a constância de um parâmetro que é passado por valor (cópia) não importa para o chamador, ele não é mutilado na assinatura da função, como pode ser visto após compilar e inspecionar o código do objeto (apenas a saída relevante):
$ g++ -c by-value.cpp $ nm -C by-value.o 000000000000001e T sum(float, float) 0000000000000000 T sum(int, int) 0000000000000087 T sum(std::vector) 0000000000000048 T sum(std::vector )
As assinaturas não expressam se os parâmetros copiados são constantes ou não nos corpos da função. Não importa. É importante apenas para a definição da função, para mostrar rapidamente ao leitor do corpo da função se esses valores irão mudar. No exemplo, apenas metade dos parâmetros são declarados como const, então podemos ver o contraste, mas se quisermos ser const-correto todos deveriam ter sido declarados assim, uma vez que nenhum deles foi modificado no corpo da função (e não deveriam).
Visto que não importa para a declaração da função que é o que o chamador vê, podemos criar o by-value.hpp
cabeçalho como este:
#include int sum(int a, int b); float sum(float a, float b); int sum(std::vector v); int sum(std::vector v);
Adicionar os qualificadores const aqui é permitido (você pode até qualificar como variáveis const que não são constantes na definição e isso vai funcionar), mas isso não é necessário e só fará as declarações desnecessariamente detalhadas.
Vamos ver by-reference.cpp
:
#include #include #include using namespace std; int sum(const int& a, int& b) { cout << 'sum(const int&, int&)' << endl; const int c = a + b; ++b; // Will modify caller variable // ++a; // Not allowed, but would also modify caller variable return c; } float sum(float& a, const float& b) { cout << 'sum(float&, const float&)' << endl; return a + b; } int sum(const std::vector& v) { cout << 'sum(const std::vector&)' << endl; return accumulate(v.begin(), v.end(), 0); } float sum(const std::vector& v) { cout << 'sum(const std::vector&)' << endl; return accumulate(v.begin(), v.end(), 0.0f); }
A constância ao passar por referência é importante para o chamador, porque dirá ao chamador se seu argumento será modificado ou não pelo receptor. Portanto, os símbolos são exportados com sua constância:
$ g++ -c by-reference.cpp $ nm -C by-reference.o 0000000000000051 T sum(float&, float const&) 0000000000000000 T sum(int const&, int&) 00000000000000fe T sum(std::vector const&) 00000000000000a3 T sum(std::vector const&)
Isso também deve estar refletido no cabeçalho que os chamadores usarão:
#include int sum(const int&, int&); float sum(float&, const float&); int sum(const std::vector&); float sum(const std::vector&);
Observe que não escrevi o nome das variáveis nas declarações (no cabeçalho) como estava fazendo até agora. Isso também é legal, para este exemplo e para os anteriores. Os nomes das variáveis não são exigidos na declaração, pois o chamador não precisa saber como você deseja nomear a sua variável. Mas os nomes dos parâmetros são geralmente desejáveis em declarações para que o usuário possa saber rapidamente o que cada parâmetro significa e, portanto, o que enviar na chamada.
quanto é o adobe xd
Surpreendentemente, os nomes das variáveis também não são necessários na definição de uma função. Eles são necessários apenas se você realmente usar o parâmetro na função. Mas se você nunca o usar, você pode deixar o parâmetro com o tipo, mas sem o nome. Por que uma função declararia um parâmetro que nunca usaria? Às vezes, funções (ou métodos) são apenas parte de uma interface, como uma interface de retorno de chamada, que define certos parâmetros que são passados para o observador. O observador deve criar um retorno de chamada com todos os parâmetros que a interface especifica, já que eles serão todos enviados pelo chamador. Mas o observador pode não estar interessado em todos eles, então ao invés de receber um aviso do compilador sobre um “parâmetro não utilizado”, a definição da função pode simplesmente deixá-lo sem um nome.
// by-pointer.cpp: #include #include #include using namespace std; int sum(int const * a, int const * const b) { cout << 'sum(int const *, int const * const)' << endl; const int c = *a+ *b; // *a = 4; // Can't change. The value pointed to is const. // *b = 4; // Can't change. The value pointed to is const. a = b; // I can make a point to another const int // b = a; // Can't change where b points because the pointer itself is const. return c; } float sum(float * const a, float * b) { cout << 'sum(int const * const, float const *)' << endl; return *a + *b; } int sum(const std::vector* v) { cout << 'sum(std::vector const *)' begin(), v->end(), 0); v = NULL; // I can make v point to somewhere else return c; } float sum(const std::vector * const v) { cout << 'sum(std::vector const * const)' begin(), v->end(), 0.0f); }
Para declarar um ponteiro para um elemento const (int no exemplo), você pode declarar o tipo como:
int const * const int *
Se você também deseja que o próprio ponteiro seja const, ou seja, que o ponteiro não possa ser alterado para apontar para outra coisa, adicione um const após a estrela:
int const * const const int * const
Se você deseja que o próprio ponteiro seja const, mas não o elemento apontado por ele:
int * const
Compare as assinaturas de função com a inspeção demangled do arquivo de objeto:
$ g++ -c by-pointer.cpp $ nm -C by-pointer.o 000000000000004a T sum(float*, float*) 0000000000000000 T sum(int const*, int const*) 0000000000000105 T sum(std::vector const*) 000000000000009c T sum(std::vector const*)
Como você pode ver, o nm
ferramenta usa a primeira notação (const após o tipo). Além disso, observe que a única constância exportada, e que importa para o chamador, é se a função modificará ou não o elemento apontado pelo ponteiro. A constância do próprio ponteiro é irrelevante para o chamador, pois o próprio ponteiro é sempre passado como uma cópia. A função só pode fazer sua própria cópia do ponteiro para apontar para outro lugar, o que é irrelevante para o chamador.
Portanto, um arquivo de cabeçalho pode ser criado como:
#include int sum(int const* a, int const* b); float sum(float* a, float* b); int sum(std::vector* const); float sum(std::vector* const);
Passar por ponteiro é como passar por referência. Uma diferença é que quando você passa por referência, espera-se que o chamador tenha passado uma referência de elemento válido, não apontando para NULL ou outro endereço inválido, enquanto um ponteiro poderia apontar para NULL, por exemplo. Ponteiros podem ser usados em vez de referências quando passar NULL tem um significado especial.
Uma vez que os valores C ++ 11 também podem ser passados com mover semântica . Este tópico não será tratado neste artigo, mas pode ser estudado em outros artigos como Passagem de argumento em C ++ .
Outro tópico relacionado que não será abordado aqui é como chamar todas essas funções. Se todos esses cabeçalhos forem incluídos de um arquivo de origem, mas não forem chamados, a compilação e a ligação serão bem-sucedidas. Mas se você quiser chamar todas as funções, haverá alguns erros porque algumas chamadas serão ambíguas. O compilador poderá escolher mais de uma versão de sum para certos argumentos, especialmente ao escolher se deseja passar por cópia ou por referência (ou referência const). Essa análise está fora do escopo deste artigo.
Vamos ver, agora, uma situação da vida real relacionada a esse assunto, onde bugs difíceis de encontrar podem aparecer.
Vá para o diretório cpp-article/diff-flags
e olhe para Counters.hpp
:
class Counters { public: Counters() : #ifndef NDEBUG // Enabled in debug builds m_debugAllCounters(0), #endif m_counter1(0), m_counter2(0) { } #ifndef NDEBUG // Enabled in debug build #endif void inc1() { #ifndef NDEBUG // Enabled in debug build ++m_debugAllCounters; #endif ++m_counter1; } void inc2() { #ifndef NDEBUG // Enabled in debug build ++m_debugAllCounters; #endif ++m_counter2; } #ifndef NDEBUG // Enabled in debug build int getDebugAllCounters() { return m_debugAllCounters; } #endif int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: #ifndef NDEBUG // Enabled in debug builds int m_debugAllCounters; #endif int m_counter1; int m_counter2; };
Esta classe possui dois contadores, que começam como zero e podem ser incrementados ou lidos. Para compilações de depuração, que é como chamarei as compilações em que NDEBUG
macro não está definida, eu também adiciono um terceiro contador, que será incrementado toda vez que qualquer um dos outros dois contadores for incrementado. Isso será uma espécie de auxiliar de depuração para esta classe. Muitas classes de bibliotecas de terceiros ou mesmo cabeçalhos C ++ embutidos (dependendo do compilador) usam truques como este para permitir diferentes níveis de depuração. Isso permite que as compilações de depuração detectem iteradores fora do intervalo e outras coisas interessantes que o criador da biblioteca poderia pensar. Chamarei builds de lançamento “builds onde o NDEBUG
macro está definido. ”
Para versões de lançamento, o cabeçalho pré-compilado se parece com (eu uso grep
para remover linhas em branco):
$ g++ -E -DNDEBUG Counters.hpp | grep -v -e '^$' # 1 'Counters.hpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'Counters.hpp' class Counters { public: Counters() : m_counter1(0), m_counter2(0) { } void inc1() { ++m_counter1; } void inc2() { ++m_counter2; } int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: int m_counter1; int m_counter2; };
Já para compilações de depuração, será semelhante a:
$ g++ -E Counters.hpp | grep -v -e '^$' # 1 'Counters.hpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'Counters.hpp' class Counters { public: Counters() : m_debugAllCounters(0), m_counter1(0), m_counter2(0) { } void inc1() { ++m_debugAllCounters; ++m_counter1; } void inc2() { ++m_debugAllCounters; ++m_counter2; } int getDebugAllCounters() { return m_debugAllCounters; } int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: int m_debugAllCounters; int m_counter1; int m_counter2; };
Há mais um contador nas compilações de depuração, conforme expliquei anteriormente.
Também criei alguns arquivos auxiliares.
// increment1.hpp: // Forward declaration so I don't have to include the entire header here class Counters; int increment1(Counters&); // increment1.cpp: #include 'Counters.hpp' void increment1(Counters& c) { c.inc1(); }
// increment2.hpp: // Forward declaration so I don't have to include the entire header here class Counters; int increment2(Counters&); // increment2.cpp: #include 'Counters.hpp' void increment2(Counters& c) { c.inc2(); }
// main.cpp: #include #include 'Counters.hpp' #include 'increment1.hpp' #include 'increment2.hpp' using namespace std; int main(int argc, char* argv[]) { Counters c; increment1(c); // 3 times increment1(c); increment1(c); increment2(c); // 4 times increment2(c); increment2(c); increment2(c); cout << 'c.get1(): ' << c.get1() << endl; // Should be 3 cout << 'c.get2(): ' << c.get2() << endl; // Should be 4 #ifndef NDEBUG // For debug builds cout << 'c.getDebugAllCounters(): ' << c.getDebugAllCounters() << endl; // Should be 3 + 4 = 7 #endif return 0; }
E um Makefile
que pode personalizar os sinalizadores do compilador para increment2.cpp
só:
all: main.o increment1.o increment2.o g++ -o diff-flags main.o increment1.o increment2.o main.o: main.cpp increment1.hpp increment2.hpp Counters.hpp g++ -c -O2 main.cpp increment1.o: increment1.cpp Counters.hpp g++ -c $(CFLAGS) -O2 increment1.cpp increment2.o: increment2.cpp Counters.hpp g++ -c -O2 increment2.cpp clean: rm -f *.o diff-flags
Então, vamos compilar tudo em modo de depuração, sem definir NDEBUG
:
$ CFLAGS='' make g++ -c -O2 main.cpp g++ -c -O2 increment1.cpp g++ -c -O2 increment2.cpp g++ -o diff-flags main.o increment1.o increment2.o
Agora execute:
$ ./diff-flags c.get1(): 3 c.get2(): 4 c.getDebugAllCounters(): 7
A saída é a esperada. Agora vamos compilar apenas um dos arquivos com NDEBUG
definido, que seria o modo de liberação, e veja o que acontece:
$ make clean rm -f *.o diff-flags $ CFLAGS='-DNDEBUG' make g++ -c -O2 main.cpp g++ -c -DNDEBUG -O2 increment1.cpp g++ -c -O2 increment2.cpp g++ -o diff-flags main.o increment1.o increment2.o $ ./diff-flags c.get1(): 0 c.get2(): 4 c.getDebugAllCounters(): 7
A saída não é a esperada. increment1
A função viu uma versão de lançamento da classe Counters, na qual existem apenas dois campos de membros int. Então, ele incrementou o primeiro campo, pensando que era m_counter1
, e não incrementou mais nada, pois não sabe nada sobre o m_debugAllCounters
campo. Eu digo que increment1
incrementou o contador porque o método inc1 em Counter
está embutido, então foi embutido em increment1
corpo de função, não chamado a partir dele. O compilador provavelmente decidiu embuti-lo porque -O2
sinalizador de nível de otimização foi usado.
Portanto, m_counter1
nunca foi incrementado e m_debugAllCounters
foi incrementado em vez dele por engano em increment1
. É por isso que vemos 0 para m_counter1
mas ainda vemos 7 para m_debugAllCounters
.
Trabalhando em um projeto onde tínhamos toneladas de arquivos fonte, agrupados em muitas bibliotecas estáticas, aconteceu que algumas dessas bibliotecas foram compiladas sem opções de depuração para std::vector
, e outras foram compiladas com essas opções.
Provavelmente em algum ponto, todas as bibliotecas estavam usando os mesmos sinalizadores, mas com o passar do tempo, novas bibliotecas foram adicionadas sem levar esses sinalizadores em consideração (eles não eram sinalizadores padrão, eles foram adicionados manualmente). Usamos um IDE para compilar, então para ver os sinalizadores de cada biblioteca, você tinha que cavar em guias e janelas, tendo sinalizadores diferentes (e múltiplos) para diferentes modos de compilação (lançamento, depuração, perfil ...), por isso foi ainda mais difícil para notar que os sinalizadores não eram consistentes.
Isso causava que, nas raras ocasiões em que um arquivo de objeto, compilado com um conjunto de sinalizadores, passasse um std::vector
para um arquivo de objeto compilado com um conjunto diferente de sinalizadores, que executava certas operações naquele vetor, o aplicativo travava. Imagine que não foi fácil depurar, uma vez que a falha foi relatada como tendo acontecido na versão de lançamento e não aconteceu na versão de depuração (pelo menos não nas mesmas situações que foram relatadas).
O depurador também fazia coisas malucas porque estava depurando um código muito otimizado. As falhas estavam ocorrendo em código correto e trivial.
Neste artigo, você aprendeu sobre algumas das construções básicas da linguagem C ++ e como o compilador trabalha com elas, desde o estágio de processamento até o estágio de vinculação. Saber como funciona pode ajudá-lo a ver todo o processo de maneira diferente e dar-lhe mais informações sobre esses processos que consideramos naturais no desenvolvimento C ++.
De um processo de compilação de três etapas à mutilação de nomes de função e produção de diferentes assinaturas de função em diferentes situações, o compilador faz muito trabalho para oferecer o poder do C ++ como uma linguagem de programação compilada.
Espero que você considere o conhecimento deste artigo útil em seus projetos C ++.
Relacionado: Como aprender as linguagens C e C ++: a lista definitiva