C++ Pointer là gì? Học về biến Pointer trong C++ > Iron Hack Việt Nam

1. Biến Pointer trong C++

Trong C++, pointer (con trỏ) là biến lưu địa chỉ bộ nhớ của các biến khác.

1.1. Địa chỉ trong C++

1.1. Địa chỉ trong C++

Nếu ta có một biến var trong chương trình của mình, &var sẽ cung cấp cho chúng ta địa chỉ của nó trong bộ nhớ. Ví dụ:

Ví dụ 1: Xuất địa chỉ biến

#include <iostream>

using namespace std;

 

int main()

{

    // declare variables

    int var1 = 3;

    int var2 = 24;

    int var3 = 17;

 

    // print address of var1

    cout << “Address of var1: “<< &var1 << endl;

 

    // print address of var2

    cout << “Address of var2: “ << &var2 << endl;

 

    // print address of var3

    cout << “Address of var3: “ << &var3 << endl;

}

Đầu ra

Address of var1: 0x7fff5fbff8ac

Address of var2: 0x7fff5fbff8a8

Address of var3: 0x7fff5fbff8a4

Ở đây, 0x ở đầu đại diện cho địa chỉ ở dạng thập lục phân.

Chú ý rằng địa chỉ đầu tiên khác địa chỉ thứ hai 4 byte và địa chỉ thứ hai khác địa chỉ thứ ba 4 byte. Đó là do kích thước của một biến int là 4 byte trong hệ thống 64 bit.

Lưu ý: Bạn có thể không nhận được kết quả tương tự khi bạn chạy chương trình.

1.2. Pointers trong C++

Như đã đề cập ở trên, pointer được sử dụng để lưu trữ địa chỉ hơn là giá trị.

Đây là cách chúng ta có thể khai báo pointer:

int *pointVar;

Ở đây, chúng ta đã khai báo biến pointer là pointerVar của kiểu int.

Ta cũng có thể khai báo biến pointer theo cách bên dưới:

int* pointVar; // preferred syntax

Lấy một ví dụ khác về khai báo biến pointer:

int* pointVar, p;

Ở đây, tôi đã khai báo một biến pointer là pointVar và một biến bình thường p.

Lưu ý: toán tử * được sử dụng sau kiểu dữ liệu để khai báo pointer.

Nhận giá trị từ địa chỉ bằng cách sử dụng pointer

Để nhận giá trị được chỉ bởi pointer, ta sử dụng toán tử *. Ví dụ:

int* pointVar, var;

var = 5;

 

// assign address of var to pointVar

pointVar = &var;

 

// access value pointed by pointVar

cout << *pointVar << endl;   // Output: 5

Trong đoạn code trên, địa chỉ của var được gán cho pointVar. Tôi đã sử dụng * pointVar để lấy giá trị được lưu trữ trong địa chỉ đó.

Khi * được sử dụng với pointer, nó được gọi là toán tử tham chiếu. Nó hoạt động trên một pointer và cung cấp giá trị được chỉ bởi địa chỉ được lưu trữ trong pointer. Đó là *pointVar = var.

Lưu ý: Trong C++, pointVar và * pointVar hoàn toán khác nhau. Chúng ta không thể cho *pointVar = &var;

Ví dụ 2: Hoạt động của Pointers trong C++

#include <iostream>

using namespace std;

int main() {

    int var = 5;

 

    // declare pointer variable

    int* pointVar;

 

    // store address of var

    pointVar = &var;

 

    // print value of var

    cout << “var = “ << var << endl;

 

    // print address of var

    cout << “Address of var (&var) = “ << &var << endl

         << endl;

 

    // print pointer pointVar

    cout << “pointVar = “ << pointVar << endl;

 

    // print the content of the address pointVar points to

    cout << “Content of the address pointed to by pointVar (*pointVar) = “ << *pointVar << endl;

    

    return 0;

}

Đầu ra

var = 5

Address of var (&var) = 0x61ff08

 

pointVar = 0x61ff08

Content of the address pointed to by pointVar (*pointVar) = 5

