Lập trình

Lập trình

Thứ Hai, 28 tháng 12, 2015

[Kĩ thuật lập trình cơ bản] - Viết chương trình báo thức bằng C++

Dưới đây là code của chương trình báo thức đơn giản viết bằng C++ cho các bạn mới bắt đầu lập trình.

Chương trình cho phép bạn nhập h giờ p phút s giây sau se hẹn bắng một đoạn beep kêu liên tục cho đến khi nào dừng
đây là code :

#include <iostream>
#include <time.h> // Cho phép dùng time()
#include <Windows.h>
#include <conio.h>

using namespace std;

int main(){
int t0 = time(0);
int h, p, s;
cout << "Gio Phut Giay : ";
cin >> h >> p >> s;

int t = h*60*60 + p*60 + s;

while(1){
int t1 = time(0); (xem thêm trên cplusplus.com)

if(t1-t0 == t){
int k = 0;
while(!kbhit()){
Beep(500, 200 + k); // trong wINDOWs.h 
Sleep(100);
k += 20;
if(k > 300){
k = 0;
}
}

break;
}
}
return 0;
}


Cảm ơn các bạn đã xem.

Thứ Bảy, 26 tháng 12, 2015

Game rắn săn mồi viết bằng C++ và kĩ thuật lập trình hướng đối tượng (OOP)

đây là game rắn săn mồi được viết bằng C++ .

File header :
snake.h
link : http://pastebin.com/10aTLYDw

file snake.cpp
link : http://pastebin.com/H3set5cw

file main.cpp :

#include <iostream>
#include "snake.h"
#include <conio.h>

using namespace std;

int main(){
snake ob;
ob.veKhung();

while (1){
ob.veRan();
ob.veTao();

ob.dieuKhien();
ob.kiemTraAn();
ob.kiemTraThua();
}

_getch();
return 0;
}

Chế độ trên cho phép bạ chạm vào cơ thể của mình có một mode khác không cho phép bạn tự đâm mình . Bạn chỉ cần thêm code sau vào trong hàm kiemTraThua();

// mode dam la chet
for (int i = 1; i < soDuoi; i++){
if (tail[0].x == tail[i].x && tail[0].y == tail[i].y){
gameOver = true;
}
}

Vì nó được viết rất đơn giản nên rất mong nó có ích cho các bạn.

Thứ Ba, 22 tháng 12, 2015

Con trỏ và bộ nhớ [3] - Bộ nhớ heap - sử dụng và lưu ý

Trong bài trước ta đã tìm hiểu cơ bản về heap Con trỏ và bộ nhớ [2] - Bộ nhớ heap hôm nay ta sẽ sử dụng nó và đưa ra một vài lưu ý khi sử dụng heap.

. Heap Array


 Trong ngôn ngữ C, thật thuận tiện để allocate một mảng trong array, bởi vì C có thể xử lý mọi pointer giống như một mảng. Kích cỡ của khối nhớ của mảng là kích cỡ của từng phần tử (được tính toán nhờ toán tử sizeof()) nhân với số phần tử. Vì vậy ta có code phân bổ một mảng gồm 100 phần tử có kiểu dữ liệu struct fraction trong heap và cho tất cả các phần tử là 22/7 và giải phóng mảng heap .

 void heapArray(){
  struct fraction* fracts;

  // cấp phát cho mảng
  fracts = malloc(sizeof(struct fraction) * 100);

  // sử dụng nó như mảng - trong trường hợp này cho tất cả các giá trị là 22/7
  for(int i = 0; i < 99; i++){
  fracts[i].numerator = 22;
  fracts[i].denominator = 7;
  }

  // Thu hồi bộ nhớ của mảng
  free(fracts);
 }

 . Ví dụ về string trong heap


 Ở đây là một ví dụ về heap hữu ích hơn mảng. Hàm StringCopy() lấy xâu ,và sao chép xâu đó vào heap, và trả cho ta pointer trỏ tới new string . Caller sẽ chiếm quyền sở hữu của new string và có trách nhiệm free nó.

/*
Lấy string C, trả về heap được cấp phát và đã copy string vào. Vấp phát khối nhớ trong heap với một kích cỡ thích hợp, sao chép string vào khối và trả về con trỏ trỏ vào khối đó. Caller chiểm quyền sở hữu của khối đó và có trách nhiệm free nó.
*/


char* StringCopy(const char* string) {
char* newString;
int len;

len = strlen(string) + 1; // +1 to account for the '\0'
newString = malloc(sizeof(char)*len); // elem-size * number-of-elements
assert(newString != NULL); // simplistic error check (a good habit)
strcpy(newString, string); // copy the passed in string to the block

return(newString); // return a ptr to the block
}


 . Chú ý về Heap string


 StringCopy() có lợi thế về cả hai tính năng của bộ nhớ heap :


  #Size: StringCopy() xác định size , lúc chạy , kích thước xác định của khối cần được lưu trữ string trong nó , kích cỡ này được đặt trong lời gọi malloc(). Bộ nhớ local không thể làm được điều này bởi vì kích cỡ của nó cần được xác định trong thời gian biên dịch tức trước là trước lúc chạy. Lời gọi sizeof(char) thực sự không cần thiết, bởi vì kích cỡ của char là 1 đã được định nghĩa. Kích thước của mảng là size của một phần tử nhân với số phần tử.

  #Vòng đời. StringCopy() cấp phát khối nhớ, nhưng sau đó nó chuyển quyền sở hữu của nó cho caller. Bởi vì không có một lời gọi đến free() nào, vì vậy khối nhớ tiếp tục tồn tại thậm chí sau khi chương trình kết thúc. Bộ nhớ local không thể làm như vậy. Caller cần quan tâm đến việc thu hồi khi nó kết thúc .

 .Memory Leaks


 Điều gì xảy nếu như có một số vùng nhớ của heap được cấp phát, nhưng không bao giờ được thu hồi? Một chương trình quên việc thu hồi khối nhớ được nói là có "memory leak" có thể không hoặc cũng có thể có một vẫn đề rất nghiêm trọng. Kết quả sẽ là heap sẽ dần dần bị làm đầy khi tiếp tục có yêu cầu cấp phát, nhưng không có lệnh thu hồi để khối đó "re-use". Vơi một chương trình chạy, tính toán thứ gì đó , và kết thúc ngay tức thì, thì memory leaks thường không được quan tâm. Như một chương trình "một lần" nó bỏ qua mọi mọi lệnh thu hồi của nó nhưng vẫn làm việc. Memory leak có nhiều vẫn đề cho một chương trình nó chạy không xác định thời gian. Trong trường hợp đó, memory leak có thể dãn dần lấp đầy heap đến khi yêu cầu cấp phát không được đáp ứng và chương trình ngừng làm việc hoặc crashing. Có rất nhiều chương trình thương mại bị memory leak, có thể khi chạy đủ dài chũn sẽ lấp đầy heap và bị crash. Thông thường phát hiện lỗi và tránh code bị heap đầy không được kiểm nghiệm tốt, chính vì trường hợp heap đầy hiểm khi gặp với chương trình chạy ngắn nên nhiều chương trình chạy ngắn gây leak memory - đó là lý do tại sao heap đầy sẽ cho kết quả là crash thay vì một lời báo lỗi . Phần lớn trình biên dịch có "heap debugging" hữu ích để thêm debugging vào chương trính để kiểm soát mọi lời cấp phát và thu hồi. Khi một cấp phát không có thu hồi, nguy cơ leak, heap debugger có thể giúp bạn tìm nó.

 .Ownership


 StringCopy() cấp phát khối nhớ heap, nhưng nó không thu hồi nó. Đây là để caller có thể sử dụng string mới. Tuy nhiên, điều này trình bày cho ta một lỗi  mà một vài người cần phải nhwor thu hồi lại khối nhớ , và nó sẽ không phải là StringCopy(). Đó là lý do tại sao comment của StringCopy() đề cập cụ thể là caller sẽ chiếm quyền sở hữu của khối . Mọi khối nhớ có chính xác một "chủ nhân" , có trách nhiệm giải phóng nó. Trên phương diện khác thì có thể có con trỏ, nhưng nó chỉ là "sharing". Nó cũng là một chủ sở hữu và comment cho StringCopy() làm cho nó rõ ràng là quyền sở hữu được truyền từ StringCopy() đến caller. Một tài liệu hay luôn nhớ bàn luận đến điều luật về quyền sở hữu. Bằng cách này hay cách khác luôn các tài liệu phải luôn bàn luận đến quyền sở hữu cho tham số hoặc giá trị trả về. Nó là một con đường mà lỗi bộ nhớ hoặc leak sẽ bị tạo ra.

 .Mô hình ownership 

 Có hai mô hình chung cho việc sở hữu đó là :

  # Caller sở hữu. caller tự nó sở hữu bộ nhớ của nó. Nó có thể thông qua con trỏ tới callee cho mục đich "sharing", nhưng caller giữ lại quyền sở hữu. Callee có thể truy cấp những thứ đó khi nó chạy, và có thể cấp phát và thu hồi bộ nhớ của nó, nhưng nó sẽ không làm gián đoạn gì đến bộ nhớ của caller.

  # Callee cấp phát và trả về. Callee cấp phát một vài bộ nhớ và trả nó về cho caller. Điều này xảy ra bởi vì kết quả của hoạt động của callee cần đến vùng nhớ mới để lưu trữ hoặc miêu tả. Vung nhớ mới này được chuyển qua cho caller vì vậy chúng có thể nhìn thấy kết quả và caller có thê chiểm quyền sở hữu với khối nhớ đó. Đây là kiểu mẫu được miêu tả trong StringCopy().

