Sunday, October 8, 2017

Kuliah 6 C++: Fungsi Virtual dan Polimorfisme



BAB 6.
Fungsi Virtual dan Polimorfisme






6.1 Pengantar
Polimorfisme (polymorphism) merupakan kombinasi dari dua kata Yunani, yaitu poly dan morphism, yang berarti bahwa kemampuan untuk berubah bentuk sesuai dengan lingkungan. Anda telah mengenal dua jenis polimorfisme, yaitu pembebanan (overloading) fungsi dan pembebanan operator. Dalam pembebanan operator, operator yang sama melakukan beberapa bentuk operasi yang berbeda sesuai dengan tipe data yang disajikan kepadanya. Pada pembebanan fungsi, beberapa fungsi dengan nama sama tetapi memiliki jumlah atau tipe argumen yang berbeda. Jadi, nama yang sama dipanggil untuk melakukan sejumlah aksi yang berbeda. Template (lihat Bab 16) juga merupakan bentuk lain dari polimorfisme. Semua kasus ini merupakan contoh dari pengikatan statis karena pilihan fungsi yang sesuai diputuskan pada waktu kompilasi, yaitu sebelum eksekusi program dimulai.

Polimorfisme dimana di dalamnya pilihan fungsi dilakukan selama eksekusi program dinamakan dengan pengikatan dinamis. Ini dilakukan melalui penggunaan fungsi virtual dan pointer ke kelas basis. Semua ini diilustrasikan pada Gambar 6.1 berikut.


GAMBAR 6.1 Polimorfisme


6.2 Fungsi Virtual
Polimorfisme dengan pengikatan dinamis dilakukan melalui fungsi virtual. Tetapi aplikasi dari fungsi virtual dilakukan melalui pointer kelas basis. Alasan dalam penggunaan pointer kelas basis diilustrasikan pada Gambar 6.2a. Pointer kelas basis dapat dibuat baik untuk menunjuk objek dari kelas basis maupun untuk menunjuk objek dari kelas terderivasi. Tetapi, pointer kelas terderivasi hanya bisa menunjuk ke objek dari kelas terderivasi saja. Pointer itu tidak dapat menunjuk ke objek dari kelas basis karena objek dari kelas basis bukanlah objek dari kelas terderivasi. Jika kelas lain menderivasi dari sebuah kelas terderivasi, yaitu kelas Terderivasi2 dari kelas Terderivasi1 seperti ditunjukkan pada gambar berikut, maka kelas Terderivasi1 pada dasarnya merupakan kelas basis bagi kelas Terderivasi2. Pada kasus itu, pointer yang menunjuk kelas Terderivasi1 dapat menunjuk objek-objek dari kelas Terderivasi1 dan objek-objek dari kelas Terderivasi2. Tetapi pointer yang menunjuk kelas Terderivasi2 tidak dapat menunjuk objek-objek dari kelas Terderivasi1. Namun, pointer kelas Basis (lihat Gambar 6.2b) dapat menunjuk objek-objek dari kelas Basis bergitu pula menunjuk objek-objek dari kelas Terderivasi1 dan objek-objek dari kelas Terderivasi2. Jadi, dengan pointer kelas basis, Anda dapat mengakses objek-objek dari keseluruhan hierarki kelas di bawahnya.


GAMBAR 6.2 Pointer-pointer kelas dan jangkauannya masing-masing


Untuk lebih jelas tentang karakteristik-karakteristik dari fungsi virtual, berikut disajikan empat buah contoh. Pada program 6.1, sebuah kelas basis dengan nama B dan sebuah kelas terderivasi dengan nama D telah didefinisikan (class D: public B). Kedua kelas memiliki sebuah fungsi publik dengan nama Tampil(). Pada kelas basis, fungsi itu didefinisikan untuk menampilkan “Apakah Anda akan belajar C++?” dan pada kelas terderivasi, fungsi itu didefinisikan untuk menampilkan “Saya juga belajar C++”. Pointer bptr merupakan pointer yang menunjuk ke kelas B. Pointer ini diinisialisasi dengan &(B)b dimana b adalah sebuah objek dari kelas B. Ketika pointer ini dipakai untuk memanggil fungsi Tampil(), secara natural ia akan menampilkan “Apakah Anda akan belajar C++?”, yaitu definisi pada kelas basis.

Sekarang pointer itu ditugasi sebuah alamat baru &d dimana d adalah sebuah objek dari kelas terderivasi D. Objek d juga merupakan objek dari kelas basis B karena mekanisme pewarisan. Fungsi Tampil() dipanggil kembali dengan pointer. Fungsi itu adakan menampilkan “Apakah Anda akan belajar C++?” yang sama dan bukan definisi yang diberikan di dalam kelas D. Lihat program berikut untuk ilustrasi.


Program 6.1 Mengilustrasikan jika fungsi tidak dideklarasikan virtual, maka pointer kelas basis akan menunjuk  definisi fungsi pada kelas basis meskipun pointer itu ditugasi alamat objek dari kelas terderivasi

#include <iostream>
using namespace std;

class B
{
   public:
      void Tampil()
      {
         cout<<"Apakah Anda akan belajar C++?"<<endl;
      }
}; //akhir dari kelas B

class D : public B
{
   public :
      void Tampil()
      {
         cout<<"Saya juga belajar C++"<<endl;
      }
}; //akhir dari kelas D

int main()
{
   B b;              //b adalah sebuah objek dari kelas basis B
   D d;              //d adalah sebuah objek dari kelas terderivasi D

   B *bptr; //pointer ke kelas B

   bptr = &(B)b;     //inisialisasi pointer ke objek dari kelas B
   bptr -> Tampil(); //fungsi Tampil() dipanggil dengan pointer

   bptr = & d;             //pointer ditugasi alamat dari d.
   bptr -> Tampil(); //pointer dipakai untuk memanggil fungsi

   return 0;
}
KELUARAN
Apakah Anda akan belajar C++?
Apakah Anda akan belajar C++?



Dari keluaran program jelas terlihat bahwa meskipun pointer bptr dari kelas basis B ditugasi alamat dari objek d dari kelas terderivasi D, fungsi yang dipanggil melalui pointer itu adalah fungsi dari kelas basis.

