Função (programação de computador)

Na programação de computadores , uma função ou sub-rotina é uma sequência de instruções de programa que executa uma tarefa específica, empacotada como uma unidade. Esta unidade pode então ser usada em programas onde quer que essa tarefa específica deva ser executada.

As funções podem ser definidas dentro de programas ou separadamente em bibliotecas que podem ser usadas por vários programas. Em diferentes linguagens de programação , uma função pode ser chamada de rotina , subprograma , sub-rotina , método ou procedimento . Tecnicamente, todos esses termos têm definições diferentes e a nomenclatura varia de idioma para idioma. O termo genérico unidade exigível às vezes é usado. [1]

Uma função geralmente é codificada para que possa ser iniciada várias vezes e de vários lugares durante uma execução do programa, inclusive de outras funções e, em seguida, retornar ( retornar ) para a próxima instrução após a chamada , uma vez que a tarefa da função é concluída .

A ideia de uma sub-rotina foi inicialmente concebida por John Mauchly e Kathleen Antonelli durante seu trabalho no ENIAC , [2] e registrada em um simpósio de Harvard em janeiro de 1947 sobre "Preparação de problemas para máquinas do tipo EDVAC ". [3] Maurice Wilkes , David Wheeler , e Stanley Gill são geralmente creditados com a invenção formal deste conceito, que eles denominaram uma sub-rotina fechada , [4] [5] em contraste com uma sub-rotina aberta ou macro . [6] No entanto, Alan Turinghavia discutido sub-rotinas em um artigo de 1945 sobre propostas de design para o NPL ACE , chegando ao ponto de inventar o conceito de uma pilha de endereços de retorno . [7]

As funções são uma ferramenta de programação poderosa, [8] e a sintaxe de muitas linguagens de programação inclui suporte para escrever e usar sub-rotinas. O uso criterioso de funções (por exemplo, por meio da abordagem de programação estruturada ) geralmente reduz substancialmente o custo de desenvolvimento e manutenção de um grande programa, ao mesmo tempo em que aumenta sua qualidade e confiabilidade. [9] As funções, geralmente coletadas em bibliotecas, são um mecanismo importante para compartilhamento e comércio de software. A disciplina de programação orientada a objetos é baseada em objetos e métodos (que são funções anexadas a esses objetos ou classes de objetos ).

Conceitos principais

O conteúdo de uma função é seu corpo, que é a parte do código do programa que é executada quando a função é chamada.

Uma função pode ser escrita de modo que espere obter um ou mais valores de dados do programa de chamada (para substituir seus parâmetros ou parâmetros formais). O programa de chamada fornece valores reais para esses parâmetros, chamados de argumentos . Diferentes linguagens de programação podem usar diferentes convenções para passar argumentos:

Convenção Descrição Uso comum
Chame por valor O argumento é avaliado e a cópia do valor é passada para a função Padrão na maioria das linguagens semelhantes a Algol após Algol 60 , como Pascal, Delphi, Simula, CPL, PL/M, Modula, Oberon, Ada e muitas outras. C, C++, Java (Referências a objetos e arrays também são passadas por valor)
Chamada por referência Referência a um argumento, normalmente seu endereço é passado Selecionável na maioria das linguagens semelhantes a Algol após Algol 60 , como Algol 68, Pascal, Delphi, Simula, CPL, PL/M, Modula, Oberon, Ada e muitos outros. C++, Fortran, PL/I
Chamada por resultado O valor do parâmetro é copiado de volta para o argumento no retorno da função Parâmetros Ada OUT
Chamada por valor-resultado O valor do parâmetro é copiado de volta na entrada para a função e novamente no retorno Parâmetros Algol, Swift in-out
Chamar pelo nome Como uma macro – substitua os parâmetros pelas expressões de argumento não avaliadas e, em seguida, avalie o argumento no contexto do chamador toda vez que a rotina chamada usar o parâmetro. Algol, Scala
Chamada por valor constante Como a chamada por valor, exceto que o parâmetro é tratado como uma constante Parâmetros PL/I NÃO ATRIBUÍVEIS, parâmetros Ada IN

Uma chamada de função também pode ter efeitos colaterais , como modificar estruturas de dados na memória de um computador , ler ou gravar em um dispositivo periférico , criar um arquivo , interromper o programa ou a máquina ou até mesmo atrasar a execução do programa por um tempo especificado. Um subprograma com efeitos colaterais pode retornar resultados diferentes cada vez que é chamado, mesmo que seja chamado com os mesmos argumentos. Um exemplo é uma função de número aleatório , disponível em vários idiomas, que retorna um número pseudo-aleatório diferente cada vez que é chamada. O uso generalizado de funções com efeitos colaterais é uma característica das linguagens de programação imperativas .

Uma função pode ser codificada para que possa chamar a si mesma recursivamente , em um ou mais locais, para executar sua tarefa. Este método permite a implementação direta de funções definidas por indução matemática e algoritmos recursivos de divisão e conquista .

Uma função cuja finalidade é calcular uma função de valor booleano (ou seja, responder a uma pergunta sim/não) às vezes é chamada de predicado. Em linguagens de programação lógica , muitas vezes [ vago ] todas as funções são chamadas de predicados, uma vez que elas determinam principalmente [ vago ] o sucesso ou o fracasso. [ citação necessária ]

Uma função que não retorna nenhum valor ou retorna um valor nulo às vezes é chamada de procedimento. Procedimentos geralmente modificam seus argumentos e são uma parte central da programação procedural .

Terminologia

