Skip to content

9. Υποπρογράμματα

  • Σχεδιασμός υποπρογραμμάτων
  • Μέθοδοι μεταβίβασης παραμέτρων
  • Τοπικά περιβάλλοντα αναφοράς
  • Υπερφορτωμένα υποπρογράμματα
  • Γενικά υποπρογράμματα
  • Ψευδωνυμία
  • Έμμεση κλήση υποπρογραμμάτων
  • Κλειστότητες
  • Συρρουτίνες

9.1 Εισαγωγή

Τα υποπρογράμματα αποτελούν αφαιρέσεις διεργασιών. Οι λεπτομέρειες μιας διεργασίας αντικαθίστανται με την κλήση ενός υποπρογράμματος. Έτσι διευκολύνεται η ανάγνωση του κώδικα, εξοικονομείται χώρος μνήμης και χρόνος κωδικοποίησης.

9.2 Βασικά στοιχεία υποπρογραμμάτων

9.2.1 Γενικά στοιχεία υποπρογραμμάτων

  • Κάθε υποπρόγραμμα έχει ένα σημείο εισόδου.
  • Μόνο ένα υποπρόγραμμα εκτελείται ανά πάσα στιγμή (δεν ισχύει στα ταυτόχρονα προγράμματα).
  • Ο έλεγχος επιστρέφει στο πρόγραμμα που καλεί ένα υποπρόγραμμα όταν ολοκληρώνεται η κλήση του.

9.2.2 Βασικοί ορισμοί

Βασικά είδη υποπρογραμμάτων: διαδικασίες, συναρτήσεις

Κεφαλίδα υποπρογράμματος

Η κεφαλίδα ενός υποπρογράμματος καθορίζει το είδος του, το όνομά του (αν έχει) και τη λίστα παραμέτρων του.

Σώμα υποπρογράμματος

Το σώμα ενός υποπρογράμματος ορίζει τις ενέργειες που θα εκτελεί το υποπρόγραμμα.

Ιδιαίτερες περιπτώσεις

Στην Python οι ορισμοί των συναρτήσεων είναι εκτελέσιμοι. Οπότε μπορεί να γραφεί κώδικας της μορφής:

x = 1
if x == 1:
    def foo():
        print("A")
else:
    def foo():
        print("B")
foo()

Η εκτέλεση του παραπάνω κώδικα θα εμφανίσει A

Στην Ruby αν και συνήθως οι μέθοδοι ορίζονται σε κλάσεις, ωστόσο μπορούν να οριστούν και εκτός κλάσεων οπότε θεωρούνται μέθοδοι της κλάσης Object.

Στην Lua οι συναρτήσεις είναι ανώνυμες, αλλά μπορούν να οριστούν με τέτοιο τρόπο που να τους αποδίδεται όνομα

function cube(x) return x * x * x end

ή (με ανώνυμη συνάρτηση)

cube = function(x) return x * x * x end

Το πρωτόκολλο ενός υποπρογράμματος περιέχει τον αριθμό των παραμέτρων, τη σειρά με την οποία παρατίθενται, τον τύπο τους και εάν πρόκειται για συνάρτηση τον τύπο επιστροφής της.

Στη C και στη C++ χρησιμοποιούνται τα πρωτότυπα συναρτήσεων που αποτελούν δηλώσεις συναρτήσεων πριν τον ορισμό τους. Στις περισσότερες άλλες γλώσσες δεν χρησιμοποιούνται ξεχωριστές δηλώσεις των υποπρογραμμάτων.

Παράδειγμα με συνάρτηση χωρίς prototype στη C

prototype0.c
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int fun1(int a, int b) { return a + b; }

int main() {
  int x = 1, y = 2, z;
  z = fun1(x, y);
  printf("z=%d\n", z);
}

Παράδειγμα με prototype στη C

prototype1.c
#include <stdio.h>

// prototype της συνάρτησης fun
int fun1(int a, int b);

int main() {
  int x = 1, y = 2, z;
  z = fun1(x, y);
  printf("z=%d\n", z);
}

int fun1(int a, int b) { return a + b; }

Παράδειγμα με prototype σε header στη C

prototype2.h
1
2
3
4
5
6
7
// prototype της συνάρτησης fun
int fun1(int, int);

int fun1(int a, int b)
{
  return a + b;
}
prototype2.c
1
2
3
4
5
6
7
8
#include "prototype2.h"
#include <stdio.h>

int main() {
  int x = 1, y = 2, z;
  z = fun1(x, y);
  printf("z=%d\n", z);
}

Παράδειγμα με prototype σε header στη C και ξεχωριστό αρχείο πηγαίου κώδικα για τη συνάρτηση που δηλώνεται στο header αρχείο

my_functions.h
// prototype της συνάρτησης fun
int fun1(int, int);
my_functions.c
int fun1(int a, int b) { return a + b; }
prototype3.c
1
2
3
4
5
6
7
8
9
#include "my_functions.h"

#include <stdio.h>

int main() {
  int x = 1, y = 2, z;
  z = fun1(x, y);
  printf("z=%d\n", z);
}

Η μεταγλώττιση και εκτέλεση σε αυτή την περίπτωση θα πρέπει να γίνει ως εξής:

$ gcc my_functions.c prototype3.c -o prototype3
$ ./prototype3

9.2.3 Παράμετροι

Οι παράμετροι στην κεφαλίδα του υποπρογράμματος ονομάζονται τυπικές ή επίσημες (formal) παράμετροι ή ορίσματα, ενώ οι παράμετροι στην κλήση του υποπρογράμματος ονομάζονται πραγματικές (actual) παράμετροι ή απλά παράμετροι.

Παράμετροι θέσης και παράμετροι λέξεων κλειδιών

Παράδειγμα στην Python με παραμέτρους λέξεων κλειδιών (keywords)

def fun(x, y, z):
    print(
        f"Το πρώτο όρισμα είναι {x}, το δεύτερο όρισμα είναι {y}, το τρίτο όρισμα είναι {z}"
    )