Sekarang jika pointer didefinisikan untuk kelas D dan diinisialisasi dengan nilai &(D) d dan jika Anda mencoba menugasinya alamat dari objek kelas basis, misalkan, &b, maka kompiler akan menghasilkan error karena b bukanlah objek dari kelas D. Objek dari kelas terderivasi adalah juga objek dari kelas basis tetapi objek dari kelas basis bukanlah objek dari kelas terderivasi. Pada dasarnya, sebuah kelas basis tidak mengetahui eksistensi dari kelas-kelas yang mewarisinya. Ini diilustrasikan pada program berikut.

Program 6.2 Mengilustrasikan pointer dari kelas terderivasi tidak bisa menunjuk ke objek dari kelas basis

#include <iostream>
using namespace std;

class B
{
   public:
      void Tampil()
      {
         cout<<"Apakah Anda akan belajar C++?"<<endl;
      }
}; //akhir dari kelas B

class D : public B
{
   public :
      void Tampil()
      {
         cout<<"Saya juga belajar C++"<<endl;
      }
}; //akhir dari kelas D

int main()
{
   B b; //b dideklarasikan sebagai objek dari kelas B
   D d; //d dideklarasikan sebagai objek dari kelas D

   D *dptr = &(D)d;    //pointer ke kelas D
   dptr -> Tampil();

   dptr = & b;             //dptr ditugasi alamat dari kelas basis
                     //ini akan menghasilkan error
   dptr -> Tampil();

   return 0;
}
KELUARAN
cannot convert from 'class B *' to 'class D *'


Hasil di atas akibat dari objek dari kelas basis bukanlah objek dari kelas terderivasi. Oleh karena itu, pointer kelas basis tidak dapat memanggil fungsi dari kelas basis.

Program 6.3 menunjukkan bahwa bentuk sesuai dari fungsi Tampil() bisa diperoleh dengan mendefinisikan pointer-pointer terpisah untuk kelas B dan kelas D. Meskipun Anda mendapatkan fungsi-fungsi yang relevan, namun ini bukanlah yang diinginkan. Apa yang diinginkan di sini adalah untuk menunjuk objek dari kelas terderivasi menggunakan pointer dari kelas basis.

Program 6.3 Mengilustrasikan aksi dari pointer dari kelas basis dan pointer dari kelas terderivasi

#include <iostream>
using namespace std;

class B
{
   public:
      void Tampil()
      {
         cout<<"Apakah Anda akan belajar C++?"<<endl;
      }
}; //akhir dari kelas B

class D : public B
{
   public :
      void Tampil()
      {
         cout<<"Saya juga belajar C++"<<endl;
      }
}; //akhir dari kelas D

int main()
{
   B b; //b dideklarasikan sebagai objek dari kelas B
   D d; //d dideklarasikan sebagai objek dari kelas D

   B *bptr = & (B) b ;     //pointer ke kelas B
   D *dptr = &(D) d ;      // pointer ke kelas D

   bptr -> Tampil();
   dptr -> Tampil();

   return 0;
}
KELUARAN
Apakah Anda akan belajar C++?
Saya juga belajar C++



Keluaran tersebut didapatkan dengan mendefinisikan dua pointer yang berbeda, yaitu satu untuk kelas basis dan pointer lain untuk kelas terderivasi. Adalah natural untuk mendapatkan bentuk sesuai dari fungsi Tampil() dengan cara ini, tetapi ini bukanlah tujuan di sini. Tujuannya adalah untuk mendapatkan fungsi tertentu dari kelas terderivasi menggunakan pointer kelas basis. Ini dapat dilakukan hanya dengan mendeklarasikan fungsi Tampil() sebagai fungsi virtual di dalam kelas basis. Kata virtual merupakan katakunci di dalam C++. Pada kelas terderivasi, kata virtual bisa digunakan atau boleh juga tidak digunakan tetapi prototipe dari fungsi virtual di dalam kelas basis harus identik dengan prototipe fungsi pada kelas terderivasi. Pada program berikut, fungsi void Tampil() dideklarasikan virtual. Definisi dari fungsi Tampil() dimodifikasi menjadi berikut:

virtual void Tampil() {statemen_statemen; }

Sekarang, fungsi yang sama dipanggil di dalam kelas terderivasi melalui pointer kelas basis. Definisi yang diberikan di dalam kelas terderivasi mendefinisikan-ulang (override) definisi pada kelas basis.


Ini diilustrasikan pada program 6.4 berikut. Pada kelas terderivasi, fungsi memiliki nama sama dan tipe-tipe parameter yang sama dengan fungsi virtual pada kelas basis. Jika tipe atau jumlah parameter berubah, maka hal ini akan menjadi fungsi terbebani (overloaded) dan akan kehilangan properti dari fungsi virtual.

Program 6.4 Mengilustrasikan aplikasi dari fungsi virtual

#include <iostream>
using namespace std;

class B
{
   public:
      virtual void Tampil()
      {
         cout<<"Apakah Anda akan belajar C++?"<<endl;
      }
}; //akhir dari kelas B

class D : public B
{
   public :
      void Tampil()
      {
         cout<<"Saya juga belajar C++"<<endl;
      }
}; //akhir dari kelas D

int main()
{
   B b; //b dideklarasikan sebagai objek dari kelas B
   D d; //d dideklarasikan sebagai objek dari kelas D

   B *bptr = &(B) b ;      //pointer kelas basis
   bptr -> Tampil(); //statemen untuk memanggil fungsi kelas basis

   bptr = & d;             //pointer kelas basis ditugasi alamat dari objek terderivasi d
   bptr -> Tampil(); //memanggil fungsi Tampil() dari kelas terderivasi

   return 0;
}
KELUARAN
Apakah Anda akan belajar C++?
Saya juga belajar C++


Perhatikan bahwa fungsi Tampil() telah dideklarasikan virtual. Pointer kelas basis ditugasi alamat objek dari kelas terderivasi dan kemudian pointer itu dipakai untuk memanggil fungsi Tampil(). Pada kasus ini, definisi kelas terderivasi dari fungsi Tampil() mendefinisikan ulang definisi kelas basis dari fungsi itu dan hasilnya seperti ditunjukkan pada keluaran program.

