Mã không an toàn trong C#

Mã không an toàn trong C#

1. Managed & Unmanaged code

Mã không an toàn trong C#

- Managed code là mã được thực thi dưới sự giám sát cửa CLR(Common Language Runtime). CLR có nhiệm vụ giống như là 1 lao công, như:

  • Quản lý bộ nhớ cho các đối tượng

  • Xác minh kiểu đang được thực thi

  • Thu gom rác (các biến mà chúng ta bầy bừa ra trong quá trình viết mã)

Người dùng (ở đây là lập trình viên) hoàn toàn bị cô lập với các nhiệm vụ trên. và anh ta không thể thao tác trực tiếp  lên bộ nhớ, vì tất cả công việc đó được thực hiện bởi CLR
- Trong khi đó, mã không được quản lý Unmanaged code được thực thi bên ngoài sự kiểm soát của CLR.
Ví dụ đẽ thấy là các thư viên Win32 DLLs của chúng ta như kernel32.dll, user32.dll, và các thành phần COM được cài đặt trên hệ thống. Giống như trong một chương trình C++ chương trình cấp phát bộ nhớ cho một con trỏ là một ví dụ của mã không được quản lý bởi vì chính lập trình viên phải có trách nhiệm:

  • Gọi hàm cấp phát bộ nhớ

  • Đảm bảo đúng khuôn mẫu

  • Đảm bảo bộ nhớ được giải phóng mỗi khi xong nhiệm vụ

 2. Unsafe code

- Mã không an toàn (unsafe) là kiểu lai ghép giữa Managed code và Unmanaged code
- Nó thực hiện dưới sự giám sát của CLR giống như Managed code, nhưng cho phép bạn thao tác địa chỉ trên bộ nhớ một cách trực tiếp thông qua việc sử dụng con trỏ giống hệt như mã không được quản lý Unmanaged code. Cho bạn có sự lựa chọn tốt nhất giữa hai cách này

3. Let's get coding

- Để viết unsafe code ta sử dụng 2 từ khóa đặc biệt: unsafe fixed. Và sử dụng 3 toán tử con trỏ

  1. *

  2. &

  3. ->

Để sử dụng các toán tử con trỏ này thì ta cần khai báo và sử dụng chúng trong khối lệnh unsafe
unsafe {

// Sử dụng fixed và các toán tử con trỏ
}

Ngoài ra bạn có thể khai báo unsafe code cho biến toàn cục, phương thức, cho class, hay struct.
Ví dụ:
public static unsafe void binhPhuong(int* number)
{
       *number = (*number) * 2;

}

4. Dùng con trỏ để tối ưu hóa thực thi

Sử dụng stackalloc để cấp phát vùng nhớ, cú pháp:

decimal *pDecimal = stackalloc decimal [10];

lệnh này chỉ đơn giản là cấp phát vùng nhớ mà không khởi tạo bất kỳ giá trị nào
đồng thời pDecimal trỏ đến ô nhớ đầu tiên của mảng 10 số Decimal
Các thao tác tính toán với con trỏ trỏ đến mảng thực hiện tương tự như trong C hay C++
Bên cạnh đó C# còn cho phép sử dụng chỉ số phần tử Indexer thay cho cú pháp thông thường. Nếu p là con trỏ và i là kiểu số nguyên thì biểu thức p[i] tương đương với *(p + i). Ví dụ:

double *pDoubles = stackalloc double [20];
pDoubles[0] = 3.0;   // pDoubles[0] is the same as *pDoubles
pDoubles[1] = 8.4;   // pDoubles[1] is the same as *(pDoubles+1)

Có những trường hợp ta cần truy xuất bộ nhớ trực tiếp khi ta muốn truy xuất vào các hàm bên ngoài (không thuộc .NET) mà đòi hỏi con trỏ được truyền vào như tham số (ví dụ như các hàm API), hoặc là vì ta muốn truy nhập vào nội dung bộ nhớ để sửa lỗi…. Trong phần này ta sẽ xem xét cách C# đáp ứng những điều này như thế nào. 

5. Con trỏ trong C#

-  Con trỏ đơn giản là 1 biến lưu  địa chỉ của một thứ khác theo cùng 1 cách như là 1 tham chiếu,  sự khác biệt là cú pháp C# trong tham chiếu không cho phép ta truy xuất vào địa chỉ bộ nhớ.

