La presentazione è in caricamento. Aspetta per favore

La presentazione è in caricamento. Aspetta per favore

FUNZIONI: IL MODELLO APPLICATIVO 1) Valutazione, nellenvironment corrente, del simbolo che denota il nome della funzione; 2) Valutazione, nellenvironment.

Presentazioni simili


Presentazione sul tema: "FUNZIONI: IL MODELLO APPLICATIVO 1) Valutazione, nellenvironment corrente, del simbolo che denota il nome della funzione; 2) Valutazione, nellenvironment."— Transcript della presentazione:

1 FUNZIONI: IL MODELLO APPLICATIVO 1) Valutazione, nellenvironment corrente, del simbolo che denota il nome della funzione; 2) Valutazione, nellenvironment corrente, delle espressioni che denotano i parametri; 3) Commutazione allenvironment di definizione della funzione.

2 FUNZIONI: IL MODELLO APPLICATIVO 4)Chiamata della funzione; 5)Esecuzione del corpo della funzione (nel suo environment di definizione); 6)Restituzione al chiamante di –controllo –risultato con ripristino dellenvironment esistente al momento della chiamata.

3 LENVIRONMENT La definizione di una funzione introduce un nuovo binding nellenvironment di definizione della funzione –in C, il global environment Al momento dellinvocazione, si crea un nuovo environment –una struttura che contiene i binding dei parametri e degli identificatori dichiarati localmente alla funzione.

4 ESEMPIO int max (int x, int y ){ return x>y ? x : y; } Lenvironment corrente viene arricchito di un nuovo binding: max / CodiceDellaFunzione Quando max viene chiamata, si commuta a un nuovo environment, in cui sono definite le variabili intere x e y

5 COMUNICAZIONE CLIENTE SERVITORE Il cliente passa informazioni al servitore mediante una serie di parametri attuali. Parametri formali : –sono specificati nella dichiarazione del servitore –esplicitano il contratto fra servitore e cliente –indicano cosa il servitore si aspetta dal cliente Parametri attuali : –sono trasmessi dal cliente allatto della chiamata –devono corrispondere ai parametri formali in numero, posizione e tipo

6 COMUNICAZIONE CLIENTE SERVITORE Il cliente passa informazioni al servitore mediante una serie di parametri attuali. I Parametri attuali sono legati ai parametri formali al momento della chiamata, in modo dinamico. Tale legame: vale solo per linvocazione corrente vale solo per la durata della funzione.

7 IL NOSTRO ESEMPIO Il servitore... int max (int x, int y ){ return x>y ? x : y; } … e un possibile cliente: main(){ int z = 8; int m = max(2*z,13); }

8 IL NOSTRO ESEMPIO Il servitore... int max (int x, int y ){ return x>y ? x : y; } … e un possibile cliente: main(){ int z = 8; int m = max(2*z,13); } 1) Valutazione del simbolo z nellenvironment corrente Si trova che z vale 8.

9 IL NOSTRO ESEMPIO Il servitore... int max (int x, int y ){ return x>y ? x : y; } … e un possibile cliente: main(){ int z = 8; int m = max(2*z,13); } 2) Calcolo dellespressione 2*z nellenvironment corrente Si trova che vale 16.

10 IL NOSTRO ESEMPIO Il servitore... int max (int x, int y ){ return x>y ? x : y; } … e un possibile cliente: main(){ int z = 8; int m = max(2*z,13); } 3) Invocazione della funzione max con parametri attuali 16 e 13. Il controllo passa al servitore.

11 IL NOSTRO ESEMPIO Il servitore... int max (int x, int y ){ return x>y ? x : y; } … e un possibile cliente: main(){ int z = 8; int m = max(2*z,13); } 4) I parametri formali x e y vengono legati ai parametri attuali 16 e 13. Inizia lesecuzione del servitore.

12 IL NOSTRO ESEMPIO Il servitore... int max (int x, int y ){ return x>y ? x : y; } … e un possibile cliente: main(){ int z = 8; int m = max(2*z,13); } 5) Viene valutata lespressione condizionale nellenvironment del servitore. Il risultato è 16.

13 IL NOSTRO ESEMPIO Il servitore... int max (int x, int y ){ return x>y ? x : y; } … e un possibile cliente: main(){ int z = 8; int m = max(2*z,13); } 6) Il valore così determinato (16) viene restituito al cliente. Il servitore termina e il controllo torna al cliente.

14 IL NOSTRO ESEMPIO Il servitore... int max (int x, int y ){ return x>y ? x : y; } … e un possibile cliente: main(){ int z = 8; int m = max(2*z,13); } 7) Il valore restituito (16) viene usato per inizializzare la variabile m (nellenvironment del cliente)

15 RIASSUNTO Allatto dellinvocazione di una funzione: si crea una nuova attivazione (istanza) del servitore si alloca la memoria per i parametri (e le eventuali variabili locali) si trasferiscono i parametri al servitore si trasferisce il controllo al servitore si esegue il codice della funzione.

16 PASSAGGIO DEI PARAMETRI In generale, un parametro può essere trasferito: per valore o copia (by value) si trasferisce il valore del parametro attuale per riferimento (by reference) si trasferisce un riferimento al parametro attuale

17 PASSAGGIO PER VALORE si trasferisce una copia del valore del parametro attuale cliente z 45

18 PASSAGGIO PER VALORE si trasferisce una copia del valore del parametro attuale clienteservitore z 45 copia w 45 valore (copiato) di z istanza del servitore

19 PASSAGGIO PER VALORE si trasferisce una copia del valore del parametro attuale clienteservitore z 45 copia w 45 valore (copiato) di z istanza del servitore Ogni azione fatta su w è strettamente locale al servitore

20 PASSAGGIO PER RIFERIMENTO si trasferisce un riferimento al parametro attuale cliente z 45

21 PASSAGGIO PER RIFERIMENTO si trasferisce un riferimento al parametro attuale clienteservitore z 45 riferimento w riferimento a z (indirizzo) istanza del servitore

22 PASSAGGIO PER RIFERIMENTO si trasferisce un riferimento al parametro attuale clienteservitore z 45 riferimento w riferimento a z (indirizzo) istanza del servitore Ogni azione fatta su w è in realtà fatta sulla variabile z del cliente!

23 PASSAGGIO DEI PARAMETRI IN C In C, i parametri sono trasferiti sempre e solo per valore (by value) si trasferisce una copia del parametro attuale, non loriginale! tale copia è strettamente privata e locale a quel servitore il servitore potrebbe quindi alterare il valore ricevuto, senza che ciò abbia alcun impatto sul cliente

24 PASSAGGIO DEI PARAMETRI IN C In C, i parametri sono trasferiti sempre e solo per valore (by value) Conseguenza: è impossibile usare un parametro per trasferire informazioni verso il cliente per trasferire (una) informazione al cliente si sfrutta il valore di ritorno della funzione

25 ESEMPIO: VALORE ASSOLUTO Definizione formale: |x|: N N |x| vale x se x 0 |x| vale -x se x 0 Codifica sotto forma di funzione C: int valAss(int x) { return (x<0) ? -x : x; }

26 ESEMPIO: VALORE ASSOLUTO Servitore & Cliente: int valAss(int x) { return (x<0) ? -x : x; } main(){ int z = -87; int absz = valAss(z); int abs4 = valAss(4); }

27 ESEMPIO: VALORE ASSOLUTO Servitore & Cliente: int valAss(int x) { return (x<0) ? -x : x; } main(){ int z = -87; int absz = valAss(z); int abs4 = valAss(4); } Quando valAss(z) viene chiamata, il valore attuale di z, valutato nel- lenvironment corrente (-87), viene copiato e passato a valAss.

28 ESEMPIO: VALORE ASSOLUTO Servitore & Cliente: int valAss(int x) { return (x<0) ? -x : x; } main(){ int z = -87; int absz = valAss(z); int abs4 = valAss(4); } valAss riceve quindi una copia del valore -87 e la lega al simbolo x. Poi si valuta lespressione, che qui vale 87, e si restituisce questo valore.

29 ESEMPIO: VALORE ASSOLUTO Servitore & Cliente: int valAss(int x) { return (x<0) ? -x : x; } main(){ int z = -87; int absz = valAss(z); int abs4 = valAss(4); } Il valore restituito (87) viene usato per inizializzare la variabile absz.

30 ESEMPIO: MASSIMO DI DUE NUMERI Definizione formale: max: N N N max(x,y) vale x se x y max(x,y) vale y se x < y Codifica sotto forma di funzione C: int max(int x, int y) { return (x<y) ? y : x; }

31 ESEMPIO: MASSIMO DI DUE NUMERI Servitore & Cliente: int max(int x, int y) { return (x<y) ? y : x; } main(){ int z = -87, y = 12; int m = max(z,18); int n = max(4*y,z+100); }

32 ESEMPIO: MASSIMO DI DUE NUMERI Servitore & Cliente: int max(int x, int y) { return (x<y) ? y : x; } main(){ int z = -87, y = 12; int m = max(z,18); int n = max(4*y,z+100); } Si valutano le due espressioni che costituiscono i parametri attuali (nel- lenvironment del main) e si trasmette alla funzione copia dei valori ottenuti.

33 ESEMPIO: MASSIMO DI DUE NUMERI Servitore & Cliente: int max(int x, int y) { return (x<y) ? y : x; } main(){ int z = -87, y = 12; int m = max(z,18); int n = max(4*y,z+100); } La funzione riceve copia dei valori -87 e 18, e li lega ai simboli x e y. Poi si valuta lespressione. Il risultato (18) è restituito al main chiamante.

34 PASSAGGIO DEI PARAMETRI IN C Perché il C adotta sempre e solo il passaggio per valore (by value)? è sicuro: le variabili del chiamante e del chiamato sono completamente disac- coppiate consente di ragionare per componenti e servizi: la struttura interna dei singoli componenti è irrilevante

35 PASSAGGIO DEI PARAMETRI IN C Limiti: consente di restituire al cliente solo valori di tipo (relativamente) semplice non consente di restituire collezioni di valori non consente di scrivere componenti software il cui scopo sia diverso dal calcolo di una espressione

36 PASSAGGIO DEI PARAMETRI Molti linguaggi mettono a disposizione il passaggio per riferimento (by reference) non si trasferisce una copia del valore del parametro attuale si trasferisce un riferimento al parametro, in modo da dare al servitore accesso di- retto al parametro in possesso del cliente il servitore accede e modifica direttamente il dato del cliente.

37 PASSAGGIO DEI PARAMETRI IN C Il C non supporta direttamente il passaggio per riferimento è una grave mancanza! il C lo fornisce indirettamente solo per alcuni tipi di dati ergo, occorre costruirselo quando serve. (vedremo dei casi) Il C++ e Java invece lo forniscono.

38 DEFINIZIONE DI NUOVE FUNZIONI & STRATEGIE DI COMPOSIZIONE La capacità di definire nuove funzioni permette: di definire nuove operazioni di introdurre variabili per denotare i dati in modo simbolico di esprimere la ripetizione di una espressione per un numero (prefissato o meno) di volte.

39 STRATEGIE DI COMPOSIZIONE Tre grandi approcci: 1) la composizione di funzioni; 2) le espressioni condizionali; 3) la ricorsione. Le funzioni definibili in termini di un insieme prescelto di primitive e delle precedenti strategie di composizione costituiscono un insieme detto delle funzioni ricorsive generali.

40 1) COMPOSIZIONE DI FUNZIONI I parametri in una chiamata di funzione pos- sono consistere/comprendere altre funzioni Es: f(x) + g(f(x), q(x + f(y))) x1 = f(x) x2 = f(x) (mossa evitabile da un automa intelligente ) x3 = f(y) x4 = x + x3 x5 = q( x4 ) x6 = g( x2,x5 ) x7 = x1 + x6

41 2) ESPRESSIONE CONDIZIONALE Lespressione condizionale riflette la consuetudine matematica di definire le funzioni per elencazione di casi. Esempio: abs: N -> N abs(x) vale x se x 0 abs(x) vale -x se x 0

42 3) LA RICORSIONE La ricorsione consiste nella pos- sibilità di definire una funzione in termini di se stessa. È basata sul principio di induzione matematica: –se una proprietà P vale per n=n 0 –e si può provare che, assumendola valida per n, allora vale per n+1 allora P vale per ogni n n 0

43 LA RICORSIONE Operativamente, risolvere un problema con un approccio ricorsivo comporta –di identificare un caso base la cui soluzione sia ovvia –di riuscire a esprimere la soluzione al caso generico n in termini dello stesso problema in uno o più casi più semplici (n-1, n-2, etc).

44 LA RICORSIONE: ESEMPIO Esempio !: N N n! vale 1 se n 0 n! vale n*(n-1)! se n > 0 Codifica: int fact(int n) { return n==0 ? 1 : n*fact(n-1); }