KARAKTERISTIK DARI FUNGSI VIRTUAL
  1. Fungsi virtual harus didefinisikan di dalam kelas dimana di dalamnya ia pertama-kali muncul (kelas basis). Fungsi ini didefinisikan sebagai fungsi anggota publik dengan katakunci virtual yang mengawali pendefinisian.
  2. Fungsi virtual tidak dapat berupa anggota statis.
  3. Fungsi virtual dapat dideklarasikan di dalam fungsi inline.
  4.  Fungsi virtual dapat dideklarasikan sebagai friend dari kelas lain.
  5. Selain nama sama, prototipe-prototipe fungsi virtual di dalam kelas basis dan di dalam kelas terderivasi harus sama persis baik tipe, jumlah argumen, dan tipe dari tiap argumen. Penyimpangan dari syarat ini akan membuat kompiler menjadikannya sebagai fungsi yang terbebani (overloaded) dan kehilangan statusnya sebagai fungsi virtual.
  6. Fungsi konstruktor tidak dapat dijadikan fungsi virtual.
  7. Fungsi destruktor dapat dijadikan sebagai fungsi virtual.
  8. Definisi fungsi virtual di dalam kelas basis didefinisikan oleh definisi fungsi tersebut di dalam kelas terderivasi. Jika di dalam kelas terderivasi fungsi tidak didefinisikan ulang, maka definisi kelas basis yang akan dipanggil.
  9. Fungsi virtual diwariskan kepada semua kelas turunannya pada hierarki pewarisan.
  10. Sebuah kelas yang memuat satu atau lebih fungsi virtual seringkali dinamakan dengan kelas polimorfis.


Program berikut mengilustrasikan aplikasi dari sebuah fungsi virtual.

Program 6.5 Mengilustrasikan polimorfisme

#include <iostream>
using namespace std;

class B
{
   public:
      virtual void Tampil()
      {
         cout<<"Tampil dari kelas B dipanggil"<<endl;
      }
}; //akhir dari kelas B

class D1 : public B
{
   public:
      void Tampil()
      {
         cout<<"Tampil dari kelas D1 dipanggil"<<endl;
      }
} ; //akhir dari kelas D1

class D2 : public B
{
   public :
      void Tampil ()
      {
         cout<<"Tampil dari kelas D2 dipanggil"<<endl;
      }
}; //akhir dari kelas D2

int main ()
{
   B b;                    //b adalah sebuah objek dari kelas B
   B *bptr = &b;     //pointer kelas basis

   D1 d1;            //d1 dan d2 adalah objek dari kelas D1
   D2 d2;            //dan kelas D2

   bptr ->Tampil();

   bptr = &d1;             //pointer kelas basis ditugasi alamat
                     //dari objek kelas terderivasi
   bptr -> Tampil();

   bptr = &d2;
   bptr ->Tampil() ;

   return 0 ;
}
KELUARAN
Tampil dari kelas B dipanggil
Tampil dari kelas D1 dipanggil
Tampil dari kelas D2 dipanggil



6.3 Array yang Memuat Pointer-Pointer Kelas Basis
Ketika terdapat sejumlah kelas terderivasi yang mendefinisikan-ulang dan menggunakan fungsi virtual dari sebuah kelas basis, adalah lebih mudah untuk mendeklarasikan sebuah array yang memuat sejumlah pointer ke kelas basis. Elemen-elemen dari array adalah alamat-alamat dari objek-objek kelas tersebut. Dimisalkan terdapat kelas D1, D2, D3, dan seterusnya yang diderivasi dari kelas basis B. Dimisalkan bahwa b, d1, d2, d3, dan seterusnya adalah objek dari kelas B, D1, D2, D3, dan seterusnya. Untuk situasi seperti ini, Anda bisa mendeklarasikan dan menginisialisasi sebuah array yang memuat pointer-pointer kelas basis sebagai berikut:

B *bptr[] = {&b, &d1, &d2, &d3};

dimana &b adalah alamat dari objek kelas basis, &d1, &d2, dan &d3 adalah alamat-alamat dari objek-objek kelas terderivasi D1, D2, dan D3. Aplikasinya diilustrasikan pada program berikut.

Program 6.6 Mengilustrasikan sebuah array yang memuat pointer-pointer kelas basis yang menunjuk ke objek-objek kelas terderivasi

#include <iostream>
using namespace std;

class B
{
   public:
      virtual void Tampil()
      {
         cout<<"Tampil dari kelas B dipanggil"<<endl;
      }
}; //akhir dari kelas B

class D1 : public B
{
   public:
      void Tampil()
      {
         cout<<"Tampil dari kelas D1 dipanggil"<<endl;
      }
} ; //akhir dari kelas D1

class D2 : public B
{
   public :
      void Tampil()
      {
         cout<<"Tampil dari kelas D2 dipanggil"<<endl;
      }
}; //akhir dari kelas D2

class D3 : public B
{
   public :
      void Tampil()
      {
         cout<<"Tampil dari kelas D3 dipanggil"<<endl;
      }
}; //akhir dari kelas D3

int main ()
{
   B b;

   D1 d1;
   D2 d2;
   D3 d3;

   B *bptr[] = {&b, &d1, &d2, &d3}; //Array yang memuat pointer-pointer kelas B

   for (int i = 0; i<4; i++)
      bptr[i] ->Tampil();

   return 0 ;
}
KELUARAN
Tampil dari kelas B dipanggil
Tampil dari kelas D1 dipanggil
Tampil dari kelas D2 dipanggil
Tampil dari kelas D3 dipanggil

Pada program tersebut, elemen-elemen dari sebuah array yang berisi pointer-pointer ke kelas basis ditugasi alamat dari objek kelas basis dan dari objek-objek kelas terderivasi. Pada keluaran program, Anda dapat melihat bahwa definisi dari tiap kelas dipanggil.



6.4 Fungsi Virtual Murni dan Kelas Abstrak
Sebuah fungsi virtual murni tidak didefinisikan di dalam kelas basis, tetapi ia didefinisikan di dalam kelas terderivasi. Pada kelas basis, fungsi itu tidak memiliki tubuh fungsi dan deklarasinya diikuti dengan = 0. Deklarasi dari fungsi virtual murni di dalam kelas basis diilustrasikan berikut:

virtual tipe pengenal(tipe parameter, —, —) = 0