fun(1, 2, 3) 
fun(x=1, y=2, z=3) # κλήση με keywords
fun(y=2, x=1, z=3) # κλήση με keywords
fun(1, z=3, y=2) # κλήση με απλά ορίσματα και keywords

θα εμφανίσει

Το πρώτο όρισμα είναι 1, το δεύτερο όρισμα είναι 2, το τρίτο όρισμα είναι 3
Το πρώτο όρισμα είναι 1, το δεύτερο όρισμα είναι 2, το τρίτο όρισμα είναι 3
Το πρώτο όρισμα είναι 1, το δεύτερο όρισμα είναι 2, το τρίτο όρισμα είναι 3
Το πρώτο όρισμα είναι 1, το δεύτερο όρισμα είναι 2, το τρίτο όρισμα είναι 3

Παράδειγμα στη C++ με "named arguments" που κάνει χρήση της δυνατότητας αρχικοποίησης δομών με χρήση ονομάτων πεδίων:

struct_initialization.cpp
#include <iostream>

using namespace std;

struct name
{
    string first;
    string last;
};

int main()
{
    struct name r1 = {"John", "Doe"};
    struct name r2 = {.first = "John", .last = "Doe"};
    struct name r3 = {.first = "John", "Doe"};
    struct name r4 = {"John", .last = "Doe"};

    cout << r1.first << " " << r1.last << endl;
    cout << r2.first << " " << r2.last << endl;
    cout << r3.first << " " << r3.last << endl;
    cout << r4.first << " " << r4.last << endl;
}

// John Doe
// John Doe
// John Doe
// John Doe
named_args.cpp
#include <iostream>

using namespace std;

struct name
{
    string first;
    string last;
};

void display_full_name(name args)
{
    cout << args.first << " " << args.last << endl;
}

int main()
{
    display_full_name({.first = "John", .last = "Doe"});
}

// John Doe

Προκαθορισμένες (default) τιμές

Σε γλώσσες όπως οι Python, Ruby, C++, PHP οι τυπικές παράμετροι μπορούν να έχουν προκαθορισμένες τιμές.

Παράδειγμα στην Python με προκαθορισμένες τιμές παραμέτρων

def fun(x=1, y=2):
    print(x, y)


fun() # κλήση της συνάρτησης fun χωρίς ορίσματα (το x λαμβάνει την τιμή 1 και το y λαμβάνει την τιμή 2)
fun(5) # το x λαμβάνει την τιμή 5 και το y λαμβάνει την προκαθορισμένη τιμή 2)
fun(x=5) # το x λαμβάνει την τιμή 5 και το y λαμβάνει την προκαθορισμένη τιμή 2)
fun(y=10) # το x λαμβάνει την προκαθορισμένη τιμή 1 και το y λαμβάνει την τιμή 10)
fun(5, 10) # το x λαμβάνει την τιμή 5 και το y λαμβάνει την τιμή 10)
fun(5, y=10) # το x λαμβάνει την τιμή 5 και το y λαμβάνει την τιμή 10)

θα εμφανίσει:

1 2
5 2
5 2
1 10
5 10
5 10

Παράδειγμα στη C++ με προκαθορισμένες τιμές παραμέτρων (οι παράμετροι με προκαθορισμένες τιμές πρέπει να εμφανίζονται τελευταίοι στη λίστα παραμέτρων της συνάρτησης)

default_args.cpp
#include <iostream>

using namespace std;

void fun(int x, int y = 1, int z = 2)
{
    cout << "x=" << x << " y=" << y << " z=" << z << endl;
}

int main()
{
    fun(10);
    fun(10, 20);
    fun(10, 20, 30);
}

/*
x=10 y=1 z=2
x=10 y=20 z=2
x=10 y=20 z=30
*/

Μεταβλητός αριθμός παραμέτρων

Παράδειγμα στην Python

def fun(*args, **kwargs):
    print(args)
    print(kwargs)


fun(1, 2, 3, a="data1", b="data2")

θα εμφανίσει:

(1, 2, 3)
{'a': 'data1', 'b': 'data2'}

Παράδειγμα χρήσης του *args έτσι ώστε να μπορούν να γίνουν αλλαγές χωρίς να επηρεάζεται παλιός κώδικας (backwards compatibility)

def fun(a,b):
    return a + b 

Έστω ότι υπάρχει ήδη ο παραπάνω κώδικας για τη συνάρτηση fun η οποία καλείται σε διάφορα σημεία τα οποία δεν θέλουμε να επηρεαστούν. Ωστόσο, επιθυμούμε να προσθέσουμε επιπλέον ορίσματα στη συνάρτηση fun.

def fun(a,b, *args):
    if args:
        return a + b + args[0]
    else:
        return a + b

print(fun(1,2)) # κλήση της συνάρτησης όπως παλιότερα
print(fun(1,2,3)) # κλήση της συνάρτησης με μια επιπλέον παράμετρο σε σχέση με πριν

Παράδειγμα στη C

H printf είναι ένα παράδειγμα variadic συνάρτησης. Η δήλωσή της είναι η ακόλουθη:

int printf(const char* format, ...);

Δείτε τη σελίδα στο cppreference για τις variadic functions στη C: https://en.cppreference.com/w/c/variadic

variadic_function.c
#include <stdarg.h>
#include <stdio.h>

// η συνάρτηση δέχεται ως πρώτο όρισμα το πλήθος των τιμών και στη συνέχεια τις ακέραιες τιμές και επιστρέφει το άθροισμά τους
int sum_all(int n, ...) {
  va_list args; // αρχικοποίηση του δείκτη των ορισμάτων
  va_start(args, n); // τοποθέτηση του δείκτη στο πρώτο (μη variadic) όρισμα.

  int sum = 0;
  for (int i = 0; i < n; i++) {
    int x = va_arg(args, int); // πρόσβαση στο επόμενο variadic όρισμα
    sum += x;
  }

  va_end(args); // τερματισμός διάσχισης των variadic ορισμάτων
  return sum;
}

int main() {
  printf("%d\n", sum_all(2, 10, 20));
  printf("%d\n", sum_all(3, 10, 20, 30));
  printf("%d\n", sum_all(4, 10, 20, 30, 40));
}