-  3 ưu điểm của con trỏ :

  • Cải thiện sự thực thi : cho ta biết những gì ta đang làm,đảm bảo rằng dữ liệu được truy xuất hay thao tác theo cách hiệu quả nhất, đó là lí do mà C và C++ cho phép dùng con trỏ  trong ngôn ngữ của mình (con trỏ là một phần sức mạnh của ngôn ngữ C++).
  • Khả năng tích hợp với các phần trước ( Backward compatibility ) – đôi khi ta phải sử dụng lại các hàm API cho mục đích của ta. Mà các hàm  API được viết bằng C,ngôn ngữ dùng con trỏ rất nhiều, nghĩa là nhiều hàm lấy con trỏ như tham số. Hoặc là các “DLL”  do 1 hãng nào đó cung cấp chứa các hàm lấy con trỏ làm  tham số . Trong nhiều trường hợp ta có thể viết các khai báo DLlImport theo cách tránh sử dụng con trỏ , ví dụ như dùng lớp System.IntPtr.
  • Ta có thể cần tạo ra các địa chỉ vùng nhớ có giá trị cho người dùng – ví dụ nếu ta muốn phát triển 1 ứng dụng mà cho phép người dùng tương tác trực tiếp đến bộ nhớ, như là 1 debugger (trình gỡ rối).

-  Nhược điểm của con trỏ:

  • Cú pháp để lấy các hàm phức tạp hơn.

  • Con trỏ khó sử dụng.

  • Nếu không cẩn thận ta có thể viết  lên các biến khác ,làm tràn stack, mất thông tin, đụng độ …

  • C# có thể từ chối thi hành những đoạn mã không an toàn này (đoạn mã có sử dụng con trỏ).

-  Ta có thể đánh dấu đoạn mã có sử dụng con trỏ bằng cách dùng từ khoá unsafe.

Ví dụ : dùng cho hàm

unsafe int GetSomeNumber()
{
    // code that can use pointers
}

-  Dùng ho lớp hay struct

unsafe class MyClass
{
    // any method in this class can now use pointers
}

-  Dùng cho 1 trường

class MyClass
{
    unsafe int* pX;   // Khai báo một trường con trỏ trong 1 class
}

-  Hoặc một khối mã:

void MyMethod()
{
    // code that doesn't use pointers
    unsafe
    {
        // unsafe code that uses pointers here
    }
    // more 'safe' code that doesn't use pointers
}

-  Tuy nhiên ta không thể đánh dấu một biến cục bộ là unsafe

int MyMethod()
{
   unsafe int *pX;   // WRONG
}

-  Để biên dịch các mã chứa khối unsafe ta dùng khối lệnh sau:

csc /unsafe MySource.cs

hay:

csc -unsafe MySource.cs

5.1 Cú pháp con trỏ

int* pWidth, pHeight;
double* pResult;

-  Lưu ý:  khác với C++, ký tự * kết hợp với kiểu hơn là kết hợp với biến – nghĩa là khi ta khai báo như ở trên thì pWidth là pHeight đề là con trỏ do đó * sau kiểu int; đối với C++ ta phải khai báo * trước 2 biến trên thì chúng mới là con trỏ.

-Cách dùng * và & giống như trong C++”:

  • & : lấy địa chỉ.
  • * : lấy nội dung của địa chỉ.

5.2 Ép kiểu con trỏ thành kiểu int

Vì con trỏ là 1 số int lưu địa chỉ nên ta có thể chuyển tường minh con trỏ thành kiểu int hay ngược lại.Ví dụ:

int x = 10;
int *pX, pY;
pX = &x;
pY = pX;
*pY = 20;
uint y = (uint)pX;
int *pD = (int*)y;

-  y là unit, sau đó được chuyển ngược lại thành biến con trỏ pD.

-  Một lý do để ta phải ép kiểu là Console.Writeline không có overload nào nhận thông số là con trỏ. Do đó, ta phải ép kiểu nó sang kiểu số nguyên int.

Console.WriteLine("Address is" + pX);   // sai
                                        // Biên dịch lỗi
Console.WriteLine("Address is" + (uint) pX);   // Đúng

5.3 Ép kiểu giữa những kiểu con trỏ

-  Ta cũng có thể chuyển đổi tường minh giữa các con trỏ trỏ đến một kiểu khác. Ví dụ:

byte aByte = 8;
byte* pByte = &aByte;
double* pDouble = (double*)pByte;