Di sini, virtual adalah katakunci dari C++, tipe adalah tipe data dari nilai balik dan pengenal adalah nama fungsi. Tidak ada definisi fungsi di dalam kelas basis. Prototipe dari fungsi diekuasi dengan nol. Tetapi fungsi virtual ini didefinisikan di dalam kelas-kelas terderivasi dengan caranya sendiri. Sebuah kelas yang memuat sebuah fungsi virtual murni disebut dengan kelas abstrak. Kelas abstrak tidak dapat memiliki objek sendiri. Berikut merupakan konsekuensi dari pendeklarasian fungsi virtual murni:
  1. Sebuah fungsi virtual murni tidak dapat dipakai untuk aksi apapun di dalam kelas basis. Pada dasarnya, kelas basis yang memuat sebuah fungsi virtual tidak dapat memiliki objek. Jadi tidak perlu diinisialisasi.
  2. Kelas dimana di dalamnya satu atau lebih fungsi virtual murni dideklarasikan disebut dengan kelas abstrak atau kelas abstrak murni.
  3. Sebuah kelas abstrak tidak dapat memiliki objek sendiri.
  4. Kelas terderivasi memiliki definisi sendiri dari fungsi virtual murni.
  5. Jika sebuah kelas terderivasi tidak mendefinisikan fungsi virtual murni yang dideklarasikan di dalam kelas basisnya, maka kelas itu juga akan menjadi kelas abstrak.


Program berikut memberikan sebuah ilustrasi dari kelas abstrak dan kompiler memberikan pesan error jika objek-objek dari kelas abstrak dideklarasikan.

Program 6.7 Mengilustrasikan fungsi virtual murni

#include <iostream>
using namespace std;

class B
{
   public :
      virtual void Tampil() = 0;
} ; //akhir dari kelas B

class D1 : public B // Derived class
{
   public :
      void Tampil(){ cout <<"Tampil() dari kelas D1 dipangil"<<endl;}
};

int main()
{
   B b ; //objek b dari kelas B dideklarasikan yang menghasilkan error

   B *bptr = &b;
   bptr -> Tampil();

   return 0 ;
}
KELUARAN
cannot instantiate abstract class


Program berikut mengilustrasikan aplikasi dari fungsi virtual murni. Sebuah kelas yang menangani sejumlah bangun dapat memiliki sebuah fungsi virtual Luas(). Fungsi Luas() didefinisikan di dalam kelas Lingkaran dan kelas Persegi yang merupakan kelas-kelas terderivasi dari kelas BangunBiasa.

Program 6.8 Mengilustrasikan aplikasi lain dari fungsi virtual murni

#include <iostream>
using namespace std;

class BangunBiasa {
   int sisi; //private secara default

   public:
      void set_sisi(int a)
      {
         sisi = a;
      }

      int get_sisi()
      {
         return sisi;
      }

      virtual double Luas (void) =0;
}; //akhir dari kelas BangunBiasa

class Lingkaran : public BangunBiasa
{
   public:
      double Luas (void)
      {
         return (3.14159*get_sisi()*get_sisi()/4);
      }
}; //akhir dari kelas Lingkaran

class Persegi : public BangunBiasa
{
   public:
      double Luas (void)
      {
         return (get_sisi()*get_sisi());
      }
}; //akhir dari kelas Persegi

int main ()
{
   double A1, A2;

   Persegi S1;
   Lingkaran C1;

   BangunBiasa* RS1 = & S1;
   BangunBiasa* RS2 = & C1;

   (*RS1).set_sisi(10);
   RS2 -> set_sisi(10);

   A1 = (*RS1).Luas(); //(*RS1).Luas() dan RS1 ->Luas() ekivalen
   A2 = RS2 -> Luas();

   cout<<"A1 = "<<A1<<"\t A2 = "<<A2<<endl;

   return 0;
}
KELUARAN
A1 = 100         A2 = 78.5397



6.5 Destruktor Virtual
Fungsi destruktor dapat dideklarasikan virtual sedangkan fungsi konstruktor tidak dapat dideklarasikan virtual. Pertanyaan selanjutnya adalah apakah keuntungan dari pendeklasian virtual atas fungsi destruktor? Pada kasus objek-objek yang diciptakan secara dinamis dari sebuah kelas terderivasi melalui pointer kelas basis, jika pointer kelas basis dihapus, maka hanya destruktor kelas basis yang dipanggil. Ini diilustrasikan pada program berikut.

Namun, jika fungsi destruktor kelas basis dideklarasikan virtual, maka pada penghapusan pointer kelas basis, fungsi-fungsi destruktor dari kelas-kelas terderivasi begitu juga dari kelas-kelas yang berelasi (kelas-kelas yang dimuat di dalam kelas terderivasi) juga akan dipanggil. Dua program berikut mengilustrasikannya.

Program 6.9 Mengilustrasikan watak dari fungsi destruktor ketika destruktor kelas basis tidak dideklarasikan virtual

#include <iostream>
using namespace std;

class B
{
   protected:
      int bx;

   public :
      B() {}         //konstruktor kosong dari kelas basis
      B(int m)
      {
         bx = m ;    //konstruktor dari kelas basis
         cout<<"Konstruktor dari kelas B dipanggil"<<endl;
      }

      virtual int Perkalian()
      {
         return 2*bx;
      }

      //fungsi destruktor tidak dideklarasikan virtual
      ~B()
      {
         cout<<"Destruktor dari kelas B dipanggil"<<endl;
      }
}; //akhir dari kelas B

class D1
{
   protected :
      int D1x ;

   public :
      D1 () {}             //konstruktor kosong (default) dari D1
      D1(int n)
      {
         D1x = n;    // constructor of class D1
         cout<<"Konstruktor dari D1 dipanggil"<<endl;
      }

      int getD1x()
      {
         return D1x;
      }
     
         ~D1()
      {
         cout<<"Destruktor dari D1 dipanggil"<<endl;
      }
};

class D2
{
   protected :
      int D2x ;

   public :
      D2(){}
      D2(int p)
      {
         D2x = p ;
         cout <<"Konstruktor dari D2 dipanggil"<<endl;
      }

      int getD2x()
      {
         return D2x;
      }

      ~D2()
      {
         cout <<"Destruktor dari D2 dipanggil"<<endl;
      }
};

class D3 : public B
{
   int D3x; // private secara default

   public :
      D3() {}
      D1 d1;  //d1 dideklarasikan sebagai objek dari kelas D1
      D2 d2;  //d2 dideklarasikan sebagai objek dari kelas D2
     
      D3(int p, int q , int r, int s): B(p), d1(q), d2(r)
      {
         D3x = s;
         cout<<"Konstruktor dari D3 dipanggil"<< endl;
      }

      int Perkalian()
      {
         return D3x * d2.getD2x() * d1.getD1x() * bx;
      }

      ~D3()
      {
         cout<<"Destruktor dari D3 dipanggil"<<endl;
      }
};