https://cdn.programiz.com/sites/tutorial2program/files/cpp-pointer-working.png

Thay đổi giá trị được chỉ bởi Pointers

Nếu pointVar chỉ đến địa chỉ của var, chúng ta có thể thay đổi giá trị của var bằng cách sử dụng *pointVar.

Ví dụ:

int var = 5;

int* pointVar;

 

// assign address of var

pointVar = &var;

 

// change value at address pointVar

*pointVar = 1;

 

cout << var << endl; // Output: 1

Ở đây, pointVar và &var có cùng địa chỉ, giá trị của var cũng sẽ bị thay đổi khi * pointVar được thay đổi.

Ví dụ 3: Thay đổi giá trị được chỉ bởi Pointers

#include <iostream>

using namespace std;

int main() {

    int var = 5;

    int* pointVar;

 

    // store address of var

    pointVar = &var;

 

    // print var

    cout << “var = “ << var << endl;

 

    // print *pointVar

    cout << “*pointVar = “ << *pointVar << endl

         << endl;

 

    cout << “Changing value of var to 7:” << endl;

 

    // change value of var to 7

    var = 7;

 

    // print var

    cout << “var = “ << var << endl;

 

    // print *pointVar

    cout << “*pointVar = “ << *pointVar << endl

         << endl;

 

    cout << “Changing value of *pointVar to 16:” << endl;

 

    // change value of var to 16

    *pointVar = 16;

 

    // print var

    cout << “var = “ << var << endl;

 

    // print *pointVar

    cout << “*pointVar = “ << *pointVar << endl;

    return 0;

}

Đầu ra

var = 5

*pointVar = 5

 

Changing value of var to 7:

var = 7

*pointVar = 7

 

Changing value of *pointVar to 16:

var = 16

*pointVar = 16

1.3. Những lỗi thường gặp khi làm việc với pointers

Giả sử, chúng ta muốn một pointer là varPoint trỏ đến địa chỉ của var. Sẽ có những trường hợp như sau:

 

int var, *varPoint;

 

// Wrong! 

// varPoint is an address but var is not

varPoint = var;

 

// Wrong!

// &var is an address

// *varPoint is the value stored in &var

*varPoint = &var;

 

// Correct! 

// varPoint is an address and so is &var

varPoint = &var;

 

 // Correct!

// both *varPoint and var are values

*varPoint = var;

  1. Pointers và Mảng (Array)

Trong C++, pointers là biến giữ địa chỉ của các biến khác. Một pointer không chỉ có thể lưu trữ địa chỉ của một biến đơn lẻ, mà nó còn có thể lưu trữ địa chỉ của các ô trong một mảng.

Hãy xem ví dụ sau:

int *ptr;

int arr[5];

 

// store the address of the first

// element of arr in ptr

ptr = arr;

Ở đây, ptr là một biến pointer trong khi arr là một mảng int. Mã ptr = arr; lưu trữ địa chỉ của phần tử đầu tiên của mảng trong biến ptr.

Lưu ý rằng tôi đã sử dụng arr thay vì arr [0]. Điều này là do cả hai đều giống nhau. Vì vậy, mã bên dưới giống như mã bên trên.

int *ptr;

int arr[5];

ptr = &arr[0];

Địa chỉ cho phần còn lại của các phần tử mảng được cung cấp bởi &arr[1], &arr[2], &arr[3] và &arr[4].

Trỏ đến mọi phần tử của mảng

Giả sử chúng ta cần trỏ đến phần tử thứ tư của mảng bằng cách sử dụng cùng một pointer ptr.

Nếu ptr trỏ đến phần tử đầu tiên trong ví dụ trên thì ptr + 3 sẽ trỏ đến phần tử thứ tư. Ví dụ:

int *ptr;

int arr[5];

ptr = arr;

 

ptr + 1 is equivalent to &arr[1];

ptr + 2 is equivalent to &arr[2];

ptr + 3 is equivalent to &arr[3];

ptr + 4 is equivalent to &arr[4];

Tương tự, ta có thể truy cập các phần tử bằng pointer đơn. Ví dụ:

// use dereference operator

