Thứ Sáu, 29 tháng 6, 2012

Tìm hiểu về Stack và Heap (phần 2)


Bài viết này sẽ trình bày chi tiết hơn về việc thi hành code. Ở bài viết trước chúng ta đã cơ bản hiểu được điều gì xảy ra trong bộ nhớ khi gọi một hàm. Và bài này các bạn sẽ được tìm hiểu kĩ hơn.
Khi chúng ta gọi một hàm thì những việc sau sẽ được thực hiện:
1. Stack sẽ Cấp phát khoảng trống cho những thông tin mà hàm cần đến để xử lý (Được gọi là Stack Frame). Ô nhớ được cấp phát này có cả địa chỉ gọi (một con trỏ) ví dụ đơn giản như là một chỉ dấn GOTO- giúp chương trình của bạn biết nơi gọi các lệnh tiếp theo khi kết thúc một công việc nào đó.
2. Các tham số của hàm sẽ được copy. Và điều này tôi sẽ trình bày kĩ trong bài.
3. Điều khiển sẽ khớp với hàm JIT’ted và thread bắt đầu thi hành code. Sau đấy sẽ có các hàm khác chèn vào stack và được gọi.
Các bạn hãy đọc đoạn code sau:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
Đây là những gì diễn ra trong stack
Như ở phần 1 tôi đã trình bày việc sắp đặt các tham số trong stack khá phức tạp còn tùy thuộc vào nó là kiểu tham trị hay kiểu tham chiếu. Một kiểu giá trị thì được copy lại nhưng kiểu tham chiếu đến một tham chiếu thì được copy đi copy lại :D .
Đầu tiên chúng ta cùng tìm hiểu về kiểu truyền tham trị
Khi chúng ta truyền tham trị, ô nhớ được cấp phát và giá trị của tham trị đó sẽ được copy vào một ô nhớ mới trong stack. Theo dõi đoạn code sau:
class Class1
{
public void Go()
{
int x = 5;
AddFive(x);
Console.WriteLine(x.ToString());
}
public int AddFive(int pValue)
{
pValue += 5;
return pValue;
}
}
Theo hàm này thì ô nhớ cho biến X giờ nhận giá trị là 5.
Tiếp đến hàm AddFive() sẽ được đặt vào một ô trong stack chứa các tham số và giá trị được copy lại dần dần từ x.
Khi hàm AddFive() kết thúc, thread sẽ gọi đến hàm Go và vì hàm AddFive() đã thực hiện xong nên, pValue về cơ bản đã bị “removed”.
Kết quả đưa ra màn hình là 5 Có nghĩa là mọi tham trị trong hàm là bản sao vào chúng ta hi vọng giá trị gốc sẽ được giữ lại.
Một điều bạn nên ghi nhớ đấy là nếu chúng ta có một kiểu tham trị lớn như là một struct phức tạp chẳng hạn và đưa nó vào stack, Điều này gây lãng phí bộ nhớ và chu trình xử lý để copy nó. Stack không phải là một không gian nhớ vô hạn nó cũng giống như một cái cốc có đáy đặt dưới vòi nước, đương nhiên là nó có thể bị tràn. Struct là một kiểu giá trị có thể nhận những giá trị khá lớn vì thế chúng ta cần nắm vững cách xử lý nó.
Ví dụ một struct khá lớn:
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
Tưởng tượng điều gì xảy ra khi chúng ta gọi hàm Go() và thực hiện thêm hàm DoSomething() dưới đây:
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(x);
}
public void DoSomething(MyStruct pValue)
{
// DO SOMETHING HERE….
}
Cách này thực sự là không khả thi. Hãy hình dung chúng ta gấp đôi MyStruct khoảng 1000 lần và bạn có thể hiểu điều gì xảy ra rồi đấy.
Vậy giải quyết vấn đền này thế nào? Chúng ta sẽ sử dụng tham chiếu đến kiểu giá trị gốc như sau:
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(ref x);
}
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
public void DoSomething(ref MyStruct pValue)
{
// DO SOMETHING HERE….
}
Có một điều duy nhất các bạn cần lưu ý là sử dụng kiểu tham trị bằng cách tham chiếu là bạn phải truy cập đến giá trị của kiểu. Mỗi khi thay đổi pValue tức là chúng ta thay đổi x. Sử dụng đoạn code dưới đây, Kết quả trả về sẽ là “12345” bởi vì pValue.a trên thực tế đã tìm đến ô nhớ mà biến x ban đầu được định nghĩa
public void Go()
{
MyStruct x = new MyStruct();
x.a = 5;
DoSomething(ref x);
Console.WriteLine(x.a.ToString());
}
public void DoSomething(ref MyStruct pValue)
{
pValue.a = 12345;
}
Sử dụng kiểu truyền tham chiếu
Trong ví dụ sau, Sử dụng biến kiểu tham chiếu nó cũng giống với việc bạn dùng tham trị nhưng truy cập theo kiểu tham chiếu:
Nếu chúng ta sử dụng tham trị:
public class MyInt
{
public int MyValue;
}
Sau đó gọi đến Go(), lớp MyInt sẽ được lưu trên Heap vì nó là kiểu tham chiếu
public void Go()
{
MyInt x = new MyInt();
}
Nếu chúng ta gọi hàm Go() như sau
public void Go()
{
MyInt x = new MyInt();
x.MyValue = 2;
DoSomething(x);
Console.WriteLine(x.MyValue.ToString());
}
public void DoSomething(MyInt pValue)
{
pValue.MyValue = 12345;
}
Những gì diễn ra trong stack và heap :
1. đầu tiên trong Go() biến x được đưa vào stack.
2. Khi chúng ta gọi hàm DoSomething() biến pValue được đưa vào stack.
3. giá trị của x (địa chỉ của MyInt trong stack) được copy vào pValue.
Có nghĩa là khi chúng ta thay đổi MyValue của đối tượng MyInt trong Heap sử dụng pValue sau đấy gọi đến đối tượng trong Heap sử dụng x, chúng ta sẽ nhận được giá trị là “12345”
Vậy điều gì sẽ xảy ra khi chúng ta tham chiếu sử dụng tham biến.
Hãy kiểm tra class sau. Chúng ta có 2 class là Animal và Vegetables đều kế thừa lơp things.
public class Thing
{
}
public class Animal:Thing
{
public int Weight;
}
public class Vegetable:Thing
{
public int Length;
}
Và chúng ta thực hiện hàm Go() như sau:
public void Go()
{
Thing x = new Animal();
Switcharoo(ref x);
Console.WriteLine(
“x is Animal : “
+ (x is Animal).ToString());
Console.WriteLine(
“x is Vegetable : “
+ (x is Vegetable).ToString());
}
public void Switcharoo(ref Thing pValue)
{
pValue = new Vegetable();
}
Biến X sẽ được trả về một Vegetable.
x is Animal : False
x is Vegetable : True
1. đầu tiên khi gọi hàm Go(), con trỏ x sẽ được đua vào stack.
2. Đối tượng Animal được đưa vào Heap.
3. Khi gọi hàm Switchroo(), pValue được đưa vào stack và trỏ đến x.
4. Đối tượng Vegetable được đưa vào Heap.
5. con trỏ x thay đổi giá trị theo pValue và trỏ đến Vegetable
Nếu chúng ta không sử dụng biến ref, chúng ta sẽ giữ lại Animal và sẽ nhận được giá trị ngược lại.

Không có nhận xét nào: