Modul 4: Pointer dan Struct - Algoritma-dan-Pemrograman-ITS/DasarPemrograman GitHub Wiki

Daftar Isi


Pointer

Alamat Memori

Operator Address-Of (&)

Setiap variabel, fungsi, struct, ataupun objek lain yang dibuat dalam program mempunyai lokasi masing-masing pada memori. Alokasi setiap variabel disimpan dalam alamat memori tertentu.

Misalnya:
Terdapat variabel bernama var. Untuk mengetahui alamat memori dari variabel, digunakan operator address-of (&) di depan nama variabelnya.

int var = 5;
printf("%d\n", var);
printf("%p\n", &var);

Output:

5
0x7fffdeb3ed84

Output bisa berbeda-beda di tiap eksekusi.
0x7fffdeb3ed84 merupakan alamat memori dari variabel var.


Pengenalan Pointer

Pointer adalah variabel spesial yang menampung alamat memori, bukan nilai seperti variabel biasa.

Deklarasi Variabel Pointer

Deklarasi variabel pointer menggunakan operator * di antara tipe data dan nama variabelnya.

int *ptr;

atau

int* ptr;

Kedua cara deklarasi di atas merupakan sintaks yang valid.

Inisialisasi Variabel Pointer

Variabel ptr di atas adalah variabel pointer yang bertipe int. Variabel pointer menampung alamat memori. Inisialisasi variabel pointer harus berupa alamat memori, bisa dari variabel lain atau alokasi secara dinamis.

int var = 55;
int *ptr = &var; // Inisialisasi menggunakan alamat dari var

Inisialisasi yang tidak sesuai akan menghasilkan error atau undefined behaviour.

// ERROR
int *ptr  = 5;
// UNDEFINED BEHAVIOUR
int *ptr2 = 0x7fffdeb3ed84;

Assignment Variabel Pointer

Cara melakukan assignment pada variabel pointer tidak sama dengan inisialisasinya.

int var, *ptr;
var = 55;
ptr = &var; // Assignment pada variabel pointer menggunakan alamat dari var

Assignment tidak perlu menggunakan simbol * di depan nama variabelnya. Berbeda pada saat deklarasi yang mana kita perlu memberitahu compiler bahwa variabel tersebut adalah variabel pointer.

Operator Dereference (*)

Operator dereference menggunakan simbol yang sama dengan simbol operator perkalian, yakni * (simbol asterisk). Namun, fungsinya sangat berbeda. Operator dereference digunakan untuk mengakses nilai yang ditunjuk (pointed) dari sebuah variabel pointer.

Untuk mengakses nilai dari sebuah variabel pointer, digunakan operator dereference di depan nama variabel pointer.

int var  = 55;
int *ptr = &var;

printf("%d\n", *ptr);
*ptr = 20;

printf("%d\n", *ptr);
printf("%d\n", var);

Output

55
20
20

Apa output dari program di bawah ini?

#include <stdio.h>

int main(void)
{
    int x, y, z;
    int *ptr1, *ptr2;
    x = 5;
    y = 6;

    ptr1 = &x;
    ptr2 = &y;

    z = *ptr1 + *ptr2;
    printf("%d\n", z);
    
    return 0;
}

Double Pointer

Variabel pointer juga dapat menunjuk variabel pointer lainnya. Hal ini disebut dengan double pointer (pointer to pointer). Untuk mendeklarasikan variabel double pointer, digunakan dua simbol *. Kegunaan paling umum dari variabel double pointer adalah untuk membuat array dua dimensi secara dinamis.

int **dbPtr;

Variabel dbPtr di atas menyimpan alamat memori dari variabel pointer lainnya.

#include <stdio.h>

int main(void)
{
    int var = 23;
    int *ptr = &var;
    int **dbPtr = &ptr;

    printf("%d\n", **dbPtr);
    
    return 0;
}

Pointer dan Array

Kita sudah mengetahui bahwa array adalah kumpulan data yang disusun secara sekuensial. Karena disusun secara sekuensial, alamat-alamat memori tiap elemen array juga tersusun secara berurutan.

