1 of 24

Intro C++ programming language

Authors: Trần Đức Mạnh, Trần Đức Khánh, Nguyễn Ngọc Đức, Lê Ngọc Thiện

Buổi 5: Con trỏ

2 of 24

Bản chất của Biến (Variables):

Khái niệm về biến, giá trị của biến, địa chỉ của biến

int digit = 42;

RAM

  • Truy cập địa chỉ: &&digit
  • Truy cập giá trị thông qua địa chỉ: **(&digit)

3 of 24

Hằng, biến cục bộ, biến toàn cục

  1. Hằng (constant)

3. Biến toàn cục (Global variables)

2. Biến cục bộ (Local variables)

Biến được định nghĩa bên trong khối lệnh được gọi là các biến cục bộ (local variables). Những biến này chỉ có thể được truy cập bên trong các khối lệnh mà nó được định nghĩa (bao gồm các khối lệnh lồng nhau), và bị hủy ngay sau khi các khối lệnh kết thúc.

Hằng (constant) là từ chỉ những thứ không thay đổi và lặp đi lặp lại.

const <kiểu dữ liệu> <tên biến> = <giá trị>;

hoặc

<kiểu dữ liệu> const <tên biến> = <giá trị>;

Các biến khai báo bên ngoài của khối lệnh được gọi là biến toàn cục. Biến toàn cục có thời gian tĩnh, nghĩa là chúng được tạo ra khi chương trình bắt đầu và bị hủy khi nó kết thúc. Các biến toàn cục có phạm vi tập tin (file scope), hay gọi là "phạm vi toàn cầu" (global scope).

4 of 24

Con trỏ:

  • Định nghĩa

2. Các khái niệm về con trỏ

Là biến trỏ tới 1 địa chỉ khác, tức giá trị nó lưu là 1 địa chỉ của 1 ô nhớ khác.

RAM

  • Giá trị của con trỏ: địa chỉ mà con trỏ trỏ đến.
  • Địa chỉ của con trỏ: Địa chỉ của bản thân biến con trỏ đó
  • Giá trị của biến nơi con trỏ đang trỏ tới
  • Địa chỉ của biến nơi con trỏ đang trỏ tới = giá trị của con trỏ.

“Con trỏ thường được nêu ra trong những cuộc tranh cãi là phần khó hiểu nhất của C++. Nhưng nó lại là tính năng mà khiến C++ trở thành một ngôn ngữ mạnh mẽ.”

5 of 24

Con trỏ:

2. Các khái niệm về con trỏ

3. Làm việc với con trỏ

RAM

  • Giá trị của con trỏ: địa chỉ mà con trỏ trỏ đến.
  • Địa chỉ của con trỏ: Địa chỉ của bản thân biến con trỏ đó
  • Giá trị của biến nơi con trỏ đang trỏ tới
  • Địa chỉ của biến nơi con trỏ đang trỏ tới = giá trị của con trỏ.
  • Cách khai báo: ` <kiểu dữ liệu> * <tên biến>`

int *ptr_int; // khai báo con trỏ để trỏ tới biến kiểu int

char *ptr_char; // khai báo con trỏ để trỏ tới biến kiểu char

float *ptr_float;//khai báo con trỏ để trỏ tới biến kiểu float

6 of 24

Con trỏ:

3. Làm việc với con trỏ

  • Cách khai báo: ` <kiểu dữ liệu> * <tên biến>`
  • Cách gán giá trị :

Ví dụ:

int *ptr; // khai báo con trỏ

int val = 5; // khai báo biến digit mang giá trị là 42

ptr = &val; // gán giá trị của con trỏ = địa chỉ của biến digit

Hoặc:

char c = ‘a’; // khai báo biến c kiểu char mang giá trị là ‘a’

char *ptr = &c; // khai báo con trỏ kiểu char đồng thời khởi tạo giá trị của nó bằng địa chỉ của biến c

Lưu ý:

