Programowanie w języku C++

Transkrypt

Programowanie w języku C++
Katedra Informatyki Stosowanej
Politechniki Łódzkiej
mgr inż. Tomasz Jaworski
Programowanie
w języku C++
Al. Politechniki 11, 90-924 Łódź
ul. Stefanowskiego 18/22, 90-924 Łódź
http://tjaworski.kis.p.lodz.pl
e-mail: [email protected]
Programowanie w języku C++
2
1. Od programowania proceduralnego do obiektowego
Konstruując program zawsze napotkamy problem definiowania przedmiotów, których opisem
program się ten zajmuje. Przez przedmiot należy tu rozumieć zarówno zjawiska, jak i
przedmioty rzeczywiste opisywane przez program oraz struktury danych wykorzystywane do
tego opisu. W odniesieniu do sposobu definiowania przedmiotów wykształciły się dwa
zasadnicze podejścia:
proceduralne:
Tworzymy strukturę zawierającą parametry definiujące przedmiot i jego stan, a odseparowane
funkcje określają jego właściwości. Rozpatrzmy prosty przykład: w celu zdefiniowania
punktu na ekranie wystarczy utworzyć następującą strukturę:
struct punkt
{
int x, y;
};
Wyświetlenie punktu na ekranie umożliwi następująca funkcja:
void Narysuj(struct punkt P)
{
//ciało funkcji
}
obiektowe:
Dane definiujące przedmiot i metody określające jego właściwości umieszczamy we
wspólnym miejscu – klasie. W ten sposób następuje integracja danych i metod uprawnionych
do ich wykorzystania. Rozpatrzmy ten sam problem, co w przypadku podejścia
proceduralnego.
class PUNKT
{
int x, y;
public:
void Narysuj(void);
};
Zdefiniowana powyżej klasa PUNKT zawiera zarówno dane definiujące punkt na ekranie, jak
I funkcję pozwalającą na jego wyświetlenie. Definicja przedmiotu i jego właściwości znalazła
się w ten sposób w jednym miejscu.
Programowanie obiektowe umożliwia:
tworzenie programowych modeli przedmiotów poprzez łączenie danych i metod
uprawnionych do ich wykorzystania,
wspólny opis całych klas przedmiotów , zjawisk i problemów.
Programowanie w języku C++
3
2. Definiowanie przedmiotów – klasy i obiekty
Podstawowym pojęciem języka C++ jest klasa. Dla jej opisu konieczne są dwa zasadnicze
elementy:
dane charakteryzujące przedmiot,
funkcje określające jego właściwości.
Jak łatwo się domyśleć elementy te muszą znaleźć się w definicji klasy. Definicja klasy
rozpoczyna się od tak zwanego słowa kluczowego klasy, którym może być jedno ze słów –
class, struct, union. Najczęściej wykorzystywane jest słowo kluczowe class.
W ciele definicji klasy mogą znaleźć się:
deklaracje danych składowych
deklaracje i definicje funkcji składowych
Wymienione powyżej elementy rozmieszcza się wekcjach definicji klasy, co ilustruje
poniższy schemat:
class Klasa
{
public:
...
//funkcje i dane składowe publiczne
protected:
...
//funkcje i dane składowe zabezpieczone
private:
...
//funkce i dane składowe prywatne
};
Specyfikatory atrybutów dostępu mogą być używane wielokrotnie w obrębie definicji klasy.
Umieszczając poszczególne elementy definicji klasy w sekcjach określamy, które funkcje
programu mają prawo się do nich odwołać. Reguły dostępności mają się następująco:
Sekcja prywatna (private)
Składowe prywatne widoczne są tylko w obrębie funkcji składowych danej klasy. Jeśli klasa
jest definiowana ze słowem kluczowym class, składowe są domyślnie prywatne.
Dane składowe definiujemy jako prywatne zwykle w jednej z następujących sytuacji:
zmiana wartości składowej przez niepowołaną do tego funkcję zewnętrzną jest ryzykowna lub
zmianie tej powinno towarzyszyć wykonanie ściśle określonych czynności. W takiej sytuacji
powinna istnieć specjalna funkcja umożliwiająca takie przypisanie, realizująca jednocześnie
te operacje,
składowa jest przeznaczona tylko do wewnętrznego wykorzystania przez funkcje składowe
klasy.
Funkcje definiujemy jako prywatne, gdy:
ich wywołanie przez funkcje zewnętrzne jest niewskazane,
gdy realizują pewien fragment algorytmu i ich wywołanie z zewnątrz nie ma sensu.
Sekcja zabezpieczona (protected)
Składowe zabezpieczone są widoczne tylko w obrębie funkcji składowych danej klasy i klas
wyprowadzonych. Składowe definiujemy jako zabezpieczone, gdy ich użycie nie jest
obarczone tak silnymi restrykcjami, jak ma to miejsce w przypadku składowych prywatnych.
Funkcje określamy jako zabezpieczone , jeśli dostęp do nich z zewnątrz nie jest wymagany,
zaś konieczny jest dostęp do analogicznych funkcji składowych odziedziczonych po
przodkach.
Sekcja publiczna (public)
Programowanie w języku C++
4
Składowe publiczne widoczne są w obszarze całego pliku. Publiczne funkcje składowe służą
zwykle do sterowania, zmiany parametrów i komunikacji z obiektem. Dane składowe
definiuje się jako publiczne w przypadku prostych klas, gdy zmiana wartości pola nie musi
być związana z wykonaniem jakichś dodatkowych czynności.
!!! DEFINICJA KLASY POWINNA KOŃCZYĆ SIĘ ŚREDNIKIEM !!!
Sposób definiowania funkcji zadeklarowanych, lecz nie zdefiniowanych w ciele klasy
rozpatrzymy na przykładzie funkcji Rysuj klasy PUNKT.
void PUNKT::Rysuj(void)
{
//ciało funkcji
}
Jako nazwę funkcji podajemy nazwę klasy, po której następuje tzw. kwalifikator zakresu (::),
a następnie nazwę definiowanej funkcji składowej.
3. Konstruktory i destruktory
Idea i budowa konstruktorów
Okazuje się, że oprócz deklarowanych funkcji składowych istnieją jeszcze dwie specjalne
funkcji, które są albo definiowane przez programistę, albo generowane przez kompilator (jeśli
nie zostały wcześniej zdefiniowane). Są to konstruktory i destruktory.
Konstruktor jest specjalną, najczęściej przeciążoną funkcją składową wywoływaną niejawnie
zawsze wtedy, gdy zachodzi konieczność utworzenia obiektu danej klasy. Deklarowanie i
definiowanie konstruktorów podlega niemal tym samym zasadom, co każda zwyczajna
funkcja składowa z tym, że:
W deklaracji konstruktora nie wolno określać typu zwracanej wartości. Niedozwolony jest
nawet specyfikator void,
Konstruktory nie mogą być wywoływane tak jak inne funkcje, gdyż są wywoływane
niejawnie w czasie tworzenia obiektu, a wybór odpowiedniego konstruktora jest dokonywany
na podstawie typów parametrów przekazanych w czasie tworzenia obiektu,
Konstruktory nie są dziedziczone,
Identyfikator konstruktora musi być identyczny z identyfikatorem klasy,
Nie można pobrać adresu konstruktora,
Konstruktor nie może być wirtualny.
Dwa typy konstruktorów pełnią szczególną rolę: tzw. konstruktor domyślny, czyli
bezparametrowy i konstruktor kopiujący. Jeżeli w klasie nie zdefiniowano żadnego
konstruktora, to kompilator utworzy własny konstruktor bezparametrowy i kopiujący.
Konstruktor służy przede wszystkim do inicjacji danych składowych obiektu. Inicjacji tej
można dokonać w dwojaki sposób:
dokonując bezpośrednich przypisań w ciele definicji konstruktora,
bezpieczniejsze i zgodniejsze z duchem języka C++ jest jednak posłużenie się tzw. listą
inicjacyjną. Elementy listy inicjacyjnej przypominają sposób, w jaki inicjowane są obiekty.
Programowanie w języku C++
5
Przykład: Definicje konstruktorów
class PUNKT
{
private:
int x, y;
unsigned kolor;
public:
PUNKT(void) {x=0; y=0; kolor=0;}
//bezpośrednie
przypisania
w
ciele funkcji
PUNKT(int _x, int _y, unsigned _kolor = 0);
PUNKT(unsigned _kolor);
void UstalWsp(int _x, int _y);
void UstalKol(unsigned _kolor);
void Rysuj(void);
};
PUNKT::PUNKT(int _x, int _y, unsigned _kolor):
//lista inicjacyjna
x(_x), y(_y), kolor(_kolor)
{
}
PINKT::PUNKT(unsigned _kolor): x(0), y(0), kolor(_kolor)
{
}
Definicję klasy PUNKT uzupełniono o następujące konstruktory:
PUNKT(void); – konstruktor domyślny, bezparametrowy,
PUNKT(int _x, int _y, unsigned _kolor=0); - konstruktor o trzech parametrach całkowitych,
z możliwością przyjęcia domyślnej wartości (zero) ostatniego,
PUNKT(unsigned _kolor); - konstruktor z jednym parametrem całkowitym oznaczającym
kolor punktu.
Konstruktor, zgodnie z nazwą, jest wywoływany podczas tworzenia obiektu danej klasy.
Definicja obiektu klasy zawierającej konstruktory może być połączona z jego inicjacją.
PUNKT
P1,
P2(1, 1),
//konstruktor bezparametrowy
//konstruktor trójparametrowy, trzeci parametr domyślny
P3(10, 10, 5), //konstruktor trójparametrowy
P4(1),
//konstruktor jednoparametrowy
tab[10];
//konstruktor bezparametrowy (10x)
Jeżeli klasa zawiera jakikolwiek konstruktor zdefiniowany przez programistę, wówczas
kompilator nie wygeneruje automatycznie nawet konstruktora domyślnego. Dobrą zasadą
zatem jest, że gdy klasa zawiera jakiś konstruktor, należy również zdefiniować ten
bezparametrowy. Należy unikać niejednoznaczności, np.:
Programowanie w języku C++
6
class X
{
X(void);
X(int j=2);
};
X obj;
Jeśli spróbujemy utworzyć obiekt obj klasy X w powyższy sposób, kompilator nie będzie
wiedział, który z konstruktorów klasy X ma zostać użyty.
Konstruktory kopiujące. Powielanie obiektów
Konstruktorem kopiującym klasy X nazywamy konstruktor o jednym parametrze typu
referencja klasy X – np.:
X(const &X);
X(const &X, int I = 0);
Konstruktor kopiujący jest wywoływany zawsze, gdy zachodzi konieczność skopiowania
obiektu danej klasy do innego obiektu tejże klasy choćby podczas inicjacji obiektu innym
obiektem, np.:
X x = y;
Jeśli konstruktor kopiujący nie został zdefiniowany jawnie, kompilator wygeneruje własny.
Konstruktor kopiujący definiujemy samodzielnie wówczas, gdy podczas kopiowania
obiektów zachodzi konieczność wykonania jakiejś dodatkowej operacji, np. skopiowania
związanych z danym obiektem dynamicznych struktur danych. W tym przypadku brak
kopiowania danych mogło by spowodować, że dynamiczne obiekty, o których mowa,
operowały by na tym samym obszarze pamięci, co nie zawsze jest wymagane.
Destruktory
Zawsze przed usunięciem obiektu z pamięci wywoływana jest niejawnie specjalna funkcja
składowa zwana destruktorem. Jeśli nie została ona zadeklarowana jawnie w obrębie definicji
klasy, kompilator wygeneruje własny destruktor. Destruktor jest funkcją o następujących
własnościach:
nazwą destruktora jest nazwa klasy poprzedzona znakiem tyldy (~),
nie wolno określać typu zwracanego przez destruktor, nie jest dozwolony nawet specyfikator
void,
destruktor może być wywoływany przy użyciu pełnej kwalifikowanej jego nazwy. W celu
wywołania destruktora klasy X należy użyć formuły:
X::~X();
Destruktor służy do wykonania niezbędnych operacji przed usunięciem obiektu z pamięci.
Operacje te mogą być różnorodne: usunięcie z ekranu figury, zwolnienie pamięci dla
dynamicznych struktur danych itp.
Programowanie w języku C++
7
4. Zmienna this
Weźmy pod uwagę definicję klasy PUNKT (nieco uproszczoną):
class PUNKT
{
int x, y;
public:
void UstalWsp(int i, int j) { x = I; y = j;}
} P, Q;
oraz obiekty P i Q. Zwróćmy uwagę, że wywołania
P.UstalWsp(1, 10);
Q.UstalWsp(2, 13);
nadają wartości składowym odpowiednio obiektom P i Q. Jak widać ta sama funkcja nadaje
wartości składowym różnych obiektów. Rodzi się więc pytanie, w jaki sposób odwołania do
składowych są wiązane z konkretnymi obiektami.
Okazuje się, że wszystkie niestatyczne funkcje składowe otrzymują niejawny parametr this,
który wskazuje obiekt, na rzecz którego zostały wywołane. W zwiąsku z tym ciało funkcji
UstalWsp w zasadzie wygląda nastepujaco:
void PUNKT::UstalWsp(int i, int j)
{
this->x = i;
this->y = j;
}
Normalnie operacja this-> jest wykonywana niejawnie. Wskaźnik this ma różne
zastosowania. Można go wykorzystywać do zwracania referencji obiektu, dla którego została
wywołana dana funkcja składowa, np.:
class PUNKT
{
public:
PUNKT& UstalWsp(int i, int j);
};
PUNKT& PUNKT::UstalWsp(int i, int j)
{
x = i;
y = j;
return *this; //zwróć referencję do danego obiektu
}
Programowanie w języku C++
8
5. Funkcje i klasy zaprzyjaźnione
W niektórych sytuacjach może zachodzić konieczność postępowania niezgodnego z regułami
określonymi atrybutami dostępu lub postępowanie takie może znacznie uprościć zapis
algorytmu, powodując wygenerowanie bardziej efektywnego kodu wynikowego. Aby
zezwolić funkcji zdefiniowanej poza klasą na dostęp do jej składowych prywatnych i
zabezpieczonych, trzeba określić relacją przyjaźni między funkcją a tą klasą. Funkcja
zaprzyjaźniona (ang. friend) z klasą, mimo że nie jest składową tej klasy, posiada pełne prawa
dostępu do wszystkich składowych tej klasy. Relację przyjaźni między funkcją a klasą
ustanawia się poprzez umieszczenie deklaracji tej funkcji poprzedzonej słowem kluczowym
friend w ciele definicji klasy.
class STRING
{
private:
char *Str;
friend void Drukuj(STRING &);
public:
//definicje konstruktorów i destruktora
};
void Drukuj(STRING &S)
{
cout << “\n” << S.Str;
}
main()
{
STRING Imie(“Radosław”);
Drukuj(Imie);
return 0;
}
Dzięki ustaleniu relacji przyjaźni pomiędzy klasą STRING a funkcją Drukuj dozwolony jest
w jej ciele dostęp do składowych prywatnych tej klasy.
Relację przyjaźni można ustalić również pomiędzy dwiema klasami. Wszystkie funkcje
składowe klasy zaprzyjaźnionej z daną mają prawo dostępu do wszystkich komponentów tej
klasy, z którą są zaprzyjaźnione. Należy pamiętać, że relacja przyjaźni nie ma odwrotności.
Programowanie w języku C++
9
class STRING
{
private:
char *Str;
public:
//definicje konstruktorów i destruktora
friend class STR_STREAM;
};
class STR_STREAM
{
public:
void Out(STRING &S)
{
cout << “\n” << S.Str;
}
};
main()
{
STRING S(“Ala ma kota”);
STR_STREAM Str;
Str.out(S);
return 0;
}
Deklaracje klas i funkcji zaprzyjaźnionych mogą znaleźć się w dowolnej sekcji definicji klasy
– nie ma to żadnego wpływu na relację przyjaźni.
Programowanie w języku C++
10
6. Funkcje operatorowe
Język C++ oprócz możliwości tworzenia różnych obiektów pozwala na definiowanie
operatorów wykonujących działania na tych obiektach (klasach). Dzięki temu dodawanie
wektorów czy łączenie łańcuchów znakowych może zyskać naturalny wygląd (np. jak każda
inna operacja matematyczna). Załóżmy, że obiekty klasy WEKTOR reprezentują wektory na
płaszczyźnie. Przedstawiona poniżej funkcja main wykonuje działania na obiektach tej klasy.
int main(void)
{
WEKTOR U(1,5), V(2,3), X(-2,1), Z(10,-3);
X = U + V;
//dodawanie wektorów
Z = X – 5 * V;
//odejmowanie wektorów i mnożenie wektora przez
liczbę
return 0;
}
Normalnie próba wykonania takich operacji na obiektach zakończyła by się komunikatem o
błędzie. Aby tego uniknąć, należy zdefiniować takie operatory.
Operatory standardowe i przeciążone
Język C++ zawiera pewien, dość obszerny zasób operatorów umożliwiających wykonanie
operacji na danych typów całkowitych, rzeczywistych oraz operatory indeksowania,
wywołania funkcji, dostępu do składowych oraz dynamicznego przydziału pamięci i in.
Większość wymienionych operatorów może zostać zdefiniowana dla operandów innych
typów. Operację taką nazywamy przeciążaniem operatorów, podobnie jak zdefiniowanie
funkcji o tej samym identyfikatorze, ale innych parametrach wejściowych. Spośród całego
zbioru operatorów tylko cztery, podobnie jak symbole preprocesora (#, ##), nie podlegają
przeciążaniu:
.
.*
::
?:
Przeciążanie operatorów polega na zdefiniowaniu tzw. funkcji operatorowej. Identyfikatorem
funkcji operatorowej jest zawsze słowo kluczowe operator, bezpośrednio po którym
wymieniony jest symbol operatora, np.:
operator+, operator-, operator<<
Funkcja operatora może być:
- niestatyczną funkcją składową klasy, na obiektach których działa operator,
- funkcją nie będącą składową klasy – najczęściej zaprzyjaźnioną,
- statyczną funkcją składową klasy.
O tym, z którym z wymienionych powyżej przypadków mamy do czynienia, decyduje rodzaj
przeciążanego operatora.
Operatory jednoargumentowe
Operator jednoargumentowy (binarny) można zdefiniować jako:
- niestatyczną, bezparametrową funkcję składową klasy,
- funkcję o jednym argumencie, nie będącą składową klasy.
Stąd wynika, że poniższe wywołanie operatora jednoargumentowego ++:
++X; lub X++;
Programowanie w języku C++
11
może być traktowane jako:
X.operator++() lub operator++(X);,
gdzie X to klasa, której funkcją składową jest operator++ lub na której on działa.
Jak widać istnieje możliwość zdefiniowania rodzaju operatora jako przed- lub przyrostkowy.
Operator przedrostkowy definiujemy tak, jak to przedstawiono dotychczas, przyrostkowy zaś,
deklarując go, jako niestatyczną funkcję składową o jednym parametrze o jednym parametrze
typu int lub jako funkcję o jednym parametrze danej klasy i dodatkowym parametrze typu int.
Parametr ten stanowi dodatkową informację, pozwalającą kompilatorowi na rozróżnienie
wersji przed- i przyrostkowej operatora.
Zmianie podlega również fakt, iż w przypadku standardowych przyrostkowych operatorów
in- i dekrementacji, kiedy to żądana operacja stosowana była dopiero po obliczeniach, dla
operatorów przeciążanych jest ona stosowana natychmiast.
Przykład.
class INTEGER
{
int val;
public:
INTEGER(int v=0): val(v) {}
INTEGER &operator++() {val++; return *this;} //przedrostkowy
friend INTEGER &operator++(INTEGER &, int);
//przyrostkowy
void Out(void) {cout << “\n” << val;}
};
INTEGER &operator++(INTEGER &a, int)
{
a.val+=2;
return a;
}
main()
{
INTEGER a(1), b(2);
++a;
b++;
//użycie operatora przedrostkowego
//użycie operatora przyrostkowego
a.Out();
b.Out();
}
Operatory dwuargumentowe
Operatory dwuargumentowe należą do najczęściej przeciążanych i można je definiować jako:
- niestatyczną funkcję składową – wówczas działanie X @ Y, gdzie @ - operator
dwuargumentowy – traktujemy jako: X.operator@(Y),
- dwuparametrową funkcję nie będącą składową klasy, na której operacje będą wykonywane –
działanie X @ Y interpretujemy wówczas jako operator@(X, Y).
Programowanie w języku C++
Obie metody definiowania operatorów dwuargumentowych są tożsamościowe pod
warunkiem odpowiedniego ich użycia, co pokazano w poniższym przykładzie.
Przykład.
class WEKTOR
{
public:
WEKTOR(void): X(0), Y(0) {};
WEKTOR(double X, double Y): X(_X), Y(_Y) {}
WEKTOR operator+(WEKTOR&);
friend WEKTOR operator-(WEKTOR&, WEKTOR&);
friend WEKTOR operator*(double, WEKTOR&);
friend ostream &operator<<(ostream&, WEKTOR&);
private:
double X, Y;
};
WEKTOR WEKTOR::operator+(WEKTOR &U)
{
return WEKTOR(this->X + U.X, this->Y + U.Y);
}
WEKTOR operator-(WEKTOR &U, WEKTOR &V)
{
return WEKTOR(U.X + V.X, U.Y + V.Y);
}
WEKTOR operator*(double k, WEKTOR &U)
{
return WEKTOR(k * U.X, k * U.Y);
}
ostream &operator<<(ostream &St, WEKTOR &U)
{
St << ‘[‘ << U.X << ‘,’ << U.Y << ‘]’;
return St;
}
main()
{
WEKTOR A(1, 1), B(5, 5), C(-3, 3);
12
Programowanie w języku C++
13
cout << “\n” << A + B << “\n” << A – B << “\n” << 2*C + A;
return 0;
}
W powyższym przykładzie zdefiniowano klasę WEKTOR, reprezentującą wektor o
współrzędnych rzeczywistych (double), a następnie zdefiniowano operatory
dwuargumentowe umożliwiające dodawanie, odejmowanie oraz mnożenie wektora przez
liczbę. Operator dodawania + zdefiniowano jako niestatyczną funkcję składową, zaś
operatory odejmowania i mnożenia jako funkcje zaprzyjaźnione z klasą WEKTOR.
Przedstawiony sposób definiowania operatorów dwuargumentowych może być traktowany
jako wzorzec postępowania przy takiej okazji. Zdefiniowanie operatora << umożliwia zapis
wektora w strumieniu wyjściowym (tu ekran).
W wyniku wykonania programu zostaną wyprowadzone następujące napisy:
[6, 6]
[-4 –4]
[-5, 7]
Istnieje możliwość definiowania operatorów dwuargumentowych na dwa sposoby – tak jak to
przedstawiono powyżej. Wszelkie niejednoznaczności są rozstrzygane przez standardowe
dopasowanie argumentów.
Operatory dwuargumentowe zdefiniowane jako niestatyczne funkcje składowe klasy
podlegają dziedziczeniu przy wyprowadzaniu klas potomnych z danej.
Operatory przypisania
Każdy z operatorów przypisania:
= *=
/=
%=
+=
-=
<<= >>= &=
^=
|=
może zostać zdefiniowany wyłącznie jako niestatyczna funkcja składowa. Operatory
przypisania nie są dziedziczone przez klasy wyprowadzone z tej, dla której zostały
zdefiniowane.
W poniższej definicji klasy wykorzystano funkcje składowe z przykładu poprzedniego.
Dodano definicję operatorów przypisania: +=, -=, *=;
class WEKTOR
{
public:
WEKTOR(void): X(0), Y(0) {};
WEKTOR(double X, double Y): X(_X), Y(_Y) {}
WEKTOR operator+(WEKTOR&);
friend WEKTOR operator-(WEKTOR&, WEKTOR&);
friend WEKTOR operator*(double, WEKTOR&);
friend ostream &operator<<(ostream&, WEKTOR&);
WEKTOR &operator+=(WEKTOR&);
WEKTOR &operator-=(WEKTOR&);
WEKTOR &operator*=(double);
private:
double X, Y;
};
Programowanie w języku C++
WEKTOR &WEKTOR::operator+=(WEKTOR &U)
{
this->X += U.X;
this->Y += U.Y;
return *this;
}
WEKTOR &WEKTOR::operator-=(WEKTOR &U)
{
this->X -= U.X;
this->Y -= U.Y;
return *this;
}
WEKTOR &WEKTOR::operator*=(double s)
{
this->X *= s;
this->Y *= s;
return *this;
}
main()
{
WEKTOR A(10, 10), B(-5, 4), C(0, 0);
cout << C << “\n”;
C += A + B;
cout << C << “\n”;
C *= -2;
cout << C << “\n”;
C -= A;
cout << C << “\n”;
return 0;
}
W efekcie wykonania programu zostaną wyprowadzona następujące rezultaty:
[0, 0]
[5, 14]
[-10, -28]
[-20, -38]
14