5.4  void Pointers (con trỏ void)

-  Nếu ta muốn giữ một con trỏ, nhưng không muốn đặc tả kiểu cho con trỏ ta có thể khai báo con trỏ là void.

void *pointerToVoid;
pointerToVoid = (void*)pointerToInt;   // pointerToInt declared as int*

mục đích là khi ta cần gọi các hàm API yêu cầu tham số void*.

5.5  Toán tử sizeof

-  Lấy thông số là tên của kiểu và trả về số byte của kiểu đó ví dụ :

int x = sizeof(double);

x có giá trị là 8.

-  Dưới đây là bảng kích thước kiểu:

-  Ta cũng có thể dùng sizeof cho struct nhưng không dùng được cho lớp.

5.6  Ví dụ 1:

Ví dụ này trình bày cách thao tác trên con trỏ và trình bày kết quả, cho phép ta thấy những gì xảy ra trong bộ nhớ và nơi biến được lưu trữ:

using System;

namespace Vidu1
{
   class MainEntryPoint
   {
      static unsafe void Main()
      {
         int x=10;
         short y = -1;
         byte y2 = 4;
         double z = 1.5;
         int *pX = &x;
         short *pY = &y;
         double *pZ = &z;

         Console.WriteLine(
            "Address of x is 0x{0:X}, size is {1}, value is {2}",
            (uint)&x, sizeof(int), x);
         Console.WriteLine(
            "Address of y is 0x{0:X}, size is {1}, value is {2}",
            (uint)&y, sizeof(short), y);
         Console.WriteLine(
            "Address of y2 is 0x{0:X}, size is {1}, value is {2}",
            (uint)&y2, sizeof(byte), y2);
         Console.WriteLine(
            "Address of z is 0x{0:X}, size is {1}, value is {2}",
            (uint)&z, sizeof(double), z);
         Console.WriteLine(
            "Address of pX=&x is 0x{0:X}, size is {1}, value is 0x{2:X}",
            (uint)&pX, sizeof(int*), (uint)pX);
         Console.WriteLine(
            "Address of pY=&y is 0x{0:X}, size is {1}, value is 0x{2:X}",
            (uint)&pY, sizeof(short*), (uint)pY);
         Console.WriteLine(
            "Address of pZ=&z is 0x{0:X}, size is {1}, value is 0x{2:X}",
            (uint)&pZ, sizeof(double*), (uint)pZ);

         *pX = 20;
         Console.WriteLine("After setting *pX, x = {0}", x);
         Console.WriteLine("*pX = {0}", *pX);

         pZ = (double*)pX;
         Console.WriteLine("x treated as a double = {0}", *pZ);

         Console.ReadLine();
      }
   }
}

-   Mã gồm 3 biến

  • int x

  • short y

  • double z

-   Cùng với các con trỏ trỏ đến các giá trị này.sau đó ta trình bày giá trị của các biến và kích thước,địa chỉ của nó.Ta dùng đặc tả {0:X} trong Console.WriteLine để  địa chỉ  bộ nhớ được trình bày theo định dạng số bát phân.

-   Cuối cùng ta dùng con trỏ pX thay đổi giá trị của x thành 20,và thử ép kiểu biến x thành 1 double để xem điều gì sẻ xảy ra

-  Biên dịch mã ,ta có kết quả sau :

Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

PointerPlayaround.cs(7,26): error CS0227: Unsafe code may only appear if
        compiling with /unsafe

csc /unsafe PointerPlayaround.cs
Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

PointerPlayaround
Address of x is 0x12F8C4, size is 4, value is 10
Address of y is 0x12F8C0, size is 2, value is -1
Address of y2 is 0x12F8BC, size is 1, value is 4
Address of z is 0x12F8B4, size is 8, value is 1.5
Address of pX=&x is 0x12F8B0, size is 4, value is 0x12F8C4
Address of pY=&y is 0x12F8AC, size is 4, value is 0x12F8C0
Address of pZ=&z is 0x12F8A8, size is 4, value is 0x12F8B4
After setting *pX, x = 20
*pX = 20
x treated as a double = 2.63837073472194E-308

5.7  Pointer Arithmetic