- Con trỏ được khai báo là kiểu dữ liệu gì thì chỉ có thể trỏ tới biến có cùng kiểu giá trị đó� (trừ con trỏ mang kiểu void mà chúng ta sẽ đề cập ngay sau đây)

7 of 24

Con trỏ:

3. Làm việc với con trỏ

  • Tham chiếu ngược: `*[tên_con_trỏ]`

Ví dụ:

#include <iostream>

using namespace std;

int main(){

int digit = 42;//khai báo biến kiểu int mang giá trị là 42

int *ptr = &digit; // khai báo con trỏ kiểu int đồng thời khởi tạo giá trị của nó bằng địa chỉ của biến digit

cout << "Gia tri khi tham chieu nguoc = "<< *ptr<<"\n";

cout<< "Gia tri bien ptr: " << ptr<< "\n";

cout<< "Gia tri dia chi bien digit: " << &digit<< "\n";

}

8 of 24

Con trỏ:

4. Con trỏ đặc biệt

  • Con trỏ rác (Null Pointer):

Hãy ghi nhớ rằng chúng ta không nên để một con trỏ là rác ( tức là không được khởi tạo giá trị). Một con trỏ rác là con trỏ không trỏ tới cái gì cả, nếu bạn sử dụng nó thì nó sẽ trỏ tới 1 địa chỉ ` ngẫu nhiên ` nào đó và sẽ thật là nguy hiểm nếu địa chỉ đó đang được sử dụng với 1 mục đích khác

int *ptr = NULL;

Hoặc

int *ptr = nullptr;

int *null_ptr; // con tro rac

int digit = 42;

int *ptr = &digit;

  • Con trỏ Void (Void Pointer):

Một con trỏ void được sử dụng để trỏ tới biến có bất kỳ kiểu dữ liệu nào. Nó có thể tái sử dụng với bất kỳ biến nào mà chúng ta muốn

void *ptr_void = nullptr;

void *ptr_void = nullptr;

int number = 54;

ptr_void = &number;

printf("Gia tri cua number = %d\n", *ptr_void); // bien dich loi

printf("Gia tri cua number = %d\n", *(int *)ptr_void);

9 of 24

Con trỏ:

5. Bản chất của con trỏ

  • Con trỏ có thể thay đổi trực tiếp giá trị của biến mà nó đang trỏ tới

void setValueToFive(int x)

{

std::cout << &x << "\n";

x = 5;

}

int x = 3;

setValueToFive( x );

std::cout << "The value of x is " << x \n";

// Outputs: The value of x is 3

void setPointerValueToFive(int *x)

{

*x = 5;

}

int x = 3;

setPointerValueToFive( &x );

std::cout << "The value of x is " << x ;

// Outputs: The value of x is 5

void setValueToFiveWithReference(int& x)

{

x = 5;

}

int x = 3;

setPointerValueToFive( &x );

std::cout << "The value of x is " << x ;

// Outputs: The value of x is 5

  • Khi thay đổi giá trị của biến, thì rõ ràng nếu tồn tại con trỏ ptr trỏ tới biến đó thì *ptr cũng sẽ thay đổi theo giá trị của biến.

10 of 24

Con trỏ:

6. Các lỗi thường gặp

  • Nhầm lẫn giữa địa chỉ và giá trị. Con trỏ là biến trỏ tới địa chỉ, không phải giá trị

int number = 54;

int *ptr = number; // sai vi number la gia tri

int *ptr = &number; // dung vi number la dia chi

  • Có thể các bạn sẽ không phân biệt được dấu * khi khai báo con trỏ và khi truy cập vào giá trị của địa chỉ mà con trỏ đang trỏ tới.

int number = 54;

int *ptr = &number; // *ptr mang y nghia la khai bao con tro

*ptr = 100; // *ptr mang y nghia la truy cap gia tri cua dia chi ma con tro dang tro toi

11 of 24

Truyền giá trị cho Hàm

Nhắc lại về hàm:

Gọi hàm bằng tham trị tức là truyền bản sao của biến vào hàm để xử lý. Bản sao của một biến mang giá trị bằng giá trị của biến đó.

