Lập trình D - Con trỏ

Con trỏ lập trình D rất dễ học và thú vị. Một số tác vụ lập trình D được thực hiện dễ dàng hơn với con trỏ và các tác vụ lập trình D khác, chẳng hạn như cấp phát bộ nhớ động, không thể thực hiện được nếu không có chúng. Một con trỏ đơn giản được hiển thị bên dưới.

Thay vì trỏ trực tiếp đến biến, con trỏ trỏ đến địa chỉ của biến. Như bạn đã biết, mọi biến đều là một vị trí bộ nhớ và mọi vị trí bộ nhớ đều có địa chỉ của nó được xác định. Địa chỉ này có thể được truy cập bằng cách sử dụng toán tử dấu và (&) biểu thị một địa chỉ trong bộ nhớ. Hãy xem xét phần sau in địa chỉ của các biến được xác định:

import std.stdio;
 
void main () { 
   int var1; 
   writeln("Address of var1 variable: ",&var1);  
   
   char var2[10]; 
   writeln("Address of var2 variable: ",&var2); 
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

Address of var1 variable: 7FFF52691928 
Address of var2 variable: 7FFF52691930

Con trỏ là gì?

A pointerlà một biến có giá trị là địa chỉ của một biến khác. Giống như bất kỳ biến hoặc hằng số nào, bạn phải khai báo một con trỏ trước khi bạn có thể làm việc với nó. Dạng chung của khai báo biến con trỏ là:

type *var-name;

Đây, typelà kiểu cơ sở của con trỏ; nó phải là một kiểu lập trình hợp lệ vàvar-namelà tên của biến con trỏ. Dấu hoa thị bạn sử dụng để khai báo một con trỏ giống với dấu hoa thị mà bạn sử dụng cho phép nhân. Tuy nhiên; trong câu lệnh này, dấu hoa thị đang được sử dụng để chỉ định một biến làm con trỏ. Sau đây là khai báo con trỏ hợp lệ:

int    *ip;    // pointer to an integer 
double *dp;    // pointer to a double 
float  *fp;    // pointer to a float 
char   *ch     // pointer to character

Kiểu dữ liệu thực tế của giá trị của tất cả các con trỏ, cho dù là số nguyên, số thực, ký tự hay cách khác, đều giống nhau, một số thập lục phân dài đại diện cho địa chỉ bộ nhớ. Sự khác biệt duy nhất giữa các con trỏ của các kiểu dữ liệu khác nhau là kiểu dữ liệu của biến hoặc hằng số mà con trỏ trỏ tới.

Sử dụng con trỏ trong lập trình D

Có một vài thao tác quan trọng khi chúng ta sử dụng con trỏ thường xuyên.

  • chúng tôi xác định một biến con trỏ

  • gán địa chỉ của một biến cho một con trỏ

  • cuối cùng là truy cập giá trị tại địa chỉ có sẵn trong biến con trỏ.

Điều này được thực hiện bằng cách sử dụng toán tử một ngôi *trả về giá trị của biến nằm tại địa chỉ được chỉ định bởi toán hạng của nó. Ví dụ sau sử dụng các thao tác này:

import std.stdio; 

void main () { 
   int var = 20;   // actual variable declaration. 
   int *ip;        // pointer variable
   ip = &var;   // store address of var in pointer variable  
   
   writeln("Value of var variable: ",var); 
   
   writeln("Address stored in ip variable: ",ip); 
   
   writeln("Value of *ip variable: ",*ip); 
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

Value of var variable: 20 
Address stored in ip variable: 7FFF5FB7E930 
Value of *ip variable: 20

Con trỏ rỗng

Luôn luôn là một phương pháp hay để gán con trỏ NULL cho một biến con trỏ trong trường hợp bạn không có địa chỉ chính xác để được gán. Điều này được thực hiện tại thời điểm khai báo biến. Một con trỏ được gán null được gọi lànull con trỏ.

Con trỏ null là một hằng số có giá trị bằng 0 được xác định trong một số thư viện chuẩn, bao gồm cả iostream. Hãy xem xét chương trình sau:

import std.stdio;

void main () { 
   int  *ptr = null; 
   writeln("The value of ptr is " , ptr) ;  
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

The value of ptr is null

Trên hầu hết các hệ điều hành, các chương trình không được phép truy cập bộ nhớ ở địa chỉ 0 vì bộ nhớ đó được hệ điều hành dành riêng. Tuy nhiên; địa chỉ bộ nhớ 0 có ý nghĩa đặc biệt; nó báo hiệu rằng con trỏ không nhằm mục đích trỏ đến vị trí bộ nhớ có thể truy cập được.

Theo quy ước, nếu một con trỏ chứa giá trị null (không), nó được coi là không trỏ đến. Để kiểm tra con trỏ null, bạn có thể sử dụng câu lệnh if như sau:

if(ptr)     // succeeds if p is not null 
if(!ptr)    // succeeds if p is null

Do đó, nếu tất cả các con trỏ không sử dụng đều được cung cấp giá trị null và bạn tránh sử dụng con trỏ null, bạn có thể tránh được việc vô tình sử dụng sai con trỏ chưa được khởi tạo. Nhiều khi, các biến chưa khởi tạo chứa một số giá trị rác và việc gỡ lỗi chương trình trở nên khó khăn.

Số học con trỏ

Có bốn toán tử số học có thể được sử dụng trên con trỏ: ++, -, + và -

Để hiểu số học con trỏ, chúng ta hãy xem xét một con trỏ số nguyên có tên ptr, trỏ đến địa chỉ 1000. Giả sử là số nguyên 32 bit, chúng ta hãy thực hiện phép toán số học sau trên con trỏ:

ptr++

sau đó ptrsẽ trỏ đến vị trí 1004 vì mỗi lần ptr được tăng lên, nó trỏ đến số nguyên tiếp theo. Thao tác này sẽ di chuyển con trỏ đến vị trí bộ nhớ tiếp theo mà không ảnh hưởng đến giá trị thực tại vị trí bộ nhớ.

Nếu ptr trỏ đến một ký tự có địa chỉ là 1000, thì thao tác trên trỏ đến vị trí 1001 vì ký tự tiếp theo sẽ có sẵn tại 1001.

Tăng một con trỏ

Chúng tôi thích sử dụng một con trỏ trong chương trình của chúng tôi thay vì một mảng vì con trỏ biến có thể tăng lên, không giống như tên mảng không thể tăng vì nó là một con trỏ hằng. Chương trình sau tăng con trỏ biến để truy cập từng phần tử kế tiếp của mảng:

import std.stdio; 
 
const int MAX = 3; 
 
void main () { 
   int var[MAX] = [10, 100, 200]; 
   int *ptr = &var[0];  

   for (int i = 0; i < MAX; i++, ptr++) { 
      writeln("Address of var[" , i , "] = ",ptr); 
      writeln("Value of var[" , i , "] = ",*ptr); 
   } 
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

Address of var[0] = 18FDBC 
Value of var[0] = 10 
Address of var[1] = 18FDC0 
Value of var[1] = 100 
Address of var[2] = 18FDC4 
Value of var[2] = 200

Con trỏ so với Mảng

Con trỏ và mảng có liên quan chặt chẽ với nhau. Tuy nhiên, con trỏ và mảng không hoàn toàn có thể hoán đổi cho nhau. Ví dụ, hãy xem xét chương trình sau:

import std.stdio; 
 
const int MAX = 3;
  
void main () { 
   int var[MAX] = [10, 100, 200]; 
   int *ptr = &var[0]; 
   var.ptr[2]  = 290; 
   ptr[0] = 220;  
   
   for (int i = 0; i < MAX; i++, ptr++) { 
      writeln("Address of var[" , i , "] = ",ptr); 
      writeln("Value of var[" , i , "] = ",*ptr); 
   } 
}

Trong chương trình trên, bạn có thể thấy var.ptr [2] để đặt phần tử thứ hai và ptr [0] được sử dụng để đặt phần tử thứ 0. Toán tử tăng dần có thể được sử dụng với ptr nhưng không được sử dụng với var.

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

Address of var[0] = 18FDBC 
Value of var[0] = 220 
Address of var[1] = 18FDC0 
Value of var[1] = 100 
Address of var[2] = 18FDC4 
Value of var[2] = 290

Con trỏ tới con trỏ

Một con trỏ tới một con trỏ là một dạng của nhiều hướng hoặc một chuỗi con trỏ. Thông thường, một con trỏ chứa địa chỉ của một biến. Khi chúng ta định nghĩa một con trỏ tới một con trỏ, con trỏ đầu tiên chứa địa chỉ của con trỏ thứ hai, con trỏ này sẽ trỏ đến vị trí chứa giá trị thực như hình dưới đây.

Một biến là một con trỏ đến một con trỏ phải được khai báo như vậy. Điều này được thực hiện bằng cách đặt một dấu hoa thị bổ sung trước tên của nó. Ví dụ, sau đây là cú pháp để khai báo một con trỏ tới một con trỏ kiểu int -

int **var;

Khi một giá trị đích được trỏ gián tiếp bởi một con trỏ đến một con trỏ, thì việc truy cập giá trị đó yêu cầu phải áp dụng toán tử dấu hoa thị hai lần, như được minh họa bên dưới trong ví dụ:

import std.stdio;  

const int MAX = 3;
  
void main () { 
   int var = 3000; 
   writeln("Value of var :" , var); 
   
   int *ptr = &var; 
   writeln("Value available at *ptr :" ,*ptr); 
   
   int **pptr = &ptr; 
   writeln("Value available at **pptr :",**pptr); 
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

Value of var :3000 
Value available at *ptr :3000 
Value available at **pptr :3000

Chuyển con trỏ đến các hàm

D cho phép bạn chuyển một con trỏ đến một hàm. Để làm như vậy, nó chỉ cần khai báo tham số hàm như một kiểu con trỏ.

Ví dụ đơn giản sau đây chuyển một con trỏ đến một hàm.

import std.stdio; 
 
void main () { 
   // an int array with 5 elements. 
   int balance[5] = [1000, 2, 3, 17, 50]; 
   double avg; 
   
   avg = getAverage( &balance[0], 5 ) ; 
   writeln("Average is :" , avg); 
} 
 
double getAverage(int *arr, int size) { 
   int    i; 
   double avg, sum = 0; 
   
   for (i = 0; i < size; ++i) {
      sum += arr[i]; 
   } 
   
   avg = sum/size; 
   return avg; 
}

Khi đoạn mã trên được biên dịch cùng nhau và được thực thi, nó sẽ tạo ra kết quả sau:

Average is :214.4

Con trỏ trả về từ các hàm

Hãy xem xét hàm sau, trả về 10 số bằng cách sử dụng một con trỏ, có nghĩa là địa chỉ của phần tử mảng đầu tiên.

import std.stdio;
  
void main () { 
   int *p = getNumber(); 
   
   for ( int i = 0; i < 10; i++ ) { 
      writeln("*(p + " , i , ") : ",*(p + i)); 
   } 
} 
 
int * getNumber( ) { 
   static int r [10]; 
   
   for (int i = 0; i < 10; ++i) {
      r[i] = i; 
   }
   
   return &r[0]; 
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

*(p + 0) : 0 
*(p + 1) : 1 
*(p + 2) : 2 
*(p + 3) : 3 
*(p + 4) : 4 
*(p + 5) : 5 
*(p + 6) : 6 
*(p + 7) : 7 
*(p + 8) : 8 
*(p + 9) : 9

Con trỏ đến một mảng

Tên mảng là một con trỏ không đổi đến phần tử đầu tiên của mảng. Do đó, trong khai báo -

double balance[50];

balancelà một con trỏ tới & balance [0], là địa chỉ của phần tử đầu tiên của số dư mảng. Do đó, đoạn chương trình sau sẽ gánp địa chỉ của phần tử đầu tiên của balance -

double *p; 
double balance[10]; 
 
p = balance;

Việc sử dụng tên mảng làm con trỏ hằng và ngược lại là hợp pháp. Do đó, * (balance + 4) là một cách hợp pháp để truy cập dữ liệu tại số dư [4].

Khi bạn lưu trữ địa chỉ của phần tử đầu tiên trong p, bạn có thể truy cập các phần tử mảng bằng cách sử dụng * p, * (p + 1), * (p + 2), v.v. Ví dụ sau đây cho thấy tất cả các khái niệm được thảo luận ở trên -

import std.stdio;
 
void main () { 
   // an array with 5 elements. 
   double balance[5] = [1000.0, 2.0, 3.4, 17.0, 50.0]; 
   double *p;  
   
   p = &balance[0]; 
  
   // output each array element's value  
   writeln("Array values using pointer " ); 
   
   for ( int i = 0; i < 5; i++ ) { 
      writeln( "*(p + ", i, ") : ", *(p + i)); 
   } 
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

Array values using pointer  
*(p + 0) : 1000 
*(p + 1) : 2 
*(p + 2) : 3.4 
*(p + 3) : 17
*(p + 4) : 50