Uma sub-rotina é uma função que não retorna um valor. [10] O objetivo principal das funções é dividir cálculos complicados em blocos significativos e nomeá-los. [11] A função pode retornar um valor calculado para seu chamador (seu valor de retorno ), ou fornecer vários valores de resultado ou parâmetros de saída. De fato, um uso comum de funções é implementar funções matemáticas , nas quais o propósito da função é puramente computar um ou mais resultados cujos valores são inteiramente determinados pelos argumentos passados ​​para a função. (Os exemplos podem incluir o cálculo do logaritmo de um número ou o determinante de uma matriz.) Em algumas linguagens, a sintaxe de um procedimento que retorna um valor é essencialmente a mesma de um procedimento que não retorna um valor, exceto pela ausência, por exemplo, da cláusula RETURNS. Em algumas linguagens, um procedimento pode optar dinamicamente por retornar com ou sem um valor, dependendo de seus argumentos.

Suporte de linguas

As linguagens de programação de alto nível geralmente incluem construções específicas para:

  • Delimitar a parte do programa (corpo) que compõe a função
  • Atribuir um identificador (nome) à função
  • Especifique os nomes e tipos de dados de seus parâmetros e valores de retorno
  • Forneça um escopo de nomenclatura privado para suas variáveis ​​temporárias
  • Identifique variáveis ​​fora da função que são acessíveis dentro dela
  • Chame a função
  • Forneça valores para seus parâmetros
  • O programa principal contém o endereço do subprograma
  • O subprograma contém o endereço da próxima instrução da chamada de função no programa principal
  • Especifique os valores de retorno de dentro de seu corpo
  • Retornar ao programa de chamada
  • Descarte os valores retornados por uma chamada
  • Lidar com quaisquer condições excepcionais encontradas durante a chamada
  • Funções de pacote em um módulo , biblioteca , objeto ou classe