int main ()
{
   B *Bp ;
   Bp = new B(10); //objek dinamis yang diciptakan dengan operator new

   cout<<"Perkalian dari B = "<<Bp ->Perkalian()<<endl;

   Bp = new D3(2, 3, 4, 5);

   cout<<"Perkalian = "<<Bp -> Perkalian()<<endl;

   delete Bp; //penghapusan pointer kelas basis

   return 0 ;
}
KELUARAN
Konstruktor dari kelas B dipanggil
Perkalian dari B = 20
Konstruktor dari kelas B dipanggil
Konstruktor dari D1 dipanggil
Konstruktor dari D2 dipanggil
Konstruktor dari D3 dipanggil
Perkalian = 120
Destruktor dari kelas B dipanggil


Pada program di atas, jelaslah bahwa hanya destruktor kelas basis yang dipanggil. Pada program berikut, fungsi destruktor dari kelas basis dideklarasikan virtual. Dengan ini, pada penghapusan pointer kelas basis, fungsi-fungsi destruktor dari kelas terderivasi maupun dari kelas-kelas yang berelasi (kelas D1 dan D2 pada kasus tersebut) juga dipanggil. Keluaran dari program ini mengilustrasikannya.

Program 6.10 Mengilustrasikan efek dari pendeklarasian virtual pada destruktor kelas basis

#include <iostream>
using namespace std;

class B
{
   protected:
      int bx;

   public :
      B() {}         //konstruktor kosong dari kelas basis
      B(int m)
      {
         bx = m ;    //konstruktor dari kelas basis
         cout<<"Konstruktor dari kelas B dipanggil"<<endl;
      }

      virtual int Perkalian()
      {
         return 2*bx;
      }

      //fungsi destruktor tidak dideklarasikan virtual
      virtual ~B()
      {
         cout<<"Destruktor dari kelas B dipanggil"<<endl;
      }
}; //akhir dari kelas B

class D1
{
   protected :
      int D1x ;

   public :
      D1 () {}             //konstruktor kosong (default) dari D1
      D1(int n)
      {
         D1x = n;    // constructor of class D1
         cout<<"Konstruktor dari D1 dipanggil"<<endl;
      }

      int getD1x()
      {
         return D1x;
      }
     
         ~D1()
      {
         cout<<"Destruktor dari D1 dipanggil"<<endl;
      }
};

class D2
{
   protected :
      int D2x ;

   public :
      D2(){}
      D2(int p)
      {
         D2x = p ;
         cout <<"Konstruktor dari D2 dipanggil"<<endl;
      }

      int getD2x()
      {
         return D2x;
      }

      ~D2()
      {
         cout <<"Destruktor dari D2 dipanggil"<<endl;
      }
};

class D3 : public B
{
   int D3x; // private secara default

   public :
      D3() {}
      D1 d1;  //d1 dideklarasikan sebagai objek dari kelas D1
      D2 d2;  //d2 dideklarasikan sebagai objek dari kelas D2
     
      D3(int p, int q , int r, int s): B(p), d1(q), d2(r)
      {
         D3x = s;
         cout<<"Konstruktor dari D3 dipanggil"<< endl;
      }

      int Perkalian()
      {
         return D3x * d2.getD2x() * d1.getD1x() * bx;
      }

      ~D3()
      {
         cout<<"Destruktor dari D3 dipanggil"<<endl;
      }
};

int main ()
{
   B *Bp ;
   Bp = new B(10); //objek dinamis yang diciptakan dengan operator new

   cout<<"Perkalian dari B = "<<Bp ->Perkalian()<<endl;

   Bp = new D3(2, 3, 4, 5);

   cout<<"Perkalian = "<<Bp -> Perkalian()<<endl;

   delete Bp; //penghapusan pointer kelas basis

   return 0 ;
}
KELUARAN
Konstruktor dari kelas B dipanggil
Perkalian dari B = 20
Konstruktor dari kelas B dipanggil
Konstruktor dari D1 dipanggil
Konstruktor dari D2 dipanggil
Konstruktor dari D3 dipanggil
Perkalian = 120
Destruktor dari D3 dipanggil
Destruktor dari D2 dipanggil
Destruktor dari D1 dipanggil
Destruktor dari kelas B dipanggil



6.6 Kelas Basis Virtual
Gambar 6.3 menunjukkan sebuah kasus pewarisan dimana di dalamnya dua kelas D1 dan D2 diderivasi dari kelas basis B. Kelas lain D3 diderivasi dari kedua kelas terderivasi tersebut. Sekarang, sebuah objek dari kelas D3 memanggil suatu fungsi dari kelas B. Kompiler menunjukkan error yang menandakan ambibuitas. Karena, kedua kelas D1 dan D2 memiliki salinan sendiri dari kelas B. Jadi, D3 memiliki dua salinan B. Kompiler tidak bisa memutuskan salinan mana dari B yang dipanggil. Ambiguitas ini dapat diselesaikan jika kelas basis dideklarasikan virtual di dalam definisi kelas D1 dan D2. Pada kasus itu, hanya ada satu salinan dari kelas basis di dalam D3 sehingga tidak ada ambiguitas. Ini ditunjukkan pada program 6.11 dan program 6.2. Pada program 6.11, ambiguitas diilustrasikan dan pada program 6.12, ambiguitas diselesaikan.

Program 6.11 Mengilustrasikan ambiguitas yang diciptakan di dalam kelas terderivasi dari kelas-kelas terderivasi multi-lintasan dari kelas basis yang sama

#include <iostream>
using namespace std;

class B
{
   public:
      void Tampil() {cout<<"Fungsi Tampil() dari kelas B dipanggil"<<endl;}
};

class D1 : public B
{};

class D2 : public B
{};

class D3 : public D2, public D1
{};

int main()
{
   D3 d3;
   d3.Tampil();

   return 0;
}
KELUARAN
'D3::Tampil' is ambiguous



GAMBAR 6.3 Pewarisan multi-lintasan dari kelas basis yang sama



Ambiguitas pada program 6.11 dapat diselesaikan dengan mendeklarasikan virtual atas kelas basis pada deklarasi kelas terderivasi.

Program 6.12 Mengilustrasikan kelas basis virtual

#include <iostream>
using namespace std;

class B
{
   public:
      void Tampil() {cout<<"Fungsi Tampil() dari kelas B dipanggil"<<endl;}
};

class D1 : virtual public B
{};