memory

Bagaimana jika kita ingin mengetahui alamat memori dari array?

#include <stdio.h>

int main()
{
    int arr[5] = {1, 2, 3, 4, 5};
    int i;
    for (i = 0; i < 5; ++i) {
        printf("&arr[%d] => %p\n", i, &arr[i]);
    }
    printf("Address of arr => %p\n", arr);
    return 0;
}

Output

&arr[0] => 0x7fffe85f0520
&arr[1] => 0x7fffe85f0524
&arr[2] => 0x7fffe85f0528
&arr[3] => 0x7fffe85f052c
&arr[4] => 0x7fffe85f0530
Address of arr => 0x7fffe85f0520

Dapat diperhatikan bahwa alamat dari &arr[0] sama dengan alamat dari arr. Dari hal ini dapat diketahui bahwa nama array menunjuk ke elemen pertama dari array tersebut. Karena &arr[0] = arr, maka dapat disimpulkan bahwa arr[0] = *arr, atau nilai dari elemen pertama dapat diakses dengan *arr atau *(arr + 0).

arr[0] = *(arr + 0)
arr[1] = *(arr + 1)
arr[2] = *(arr + 2)
.
.
dst

Kesimpulan: Nama array merujuk pada alamat memori dari elemen pertama pada array. Berbekal dari hal tersebut, maka kita dapat melakukan hal demikian.

#include <stdio.h>

int main()
{
    int arr[5] = {1, 2, 3, 4, 5}, i;
    int *ptr = arr;

    for (i = 0; i < 5; ++i) {
        printf("%d ", *(ptr+i));
    }
    return 0;
}

Output

1 2 3 4 5

Pointer dan Fungsi

Sebelumnya kita sudah mengetahui bahwa fungsi dapat menerima parameter sebagai input. Penggunaan-penggunaan parameter fungsi selama ini sebenarnya menggunakan konsep pass by value. Selain menggunakan cara itu, terdapat cara lain untuk passing argumen pada fungsi.

Pass by Value

Pass by Value berarti saat kita memasukkan (passing) argumen pada fungsi, nilai dari argumen tersebut akan disalin ke variabel yang berada pada parameter fungsi. Karena hanya nilainya saja yang diterima oleh fungsi, perubahan yang terjadi pada variabel parameter fungsi tidak akan berpengaruh terhadap variabel asalnya.

Contoh:

#include <stdio.h>

void change(int a, int b)
{
    a = a + 5;
    b = b + 5;
}

int main()
{
    int x = 10, y = 6;
    change(x, y);
    printf("%d %d\n", x, y);

    return 0;
}

Nilai pada variabel x dan y tidak berubah karena metode passing yang digunakan adalah Pass by Value.

Pass by Address/Reference

Berbeda dengan sebelumnya, sesuai namanya, metode Pass by Address berarti argumen yang dimasukkan (passing) ke parameter fungsi adalah alamat memori variabel. Segala perubahan yang terjadi pada variabel tersebut, juga mempengaruhi langsung ke variabel asalnya. Hal ini terjadi karena argumennya adalah langsung berupa alamat memorinya.

#include <stdio.h>

void change(int *a, int *b)
{
    *a = *a + 5;
    *b = *b + 5;
}

int main()
{
    int x = 10, y = 6;
    change(&x, &y);
    printf("%d %d\n", x, y);

    return 0;
}

Karena parameternya menerima alamat memori, maka variabel parameternya harus berupa pointer.

Passing array sebagai parameter fungsi juga dapat dilakukan dengan pointer. Segala perubahan pada array akan berpengaruh pada array asalnya.

#include <stdio.h>

void printArr(int *arr)
{
    // ...
    // ...
}

int main()
{
    int num[5] = {1, 2, 3, 4, 5}, i;
    printArr(num);
    // ...
    // ...
    return 0;
}

Struct

Pengenalan Struct