Algumas linguagens de programação , como Pascal , Fortran , Ada e muitos dialetos do BASIC , distinguem entre funções ou subprogramas de função, que fornecem um valor de retorno explícito ao programa de chamada, e sub-rotinas ou procedimentos, que não fornecem. Nessas linguagens, as chamadas de função são normalmente embutidas em expressões (por exemplo, uma sqrtfunção pode ser chamada como y = z + sqrt(x)). As chamadas de procedimento se comportam sintaticamente como instruções (por exemplo, um printprocedimento pode ser chamado como if x > 0 then print(x)ou é invocado explicitamente por uma instrução como CALLou GOSUB(por exemplo, call print(x)). Outras linguagens, comoC e Lisp , não fazem distinção entre funções e sub-rotinas.

Em linguagens de programação estritamente funcionais , como Haskell , os subprogramas podem não ter efeitos colaterais , o que significa que vários estados internos do programa não serão alterados. As funções sempre retornarão o mesmo resultado se chamadas repetidamente com os mesmos argumentos. Essas linguagens normalmente oferecem suporte apenas a funções que retornam um valor, pois as funções que não retornam um valor não têm utilidade, a menos que possam causar um efeito colateral.

Em linguagens de programação como C , C++ e C# , as funções que retornam um valor e as funções que não retornam nenhum valor são chamadas de "funções" (não confundir com funções matemáticas ou programação funcional , que são conceitos diferentes).

O compilador de uma linguagem geralmente traduz chamadas de procedimento e retorna em instruções de máquina de acordo com uma convenção de chamada bem definida , para que as funções possam ser compiladas separadamente dos programas que as chamam. As sequências de instrução correspondentes às instruções call e return são chamadas de prólogo e epílogo do procedimento .

Vantagens

As vantagens de dividir um programa em funções incluem:

  • Decompor uma tarefa de programação complexa em etapas mais simples: esta é uma das duas principais ferramentas da programação estruturada , juntamente com as estruturas de dados
  • Reduzindo o código duplicado dentro de um programa
  • Habilitando a reutilização de código em vários programas
  • Dividir uma grande tarefa de programação entre vários programadores ou várias etapas de um projeto
  • Ocultar detalhes de implementação dos usuários da função
  • Melhorar a legibilidade do código substituindo um bloco de código por uma chamada de função em que um nome de função descritivo serve para descrever o bloco de código. Isso torna o código de chamada conciso e legível, mesmo que a função não deva ser reutilizada.
  • Melhorar a rastreabilidade (ou seja, a maioria das linguagens oferece maneiras de obter o rastreamento de chamada que inclui os nomes das funções envolvidas e talvez até mais informações, como nomes de arquivos e números de linha); ao não decompor o código em funções, a depuração seria severamente prejudicada

Desvantagens

Comparado ao uso de código em linha, invocar uma função impõe alguma sobrecarga computacional no mecanismo de chamada. [ citação necessária ]

Uma função normalmente requer um código de manutenção padrão – tanto na entrada quanto na saída da função ( prólogo e epílogo da função – geralmente salvando registros de uso geral e endereço de retorno no mínimo).

História

A ideia de uma sub-rotina foi elaborada depois que as máquinas de computação já existiam há algum tempo. As instruções de salto aritmético e condicional foram planejadas com antecedência e mudaram relativamente pouco, mas as instruções especiais usadas para chamadas de procedimento mudaram muito ao longo dos anos. Os primeiros computadores e microprocessadores, como o Manchester Baby e o RCA 1802 , não tinham uma única instrução de chamada de sub-rotina. As sub-rotinas podiam ser implementadas, mas exigiam que os programadores usassem a sequência de chamada - uma série de instruções - em cada local de chamada .

As sub-rotinas foram implementadas no Z4 de Konrad Zuse em 1945.

Em 1945, Alan M. Turing usou os termos "enterrar" e "desenterrar" como um meio de chamar e retornar de sub-rotinas. [12] [13]

Em janeiro de 1947, John Mauchly apresentou notas gerais no 'A Symposium of Large Scale Digital Calculating Machinery' sob o patrocínio conjunto da Universidade de Harvard e do Bureau of Ordnance, Marinha dos Estados Unidos. Aqui ele discute a operação serial e paralela sugerindo

...a estrutura da máquina não precisa ser nem um pouco complicada. É possível, desde que todas as características lógicas essenciais para este procedimento estejam disponíveis, desenvolver uma instrução de codificação para colocar as sub-rotinas na memória em lugares conhecidos pela máquina, e de forma que possam ser facilmente colocadas em uso.

Em outras palavras, pode-se designar a sub-rotina A como divisão e a sub-rotina B como multiplicação complexa e a sub-rotina C como a avaliação de um erro padrão de uma sequência de números, e assim sucessivamente através da lista de sub-rotinas necessárias para um determinado problema. ... Todas essas sub-rotinas serão então armazenadas na máquina, bastando fazer uma breve referência a elas por número, conforme indicado na codificação. [3]

Kay McNulty trabalhou em estreita colaboração com John Mauchly na equipe do ENIAC e desenvolveu uma ideia para sub-rotinas para o computador ENIAC que ela estava programando durante a Segunda Guerra Mundial. [14] Ela e os outros programadores do ENIAC usaram as sub-rotinas para ajudar a calcular as trajetórias dos mísseis. [14]

Goldstine e von Neumann escreveram um artigo datado de 16 de agosto de 1948 discutindo o uso de sub-rotinas. [15]

Alguns computadores e microprocessadores muito antigos, como o IBM 1620 , o Intel 4004 e o Intel 8008 , e os microcontroladores PIC , têm uma chamada de sub-rotina de instrução única que usa uma pilha de hardware dedicada para armazenar endereços de retorno - esse hardware suporta apenas alguns níveis de aninhamento de sub-rotina, mas pode suportar sub-rotinas recursivas. As máquinas anteriores a meados da década de 1960 - como o UNIVAC I , o PDP-1 e o IBM 1130 - normalmente usavam uma convenção de chamadaque salvou o contador de instruções no primeiro local de memória da sub-rotina chamada. Isso permite níveis arbitrariamente profundos de aninhamento de sub-rotinas, mas não oferece suporte a sub-rotinas recursivas. O IBM System/360 tinha uma instrução de chamada de sub-rotina que colocava o valor do contador de instruções salvas em um registrador de uso geral; isso pode ser usado para suportar o aninhamento de sub-rotina arbitrariamente profundo e sub-rotinas recursivas. O PDP-11 (1970) é um dos primeiros computadores com uma instrução de chamada de sub-rotina push-push; esse recurso também oferece suporte a aninhamento de sub-rotina arbitrariamente profundo e sub-rotina recursiva. [16]

Suporte de linguas

Nos primeiros montadores, o suporte a sub-rotinas era limitado. As sub-rotinas não eram explicitamente separadas umas das outras ou do programa principal e, de fato, o código-fonte de uma sub-rotina podia ser intercalado com o de outros subprogramas. Alguns montadores ofereceriam macros predefinidas para gerar as sequências de chamada e retorno. Na década de 1960, os montadores geralmente tinham um suporte muito mais sofisticado para sub-rotinas embutidas e montadas separadamente que podiam ser vinculadas.

Uma das primeiras linguagens de programação a suportar sub-rotinas e funções escritas pelo usuário foi o FORTRAN II . O compilador IBM FORTRAN II foi lançado em 1958. ALGOL 58 e outras linguagens de programação anteriores também suportavam programação processual.

bibliotecas

Mesmo com essa abordagem incômoda, as sub-rotinas se mostraram muito úteis. Eles permitiram o uso do mesmo código em muitos programas diferentes. A memória era um recurso muito escasso nos primeiros computadores e as sub-rotinas permitiam uma economia significativa no tamanho dos programas.

Muitos dos primeiros computadores carregavam as instruções do programa na memória a partir de uma fita de papel perfurada . Cada sub-rotina poderia então ser fornecida por um pedaço separado de fita, carregado ou emendado antes ou depois do programa principal (ou "linha principal" [17] ); e a mesma fita de sub-rotina poderia então ser usada por muitos programas diferentes. Uma abordagem semelhante foi aplicada em computadores que usavam cartões perfurados como entrada principal. O nome biblioteca de sub-rotinas significava originalmente uma biblioteca, no sentido literal, que mantinha coleções indexadas de fitas ou baralhos de cartas para uso coletivo.

Retorno por salto indireto

Para eliminar a necessidade de código auto-modificável , os projetistas de computadores eventualmente forneceram uma instrução de salto indireta , cujo operando, em vez de ser o próprio endereço de retorno , era a localização de uma variável ou registrador do processador contendo o endereço de retorno.

Nesses computadores, em vez de modificar o salto de retorno da função, o programa chamador armazenaria o endereço de retorno em uma variável para que, quando a função fosse concluída, executasse um salto indireto que direcionaria a execução para o local fornecido pela variável predefinida.

Ir para a sub-rotina

Outro avanço foi o salto para instrução de sub-rotina, que combinava o salvamento do endereço de retorno com o salto de chamada, minimizando significativamente a sobrecarga .

No IBM System/360 , por exemplo, as instruções de desvio BAL ou BALR, destinadas à chamada de procedimento, salvariam o endereço de retorno em um registrador do processador especificado na instrução, por convenção do registrador 14. Para retornar, bastava a sub-rotina executar uma instrução de desvio indireto (BR) por meio desse registrador. Se a sub-rotina precisasse daquele registrador para algum outro propósito (como chamar outra sub-rotina), ela salvaria o conteúdo do registrador em um local de memória privada ou em uma pilha de registradores .

Em sistemas como o HP 2100 , a instrução JSB executaria uma tarefa semelhante, exceto que o endereço de retorno era armazenado no local da memória que era o destino da ramificação. A execução do procedimento realmente começaria no próximo local de memória. Na linguagem assembly do HP 2100, escreveríamos, por exemplo

...
JSB MYSUB (Chama a sub-rotina MYSUB.)
BB... (Voltarei aqui depois que o MYSUB terminar.)

para chamar uma sub-rotina chamada MYSUB do programa principal. A sub-rotina seria codificada como

MYSUB NOP (Armazenamento para o endereço de retorno do MYSUB.)
AA ... (Início do corpo de MYSUB.)
...
JMP MYSUB,I (Retorna ao programa de chamada.)

A instrução JSB colocou o endereço da instrução NEXT (ou seja, BB) no local especificado como seu operando (ou seja, MYSUB) e, em seguida, desviou para o local NEXT depois disso (ou seja, AA = MYSUB + 1). A sub-rotina pode então retornar ao programa principal executando o salto indireto JMP MYSUB, I que se ramifica para o local armazenado no local MYSUB.

Compiladores para Fortran e outras linguagens podem facilmente usar essas instruções quando disponíveis. Essa abordagem suportava vários níveis de chamadas; no entanto, como o endereço de retorno, os parâmetros e os valores de retorno de uma sub-rotina foram atribuídos a locais de memória fixos, isso não permitia chamadas recursivas.

Aliás, um método semelhante foi usado pelo Lotus 1-2-3 , no início dos anos 1980, para descobrir as dependências de recálculo em uma planilha. Ou seja, um local foi reservado em cada célula para armazenar o endereço do remetente . Como as referências circulares não são permitidas para ordem de recálculo natural, isso permite uma caminhada na árvore sem reservar espaço para uma pilha na memória, que era muito limitada em computadores pequenos como o IBM PC .

pilha de chamadas

A maioria das implementações modernas de uma chamada de função usa uma pilha de chamada , um caso especial da estrutura de dados da pilha , para implementar chamadas e retornos de função. Cada chamada de procedimento cria uma nova entrada, chamada de quadro de pilha , no topo da pilha; quando o procedimento retorna, seu quadro de pilha é excluído da pilha e seu espaço pode ser usado para outras chamadas de procedimento. Cada quadro de pilha contém os dados privados da chamada correspondente, que normalmente inclui os parâmetros e variáveis ​​internas do procedimento e o endereço de retorno.

A sequência de chamada pode ser implementada por uma sequência de instruções comuns (uma abordagem ainda usada em arquiteturas de computação de conjunto de instruções reduzidas (RISC) e palavra de instrução muito longa (VLIW), mas muitas máquinas tradicionais projetadas desde o final da década de 1960 incluíram instruções especiais para esse propósito.

A pilha de chamadas geralmente é implementada como uma área contígua de memória. É uma escolha arbitrária de projeto se a parte inferior da pilha é o endereço mais baixo ou mais alto dentro dessa área, de modo que a pilha possa crescer para frente ou para trás na memória; no entanto, muitas arquiteturas escolheram o último. [ citação necessária ]

Alguns projetos, principalmente algumas implementações do Forth , usavam duas pilhas separadas, uma principalmente para informações de controle (como endereços de retorno e contadores de loop) e outra para dados. O primeiro era, ou funcionava como, uma pilha de chamadas e era acessível apenas indiretamente ao programador por meio de outras construções de linguagem, enquanto o último era acessível mais diretamente.

Quando as chamadas de procedimento baseadas em pilha foram introduzidas pela primeira vez, uma motivação importante era economizar memória preciosa. [ citação necessária ] Com este esquema, o compilador não precisa reservar espaço separado na memória para os dados privados (parâmetros, endereço de retorno e variáveis ​​locais) de cada procedimento. Em qualquer momento, a pilha contém apenas os dados privados das chamadas que estão atualmente ativas (ou seja, que foram chamadas, mas ainda não retornaram). Por causa das maneiras como os programas geralmente eram montados a partir de bibliotecas, não era (e ainda é) incomum encontrar programas que incluem milhares de funções, das quais apenas um punhado está ativo em um determinado momento. [ citação necessária ]Para tais programas, o mecanismo de pilha de chamadas pode economizar quantidades significativas de memória. De fato, o mecanismo de pilha de chamadas pode ser visto como o método mais antigo e simples para gerenciamento automático de memória .

No entanto, outra vantagem do método de pilha de chamadas é que ele permite chamadas de função recursivas , pois cada chamada aninhada para o mesmo procedimento obtém uma instância separada de seus dados privados.

Em um ambiente multithread , geralmente há mais de uma pilha. [18] Um ambiente que suporta totalmente corrotinas ou avaliação preguiçosa pode usar estruturas de dados diferentes de pilhas para armazenar seus registros de ativação.

Empilhamento retardado

Uma desvantagem do mecanismo de pilha de chamadas é o aumento do custo de uma chamada de procedimento e seu retorno correspondente. [ esclarecimento necessário ] O custo extra inclui incrementar e decrementar o ponteiro da pilha (e, em algumas arquiteturas, verificar o estouro da pilha ) e acessar as variáveis ​​e parâmetros locais por endereços relativos ao quadro, em vez de endereços absolutos. O custo pode ser percebido em maior tempo de execução ou maior complexidade do processador, ou ambos.

Essa sobrecarga é mais óbvia e questionável em procedimentos de folha ou funções de folha , que retornam sem fazer nenhuma chamada de procedimento. [19] [20] [21] Para reduzir essa sobrecarga, muitos compiladores modernos tentam atrasar o uso de uma pilha de chamadas até que seja realmente necessário. [ citação necessária ] Por exemplo, a chamada de um procedimento P pode armazenar o endereço de retorno e os parâmetros do procedimento chamado em determinados registradores do processador e transferir o controle para o corpo do procedimento por um simples salto. Se o procedimento P retornar sem fazer nenhuma outra chamada, a pilha de chamadas não será usada. Se Pprecisar chamar outro procedimento Q , ele usará a pilha de chamadas para salvar o conteúdo de quaisquer registradores (como o endereço de retorno) que serão necessários após o retorno de Q.

Exemplos

C e C++

Nas linguagens de programação C e C++ , os subprogramas são chamados de funções (também classificados como funções de membro quando associados a uma classe ou funções livres [22] quando não). Essas linguagens usam a palavra-chave especial voidpara indicar que uma função não retorna nenhum valor. Observe que as funções C/C++ podem ter efeitos colaterais, incluindo a modificação de quaisquer variáveis ​​cujos endereços são passados ​​como parâmetros. Exemplos:

void Function1 () { /* algum código */ }    

A função não retorna um valor e deve ser chamada como uma função autônoma, por exemplo,Function1();

função int2 () { return 5 ; }  
   

Esta função retorna um resultado (o número 5), e a chamada pode ser parte de uma expressão, por exemplo,x + Function2()

char Function3 ( int number ) { char selection [] = { 'S' , 'M' , 'T' , 'W' , 'T' , 'F' , 'S' }; seleção de retorno [ número ]; }   
           
   

Esta função converte um número entre 0 e 6 na letra inicial do dia da semana correspondente, ou seja, 0 para 'S', 1 para 'M', ..., 6 para 'S'. O resultado da chamada pode ser atribuído a uma variável, por exemplo, num_day = Function3(number);.

void Function4 ( int * pointer_to_var ) { ( * pointer_to_var ) ++ ; }   
  

Esta função não retorna um valor, mas modifica a variável cujo endereço é passado como parâmetro; seria chamado com Function4(&variable_to_increment);.

dialetos BÁSICOS

Microsoft Pequeno Básico

Exemplo () ' Chama a sub-rotina                               

Sub Exemplo ' Inicia a subrotina TextWindow . WriteLine ( "Este é um exemplo de uma sub-rotina no Microsoft Small Basic." ) ' O que a sub-rotina faz EndSub ' Encerra a sub-rotina                              
      
                                  

No exemplo acima, Example()chama a sub-rotina. [23] Para definir a sub-rotina atual, a Subpalavra-chave deve ser usada, com o nome da sub-rotina após Sub. Após o conteúdo ter seguido, EndSubdeve ser digitado.

Visual Basic (clássico)

Na linguagem Visual Basic (clássica) , os subprogramas são denominados funções ou subs (ou métodos quando associados a uma classe). O Visual Basic 6 usa vários termos chamados tipos para definir o que está sendo passado como parâmetro. Por padrão, uma variável não especificada é registrada como um tipo de variante e pode ser passada como ByRef (padrão) ou ByVal . Além disso, quando uma função ou sub é declarada, ela recebe uma designação public, private ou friend, que determina se ela pode ser acessada fora do módulo ou projeto em que foi declarada.

  • Por valor [ByVal] – uma forma de passar o valor de um argumento para um procedimento passando uma cópia do valor, em vez de passar o endereço. Como resultado, o valor real da variável não pode ser alterado pelo procedimento para o qual é passado.
  • Por referência [ByRef] – uma forma de passar o valor de um argumento para um procedimento passando um endereço da variável, em vez de passar uma cópia de seu valor. Isso permite que o procedimento acesse a variável real. Como resultado, o valor real da variável pode ser alterado pelo procedimento para o qual ela é passada. A menos que especificado de outra forma, os argumentos são passados ​​por referência.
  • Public (opcional) – indica que o procedimento da função é acessível a todos os outros procedimentos em todos os módulos. Se usado em um módulo que contém uma Opção Private, o procedimento não está disponível fora do projeto.
  • Private (opcional) – indica que o procedimento da função é acessível apenas para outros procedimentos no módulo onde é declarado.
  • Amigo (opcional) – usado apenas em um módulo de classe. Indica que o procedimento Function é visível em todo o projeto, mas não visível para um controlador de uma instância de um objeto.
Private Function Function1 () ' Some Code Here End Function  
    
 

A função não retorna um valor e deve ser chamada como uma função autônoma, por exemplo,Function1

Função Privada Function2 () as Integer Function2 = 5 End Function    
      
 

Esta função retorna um resultado (o número 5), e a chamada pode ser parte de uma expressão, por exemplo,x + Function2()

Função privada Function3 ( ByVal intValue as Integer ) as String Dim strArray ( 6 ) as String strArray = Array ( "M" , "T" , "W" , "T" , "F" , "S" , "S" ) Function3 = strArray ( intValue ) Função final       
       
            
      
 

Esta função converte um número entre 0 e 6 na letra inicial do dia da semana correspondente, ou seja, 0 para 'M', 1 para 'T', ..., 6 para 'S'. O resultado da chamada pode ser atribuído a uma variável, por exemplo, num_day = Function3(number).

Função Privada Function4 ( ByRef intValue as Integer ) intValue = intValue + 1 End Function     
        
 

Esta função não retorna um valor, mas modifica a variável cujo endereço é passado como parâmetro; seria chamado com " Function4(variable_to_increment)".

PL/I

Em PL/I, um procedimento chamado pode receber um descritor fornecendo informações sobre o argumento, como comprimentos de string e limites de array. Isso permite que o procedimento seja mais geral e elimina a necessidade do programador passar tais informações. Por padrão, PL/I passa argumentos por referência. Uma função (trivial) para alterar o sinal de cada elemento de uma matriz bidimensional pode ser semelhante a:

change_sign: procedimento(array);
  declara array(*,*) float;
  matriz = -matriz;
fim change_sign;

Isso pode ser chamado com vários arrays da seguinte maneira:

/* limites do primeiro array de -5 a +10 e 3 a 9 */
declara array1 (-5:10, 3:9) float;
/* limites do segundo array de 1 a 16 e 1 a 16 */
declara array2 (16,16) float;
chamar change_sign(array1);
chamar change_sign(array2);

Pitão

Em Python , a palavra-chave defé usada para definir uma função. As instruções que formam o corpo da função devem continuar na mesma linha ou começar na próxima linha e ser indentadas. [24] O programa de exemplo a seguir imprime "Hello world!" seguido por "Wikipedia" na próxima linha.

def  simple_function (): 
    print ( 'Hello world!' ) 
    print ( 'Wikipedia' ) 
simple_function () 
# a saída será: 
Hello  World ! 
Wikipédia
                  

def  func ( nome ) 
    print ( "bem-vindo" + nome ) 
print ( func ( "martin" )) 
#saída será: bem-vindo martin


Variáveis ​​locais, recursão e reentrância

Um subprograma pode achar útil usar uma certa quantidade de espaço temporário ; ou seja, a memória usada durante a execução desse subprograma para armazenar resultados intermediários. As variáveis ​​armazenadas nesse espaço de rascunho são denominadas variáveis ​​locais e o espaço de rascunho é denominado registro de ativação . Um registro de ativação geralmente tem um endereço de retorno que informa para onde passar o controle de volta quando o subprograma terminar.

Um subprograma pode ter qualquer número e natureza de locais de chamada. Se a recursão for suportada, um subprograma pode até chamar a si mesmo, fazendo com que sua execução seja suspensa enquanto outra execução aninhada do mesmo subprograma ocorre. A recursão é um meio útil para simplificar alguns algoritmos complexos e quebrar problemas complexos. Linguagens recursivas geralmente fornecem uma nova cópia de variáveis ​​locais em cada chamada. Se o programador deseja que o valor das variáveis ​​locais permaneça o mesmo entre as chamadas, elas podem ser declaradas estáticas em algumas linguagens, ou valores globais ou áreas comuns podem ser usados. Aqui está um exemplo de uma função recursiva em C/C++ para encontrar números de Fibonacci :

int Fib ( int n ) { if ( n <= 1 ) { return n ; } return Fib ( n - 1 ) + Fib ( n - 2 ); }   
      
     
  
         

As primeiras linguagens como Fortran inicialmente não suportavam a recursão porque as variáveis ​​eram alocadas estaticamente, assim como o local para o endereço de retorno. [25] Os primeiros conjuntos de instruções de computador dificultavam o armazenamento de endereços de retorno e variáveis ​​em uma pilha. Máquinas com registradores de índice ou registradores de uso geral , por exemplo, série CDC 6000 , PDP-6 , GE 635 , System/360 , série UNIVAC 1100 , podem usar um desses registradores como um ponteiro de pilha .

Linguagens modernas após ALGOL , como PL/I e C , quase invariavelmente usam uma pilha, geralmente suportada pela maioria dos conjuntos de instruções de computador modernos para fornecer um novo registro de ativação para cada execução de um subprograma. Dessa forma, a execução aninhada fica livre para modificar suas variáveis ​​locais sem se preocupar com o efeito em outras execuções suspensas em andamento. Conforme as chamadas aninhadas se acumulam, uma estrutura de pilha de chamadas é formada, consistindo em um registro de ativação para cada subprograma suspenso. Na verdade, essa estrutura de pilha é virtualmente onipresente e, portanto, os registros de ativação são comumente chamados de quadros de pilha .

Algumas linguagens como Pascal , PL/I e Ada também suportam funções aninhadas , que são funções que podem ser chamadas apenas dentro do escopo de uma função externa (pai). As funções internas têm acesso às variáveis ​​locais da função externa que as chamou. Isso é feito armazenando informações extras de contexto no registro de ativação, também chamado de display .

Se um subprograma pode ser executado adequadamente mesmo quando outra execução do mesmo subprograma já está em andamento, esse subprograma é considerado reentrante . Um subprograma recursivo deve ser reentrante. Os subprogramas reentrantes também são úteis em situações multiencadeadas , pois vários encadeamentos podem chamar o mesmo subprograma sem medo de interferir uns nos outros. No sistema de processamento de transações IBM CICS , quase-reentrante era um requisito um pouco menos restritivo, mas semelhante, para programas aplicativos que eram compartilhados por muitos encadeamentos.

Sobrecarga

Em linguagens fortemente tipadas , às vezes é desejável ter várias funções com o mesmo nome, mas operando em diferentes tipos de dados ou com diferentes perfis de parâmetros. Por exemplo, uma função de raiz quadrada pode ser definida para operar em reais, valores complexos ou matrizes. O algoritmo a ser usado em cada caso é diferente, e o resultado de retorno pode ser diferente. Ao escrever três funções separadas com o mesmo nome, o programador tem a comodidade de não ter que lembrar nomes diferentes para cada tipo de dado. Além disso, se um subtipo pode ser definido para os reais, para separar reais positivos e negativos, duas funções podem ser escritas para os reais, uma para retornar um real quando o parâmetro for positivo e outra para retornar um valor complexo quando o parâmetro for negativo.

Na programação orientada a objetos , quando uma série de funções com o mesmo nome pode aceitar diferentes perfis de parâmetros ou parâmetros de diferentes tipos, cada uma das funções é considerada sobrecarregada .

Aqui está um exemplo de sobrecarga de função em C++ , demonstrando a implementação de duas funções com o mesmo nome (Area) mas com parâmetros diferentes:

#include <iostream> 

Área dupla ( duplo h , duplo w ) { return h * w ; } /* A primeira função Area é para encontrar a área de um retângulo, * então ela aceita dois números como parâmetros, para altura e largura. */           



Área dupla ( duplo r ) { return r * r * 3,14 ; } /* A segunda função Área serve para encontrar a área de um círculo, * por isso só aceita um número como parâmetro, para o raio. */          



int main () { double retângulo_area = Área ( 3 , 4 ); // Isso chama a primeira função Area, porque dois parâmetros são fornecidos. double circle_area = Área ( 5 ); // Isso chama a segunda função Area, porque apenas um parâmetro é fornecido.  
       
            

  std :: cout << "A área de um retângulo é " << retângulo_area << std :: endl ; std :: cout << "A área de um círculo é " << circle_area << std :: endl ; }      
        

Como outro exemplo, uma função pode construir um objeto que aceitará direções e traçar seu caminho até esses pontos na tela. Há uma infinidade de parâmetros que podem ser passados ​​para o construtor (cor do traço, início das coordenadas x e y, velocidade do traço). Se o programador desejasse que o construtor aceitasse apenas o parâmetro de cor, ele poderia chamar outro construtor que aceitasse apenas cores, que por sua vez chama o construtor com todos os parâmetros passando um conjunto de valores padrão para todos os outros parâmetros ( X e Y geralmente seriam centralizados na tela ou colocados na origem, e a velocidade seria definida para outro valor à escolha do codificador).

PL/I tem o GENERICatributo para definir um nome genérico para um conjunto de referências de entrada chamado com diferentes tipos de argumentos. Exemplo:

DECLARE gen_name GENERIC(
                    nome QUANDO(BINÁRIO FIXO),
                    chama QUANDO (FLUTUANTE),
                    nome do caminho OUTRO
                         );

Várias definições de argumento podem ser especificadas para cada entrada. Uma chamada para "gen_name" resultará em uma chamada para "name" quando o argumento for FIXED BINARY, "flame" quando FLOAT", etc. Se o argumento corresponder, nenhuma das opções "pathname" será chamada.

fechamentos

Um fechamento é um subprograma junto com os valores de algumas de suas variáveis ​​capturados do ambiente em que foi criado. Closures eram uma característica notável da linguagem de programação Lisp, introduzida por John McCarthy . Dependendo da implementação, os fechamentos podem servir como um mecanismo para efeitos colaterais.

Convenções

Um grande número de convenções para a codificação de funções foi desenvolvido. Com relação à nomenclatura, muitos desenvolvedores adotaram a abordagem de que o nome de uma função deve ser um verbo quando realiza uma determinada tarefa, um adjetivo quando faz alguma consulta e um substantivo quando é usado para substituir variáveis.

Alguns programadores sugerem que uma função deve executar apenas uma tarefa e, se uma função executar mais de uma tarefa, ela deve ser dividida em mais funções. Eles argumentam que as funções são componentes-chave na manutenção do código e suas funções no programa devem permanecer distintas.

Os defensores da programação modular (modularização do código) defendem que cada função deve ter dependência mínima de outras partes do código. Por exemplo, o uso de variáveis ​​globais é geralmente considerado imprudente pelos defensores dessa perspectiva, porque adiciona um acoplamento estreito entre a função e essas variáveis ​​globais. Se tal acoplamento não for necessário, seu conselho é refatorar as funções para aceitar os parâmetros passados . No entanto, aumentar o número de parâmetros passados ​​para funções pode afetar a legibilidade do código.

Códigos de retorno

Além de seu efeito principal ou normal , uma sub-rotina pode precisar informar ao programa chamador sobre condições excepcionais que possam ter ocorrido durante sua execução. Em algumas linguagens e padrões de programação, isso geralmente é feito por meio de um código de retorno , um valor inteiro colocado pelo subprograma em algum local padrão, que codifica as condições normais e excepcionais.

No IBM System/360 , onde o código de retorno era esperado da sub-rotina, o valor de retorno era frequentemente projetado para ser um múltiplo de 4 - para que pudesse ser usado como um índice direto da tabela de ramificação em uma tabela de ramificação geralmente localizada imediatamente após o instrução de chamada para evitar testes condicionais extras, melhorando ainda mais a eficiência. Na linguagem assembly do System/360 , escreveríamos, por exemplo:

           BAL 14, SUBRTN01 vai para uma sub-rotina, armazenando endereço de retorno em R14
           B TABLE(15) usa o valor retornado no reg 15 para indexar a tabela de ramificação,
* ramificação para o ramo apropriado instr.
TABELA B Código de retorno OK =00 BOM }
           B Código de retorno BAD =04 Entrada inválida } Tabela de ramificação
           Código de retorno B ERROR =08 Condição inesperada }