.Tóm lại.


Heap memory cung cấp một sự điều khiển tuyệt vời cho các lập trình viên - những khối nhớ có thể được yêu cầu với một vài kích cỡ, và chúng tiếp tục được cấp phát cho đến khi nó được giải phóng (deallocated) một cách rõ ràng. Heap memory có thể thông qua lại caller bởi vì nó không bị thu hồi khi kết thúc và nó sử dụng cho cấu trúc liên kết như : linked list, binary trees, ... . Khuyến điểm của heap là chương trình phải làm công việc cấp phát và thu hồi một cách rõ ràng gọi đến trình quản lý heap.  Heap không điều hành tự động cà tiện lợi như local memory làm.


Cảm ơn các bạn đã xem .

Chuyển đổi kiểu trong lập trình (type conversion) : Chuyển đổi ngầm (implicit conversion)

Khi lập trình có rất nhiều lúc xảy ra chuyển kiểu, hôm nay ta sẽ tìm hiểu về các loại của chuyển kiểu và các lưu ý khi chuyển kiểu.

Chuyển kiểu (conversion) Có hai kiểu chuyển đổi cơ bản : implicit conversion, compiler tự động chuyển từ kiểu này đến kiểu khác và explicit conversion, lập trình viên sử dụng toán tử casting để điều khiển việc chuyển đổi. Hôm nay ta sẽ tìm hiểu chuyển kiểu ngầm

+ Chuyển đổi ngầm (implicit conversion)

Ta đã biết giá trị của một biến được lưu trữ với một dãy bit, và kiểu dữ liệu của biến nói cho trình biên dịch diễn tả các bit đó vào các giá trị có ý nghĩa. Các kiểu dữ liệu khác nhau có thể tượng trưng với cùng một số - ví dụ, kiểu integer có giá trị là 3 và float có giá trị là 3.0 được lưu trữ dưới các dạng binary hoàn toàn khác nhau.

Vậy điều gì xảy ra nếu ta làm thứ như sau :

float f = 3; // gán một integer 3 cho biến float

Với ví dụ trên, compiler không thể chỉ copy dãy bit đại diện cho giá trị của 3 sang f được. Thay vào đó nó cần chuyển int 3 sang kiểu số thực, cái mà có thể gán cho f.

Tiến trình của sự chuyển đổi giá trị từ một kiểu dữ liệu này sang kiểu dữ liệu khác được gọi là chuyển đổi kiểu "type conversion". Chuyển đổi kiểu có thể xảy ra ở rất nhiều trường hợp khác nhau:

Gán hoặc khởi tạo một biến với giá trị không cùng kiểu :

double d = 3; // gán một số nguyên có giá trị bằng 3 cho một biến kiểu double
double s(3); // khởi tạo kiểu double từ một kiểu nguyên có giá trị bằng 3

Truyền giá trị vào hàm mà tham số của hàm có kiểu dữ liệu khác :

void doSomething(long i){
// do anything
}

doSomething(3); // truyền vào giá trị 3 kiểu int cho hàm cần tham số kiểu long

Trả về một giá trị từ một hàm mà kiểu trả về của hàm cần là kiểu khác :

float fo(){
return 3.0; // Trả về kiểu double có giá trị là 3.0 cho hàm cần trả về kiểu float
}

Sử dụng toán tử với hai toán hạng khác kiểu :

double divi = 4.0 / 3; // Phép chia với double và intteger

Với các ví dụ trên (và một vài ví dụ khác), C++ sẽ sử dụng chuyển đổi kiểu để chuyển đỗi dữ liệu từ kiểu này sang kiểu khác

Có hai loại chuyển đổi kiểu cơ bản là implicit type conversion, trình biên dịch sẽ tự động chuyển dữ liệu từ một kiểu cơ bản sang một kiểu dữ liệu khác, và explicit type conversions, lập trình viên sử dụng các toán tử chuyển đổi để điều khiển trực tiếp việc chuyển đổi.

Chúng ta sẽ xem xét implicit type conversion ở bài này và tìm hiểu explicit type conversion ở bài tiếp.

Implicit type conversion


Implicit type conversion cũng được gọi là kiểu chuyển đổi tự động hay coercion được chuyển đổi bất cứ khi nào một kiểu dữ liệu cơ bản được mong đợi, nhưng lại cung cấp một kiểu dữ liệu cơ bản khác, và người dùng không nói rõ ràng cho máy tính là chuyển đổi như thế nào.

Tất cả các ví dụ trên đều là trường hợp của chuyển đổi ngầm được sử dụng.

Có hai loại cơ bản của chuyển đổi kiểu ngầm là : promotions và conversions.

.Numeric promotion


Bất cứ khi nào một giá trị từ một kiểu được chuyển đến giá trị của kiểu tương tự nhưng rộng hơn, nó được gọi là numeric promotion (hay là widening , nó thường xuyên dành cho kiểu int). Cho ví dụ, kiểu int có thể chuyển đến kiểu rộng hơn là long, float chuyển đến double.

long l(64); // mở rộng số nguyên 64 trong long
double d(0.12f); // "thăng cấp" float 0.12 trong kiểu double

Trong thuật ngữ "numeric promotion" chuyển một vài loại hình promotion,cũng có hai kiểu chuyển đổi khác cần lưu ý :

+ Intergral promotion liên quan đến chuyển đổi kiểu đổi của integer đến kiểu có phạm vi hẹp hơn int (ví dụ như bool, char, usigned char, signed char, usigned short , signed short) đến kiểu int (nếu có thể) hoặc kiểu unsigned int.

+Float point promotion liên quan đến chuyển đổi từ kiểu float sang double.


Hai loại trên đều được sử dụng trong các trườn hợp cụ thể đặc biệt để chuyển một kiểu dữ liệu nhờ nhơn đến int/ usigned int hay double, bởi vì thực hiện trên kiểu dữ liệu đó có hiệu suất nhất.

Một thứ quan trọng cần nhớ về promotions là chúng luôn an toán và không có dữ liệu nào bị mất.

.Numeric conversions


Khi chúng ta chuyển đổi một giá trị từ liệu kiểu dữ liệu rộng hơn rang kiểu dữ liệu tương tự nhưng bé hơn hoặc hai kiểu khác biệt, nó được gọi là numeric conversions. Ví dụ :

double d = 3; // chuyển kiểu integer 3 đến kiểu double
short s = 2; // chuyển integer 2 đến short

Không giống như loại promotions luôn an toàn thì chuyển đổi kiểu numeric conversions có thể có hoặc cũng có thể không gây ra mất dữ liệu.  Do điều này , một vài code thực hiện chuyển đổi ngầm sẽ thường xuyên dẫn tới compiler đưa ra cảnh báo.

Luật của kiểu chuyển đổi này rất phức tạp và nhiều, vì vậy chúng ta chỉ đề cập đến các trường hợp phổ biến ở đây.

Trong tất cả các trường hợp, chuyển đổi giá trị đến một kiểu không có phạm vi đủ lớn để hỗ trọ biểu diễn giá trị đó sẽ dẫn tới kết quả ngoài mong đợi. Nên tránh điều này. Ví dụ:

int main(){
int i = 30000;
char c = i;

std::cout << static_cast<int>(c);

return 0;
}

Trong ví dụ này chúng ta đã gán một giá trị rất lớn kiểu int cho char (giới hạn từ -128 đến 127). Điều này dẫn tới char sẽ bị tràn. Nó sẽ đưa ra kết quả là : 48

Tuy nhiên , việc chuyển đổi từ kiểu rộng hay kiểu dấu phảy động đến kiểu tương tự nhở hơn nhìn chung là vẫn làm việc miễn là giá trị này phù hợp với phạm vi của kiểu nhỏ hơn. Ví dụ:

int i = 2;
short s = i; // int đến short
std:: cout << i;

double d = 0.1234;
float f = d;
std:: cout << f;

có kết quả là 2 và 0.1234 .

Trong trường hợp giá trị là dấu phảy động, một vài phép làm tròn có thể xảy ra dẫn tới việc mất dữ liệu cho kiểu nhỏ hơn.

Ví dụ :
float f = 0.123456789; // kiểu double 0.123456789 có 9 chữ số thập phân, nhưng float chỉ có thể
                       // biểu diễn được 7 số

std:: cout << std::setprecision(9) << f;

Trong trường hợp này ta nhìn mất đi một số sau dấu phẩy bởi vì float không thể tổ chức nhiều số chữ số sau dấu phảy bằng double. (nó chỉ lưu trữ được 7 số) nên có kết quả :
0.123456791

Chuyển đổi từ một integer đến dấu phảy động nhìn chung là làm việc được miễn là trong phạm vi của nó. Ví dụ :

int i = 10;
float f = i;
std::cout << f;

kết quả là 10 .

Chuyển từ dấu phảy động sang integer làm việc được miễn là nằm trong khoảng giá trị của nó, nhưng phần thập phân bị mất ví dụ :

int i = 3.5;
cout << i;

kết quả là 3


.Phỏng đoán một biểu thức


Khi tính toán một biểu thức với các kiểu khác nhau, compiler tách từng biểu thức ra làm biểu thức con. Các toán tử toán học yêu cầu toán hạng của nó cùng kiểu. Nếu các toán hạng không cùng kiểu, compiler sẽ thực hiện chuyển đổi ngầm để chuyển một toán hạng. Để làm được điều này, nó phải tuân thủ các luật lệ sau:

+ NẾu toán hạng là kiểu nguyên, nó trải qua sự chuyển đổi "integral promotion" như đã nói ở trên.

+ Nếu toán hạng vẫn không phù hợp, compiler tìm toán hạng ưu tiên nhất (trong các toán hạng có trong biểu thức đó) và chuyển nó sang để phù hợp.

Toán hạng được ưu tiên như sau :

+ long double (highest)
+ double
+float
+ unsigned long long
+ long long
+unsigned long
+ long
+ usigned int
+ int (lowest)

Chúng ta có thể xem sự chuyển đổi số học qua việc sử dụng typeid() (trong file typeinfo header), nó có thể thường được dùng để chỉ ra kết quả của việc chuyển đổi.


#include <iostream>
  #include <typeinfo> // for typeid()

int main()
{
    short a(4);
    short b(5);
    std::cout << typeid(a + b).name() << " " << a + b << std::endl; // show us the type of a + b

   return 0;
}


Vì short là integer , chúng được trải qua "integral promotion" để chuyển thành int trước khi cộng. Kết quả của phép cộng 2 int là int, như chúng ta mong đợi :
int 9

Chúng ta xem một trường hợp khác :

#include <iostream>
#include <typeinfo> // for typeid()

int main()
{
    double d(4.0);
    short s(2);
    std::cout << typeid(d + s).name() << " " << d + s << std::endl; // show us the type of d + s

    return 0;
}

Trong trường hợp này , short trải qua "integral promotion" thành int. Tuy nhiên, int cộng double vẫn không hoạt động (do không cùng kiểu). Do double là kiểu có độ ưu tiên cao hoen nên integer được chuyển thành double 2.0, và double + double là double.

=> kết quả : double 6.0

Hệ thống cấp bậc này có thể dẫn tới một vài vấn đề thú vị. Ví dụ, xem code :
std::cout << 5u - 10; // 5u có nghĩa là 5 như kiểu unsigned intteger

Bạn có lẽ mong rằng biểu thức 5u - 10 được tính là -5 vì 5 - 10 = -5 . Nhưng thực tế kết quả là
4294967291

Trong trường hợp này, signed integer (10) được "nâng cấp" lên thành unsigned int( vì nó cod thự tự ưu tiên cao hơn), và biểu thức được tính với unsigned integer. Kết quả overflow  và chúng ta nhận kết quả ko như mong đợi

Đây là một trong rất nhiều lý do để tránh việc sử dụng unsigned int.

Cảm ơn các bạn đã dành thời gian để xem .

Con trỏ và bộ nhớ [2] - Bộ nhớ heap

Lần trước chúng ta đã tìm hiểu các khái niệm về local memory trong bộ nhớ Tìm hiểu con trỏ và bộ nhớ [1] . Bên cạnh các điểm mạnh thì nó cũng có các điểm yếu, để khắc phục ta sẽ tìm hiểu một vùng nhớ rất quan trọng trong programming là HEAP MEMORY , hôm nay ta tìm hiểu một vài khái niệm đơn giản và cách sử dụng của heap.

[HEAP MEMORY]


Bộ nhớ heap , cũng được biết đến như là bộ nhớ động, nó thay thế cho bộ nhớ local. Local memory đã bàn ở phần trước, nó khá tự động - Nó được cấp phát tự động trên lời gọi function và nó được deallocated tự động khi function kết thúc. Heap memory khác trên mọi phương diện. Lập trình viên bắt buộc phait yêu cầu một cách rõ ràng cho việc cấp phát bộ nhớ với một size cụ thể, và khối đó vẫn được ấp phát cho đến khi lập trình viên yêu cầu deallocate một cách rõ ràng. Không có thứ gì xảy ra một cách tự động. Vì vậy lập trình viên điều khiển bộ nhớ nhiều hơn, nhưng trách nhiệm của lập trình viên nhiều hơn bởi vì bộ nhớ lúc này phải được quản lý tích cực. 
  

  * Lợi ích của heap memory rất nhiều ...



    # Vòng đời(lifetime) Bởi vì lập trình viên bây giờ điều khiển chính xác khi bộ nhớ được phân bổ và bị deallocated, nó có thể khả năng xây dựng một cấu trúc dữ liệu ở bộ nhớ và trả về cấu trúc dữ liệu đó cho caller (hàm gọi - như đã nói ở bài trước). Điều này không bao giờ là có thể đối với local memory , điều này là vì nó tự động deallocated khi function kết thúc


    # Kích cỡ (Size) Kích cỡ của bộ nhớ được cấp phát có thể được điều khiển một cách chi tiết hơn. Ví dụ một chuỗi có thể được allocated tại thời gian chạy , nó phải là một kích cỡ chính xác phù hợp để tổ chức một chuỗi cụ thể. Với local memory thì code giống như khai báo một bộ đếm có kích cỡ là 1000 và hi vọng nó phù hợp nhất .

  * Khuyết điểm của việc sử dụng bộ nhớ heap.


    # Làm nhiều việc hơn. Cấp phát với bộ nhớ heap cần phải bố trí một cách rõ ràng trong code, điều này chỉ làm nhiều việc hơn.

    # Nhiều bug. Bởi vì nó bây giờ được làm một cách rõ ràng trong code, cấp phát có thể sẽ thực hiện không đúng dẫn đến memory bugs. Mặc dù local memory giới hạn nhưng nó không bao giờ sai.

Dù sao thì có rất nhiều vẫn đề có thể chỉ được giải quyết với heap memory. Trong nhiều ngôn ngữ với thùng thu gom rác như Perl , LISP, Java thì khuyết điểm trên của heap phần lớn được loại bỏ. Thúng thu gom rác chiếm phần lớn trách nhiệm cho việc quản lý heap , với chi phí thêm một chút thời gian trong thời gian chương trình chạy (run-time).

Vậy heap hoạt động như thế nào ?


Trước khi xem chi tiết ta cùng xem một vài ví dụ về việc cấp phát và thu hồi trong heap...

. Allocation


Heap là một vùng nhớ rất rộng có sẵn để chương trình sử dụng. Chương trình có thể yêu cầu vùng hoặc khối nhớ để nó sử dụng ở trong heap. Để cấp phát một khối nhớ, chương trình làm yêu cầu minh bạch gọi hàm cấp phát của heap. Hàm cấp phát sẽ để riêng một khối nhớ vừa được yêu cầu kích cở trong heap và trả về con trỏ trỏ vào nó. Giả sủ chương trình làm ba cấp phát yêu cầu cấp phát động , tổ chức nó riêng biệt để lưu trữ ảnh GIF trong heap , mỗi kích cỡ là 1024 bytes. Sau khi yêu cầu cấp phát, bộ nhớ được nhìn như thế này :

 

 Mỗi cấp phát yêu cầu dành riêng một vùng nhớ liên tục với kích cỡ được yêu cầu trong heap và trả về con trỏ trỏ đến khỗi nhớ đó cho chương trình. Bởi vì khỗi này luôn được trỏ bởi con trỏ, nên khối này luôn có vai trò như "pointee - đã nhắc ở phần 1" và chương trình luôn dùng khối bộ nhớ thông qua con trỏ. Con trỏ của khối nhớ heap thỉnh thoảng được biết đến như địa chỉ cơ sở bởi vì theo hiệp ước chúng chỉ tới địa chỉ cơ sở của khối (chính là byte địa chỉ thấp nhất).

Trong ví dụ trên, ba khối được cấp phát LIÊN TỤC TỪ PHÍA DƯỚI của bộ nhớ heap và mỗi khối là 1024 byte như kích cỡ được yêu cầu. Trên thực tế quản lý heap "heap manager" có thể cấp phát khối nhớ bất cứ nơi nào nó muốn trong heap miễn là những khối không chồng chéo lên nhau và có ít nhất một yêu cầu có kích cỡ. Tại một vài thời điểm cụ thể, một vài vùng trong heap đã được cấp phát cho chương trình , vì vậy nó được coi là đang sử dụng "in use". Vài vùng khác chưa được ủy thác nên nó được coi là tự do "free" và có sãn để đáp ứng cho yêu cầu cấp phát. Quản lý vùng nhớ heap tự có riêng của mình câu trúc dữ liệu private "private data structures " để ghi lại đâu là vùng nhớ của heap đã được giao phó cho mục đích tại một vài thời điểm. Quản lý heap đáp ứng cho mỗi yêu cầu cấp phát thừ khối bộ nhớ đang tự do "free memory" sau đó cập nhật nó vào private dât structures để ghi nó lại là bộ nhớ đó đang trong sử dụng.


