Apostila de Introdução à linguagem C

Site: Moodle USP: e-Disciplinas
Curso: PMR3402 - Sistemas Embarcados (2023)
Livro: Apostila de Introdução à linguagem C
Impresso por: Usuário visitante
Data: terça-feira, 21 mai. 2024, 06:59

1. Instalação do compilador (GCC / MinGW)

O GCC (GNU Compiler Collection) foi originalmente desenvolvido para compilar o sistema operacional GNU/Linux, que foi um dos marcos do movimento de software livre. Como tal, o GCC é 100% software livre, e pode ser instalado diretamente no Linux ou MAC. Para o Windows, foi portado pelo popular projeto MinGW, em versões 32-bit e 64-bit.

1.1 Windows (MinGW)

Para instalar o MinGW no Windows, é necessário baixar o gerenciador de instalação em https://sourceforge.net/projects/mingw/. Quando executá-lo, escolha a pasta de instalação. O download inicial será feito e, em seguida, uma lista de pacotes para instalação será apresentada. Selecione todos os pacotes da instalação básica (Basic Setup), como mostrado na Fig. abaixo:

Pacotes para instalação do MinGW

Para proceder a instalação, clique em InstallationApply Changes e depois clique em Apply quando for pedido. Todos os pacotes selecionados serão baixados; uma vez finalizada esta etapa, feche o instalador.

Por fim, para possibilitar o acesso aos executáveis do GCC de qualquer pasta, é necessário inserir o caminho da pasta bin na variável Path do sistema. Para tal, abra a janela de configurações avançadas do sistema e clique em "Variáveis de Ambiente" (vide Fig. abaixo).

Menu de configurações avançadas de ambiente

Na janela de "Variáveis de Ambiente", siga os seguintes passos:

  1. Encontre e selecione a variável Path no grupo de "Variáveis do sistema";
  2. clique em "Editar..." para editar a variável;
  3. clique em "Novo" para adicionar um novo caminho na variável Path;
  4. insira o caminho da pasta bin da sua instalação do MinGW, p. ex.: C:\MinGW\bin; e
  5. clique em "OK " para confirmar.

Para melhor visualização do processo, os passos estão indicados na Fig. abaixo.

Configurando uma variável de ambiente

1.2 MAC

No Mac, você pode instalar o GCC utilizando o Homebrew:

$ brew install gcc

Ou você pode utilizar o Clang que vem instalado no Xcode (disponível na App Store).

1.3 Linux

Você pode utilizar o gerenciador de pacotes de sua distribuição para instalar o GCC no Linux. Por exemplo, no Ubuntu/Debian, execute

$ sudo apt install build-essential

Para outras distribuições, consulte o repositório correspondente.

1.4 Testando a instalação do compilador

Para testar se o compilador GCC está instalado corretamente, abra uma janela do terminal e navegue para a pasta do usuário com:

$ cd ~

E depois execute:

$ gcc --version

Se a instalação estiver correta, o programa deve exibir a versão no console. Um exemplo de saída é:

gcc.exe (tdm-1) 5.1.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

1.5 IDE (Integrated Development Environment)

Para estas aulas, iremos executar o compilador pela linha de comando. Sendo assim, qualquer IDE pode ser utilizada para editar o código. Entre as IDEs mais populares, podemos incluir: Visual Studio Code, Sublime Text, Dev-C++, Code::Blocks, Eclipse, Netbeans, entre outros. A maioria destas são multiplataforma, então sua escolha vai muito de gosto pessoal.