Otimização de chamadas de função

Há uma sobrecarga de tempo de execução significativa na chamada de uma função, incluindo a passagem dos argumentos, ramificação para o subprograma e ramificação de volta para o chamador. A sobrecarga geralmente inclui salvar e restaurar determinados registradores do processador , alocar e recuperar o armazenamento do quadro de chamada, etc. . Uma fonte significativa de sobrecarga em linguagens orientadas a objetos é o despacho dinâmico usado intensivamente para chamadas de método.

Existem algumas otimizações aparentemente óbvias de chamadas de procedimento que não podem ser aplicadas se os procedimentos puderem ter efeitos colaterais. Por exemplo, na expressão (f(x)-1)/(f(x)+1), a função fdeve ser chamada duas vezes, pois as duas chamadas podem retornar resultados diferentes. Além disso, o valor de xdeve ser buscado novamente antes da segunda chamada, pois a primeira chamada pode tê-lo alterado. Determinar se um subprograma pode ter um efeito colateral é muito difícil (na verdade, indecidível em virtude do teorema de Rice ). Portanto, embora essas otimizações sejam seguras em linguagens de programação puramente funcionais, os compiladores de programação imperativa típica geralmente precisam assumir o pior.

Inlining

Um método usado para eliminar essa sobrecarga é a expansão inline ou inline do corpo do subprograma em cada site de chamada (em vez de ramificar para a função e vice-versa). Isso não apenas evita a sobrecarga da chamada, mas também permite que o compilador otimize o corpo do procedimento com mais eficiência, levando em consideração o contexto e os argumentos dessa chamada . O corpo inserido pode ser otimizado pelo compilador. Inlining, no entanto, normalmente aumentará o tamanho do código, a menos que o programa contenha apenas uma chamada para a função.