12 of 24

Truyền giá trị cho Hàm

Nhắc lại về hàm:

#include <stdio.h>

int multiply(int x, int y){

int z;

z = x * y;

return z;

}

main(){

int x = 3, y = 5;

int product = multiply(x,y);

printf("Product = %d\n", product);

/* prints "Product = 15" */

}

13 of 24

Truyền giá trị cho Hàm

Gọi hàm bằng tham chiếu:

#include <stdio.h>

int multiply(int *x, int *y){

int z;

z = (*x) * (*y);

return z;

}

main(){

int x = 3, y = 5;

int product = multiply(&x,&y);

printf("Product = %d\n", product);

/* prints "Product = 15" */

}

Gọi hàm bằng tham chiếu. Chúng ta truyền địa chỉ hoặc tham chiếu của biến vào hàm, lúc này không có bản sao chép nào được tạo cả, đảm bảo việc không bị lãng phí bộ nhớ. Chúng ta có thể truy cập vào giá trị lưu trong các địa chỉ này bằng toán tử tham chiếu ngược `*`.

14 of 24

Con trỏ và mảng

  • Con trỏ với mảng có mối quan hệ gần gũi và mạnh mẽ với nhau.
  • Điểm mạnh của con trỏ khi thao tác với mảng:
  • Nhanh hơn
  • Tối ưu hơn về mặt bộ nhớ
  • Khả năng cấp phát động

15 of 24

Con trỏ và mảng 1 chiều

Truy cập phần tử của mảng khi sử dụng chỉ số:

int prime[5] = {2,3,5,7,11};

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

{

printf("Chi so = %d, Dia chi= %d, Gia tri= %d\n", i, &prime[i], prime[i]);

}

Output:

Chi so= 0, Dia chi= 6422016, Gia tri= 2

Chi so= 1, Dia chi= 6422020, Gia tri= 3

Chi so= 2, Dia chi= 6422024, Gia tri= 5

Chi so= 3, Dia chi= 6422028, Gia tri= 7

Chi so= 4, Dia chi= 6422032, Gia tri= 11

16 of 24

Con trỏ và mảng 1 chiều

Điều gì xảy ra nếu chúng ta viết int myArrray[5]?

Đáp án:

  • 5 khối bộ nhớ liên tiếp bắt đầu từ myArray[0] tới myArray[4] được tạo ra với các giá trị rác.
  • Mỗi khối bộ nhớ có kích thước là 4 bytes.

Hãy cùng xem xét ví dụ sau:

int prime[5] = {2,3,5,7,11};

printf("Ket qua khi dung &prime = %d\n",&prime);

printf("Ket qua khi dung prime = %d\n",prime);

printf("Ket qua khi dung &prime[0] = %d\n",&prime[0]);

/* Output */

Ket qua khi dung &prime = 6422016

Ket qua khi dung prime = 6422016

Ket qua khi dung &prime[0] = 6422016

Như vậy, &prime, prime và &prime[0] tất cả cùng trỏ đến một địa chỉ !

17 of 24

Con trỏ và mảng 1 chiều

Hãy cùng cộng vào mỗi con trỏ &prime, prime, và &prime[0] thêm 1

printf("Ket qua khi dung &prime = %d\n",&prime + 1);

printf("Ket qua khi dung prime = %d\n",prime + 1);

printf("Ket qua khi dung &prime[0] = %d\n",&prime[0] + 1);

/* Output */

Ket qua khi dung &prime = 6422036

Ket qua khi dung prime = 6422020

Ket qua khi dung &prime[0] = 6422020

Nhận xét:

  • prime + 1 và &prime[0] + 1 vẫn bằng nhau.
  • &prime + 1 mang giá trị khác

Giải thích:

  • prime là một con trỏ trỏ tới phần tử đầu tiên của mảng (phần tử ở vị trí thứ 0).
  • &prime[0] là địa chỉ của phần tử đầu tiên của mảng
  • &prime, ở khía cạnh khác, là 1 con trỏ tới mảng int có kích thước 5. Nó lưu địa chỉ “gốc” của mảng prime[5], địa chỉ này ban đầu bằng với địa chỉ của phần tử đầu tiên của mảng. Tuy nhiên, khi cộng thêm 1 sẽ có kết quả lại là địa chỉ cũ cộng thêm 5 x 4 = 20 bytes.

Kết luận:

  • arrayName và arrayName[0] trỏ tới phần tử đầu tiên
  • &arrayName trỏ tới toàn bộ mảng

18 of 24

Truy cập phần tử của mảng

19 of 24

Truy cập phần tử của mảng

Dùng con trỏ:

int prime[5] = {2,3,5,7,11};

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

{

printf("Chi so = %d, Dia chi= %d, Gia tri= %d\n", i, prime + i, *(prime + i));

}

  • Truy cập bằng con trỏ nhanh hơn

20 of 24

Con trỏ và chuỗi (string)

Định nghĩa: Mỗi chuỗi (string) là 1 mảng 1-chiều gồm các ký tự và được kết thúc bởi ký tự null (\0). Khi chúng ta viết char name[] = “ITMO_BRAIN”; , mỗi ký tự chiếm 1 byte trong bộ nhớ và mặc định ký tự cuối cùng luôn luôn phải là \0.

  • name và &name[0] trỏ tới ký từ thứ 0 của chuỗi
  • &name trỏ tới toàn bộ chuỗi.
  • name[i] cũng có thể viết thành *(name +i).

21 of 24

Con trỏ và chuỗi (string)

22 of 24

Mảng các con trỏ

Định nghĩa:

  • Như một mảng int và mảng char, chúng ta cũng có mảng các con trỏ
  • Mảng con trỏ đơn giản là tập hợp các địa chỉ ô nhớ
  • Những ô nhớ này trỏ tới các địa chỉ ô nhớ trong bộ nhớ.

Cú pháp khai báo:

dataType *variableName[size];

/* Examples */

int *example1[5]; // khai báo mảng example1 chứa 5 con trỏ kiểu int

char *example2[8];// khai báo mảng example2 chứa 8 con trỏ kiểu char

23 of 24

Con trỏ trỏ tới mảng

Định nghĩa:

  • Cũng giống như “con trỏ tới int” hoặc “con trỏ tới char”, chúng ta cũng có con trỏ trỏ tới mảng
  • Loại con trỏ này trỏ tới toàn mảng hoặc các phần tử của mảng đó.

Note: Kiến thức ở phần trước, &arrayName trỏ tới toàn bộ mảng.

Khai báo:

dataType (*variableName)[size];

/* Examples */

int (*ptr1)[5];

char (*ptr2)[15];

Lưu ý:

  • Dấu ngoặc tròn () . Nếu không có nó, những gì chúng ta khai báo sẽ trở thành mảng các con trỏ chứ không phải con trỏ trỏ tới mảng.
  • Trong ví dụ thứ nhất thì ptr1 là con trỏ trỏ tới mảng chứa 5 số nguyên ( 5 integers).

24 of 24

Con trỏ trỏ tới mảng

int goals[] = { 85,102,66,69,67};

int (*pointerToGoals)[5] = &goals;

printf("Địa chỉ lưu trong pointerToGoals %d\n", pointerToGoals);

printf("giá trị %d\n",*pointerToGoals);

/* Output */

Địa chỉ lưu trong pointerToGoals 6422016

Giá trị 6422016

  • Tham chiếu ngược (dereference) một con trỏ, nó trả lại giá trị lưu trong địa chỉ ô nhớ đó.
  • Tương tự, khi tham chiếu ngược một con trỏ tới mảng, chúng ta có được mảng và tên của mảng trỏ tới địa chỉ gốc.
  • Nếu chúng ta tham chiếu ngược 1 lần nữa, chúng ta sẽ nhận được giá trị lưu trong địa chỉ đó.

Chúng ta thử in chúng ra bằng cách sử dụng pointerToGoals nhé.