45 LA RICORSIONE: ESEMPIO Esempio !: N N n! vale 1 se n 0 n! vale n*(n-1)! se n > 0 Codifica: int fact(int n) { return n==0 ? 1 : n*fact(n-1); } Attenzione: la codifica non corrisponde alla specifica!! Il 2° caso si applica per n 0, cioè anche per n<0 !! MA COSÌ PUÒ NON TERMINARE!

46 LA RICORSIONE: ESEMPIO Esempio !: N N n! vale 1 se n 0 n! vale n*(n-1)! se n > 0 Codifica: int fact(int n) { /* return n==0 ? 1 : n*fact(n-1); */ return n>0 ? n*fact(n-1) : 1; } Nuova codifica

47 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); }

48 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Si valuta lespressione che costituisce il parametro attuale (nellenvironment del main) e si trasmette alla funzione fatt una copia del valore così ottenuto (7).

49 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } La funzione riceve una copia del valore 7 e la lega al simbolo n. Poi valuta lespres- sione condizionale: ciò impone di valutare una espressione che contiene una nuova chiamata di funzione.

50 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Si valuta quindi, nellenvironment di fatt, lespressione n-1 (che vale 6), e si effettua una nuova chiamata al servitore fatt, pas- sandogli una copia del valore 6.

51 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Il (nuovo) servitore riceve quindi una copia del valore 6 e, come sopra, valuta lespres- sione condizionale. Ciò lo porta a dover fare una nuova chiamata passando 5.

52 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } … la cosa prosegue, servitore dopo servitore.....

53 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Prima o poi, dato che il valore passato cala ogni volta, si giunge a invocare fatt con parametro 1. In questo caso, la valutazione dellespressione dà come risultato 1.

54 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Ciò chiude la sequenza di chiamate ricorsive. Il controllo torna al servitore precedente, che può finalmente valutare lespressione n*1 (valutando n nel suo environment, dove vale 2) ottenendo 2.

55 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Il valore 2 viene restituito al servitore pre- cedente, che a sua volta può così valutare lespressione n*2 (valutando n nel suo environment, dove vale 3) ottenendo 6.

56 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } … la cosa prosegue...

57 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Prima o poi, a forza di retrocedere, si torna al primo servitore attivato, che può quindi valutare lespressione n*720 (valutando n nel suo environment, dove vale 7), giun- gendo così a trovare il valore 5040.

58 ESEMPIO: FATTORIALE Servitore & Cliente: int fatt(int n) { return (n>0) ? n*fatt(n-1) : 1; } main(){ int z = 5; int fz = fatt(z+2); int f6 = fatt(6); } Il valore 5040, restituito dal servitore fatt, può quindi essere usato per inizializzare la variabile fz.

59 UN ALTRO ESEMPIO Problema: calcolare la somma dei primi N interi Specifica: Considera la somma (1+2+3+...+(N-1)+N come composta di due termini: (1+2+3+...+(N-1)) N Esiste un caso banale assolutamente ovvio: la somma fino a 1 vale 1.

60 UN ALTRO ESEMPIO Problema: calcolare la somma dei primi N interi Specifica: Considera la somma (1+2+3+...+(N-1)+N come composta di due termini: (1+2+3+...+(N-1)) N Esiste un caso banale assolutamente ovvio: la somma fino a 1 vale 1. Il primo termine non è altro che la soluzione allo stesso problema in un caso più semplice Il secondo termine è un valore già noto.

61 UN ALTRO ESEMPIO Problema: calcolare la somma dei primi N interi Codifica: int sommaFinoA(int n){ return (n==1) ? 1 : sommaFinoA(n-1) + n; }

62 UN TERZO ESEMPIO Problema: calcolare lN-esimo numero di Fibonacci 0, se n=0 fib(n-1) + fib(n-2), altrimenti fib (n) =1, se n=1

63 UN TERZO ESEMPIO Problema: calcolare lN-esimo numero di Fibonacci Codifica: unsigned fibonacci(unsigned n) { return (n<2) : n ? fibonacci(n-1) + fibonacci(n-2); }

64 UN TERZO ESEMPIO Problema: calcolare lN-esimo numero di Fibonacci Codifica: unsigned fibonacci(unsigned n) { return (n<2) : n ? fibonacci(n-1) + fibonacci(n-2); } Ricorsione non lineare: ogni invocazione del servitore causa due nuove chiamate al servitore medesimo.

65 UNA RIFLESSIONE Negli esempi di ricorsione visti finora fattoriale somma dei primi N interi calcolo dellN-esimo numero di Fibonacci si inizia a sintetizzare il risultato solo dopo che si sono aperte tutte le chiamate, a ritroso, mentre le chiamate si chiudono.

66 UNA RIFLESSIONE Le chiamate ricorsive decompongono via via il problema, ma non calcolano nulla Il risultato viene sintetizzato a partire dalla fine, perché prima occorre arrivare al caso banale: il caso banale fornisce il valore di partenza poi, e solo poi, si sintetizzano, a ritroso, i successivi risultati parziali.

67 UNA RIFLESSIONE Ciò indica che tali soluzioni sono sintatticamente ricorsive e danno luogo a un processo computa- zionale effettivamente ricorsivo.

68 UN ESEMPIO DIVERSO Problema: trovare il Massimo Comun Divisore tra N e M m, se m=n MCD(m, n-m),se m<n MCD(m, n) =MCD(m-n, n),se m>n

69 UN ESEMPIO DIVERSO Problema: calcolare il Massimo Comun Divisore tra N e M Codifica: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); }