Dalam bahasa C, struct adalah salah satu tipe data turunan atau bisa disebut juga user defined data type yang dapat mengelompokkan beberapa variabel di bawah satu nama. Tidak seperti array yang hanya dapat menyimpan elemen dengan tipe data sama, struct dapat mengelompokkan elemen dengan tipe data yang berbeda-beda.

Contoh:

Perhatikan gambar di atas. Mahasiswa merupakan suatu entitas yang di dalamnya terdapat atribut-atribut berupa:

  • Nama
  • NRP
  • Umur
  • IPK
  • Semester
  • Status

Atribut-atribut inilah yang nantinya berperan sebagai member dari struct Mahasiswa.

Deklarasi Struct

Seperti variabel, struct harus dideklarasikan terlebih dahulu sebelum bisa digunakan. Pendeklarasian struct menggunakan sintaks sebagai berikut.

struct <nama_struct> {
    <tipe_data_member> <nama_member>;
    <tipe_data_member> <nama_member>;
    <tipe_data_member> <nama_member>;
    .
    .
    .
};

Berikut adalah contoh deklarasi struct berdasarkan kasus Mahasiswa di atas.

struct Mahasiswa {
    char nama[100];
    char nrp[20];
    int umur;
    double ipk;
    int semester;
    int status;
};

Variabel Struct

Setelah dideklarasikan, sebuah struct akan menjadi tipe data baru. Maka dalam kasus ini, struct Mahasiswa di sini menjadi tipe data baru dengan member-member berupa nama, nrp, umur, ipk, semester, dan status. Untuk membuat variabel dengan tipe data struct, dilakukan dengan sintaks berikut.

struct <nama_struct> <nama_variabel>;

Contoh:

struct Mahasiswa mhs1;
struct Mahasiswa mhs2;

Contoh di atas menunjukkan terdapat dua variabel mhs1 dan mhs2 bertipe struct Mahasiswa.

Akses Member Struct

Lalu bagaimana cara untuk mengakses member dari variabel struct yang telah dibuat? Untuk mengakses member-member dari struct, digunakan operator dot (.) setelah nama variabelnya.

<nama_variabel>.<member_struct>

Contoh:

mhs1.umur = 19;
mhs1.semester = 3;

mhs2.umur = 20;
mhs2.semester = 5;

Contoh program untuk mendemonstrasikan Struct:

#include <stdio.h>
#include <string.h>

struct Mahasiswa {
    char nama[100];
    char nrp[20];
    int umur;
    double ipk;
    int semester;
    int status;
};

int main(void)
{
    struct Mahasiswa mhs1;

    strcpy(mhs1.nama, "Ahmad");
    strcpy(mhs1.nrp, "05111940000012");
    mhs1.umur = 18;
    mhs1.ipk = 3.94;
    mhs1.semester = 3;
    mhs1.status = 1;

    printf("Nama\t: %s\n", mhs1.nama);
    printf("NRP\t: %s\n", mhs1.nrp);
    printf("Umur\t: %d\n", mhs1.umur);
    printf("IPK\t: %.2lf\n", mhs1.ipk);
    printf("Sem\t: %d\n", mhs1.semester);
    printf("Status\t: %s\n", (mhs1.status == 1 ? "Aktif" : "Tidak Aktif"));
    
    return 0;
}

Array of Struct

Kita juga dapat membuat array dengan tipe data struct. Caranya sama persis dengan deklarasi array pada umumnya.

#include <stdio.h>
struct Point {
    int x, y;
};

int main()
{
    struct Point arr[3];
    arr[0].x = 2, arr[0].y = 3;
    arr[1].x = 5, arr[1].y = 3;
    arr[2].x = 2, arr[2].y = 8;

    printf("%d %d\n", arr[0].x, arr[0].y);
    printf("%d %d\n", arr[1].x, arr[1].y);
    printf("%d %d\n", arr[2].x, arr[2].y);
    
    return 0;
}

Pointer dan Struct

Kita juga dapat membuat pointer menuju variabel dengan tipe data struct. Untuk mengakses atribut di dalam struct, digunakan operator arrow (->). Operator arrow memiliki fungsi yang mirip dengan operator dot (.).