-   Ta có thể cộng hay trừ số nguyên trên con trỏ.Ví dụ , giả sử ta có 1 con trỏ trỏ đến số nguyên,và ta thử cộng 1 vào giá trị của nó .trình biên dịch sẽ biết và tăng vùng nhớ lên 4 byte ( do kiểu int có kích thước 4 byte). Nếu là kiểu double thì khi cộng 1 sẽ tăng giá trị của con trỏ lên 8 byte.

-  Ta có thể dùng toán tử +, -, +=, -=, ++,và — với biến bên phía phải của toán tử này là long hay ulong

-  Ví dụ :

        uint u = 3;
        byte b = 8;
        double d = 10.0;
        uint* pUint = &u;        // size of a uint is 4
        byte* pByte = &b;       // size of a byte is 1
        double* pDouble = &d;   // size of a double is 8

-  Giả sử địa chỉ của những con trỏ này trỏ đến là:

  • pUint: 1243332

  • pByte: 1243328

  • pDouble: 1243320

-  Sau khi thực thi ta có:

++pUint;              // adds 1= 4 bytes to pUint
pByte -= 3;           // subtracts 3=3bytes from pByte
double *pDouble2 = pDouble - 4; // pDouble2 = pDouble - 32 bytes (4*8 bytes)

-  Con trỏ sẽ có giá trị:

  • pUint: 1243336

  • pByte: 1243321

  • pDouble2: 1243328

-  Ta cũng có thể trừ 2 con trỏ với nhau. Giá trị kết quả là kiểu long bằng giá trị con trỏ chia cho kích thước của kiểu mà nó đại diện. Ví dụ :

        double* pD1 = (double*)1243324;   // note that it is perfectly valid to
        // initialize a pointer like this.
        double* pD2 = (double*)1243300;
        long L = pD1 - pD2;                 // gives the result 3 (=24/sizeof(double))

 

5.8  Con trỏ đến struct – toán tử truy suất các thành viên con trỏ

-  Cũng giống như con trỏ trong các kiểu dữ liệu có sẵn. tuy nhiên thêm 1 điều kiện là – Struct không chứa bất kì kiểu tham chiếu nào. Do con trỏ không thể trỏ đến bất kì kiểu tham chiếu nào, để tránh điều này , trình biên dịch sẽ phất cờ lỗi nếu ta tạo ra một con trỏ đến bất kì Struct nào chứa kiểu tham chiếu .

-  Giả sử ta có struct như sau :

struct MyGroovyStruct
{
    public long X;
    public float F;
}

-  Sau đó ta định nghĩa con trỏ cho nó:

MyGroovyStruct* pStruct;

-  Khởi tạo nó:

MyGroovyStruct Struct = new MyGroovyStruct();
pStruct = &Struct;

-  Cũng có thể truy xuất giá trị thành viên của 1 struct bằng con tro:

(*pStruct).X = 4;
(*pStruct).F = 3.4f

-  Tuy nhiên cú pháp hơi phức tạp. C# định nghĩa một toán tử khác cho phép ta truy xuất các thành viên của struct bằng con trỏ đơn giản hơn, gọi là toán tử truy xuất thành viên con trỏ, ký hiệu là –>

Các dùng:

pStruct->X = 4;
pStruct->F = 3.4f;

-  Ta cũng có thể thiết đặt trực tiếp con trỏ của kiểu tương  đương để trỏ đến các trường trong struct.

long* pL = &(Struct.X);
float* pF = &(Struct.F);

hay

long* pL = &(pStruct->X);
float* pF = &(pStruct->F);

5.9  Con trỏ đến các thành viên của lớp

-  Ta đã nói rằng không thể tạo ra con trỏ đến lớp.vì việc tạo có thể làm cho bộ gom rác hoạt động không đúng.  Tuy nhiên ta có thể tạo các con trỏ đến các thành viên của lớp. Ta sẽ viết lại struct của ví dụ trước như là lớp :

class MyGroovyClass
{
    public long X;
    public float F;
} 

-  Sau đó để tạo một con trỏ đến các trường của nó X và F. Tuy nhiên làm như vậy sẽ gây ra lỗi.

        MyGroovyClass myGroovyObject = new MyGroovyClass();
        long* pL = &(myGroovyObject.X);   // wrong
        float* pF = &(myGroovyObject.F);  // wrong