70 UN ESEMPIO DIVERSO Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); } main(){ int m = mcd(36,15); }

71 UN ESEMPIO DIVERSO Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); } main(){ int m = mcd(36,15); } Si valutano i parametri attuali e si invoca la funzione con parametri 36 e 15

72 UN ESEMPIO DIVERSO Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); } main(){ int m = mcd(36,15); } Si legano i parametri 36 e 15 ai parametri formali m e n, e si valuta lespressione condizionale.

73 UN ESEMPIO DIVERSO Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); } main(){ int m = mcd(36,15); } Poiché 36 15, si invoca nuovamente la funzione con parametri m-n (21) e n (15).

74 UN ESEMPIO DIVERSO Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); } main(){ int m = mcd(36,15); } Il nuovo servitore, poiché 21 15, fa la stessa cosa e invoca nuovamente la funzione con parametri m-n (6) e n (15).

75 UN ESEMPIO DIVERSO Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); } main(){ int m = mcd(36,15); } Il nuovo servitore, poiché 6 15, invoca un ulteriore servitore con parametri m (6) e n-m (9).

76 UN ESEMPIO DIVERSO Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); } main(){ int m = mcd(36,15); } Poiché 6 9, si ha una nuova chiamata con parametri m (6) e n-m (3).

77 UN ESEMPIO DIVERSO Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); } main(){ int m = mcd(36,15); } Poiché 6 3, si ha una nuova invocazione (lultima!) con parametri m-n (3) e n (3).

78 UN ESEMPIO DIVERSO Servitore & Cliente: int mcd(int m, int n){ return (m==n) : m ? (m>n) ? mcd(m-n, n) : mcd(m, n-m); } main(){ int m = mcd(36,15); } Poiché 3 = 3, il servitore termina e restituisce come risultato 3.

79 UN ESEMPIO DIVERSO Perché questo esempio è diverso ? il risultato viene sintetizzato via via che le chiamate si aprono, in avanti quando le chiamate si chiudono non si fa altro che riportare indietro, fino al cliente, il risultato ottenuto.

80 UNA RIFLESSIONE La soluzione ricorsiva individuata per lMCD è sintatticamente ricorsiva... … ma dà luogo a un processo computa- zionale diverso dal precedente: un processo computazionale ITERATIVO Il risultato viene sintetizzato in avanti ogni passo decompone e calcola e porta in avanti il nuovo risultato parziale

81 UNA RIFLESSIONE Ogni processo computazionale ITERATIVO calcola a ogni passo un risultato parziale dopo k passi, si ha a disposizione il risultato parziale relativo al caso k questo non è vero nei processi computa- zionali ricorsivi là, finché non si sono aperte tutte le chiamate, non è disponibile nessun risultato!

82 RICORSIONE TAIL Una ricorsione che realizza un processo computazionale ITERATIVO è una ricorsione solo apparente la chiamata ricorsiva è sempre lultima istruzione i calcoli sono fatti prima la chiamata serve solo, dopo averli fatti, per proseguire la computazione questa forma di ricorsione si chiama RICORSIONE TAIL (ricorsione in coda)


Scaricare ppt "FUNZIONI: IL MODELLO APPLICATIVO 1) Valutazione, nellenvironment corrente, del simbolo che denota il nome della funzione; 2) Valutazione, nellenvironment."

Presentazioni simili


Annunci Google