#include <stdio.h>
struct Point {
    int x, y;
    int* ptr;
};

int main()
{
    int val = 50;
    struct Point *coordinate;
    coordinate->x = 5; coordinate->y = 10;
    coordinate->ptr = &val;

    printf("%d %d %d\n", coordinate->x, coordinate->y, *coordinate->ptr);
    
    return 0;
}

Pengelolaan Memory

Layout memory di dalam C

Di dalam program C, lokasi memori itu dibagi menjadi beberapa segment, yaitu:

  1. Code/Text Segment: menyimpan kode program (instruksi).
  2. Data Segment: menyimpan variabel global dan statis yang diinisialisasi.
  3. BSS Segment: menyimpan variabel global dan statis yang tidak diinisialisasi.
  4. Heap Segment: menyimpan memori yang dialokasikan secara dinamis selama eksekusi program.
  5. Stack Segment: menyimpan variabel lokal dan informasi pemanggilan fungsi.

memory_layout

Di dalam modul sebelumnya, setiap kali kita deklarasi sebuah variabel, variabel itu biasanya disimpan dalam stack segment atau data/BSS segment, sedangkan semua kode program disimpan di code/text segment.

#include <stdio.h>

int a = 10; // 'a' itu disimpan di data segment
char b; // 'b' itu disimpan di BSS segment

void function() {
    static int c = 20; // 'c' itu disimpan di data segment

    static char d; // 'd' itu disimpan di BSS segment

    int e; // 'e' itu disimpan di stack segment

    long long int f[1000] = {0}; // 'f' itu disimpan di stack segment
}

// Semua instruksi ini disimpan di code/text segment

Memory Statis vs Memory Dinamis

Perhatikan bahwa heap segment itu tidak ada dibahas dari kode diatas. Itu dikarenakan heap segment itu khusus digunakan untuk ketika kita secara eksplisit mengalokasikan memori selama eksekusi program. Ini dikenal sebagai alokasi memori dinamis.

Memori dinamis berbeda dengan alokasi memori statis dalam banyak hal, seperti:

Aspek Static Memory Allocation Dynamic Memory Allocation
Kecepatan Umumnya lebih cepat Umumnya lebih lambat
Batasan Ukuran Dibatasi oleh ukuran tumpukan yang diinisialisasi (biasanya lebih kecil) Dibatasi oleh memori sistem (biasanya lebih besar)
Ukuran Memori Tetap pada saat kompilasi (yaitu sebelum program dijalankan) Dapat disesuaikan pada saat runtime (yaitu saat program sedang berjalan)
Masa Pakai Memori Dikelola secara otomatis oleh compiler Harus dikelola secara manual oleh programmer
Alokasi Memori Dilakukan secara otomatis saat variabel dideklarasikan Dilakukan secara eksplisit melalui fungsi perpustakaan
Keamanan Lebih sedikit rentan terhadap kebocoran memori Lebih rentan terhadap kebocoran memori jika tidak dikelola dengan baik

Pengunaan Memory Dinamis

Alokasi memori dinamis itu dilakukan dengan fungsi-fungsi yang disediakan oleh library stdlib.h, yaitu:

Fungsi Deskripsi Return Value
malloc(size_t size) Mengalokasikan blok memori berukuran size. Isi memori yang dialokasikan mungkin mengandung nilai acak. Pointer ke blok memori yang dialokasikan atau NULL jika alokasi gagal.
calloc(size_t num, size_t size) Mengalokasikan memori untuk array dengan num elemen, masing-masing berukuran size byte, dan menginisialisasi semua byte dalam penyimpanan yang dialokasikan menjadi nol. Pointer ke blok memori yang dialokasikan atau NULL jika alokasi gagal.
realloc(void *ptr, size_t new_size) Mengubah ukuran blok memori yang ditunjuk oleh ptr menjadi new_size byte. Isi memori akan tetap sama dalam rentang dari awal blok hingga batas minimum antara ukuran lama dan baru. Jika ukuran baru lebih besar, memori tambahan tidak akan diinisialisasi. Pointer ke blok memori yang dialokasikan ulang atau NULL jika alokasi gagal.
free(void *ptr) Membebaskan memori yang sebelumnya dialokasikan oleh panggilan ke malloc, calloc, atau realloc. Fungsi ini tidak mengembalikan nilai. None