*ptr == arr[0];

*(ptr + 1) is equivalent to arr[1];

*(ptr + 2) is equivalent to arr[2];

*(ptr + 3) is equivalent to arr[3];

*(ptr + 4) is equivalent to arr[4];

Giả sử, nếu chúng ta đã khởi tạo ptr = &arr[2], thì:

ptr – 2 is equivalent to &arr[0];

ptr – 1 is equivalent to &arr[1]; 

ptr + 1 is equivalent to &arr[3];

ptr + 2 is equivalent to &arr[4];

https://cdn.programiz.com/sites/tutorial2program/files/cpp-pointers-and-arrays.png

Lưu ý: Địa chỉ giữa ptr và ptr + 1 khác nhau 4 byte. Đó là bởi vì ptr là một pointer trỏ đến dữ liệu int. Và kích thước của int là 4 byte trong hệ điều hành 64 bit.

Tương tự, nếu pointer ptr đang trỏ đến dữ liệu kiểu char, thì địa chỉ giữ ptr và ptr + 1 là 1 byte. Đó là vì kích thước của một ký tự là 1 byte.

Ví dụ 1: Pointers và mảng trong C++

// C++ Program to display address of each element of an array 

 

#include <iostream>

using namespace std;

 

int main()

{

    float arr[3];

 

    // declare pointer variable

    float *ptr;

    

    cout << “Displaying address using arrays: “ << endl;

 

    // use for loop to print addresses of all array elements

    for (int i = 0; i < 3; ++i)

    {

        cout << “&arr[“ << i << “] = “ << &arr[i] << endl;

    }

 

    // ptr = &arr[0]

    ptr = arr;

 

    cout<<“\nDisplaying address using pointers: “<< endl;

 

    // use for loop to print addresses of all array elements

    // using pointer notation

    for (int i = 0; i < 3; ++i)

    {

        cout << “ptr + “ << i << ” = “<< ptr + i << endl;

    }

 

    return 0;

}

Đầu ra

Displaying address using arrays: 

&arr[0] = 0x61fef0

&arr[1] = 0x61fef4

&arr[2] = 0x61fef8

 

Displaying address using pointers: 

ptr + 0 = 0x61fef0

ptr + 1 = 0x61fef4

ptr + 2 = 0x61fef8

Trong chương trình trên, đầu tiên tôi cần xuất địa chỉ của các phần tử mảng mà không cần sử dụng biến pointer ptr.

Sau đó, tôi sử dụng pointer ptr để trỏ đến địa chỉ của [0], ptr + 1 để trỏ đến địa chỉ của [1], v.v…

Trong đầu hết các ngữ cảnh, tên mảng phân rã thành pointer. Nói đơn giản, tên mảng được chuyển đổi thành pointer. Đó là lý do vì sao ta có thể sử dụng pointer để truy cập các phần tử của mảng.

Tuy nhiên, bạn cần nhớ rằng con trỏ và mảng không giống nhau. Có một số trường hợp tên mảng không phân rã thành pointer.

Ví dụ 2: Tên mảng được sử dụng như pointer

// C++ Program to insert and display data entered by using pointer notation.

 

#include <iostream>

using namespace std;

 

int main() {

    float arr[5];

    

   // Insert data using pointer notation

    cout << “Enter 5 numbers: “;

    for (int i = 0; i < 5; ++i) {

 

        // store input number in arr[i]

        cin >> *(arr + i) ;

 

    }

 

    // Display data using pointer notation

    cout << “Displaying data: “ << endl;

    for (int i = 0; i < 5; ++i) {

 

        // display value of arr[i]

        cout << *(arr + i) << endl ;

 

    }

 

    return 0;

}

Đầu ra

Enter 5 numbers: 2.5

3.5

4.5

5

2

Displaying data: 

2.5

3.5

4.5

5

2

Ở đây:

  • Đầu tiên, tôi sử dụng ký hiệu pointer để lưu trữ các số được người dùng nhập vào mảng arr.

cin >> *(arr + i) ;

Code này tương ứng với code bên dưới:

cin >> arr[i];