.Thu hồi (deallocation)


Khi chương trình đang sử dụng khối nhớ kết thúc, nó làm một yêu cầu thu hồi (deallocation) minh bạch để chỉ cho heap manager rằng chương trình đó đã kết thúc bây giờ với khối nhớ. Heap manager cập nhất private data structures của nó chỉ ra rằng vùng nhớ đã chiếm đóng đã free trở lại và vì vậy có thể tái sử dụng "re-used" để đáp ứng cho tính năng yêu cầu allocation. Đâu là những gì mà heap muốn thấy nếu chương trình deallocates khối thử hai của ba khối :



Sau khi deallocation, con trỏ tiếp tục trỏ vào khối đã bị deallocated ngay lúc đó. Nhưng chương trình không được truy cập vào vùng nhớ đã bị deallocated. Đó là lý do tại sao con trỏ được vẽ màu xám - CON TRỎ VẪN Ở ĐÓ NHƯNG NÓ KHÔNG ĐƯỢC SỬ DỤNG. Thỉnh thoảng code sẽ đặt con trỏ trỏ tới NULL ngay tức thì sau khi deallocation được làm một cách minh bạch thực tế điều này khồn có giá trị.

. Lập trình với heap (programming the heap)

Lập trình với heap rất giống nhau trogn nhiều ngôn ngữ.  Tính năng căn bản là :

    # Heap là một vùng nhớ có sẵn để cấp phát vùng nhớ cho chương trình

    # Có một vài thư viện quản lý heap, nó quản lý cho chương trình. Lập trình viên cần thực hiện lời yêu cầu đến quản lý heap, nó lần lượt quản lý những thứ bên trong của heap. Trong C, heap được quản lý bởi thư viện hàm ANSI malloc(), free() và realloc().

    # Bộ quản lý Heap sử dụng cấu trúc dữ liệu của chính nó để theo dõi khối nào trong heap là "free" (có thể dùng để sử dụng) và khối nào hiện tại đã sử dụng bởi chương trình và độ rộng của nó như thế nào. Lúc mới cài đặt (hay ban đầu) thì tất và heap đều "free"

    # Heap có lẽ là một bộ nhớ giới hạn, hoặc nó có thể giống như một khối bị giới hạn nhưng kích cỡ cực kì rộng do được hỗ trợ bởi bộ nhớ ảo. Trong cả hai trường hợp, sẽ là có thể nếu heap bị đầy bởi việc cấp phát và vì vậy nó không thể đáp ứng được yêu cầu cấp phát. Hàm cấp phát sẽ thường trả về con trỏ NULL.

    # hàm cấp phát yêu cầu một khối trong heap với một kích cỡ cụ thể. Trình quản lý heap chọn một vùng nhớ để sử dụng cho việc đáp ứng yêu cầu đó, đánh dấu vùng nhớ đó như "in use" trong cấu trúc dữ liệu riêng của nó (private data structures), và trở về một con trỏ trỏ tới khối nhớ heap đó. Caller bây giờ tự do sử dụng vởi tham chiếu của con trỏ. Khối nhớ được đảm bảo là dành riêng quyền sử dụng cho caller - heap sẽ không trao vùng nhớ đó cho bất kì caller nào khác. Khối không di chuyển xung quanh ở trong heap - vị trí của nó và kích cỡ của nó là giới hạn ngay khi nó được cấp phát. Nói chung chung, khi một khối được cấp phát , nội dung của nó là bất kì. Chủ sở hữu mới phải chịu trách nhiệm thiết lập cho bộ nhớ cái gì đó có nghĩa. Thỉnh thoảng hàm cấp phát sẽ thiết lập tất cả khối nhớ là 0 (calloc() trong C).

    # Hàm thu hồi (deallocation function) đối lập với hàm cấp phát. Chương trình cho một lời gọi thu hồi để trả khối nhớ về cho vùng "free" của heap để tái sử dụng "re-use" sau đó. Mỗi một khối chỉ nên deallocated một lần. Chức năng deallocation làm cho tham biến là con trỏ trỏ tới heap mất vùng nhớ mà được cung cấp bởi chức năng allocation trước đó. Sau khi deallocation thì con trỏ phải trỏ chính xác vào khối nhớ giống như con trỏ trước đó đực cung cấp bởi chức năng allocation, mà không phải bất kì con trỏ nào trong block. Sau khi deallocation, chương trình xử lý pointer như "bad pointer" và không được truy cập vào pointee đã bị deallocated.


.Những đặc biệt trong C


Trong ngôn ngữ C, thư viện hàm thực hiên yêu cầu cho heap là malloc() (memory allocate) và free(). Nguyên mẫu cho những hàm trên trong file header <stdlib.h>. Mặc dù là cú pháp khác nhau giữa các ngôn ngữ nhưng chức năng của malloc() và free() trong mọi ngôn ngữ là tương tự nhau:
# void* malloc(unsigned long size); hàm malloc() lấy tham số truyền và là unsigned integer tượng trưng cho yêu cầu về kích thước của khối đo bằng bytes. Malloc() trả về một con trỏ trỏ tới khối nhớ mới trong heap nếu cấp phát thành công, và NULL nếu yêu cầu không đươc đáp ứng bởi vì heap đã đầy. C cung cấp toán tử sizeof() là một các thuận tiện để tính kích cở với byte của kiểu dữ liệu - sizeof(int) là kích cỡ của pointee kiểu int ... 

# void free(void* heapBlockPointer); hàm free() lấy con trỏ trỏ tới khối nhớ heap và trả nó lại vùng free để sau này tái sử dụng "re-use". Con trỏ qua free() phải chính xác là con trỏ trả về do malloc() trước đó, không phải là con trỏ trỏ nơi nào đó trong block. Tức là gọi free() và malloc() thì con trỏ vẫn  thế không thay đổi chỉ khác là pointee không được truy cập vào nữa. Gọi hàm free() với các loại lỗi sai của con trỏ là nổi tiếng đặc biệt kinh khủng với loại crashing mà nó dẫn đến. Lời gọi tới free() không cần cho kích cỡ của khối heap - trình quản lý heap sẽ tự động lưu ý kích cỡ trong private dât structures. Lời gọi tới free() chỉ cần xác định khối nào cần deallocate, bởi con trỏ của nó. Nếu chương trình deallocate đúng tất cả vùng nhớ mà nó allocate, thì sau đó mọi lời gọi đến malloc() sau đó sẽ được xuất hiện bời chính xác một lời gọi free() là một vấn đề thực tế, tuy nhiên nó không phải luôn luôn cần thiết cho một chương trình để dellocate mọi khối mà nó allocate - Hãy xem "Memory Leaks" bên dưới.

.Ví dụ đơn giản về heap


Đây là một ví dụ đơn giản thứ mà nó cấp phát một khối kiểu int trong heap, lưu số 42 vào trong khối đó, và sau đó deallocate nó. Đây là ví dụ đơn giản nhất có thể của heap với ba công đoạn : cấp phát (allocation), sử dụng (use), và giải phóng (deallocation). Ví dụ chỉ ra tình trạng của bộ nhớ tại ba lần khác nhau trong suất thời gian thực thi của code bên dưới. Stack và heap cần phân biệt giữa hai vùng để thật chính xác với vì hai khu vực rất khác nhau. Trong trường hợp đó vòng đời của biến local intPtr hoàn toàn tách biệt với vòng đời của khối nhớ trong heap phân cho nó và hình vẽ vần phản chiếu lại điều khác nhau đó.
 

 Những chú ý cơ bản của heap

  # Sau khi allocation gọi và cấp phát một khối trong heap. Chương trình lưu con trỏ trỏ tới block đó vào trong biến local intPtr. Khối này là pointee và intPtr là con trong của nó (được chỉ trong T2). Trong trạng thái này, con trỏ có lẽ được được dereferenced an toàn để điều khiển pointee."Bộ luật" pointer và pointee trong phần 1 đã được áp dụng, chỉ có một điều khác nhau duy nhất là cách mà pointee được khởi tạo cấp phát ban đầu.

  # Tại T1 trước khi gọi malloc(), intPtr không được khởi tạo không có pointee - tại điểm này inPtr "bad" giống hoàn cảnh như đã bàn ở phần 1. Như trước dereferencing với một con trỏ không được khởi tạo là một phổ biến , nhưng lỗi thàm khốc. Thỉnh thoảng  lỗi này gây crash ngay tức thì (trường hợp này may mắn). Lần khác nó sẽ làm hỏng một cấu trúc dữ liệu bất kì (trường hợp không may mắn.)

  # Lời gọi đến free() giải phóng POINTEE như được chỉ ra tại T3, hãy nhớ đây là giải phóng pointee nên nó được giải phỏng bởi tất cả các pointer đang chỉ vào nó. Sự tham chiếu của pointer sau khi pointee bị deallocated là một lỗi. 
  Thật không may , lỗi này phần lớn không được gắn cờ như một cách trực tiếp nên nó là lỗi run-time, chỉ thấy khi chạy. 99% số lần tham chiếu (sử dụng * truy cập vào vùng nhớ là con trỏ đang trỏ tới) sẽ cho một kết quả hợp lý, 1% còn lại của tham chiếu sẽ cho kết quả sai. Vậy chắc chắn, một lỗi hiếm khi xuất hiện là loại khó nhất để theo dõi.

  # Khi function kết thúc, biến local của nó intPtr sẽ tự động deallocated đi với điều luật thướng thấy ở phần 1 . Vì vậy function này có một hành vi bộ nhớ gọn gàng - Tất cả bộ nhớ của nó allocate trong khi chạy (bieens local của nó và heap block của nó)được deallocated khi nó kết thúc.

Cảm ơn các bạn đã dành thời gian để theo dõi .

Xem phần tiếp theo tại : Con trỏ và bộ nhớ [3] - Bộ nhớ heap - sử dụng và lưu ý .

Thứ Ba, 15 tháng 12, 2015

Tìm hiểu con trỏ và bộ nhớ [1]

Con trỏ là một kiểu chứa địa chỉ của biến mà nó trỏ đến, biến mà nó trỏ đến theo thuật ngữ gọi là pointee
, qua con trỏ ta có thể truy cập được vào ô nhớ mà con trỏ đang tham chiếu, nhưng con trỏ bắt buộc phải chiếu đến một pointee trước khi nó truy cập vào pointee

NULL pointer là một con trỏ đặc biệt , được hiểu rằng nó không trỏ đến cái gì. Nó có ý nghĩa là con trỏ không trỏ đến pointee nào . Trong ngôn ngữ C kí hiệu NULL được sử dụng cho mục đích này. Chương trình bị runtime error nếu ta cố tình tham chiếu con trỏ NULL. (NULL có giá trị kiểu int là 0).

- Gán 2 con trỏ:
Việc gán 2 con trỏ sử dụng operator "=" giữa hai con trỏ làm cho con trỏ trỏ cùng đến một pointee. Việc hai con trỏ cùng trỏ đến một pointee theo thuật ngữ tin học người ta gọi nó là "sharing"

- Shallow and Deep copying
  http://theironns.blogspot.com/2015/12/shallow-copy-va-deep-copy.html

- Cần initation cho con trỏ vừa khai báo , vì ngày khi vừa khai báo, con trỏ chỉ được phân bổ vùng nhớ nhưng chưa cho trỏ tới pointee nào, người ta gọi là bad value

- Toán tử * là toán tử đơn dành cho con trỏ để dereference tới pointee mà nó đang trỏ, nó gây ra runtime nếu con trỏ không trỏ tới pointee nào.

- Con trỏ phải được trỏ đến pointee trước khi sử dụng, nếu không sẽ bị crash
Trường hợp dưới đây may mắn được compiler báo lỗi :
 #include <iostream>

using namespace std;

int main(){
int* p = new int;
int**q = new int*;
int* i;
cout << i << " " << *i;
*i = 7;
return 0;
}

error C4700: uninitialized local variable 'i' used

Dưới đây là các luật cần phải nắm rõ khi sử dụng con trỏ:

- Con trỏ lưu địa chỉ tới pointee của nó, pointee là nới chưa các giá trị hữu ích .

- Toán tử derefrence "*" tham chiếu giúp con trỏ tham chiều tới pointee của nó. Một con tro có thể được dereference sau khi  gán nó trỏ tới một pointee. Phần lớn các bug liên quan đến con trỏ đều liên quan đến việc vi phạm điều này.

- Việc phân bổ một con trỏ không tự động gán nó trỏ tới một pointee. Gán con trỏ trỏ tới một pointee đặc biệt là một sự thi hành riêng vì vậy nó rất dễ bị lãng quên.

- Việc gán giữa hai con trỏ cho nhau làm chúng trỏ cùng tới một pointee giống nhau

Biến locals của function.


Khi gọi một hàm con trong một hàm khác thì tất cả các biến local của nó được phân bổ bộ nhớ chứ không phân bố lần lượt như cứ gặp khai báo biến nào thì biến đó mới được phân bố bộ nhớ cho.

- The locals are allocated when their function begins running and are deallocated when it exits

# Thuận tiện của việc dùng biến cục bộ (locals variable)

- Thuận tiện: Cục bộ đáp ứng một nhu cầu thuận tiện - những hàm thường xuyên cần bộ nhớ tạm thời thứ mà tồn tại trong suất thời gian tính toán của hàm. Biến cục bộ cung cấp bộ nhớ tạm thời ngắn hạn, một vùng nhớ độc lập

- Hiệu quả : Liên quan tới các kĩ thuật sử dụng bộ nhớ khác . Allocating and deallocating chúng rất hiệu quả , rất nhanh và chúng là không gian hiệu quả trong cách chúng sử dụng và tái sử dụng bộ nhớ (hình ảnh).

- copy value of local: tham số cục bộ là một kiểu copy cục bộ cơ bản lấy thông tin từ lời gọi. Cái này được biết như là "tham trị". Tham số là các biến cục bộ, thứ được init với toán tử gán (=) toán tử được gọi. Lời gọi không chia sẻ (sharing) giá trị tham số cúng cho callee như trong cách dùng con trỏ (với pointee) - callee nhận nó làm của riêng khi copy. Điều này có một lợi ích là callee có thể thay đổi giá trị mà nó mà ko ảnh hưởng đến caller. Cái độc lập này rất tốt vì nó giữ hoạt động của caller và callee của function tách biệt - Đây là một qui tắc rất quan trọng cho kĩ sư phần mềm: đó là giữ cho các thành phần tách biệt nhất có thể

# Điểm yếu của biến cục bộ

- Vòng đời ngắn. allocation và deallocation của chúng rất khăc nghiệt. Đôi khi một chương trình cần bộ nhớ thứ tiếp tục được phân bổ (vẫn cần dùng đến biến cục bộ) ngay cả sau khi hàm (hàm ban đầu đã allocated co nó ) kết thúc. Biến cục bộ sẽ không làm việc bởi vì chúng tự động deallocated khi hàm của nó kết thúc. Vẫn đế này sẽ được giải quyết với heap memory.

- Giao tiếp hạn chế: bới vì biến cục bộ copy từ tham biến caller , chúng không cung cấp phương tiện giao tiếp lại với caller , đây chính là nhược điểm của lợi ích "phải tách biệt" ở trên. Vấn đề này được giải quyết bởi tham chiếu (reference parameters ).


# Có một vài từ đồng nghĩa cới "Local"
Biến local cũng được biết đến như biến "automatic" vì cấp phát và deallocation(trả vung nhớ về cho máy) được làm một cách tự động như là một phần của cơ chế gọi của function. Biến local cũng được biết như biến "stack" vì , ở mức thấp, ngôn ngữ gần như luôn luôn thực hiện biến local sử dụng với cấu trúc stack trong bộ nhớ

# Dấu và (&)
Chúng ta đã hiểu cách phân bổ và làm việc của locals variable, bạn có thể đánh giá một trong những đoạn bug tồi tệ nhất có thể xảy ra trong C và C++. Điều gì sai với đoạn code sao ở nơi function Victim gọi function TAB(). Hãy xem vấn đến và nó có thể hữu ích khi làm một vài hình ảnh vẽ ra bộ nhớ local của 2 function :

int* TAB(){
int temp;
return (&temp); // trả về đia chỉ của biến local kiểu int
}

void Victim(){
int* ptr;
ptr = TAB();
*ptr = 42// Error!
}

TAB() hoàn toàn tốt trong khi nó chạy. Vẫn đề xảy ra khi lời gọi sau khi TAB() thoát. TAB() trả về một con trỏ trỏ tới kiểu int, nhưng đâu là nơi int đó được cấp phát? Vấn đề là những biến local temp được phân bố chỉ khi TAB() đang chạy. Khi TAB() kết thúc thì nó bị deallocate. Tất cả các biến local của nó đều bị deallocate khi nó thoát.

Tóm lại là chúng ta đã sử dụng lại bộ nhớ khi bộ nhớ đó đã bị deallocate trong ví dụ trên nên nó dẫn tới lỗi.


#Tóm lại cho bộ nhớ local
Locals rất thuận tiện với những gì chúng làm - cung cấp một bộ nhớ tiện ích và hiệu quả cho function thứ mà tồn tại chỉ khi nó đang thực thi. Locals có hai khiếm khuyết - Làm thế nào để giao tiếp lại với caller của nó (caller đã đề cập cách nói này ở trên) và làm thế nào mà function phân bố bộ nhớ với vòng đời bị giới hạn của nó. (Ta xem ở bài sau).

Cảm ơn các bạn đã xem . Hẹn gặp lại các bạn.

Xem phần tiếp theo tại Con trỏ và bộ nhớ [2] - Bộ nhớ heap

Thứ Tư, 9 tháng 12, 2015

Shallow copy và deep copy

Shadow copy và Deep copy

 Giới thiệu mở đầu

Một function thông qua con trỏ lấy giá trị của một ô nhớ của một chương trình khác . Cả hai function này  đều truy cập được giá trị của ô nhớ nhưng bản thân giá trị của ô nhớ không sao chép ra. Phương thức này được gọi là "shallow" bởi vì thay vì một bản sao giá trị của ô nhớ được tạo ra và gửi cho function nhưng đây ô nhớ được chia sẻ , dùng chung.
VD : int [] p = {1, 2, 3, 4}; // p chưa địa chỉ của {1, 2, 3, 4} 0xff00001
     int *q = NULL; // q có giá trị là 0x00000000
     q = p; // gọi operator = , copy thông thường, gán giá trị của p sang q
Ở trên là một ví dụ về shallow copy, nó có ích trong vài trường hợp ví dụ bạn muốn swap mảng A chưa 10000 phần tử với  mảng B cũng có 1000 phần tử ta chỉ cần swap giá trị địa chỉ 2 cái cho nhau là xong chứ không cần phải swap từng phần tử

int* temp = A;
A = B;
B = temp;

Nhưng có vấn đề khác là ở ví dụ đầu với p q , vùng nhớ của p q là một không tách biệt nhau điều đó dẫn đến khi ta thay đổi p hoặc q thì ô nhớ đều thay đổi.

VD p[1] = 2;
thì mặc dù không muốn nhưng q[1] == 2.

Còn Deep copy thì ta thực hiện copy từng phần tử của p sang q (tất nhiên là q đã có đầy đủ bộ nhớ)


+ Shallow copying (trong class)

 Vì C++ không biết nhiều về class của bạn, nên nó sẽ cung cấp hàm copy mặc định và toán tử gán mặc định như một phương thức sao chép được biết như là  shallow copy. Shallow copy có nghĩa là có nghĩa là C++ copy từng phần tử của class riêng lẻ sử dụng toán tử gán(assignment operator ) . Khi  class đơn giản , không chứa cấp phát động thì điều này làm việc rất tốt.

 Ví dụ class Tuoi{
 private:
  int m_tuoi;
 public:
  Tuoi(int tuoi = 0){ (copy constructor )
  m_tuoi = tuoi;
  }
 }

 Khi C++ làm shallow copy với class này, nó sẽ copy m_tuoi sẽ sử dụng toán sử gán int tiêu chuẩn, đó là tất cả những gì ta muốn làm khi tự viết ra toán tủ gán hay hàm tạo sao chép của riêng mình, vì vậy không có lý do gì để ta viết toán tử hay function mới trong trường hợp này.


 Tuy nhiên, khi thiết kế class nếu nó sử dụng cấp phát bộ nhớ động (dynamically allocated memory) , khi copy từng thuộc tính với shallow sẽ gây ra một vấn đề! Bởi vì phép gán trong con trỏ đơn giản là nó chỉ gán địa chỉ của con trỏ này sang con trỏ kia- Nó không cấp pháp bộ nhớ hay copy nội dung đang được trỏ đến.


 Một ví dụ về class MyString rất hay trên (learncpp.com)


 class MyString
{
private:
    char *m_pchString;
    int m_nLength;

public:
    MyString(char *pchString="")
    {
        // Find the length of the string
        // Plus one character for a terminator
        m_nLength = strlen(pchString) + 1;
       
        // Allocate a buffer equal to this length
        m_pchString= new char[m_nLength];
       
        // Copy the parameter into our internal buffer
        strncpy(m_pchString, pchString, m_nLength);
   
        // Make sure the string is terminated
        m_pchString[m_nLength-1] = '\0';
    }

    ~MyString() // destructor
    {
        // We need to deallocate our buffer
        delete[] m_pchString;

        // Set m_pchString to null just in case
        m_pchString = 0;
    }

    char* GetString() { return m_pchString; }
    int GetLength() { return m_nLength; }
};

Xét ví dụ dưới đây :

MyString cHello("Hello, world!");

{
    MyString cCopy = cHello; // use default copy constructor
} // cCopy goes out of scope here

std::cout << cHello.GetString() << std::endl; // this will crash

Dòng MyString cHello("Hello, world!"); gọi MyString constructor , nó sẽ cấp phát động bộ nhớ ở heap rồi cho cHello.m_pchString trỏ đến đó và sao đó copy "Hello, world" vào trong vùng nhớ đó

 MyString cCopy = cHello; // use default copy constructor

 dòng này sử dụng hàm tạo copy mặc định bởi vì ta không tự mình cung cấp một hàm copy . Vì vậy nó sẽ copy theo kiểu shallow, tức là giá trị của cHello.m_pchString được copy từ cHello.m_pchString và như kết quả hai con trỏ này trỏ cùng vào một vùng nhớ.

 với dòng : } // cCopy goes out of scope here

 Khi cCopy ra khỏi phạm vi {} thì class  MyString sẽ gọi hàm hủy cho cCopy. Hàm hủy delete hết cấp phát động của cả cCopy.m_pchString và cHello.m_pchString đang trỏ đến, chúng ta cũng vô tình làm ảnh hưởng đến cHello. Chú ý là hàm hủy sẽ đưa con trỏ cCopy.m_pchString trỏ tới 0, nhưng cHello.m_pchString sẽ trỏ vào vùng nhớ đã bị xóa (không hợp lệ).


std::cout << cHello.GetString() << std::endl; // this will crash

Bây giờ bạn có thể thấy tại sao bị crashes. Chùng ta đã delete string cHello đang trỏ tới và bây giờ chúng ta đang cố gắng in giá trị của vùng nhớ không còn được cấp phát.


Deep Copying


Câu trả lời cho vấn đề trên là làm deep copy trên con trỏ không trỏ tới NULL đang được copy (từ nó). Deep copy tạo một bản sao của đối tượng hay biến đang được trỏ tới cho đích đến (chính là đối tượng được gán) tự nó copy . Với cách này đích đến có thể làm bấu cứ cái gì nó muốn và đối tượng được copy từ nó sẽ không bị ảnh hưởng. Làm việc với deep copy yêu caaud chùng ta phải tự mình viết ra copy constructors và nạp chồng toán tử gán.

Chúng ta cùng đến và xem điều đó được làm như thế nào trong class MyString của chúng ta:


// Copy constructor
MyString::MyString(const MyString& cSource)
{
    // because m_nLength is not a pointer, we can shallow copy it
    m_nLength = cSource.m_nLength;

    // m_pchString is a pointer, so we need to deep copy it if it is non-null
    if (cSource.m_pchString)
    {
        // allocate memory for our copy
        m_pchString = new char[m_nLength];

        // Copy the string into our newly allocated memory
        strncpy(m_pchString, cSource.m_pchString, m_nLength);
    }
    else
        m_pchString = 0;
}

Bây giờ chúng ta cùng đến với nạp chồng toán tử toán tử gán:

// Assignment operator
MyString& MyString::operator=(const MyString& cSource)
{
    // check for self-assignment
    if (this == &cSource)
        return *this;

    // first we need to deallocate any value that this string is holding!
    delete[] m_pchString;

    // because m_nLength is not a pointer, we can shallow copy it
    m_nLength = cSource.m_nLength;

    // now we need to deep copy m_pchString
    if (cSource.m_pchString)
    {
        // allocate memory for our copy
        m_pchString = new char[m_nLength];

        // Copy the parameter the newly allocated memory
        strncpy(m_pchString, cSource.m_pchString, m_nLength);
    }
    else
        m_pchString = 0;

    return *this;
}

Chú ý là toán tử gán của chúng ta rất giống với hàm tạo copy, nhưng có ba khác biệt lớn :
    + Chúng ta kiểm ta xem có nó tự gán với chính mình hay không
    + Chúng ta return *this bởi vì chúng ta có thể xâu chuỗi toán tử gán
    + Chúng ta cần deallocate một cách rõ ràng giá trị cái mà string đã tổ chức

Chúng ta phải delete vùng nhớ được cấp phát trước khi copy vì nếu không làm (mặc dù code không bị crash) nhưng chúng ta sẽ có một memory leak, nó làm lãng phí bộ nớ mỗi lần chúng ta thực hiện phép gán.

Tại sao cần phải kiểm tra xem nó có tự gán với mình trước không?

Thứ nhất là nếu chúng ta không cần copy thì tại sao phải làm một cái.
Lý do thứ 2 là nó sẽ dẫn đến lỗi khi trong class sử dụng cấp phát động



// Problematic assignment operator
MyString& MyString::operator=(const MyString& cSource)
{
    // Note: No check for self-assignment!

    // first we need to deallocate any value that this string is holding!
    delete[] m_pchString;

    // because m_nLength is not a pointer, we can shallow copy it
    m_nLength = cSource.m_nLength;

    // now we need to deep copy m_pchString
    if (cSource.m_pchString)
    {
        // allocate memory for our copy
        m_pchString = new char[m_nLength];

        // Copy the parameter the newly allocated memory
        strncpy(m_pchString, cSource.m_pchString, m_nLength);
    }
    else
        m_pchString = 0;

    return *this;
}


Điều gì xảy ra với cHello = cHello ;

khi đó con trỏ this trỏ cHello và cSource cũng tham chiếu đến cHello vì vậy điều này dẫn đến m_pchString và cSource.m_pchString là một.

Sau đó khi thực hiện lệnh delete[] m_pchString; tức là cSource.m_pchString cũng bị delete theo dẫn đến lệnh copy tiếp theo sẽ thực hiện mà đối tượng được copy đến đã bị delete


Ngăn chặn việc copy:

Đôi khi chúng ta chỉ đơn giản không muốn class của chúng ta bị copy. Cách tốt nhất là thêm khai báo cho toán tử gán và copy constructor vào trong mục private của class.

class MyString
{
private:
    char *m_pchString;
    int m_nLength;

    MyString(const MyString& cSource);
    MyString& operator=(const MyString& cSource);
public:
    // Rest of code here
};


Trong trường hợp nayfC++ không tự động tạo một hàm tạo mặc định hay toán tử gán mặc định bởi vì chúng ta đã nói với compiler là chúng ta đã tự định nghĩa nó. Nhưng không thể truy cập vào nó vì nó private.

Ví dụ với code :

#include <iostream>
#include <conio.h>
#include <string.h>

using namespace std;

class MyString
{
private:
    char *m_pchString;
    int m_nLength;

    MyString(const MyString& cSource);
    MyString& operator=(const MyString& cSource);
public:
    // Rest of code here
};

MyString::MyString(const MyString& cSource){
    m_nLength = strlen(cSource.m_pchString);
    strncpy(m_pchString, cSource.m_pchString, m_nLength);
}

int main(){
    MyString s; // error
    MyString t = s;
    return 0;
}

Khi chạy ta nhận được lỗi : [Error] no matching function for call to 'MyString::MyString()


Tóm lại:


- Copy constructor mặc định và toán tử gán mặc định làm shallow copy , điều này tốt khi class gồm nhưng biến không cấp phát động.
- Nhưng class sử dụng cấp vát động cần một hàm tạo sao chép và toán tử gán riêng để deep copy
- Toán tử gán thường được thực hiện giống như hàm tạo sao chép nhưng đầu tiên phải kiểm tra xem nó có tự gán cho nó không và return *this, deallocates những bộ nhớ đã được cấp phát trước đó để deep copying
-Nếu bạn không muốn class sao chép sử dụng hàm tạo copy và toán tử gán prototype trong file header class.

Thứ Sáu, 4 tháng 12, 2015

Game nhanh tay viết bằng C++ trên console

Các bạn ấn 1 2 3 tùy theo độ sáng của khung, nêu lâu quá hoặc bấm nhầm bạn sẽ thua. :)
Cảm ơn các bạn đón xem.

#include<iostream>
#include<windows.h>
#include<conio.h>
#include<time.h>
#include<stdio.h>
#include<fstream>

using namespace std;

//int t = 0;
int diem = 0;
bool landau = true;

void txtColor(int mau);
void gotoxy(int x,int y);
void taokhung();
void play();
void vemau(int dapan);
void xoadapan(int dapan);
void save();

int main(){
SetConsoleTitle("Game : 1 2 3 . Developer : Nguyen Dinh Tuan - theiron1997@gmail.com");
taokhung();
play();
getch();
return 0;
}

void gotoxy(int x,int y){
COORD coord;
coord.X = x;
coord.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),coord);
}
void txtColor(int mau){
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),mau);
}

void taokhung(){
txtColor(10);
for(int i=10;i<70;i++){
gotoxy(i,5);
cout << char(220);
gotoxy(i,20);
cout << char(223);
}
txtColor(7);
for(int i=11;i<69;i++){
for(int j=6;j<=19;j++){
gotoxy(i,j);
cout << char(219);
}
}
txtColor(12);
for(int i=6;i<20;i++){
gotoxy(10,i);
cout << char(219);
gotoxy(30,i);
cout << char(219);
gotoxy(50,i);
cout << char(219);
gotoxy(69,i);
cout << char(219);
}
txtColor(7);
gotoxy(20,4);
cout << "1";
gotoxy(40,4);
cout << "2";
gotoxy(60,4);
cout << "3";
}
void play(){
gotoxy(15,1);
cout << "Diem : " << diem;
//taokhung();
if(landau){
gotoxy(15,3);
for(int i=4 ;i>=1;i--){
gotoxy(15,3);
cout << i;
Sleep(1000);
gotoxy(15,3);
cout << " ";
}
gotoxy(15,3);
cout << "Start!";
Sleep(1000);
gotoxy(15,3);
cout << "           ";
landau = false;
}
srand(time(0));
int dapan = (rand()*rand()+rand()-2)%3+1;
vemau(dapan);
char *c = new char[1] ;
itoa(dapan,c,10);
int t = 0;
fflush(stdin);
do{
t++;
if(t%34590==0){
txtColor(10);
gotoxy(15,3);
cout << "Game over - Qua cham";
getch();
save();
exit(0);
}
}while(!kbhit());
char ch = getch();

if(ch==*c){
diem ++;
xoadapan(dapan);
play();
}
txtColor(10);
gotoxy(15,3);
cout << "Game over - Bam sai";
    getch();
    save();
    exit(0);
}
void vemau(int dapan){
txtColor(6);
switch(dapan){
case 1 : for(int i=11;i<30;i++){
        for(int j=6;j<=19;j++){
        gotoxy(i,j);
        cout << char(219);
        }
        }
        break;
case 2 : for(int i=31;i<50;i++){
        for(int j=6;j<=19;j++){
        gotoxy(i,j);
        cout << char(219);
        }
        }
        break;
case 3 : for(int i=51;i<=68;i++){
        for(int j=6;j<=19;j++){
        gotoxy(i,j);
        cout << char(219);
        }
        }
}
}
void xoadapan(int dapan){
txtColor(7);
switch(dapan){
case 1 : for(int i=11;i<30;i++){
        for(int j=6;j<=19;j++){
        gotoxy(i,j);
        cout << char(219);
        }
        }
        break;
case 2 : for(int i=31;i<50;i++){
        for(int j=6;j<=19;j++){
        gotoxy(i,j);
        cout << char(219);
        }
        }
        break;
case 3 : for(int i=51;i<=68;i++){
        for(int j=6;j<=19;j++){
        gotoxy(i,j);
        cout << char(219);
        }
        }
}
}
void save(){
char *tmoi = new char[70];
char *tcu = new char[70];
int diemcu;

ifstream f("tuan.pro");
if(!f.is_open()){
ofstream f("tuan.pro");
system("cls");
txtColor(10);
gotoxy(15,1);
   cout << "Diem : " << diem;
gotoxy(15,8);
cout << "Than tich moi ";
gotoxy(15,10);
cout << "Ten ban la : ";
cin.getline(tmoi,69);
f << tmoi << "|" << diem;
}
else{
f.getline(tcu,69,'|');
f >> diemcu;
if(diemcu < diem){
system("cls");
   txtColor(10);
   gotoxy(15,1);
       cout << "Diem : " << diem;
   gotoxy(15,8);
   cout << "Than tich moi ";
   gotoxy(15,10);
   cout << "Ten ban la : ";
   cin.getline(tmoi,69);
   ofstream f("tuan.pro");
   f << tmoi << "|" << diem;
}
else{
system("cls");
   txtColor(10);
   gotoxy(15,8);
   cout << "Diem cua ban " << diem;
   gotoxy(15,10);
   cout << "Thanh tich cao nhat  : ";
   cout << tcu << " duoc " << diemcu;
   getch();
}
}
f.close();
}

Thứ Tư, 2 tháng 12, 2015

Game dò mìn viết bằng C++ trên console

Dưới đây là game dò mìn do mình biết bằng C++ rất mong nó có ích cho các bạn :)




#include <iostream>
#include <Windows.h>
#include <conio.h>
#include <time.h>

struct ToaDo{
int _x;
int _y;

ToaDo(int x, int y);
};

ToaDo::ToaDo(int x, int y){
this->_x = x;
this->_y = y;
}

using namespace std;

#define YMAX 50
#define XMAX 50
//Row 0 = column 0 = 10
#define X0 10
#define Y0 10
#define CHAR 219
#define BOOM 100

void resizeConsole(int width, int height);
void textcolor(int x);
void gotoxy(int x, int y);
void XoaManHinh();

void setScreen();
void init2D(int** arr, int c, int r);

void randBoom(int** arr, int c, int r);
void setCell(int** arr, int c, int r);
int countBoom(int** arr, int x, int y, int col, int row);
void creatMapBoom(int **arr, int numCol, int numRow);

inline void playGame(ToaDo * pnt, ToaDo * pntArr, int numCol, int numRow, int** arr, int* boomNotRemoved, int* realBomNotRemoved);
void covertCoordToArr(ToaDo * pnt, const ToaDo * pntArr);
void movePointer(ToaDo * pnt, ToaDo * pntArr, int numCol, int numRow);
void updateBoomRemove(const int* bomNotRemoved);

void printArr(int** Arr, int* numCol, int* numRow){
for(int i = 0; i < *numCol ; i++){
for(int j = 0; j < *numRow ; j++){
if(Arr[i][j] == -1) cout << "* ";
else cout << Arr[i][j] << " ";
}
cout << endl;
}
}