-  Do X và F nằm trong 1 lớp , mà được đặt trong heap.nghĩa là chúng vẫn gián tiếp chịu sự quản lý của bộ gom rác.cụ thể bộ gom rác có thể quyết định di chuyển MyGroovyClass đến 1 vị trí mới trong bộ nhớ để dọn dẹp heap. Nếu làm điều này thì bộ gom rác tất nhiên sẽ cập nhật tất cả các tham chiếu đến đối tượng ,giả sử như biến myGrooveObject  vẫn sẽ trỏ đến đúng vị trí. Tuy nhiên bộ gom rác không biết gì về con trỏ cả. vì thế nếu di chuyển các đối tượng tham chiếu bởi myGrooveObject,pL và pF sẽ vẫn không thay đôỉ  và kết cuộc là trỏ đến sai vị trí vùng nhớ.

-  Để giải quyết vấn đề này ta dùng từ khóa fixed, mà cho bộ gom rác biết rằng có thể có con trỏ trỏ đến các thành viên của các thể hiện lớp, vì thế các thể hiện lớp này sẽ không được di chuyển. Cú pháp như sau nếu ta chỉ muốn khai báo 1 con trỏ :

MyGroovyClass myGroovyObject = new MyGroovyClass();
// do whatever
fixed (long *pObject = &( myGroovyObject.X))
{
   // do something
}

-  Nếu muốn khai báo nhiều hơn 1 con trỏ ta có thể đặt nhiều câu lệnh fixed trước khối mã giống nhau:

MyGroovyClass myGroovyObject = new MyGroovyClass();
fixed (long *pX = &( myGroovyObject.X))
fixed (float *pF = &( myGroovyObject.F))
{
   // do something
}

-  Ta có thể lồng các khối fixed nếu muốn fix các con trỏ trong các thời điểm khác nhau:

MyGroovyClass myGroovyObject = new MyGroovyClass();
fixed (long *pX = &( myGroovyObject.X))
{
   // do something with pX
   fixed (float *pF = &( myGroovyObject.F))
   {
      // do something else with pF
   }
}

-  Ta cũng có thể khởi tạo vài biến trong cùng 1 khối fixed:

MyGroovyClass myGroovyObject = new MyGroovyClass();
MyGroovyClass myGroovyObject2 = new MyGroovyClass();
fixed (long *pX = &( myGroovyObject.X), pX2 = &( myGroovyObject2.X))
{
   // etc.

    }
}

5.10  Thêm các lớp và Struct đến ví dụ

-  Trong phần này ta sẽ minh họa việc tính toán trên con trỏ và các con trỏ đến struct và lớp.

struct CurrencyStruct
{
    public long Dollars;
    public byte Cents;

    public override string ToString()
    {
        return "$" + Dollars + "." + Cents;
    }
}

class CurrencyClass
{
    public long Dollars;
    public byte Cents;

    public override string ToString()
    {
        return "$" + Dollars + "." + Cents;
    }
}

-  Bây giờ ta có thể áp dụng con trỏ cho các struct và lớp của ta, ta bắt đầu bằng việc trình bày kích thước của stuct, tạo ra 1 vài thể hiện của nó cùng với con trỏ. Ta dùng những con trỏ này để khởi tạo 1 trong những struct Currency, amount1. và trình  bày các địa chỉ của các biến :

public static unsafe void Main()
{
    Console.WriteLine(
       "Size of Currency struct is " + sizeof(CurrencyStruct));
    CurrencyStruct amount1, amount2;
    CurrencyStruct* pAmount = &amount1;
    long* pDollars = &(pAmount->Dollars);
    byte* pCents = &(pAmount->Cents);

    Console.WriteLine("Address of amount1 is 0x{0:X}", (uint)&amount1);
    Console.WriteLine("Address of amount2 is 0x{0:X}", (uint)&amount2);
    Console.WriteLine("Address of pAmt is 0x{0:X}", (uint)&pAmount);
    Console.WriteLine("Address of pDollars is 0x{0:X}", (uint)&pDollars);
    Console.WriteLine("Address of pCents is 0x{0:X}", (uint)&pCents);
    pAmount->Dollars = 20;
    *pCents = 50;
    Console.WriteLine("amount1 contains " + amount1);

}

-  Ta biết rằng amount2 sẽ được lưu trữ ở 1 địa chỉ ngay sau amount1, sizeof ( CurrencyStru) trả về 16, vì vậy CurrencyStruct sẽ nằm ở địa chỉ là bội số của 4 byte. Do đó sau khi giảm con trỏ currency , nó sẽ trỏ đến amount2:

--pAmount;   // this should get it to point to amount2
Console.WriteLine("amount2 has address 0x{0:X} and contains {1}",
   (uint)pAmount, *pAmount);

-  Ta trình bày nội dụng của amount2 nhưng chưa khởi tạo nó. Dù trình biên dịch C# ngăn không cho chúng ta dùng các giá trị chưa được khởi tạo nhưng khi dùng con trỏ thì điều này không còn đúng nửa.trình biên dịch không cách nào biết nội của amount2 mà ta trình bày, chỉ có ta biết.

-  Kế tiếp ta sẽ tính toán trên con trỏ pCents,pCents hiện thời trỏ đến amount1.Cents , nhưng mục đích của ta là làm cho nó trỏ đến amount2.Cents. Làm điều này ta cần giảm địa chỉ của nó. Ta cần làm một vài ép kiểu  :

// do some clever casting to get pCents to point to cents
// inside amount2
CurrencyStruct *pTempCurrency = (CurrencyStruct*)pCents;
pCents = (byte*) ( --pTempCurrency );
Console.WriteLine("Address of pCents is now 0x{0:X}", (uint)&pCents);

-  Cuối cùng ta dùng vài từ khoá fixed để tạo ra một vài con trỏ mà trỏ đến các trường trong thể hiện lớp, và dùng những con trỏ này để thiết đặt giá trị của thể hiện này. Lưu ý rằng điều này cũng là lần đầu tiên ta thấy địa chỉ của một mục được lưu trữ trên heap hơn là trên stack:

Console.WriteLine("\nNow with classes");
// now try it out with classes
CurrencyClass amount3 = new CurrencyClass();

fixed(long *pDollars2 = &(amount3.Dollars))
fixed(byte *pCents2 = &(amount3.Cents))
{
   Console.WriteLine(
      "amount3.Dollars has address 0x{0:X}", (uint)pDollars2);
   Console.WriteLine(
      "amount3.Cents has address 0x{0:X}", (uint) pCents2);
   *pDollars2 = -100;
   Console.WriteLine("amount3 contains " + amount3);
}

-  Kết quả khi chạy chương trình là:

Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

PointerPlayaround2
Size of Currency struct is 16
Address of amount1 is 0x12F8A8
Address of amount2 is 0x12F898
Address of pAmt is 0x12F894
Address of pDollars is 0x12F890
Address of pCents is 0x12F88C
amount1 contains $20.50
amount2 has address 0x12F898 and contains $5340121818976080.102
Address of pCents is now 0x12F88C

Now with classes
amount3.Dollars has address 0xBA4960
amount3.Cents has address 0xBA4968
amount3 contains $-100.0

6. Dùng con trỏ để tối ưu hóa thực thi

Sau đây ta sẽ áp dụng những hiểu biết về con trỏ và minh họa 1 ví dụ mà ta  thấy rõ lợi ích của việc dùng con trỏ trong thực thi.

6.1 Tạo ra mảng có nền là Stack

-  Để tạo ra mảng này ta cần từ khoá  stackalloc. lệnh stackalloc chỉ dẫn thời gian chạy .NET để cấp phát 1 số vùng nhớ trên stack khi ta gọi nó ,ta cần cung cấp cho nó 2 thông tin

  • Kiểu của biến mà ta muốn lưu trữ

  • Ta cần lưu bao nhiêu biến

trong ví dụ , để cấp  phát đủ vùng nhớ lưu trữ 10 số thập phân decimal , ta viết :

decimal *pDecimals = stackalloc decimal [10];

lệnh này chỉ đơn giản cấp phát vùng nhớ. không khởi tạo bất kì giá trị nào.

-  Để lưu  20 số double ta viết :

double *pDoubles = stackalloc double [20];

mặc dù dòng mã này đặc tả số biến được lưu là hằng, điều này có thể  là 1 định giá số lượng vào lúc chạy. vì thế ta có thể viết tương đương với ví dụ trên như sau :

int size;
size = 20;   // or some other value calculated at run-time
double *pDoubles = stackalloc double [size];

-  Kiểu mảng cơ bản nhất mà có thể có là 1 khối bộ nhớ lưu các phần tử như sau :

image