Lưu ý rằng tôi đã không khai báo một biến pointer riêng biệt, mà tôi đang sử dụng tên mảng arr cho ký hiệu pointer.

Như ta đã biết, tên mảng arr trỏ đến phần tử đầu tiên của mảng. Vì vậy, chúng ta có thể coi arr hoạt động như một pointer.

  • Tượng tự, sau đó tôi sử dụng vòng lặp for để hiển thị các giá trị của arr bằng cách sử dụng ký hiệu pointer.

cout << *(arr + i) << endl ;

Code này tương đương với:

cout << arr[i] << endl ;

  1. Pointers và hàm trong C++

Ở hướng dẫn về hàm trong C++, chúng ta đã tìm hiểu về cách truyền các đối số cho một hàm. Phương thức này được sử dụng gọi là truyền theo giá trị vì giá trị thực được truyền.

Tuy nhiên, có một cách khác để truyền đối số cho một hàm mà giá trị thực của đối số không được truyền. Thay vào đó, tham chiếu đến các giá trị được truyền.

Ví dụ:

// function that takes value as parameter

 

void func1(int numVal) {

    // code

}

 

// function that takes reference as parameter

// notice the & before the parameter

void func2(int &numRef) {

    // code

}

 

int main() {

    int num = 5;

 

    // pass by value

    func1(num);

 

    // pass by reference

    func2(num);

 

    return 0;

}

Lưu ý về & trong void func2(int &numRef). Nó thể hiện rằng tôi đang sử dụng địa chỉ của biến làm tham số của tôi.

Vì vậy, khi tôi gọi hàm func2() trong hàm main() bằng cách chuyển biến num làm đối số, tôi đã chuyển địa chỉ của biến num thay vì giá trị 5.

https://cdn.programiz.com/sites/tutorial2program/files/cpp-pass-by-reference.png

Ví dụ 1: Truyền qua tham chiếu không có pointers

#include <iostream>

using namespace std;

 

// function definition to swap values

void swap(int &n1, int &n2) {

    int temp;

    temp = n1;

    n1 = n2;

    n2 = temp;

}

 

int main()

{

 

    // initialize variables

    int a = 1, b = 2;

 

    cout << “Before swapping” << endl;

    cout << “a = “ << a << endl;

    cout << “b = “ << b << endl;

 

    // call function to swap numbers

    swap(a, b);

 

    cout << “\nAfter swapping” << endl;

    cout << “a = “ << a << endl;

    cout << “b = “ << b << endl;

 

    return 0;

}

Đầu ra

Before swapping

a = 1

b = 2

 

After swapping

a = 2

b = 1

Trong chương trình này, tôi đã chuyển các biến a và b vào hàm swap(). Chú ý đến định nghĩa hàm:

void swap(int &n1, int &n2)

Ở đây, tôi đang sử dụng & để biểu thị rằng hàm sẽ chấp nhận địa chỉ làm tham số của nó.

Do đó, trình biên dịch có thể xác định rằng, thay vì các giá trị thực, tham chiếu của các biến sẽ được chuyển cho các tham số hàm.

Trong hàm swap(), các tham số của hàm n1 và n2 trỏ đến cùng giá trị với các biến a và b tương ứng. Do đó, việc hoán đổi diễn ra trên giá trị thực tế.

Tác vụ tương tự có thể được thực hiện bằng cách sử dụng pointer.

Ví dụ 2: Truyền qua tham chiếu sử dụng pointers

#include <iostream>

using namespace std;

 

// function prototype with pointer as parameters

void swap(int*, int*);

 

int main()

{

 

    // initialize variables

    int a = 1, b = 2;

 

    cout << “Before swapping” << endl;

    cout << “a = “ << a << endl;

    cout << “b = “ << b << endl;

 

    // call function by passing variable addresses

    swap(&a, &b);

 

    cout << “\nAfter swapping” << endl;

    cout << “a = “ << a << endl;

    cout << “b = “ << b << endl;

    return 0;

}

 

// function definition to swap numbers