/*
30
60
100
*/

ισοδύναμος κώδικας σε Python

def sum_all(*args):
    return sum(args)

print(sum_all(10,20))
print(sum_all(10,20,30))
print(sum_all(10,20,30,40))

θα εμφανίσει:

30
60
100

9.2.4 Διαδικασίες και συναρτήσεις

Μόνο κάποιες παλιότερες γλώσσες όπως η Ada, Pascal και Fortran υποστηρίζουν τις διαδικασίες. Οι διαδικασίες είναι συλλογές εντολών που ορίζουν υπολογισμούς που καθορίζονται με βάση τις παραμέτρους. Οι συναρτήσεις είναι μια αφαίρεση των μαθηματικών συναρτήσεων. Ωστόσο οι συναρτήσεις μπορούν να προκαλούν side-effects, δηλαδή να αλλάζουν τιμές παραμέτρων ή τιμές άλλων μεταβλητών που ορίζονται εκτός των συναρτήσεων.

9.3 Ζητήματα σχεδιασμού για υποπρογράμματα

Υπερφορτωμένο είναι ένα υποπρόγραμμα που έχει το ίδιο όνομα με ένα άλλο υποπρόγραμμα στο ίδιο περιβάλλον αναφοράς.

Γενικό είναι ένα υποπρόγραμμα το οποίο μπορεί να κληθεί με παραμέτρους διαφορετικών τύπων από κλήση σε κλήση.

Κλειστότητα είναι ένα ένθετο υποπρόγραμμα και το περιβάλλον αναφοράς του.

9.4 Τοπικά περιβάλλοντα αναφοράς

9.4.1 Τοπικές μεταβλητές

Τα υποπρογράμματα μπορούν να ορίζουν τις δικές τους μεταβλητές, ορίζοντας με αυτόν τον τρόπο τοπικά περιβάλλοντα αναφοράς.

Παράδειγμα με τοπικές μεταβλητές (στατικές και δυναμικής δέσμευσης μνήμης) στη C++

static2.cpp
#include <iostream>

using namespace std;

int adder(int list[], int listlen) {
  static int sum = 0; // στατική τοπική μεταβλητή
  int count; // τοπική μεταβλητή δυναμική-στοίβας
  for (count = 0; count < listlen; count++) {
    sum += list[count];
  }
  return sum;
}

int main() {
  int a[] = {1, 2, 3};    // -> 6
  int b[] = {4, 5, 6, 7}; // -> 22
  cout << adder(a, 3) << endl;
  cout << adder(b, 4) << endl;
}

9.4.2 Ένθετα υποπρογράμματα

Η πρώτη γλώσσα που υποστήριξε την ένθεση υποπρογραμμάτων, δηλαδή τον ορισμό ενός υποπρογράμματος μέσα σε ένα άλλο υποπρόγραμμα ήταν η Algol 60. Άλλες γλώσσες προγραμματισμού που επίσης υποστηρίζουν την ένθεση υποπρογραμμάτων είναι οι Ada, Pascal, Python, JavaScript, Ruby και Lua.

Παράδειγμα ένθετης συνάρτησης στην Python:

def outer_fun():
    a = 1
    print(f"αρχή συνάρτησης outer_fun, a={a}")
    def inner_fun(x):
        print(f"κλήση ένθετης συνάρτησης inner_fun, x={x}, a={a}")

    inner_fun(2)
    print(f"τέλος συνάρτησης outer_fun, a={a}")

outer_fun()

θα εμφανίσει:

αρχή συνάρτησης outer_fun, a=1
κλήση ένθετης συνάρτησης inner_fun, x=2, a=1
τέλος συνάρτησης outer_fun, a=1

9.5 Μέθοδοι μεταβίβασης παραμέτρων

Μέθοδος μεταβίβασης παραμέτρων είναι ο τρόπος με τον οποίο οι παράμετροι μεταδίδονται προς και από τα καλούμενα υποπρογράμματα.

9.5.1 Μοντέλα σημασιολογίας μεταβίβασης παραμέτρων

  • Λειτουργία εισόδου: η τυπική παράμετρος δέχεται δεδομένα από την πραγματική παράμετρο
  • Λειτουργία εξόδου: η τυπική παράμετρος επιστρέφει δεδομένα στην πραγματική παράμετρο
  • Διπλή λειτουργία: η τυπική παράμετρος δέχεται δεδομένα απο την πραγματική παράμετρο και επιστρέφει δεδομένα στην πραγματική παράμετρο

Υπάρχουν δύο εννοιολογικά μοντέλα για τον τρόπο μεταφοράς δεδομένων στις παραμέτρους

  • αντιγραφή της τιμής
  • μετάδοση μιας διαδρομής πρόσβασης (δείκτης ή αναφορά)

9.5.2 Μοντέλα υλοποίησης μεταβίβασης παραμέτρων

9.5.2.1 Μεταβίβαση κατά τιμή ή κατ' αξία (pass by value)

Η τιμή της πραγματικής μεταβλητής χρησιμοποιείται για την αρχικοποίηση της αντίστοιχης τυπικής παραμέτρου (λειτουργία εισόδου).

9.5.2.2 Μεταβίβαση κατ' αποτέλεσμα (pass by result)

Δεν μεταβιβάζεται καμία τιμή στο υποπρόγραμμα κατά την κλήση. Η τελική τιμή της τυπικής παραμέτρου μεταδίδεται στην αντίστοιχη τοπική μεταβλητή.

9.5.2.3 Μεταβίβαση κατά τιμή και αποτέλεσμα (pass by value-result) ή με αντιγραφή

Συνδυασμός της μεταβίβασης κατά τιμή και της μεταβίβασης κατά αποτέλεσμα. Η πραγματική παράμετρος αντιγράφεται στην τυπική όταν ξεκινά το υποπρόγραμμα και μετά αντιγράφεται ξανά πίσω όταν τερματίζεται το υποπρόγραμμα.

9.5.2.4 Μεταβίβαση κατά αναφορά (pass by reference)

Δεν πραγματοποιούνται αντιγραφές τιμών από τις πραγματικές παραμέτρους προς τις τυπικές παραμέτρους και αντίστροφα, όπως στη μεταβίβαση κατά τιμή και κατά αποτέλεσμα, αλλά μεταδίδεται μια διαδρομή πρόσβασης, συνήθως απλά μια διεύθυνση στο καλούμενο υποπρόγραμμα.

9.5.2.5 Μεταβίβαση κατά όνομα (pass by name)

Είναι μέθοδος μεταβίβασης διπλής λειτουργίας. Λειτουργεί με αντικατάσταση στο κείμενο του υποπρογράμματος του ονόματος του ορίσματος (παρόμοια με τις μακροεντολές της C). Δεν χρησιμοποιείται πλέον σε κάποια διαδεδομένη γλώσσα.

Δείτε σχετικά στο https://stackoverflow.com/questions/838079/what-is-pass-by-name-and-how-does-it-work-exactly

9.5.3 Υλοποίηση μεθόδων μεταβίβασης παραμέτρων

Στις περισσότερες σύγχρονες γλώσσες, η επικοινωνία μεταξύ παραμέτρων γίνεται μέσω της στοίβας χρόνου εκτέλεσης.

9.5.4 Μέθοδοι μεταβίβασης παραμέτρων σε γνωστές γλώσσες

C

Στη C όλες οι παράμετροι μεταβιβάζονται κατά τιμή. Με τη χρήση δεικτών επιτυγχάνεται η μεταβίβαση κατά αναφορά, και επιπλέον με τη χρήση του const στις τυπικές παραμέτρους η μονόδρομη μεταβίβαση κατά αναφορά.

pass_by_value.c
#include <stdio.h>

// x μεταβίβαση κατά τιμή
// y μεταβίβαση κατά αναφορά
// z μεταβίβαση κατά αναφορά (μονόδρομη)
void fun1(int x, int *y, const int *z) {
  x++;
  (*y)++;
  //   (*z)++; // error: increment of read-only location '*z'
  printf("fun1: x=%d, y=%d, z=%d\n", x, *y, *z);
}

int main() {
  int a = 1, b = 1, c = 1;
  fun1(a, &b, &c);
  printf("main: a=%d, b=%d, c=%d\n", a, b, c);
}

/*
fun1: x=2, y=2, z=1
main: a=1, b=2, c=1
*/

C++

Στη C++ όλες οι παράμετροι μεταβιβάζονται κατά τιμή. H C++ περιέχει ένα ειδικό δείκτη, την αναφορά που χρησιμοποιείται στις τυπικές παραμέτρους έτσι ώστε να υποδηλώσει μεταβίβαση με αναφορά.

pass_by_value.cpp
#include <iostream>

using namespace std;

// x μεταβίβαση κατά τιμή
// y μεταβίβαση κατά αναφορά
// z μεταβίβαση κατά αναφορά (μονόδρομη)
void fun1(int x, int &y, const int &z)
{
    x++;
    y++;
    // z++; // error: increment of read-only reference 'z'
    cout << "fun1: x=" << x << " y=" << y << " z=" << z << endl;
}

int main()
{
    int a = 1, b = 1, c = 1;
    fun1(a, b, c);
    cout << "main: a=" << a << " b=" << b << " c=" << c << endl;
}

/*
fun1: x=2 y=2 z=1
main: a=1 b=2 c=1
*/

Java

Στη Java όλες οι παράμετροι μεταβιβάζονται κατά τιμή. Ωστόσο, επιτυγχάνεται προσομοίωση μεταβίβασης με αναφορά με χρήση αντικειμένων.

Example1.java
public class Example1 {
    public static void foo(int x) {
        x++;
    }

    public static void bar(MyClass obj) {
        obj.a++;
    }

    public static void main(String[] args) {
        int x = 5;
        // μεταβίβαση κατά τιμή
        foo(x);
        System.out.println(x);

        MyClass obj = new MyClass(5);
        // προσομοίωση μεταβίβασης κατά αναφορά
        bar(obj);
        System.out.println(obj.a);
    }

}

class MyClass {
    public int a;

    public MyClass(int a) {
        this.a = a;
    }
}

Python

Στην Python όλες οι παράμετροι μεταβιβάζονται κατά εκχώρηση. Ωστόσο, επιτυγχάνεται προσομοίωση μεταβίβασης με αναφορά με χρήση αντικειμένων.

pass_by_assignment.py
def foo(x):
    x += 1


a = 5
# μεταβίβαση κατά τιμή
foo(a)
print(a)

print("#" * 40)


def fun(x):
    x[0] = 99


a_list = [1, 2, 3, 4, 5]
fun(a_list)
print(a_list)

print("#" * 40)


class MyClass:
    def __init__(self, a):
        self.a = a


def bar(x):
    x.a += 1


obj = MyClass(5)
# μεταβίβαση κατά αναφορά
bar(obj)
print(obj.a)

9.5.5 Έλεγχος τύπων παραμέτρων

Στην C (από την έκδοση C99 και μετά), C++, Java και C# γίνεται έλεγχος τύπων παραμέτρων. Στην Python και στην Ruby δεν γίνεται έλεγχος τύπων παραμέτρων.

9.5.6 Διατάξεις πολλαπλών διαστάσεων ως παράμετροι

Οι διατάξεις δύο διαστάσεων στη C είναι διατάξεις διατάξεων και αποθηκεύονται κατά γραμμές στη μνήμη.

Αντιστοίχιση δισδιάστατου πίνακα M x N (M γραμμές και N στήλες) σε μονοδιάστατο πίνακα στη C.

διεύθυνση(mat[i][j]) -> διεύθυνση(mat[0][0])  +  (i * N + j) * μέγεθος_τύπου_δεδομένων_περιεχομένων_του_mat)

Όταν ένας πίνακας μεταβιβάζεται ως παράμετρος, η τυπική παράμετρος θα πρέπει να περιλαμβάνει το πλήθος των στηλών στο δεύτερο ζεύγος αγκυλών (λόγω της δυνατότητας ξεχωριστής μεταγλώττισης των υποπρογραμμάτων). Συνεπώς, δεν μπορεί να χρησιμοποιηθεί η ίδια συνάρτηση για δισδιάστατο πίνακα με διαφορετικό αριθμό στηλών.

matrix1.c
#include <stdio.h>

void fun1(int matrix[4][3]) {
  for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 3; j++) {
      printf("%3d", matrix[i][j]);
    }
    printf("\n");
  }
}

void fun2(int matrix[][3]) {
  for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 3; j++) {
      printf("%3d", matrix[i][j]);
    }
    printf("\n");
  }
}

int main() {
  int mat[4][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}};
  fun1(mat);
  printf("\n");
  fun2(mat);
}

/*
  1  2  3
  4  5  6
  7  8  9
 10 11 12

  1  2  3
  4  5  6
  7  8  9
 10 11 12
*/

Μια λύση στο παραπάνω πρόβλημα είναι η μεταβίβαση του πίνακα ως δείκτη καθώς και των διαστάσεών του.

matrix2.c
#include <stdio.h>

void fun(int *mat_ptr, int M, int N) {
  for (int i = 0; i < M; i++) {
    for (int j = 0; j < N; j++) {
      printf("%3d", *(mat_ptr + (i * N) + j));
    }
    printf("\n");
  }
}

int main() {
  int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
  fun(a, 4, 3);
  printf("\n");
  fun(a, 3, 4);
}

/*
  1  2  3
  4  5  6
  7  8  9
 10 11 12

  1  2  3  4
  5  6  7  8
  9 10 11 12
*/

ή με χρήση μακροεντολής για "καθαρότερο" κώδικα.

matrix2b.c
#include <stdio.h>

#define mat_ptr(r, c) (*(mat_ptr + ((r) * N) + c)))

void fun(int *mat_ptr, int M, int N) {
  for (int i = 0; i < M; i++) {
    for (int j = 0; j < N; j++) {
      printf("%3d", mat_ptr(i,j);
    }
    printf("\n");
  }
}

int main() {
  int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
  fun(a, 4, 3);
  printf("\n");
  fun(a, 3, 4);
}

/*
  1  2  3
  4  5  6
  7  8  9
 10 11 12

  1  2  3  4
  5  6  7  8
  9 10 11 12
*/

Java

Στη Java οι διατάξεις είναι αντικείμενα. Κάθε διάταξη διαθέτει μια σταθερά length που είναι ίση με το μήκος της διάταξης και η οποία αρχικοποιείται όταν δημιουργείται η διάταξη. Κάθε στοιχείο μιας διάταξης μπορεί να είναι μια άλλη διάταξη.

Example1.java
class Example1 {

    static void fun(int mat[][]) {
        for (int i = 0; i < mat.length; i++) {
            for (int j = 0; j < mat[i].length; j++) {
                System.out.format("%3d", mat[i][j]);
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        int a[][] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
        fun(a);

        System.out.println();

        int b[][] = { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
        fun(b);
    }
}

// 1 2 3
// 4 5 6
// 7 8 9

// 1 2
// 3 4
// 5 6
// 7 8

9.5.7 Θέματα σχεδιασμού

Στη C++ (αλλά και σε άλλες γλώσσες προγραμματισμού) οι διατάξεις μεταβιβάζονται στα υποπρογράμματα με αναφορά προκειμένου να αποφευχθεί η επιβάρυνση που θα προκαλούσε η μεταβίβαση κατά τιμή καθώς σε αυτή την περίπτωση θα χρειαζόταν να αντιγραφούν όλα τα δεδομένα της διάταξης, κάτι που θα απαιτούσε επιπλέον χρόνο αλλά και χώρο.

9.5.8 Παραδείγματα μεταβίβασης παραμέτρων

Η ακόλουθη συνάρτηση δεν πραγματοποιεί σωστή αντιμετάθεση των παραμέτρων που δέχεται. Αν και οι μεταβλητές a και b ανταλλάσσουν τιμές, οι τιμές των c και d δεν αλλάζουν διότι δεν επιστρέφεται κάτι από τη συνάρτηση.

void swap1(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}
...
swap1(c,d);

Σωστή συνάρτηση αντιμετάθεση (με χρήση δεικτών):

void swap2(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
...
swap1(&c,&d);

Συνάρτηση αντιμετάθεσης στη C++ (με χρήση αναφορών):

void swap3(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}
...
swap1(c,d);

9.6 Παράμετροι που είναι υποπρογράμματα

Στη C και στη C++ οι συναρτήσεις δεν μπορούν να μεταβιβαστούν ως παράμετροι, μπορούν όμως οι δείκτες σε συναρτήσεις.

9.7 Έμμεση κλήση υποπρογραμμάτων

Υπάρχουν περιπτώσεις που τα υποπρογράμματα πρέπει να καλούνται έμμεσα και συνήθως αυτό συμβαίνει όταν το υποπρόγραμμα που πρέπει να κληθεί δεν είναι γνωστό παρά μόνο στην εκτέλεση.

function_pointer.c
#include <stdio.h>

double foo(int x) {
  printf("double foo(int)\n");
  return 3.14;
}

double bar(int x) {
  printf("double bar(int)\n");
  return 2.718;
}

int main() {
  double (*f)(int) = &foo;
  double r = f(1);
  printf("%f\n", r);

  f = &bar;
  r = f(2);
  printf("%f\n", r);
}

/*
double foo(int)
3.140000
double bar(int)
2.718000
*/
function_pointer2.c
#include <stdio.h>

int fun(int a, int b) { return a + b; }

int main() {
  int (*ptr)(int, int);
  ptr = &fun; // ή ptr = fun;

  int result;
  result = (*ptr)(1, 2); // ή ptr(1,2); ή fun(1,2);

  printf("%d\n", result);
}
trapezoid_rule.c
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

double f1(double x) { return x; }
double f2(double x) { return x * x; }
double f3(double x) { return x * x * x; }

double trapezio(double a, double b, int n, double (*fp)(double)) {
  double x_i, approx;
  int i;
  double h = (b - a) / n;
  approx = (fp(a) + fp(b)) / 2.0;
  for (i = 1; i <= n - 1; i++) {
    x_i = a + i * h;
    approx += fp(x_i);
  }
  approx *= h;
  return approx;
}

int main(int argc, char **argv) {
  double global_result = 0.0;

  int n = 1000000;
  double approx;
  double a = 0.0, b = 10.0;

  approx = trapezio(a, b, n, f1);
  printf("result: %.5f\n", approx);
  approx = trapezio(a, b, n, f2);
  printf("result: %.5f\n", approx);
  approx = trapezio(a, b, n, f3);
  printf("result: %.5f\n", approx);
  approx = trapezio(a, b, n, exp);
  printf("result: %.5f\n", approx);
  approx = trapezio(a, b, n, sqrt);
  printf("result: %.5f\n", approx);

  return 0;
}

// result: 50.00000
// result: 333.33333
// result: 2500.00000
// result: 22025.46579
// result: 21.08185

9.8 Ζητήματα σχεδιασμού για συναρτήσεις

9.8.1 Συναρτησιακές παράπλευρες συνέπειες

Οι περισσότερες προστακτικές γλώσσες μπορούν να εφαρμόζουν μεταβίβαση κατά τιμή ή μεταβίβαση κατά αναφορά. Η μεταβίβαση κατά αναφορά μπορεί να προκαλεί παράπλευρες συνέπειες. Από την άλλη μεριά οι αμιγείς συναρτησιακές γλώσσες δεν παρουσιάζουν παράπλευρες συνέπειες.

9.8.2 Τύποι επιστρεφόμενων τιμών

Οι συναρτήσεις στη C μπορούν να επιστρέφουν τιμές οποιουδήποτε τύπου εκτός από διατάξεις και συναρτήσεις. Ωστόσο, τόσο οι διατάξεις αλλά και οι συναρτήσεις επιστρέφονται μέσω δεικτών.

9.8.3 Πλήθος επιστρεφόμενων τιμών

Στις περισσότερες γλώσσες προγραμματισμού οι συναρτήσεις μπορούν να επιστρέφουν μόνο μια τιμή. Ωστόσο, γλώσσες όπως η Ruby και η Python επιτρέπουν την επιστροφή πολλών τιμών.

def fun(x):
    return x, 2*x, 3*x

a,b,c = fun(10) # οι τιμές που λαμβάνουν οι μεταβλητές a, b, c είναι 10, 20 και 30 αντίστοιχα.

9.9. Υπερφορτωμένα υποπρογράμματα

Ένα υποπρόγραμμα είναι υπερφορτωμένο αν έχει το ίδιο όνομα με άλλα υποπρογράμματα στο ίδιο περιβάλλον αναφοράς.

overload_example.cpp
#include <iostream>

using namespace std;

void foo(int x)
{
    cout << "int " << x << endl;
}

void foo(float x)
{
    cout << "float " << x << endl;
}

void foo(int x, float y, char z)
{
    cout << "another overloaded function" << endl;
}

int main()
{
    foo(4);
    foo(4.5f);
    foo(45, 6.7f, 'a');
}

Στις C++, Java, C# ο τύπος επιστροφής των συναρτήσεων δεν μπορεί να χρησιμοποιηθεί για τη διαφοροποίηση μεταξύ υπερφορτωμένων συναρτήσεων.

Για παράδειγμα οι ακόλουθοι ορισμοί συναρτήσεων στη C++ θα οδηγήσουν σε αδυναμία μεταγλώττισης.

int fun(int x) {
  return 0;
}

float fun(int x) {
  return 0.0;
}
overload_error.cpp
#include <iostream>

int fun(int x)
{
    // dummy
    return 0;
}

float fun(int x) //  error: ambiguating new declaration of 'float fun(int)'
{
    // dummy
    return 0.0;
}

int main() {}

Επιπλέον, υπερφορτωμένα υποπρογράμματα με προεπιλεγμένες παραμέτρους μπορεί να οδηγήσουν σε ασάφεια για το ποιο υποπρόγραμμα καλείται. Δείτε για παράδειγμα τον ακόλουθο κώδικα σε C++.

void fun(float b = 0.0)
{
    cout << "Calling function fun(float)" << endl;
}

void fun()
{
    cout << "Calling function fun()" << endl;
}
overload_problem.cpp
#include <iostream>

using namespace std;

void fun(float b = 0.0)
{
    cout << "Calling function fun(float)" << endl;
}

void fun()
{
    cout << "Calling function fun()" << endl;
}

int main()
{
    fun(1.5); // ok
    // fun(); // error: call of overloaded 'fun()' is ambiguous
}

Η κλήση fun(); είναι ασαφής (ambiguous) και προκαλεί σφάλμα μεταγλώττισης.

Η C δεν υποστηρίζει υπερφόρτωση συναρτήσεων. Δείτε για παράδειγμα:

no_overload_support.c
// Η C δεν υποστηρίζει υπερφόρτωση συναρτήσεων

#include <stdio.h>

int fun(int x) {
  // dummy
  return 0;
}

int fun(char x) { // error: conflicting types for 'fun'
  // dummy
  return 0;
}

int main() {}

9.10 Γενικά υποπρογράμματα

Ένα πολυμορφικό υποπρόγραμμα δέχεται παραμέτρους διαφορετικών τύπων σε διαφορετικές ενεργοποιήσεις. Ο παραμετρικός πολυμορφισμός αφορά υποπρογράμματα που δέχονται γενικές παραμέτρους και τα υποπρογράμματα αυτά συχνά αποκαλούνται γενικά ή γενερικά.

9.10.1 Γενικές συναρτήσεις στη C++

Οι γενικές συναρτήσεις στη C++ ονομάζονται πρότυπες (templates) συναρτήσεις.

Παράδειγμα σύγκρισης γενικής συνάρτησης με μακροεντολή.

template1.cpp
#include <iostream>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

template <class T>
T max(T first, T second)
{
    return first > second ? first : second;
}

void test1()
{
    std::cout << "Using template" << std::endl;
    int a = 2, b = 1, c;
    char d = 'B', e = 'A', f;

    c = max(a, b);
    f = max(d, e);

    std::cout << a << " " << b << " " << c << std::endl;
    std::cout << d << " " << e << " " << f << std::endl;
}

void test2()
{
    std::cout << "Using macro" << std::endl;
    int a = 2, b = 1, c;
    char d = 'B', e = 'A', f;

    c = MAX(a, b);
    f = MAX(d, e);

    std::cout << a << " " << b << " " << c << std::endl;
    std::cout << d << " " << e << " " << f << std::endl;
}

// η test3 και η test4 παρουσιάζουν διαφορετική συμπεριφορά λόγω side-effects
void test3()
{
    std::cout << "Using template" << std::endl;
    int a = 2, b = 1, c;
    char d = 'B', e = 'A', f;

    c = max(a++, b);
    f = max(d++, e);

    std::cout << a << " " << b << " " << c << std::endl;
    std::cout << d << " " << e << " " << f << std::endl;
}

// επειδή η τιμή του a είναι μεγαλύτερη από την τιμή του b, η τιμή του a, λόγω macro, αυξάνεται 2 φορές
void test4()
{
    std::cout << "Using macro" << std::endl;
    int a = 2, b = 1, c;
    char d = 'B', e = 'A', f;

    c = MAX(a++, b);
    f = MAX(d++, e);

    std::cout << a << " " << b << " " << c << std::endl;
    std::cout << d << " " << e << " " << f << std::endl;
}

int main()
{
    test1();
    test2();
    test3();
    test4();
}

/*
Using template
2 1 2
B A B
Using macro
2 1 2
B A B
Using template
3 1 2
C A B
Using macro
4 1 3
D A C
*/

Μια γενική συνάρτηση ταξινόμησης.

generic_sort.cpp
#include <iostream>

using namespace std;

template <class Type>
void generic_sort(Type list[], int len)
{
    int top, bottom;
    Type temp;
    for (top = 0; top < len - 2; top++)
        for (bottom = top + 1; bottom < len - 1; bottom++)
            if (list[top] > list[bottom])
            {
                temp = list[top];
                list[top] = list[bottom];
                list[bottom] = temp;
            }
}

int main()
{
    int a[] = {3, 2, 1, 4, 5};
    generic_sort(a, 5);
    for (int i = 0; i < 5; i++)
    {
        cout << a[i] << " ";
    }
    cout << endl;

    int b[] = {'b', 'e', 'c', 'a', 'd', 'f'};
    generic_sort(b, 6);
    for (int i = 0; i < 6; i++)
    {
        cout << (char)b[i] << " ";
    }
    cout << endl;
}

/*
1 2 3 4 5 
a b c d e f
*/

9.10.2 Γενικές μέθοδοι στη Java 5.0

H Java ξεκίνησε να υποστηρίζει γενικούς τύπους και μεθόδους στην έκδοση 5.0. Η δε υλοποίηση στη Java διαφέρει από την υλοποίηση της C++. Σημαντικές διαφορές είναι οι ακόλουθες: 1. Στη Java οι γενικές παράμετροι πρέπει να είναι κλάσεις και όχι βασικοί τύποι. 2. Στη Java κατασκευάζεται ένα μόνο αντίγραφο του κώδικα (raw κώδικας) ακόμα και αν η γενική μέθοδος καλείται με διαφορετικούς τύπους δεδομένων (στη C++ δημιουργείται, κατά τη μεταγλώττιση, ξεχωριστό αντίγραφο του κώδικα για κάθε κλήση του υποπρογράμματος που χρησιμοποιεί διαφορετικούς τύπους). 3. Στη Java μπορούν να οριστούν περιορισμοί (bounds) για το εύρος των κλάσεων που μπορούν να μεταβιβαστούν στη γενική μέθοδο ως γενικές παράμετροι. 4. H Java υποστηρίζει τύπους μπαλαντέρ, π.χ. Collection<?> σημαίνει οποιοσδήποτε τύπος συλλογής με περιεχόμενα αντικείμενα οποιασδήποτε κλάσης (collection of unknown).

Παράδειγμα 1 με generics στη Java

generics1.Example.java
public class Example {

    public static <T> void doIt(T[] list) {
        for (T elem : list) {
            System.out.println(elem);
        }
    }

    public static void main(String[] args) {
        Integer a[] = new Integer[] { 1, 2, 3, 4, 5 };
        doIt(a);

        System.out.println();

        String b[] = new String[] { "Arta", "Ioannina", "Preveza", "Igoumenitsa" };
        doIt(b);

        System.out.println();

        MyClass c[] = new MyClass[] { new MyClass(1), new MyClass(2), new MyClass(3) };
        doIt(c);
    }
}

class MyClass {
    private int x;

    public MyClass(int x) {
        this.x = x;
    }

    public String toString() {
        return String.format("Object:%d value: %d", this.hashCode(), x);
    }

}

// 1
// 2
// 3
// 4
// 5

// Arta
// Ioannina
// Preveza
// Igoumenitsa

// Object:617901222 value: 1
// Object:1149319664 value: 2
// Object:2093631819 value: 3
generics2.ExampleBounds.java
import java.util.Arrays;   
import java.util.Collections;

public class ExampleBounds {

    public static <T extends Comparable> void doIt(T[] list) {
        Arrays.sort(list, Collections.reverseOrder());
        for (T elem : list) {
            System.out.println(elem);
        }
    }

    public static void main(String[] args) {
        Integer a[] = new Integer[] { 1, 2, 3, 4, 5 };
        doIt(a);

        System.out.println();

        String b[] = new String[] { "Arta", "Ioannina", "Preveza", "Igoumenitsa" };
        doIt(b);

        System.out.println();

        // MyClass c[] = new MyClass[] { new MyClass(1), new MyClass(2), new MyClass(3) };
        // doIt(c); // error: method doIt in class Example cannot be applied to given types;
    }
}

class MyClass {
    private int x;

    public MyClass(int x) {
        this.x = x;
    }

    public String toString() {
        return String.format("Object:%d value: %d", this.hashCode(), x);
    }

}

// 1
// 2
// 3
// 4
// 5

// Arta
// Ioannina
// Preveza
// Igoumenitsa
generics3.ExampleWildcards.java
import java.util.Collection;
import java.util.ArrayList;

public class ExampleWildcards {

    static void printCollection(Collection<?> c) {
        for (Object e : c) {
            System.out.println(e);
        }
    }

    public static void main(String[] args) {
        Collection<String> c = new ArrayList<String>();
        c.add("Arta");
        c.add("Ioannina");
        c.add("Preveza");
        c.add("Igoumenitsa");

        printCollection(c);
    }

}

// Arta
// Ioannina
// Preveza
// Igoumenitsa

9.10.3 Γενικές μέθοδοι στη C# 2005

Οι γενικές μέθοδοι στη C# είναι παρόμοιες με τις γενικές μεθόδους στη Java, αλλά δεν υποστηρίζονται τύποι μπαλαντέρ.

9.10.4 Γενικές συναρτήσεις στη F

H συναρτησιακή γλώσσα F# υποστηρίζει γενικές συναρτήσεις, αλλά είναι λιγότερο χρήσιμες από τις γενικές συναρτήσεις των C++, Java και C#.

9.11 Υπερφορτωμένοι τελεστές που ορίζει ο χρήστης

Υπερφόρτωση τελεστών υποστηρίζουν οι γλώσσες: Ada, C++, Python, Ruby.

Παράδειγμα υπερφόρτωσης τελεστών στην Python. Υλοποίηση πράξεων +, -, * για intervals. Δείτε την περιγραφή των πράξεων με διαστήματα στο https://en.wikipedia.org/wiki/Interval_arithmetic.

class Interval:
    def __init__(self, start, end):
        self.start = start
        self.end = end

    def __str__(self):
        return f'({self.start},{self.end})'

    def __add__(self, other):
        return Interval(self.start + other.start, self.end + other.end)

    def __sub__(self, other):
        return Interval(self.start - other.end, self.end - other.start)

    def __mul__(self, other):
        minimum = min([self.start * other.start, self.start * other.end, self.end * other.start, self.end*other.end])
        maximum = max([self.start * other.start, self.start * other.end, self.end * other.start, self.end*other.end])
        return Interval(minimum, maximum)

i1 = Interval(1,5)
i2 = Interval(4,6)
print(i1+i2)
print(i1-i2)
print(i2-i1)
print(i1*i2)

θα εμφανίσει:

(5,11)
(-5,1)
(-1,5)
(4,30)

9.12 Κλειστότητες

Κλειστότητα (closure) είναι ένα υποπρόγραμμα και το περιβάλλον αναφοράς στο οποίο ορίζεται. Οι κλειστότητες είναι χρήσιμες σε γλώσσες που επιτρέπουν ένθετα υποπρογράμματα, έχουν στατική εμβέλεια και επιτρέπουν την επιστροφή υποπρογραμμάτων από συναρτήσεις και την εκχώρησή τους σε μεταβλητές.

Οι κλειστότητες υποστηρίζονται από τις γλώσσες συναρτησιακού προγραμματισμού, από τις περισσότερες γλώσσες σεναρίων και από την C# (προστακτική γλώσσα).

Παράδειγμα κλειστότητας σε JavaScript

sebesta_9_12.js
1
2
3
4
5
6
7
8
9
function makeAdder(x) {
    return function(y) {return x + y;}
}

var add10 = makeAdder(10);
var add5 = makeAdder(5);

console.log("Add 10 to 20: " + add10(20));
console.log("Add 5 to 20: " + add5(20));

Σε αυτό το παράδειγμα η κλειστότητα είναι η ανώνυμη συνάρτηση που ορίζεται μέσα στη συνάρτηση makeAdder. Η μεταβλητή x που αναφέρεται μέσα στη συνάρτηση κλειστότητας προσδένεται με την παράμετρο που στέλνεται κάθε φορά στη makeAdder. Στο παράδειγμα, η συνάρτηση makeAdder καλείται δύο φορές, μια με παράμετρο 10 και μια με παράμετρο 5. Κάθε κλήση επιστρέφει διαφορετική εκδοχή της κλειστότητας.

θα εμφανίσει:

Add 10 to 20: 30
Add 5 to 20: 25

Παραδείγματα κλειστοτήτων με την Python

closure1.py
def outer_func():
    message = "Hi"

    def inner_function():
        print(message)  # free variable

    return inner_function()


outer_func()
$ python closure1.py
Hi
closure2.py
def outer_func():
    message = "Hi"

    def inner_function():
        print(message)  # free variable

    return inner_function  # αλλαγή σε σχέση με το closure1.py, επιστρέφει το όνομα της εμφωλευμένης συνάρτησης


my_func = outer_func()
my_func()
$ python closure2.py
Hi
closure3.py
def outer_func(msg):
    message = msg

    def inner_function():
        print(message)  # free variable

    return inner_function


hi_func = outer_func("Hi")
hello_func = outer_func("hello")

hi_func()
hello_func()
$ python closure3.py
Hi
hello
closure4.py
import logging

logging.basicConfig(level=logging.INFO)


def logger(func):
    def log_func(*args):
        logging.info(f'Running "{func.__name__}" with arguments {args}')
        print(func(*args))

    return log_func


def add(x, y):
    return x + y


def sub(x, y):
    return x - y


add_logger = logger(add)
sub_logger = logger(sub)

add_logger(3, 3)
sub_logger(3, 3)
$ python closure4.py
INFO:root:Running "add" with arguments (3, 3)
6
INFO:root:Running "sub" with arguments (3, 3)
0

9.13 Συρρουτίνες

Μια συρρουτίνα (coroutine) είναι ένα ειδικό υποπρόγραμμα που έχει πολλαπλές εισόδους.

Δείτε το Coroutines As Threads.