Veja também

Referências

  1. ^ Comissão de Assistência Eleitoral dos EUA (2007). "Definições de palavras com significados especiais". Diretrizes do Sistema de Votação Voluntária . Arquivado do original em 8 de dezembro de 2012 . Acesso em 14 de janeiro de 2013 .
  2. ^ Subrata Dasgupta (7 de janeiro de 2014). Começou com Babbage: A Gênese da Ciência da Computação. Imprensa da Universidade de Oxford. págs. 155–. ISBN 978-0-19-930943-6.
  3. ^ ab Mauchly, JW (1982). "Preparação de Problemas para Máquinas do tipo EDVAC". Em Randell, Brian (ed.). As Origens dos Computadores Digitais . Springer. pp. 393–397. doi : 10.1007/978-3-642-61812-3_31. ISBN 978-3-642-61814-7.
  4. ^ Wheeler, DJ (1952). "O uso de sub-rotinas em programas" (PDF) . Anais do encontro nacional ACM de 1952 (Pittsburgh) em - ACM '52 . pág. 235. doi : 10.1145/609784.609816 .
  5. ^ Wilkes, MV; Wheeler, DJ; Gill, S. (1951). Preparação de Programas para um Computador Eletrônico Digital . Addison-Wesley.
  6. ^ Dainith, John (2004). "" abrir sub-rotina." Um dicionário de computação". Encyclopedia. com . Acesso em 14 de janeiro de 2013 .
  7. ^ Turing, Alan M. (1945), relatório do Dr. AM Turing sobre propostas para o desenvolvimento de um Automatic Computing Engine (ACE): submetido ao Comitê Executivo do NPL em fevereiro de 1946reimpresso em Copeland, BJ , ed. (2005). Mecanismo de Computação Automática de Alan Turing . Oxford: Oxford University Press. pág. 383. ISBN 0-19-856593-3.
  8. ^ Donald E. Knuth (1997). A Arte da Programação de Computadores, Volume I: Algoritmos Fundamentais . Addison-Wesley. ISBN 0-201-89683-4.
  9. ^ O.-J. Dahl; EW Dijkstra; CAR Hoare (1972). Programação Estruturada . Imprensa Acadêmica. ISBN 0-12-200550-3.
  10. ^ Wilson, Leslie B. (2001). Linguagens de Programação Comparadas, Terceira Edição . Addison-Wesley. pág. 140. ISBN 0-201-71012-9.
  11. ^ Stroustrup, Bjarne (2013). A Linguagem de Programação C++, Quarta Edição . Addison-Wesley. pág. 307. ISBN 978-0-321-56384-2.
  12. Turing, Alan Mathison (19 de março de 1946) [1945], Propostas para o desenvolvimento na Divisão de Matemática de um Motor de Computação Automático (ACE)(NB. Apresentado em 19/03/1946 perante o Comitê Executivo do Laboratório Nacional de Física (Grã-Bretanha).)
  13. ^ Carpinteiro, Brian Edward ; Doran, Robert William (1 de janeiro de 1977) [outubro de 1975]. "A outra máquina de Turing". O jornal do computador . 20 (3): 269–279. doi : 10.1093/comjnl/20.3.269 .(11 páginas)
  14. ^ ab Isaacson, Walter (18 de setembro de 2014). "Walter Isaacson sobre as mulheres do ENIAC". Fortuna . Arquivado do original em 12 de dezembro de 2018 . Acesso em 14 de dezembro de 2018 .
  15. ^ Planejamento e Codificação de Problemas para um Instrumento de Computação Eletrônica, Pt 2, Vol. 3 https://library.ias.edu/files/pdfs/ecp/planningcodingof0103inst.pdf Arquivado em 12 de novembro de 2018 na Wayback Machine (consulte a página 163 do pdf para a página relevante)
  16. ^ Guy Lewis Steele Jr. AI Memo 443. 'Desmistificando o mito da "chamada de procedimento caro"; ou, Implementações de chamadas de procedimento consideradas prejudiciais". Seção "C. Por que as chamadas de procedimento têm má reputação".
  17. ^ Frank, Thomas S. (1983). Introdução ao PDP-11 e sua linguagem Assembly. Série de software Prentice-Hall. Prentice-Hall. pág. 195. ISBN  9780134917047. Consultado em 6 de julho de 2016 . Poderíamos fornecer ao nosso auxiliar de montagem cópias do código-fonte para todas as nossas sub-rotinas úteis e então, ao apresentar a ele um programa de linha principal para montagem, dizer a ele quais sub-rotinas serão chamadas na linha principal [...]
  18. ^ Buttlar, Dick; Farrel, Jacqueline; Nichols, Bradford (1996). Programação PThreads: um padrão POSIX para melhor multiprocessamento. "O'Reilly Media, Inc.". pp. 2–5. ISBN 978-1-4493-6475-5. OCLC  1036778036.
  19. ^ "Centro de Informações ARM" . Infocenter.arm. com . Consultado em 29 de setembro de 2013 .
  20. ^ "uso de pilha x64" . Documentos da Microsoft . Microsoft . Acesso em 5 de agosto de 2019 .
  21. ^ "Tipos de funções". msdn.microsoft. com . Consultado em 29 de setembro de 2013 .
  22. ^ "o que se entende por uma função livre" .
  23. ^ "Pequeno guia básico de introdução: Capítulo 9: Sub-rotinas" . Microsoft.
  24. ^ "4. Mais ferramentas de fluxo de controle — documentação do Python 3.9.7".
  25. ^ Verhoeff, Tom (2018). "A Master Class em recursão". Em Böckenhauer, Hans-Joachim; Komm, Dennis; Unger, Walter (eds.). Aventuras entre limites inferiores e altitudes superiores: ensaios dedicados a Juraj Hromkovič por ocasião de seu 60º aniversário . Springer. pág. 616. ISBN 978-3-319-98355-4. OCLC  1050567095.