void swap(int* n1, int* n2) {

    int temp;

    temp = *n1;

    *n1 = *n2;

    *n2 = temp;

}

Đầu ra

Before swapping

a = 1

b = 2

 

After swapping

a = 2

b = 1

Ở đây, ta có thể thấy đầu ra giống như ví dụ trước. Lưu ý dòng:

// &a is address of a

// &b is address of b

swap(&a, &b);

Tại đây, địa chỉ của biến được chuyển trong quá trình gọi hàm chứ không phải biến.

Vì địa chỉ được chuyển thay vì giá trị, nên một toán tử tham chiếu * phải được sử dụng để truy cập giá trị được lưu trữ trong địa chỉ đó.

temp = *n1;

*n1 = *n2;

*n2 = temp;

*n1 và *n2 cho giá trị được lưu trữ tại địa chỉ n1 và n2.

Vì n1 và n2 chứa địa chỉ của a và b, nên bất thao tác được thực hiện với *n1 và *n2 sẽ thay đổi giá trị thực của a và b.

Do đó, khi ta xuất giá trị của a và b trong hàm main(), các giá trị sẽ bị thay đổi.

  1. Quản lý bộ nhớ trong C++

C++ cho phép chúng ta cấp phát bộ nhớ của một biến hoặc một mảng trong thời gian chạy. Đây được gọi là cấp phát bộ nhớ động.

Trong các ngôn ngữ lập trình khác như Java và Python, trình biên dịch tự động quản lý các bộ nhớ được cấp phát cho biến. Nhưng trong C++ lại không giống vậy.

Trong C++, chúng ta cần phân bổ vùng nhớ được cấp phát động theo cách thủ công sau khi không sử dụng biến.

Ta có thể cấp phát và sau đó phân bổ bộ nhớ động bằng cách sử dụng các toán tử new và delete tương ứng.

4.1. Toán tử new trong C++

Toán tử new cấp phát bộ nhớ cho một biến. Ví dụ:

// declare an int pointer

int* pointVar;

 

// dynamically allocate memory

// using the new keyword 

pointVar = new int;

 

// assign value to allocated memory

*pointVar = 45;

Ở đây, tôi đã cấp phát động bộ nhớ cho một biến int bằng toán tử new.

Lưu ý rằng tôi đã sử dụng pointer pointVar để cấp phát bộ nhớ động, do toán tử new trả về địa chỉ của vị trí trị bộ nhớ.

Trong trường hợp của một mảng, toán tử new trả về địa chỉ của phần tử đầu tiên của mảng.

Từ ví dụ trên, ta có thể thấy cú pháp để sử dụng toán tử new là:

pointerVariable = new dataType;

4.2. Toán tử delete trong C++

Khi ta không còn cần sử dụng một biến mà ta đã khai báo động nữa, chúng ta có thể phân bổ bộ nhớ bị chiếm bởi biến bằng cách sử dụng toán tử delete.

Nó sẽ trả lại bộ nhớ cho hệ điều hành, được gọi là sự phân bổ bộ nhớ.

Cú pháp cho toán tử delete là:

delete pointerVariable;

Xem xét đoạn code:

// declare an int pointer

int* pointVar;

 

// dynamically allocate memory

// for an int variable 

pointVar = new int;

 

// assign value to the variable memory

*pointVar = 45;

 

// print the value stored in memory

cout << *pointVar; // Output: 45

 

// deallocate the memory

delete pointVar;

Ở đây, tôi đã cấp phát bộ nhớ động cho một biến int bằng các sử dụng biến pointer pointVar.

Sau khi xuất nội dung của pointVar, tôi đã phân bổ bộ nhớ bằng cách sử dụng delete.

Lưu ý: Nếu chương trình sử dụng một lượng lớn bộ nhớ không mong muốn khi dùng new, hệ thống có thể bị treo vì không còn bộ nhớ cho hệ điều hành. Trong trường hợp này, toán tử delete có thể giúp hệ thống khỏi bị treo.

Ví dụ 1: Phân bổ bộ nhớ động trong C++

#include <iostream>

using namespace std;

 