Perbedaan antara malloc dan calloc

Baik malloc maupun calloc dapat digunakan secara bergantian untuk mengalokasikan memori. Namun, terdapat beberapa perbedaan kunci antara kedua fungsi tersebut:

Saat menginisialisasi memori, malloc hanya mengalokasikan memori tanpa membersihkan data sebelumnya, artinya memori yang dialokasikan mungkin mengandung nilai sampah.

Saat menginisialisasi memori, calloc mengalokasikan memori dan secara sengaja menginisialisasi semua byte menjadi nol.

Dari sudut pandang yang sederhana, calloc seharusnya selalu diutamakan daripada malloc, karena menjamin bahwa memori yang dialokasikan diinisialisasi menjadi nol. Namun, calloc umumnya lebih lambat daripada malloc karena langkah tambahan dalam menginisialisasi memori.

Menggunakan memory dinamis di C

Penggunaan alokasi memori dinamis yang tepat dalam C melibatkan langkah-langkah berikut:

  1. Sertakan perpustakaan stdlib.h.
  2. Alokasikan memori menggunakan malloc atau calloc.
  3. Gunakan memori yang telah dialokasikan.
    1. Ubah ukuran memori yang telah dialokasikan menggunakan realloc jika diperlukan.
  4. Bebaskan memori yang telah dialokasikan menggunakan free untuk menghindari kebocoran memori.

Contoh sederhana penggunaan alokasi memori dinamis dalam C:

#include <stdio.h>
#include <stdlib.h> // Langkah 1: include library stdlib.h

int main() {
    // Langkah 2: Alokasikan memori untuk array 5 integer
    int* a = (int*) malloc(5 * sizeof(int));
    char* b = (char*) calloc(10, sizeof(char));
    if (a == NULL) {
        printf("Alokasi memori gagal\n");
        return 1; // Keluar jika alokasi memori gagal
        // return 1 menandakan terjadi kesalahan
    }
    if (b == NULL) {
        printf("Alokasi memori gagal\n");
        free(a); // Bebaskan memori yang telah dialokasikan sebelum keluar
        return 1; // return jika salah satu alokasi memori gagal
    }
    // Catatan: Disarankan untuk tidak memeriksa kedua pointer dalam pernyataan if yang sama
    // Alasan akan dijelaskan di bagian Common Pitfalls

    // Langkah 3: Gunakan memori yang dialokasikan
    for (int i = 0; i < 5; i++) {
        a[i] = i + 1;
    }

    for (int i = 0; i < 5; i++) {
        printf("%d ", a[i]);
    }
    
    // Langkah 4: Bebaskan memori yang dialokasikan
    free(a);
    free(b);
    return 0;
}

Common Pitfalls

Saat menggunakan alokasi memori dinamis, ada beberapa jebakan umum yang harus diketahui oleh setiap programmer:

  1. Kebocoran Memori: kebocoran memori adalah segmen memori yang tidak lagi dibutuhkan tetapi tidak dilepaskan kembali ke sistem. Hal ini dapat menyebabkan penggunaan memori yang meningkat karena program mungkin berakhir tetapi memori masih dialokasikan/digunakan, sehingga sebagian dari memori keseluruhan menjadi tidak dapat digunakan. Pastikan selalu bahwa setiap panggilan malloc, calloc, atau realloc disertai dengan panggilan free yang sesuai.
int main() {
    int* a = (int*)malloc(5 * sizeof(int));
    int* b = (int*)malloc(10 * sizeof(int));
    if (a == NULL || b == NULL) { // Pemeriksaan yang salah
        printf(β€œAlokasi memori gagal\n”);
        
        // Dalam situasi ini, memori mana yang harus dibebaskan?
        // Program kita seharusnya dapat mengalokasikan memori untuk a dan b
        // tetapi jika salah satunya gagal, kita tidak tahu mana yang gagal
        free(a); 
        free(b); 
        return 1; 
    }
    // Tidak ada free(a); menyebabkan kebocoran memori
    return 0;
}
  1. Dangling Pointers: Mengakses memori yang telah dibebaskan dapat menyebabkan perilaku yang tidak terdefinisi. Perilaku yang tidak terdefinisi dapat menyebabkan hasil yang tidak terduga, crash, atau kerentanan keamanan. Selalu atur penunjuk ke NULL setelah membebaskannya untuk menghindari penunjuk yang tergantung.
int main() {
    int* a = (int*)malloc(5 * sizeof(int));
    // ... beberapa operasi pada β€˜a’
    free(a);
    // ... beberapa operasi pada β€˜a’ (Dangling Pointer)
    return 0;
}
  1. Double Free: Mencoba membebaskan blok memori yang sama lebih dari sekali dapat menyebabkan perilaku yang tidak terdefinisi, program crash, atau kerusakan pada tumpukan memori. Pastikan selalu bahwa sebuah pointer hanya dibebaskan sekali.
int* a = (int*)malloc(5 * sizeof(int));
// ... beberapa operasi pada β€˜a’
free(a);
free(a); // Kesalahan Double Free
  1. Buffer Overflow: Alokasi memori yang tidak cukup untuk struktur data dapat menyebabkan buffer overflow, yang dapat menimbulkan kerentanan keamanan dan kegagalan program.
char *buffer = (char *)malloc(10 * sizeof(char));

if (buffer == NULL) {
  printf(β€œAlokasi memori gagal\n”);
  return 1;
}

// Mencoba copy string yang lebih panjang dari buffer yang dialokasikan
// ini akan menyebabkan buffer overflow dari heap
strcpy(buffer, β€œIni adalah string yang jelas lebih panjang dari 10 byte.”);

printf(β€œIsi buffer: %s\n”, buffer); // Undefined behaviour --> error atau crash

free(buffer); // free memory yang dialokasikan

Soal Latihan

Soal 1

Implementasikan fungsi bernama tambah berisi 3 parameter, di mana parameter pertama merupakan bilangan 1, parameter kedua merupakan bilangan 2, dan parameter terakhir adalah variabel tempat hasil output.

Contoh pemanggilan:

int a = 1;
int b = 2;
int c;
tambah(a, b, &c);
printf(β€œ%d”, c);

Output-nya:

3

Soal 2

Buatlah struct untuk menyimpan data nilai UN mahasiswa yang berisi nama, nilai Matematika, nilai IPA, nilai Bahasa Indonesia, dan nilai Bahasa Inggris. Setelah itu buat program yang dapat memasukkan list data nilai UN lalu menampilkan data sesuai nama.

Keterangan: urutan pemasukan nilai adalah Matematika, IPA, Bahasa Indonesia, Bahasa Inggris. Berikut merupakan contoh input dan output. 4 kelompok data di awal merupakan jumlah data nilai UN yang akan dimasukkan. Angka 3 di akhir merupakan jumlah nama yang akan dicari.

Sample Input

4
Hope
100
90
20
90
Ricky
80
70
80
90
Maden
100
100
100
100
Tenten
90
80
99
100
3
Maden
Dennis
Tenten

Sample Output

Nilai Maden 
Matematika : 100
IPA : 100
Bahasa Indonesia : 100
Bahasa Inggris : 100
Nilai Dennis tidak ditemukan
Nilai Tenten
Matematika : 90
IPA : 80
Bahasa Indonesia : 99
Bahasa Inggris : 100

Soal 3

Buatlah fungsi bernama reverse() untuk me-reverse array of integer menggunakan pointer. Fungsinya dapat digunakan seperti berikut.

int arr[5]
.
.
//input

reverse(arr, 5);
.
.
//print isi arr

Sample Input:

5
8 4 2 3 1

Sample Output:

1 3 2 4 8
⚠️ **GitHub.com Fallback** ⚠️