class D2 : virtual public B
{};

class D3 : public D2, public D1
{};

int main()
{
   D3 d3;
   d3.Tampil();

   return 0;
}
KELUARAN
Fungsi Tampil() dari kelas B dipanggil



6.7 RTTI (run-time type information)
Seperti yang telah dijelaskan, pointer kelas basis dapat dibuat untuk menunjuk ke objek-objek dari sembarang kelas terderivasi, bahkan menunjuk ke objek-objek dari kelas yang diderivasi dari kelas terderivasi. Namun, pointer itu tidak membawa informasi apapun tentang objek kelas mana yang ditunjuknya. Pada program tertentu, informasi semacam ini diperlukan jika Anda ingin menerapkan suatu fungsi spesial terhadap objek-objek dari kelas tertentu saja. Ini melibatkan pembedaan tipe-tipe objek dan pencarian objek-objek dari kelas yang diinginkan sehingga fungsi itu dapat diterapkan.


Salah satu solusinya adalah dengan mendefinisikan sebuah fungsi anggota di dalam tiap kelas untuk menyediakan informasi tipe dan yang dapat dipakai untuk memilih tipe objek yang diinginkan. Ketika jumlah kelas semakin banyak, kode untuk pemilihan ini akan semakin panjang dengan rantai if-else atau switch. Fungsi virtual juga bisa menjadi alternatif solusi. Program berikut menggunakan fungsi virtual Tipe() untuk mendapatkan informasi tipe runt-time.

Program 6.13 Mendapatkan RTTI

#include <iostream>
using namespace std;

class B
{
   public:
      virtual char Tipe(){return 'B';}
};

class C : public B
{
   public:
      char Tipe(){return 'C';}
};

class D : public B
{
   public:
      char Tipe(){return 'D';}
};

int main()
{
   B b;
   C c ;
   D d ;

   B* pb = &b;
   cout<<"Tipe dari pb sekarang adalah : "<< pb -> Tipe()<<endl;

   pb = & c;
   cout<<"Tipe dari pb sekarang adalah : "<<pb -> Tipe()<<endl;

   pb = & d;
   cout<<"Tipe dari pb sekarang adalah : "<<pb -> Tipe()<<endl;

   return 0 ;
}
KELUARAN
Tipe dari pb sekarang adalah : B
Tipe dari pb sekarang adalah : C
Tipe dari pb sekarang adalah : D


Tetapi C++ menyediakan dukungan langsung untuk RTTI menggunakan dua operator, yaitu dynamic_cast() dan typeid(). Operator typeid() dapat diterapkan pada kelas polimorfis maupun pada kelas tak-polimorfis. Operator ini menentukan dan menghasilkan sebuah referensi terhadap tipe eksak dari objek. Nama kelas dari objek juga dapat diperoleh menggunakan fungsi name(). Gambar 6.4 memberikan ilustrasi dari operator typeid().


GAMBAR 6.4 Identifikasi kelas dari objek-objek


Kelas-kelas yang berkaitan dengan typeid() dimuat di dalam file header <typeinfo> dan file header ini harus dicantumkan di dalam program jika objek-objeknya akan dipakai di dalam program. Sintaksis untuk typeid() adalah sebagai berikut:

typeid(d);

dimana d adalah sebuah objek kelas. Kode ini menghasilkan referensi terhadap tipe dari objek d. File header <typeinfo> juga mendefinisikan dua operator relasional, == dan !=, untuk membandingkan tipe dari dua objek, katakanlah d1 dan d2. Sintaksisnya diilustrasikan sebagai berikut:

if(typeid(d1) == typeid(d2))

atau

if (typeid(d1) != typeid(d2))

File header <typeinfo> juga mendefinisikan fungsi name() yang menghasilkan nama dari tipe atau nama dari kelas dari suatu objek. Sintaksis untuk menggunakan name() diberikan berikut:

typeid(d1).name();

Fungsi ini menghasilkan nama tipe dari objek d1. Kode berikut dipakai untuk menampilkan nama tipe dari objek d1:

cout<<typeid(d1).name();

Aplikasi dari operator typeid() diilustrasikan pada program berikut.

Program 6.14 Aplikasi typeid() pada kelas tak-polimorfis

#include <iostream>
#include <typeinfo> //file header untuk typeid()
using namespace std;

class B
{};

class D1 : public B
{};

class D2 : public D1
{};

int main ()
{
   D2 d2;
   D1 d1;

   if (typeid( d1) == typeid (d2))
      cout<<"Kedua objek sama"<<endl;
   else
      cout<<"Kedua objek berbeda tipe"<<endl;

   cout<<"Tipe dari d1 adalah "<<typeid(d1).name()<<endl;
   cout<<"Tipe dari d2 adalah "<<typeid(d2).name()<<endl;

   return 0;
}
KELUARAN
Kedua objek berbeda tipe
Tipe dari d1 adalah class D1
Tipe dari d2 adalah class D2



6.8 Operator-Operator Casting pada C++
OPERATOR dynamic_Cast()
Operator ini dipakai untuk mengkonversi tipe objek pada saat run time dan hanya berlaku pada kelas polimorfis, yaitu kelas basis harus memiliki sedikitnya satu fungsi virtual. Operator dynamic_cast<>() juga dapat diterapkan pada pointer dan referensi. Untuk pointer, berikut sintaksisnya:

dynamic_cast <tipe_derivasi*> (ptr_basis);

Operator dynamic_cast() ini mengkonversi pointer kelas basis (ptr_basis) menjadi pointer kelas terderivasi (tipe_derivasi*) dikarenakan bahwa objek yang ditunjuk oleh ptr_basis adalah dari kelas terderivasi yang diinginkan atau kelas yang diderivasi dari kelas terderivasi yang diinginkan.  Jadi, ini merupakan cara mudah untuk memilih objek-objek dari kelas terderivasi yang diinginkan dari sejumlah objek dari beberapa kelas terderivasi. Ilustrasi tentang hal ini ditampilkan pada Gambar 6.5. Pointer-pointer yang nantinya diinginkan untuk memilih objek-objek dari kelas tertentu semuanya dikonversi dengan operator dynamic_cast(). Pointer-pointer yang menunjuk objek-objek dari kelas yang diinginkan akan dikonversi sedangkan pointer-pointer lainnya akan menghasilkan 0. 


GAMBAR 6.5 Ilustrasi dari operator dynamic_cast()