int main() {

    // declare an int pointer

    int* pointInt;

 

    // declare a float pointer

    float* pointFloat;

 

    // dynamically allocate memory

    pointInt = new int;

    pointFloat = new float;

 

    // assigning value to the memory

    *pointInt = 45;

    *pointFloat = 45.45f;

 

    cout << *pointInt << endl;

    cout << *pointFloat << endl;

 

    // deallocate the memory

    delete pointInt, pointFloat;

 

    return 0;

}

Đầu ra

45

45.45

Trong chương trình này, tôi đã cấp phát động bộ nhớ cho hai biến kiểu int và float. Sau khi gán các giá trị cho chúng và xuất chúng, tôi đã phân bổ các bộ nhớ bằng cách sử dụng code:

delete pointInt, pointFloat;

Lưu ý: Phân bổ bộ nhớ động có thể giúp quản lý bộ nhớ hiệu quả hơn. Đặc biệt là đối với mảng, nơi mà rất nhiều lần chúng ta không biết kích thước của mảng cho đến khi chạy.

Ví dụ 2: Toán tử new và delete cho mảng

// C++ Program to store GPA of n number of students and display it

// where n is the number of students entered by the user

 

#include <iostream>

#include <cstring>

using namespace std;

 

int main() {

    int num;

    cout << “Enter total number of students: “;

    cin >> num;

    float* ptr;

    

    // memory allocation of num number of floats

    ptr = new float[num];

 

    cout << “Enter GPA of students.” << endl;

    for (int i = 0; i < num; ++i) {

        cout << “Student” << i + 1 << “: “;

        cin >> *(ptr + i);

    }

 

    cout << “\nDisplaying GPA of students.” << endl;

    for (int i = 0; i < num; ++i) {

        cout << “Student” << i + 1 << ” :” << *(ptr + i) << endl;

    }

 

    // ptr memory is released

    delete [] ptr;

 

    return 0;

}

Đầu ra

Enter total number of students: 4

Enter GPA of students.

Student1: 3.6

Student2: 3.1

Student3: 3.9

Student4: 2.9

 

Displaying GPA of students.

Student1 :3.6

Student2 :3.1

Student3 :3.9

Student4 :2.9

Trong chương trình này, tôi đã yêu cầu người dùng nhập số lượng sinh viên và lưu trữ nó trong biến num.

Sau đó, tôi đã cấp phát bộ nhớ động cho mảng float bằng cách sử dụng new.

Tôi nhập dữ liệu vào mảng (và sau đó xuất chúng) bằng cách sử dụng ký hiệu pointer.

Khi không cần mảng nữa, tôi phân bổ bộ nhớ mảng bằng cách sử dụng code delete [ ] ptr;

Lưu ý: Việc sử dụng [ ] sau delete để biểu thị rằng vị trí thỏa thuận bộ nhớ là của một mảng.

Ví dụ 3: Toán tử new và delete cho đối tượng trong C++

#include <iostream>

using namespace std;

 

class Student {

    int age;

 

   public:

 

    // constructor initializes age to 12

    Student() : age(12) {}

 

    void getAge() {

        cout << “Age = “ << age << endl;

    }

};

 

int main() {

 

    // dynamically declare Student object

    Student* ptr = new Student();

 

    // call getAge() function

    ptr->getAge();

 

    // ptr memory is released

    delete ptr;

 

    return 0;

}

Đầu ra

Age = 12

Trong chương trình này, tôi đã tạo một lớp (class) Student có biến riêng là age.

Tôi đã khởi tạo age là 12 trong hàm khởi tạo Student() và xuất giá trị của nó với hàm getAge().

Trong main(), tôi tạo đối tượng Student bằng toán tử new và sử dụng pointer ptr để trỏ đến địa chỉ của nó.

Thời điểm đối tượng được tạo, hàm khởi tạo Student() khởi tạo tuổi thành 12.

Sau đó, tôi gọi hàm getAge() bằng cách dùng code:

ptr->getAge();

Lưu ý: Toán tử mũi tên -> được dùng để truy cập các thành phần trong lớp (class) bằng cách sử dụng pointers.