int main(){

    int bomNotRemoved = BOOM;
    int realBomNotRemoved = BOOM;
   
int numCol = XMAX - X0 + 10;
int numRow = YMAX - Y0 + 10;
int **arr = new int*[numCol];

for (int i = 0; i < numCol; i++){
arr[i] = new int[numRow];
}

numCol -= 16;
numRow -= 16;

creatMapBoom(arr, numCol, numRow);
ToaDo pnt(X0 + 3, Y0 + 3);
ToaDo pntArr(0, 0);
resizeConsole(600, 800);
setScreen();
//printArr(arr, &numCol, &numRow);

    updateBoomRemove(&bomNotRemoved);
gotoxy(pnt._x, pnt._y);
playGame(&pnt, &pntArr, numCol, numRow, arr, &bomNotRemoved, &realBomNotRemoved);


_getch();
return 0;
}

// Hàm thay đ?i kích c? c?a khung cmd.
void resizeConsole(int width, int height)
{
SetConsoleTitle("Developer : Nguyen Dinh Tuan - theiron1997@gmail.com");
HWND console = GetConsoleWindow();
RECT r;
GetWindowRect(console, &r);
MoveWindow(console, r.left, r.top, width, height, TRUE);
}

// Hàm tô màu.
void textcolor(int x)
{
HANDLE mau;
mau = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(mau, x);
}

// Hàm d?ch chuy?n con tr? đ?n t?a đ? x, y.
void gotoxy(int x, int y)
{
HANDLE hConsoleOutput;
COORD Cursor_an_Pos = { x - 1, y - 1 };
hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hConsoleOutput, Cursor_an_Pos);
}

// Hàm xóa màn h?nh.
void XoaManHinh()
{
HANDLE hOut;
COORD Position;
hOut = GetStdHandle(STD_OUTPUT_HANDLE);
Position.X = 0;
Position.Y = 0;
SetConsoleCursorPosition(hOut, Position);
}

void setScreen(){
resizeConsole(800, 600);
for (int i = X0; i <= XMAX; i++){
textcolor(14);
gotoxy(i, Y0);
cout << char(CHAR);
gotoxy(i, YMAX);
cout << char(CHAR);
}

for (int i = Y0; i <= YMAX; i++){
textcolor(14);
gotoxy(X0, i);
cout << char(CHAR);
gotoxy(XMAX, i);
cout << char(CHAR);
}

for (int i = X0 + 3; i <= XMAX - 3; i++){
for (int j = Y0 + 3; j <= YMAX - 3; j++){
if (i % 2 == 0){
if (j % 2 == 0){
textcolor(8);
gotoxy(i, j);
cout << char(CHAR);
}
else{
textcolor(5);
gotoxy(i, j);
cout << char(CHAR);
}
}
else{
if (j % 2 == 0){
textcolor(5);
gotoxy(i, j);
cout << char(CHAR);
}
else{
textcolor(8);
gotoxy(i, j);
cout << char(CHAR);
}
}
}
}

textcolor(7);
}

void init2D(int** arr, int c, int r){
for (int i = 0; i < c; i++){
for (int j = 0; j < r; j++){
arr[i][j] = 0;
}
}
}

void randBoom(int** arr, int c, int r){
int boom = BOOM;
int x, y;
while (boom){
do{
x = rand() % c;
y = rand() % r;
} while (arr[x][y] != 0);
arr[x][y] = -1; // Number -1 represent "boom" .
boom--;
}
}

void setCell(int** arr, int c, int r){
for (int i = 0; i < c; i++){
for (int j = 0; j < r; j++){
if (arr[i][j] != -1){
arr[i][j] = countBoom(arr, i, j, c, r);
}
}
}
}

int countBoom(int** arr, int x, int y, int col, int row){
int numBoom = 0;
for (int i = -1; i <= 1; i++){
for (int j = -1; j <= 1; j++){
if ((i != 0 || j != 0) && 0 <= x + i && x + i < col && 0 <= y + j && y + j < row){
if (arr[x + i][y + j] == -1){
numBoom++;
}
}
}
}
return numBoom;
}

void creatMapBoom(int **arr, int numCol, int numRow){
init2D(arr, numCol, numRow);
srand(time(NULL));
randBoom(arr, numCol, numRow);
setCell(arr, numCol, numRow);
}

 void movePointer(ToaDo * pnt, ToaDo * pntArr, int numCol, int numRow){

if (GetAsyncKeyState(VK_LEFT)){
if (pntArr->_y > 0){
--(pntArr->_y);
covertCoordToArr(pnt, pntArr);
gotoxy(pnt->_x, pnt->_y);
}
}
if (GetAsyncKeyState(VK_RIGHT)){
if (pntArr->_y < numCol){
++(pntArr->_y);
covertCoordToArr(pnt, pntArr);
gotoxy(pnt->_x, pnt->_y);
}
}
if (GetAsyncKeyState(VK_UP)){
if (pntArr->_x > 0){
--pntArr->_x;
covertCoordToArr(pnt, pntArr);
gotoxy(pnt->_x, pnt->_y);
}
}
if (GetAsyncKeyState(VK_DOWN)){
if (pntArr->_x < numRow){
pntArr->_x++;
covertCoordToArr(pnt, pntArr);
gotoxy(pnt->_x, pnt->_y);
}
}

Sleep(100);
}

void areaOpen(int** arr, const ToaDo* pnt, const ToaDo* pntArr, int numCol, int numRow);

void updateBoomRemove(const int* boomNotRemoved);

 void actionPointer(ToaDo * pnt, ToaDo * pntArr, int numCol, int numRow, int** arr
                         , int* bomNotRemoved, int* realBomNotRemoved){
if (GetAsyncKeyState(VK_SPACE)){
if (arr[pntArr->_x][pntArr->_y] == -1){
cout << "*";
gotoxy(65, 18);
textcolor(14);
cout << "Thua roi .";
cin.ignore(1);
exit(0);
}
else{
if (/*arr[pntArr->_x][pntArr->_y] != 0 &&*/ arr[pntArr->_x][pntArr->_y] != 13){
if(arr[pntArr->_x][pntArr->_y] == 0){
gotoxy(pnt->_x, pnt->_y);
cout << " ";
arr[pntArr->_x][pntArr->_y] = 13;
areaOpen(arr, pnt, pntArr, numCol, numRow);
gotoxy(pnt->_x, pnt->_y);
 }
}
if(arr[pntArr->_x][pntArr->_y] != 0 && arr[pntArr->_x][pntArr->_y] != 15 // 15 is removed boom
       && arr[pntArr->_x][pntArr->_y] != 13){ // 13 is opened cell
  gotoxy(pnt->_x, pnt->_y);
   cout << arr[pntArr->_x][pntArr->_y];
   areaOpen(arr, pnt, pntArr, numCol, numRow);
   gotoxy(pnt->_x, pnt->_y);
 }
}
}

if(GetAsyncKeyState(VK_BACK)){
if(arr[pntArr->_x][pntArr->_y] != 13 && arr[pntArr->_x][pntArr->_y] != 15){
if(arr[pntArr->_x][pntArr->_y] == -1){
--(*realBomNotRemoved);
//gotoxy(1, 1);
//textcolor(12);
//cout << "tt " << (*realBomNotRemoved);
//Win
if((*realBomNotRemoved) == 0){
       gotoxy(65, 18);
       textcolor(14);
       cout << "Thang ^^ .";
       cin.ignore(1);
       exit(0);
}
}
gotoxy(pnt->_x, pnt->_y);
textcolor(12);
cout << char(CHAR);
arr[pntArr->_x][pntArr->_y] = 15; // Removed Boom
(*bomNotRemoved)--;
updateBoomRemove(bomNotRemoved);
gotoxy(pnt->_x, pnt->_y);
textcolor(7);
}
//if()
}

Sleep(100);
 }

void covertCoordToArr(ToaDo * pnt, const ToaDo * pntArr){
pnt->_x = X0 + 3 + pntArr->_y;
pnt->_y = Y0 + 3 + pntArr->_x;
}

inline void playGame(ToaDo * pnt, ToaDo * pntArr, int numCol, int numRow, int** arr, int* boomNotRemoved, int* realBomNotRemoved){
while (1){
movePointer(pnt, pntArr, numCol, numRow);
actionPointer(pnt, pntArr, numCol, numRow, arr, boomNotRemoved, realBomNotRemoved);

}

}

void areaOpen(int** arr,const ToaDo* pnt,const ToaDo* pntArr, int numCol, int numRow){
for(int i = -2; i <= 2; i++){
for(int j = -2; j <= 2; j++){
if(0 <= pntArr->_x + i && pntArr->_x + i < numCol &&
                 0 <= pntArr->_y + j && pntArr->_y + j < numRow){
                  if(arr[pntArr->_x + i][pntArr->_y + j] != 13 && arr[pntArr->_x + i][pntArr->_y + j] != 15
  && arr[pntArr->_x + i][pntArr->_y + j] != -1){
                  gotoxy(pnt->_x + j, pnt->_y + i);
                   if(arr[pntArr->_x + i][pntArr->_y + j] == 0) cout << " ";
                   else{
                    cout << (arr[pntArr->_x + i][pntArr->_y + j]);
}
                   arr[pntArr->_x + i][pntArr->_y + j] = 13;
 }

}
}
}
}

void updateBoomRemove(const int* boomNotRemoved){
gotoxy(20, 5);
cout << "BOOM : " << *boomNotRemoved;
}



Rất mong các bạn góp ý :)

Các bạn ấn space để dò mìn và Backspace để đặt cờ :)