Program 6.15a Aplikasi dari operator dynamic_cast()

#include <iostream>
using namespace std;

class B
{
   public:
      B() {};
      virtual void Fungsi(){};
};

class D1 : public B
{};

class D2 : public B
{};

class D3 : public B
{};

int main ()
{
   B* Ab[3];
   Ab[0] = new D1;
   Ab[1] = new D2;
   Ab[2] = new D3;

   for (int i =0; i<3; i++)
   {
      D2 *ptrd2 = dynamic_cast<D2*>(Ab[i]);
     
         if (ptrd2 != 0 )
         cout<<"Objek D2 ditemukan\n";
   }

   return 0;
}
KELUARAN
Kedua objek berbeda tipe
Tipe dari d1 adalah class D1
Tipe dari d2 adalah class D2


File header <typeinfo> juga mendefinisikan dua fungsi, yaitu bad_typeid dan bad_cast. Jika operator typeid() tidak berhasil, maka ia akan melemparkan sebuah eksepsi bad_typeid. Jika operator dynamic_cast() tidak berhasil, maka bad_cast akan dilempar dan program akan berhenti.

OPERATOR static_cast()
Operator static_cast() melakukan konversi dari satu tipe ke tipe lain dengan cara yang sama dengan cara casting konvensional dari bahasa C. Sintaksisnya diberikan berikut:

static_cast<T>(objek)

Ekspresi ini mengkonversi tipe objek menjadi T. Tipe T bisa berupa salah satu dari berikut:
a.        Tipe data fundamental seperti int, double, char, dan lainnya.
b.       Tipe yang didefinisikan user, yaitu nama kelas.
c.        Pointer.
d.       Referensi.
e.        Tipe enumerasi.

Seperti diimplikasi dari namanya, operator ini hanya dapat diterapkan pada kasus tak-polimorfis. Objek dari satu kelas dapat diubah menjadi objek dari kelas lain jika diberikan fungsi-konstruksi yang diperlukan. Program berikut memberikan ilustrasi dari operator static_cast() untuk pointer (yang mengkonversi dari pointer kelas basis menjadi pointer kelas terderivasi)

Program 6.15b Aplikasi dari operator static_cast()

#include <iostream>
using namespace std;

class Basis
{
   public :
      int x;
      int y;
      void Bicara() { cout<<"Saya adalah kelas Basis.\n";}
}; //akhir dari kelas Basis

class D : public Basis
{
   public:
      void bicara() { cout<<"Saya adalah kelas D. \n";}

      Basis B;       //B adalah objek dari kelas Basis

      int Luas(Basis B)
      {return B.x*B.y;}
};

int main()
{
   Basis B ;

   B.x =5;
   B.y = 20;

   Basis * basisp = &B; // pointer ke kelas Basis
   cout<<"Fungsi Bicara() dipanggil dengan basisp."<<endl;

   basisp -> Bicara();

   cout<<"Setelah operator static cast(), fungsi D dipanggil"<<endl;
   D * dp = static_cast<D*>(basisp);     //basisp cast dikonversi menjadi tipe D
   dp->bicara();           //fungsi Bicara() dari kelas D dipanggil oleh pointer dp

   D* dp1 = (D*)basisp; //konversi konvensional dari bahasa C
   cout<<"Setelah konversi konvensional dari bahasa C, untuk memanggil Bicara()"<<endl;
   dp1 -> bicara();

   D d; //d dideklarasikan sebagai objek dari kelas D
   cout<<"Luas = "<< d.Luas(B)<<endl;    //memanggil fungsi Luas()

   cout<<"Setelah mengkonversi sebuah objek menjadi referensi menggunakan konversi konvensional"<< endl;
   D &dp2 = (D&) B;    //mengkonversi sebuah referensi dengan konversi konvensional bahasa C
   dp2.Bicara();     //memanggil sebuah fungsi dari kelas Basis
   dp2.bicara();     //memanggil sebuah fungsi dari kelas D

   D &dp3 = static_cast<D&>(B); //mengkonversi sebuah referensi dengan operator static_cast()
   cout<<"Setelah penerapan operator static_cast(), fungsi-fungsi dipanggil lagi"<<endl;
   dp3.Bicara(); //Bicara() dan Bicara() adalah dua fungsi yang berbeda
   dp3.bicara();

   return 0 ;
}
KELUARAN
Fungsi Bicara() dipanggil dengan basisp.
Saya adalah kelas Basis.
Setelah operator static cast(), fungsi D dipanggil
Saya adalah kelas D.
Setelah konversi konvensional dari bahasa C, untuk memanggil Bicara()
Saya adalah kelas D.
Luas = 100
Setelah mengkonversi sebuah objek menjadi referensi menggunakan konversi konvensional
Saya adalah kelas Basis.
Saya adalah kelas D.
Setelah penerapan operator static_cast(), fungsi-fungsi dipanggil lagi
Saya adalah kelas Basis.
Saya adalah kelas D.


OPERATOR reinterpret_cast<>()
Operator ini memiliki sintaksis berikut:

reinterpret_cast<T>(objek)

Operator ini menghasilkan interpretasi-ulang atas operandnya yang cukup berbeda dari semula. Sebagai contoh, operator ini dapat mengkonversi sebuah int menjadi sebuah tipe pointer dan sebuah pointer menjadi sebuah int atau mengubah sebuah pointer menjadi sebuah char, dan seterusnya. Kedua operator, dynamic_cast() dan reinterpret_cast(), sangat rentan dengan error sehingga perlu digunakan dengan hati-hati.

Operator reinterpret_cast() dapat dipakai pada pointer maupun pada referensi. Operator ini juga dapat dipakai objek statis maupun objek dinamis. Sama seperti operator static_cast(), operator ini mempertahankan kekonstanan dari sebuah objek. Program berikut mengilustrasikan aplikasi dari reinterpret_cast() untuk mengkonversi pointer dan referensi. Operator ini juga dapat diterapkan pada objek-objek dari kelas yang tak-utuh.

Program 6.16 Aplikasi dari operator reinterpret_cast()

#include <iostream>
#include <string>
using namespace std;

class A;      //kelas tak-utuh
class B;      //kelas tak-utuh

class Basis
{
   public :
      void Bicara() {cout<<"Saya adalah kelas Basis.\n";}
};

class D : public Basis
{
   public:
      void bicara() {cout<<"Saya adalah kelas D.\n";}
};