-  Câu hỏi được đặt ra là làm thế nào ta sử dụng vùng nhớ mà ta vừa tạo. Trở lại ví dụ ta vừa nói rằng giá trị trả về từ stackalloc  trỏ đến bắt đầu của vùng nhớ. Do đó  cho phép ta có thể lấy vị trí đầu tiên của vùng nhớ được cấp phát. Ví dụ để cấp phát các số double  và thiết lập phần tử đầu tiên (phần tử 0 của mảng) giá trị 3.0  ta có thể viết :

double *pDoubles = stackalloc double [20];
*pDoubles = 3.0;

-  Ta có thể thiết lập phần tử thứ 2 của mảng bằng cách dùng cách tính toán trên con trỏ mà ta đã biết. Ví dụ nếu ta muốn đặt giá trị của phần tử thứ hai  ta làm như sau :

double *pDoubles = stackalloc double [20];
*pDoubles = 3.0;
*(pDoubles+1) = 8.4;

-  Nó chung ta có thể lấy phần tử thứ X của mảng với biểu thức *(pDoubles+X)

-  Bên cạnh đó C# cũng định nghĩa 1 cú pháp thay thế .Nếu p là con trỏ và X là kiểu số thì biểu thức p[X] tương đương với *(p+X).

double *pDoubles = stackalloc double [20];
pDoubles[0] = 3.0;   // pDoubles[0] is the same as *pDoubles
pDoubles[1] = 8.4;   // pDoubles[1] is the same as *(pDoubles+1)

-  Mặc dù mảng của ta có thể được truy xuất theo cùng cách như mảng bình thường, ta cần quan tâm đến cảnh báo sau. Đoạn mã sau đây sẽ gây ra 1 biệt lệ:

double [] myDoubleArray = new double [20];
myDoubleArray[50] = 3.0;

-  Biệt lệ xuất hiện vì ta cố truy xuất vào mảng dùng chỉ mục vượt quá mảng ( chỉ mục là 50, nhưng giá trị lớn nhất cho phép là 19). Tuy nhiên, nếu ta khai báo 1 mảng dùng stackalloc , điều đó sẽ không gây ra biệt lệ : 

double *pDoubles = stackalloc double [20];
pDoubles[50] = 3.0;

6.2  Ví dụ 2

using System;

namespace Wrox.ProCSharp.AdvancedCSharp
{
   class MainEntryPoint
   {
      static unsafe void Main()
      {
         Console.Write("How big an array do you want? \n> ");
         string userInput = Console.ReadLine();
         uint size = uint.Parse(userInput);

         long *pArray = stackalloc long [(int)size];
         for (int i=0 ; i<size ; i++)
            pArray[i] = i*i;

         for (int i=0 ; i<size ; i++)
            Console.WriteLine("Element {0} = {1}", i, *(pArray+i));
      }
   }
}

-  Kết quả chương trình khi chạy là:

How big an array do you want?
> 15
Element 0 = 0
Element 1 = 1
Element 2 = 4
Element 3 = 9
Element 4 = 16
Element 5 = 25
Element 6 = 36
Element 7 = 49
Element 8 = 64
Element 9 = 81
Element 10 = 100
Element 11 = 121
Element 12 = 144
Element 13 = 169
Element 14 = 196

7. Tài liệu tham khảo

-  MSDN

- Wrox.com

Bạn thấy bài viết này như thế nào?: 
No votes yet
Ảnh của Binh Tran Thanh

Drupal Consultant

Started my career as a drupal8 developer in EM Solutions . I love learning Web technologies like HTML, CSS, PHP, Jquery Ajax and Drupal backend . Currently working as a drupal backend developer.

Advertisement

 

jobsora

Dich vu khu trung tphcm

Dich vu diet chuot tphcm

Dich vu diet con trung

Quảng Cáo Bài Viết

 

Cuộc đời Drupal Developer qua Illustrated by Cat GIFs

The feature is reverted but shows as overridden

Khái niệm thiết kế điện thoại Facebook và Nokia Lumia Elements

Khái niệm thiết kế điện thoại Facebook và Nokia Lumia Elements

Với một chiếc điện thoại Facebook, người dùng Facebook có thể truy cập vào trang web mạng xã hội này bất cứ nơi nào tại bất kì thời điểm nào, một lợi thế rất lớn so với những thiết bị khác.

Đồ chơi cho editors là Paragraphs Sets module trong Drupal 8

Đồ chơi cho editors là Paragraphs Sets module trong Drupal 8

For us, the Paragraphs module is the holy grail of structured content creation.

Công ty diệt chuột T&C

 

Diet con trung