Para o VSCode (https://code.visualstudio.com/), as seguintes extensões são sugeridas:

  • C/C++: Intellisense (funcionalidade de auto-completar) e depuração.
  • Code Runner: compila e executa o arquivo fonte C com um comando.

1.6 IDE Online

A instalação descrita anteriormente é altamente recomendada e necessária para completar todos os passos desta apostila/aula. Entretanto, caso não consiga realizar a instalação neste momento, temporariamente pode usar uma IDE online, disponível em:

https://www.onlinegdb.com/online_c_compiler#

Bibliografia para este capítulo

  • Brian W. Kernighan and Dennis M. Ritchie. 1989. The C programming language. Prentice Hall Press, USA.

2. Programa Hello World

Vamos criar o nossa primeira aplicação em C. Para tal, crie um arquivo hello.c no seu IDE de preferência. O código do primeiro programa, chamado Hello World é o seguinte:

#include <stdio.h>

int main()
{
    printf("Hello World!\n");
    return 0;
}

O programa Hello World apenas imprime a frase "Hello World!" na tela. Antes de explicar o funcionamento do código, vamos compilar e executar o programa.

2.1 Compilação e execução do programa

Para compilar o programa utilizando o GCC, abra uma janela de terminal na mesma pasta do arquivo .c e execute:

$ gcc -Wall hello.c -o hello.exe

Como resultado da operação, o GCC deve gerar o executável hello.exe na mesma pasta. Note que, no MAC e Linux, não é necessária a extensão .exe, então o comando deveria ser:

$ gcc -Wall hello.c -o hello

Para executá-lo, faça:

$ ./hello

A saída do programa, como esperado, deve ser:

Hello World!

2.2 Observações sobre o programa

Comentários podem ser adicionados para facilitar a compreensão dos comandos:

#include <stdio.h>

int main()
{
    printf("Hello World!\n"); /* Imprime "Hello World!" (esta é uma forma de criar comentários) */
    return 0;  /* finalizar programa com sucesso */
}

O "Hello World" possui praticamente o mínimo de código para gerar um programa e, por este motivo, é mais útil para testar o ambiente de desenvolvimento. No entanto é possível observar algumas particularidades da linguagem C. Uma delas é que a execução do código sempre inicia na função main. Assim, os comandos entre as chaves após int main() são executados, ou seja:

int main()
{
    /* codigo executado */
}

Na linha

    printf("Hello World!\n");

a função printf da biblioteca padrão é chamada para imprimir um string na tela. O comentário é ignorado pelo compilador.

Ao fim da chamada, é utilizado o ponto e vírgula ; para indicar o fim da instrução (statement). Em C, indentações e quebras de linha não são impostas, de modo que o corpo da função main poderia ser escrito em uma única linha (apesar de não ser recomendado, obviamente). Desse modo, o uso de ponto e vírgula é imprescindível e, se esquecido, ocasionará erros de compilação.

Por fim, o programa acaba retornando o valor 0 na linha

    return 0;

É convencionado que, ao retornar o valor 0, o programa indica que terminou corretamente (sem erros).

Bibliografia para este capítulo

  • Brian W. Kernighan and Dennis M. Ritchie. 1989. The C programming language. Prentice Hall Press, USA.

3. Loops, variáveis e constantes simbólicas

O programa abaixo imprime uma tabela que realiza a conversão de graus em Celsius para Fahrenheit:

#include <stdio.h>

#define LOWER 10 /* limite inferior */
#define UPPER 20 /* limite superior */

/* imprime a tabela de conversão Fahrenheit-Celsius */
int main()
{
    float fahr;
    int celsius = LOWER;
    while (celsius <= UPPER) {
        fahr = (9.0/5.0) * celsius + 32.0;
        printf("%d %6.1f\n", celsius, fahr);
        ++celsius;
    }
}

A saída deste programa é

 10   50.0
 11   51.8
 12   53.6
 13   55.4
 14   57.2
 15   59.0
 16   60.8
 17   62.6
 18   64.4
 19   66.2
 20   68.0

A princípio, existem vários novos conceitos neste código em relação ao primeiro exemplo. No entanto, o fluxo do programa pode ser resumido a:

/* Inicializa celsius com valor 10 */
/* Enquanto celsius for menor ou igual a 20 */
    /* Imprime linha da tabela */
    /* Adiciona passo a celsius */

Nas próximas seções iremos dissecar cada parte do programa para introduzir os conceitos de loops, variáveis e constante simbólicas em C.

3.1 Loops

Os principais loops em C são while e for. Como visto no código do programa, um loop while é escrito

while(expressao)
{
    instrucao1;
    instrucao2;
}

Quando apenas uma instrução faz parte do loop, as chaves podem ser omitidas, então

while(expressao)
    instrucao;

é válido, mas não é muito recomendado.

O loop for tem o seguinte formato

for(expressao1; expressao2; expressao3 )
    instrucao

que é equivalente a

expressao1;
while(expressao2) {
    instrucao;
    expressao3;
}

3.2 Variáveis e tipos

Em C, as variáveis tem que ser declaradas antes de serem utilizadas. Na declaração, o tipo da variável tem que ser especificado. Então, a linha

    float fahr;

declara uma variável do tipo float.

A variável também pode ser inicializada na declaração. Assim é valido escrever

    int celsius = 0;

ou

    float fahr = (9.0/5.0) * celsius + 32.0;

Entretanto, este último caso não é usual, uma vez que se convenciona colocar as declarações de variáveis no início de cada função (lembrando que main é uma função).

Existem outros tipos de variáveis, os mais utilizados são:

  • char: um único byte, capaz de armazenar um caractere no conjunto de caracteres locais
  • int: um inteiro
  • short / short int: inteiro que utiliza um menos número de bytes para armazenamento (admite menor amplitude de valores)
  • long / long int: inteiro que utiliza um maior número de bytes para armazenamento (admite maior amplitude de valores)
  • float: ponto flutuante de precisão única
  • double: ponto flutuante de precisão dupla
  • long double: ponto flutuante de precisão estendida (maior que dupla)

Também pode ser adicionado um qualificador signed e unsigned para especificar variáveis com ou sem sinal. Por exemplo, unsigned int admite apenas valores inteiros positivos.

Outro qualificador importante é o const, que pode ser adicionado à qualquer declaração de variável para indicar que o seu valor não será modificado.

3.3 Conversões de tipo

Quando um operador (+,*,-, entre outros) envolve dois tipos diferentes, eles são convertidos para um tipo comum. Em geral, as únicas conversões automáticas são as que convertem de um tipo mais "restrito" para um mais "abrangente" sem perder informação (p. ex. de int para float). Assim, o resultado da operação

    (9.0/5.0) * celsius

é convertido automaticamente para float (celsius é tipo int e o resultado de (9.0/5.0) é do tipo float).

Para forçar uma conversão, é possível utilizar um operador unário chamado cast. Por exemplo, para realizar a divisão de celsius (tipo int) por outra variável n do mesmo tipo, podemos escrever

    (double) celsius / n

para obter o resultado em ponto flutuante (com casas decimais); esta é a forma mais recomendada.

3.4 Escopo de variáveis e funções

As variáveis e funções (que serão vistas mais adiante) só podem ser acessadas dentro do bloco em que são declaradas. Cada bloco é delimitado pela abertura e fechamento de chaves {}. Em alguns casos excepcionais, as chaves podem ser omitidas; porém o bloco ainda é definido. Além disso, o acesso só pode ocorrer em linhas subsequentes do código. Por isso e para uma melhor clareza, é boa prática declarar todas as variáveis no início de cada bloco.

Por exemplo, as variáveis fahr e celsius do código

#include <stdio.h>
int main()
{
    float fahr, celsius;
    ...
}

só podem ser acessadas dentro da função main (no trecho ... e não antes).

Variáveis definidas fora de qualquer bloco interno do programa são consideradas globais e podem ser acessadas dentro de qualquer bloco. Em projetos com mais de um arquivo fonte, podemos limitar o escopo da variável global para o arquivo corrente com o qualificador static. No entanto, se uma variável local (dentro de uma função, por exemplo) for declarada como static, o seu valor vai ser mantido entre chamadas de função.

3.5 Constantes simbólicas

O programa poderia ser escrito de forma mais sucinta como

#include <stdio.h>
int main()
{
    float fahr;
    int celsius = 10;
    while (celsius <= 20) {
        fahr = (9.0/5.0) * celsius + 32.0;
        printf("%d %6.1f\n", celsius, fahr);
        ++celsius;
    }
}

No entanto, se desejarmos modificar os limites da tabela, temos que achar esses valores no meio do código fonte, o que não é sempre fácil. Esses são chamados números mágicos, e são usualmente indesejados. Desse modo, podemos substituir os números mágicos por constantes, que são incluídas na parte inicial do programa para facilitar modificações.

No programa desta seção, as constantes simbólicas estão definidas nas linhas

#define LOWER 10
#define UPPER 20

Na prática, antes da compilação, as palavras LOWER, UPPER são substituídas pelos valores 10 e 20, respectivamente.

3.6 Incremento e decremento

A linha

        ++celsius; /* ou celsius++; */

é equivalente a escrever

        celsius = celsius + 1;

No entanto, existe uma diferença em escrever o argumento ++celsius e celsius++. No primeiro caso, a variável é incrementada antes de seu valor ser utilizado pela função e, no segundo caso, a variável é incrementada depois de seu valor ser utilizado.

Assim, podemos converter o nosso programa para

#include <stdio.h>

#define LOWER 10 /* limite inferior */
#define UPPER 20 /* limite superior */

/* imprime a tabela de conversão Fahrenheit-Celsius */
int main()
{
    float fahr;
    int celsius = LOWER;
    while (celsius <= UPPER) {
        fahr = (9.0/5.0) * celsius + 32.0;
        printf("%d %6.1f\n", celsius++, fahr);
    }
}

No entanto, a mudança

        printf("%d %6.1f\n", ++celsius, fahr); /* ERRADO */

acarretaria em um comportamento diferente do desejado. Também note que uma expressão pode ser utilizada no lugar de um argumento de função (isso será melhor explicado em outro capítulo).

Neste mesmo grupo, temos também as operações

        celsius--; /* igual a celsius = celsius - 1; */
        celsius+=10; /* igual a celsius = celsius + 10; */

Bibliografia para este capítulo

  • Brian W. Kernighan and Dennis M. Ritchie. 1989. The C programming language. Prentice Hall Press, USA.

4. Arrays e expressões condicionais

Considere o programa abaixo, que conta o número de dígitos (0 - 9), espaços em brancos e outros caracteres para uma linha de texto que é uma entrada de usuário:

#include <stdio.h>

/* conta digitos, espaco, outros */
int main()
{
    int c;
    int i;
    int nwhite;
    int nother;
    int ndigit[10];

    nwhite = nother = 0;
    for (i = 0; i < 10; ++i)
        ndigit[i] = 0;
    while ((c = getchar()) != '\n')
        if (c >= '0' && c <= '9')
            ++ndigit[c-'0'];
        else if (c == ' ' || c == '\t')
            ++nwhite;
        else
            ++nother;
    printf("digitos =");
    for (i = 0; i < 10; ++i)
        printf(" %d", ndigit[i]);
    printf(", espaco = %d, outros = %d\n", nwhite, nother);
    return 0;
}

A função getchar espera pela entrada de um caractere pelo usuário. Uma vez recebida a entrada, o contador correspondente é incrementado e, ao receber uma quebra de linha (tecla enter), o programa imprime as contagens na tela e termina.

Como sabemos de antemão que existem 12 contadores (10 para dígitos, 1 para espaços em branco e 1 para outros), poderíamos criar doze variáveis do tipo int para realizar a contagem. No entanto, é mais conveniente criar um array de tamanho 10 para os contadores de dígitos; este é declarado na linha

    int ndigit[10];

E, para acessar o i-ésimo valor do array ndigit e atribuir o valor zero para ele, é possível escrever

    ndigit[i] = 0;

4.1 Expressões condicionais

Ao receber um caractere, o programa deve incrementar o contador. A escolha de qual contador a ser utilizado é realizada no trecho

        if (c >= '0' && c <= '9')
            ++ndigit[c-'0'];
        else if (c == ' ' || c == '\t')
            ++nwhite;
        else
            ++nother;

Primeiramente, é checado se o caractere c é um algarismo. Como as constantes de caracteres ('0', '9', ' ', '\t') e variáveis do tipo char são inteiros pequenos, isto é, possuem um valor inteiro correspondente, é possível checar se c está entre '0' e '9' com a condicão:

if (c >= '0' && c <= '9')

Isso funciona se os valores correspondentes aos caracteres '0', '1', ... , '9' forem consecutivos e crescentes, o que é verdadeiro para todos os conjuntos de caracteres padrões. Esse fato também justifica o porquê de c - '0' corresponder ao índice do contador de dígitos desejado.

Caso a primeira condição não seja aceita, é verificado se c é um espaço ou uma tabulação. Por fim, se nenhuma das condições acima for atendida, o contador other é incrementado.

É possível observar que a sintaxe para as expressões condicionais if, else if são parecidas com as do loops while e for. Todas são seguidas de uma ou mais expressões dentro de parênteses e depois das instruções a serem executadas. Assim como no caso dos loops, quando o corpo das expressões condicionais possuir mais de uma instrução, estas devem estar envoltas de chaves. Por exemplo, o primeiro if pode ser reescrito como

        if (c >= '0' && c <= '9')
        {
            i = c - '0';
            ++ndigit[i];
        }

Bibliografia para este capítulo

  • Brian W. Kernighan and Dennis M. Ritchie. 1989. The C programming language. Prentice Hall Press, USA.

5. Switch e enums

Podemos reescrever o programa do capítulo 4, que conta os dígitos de uma string, da seguinte forma:

#include <stdio.h>

/*  conta  digitos , espaco , outros  */
int main()
{
    int c, i, nwhite, nother, ndigit[10];
    nwhite = nother = 0;
    for (i = 0; i < 10; i++)
        ndigit[i] = 0;
    while ((c = getchar()) != '\n') {
        switch (c) {
        case '0': case '1': case '2': case '3': case '4':
        case '5': case '6': case '7': case '8': case '9':
            ndigit[c-'0']++;
            break;
        case ' ':
        case '\n':
        case '\t':
            nwhite++;
            break;
        default:
            nother++;
            break;
        }
    }
    printf("digits =");
    for (i = 0; i < 10; i++)
        printf(" %d", ndigit[i]);
    printf(", white space = %d, other = %d\n", nwhite, nother);
    return 0;
}

A instrução switch realiza testes de decisão com múltiplas escolhas a partir de uma expressão e um número de constantes inteiras. O seu formato é o seguinte

switch (expressao) {
    case const-expr: instrucoes
    case const-expr: instrucoes
    default: instrucoes
}

A execução é redirecionada para o rótulo default quando nenhuma das outras condições foram atendidas.

É possível observar que, após as instruções de cada caso no programa, a instrução break é inserida. Esta instrução é necessária para que o código não prossiga para o próximo case.

5.1 Enumeradores

Um enumerador é uma lista de valores inteiros constantes. Por exemplo, na declaração

enum boolean { NO, YES };

NO recebe o valor 0 e YES recebe 1. Se existissem mais nomes, eles teriam o valor de 2, 3, etc.

Se modificarmos o programa deste capítulo para classificar cada entrada e armazenar essa informação em um vetor, podemos reescrevê-lo como:

#include <stdio.h>

#define MAXSIZE 100

/*  conta  digitos , espaco , outros  */
int main()
{
    int c, i, numchars;
    enum chartype {DIGIT, WHITE, OTHER} types[MAXSIZE];
    numchars = 0;

    while ((c = getchar()) != '\n') {
        if (c >= '0' && c <= '9')
            types[numchars++] = DIGIT;
        else if (c == ' ' || c == '\t')
            types[numchars++] = WHITE;
        else
            types[numchars++] = OTHER;
    }
    for (i = 0; i < numchars; i++)
    {
        switch (types[i]) {
        case DIGIT:
            printf("d");
            break;
        case WHITE:
            printf("w");
            break;
        default:
            printf("o");
            break;
        }
    }
    return 0;
}

Bibliografia para este capítulo

  • Brian W. Kernighan and Dennis M. Ritchie. 1989. The C programming language. Prentice Hall Press, USA.

6. Funções

O programa abaixo apresenta uma definição de uma simples função que realiza a operação formula.

#include <stdio.h>

#define RANGE 10

int power(int m, int i);

/* Teste da funcao exponencial */
int main()
{
    int i;
    for (i = 0; i < RANGE; ++i)
        printf("%d %d %d\n", i, power(2,i), power(-3,i));
    return 0;
}

/* power: m exponencial i; i >= 0 */
int power(int m, int i)
{
    int p;
    for (p = 1; i > 0; --i)
        p = p * m;
    return p;
}

É possível observar que power aparece em três linhas do código: uma antes e uma depois da função main, e outra dentro do corpo de main. As utilizações em main são as chamadas para a função, que devem retornar o valor de formula e formula para i entre 0 e 10.

É também possível constatar que a função está definida no seguinte trecho:

int power(int m, int i)
{
    int p;
    for (p = 1; i > 0; --i)
        p = p * m;
    return p;
}

Sabendo que cada função possui a seguinte forma:

tipo-de-retorno nome-da-funcao(declaracao de argumentos)
{
    declaracoes e instrucoes
}

podemos deduzir que a função power recebe dois argumentos do tipo int e retorna um tipo int também. A expressão return p indica que o valor de p será retornado.

A linha:

int power(int m, int i);

define a função para que ela possa ser utilizada em main. Se esta linha fosse omitida, power só estaria acessível para funções / expressões localizadas após a declaração de power; isto é, ao fim do arquivo. Alternativamente, a declaração de power poderia ser feita antes da função main.

6.1 Passagem de argumentos por valor

Ao observar a função power:

int power(int m, int i)
{
    int p;
    for (p = 1; i > 0; --i)
        p = p * m;
    return p;
}

é possível observar que a variável i é alterada. Entretanto, se, nas chamadas power(2,i) e power(-3,i), o valor de i fosse alterado, o programa não funcionaria como esperado. O que está ocorrendo?

Em C, os argumentos de uma função são passados por valor. Isto é, dentro da função power, é criada uma nova variável e o valor de i é atribuído a ela. Assim, alterar o valor de i dentro da função power não modifica a variável i em main.

Bibliografia para este capítulo

  • Brian W. Kernighan and Dennis M. Ritchie. 1989. The C programming language. Prentice Hall Press, USA.

7. Ponteiros, Arrays (revisitado) e Ponteiros para Funções

Essencialmente, um ponteiro é uma variável que armazena o endereço onde o valor está. O tipo do ponteiro (int, float, char, double) indica o tipo armazenado no endereço apontado, como pode ser visto na figura abaixo.

Podemos declarar um ponteiro utilizando a notação tipo *nome-da-variavel, como nos exemplos abaixo.

    char *p;
    short *q;
    long *r;

Neste código, o espaço alocado para as variáveis p, q e r é o mesmo; isto porque cada ponteiro ocupa o tamanho de um endereço da memória. Para manipular o conteúdo apontado, devemos utilizar o operador *, chamado de operador desreferenciador. Por exemplo, poderíamos escrever:

    *p = 'a';
    *q = 10;
    *r = 2147483647;

A Fig. abaixo pode nos auxiliar a entender as operações realizadas com ponteiros no nosso exemplo.

Exemplo de utilização de ponteiros

Outro operador importante para manipulação de ponteiros é o operador &, que retorna o endereço da variável à sua direita. Sendo assim, este pode ser utilizado para criar ponteiros para variáveis já existentes; por exemplo, no código

    int c;
    short *q;
    q = &c;
    c = 0;
    *q = 10;

o valor final da variável c é 10.

7.1 A notação de Array para ponteiros

Arrays representam blocos consecutivos de dados de um tipo. Como regra geral, expressões com arrays são convertidos para ponteiros que apontam para o primeiro elemento do bloco. Ou seja, podemos escrever

int a[5] = {1, 2, 3, 4, 5};
int *b;
b = a; /* que é o mesmo que b = &a[0] */

Como vimos antes, podemos acessar o elemento de um array a utilizando o operador []. Sendo assim, temos que

/* expressões abaixo são verdadeiras */
*a == a[0];
*(a+5) == a[5];

A Fig. abaixo ilustra o caso acima.

Exemplo de utilização de ponteiros com array

No caso de arrays bidimensionais, como no exemplo

#define MAX_ROWS 2
#define MAX_COLS 5
short m_table[MAX_ROWS][MAX_COLS] =
{
    {1, 2, 3, 4, 5},
    {6, 7, 8, 9, 10}
};

temos que o valor de m_table[0] é convertido para um ponteiro para o primeiro elemento do array unidimensional {1, 2, 3, 4, 5}. Analogamente, m_table[1] é convertido para um ponteiro para o primeiro elemento do array unidimensional {6, 7, 8, 9, 10}. Ou seja, as seguintes equivalências são verdadeiras:

*m_table[0] == m_table[0][0] == 1
(*m_table[0]+1) == m_table[0][1] == 2
*m_table[1] == m_table[1][0] == 6

O que significa que os valores de cada linha são armazenados sequencialmente.

7.2 Passagem de argumentos por referência

Digamos que gostaria de escrever uma função power que tivesse a base e o expoente como argumentos, mas, ao invés de retornar o exponencial, atualizasse a variável da base com o resultado. Para isso, passaríamos a referência da variável para a função, deste modo:

int main() {
    int p2, p3, i;
    for (i = 0; i < 10; ++i) {
        p2 = 2;
        p3 = 3;
        power(&p2,i);
        power(&p3,i);
        printf("%d %d %d\n", i, p2, p3);
    }
}

O caractere & antes de p2 e p3 é uma operação que obtém o endereço da variável. Assim, na declaração da nova função power:

void power(int *m, int i)
{
    int base = *m;
    for (*m = 1; i > 0; --i)
        *m = *m * base;
}

O tipo void apenas indica que não será retornado nenhum valor.

Quando o endereço de uma variável tipo int é recebido pela função power, ele é copiado para m (tipo int*). A operação *m obtém o valor armazenado no endereço apontado por m e, desse modo, é possível alterar o valor da variável.

Por fim, é importante observar que arrays são sempre passados por referência para funções, uma vez que são convertidos para ponteiros. Assim, o programa poderia ser adaptado para:

#include <stdio.h>

void power(int bases[], int size, int n);

int main() {
    int p[2], i;
    for (i = 0; i < 10; ++i) {
        p[0] = 2;
        p[1] = 3;
        power(p,2,i);
        printf("%d %d %d\n", i, p[0], p[1]);
    }
}

void power(int m[], int size, int n)
{
    int i, j, base;
    for(j = 0; j < size; j++) {
        base = m[j];
        for (m[j] = 1, i = 0; i <= n; ++i)
            m[j] = m[j] * base;
    }
}

7.3 Ponteiros para funções

Apesar de não ser variável, uma função também possui um endereço. Sendo assim, é possível criar ponteiros para funções, com a seguinte sintaxe

tipo_do_retorno (*nome)(lista_de_argumentos);

Como exemplos, temos

int (*comp)(void *, void *);
char *(*weird)(void);

Os operadores & e * também podem ser usados com ponteiros para funções, como mostrado no código

#include <stdio.h>

int comp(int a, int b)
{
    return (a == b);
}

int main()
{
    int num1 = 10;
    int num2 = 20;
    int (*comp_ptr)(int, int) = &comp;

    if((*comp_ptr)(num1, num2) == 1) {
        printf("The numbers are the same");
    }
    else {
        printf("The numbers are different");
    }

    return 0;
}

Neste código, o ponteiro comp_ptr é declarado e atribuído ao endereço da função comp. Depois, o operador desreferenciador é utilizado para fazer a chamada da função apontada por comp_ptr.

Note que ponteiro para funções pode ser bastante útil como argumento de função. Por exemplo, para definir um algoritmo de ordenação genérico, um dos argumentos pode ser um ponteiro para função de comparação entre dois elementos.

Bibliografia para este capítulo

  • Brian W. Kernighan and Dennis M. Ritchie. 1989. The C programming language. Prentice Hall Press, USA.

8. Estruturando o projeto em vários arquivos

Considere o programa abaixo, que simula um crescimento exponencial com entrada de parâmetros pelo usuário e gera um gráfico primitivo.

#include <stdio.h>
#include <math.h>

#define MAXCHARS 100

int initial, dblrate, maxinfected;

int infected(int day);
void plot(int day);

int main()
{
    int i, days;
    printf("Digite o numero atual de casos: ");
    scanf("%d", &initial);
    printf("Digite o numero de dias para dobrar os casos: ");
    scanf("%d", &dblrate);
    printf("Digite o periodo de dias: ");
    scanf("%d", &days);

    maxinfected = infected(days);
    printf("Ao final de %d dias existirao %d infectados\n", days, maxinfected);
    printf("Grafico:\n");
    for(i = 0; i <= days; ++i)
        plot(i);

    return 0;
}

int infected(int day) {
    return initial * pow(2.0, (double)day / dblrate );
}

void plot(int day) {
    int i, numbars, total;
    /* Obtem o numero de infectados para o dia */
    total = infected(day);
    printf("Dia %3d (%7d infectados): ", day+1, total);
    /* Calcula o numero de barras */
    numbars = (double)total/ maxinfected * MAXCHARS;
    /* Imprime as barras na tela */
    for(i = 0; i < numbars; ++i)
        printf("#");
    printf("\n");
}

Vamos dividir o programa em arquivos, a fim de simplificar a compreensão e manutenção do código. A função main pode ser incluída no arquivo main.c; a função infected e suas variáveis no arquivo growth.c; e plot no arquivo plot.c.

Para desenvolver programas com múltiplos arquivos, é importante lembrar da distinção entre declaração e definição, que vale para funções e variáveis externas: só pode existir uma definição de uma variável externa ou função no conjunto de arquivos que formam o código do programa. Declarações podem ser realizadas múltiplas vezes, e, portanto, são utilizadas para que todos os arquivos possam acessar as variáveis/funções compartilhadas.

Para declarar uma variável externa, sem definí-la, basta utilizar o qualificador extern. No caso de funções, a definição só ocorre quando o corpo da função é especificado. Assim, o qualificador extern é implicitamente assumido para toda declaração de cabeçalho de função. Este é o motivo da possibilidade de dupla declaração de funções nos códigos exibidos até agora.

8.1 Header files (arquivos de cabeçalho)

Em cada arquivo a ser compilado, todas as funções e variáveis utilizadas devem ser declaradas. No entanto, estas devem ser definidas apenas uma vez no código inteiro do programa.

A fim de centralizar as declarações, são utilizados arquivos de cabeçalhos, ou header files, que possuem extensão .h e contêm as declarações de funções e variáveis externas compartilhadas. A diretiva include é então utilizada para inserir as declarações nos arquivos .c. Para incluir headers criados pelo usuário, são utilizadas aspas no nome do arquivo. Dessa maneira, o programa proposto possui a seguinte estrutura:

Estrutura do programa em C com múltiplos arquivos

Em C, não existe nenhuma exigência em relação aos nomes dos arquivos. Ademais, as funções declaradas em um header podem ser definidas em arquivos separados. No entanto, para uma melhor organização do código, é comum gerar arquivos de cabeçalhos com o mesmo nome base do arquivo "c", que contém todas definições correspondentes ao header.

8.2 Include guards

A diretiva #include é ocasionalmente utilizada em arquivos headers. Assim, quando esse header é incluído em um outro arquivo, é possível que ocorram problemas como redefinições e recursão infinita de inclusões. Para evitar esses erros, são utilizadas outras diretivas: #define, #ifndef e #endif

Estas diretivas são responsáveis pela definição e checagem de tokens. Por exemplo, o arquivo growth.h pode ser escrito da seguinte forma:

#ifndef GROWTH_H_INCLUDED
#define GROWTH_H_INCLUDED

extern int dblrate;
extern int initial;
int infected(int day);

#endif /* GROWTH_H_INCLUDED */

A primeira linha confere se o token GROWTH_H_INCLUDED ainda não foi definido. O #endif indica o final da condicional iniciada na primeira linha. Em caso afirmativo, o token é definido e o código do header pode ser incluído. Se existir um segundo #include para esse mesmo arquivo, o seu conteúdo será ignorado, uma vez que não passará pela checagem #ifndef GROWTH_H_INCLUDED.

8.3 Compilando múltiplos arquivos com o GCC

Para criar o executável, cada arquivo é compilado individualmente, gerando arquivos em código de máquina não executáveis. O linker é o programa responsável por juntar esses arquivos e construir o executável. Nesta etapa, as correspondências entre as declarações e definições são verificadas e realizadas. Por exemplo, ao construir (build) este projeto, os seguintes comandos são executados:

$ gcc.exe -Wall -g -c growth.c -o growth.o
$ gcc.exe -Wall -g -c main.c -o main.o
$ gcc.exe -Wall -g -c plot.c -o plot.o
$ gcc.exe -o app.exe growth.o main.o plot.o

Os três primeiros comandos executam o compilador para cada arquivo fonte, gerando três objetos (arquivos com extensão .o). Por fim, o GCC é chamado para realizar o link desses arquivos para gerar o executável.

Bibliografia para este capítulo

  • Brian W. Kernighan and Dennis M. Ritchie. 1989. The C programming language. Prentice Hall Press, USA.

9. Implementando uma máquina de estados em C

Considere a seguinte máquina de estados

Máquina de estados

que representa um sistema de alarme residencial. O estado inicial Espera representa o alarme desativado (geralmente quando o morador está em casa). Ao acionar o alarme, um temporizador é ativado para permitir que o morador saia sem ativar o alarme (estado Saida). Uma vez ativado (estado Alerta), o processo contrário deve ser realizado para desacionar o alarmar (AlertaEntradaAcionado).

9.1 Obtendo e construindo a aplicação com o Makefiles

Antes de entrarmos em maior detalhe na implementação, obtenha o código modelo no link:

https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme

e siga as instruções do README.md para construir o executável.

Note que o sistema de build é automatizado através da ferramenta Makefiles. Além de simplificar o desenvolvimento, o uso de ferramentas de build possibilita maior eficiência na compilação, pois permite fazer a construção de forma incremental. Outros sistemas populares incluem CMake e Ninja.

9.2 Exemplo de execução da aplicação

Ao iniciar a execução, a seguinte mensagem deve ser exibida:

Alarme iniciado
obter teclas:

Para acionar o alarme (executar A01 e mudar para o estado Saida), entre com a senha (que é 12) seguido do caractere a:

obter teclas: 12a
Estado: 1 Evento: 0 Acao:0

O temporizador deve ser iniciado; infelizmente, ele não é finalizado automaticamente. Deste modo, você precisa esperar um tempo e entrar com qualquer caractere para checar se o timeout ocorreu:

obter teclas:qqcoisa
Bip da sirene
Comunicacao com a Central: Alarme em alerta
Estado: 2 Evento: 1 Acao:1

Uma vez montado, para disparar o alarme, basta entrar com o caractere l:

obter teclas:l
Estado: 3 Evento: 2 Acao:4

Isso apenas ativará o temporizador; mais uma vez é necessário esperar um tempo e inserir qualquer string como entrada:

obter teclas:qqcoisadnovo
Acionamento da sirene: 1
Comunicacao com a Central: Invasao
Estado: 5 Evento: 1 Acao:5

A qualquer hora, é possível desativar o alarme inserindo a senha seguido da letra d:

obter teclas:12d
Comunicacao com a Central: Alarme desacionado
Acionamento da sirene: 0
Estado: 0 Evento: 3 Acao:6

9.2 Tabelas de transição

O diagrama de estados pode ser implementado utilizando uma tabela de transição de estados, onde nas linhas temos os estados e as colunas representam as ações.

Para a máquina de estados do alarme residencial, temos a seguinte tabela:

acionar timeout disparar desacionar
Espera Saida / A01
Saida Alerta / A02 Espera / A03
Alerta Entrada / A05 Espera / A04
Entrada Acionado / A06 Espera / A07
Acionado Espera / A07

No código em C, esta tabela pode ser implementado utilizando dois arrays bidimensionais, como mostrado no código

  int acao_matrizTransicaoEstados[NUM_ESTADOS][NUM_EVENTOS];
  int proximo_estado_matrizTransicaoEstados[NUM_ESTADOS][NUM_EVENTOS];

...

void iniciaMaquinaEstados()
{
  int i;
  int j;

  for (i=0; i < NUM_ESTADOS; i++) {
    for (j=0; j < NUM_EVENTOS; j++) {
       acao_matrizTransicaoEstados[i][j] = NENHUMA_ACAO;
       proximo_estado_matrizTransicaoEstados[i][j] = i;
    }
  }

  proximo_estado_matrizTransicaoEstados[ESPERA][ACIONAR] = SAIDA;
  acao_matrizTransicaoEstados[ESPERA][ACIONAR] = A01;

  proximo_estado_matrizTransicaoEstados[SAIDA][DESACIONAR] = ESPERA;
  acao_matrizTransicaoEstados[SAIDA][DESACIONAR] = A03;

  proximo_estado_matrizTransicaoEstados[SAIDA][TIMEOUT] = ALERTA;
  acao_matrizTransicaoEstados[SAIDA][TIMEOUT] = A02;

  proximo_estado_matrizTransicaoEstados[ALERTA][DESACIONAR] = ESPERA;
  acao_matrizTransicaoEstados[ALERTA][DESACIONAR] = A04;

  proximo_estado_matrizTransicaoEstados[ALERTA][DISPARAR] = ENTRADA;
  acao_matrizTransicaoEstados[ALERTA][DISPARAR] = A05;

  proximo_estado_matrizTransicaoEstados[ENTRADA][TIMEOUT] = ACIONADO;
  acao_matrizTransicaoEstados[ENTRADA][TIMEOUT] = A06;

  proximo_estado_matrizTransicaoEstados[ENTRADA][DESACIONAR] = ESPERA;
  acao_matrizTransicaoEstados[ENTRADA][DESACIONAR] = A07;

  proximo_estado_matrizTransicaoEstados[ACIONADO][DESACIONAR] = ESPERA;
  acao_matrizTransicaoEstados[ACIONADO][DESACIONAR] = A07;
}

Note que, no arquivo definicoes_sistema.h, definimos todas as constantes simbólicas utilizadas para a implementação da máquina de estados.

9.3 Laço principal do programa

O main contém o laço principal do programa

int main() {

  int codigoEvento;
  int codigoAcao;
  int estado;
  int eventoInterno;

  estado = ESPERA;
  eventoInterno = NENHUM_EVENTO;

  iniciaSistema();
  printf ("Alarme iniciado\n");
  while (true) {
    if (eventoInterno == NENHUM_EVENTO) {
        codigoEvento = obterEvento();
    } else {
        codigoEvento = eventoInterno;
    }
    if (codigoEvento != NENHUM_EVENTO)
    {
       codigoAcao = obterAcao(estado, codigoEvento);
       estado = obterProximoEstado(estado, codigoEvento);
       eventoInterno = executarAcao(codigoAcao);
       printf("Estado: %d Evento: %d Acao:%d\n", estado, codigoEvento, codigoAcao);
    }
  } // while true
} // main

Após as inicializações das variáveis, o programa entra em um loop infinito (while (true)). Dentro dele, a função obterEvento() é chamada para determinar o evento atual.

Depois, o evento é processado nas funções obterAcao() e executarAcao(), que utilizam a tabela de transições de estado. Neste processo, a máquina pode transicionar para um novo estado.

Por fim, a ação correspondente a transição é executada. Embora não utilizado neste exemplo, é previsto a possibilidade de transição de estado devido a uma ação, o que é considerado um evento interno.

As novas informações sobre o estado da máquina são impressas e o loop recomeça.

10. Structs e Unions

A linguagem C fornece algumas estruturas de dados básicas para se organizar melhor o código.

10.1 Struct

A primeira que iremos discutir é a struct, que é uma coleção de uma ou mais variáveis, possivelmente de tipos diferentes, agrupadas em um nome comum.

Um exemplo de uma struct para representar um ponto em coordenadas cartesianas bidimensionais é

struct point {
    int x;
    int y;
};

Este código é apenas a declaração da estrutura, ou seja, apenas um template do struct. Para definir uma variável do tipo point, podemos escrever

struct point pt;

/* ou, com inicialização */

struct point maxpt = { 320, 200 };

Para acessar um membro do struct, devemos utilizar a construção

nome-do-struct.membro

Assim, podemos imprimir as coordenadas do ponto com o código

printf("%d,%d", maxpt.x, maxpt.y);

Sobre os structs:

  • podem ser copiados ou atribuídos a outras variáveis;
  • possuem endereço acessível com o operador &; e
  • possibilitam acesso aos seus membros.

Além disso, devido à primeira propriedade citada, podem ser argumentos de função ou seu tipo retornado.

É também possível criar um ponteiro para struct, como por exemplo

struct point *pt;
pt = &maxpt;

Podemos imprimir os membros x e y do ponteiro pt com:

printf("%d,%d", (*pt).x, (*pt).y);

Como essa construção é bastante comum, a linguagem C providencia a sintaxe mais compacta

printf("%d,%d", pt->x, pt->y);

10.2 Typedef

Também para facilitar a legibilidade do código, é possível criar novos tipos de dados (ou renomeá-los) com typedefs. Por exemplo, no código

typedef int Length;

, Length é um sinônimo de int. Ou seja, podemos usar Length da mesma forma que int. Consequentemente,

Length len, maxlen;
Length *lengths[];

é igual a

int len, maxlen;
int *lengths[];

Essa facilidade é bastante usada para declarar structs. Aproveitando nosso exemplo anterior, podemos rescrevê-lo como

typedef struct point
{
    int x;
    int y;
} point;

point maxpt;
maxpt.x = 320;
maxpt.y = 200;
printf("%d,%d", maxpt.x, maxpt.y);

10.3 Union

Union é uma variável que pode assumir (em tempos distintos) objetos de tipos diferentes. Considere o código

union u_tag {
    int ival;
    float fval;
    char *sval;
} u;

Para acessar os membros, podemos escrever

nome-do-union.membro

ou

nome-do-union->membro

Note que a sintaxe é parecida com struct; mas, diferentemente deste, apenas um dos três tipos - ival, fval ou sval - pode estar definido em um determinado instante. Apenas o último valor atribuído poderá ser acessado.

Um exemplo de uso de union é mostrado em

if (utype == INT)
    printf("%d\n", u.ival);
if (utype == FLOAT)
    printf("%f\n", u.fval);
if (utype == STRING)
    printf("%s\n", u.sval);
else
    printf("bad type %d in utype\n", utype);

onde utype é uma variável corresponde a um enum. No caso, podemos definir como imprimir a variável dependendo do tipo armazenado nela atualmente.

Bibliografia

  • Brian W. Kernighan and Dennis M. Ritchie. 1989. The C programming language. Prentice Hall Press, USA.

11. Breve introdução ao C++

Tanto a linguagem C como C++ nasceram nos laboratórios do Centro de Pesquisa em Ciência da Computação da Bell Labs em Murray hill, New Jersey. Ambas as linguagens foram projetadas com foco na utilização em sistemas rígidos, como por exemplo kernels de sistemas operacionais, sistemas embarcados e compiladores.

Abaixo listamos algumas características da programação em C++:

  • Assim como C, C++ também é uma linguagem compilada;
  • C++ pode ser considerada um superconjunto de C, ou seja, praticamente todas as funcionalidades de C estão contidas em C++;
  • C++ é uma linguagem de plataforma cruzada que pode ser usada para criar aplicativos de alto desempenho. A linguagem permanece relativamente perto do hardware, permitindo uma programação flexível e eficiente;
  • Apesar de seu desenvolvimento remontar aos anos 1980, C++ ainda é muito popular entre programadores. A linguagem continua sendo atualizada regularmente.

Quanto à performance, não existe diferença significativa entre C e C++. A principal diferença entre elas é que C é uma linguagem de programação procedural e não oferece suporte a classes e objetos, enquanto C++ é uma combinação de programação procedural e orientada a objetos. No próximo capítulo, discutiremos em detalhes a orientação a objetos.

11.1 Sobrecarga

Em C++, podemos utilizar o mesmo identificador para duas funções que admitem tipos diferentes. Essa operação é chamada de sobrecarga, ou overloading.

Quando você chama uma função ou operador sobrecarregado, o compilador determina a definição mais apropriada a ser usada, comparando os tipos de argumento usados ​​para chamar a função ou operador com os tipos de parâmetros especificados nas definições.

#include <stdio.h>

int maxInput(int x, int y)
{
  if (x > y) return x;
  else return y;    
}

char maxInput(char x, char y)
{
  if (x > y) return x;
  else return y;    
}
  
int main()
{
  // Imprime maxInput para o tipo int 
  printf("%i \n", maxInput(1, 5)); 
  
  /* Imprime maxInput para o tipo char */
  printf("%c", maxInput('d', 'b'));

  return 0;
}

A resposta do programa é:

5  
d

Note que em C++ também podemos fazer comentários utilizando // (que é válido apenas para a linha atual).

11.2 Ponteiros e Referências

Como discutido em capítulos anteriores, um ponteiro é uma variável que guarda o endereço de memória de outra variável. C++ facilita a utilização de ponteiros por meio de referências. De certo modo, uma refêrencia é um ponteiro constante, ou seja, um ponteiro que, uma vez inicializado para uma certa variável, não pode ser mudado.

Por exemplo,

#include <stdio.h>

int main ()
{
  int var = 5;
  int *myPointer = &var;
  printf("%i", *myPointer);
  
  int newVar = 10;
  myPointer = &newVar;
  printf("%i", *myPointer);
  
  var = 15;
  printf("%i", *myPointer); // Como o ponteiro agora aponta para newVar, a alteracao de var nao afeta o ponteiro.
  
  return 0;
}

A resposta do programa é:

5  
10  
10

Nesse caso, para atribuir um novo valor a newVar, basta passá-lo pelo ponteiro

*myPointer = 20;
printf("%i", newVar);

cuja resposta deve ser:

20

No entanto, no código abaixo, myReference está permanentemente associada à variável var.

#include <stdio.h>

int main ()
{
  int var = 15;
  int &myReference = var;
  printf("var: %i --- myReference: %i \n", var, myReference);
  	
  int newVar = 20;
  myReference = newVar; // myReference continua apontando para *var*. Aqui, atribuimos indiretamente a *var* o valor de *newVar*.
  printf("var: %i --- myReference: %i --- newVar: %i \n", var, myReference, newVar);
  			
  myReference = 10;
  printf("var: %i --- myReference: %i --- newVar: %i \n", var, myReference, newVar);
  	
  return 0;
}

Ao que o programa responde:

var: 15 --- myReference: 15  
var: 20 --- myReference: 20 --- newVar: 20  
var: 10 --- myReference: 10 --- newVar: 20

Observe também que, quando utilizamos referências, não é necessário usar o operador de desreferência * para acessar o valor das variáveis.

Agora, considere os seguintes exemplos. Em C++, na chamada de funções que utilizam ponteiros como parâmetros, é preciso passar o endereço da varíavel como argumento de chamada. Para isso, utilizamos o operador referenciador:

#include <stdio.h>

void imprimeVariavel(int* i)
{
  printf("%i", *i);
}

int main()
{
  int var = 10;
  imprimeVariavel(&var); // Utilizamos o operador referenciador &
  
  return 0;
}

No entanto, quando utilizamos referências, o operador referênciador não é mais necessário na chamada da função:

#include <stdio.h>

void imprimeVariavel(int &i)
{
  printf("%i", i);
}

int main()
{
  int var = 10;
  imprimeVariavel(var); // Passa *var* como referencia, apesar da sintaxe sugerir que é passada como um valor.

  return 0;
}

Bibliografia para este capítulo

  • Stroustrup B. 2014. Programming: Principles and Practice Using C++. 2ed. Addison-Wesley Professional, USA.
  • Prata S. 2012. C++ Primer Plus. 6ed. Addison-Wesley Professional, USA.

12. Orientação a objetos no C++

A linguagem C++ é uma combinação de programação procedural e orientada a objetos. A orientação a objetos é um conceito fundamental na programação e apresenta grandes vantagens em relação a programação procedural:

  • Proporciona maior segurança de dados, pois estes são tratados por meio de objetos;
  • Facilita a estruturação dos programas;
  • Ajuda a diminuir a repetição de código e a manter o programa enxuto;
  • Apresenta recursos importantes como abstração, encapsulamento, polimorfismo e herança.

Em C++, a orientação a objetos se dá por meio de classes. Classes são abstrações de estruturas de dados que podem conter variáveis, funções (conhecidas também como métodos) e construtores e destrutores. Um objeto é uma instância de uma classe, ou seja, uma classe fornece a forma geral que um objeto pode ter.

No Arduino IDE, classes são geralmente utilizadas para definir bibliotecas. Assim, para utilizar um biblioteca externa, você deve primeiro importá-la para o seu projeto. Depois, deve instanciar o objeto correspondente para poder utilizar os seus métodos.

12.1 Criando nossa primeira Classe em C++

Por exemplo, imagine que queiramos criar uma classe de frutas. Nesse caso, cada fruta poderia ser entendida como uma instância da classe frutas, ou seja, como um objeto daquela classe. A classe de frutas poderia conter variáveis como cor, acidez, nome da espécie etc. Para cada objeto da classe, é possível atribuir um valor a essas variáveis.

No nosso exemplo,

#include <stdio.h>

using namespace std;

class Fruit {
  public: 
    char *cor;
    double acidez;
    char *especie;
};

int main()
{
  Fruit limao;
  limao.acidity = 1.8;
  limao.color = "verde";
  limao.especie = "galego";

  return 0;
}

O indentificador public é um especificador de acesso, e indica que podemos acessar as variáveis cor, acidez e especie de fora da classe (no nosso caso, diretamente da função main). Se o especificador não for fornecido, os membros serão considerados privados (private), e o acesso a eles só será concedido dentro da própria classe.

Podemos agora criar funções (ou métodos) que operam dados dos objetos. Neste caso, criamos um método dentro da definição da classe:

#include <stdio.h>

class Fruit {
  public: 
    char *color;
    double acidity;
    char *species;
    
    void howAcid(){
      if (acidity < 4) { printf("Consume sparingly or NEVER!"); }
      else if (acidity < 9) { printf("Just grab some fruit!"); }
      else { printf("Consume freely. Raw is best!"); }
    }
};  

int main()
{
  Fruit limao;
  limao.acidity = 1.8;
  
  limao.howAcid(); // por ser membro de Fruit, o método tem acesso direto a acidity
  
  return 0;
}

Resposta:

Consume sparingly or NEVER!

12.2 Separando a declaração e definição de métodos de classes

Alternativamente, podemos criar métodos fora da definição, como no exemplo que segue.

#include <stdio.h>

class Fruit {
  public: 
    char *color;
    double acidity;
    char *species;
		
    void howAcid(); // Nao se esqueca de declarar seu metodo definido fora da classe!
};

void Fruit::howAcid(){
  if (acidity < 4) { printf("Consume sparingly or NEVER!"); }
  else if (acidity < 9) { printf("Just grab some fruit!"); }
  else { printf("Consume freely. Raw is best!"); }
}
		
int main()
{
  Fruit limao;
  limao.acidity = 1.8;
  limao.howAcid();
	
  return 0;
}

O comportamento do programa é idêntico ao anterior. Este modo de definir separadamente o corpo da função possibilita separar a codificação da classe em arquivos de cabeçalho (.h) e arquivos .cpp.

12.3 Construtor da classe

Em classes de C++, o construtor é declarado como um método com o mesmo nome da classe e sem tipo de retorno especificado. Por exemplo:

#include <stdio.h>

class Fruit {
public: 
  char *color;
  double acidity;
  char *species;
    
  // Construtor
  Fruit(char *_color, double _acidity, char * _species) {
    color = _color;
    acidity = _acidity;
    species = _species;
  } 
  
  void printSpecies(); 
};

void Fruit::printSpecies() {
  printf("The species is %s !", species);
}

int main()
{
  Fruit tamara("brown", 7.5, "phoenix dactylifera");
  tamara.printSpecies();
  
  return 0;
}

Resposta:

The species is phoenix dactylifera!

Assim como as funções, os construtores também podem ser definidos fora da classe. No exemplo acima, o código seria:

#include <stdio.h>

class Fruits {
public:
  char *color;
  double acidity;
  char *species;
    
  Fruit(char *_color, double _acidity, char * _species); // Declaracao do construtor
  void printSpecies();
};  

//Construtor
Fruit::Fruit(char *_color, double _acidity, char *_species) {
  color = _color;
  acidity = _acidity;
  species = _species;
}
		
void Fruit::printSpecies() {
  printf("The species is %s !", species);
}

int main()
{
  Fruits tamara("brown", 7.5, "phoenix dactylifera");
  tamara.printSpecies();
  
  return 0;
}

12.4 Encapsulamento

É considerada boa prática manter os atributos de uma classe privados, sempre que possível. Isso garante melhor controle dos seus dados. Para isso, geralmente definem-se os métodos getter e setter para extrair e atribuir, respectivamente, o valor da variável que queremos manter privada.

Por exemplo,

#include <stdio.h>

class Fruit {
  private: 
    char *color;
  	
  public:
    void setColor(char *_color) { color = _color; }
    char *getColor() { return color; }	
};

int main()
{
  Fruit banana;
  banana.setColor("yellow");
  printf("%s", banana.getColor());
  
  return 0;
}

Resposta:

yellow

Bibliografia para este capítulo

  • Stroustrup B. 2014. Programming: Principles and Practice Using C++. 2ed. Addison-Wesley Professional, USA.
  • Prata S. 2012. C++ Primer Plus. 6ed. Addison-Wesley Professional, USA.