int main()
{
   int x = 65;
   
   int* px = &x;           //px adalah pointer yang menunjuk ke int x
   char* chp = (char*) px; //px dikonversi menjadi pointer char* chp
                           //dengan konversi konvensional bahasa C

   cout<<"*chp = "<<*chp<<endl;   //statemen keluaran untuk *chp
   cout<<*px <<endl;

   //berikut konversi sama yang dilakukan oleh reinterpret_cast<>()
   char* ptrx = reinterpret_cast<char*> (px);
   cout<<"*ptrx = "<<*ptrx<<endl;

   D* ptrd = reinterpret_cast<D*>(px); //px dikonversi menjadi tipe D
   ptrd -> bicara();

   B* bp;            //Operator ini juga dapat diterapkan pada kelas tak-utuh
   A* ap = reinterpret_cast<A*>(bp);
}
KELUARAN
*chp = A
65
*ptrx = A
Saya adalah kelas D.


Pada program berikut, operator reinterpret_cast() dipakai pada pointer yang menunjuk ke fungsi.

Program 6.17 Aplikasi dari operator reinterpret_cast()

#include <iostream>
using namespace std;

void Fungsi1()
{
   cout<<"Ini adalah fungsi void dengan argumen void.\n";
}

void Fungsi2(int m, int n)
{
   cout<<"Ini adalah fungsi void dengan dua argumen.\n";
}

int Fungsi3 (int x, int y)
{
   return x*y;
}

int main()
{
   int x = 65;
  
   int * px = &x;                 //px adalah pointer yang menunjuk ke int x
   typedef void (*PF)();   //typedef atas pointer ke fungsi void

   PF fungsi1 = (PF)Fungsi1;      //menggunakan konversi konvensional
   fungsi1() ;                                  //pemanggilan fungsi

   PF fungsi2 = reinterpret_cast<PF>(Fungsi1);  //oleh reinterpret_cast
   fungsi2();                            // fungsi2 dipanggil

   int y = 2;
   PF aFungsi2 = reinterpret_cast<PF>(Fungsi2);
   aFungsi2();

   typedef int(*pF)(int, int);    //definisi tipe atas pointer yang menunjuk ke
                                  //fungsi-fungsi int yag memiliki dua parameter int
   pF fungsi3 = (pF)Fungsi3 ;             //konversi konvensional dari bahasa C
   cout<<fungsi3(x,y )<<endl;             //pemanggilan fungsi

   //berikut adalah konversi yang sama menggunakan reinterpret_cast<>()
   pF fungsi4 = reinterpret_cast<pF>(Fungsi3);
   cout<< fungsi4(x,y)<< endl;

   return 0;
}
KELUARAN
Ini adalah fungsi void dengan argumen void.
Ini adalah fungsi void dengan argumen void.
Ini adalah fungsi void dengan dua argumen.
130
130


OPERATOR const_cast<>()
Operator ini menyediakan akses terhadap sebuah variabel dengan atribut const atau volatile. Objek-objek yang dideklarasikan volatile dapat dimodifikasi dari luar kendali program. Operator ini tidak mengubah atribut dasar dari variabel. Sintaksis dari operator ini adalah berikut.

const_cast <T*> (objek_ptr)

Program berikut mencoba untuk mengaplikasikan operator const_cast().

Program 6.18 Aplikasi dari operator const_cast()

#include <iostream>
using namespace std;

void F(char* M)
{cout<<M<<endl;}

int main()
{
   const char * S= "Jakarta";

   //F(S); Error, tidak dapat mengkonversi dari const char* menjadi char*
   F(const_cast<char*>(S)); //ini OK

   const int n = 10;
   const int *ptrn = &n;

   // n= n+5; Error, n adalah const. Tetapi berikut OK
   *(const_cast<int*>(ptrn)) = n + 5;
   cout<<*(const_cast<int*>(ptrn))<<endl;

   //const_cast<int>(n) = n+8; Ini menghasilkan error, operator const_cast
   //tidak dapat mengkonversi dari const int menjadi int.
   cout << n <<endl;

   return 0 ;
}
KELUARAN
Jakarta
15
10

LATIHAN
1.       Bagaimana Anda mendeklarasikan pointer yang menunjuk ke sebuah kelas?
2.       Bagaimana Anda  membuat pointer kelas basis agar menunjuk ke objek dari kelas terderivasi?
3.       Apa keuntungan dari pendeklarasian sebuah fungsi kelas basis sebagai fungsi virtual?
4.       Apa itu kelas basis virtual?
5.       Apa perbedaan antara fungsi virtual dan fungsi terbebani?
6.       Apa itu kelas abstrak? Apa gunanya kelas abstrak?
7.       Apa yang Anda pahami tentang pengikatan dinamis?
8.       Apa yang Anda pahami tentang polimorfisme?
9.       Mengapa konstruktor tidak bisa dideklarasikan virtual sementara destruktor dapat dideklarasikan virtual?
10.    Apa keuntungan dari pendeklasian virtual atas destruktor?
11.    Tulislah sebuah program yang mengilustrasikan fungsi virtual murni Bicara() di dalam kelas basis Mahluk. Kelas-kelas terderivasi adalah kelas Anjing, kelas Kucing, kelas IngMan (orang Inggris), dan IndMan (orang Indonesia). Fungsi Bicara() menampilkan bahasa lidah masing-masing.

Jawaban:

Program 6.19 Contoh lain dari kelas abstrak dan fungsi virtual murni

#include <iostream>
using namespace std;

class Mahluk
{
   public:
      virtual void Bicara() = 0;
};

class Anjing : public Mahluk
{
   public:
      void Bicara ()
         {cout<<"Guk guk guk"<<endl;}
};

class Kucing : public Mahluk
{
   public:
      void Bicara(void)
         {cout<<"Meonk meonk meonk"<<endl;}
};

class IngMan : public Mahluk
{
   public :
      void Bicara(){cout<<"I can speak English"<<endl;}
};

class IndMan : public Mahluk
{
   public :
      void Bicara(){cout<<"Saya bicara bahasa Indonesia"<<endl;}
};

void main ()
{
   Anjing A;
   Kucing K;

   IngMan IgM;
   IndMan InM;

   A.Bicara();
   K.Bicara();

   IgM.Bicara();
   InM.Bicara();

   cout<<"\n";
}
KELUARAN
Guk guk guk
Meonk meonk meonk
I can speak English
Saya bicara bahasa Indonesia






No comments: