< Lập trình tân binh | 1.4. Quản lý và sử dụng bộ nhớ

1.4. Quản lý và sử dụng bộ nhớ

Cho tới lúc này, bạn đã biết cách để viết và biên dịch những chương trình console đầu tiên. Những chương trình đó làm 1 việc đơn giản là in 1 thông điệp ra màn hình và... hết.

Đó là do chương trình mà các bạn tạo ra không có khả năng giao tiếp với người dùng. Chúng ta sẽ có cơ hội học điều đó trong những chương sau. Tuy nhiên trước đó, tôi cần phải giúp các bạn nắm vững 1 khái niệm cơ bản của lập trình tin học, đó là các biến (variable).

Các biến cho phép chúng ta sử dụng bộ nhớ của máy tính để lưu trữ một thông tin và sau đó sử dụng nó vào lúc khác. Tôi đoán chác hẳn các bạn đã từng sử dụng những chiếc máy tính bỏ túi. Trong trường hơp đó, các bạn hẳn đã thấy qua hoặc sử dụng những phím như M+, M- hay MC để lưu trữ dữ liệu tạm thời. Chúng ta sẽ học cách sử dụng máy vi tính để làm nhữn việc tương tự như thế vì nói cho cùng thì nó cũng chỉ la 1 cái máy để tính toán cỡ bé bự mà thôi :D.

Thế nào là biến ?

Ví dụ trên đây về máy tính bỏ túi được tôi đưa ra bởi vì trong thế giới công nghệ thông tin, nguyên lý cơ bản này là giống nhau. Đâu đó bên trong máy tính của bạn có một số bộ phận có khả năng chứa đựng các thông tin và dữ liệu trong một khoảng thời gian nhất định. Để giải thích chính xác cách thúc hoạt động thì khá là phức tạp và bạn có thể bỏ qua nó.

Cái duy nhất bạn cần quan tâm đến là 1 biến là một phần của bộ nhớ máy tính mà bạn có thể sử dụng để chứa 1 giá trị. Tưởng tượng rằng máy vi tính là 1 cái tủ khổng lồ với hàng tỉ ngăn kéo nhỏ. Vậy thì các biến của chúng ta đang được cất trong những ngăn kéo đó.

Trong trường hợp của một cái máy tính bỏ túi đơn giản, chúng ta chỉ có thể ghi nhớ được 1 số mỗi lần. Tuy nhiên, chương trình của chúng ta rất có nguy cơ là sẽ phải lưu trữ thật nhiều nhiều thứ cùng lúc. Vì vậy phải có 1 cách để phân biệt các biến đó với nhau. Đấy là lý do vì sao chúng ta đặt tên cho các biến. Nó giống như kiểu dán nhãn cho các ngăn kéo vậy.

Thêm 1 điều nữa cần biết là ngoài các số, máy tính của chúng ta có thể lưu trữ được rất nhiều các thứ khác: chữ cái, chuỗi ký tự, hình ảnh, vv... Những cái đó được gọi là các kiểu dữ liệu khác nhau. Lúc này thì giống như chúng ta có nhiều kiểu ngăn kéo, cái để đựng cái bánh mỳ thì sẽ khác với cái để đựng nước.

Tên biến

Bắt đầu với câu hỏi về cách đặt tên biến. Trong C++, có 1 số nguyên tắc cần tuân thủ khi đặt tên cho 1 biến.

  • Tên biến chỉ bao gồm chữ, số, và dấu gạch dưới "_"
  • Phải bắt đầu bằng chữ
  • Không thể dùng các ký tự có dấu
  • Không thể chứa dấu cách " "

Một số ví dụ giúp các bạn thêm dễ hiểu. Những cái tên như lapTrinhTanBinh00, lap_trinh_tan_binh hay LAPTRINH_TANBINH đều được chấp nhận trong khi _laptrinh, LậpTrình hay lap trinh đều bị coi là không hợp lệ.

Thêm 1 điều cần chú ý nữa la ngôn ngữ C++ phân biệt giữa viết hoa và viết thường. Điều này có nghia là laptrinh, LapTrinh và LAPTRINH là ba cái tên biến hoàn toàn khác nhau.

! Chú ý, khi đặt tên biến, hãy đặt những cái tên có ý nghĩa. Có nghĩa là nếu biến của bạn dùng để chứa dữ liệu về số tuổi của 1 nguoi, hãy đặt tên cho nó la "tuoi", "soTuoi" hay "age" cho bạn nào thích tiếng Anh. Không nên đặt những tên kiểu "trau", "bo", "ga" hay "mickey" làm gì. Với trình biên dịch, tất cả bọn chúng đều giống nhau nhưng với lập trình viên thì chúng lại rất rất quan trọng.

Thêm vào đây là một số thói quen bạn nên có khi đặt tên, được rất nhiều lập trình viên chia sẻ với nhau. Trong những dự án lớn, thậm chí còn những quy tắc thêm được đặt ra và lập trình viện bắt buộc phải thực hiện dù có lúc rất khó khăn. Tất cả nhằm múc đích giúp mã nguồn mà bạn viết dễ đọc và dễ hiểu hơn.

  • Tên biến bắt đầu bằng chữ thường (không viết hoa)
  • Nếu tên bao gồm nhiều chữ thì viết chúng dính liền với nhau
  • Tất cả các chữ cái đầu tiên của các chữ trừ chữ đầu tiên được viết hoa

Ví dụ cụ thể với biến chứa số tuổi:

  • SoTuoi :  loại vì chữ đầu viết hoa
  • so_tuoi : cũng tạm tạm, 1 số lập trình viên thích viết kiểu này nhưng vì chúng ta muốn viết liền tất cả các chữ nên loại
  • sotuoi: loại do chữ đầu tiên của "tuoi" không viết hóa, khó đọc
  • batMan : loại, tên biến không nói lên tác dụng của biến
  • soTuoi: ok

Tôi đề nghị các bạn cũng giữ những thói quen tương tự. Việc giúp cho lập trình viên khác hiểu là rất quan trọng. Và chỉ cần trình bày rõ ràng là đã đủ rồi.

! Sai lầm thường gặp là đặt cùng tên cho 2 biến khác nhau trong cùng 1 hàm khiến cho trình biên dịch không thể dịch được và báo lỗi. Chú ý đặt 1 cái tên duy nhất cho mỗi biến khác nhau trong cùng 1 hàm.

Các kiểu dữ liệu

 Trong phần bên trên, chúng ta đã nói một biến sẽ có tên và kiểu dữ liệu của nó. Chúng ta đã học cách đặt tên cho một biến, giờ thì hãy xem xét về các kiểu dữ liệu khác nhau có thể gán cho một biến. Máy tính sẽ rất muốn biết trong bộ nhớ có chứa những loại thông tin gì. Vì thế nên với mỗi biến, chúng ta cần chỉ cho máy tính biết, kiểu dữ liệu của nó là gì : 1 số, một ký tự hay là 1 chữ, vv...

Dưới đây là các kiểu dữ liệu khác nhau mà chúng ta có thể sử dụng trong C++.

Tên kiểu dữ liệu Thông tin mà kiểu dữ liệu này có thể chứa
 bool  1 trong 2 gía trị true hoặc false
 char  ký tự
 int  số nguyên
 unsigned int  số nguyên dương
 double  số thực
 string  chuỗi ký tự

Nếu bạn gõ tên của một trong số những kiểu dữ liệu trên đây vào IDE, bạn sẽ thấy màu của nó thay đổi. Điều này có nghĩa là IDE nhận ra đây là những từ khóa đặc biệt. Trường hợp của kiểu string hơi đặc biệt, chúng ta sẽ xem xét nó sau. Tôi hứa là chúng ta sẽ nói kỹ về vấn đề này.

! Mỗi kiểu dữ liệu đều có giới hạn lưu trữ, nghĩa là những giá trị lớn nhất hoặc nhỏ nhất mà nó có thể nhận được, ví dụ cụ thể là sẽ có những số quá lớn để có thể được lưu trong kiểu int. Những giá trị giới hạn này thay đổi tùy vào máy tính và hệ điều hành và trình biên dịch mà bạn sử dụng. Tuy nhiên, những giá trị giới hạn này không có quá nhiều ảnh hưởng trong đa số trường hợp mà ta hay sử dụng. Bạn sẽ hay gặp những vấn đề này hơn nếu bạn viết những chương trình dành cho điện thoại hay bộ vi xử lý vì chúng cần có những giới hạn vượt xa máy tính thông thường. Ngoài những kiểu dữ liệu trên đây, vẫn còn một vài kiểu khác với những giá trị giới hạn khác nhau nhưng chúng ta hiếm khi sử dụng hơn.

Khi chúng ta cần tạo ra một biến, nên tự đặt câu hỏi xem biến đấy sẽ chứa giá trị kiểu gì. Ví dụ để lưu số người có mặt trong phòng thì kiểu unsigned int hay int là khá thích hợp. Trong khi đó, kiểu double có thể được dùng để lưu trữ cân nặng của một người hay kiểu string để lưu giữ họ tên người đó.

? Thế kiểu bool dùng để làm gì? Tôi chưa bao giờ nghe nói đến nó !

Một biến bool hay boolean, hay một giá trị logic là một biến chỉ có thể nhận được 1 trong 2 giá trị là true ( đúng ) hoặc false ( sai ). Ta có thể dùng để lưu những giá trị kiểu như trong phòng có người hoặc không, mật khẩu dùng để đăng nhập đúng hoặc sai, vv... Nếu bạn muốn 1 biến để lưu trữ câu trả lời cho những câu hỏi trên thì boolean là kiểu dữ liệu bạn nên xem xét.

Khai báo biến

Chúng ta sẽ nói luôn vào vấn đề chính, làm sao để có thể mượn máy vi tính 1 cái "ngăn kéo" của nó để sử dụng. Từ chuyên môn là khai báo biến.

Khi khai báo, chúng ta phải nói cho mày tính biết kiểu dữ liệu của biến mà chúng ta muốn sử dụng, tên biến và có thể cả giá trị ban đầu mà chúng ta muốn gán cho nó. Dưới đây là cú pháp để khai báo biến trong C++.

kiểu_dữ_liệu tên_biến (giá_trị);

 Chúng ta cũng có thể sử dụng cú pháp kế thừa từ ngôn ngữ C

kiểu_dữ_liệu tên_biến = giá_trị;

 2 cú pháp trên hoàn toàn tương đương nhau. Tuy nhiên tôi khuyên các bạn sử dụng cú pháp thứ nhất vì 1 số lý do mà các bạn sẽ hiểu rõ trong phần sau của giáo trình. Trong phần sau của giáo trình, cú pháp thứ 2 sẽ không được sử dụng. Tôi trình bày nó ở đây trong trường hợp các bạn thắc mắc khi nhìn thấy ví dụ khác mà các bạn tìm thấy trên web hoặc trong sách.

! Tuyệt đối đừng bỏ quên dấu ";" ở cuối dòng ! Đây là một trong những lỗi mà chúng ta rất dễ mắc phải và trình biên dịch thì hoàn toán không thích điều này.

Quay lại với ví dụ bài trước nhưng chúng ta sẽ thêm vào đó dòng lệnh để khai báo số tuổi người dùng

#include <iostream> 
using namespace std;

int main(){
  int tuoiNguoiDung(16);
  return 0;
}

Chuyện gì sẽ diễn ra ở dòng thứ 5 của chương trình?

Máy tính sẽ cho chúng ta mượn 1 cái "ngăn kéo" của nó có những đặc điểm sau đây:

  • Có thể chứa các số nguyên
  • Có dán nhãn là tuoiNguoiDung
  • Có chứa giá trị là 16

Vì trong phần tiếp theo, chúng ta sẽ nhắc đến rất nhiều ngăn kéo, tôi đề nghị chúng ta sẽ dử dụng mội hình vẽ đơn giản để mình họa.

Bộ nhớ

Trong hình vẽ trên đây, chúng ta thấy một hình vuông đại diện cho bộ nhớ của máy vi tính. Nó chỉ có chứa 1 ô vuông màu vàng là phần bộ nhớ mà máy tính đã cho chúng ta mượn. Trong đó có chứa giá trị 16 và nó được gán 1 cái nhãn là tuoiNguoiDung. Ngoài ra thì bộ nhớ còn trống rất nhiều chỗ. Đại khái là vậy.

Chúng ta không nên dừng lại ở đó chứ hả? Hãy khai báo thêm các biến khác nhé.

#include <iostream>
using namespace std;

int main(){
  int tuoinguoiDung(16);
  int soNguoi(432); //So nguoi dung
  double pi(3.14159);
  bool quenBiet(true); //Nguoi dung nay co phai nguoi quen khong?
  char chu('a');
  return 0;
}

 Có 2 điều quan trọng cần chú ý ở đây. Đó là biến kiểu bool chỉ có thể nhận 2 giá trị là true hoặc false. Điểm thứ 2 đó là với kiểu dữ liệu ký tự, giá trị phải được đặt giữa 2 dấu ('), nghĩa là phải viết là chu('a') chứ không phải là chu(a). Đây là một lỗi rất dễ mắc phải khi mới học lập trình.

! 1 comment khá hữu dụng để giải thích 1 biến có tên hơi trừu tượng dùng để làm gì

 Hình vẽ của chúng ta sẽ có thêm 1 số thứ.

Bộ nhớ

Bạn có thể thử biên dịch và thử chạy chương trình. Ta sẽ thấy là chương trình này thật ra chẳng làm gì cả. Các bạn sẽ không thất vọng đấy chứ. Thật ra thì đã có khá nhiều thứ diễn ra, chỉ đơn giản là bạn không nhìn thấy thôi.

  1. Chương trình yêu cấu máy tính cấp cho 1 phần bộ nhớ
  2. Hệ điều hành sẽ tìm kiếm vùng trống trong bộ nhớ và nói cho chương trình vùng nào có thể sử dụng
  3. Chương trình ghi giá trị 16 vào ô nhớ
  4. Chương trình tiếp tục làm tương tự với những biến còn lại
  5. Đến dòng cuối cùng, chương trình làm trống các ô nhớ và trả lại cho máy tính

Thế đấy, chúng ta không nhìn thấy gì trên màn hình vì đơn giản, chúng ta chả yêu cầu máy tính hiển thị gì ra màn hình cả.

Kiểu dữ liệu string (chuỗi ký tự)

Việc khai báo sử dụng một chuỗi ký tự có hơi rắc rối hơn 1 chút, tuy nhiên cũng không khác biệt là bao. Để sử dụng kiểu dữ liêu này, bạn phải thông báo với trình biên dịch là bạn sẽ làm việc với chuỗi ký tự. Nếu bạn không khai báo, trình biên dịch sẽ thiếu các công cụ cần thiết để quán lý kiểu dữ liệu này. Để thông báo, bạn chỉ cần thêm dòng #include

#include <iostream>
#include <string>
using namespace std;

int main(){
  string tenNguoiDung("Bruce Wayne");
  return 0;
}

Chắc hẳn các bạn cũng nhận ra được sự khác biệt. Gần giống với kiểu ký tự, giá trị của kiểu chuỗi ký tự cần phải được đặt trong dấu ngoặc kép ("). Ngoài ra, IDE cũng sẽ hiển thị cho bạn giá trị này bằng một màu khác. Đôi khi có người sẽ nhầm lẫn giữa (') và (") dẫn đến việc trình biên dịch không được hài lòng cho lắm. Nhưng bạn không cần quá lo lắng, các trình biên dịch đã từng thấy những sai lầm kinh khủng hơn nhiều rồi.

Cách thức để tiết kiệm chỗ

Trước khi chuyển sang phần tiếp theo, tôi sẽ chỉ cho các bạn một chiêu thức nhỏ mà các lập trình viên hay làm .

Khi bạn phải phải khai báo nhiều biến có cùng kiểu dữ liệu, bạn có thể viết tất cả trên cùng 1 dòng, cách nhau bằng dấu phẩy (,)

int a(2),b(4),c(-1); //Khai bao dong thoi 3 bien a = 2, b = 4 va c = -1

string ten("Bruce"), ho("Wayne"); //Khai bao 2 chuoi ky tu

Khá là hữu ích khi bạn phải khai bao đồng thời nhiều biến. Chúng ta tránh được việc phải nhắc lại kiểu dữ liệu của từng biến. Tuy nhiên tôi khuyên các bạn không nên lạm dụng cách thức này vì nó sẽ khiến mã mà bạn viết ra khó đọc và khó hiểu hơn một ít.

Khai báo biến không gán giá trị ban đầu

 Sau khi đã có cái nhìn tổng thế, chúng ta sẽ đi sâu thêm 1 chút vào chi tiết.

Khi bạn khai báo một biến, chương trình sẽ thực hiện liên tiếp 2 hành động.

  1. Yêu cầu máy tính cung cấp cho 1 vùng nhớ, thuật ngữ tin học là "phân bổ (allocation) bộ nhớ"
  2. Ghi giá trị ban đầu vào vùng nhớ được phân, thuật ngữ là "khởi tạo biến"

2 hành động này được thực hiện một các tự động mà không cần chúng ta phải làm gì cả.

Tuy nhiên, đôi khi chúng ta muốn tạo ra một biến mà không biết phải gán cho nó giá trị ban đầu như thế nào. Vì thế, C++ cung cấp cho chúng ta cách thức có thể khai báo biến mà không đưa ra giá trị ban đầu. Cú pháp khá đơn giản khi bạn chỉ cần đưa ra kiểu dữ liệu và tên biến:

Kiểu_dữ_liệu tên_biến;

Một ví dụ cụ thể:

#include <iostream>
#include <string>
using namespace std;
int main(){
  string tenNguoiChoi; //Ten nguoi tham gia tro choi
  int soNguoi; //So nguoi tham gia tro choi
  bool chienThang; //Nguoi choi co chien thang khong?
  return 0;
}

!Lỗi thường gặp trong trường hợp này là thêm ngoặc trống vào sau khi khai báo tên biến, như int soNguoi(). Đây là một sai lầm, chỉ cần có kiểu dữ liệu và tên biến là đủ.

Quá dễ, đúng không? Hình ảnh minh họa cho phần này:

Bộ nhớ

Chúng ta có đủ 3 ô trong bộ nhớ với những nhãn dán tên các biến. Khác biệt duy nhất là chúng ta không biết trong ô nhớ đó chưa cái gì. Trong phần tới, chúng ta sẽ xem làm sao có thể gán giá trị cho các biến đó và thay dẩu ? bằng các giá trị thích hợp hơn.

! Dù là chúng ta vừa học các khai báo biến mà không cần giá trị ban đầu, tôi vẫn khuyến khích các bạn luôn luôn đưa ra một giá trị ban đầu trừ khi bạn hoàn toàn không biết được là phải cho giá trị gì vào đó mà trường hợp này thì khá hiếm.

Giờ là lúc chúng ta học sử dụng các biến trong các phép toán. Thật đáng buồn là đến giờ thì màn hình của chúng ta vẫn còn trống :(.

Hiển thị giá trị biến ra màn hình

 Trong bài học trước, chúng ta đã học cách hiển thị một thông điệp ra màn hình. Hy vọng là các bạn vẫn chưa quên mất đấy chứ.

Đúng thế, chung ta cũng sẽ dùng cách tương tự để in giá trị của một biến ra màn hình. Chúng ta vẫn sẽ sử dụng << chỉ khác là ở chỗ trước đây là thông điệp thì chúng ta sẽ thay thế bằng tên biến.

cout << soTuoi;

Dễ không? Tiếp với 1 ví dụ hoàn chỉnh nhé:

#include <iostream>
using namespace std;
int main(){
  int soTuoi(16);
  cout << "Tuoi cua ban la : ";
  cout << soTuoi;
  return 0;
}

Khi chạy chương trình, chúng ta sẽ có:

 Kết quả

Đúng những gì chúng ta chờ đợi. Thậm chí còn dễ dàng hơn nếu chúng ta viết trên cùng một dòng và cuối cùng thêm xuống dòng.

!Nhớ thêm 1 dấu cách vào cuối thông điệp nếu không thì giá trị của biến sẽ dính liền vào thông điệp phía trước.

#include <iostream>
using namespace std;

int main(){
  int soTuoi(16);
  cout << "Tuoi cua ban la : " << soTuoi << endl;
  return 0;
}

 Bạn cũng có thể, trong cùng 1 câu lệnh in ra giá trị của 2 biến.

#include <iostream>
#include <string>
using namespace std; 
int main(){
   int soAnhEm(0);
   string ten("Batman");
   cout << "Ban ten la " << ten << " ban co " << soAnhEm << " anh chi em" << endl;
   return 0; 
}

Và kết quả nhận được sẽ là:

Kết quả

Các bạn sẽ không hài lòng chỉ với thế chứ? Trong bài học tiếp theo, chúng ta sẽ học cách nhận 1 giá trị và gán nó vào 1 biến.

Tham chiếu

Trước khi kết thúc chương này, tôi xin phép được nói thêm với các bạn một khái niêm quan trọng cuối cùng, đó là các tham chiếu. Tôi đã giải thích cho các bạn ở đầu bài học là một biến có thể coi như là 1 ngăn kéo với một cái nhãn đính trên đó. Trong cuộc sống thì dù là cùng 1 ngăn kéo, ta cũng có thể dán lên đấy đồng thời nhiều cái nhãn. Trong C++ cũng vậy, ta có thể cho mỗi ô nhớ 2, 3 hay nhiều cái nhãn.

Bằng cách đó thì ta có thêm 1 cách khác để tiếp cận cùng 1 ô nhớ. Giống như kiểu chúng ta đặt biệt danh cho ô nhớ này vậy. Thuật ngữ này C++ gọi là tham chiếu (trong tiếng Anh là reference).

Chúng ta có thể hình dung với hình vẽ sau

Bộ nhớ

Trong đoạn mã, chúng ta sẽ dùng dấu và (&) để khai báo một tham chiếu.

int soTuoi(16); 

int& thamChieu(soTuoi);

Ở dòng thứ nhất, chúng ta khai báo một ô nhớ mang tên soTuoi, trong đấy có giá trị 16. Ở dòng thứ 3 thì chúng ta lại dán thêm 1 cái nhãn khác vào ô nhớ đấy, đó là thamChieu. Thế là chúng ta đã có 2 cách để tiếp cận với vùng nhớ đấy rồi.

Người ta gọi thamChieutham chiếu của soTuoi.

!1 tham chiếu phải có cùng kiểu dữ liệu với biến mà nó gán đến. Rất là hợp lý khi 1 int& chỉ có thể tham chiếu đến 1 int và 1 string& chỉ tham chiếu đến 1 string.

Hãy kiểm tra thử để chắc là 2 cái nhãn của chúng ta nằm trên cùng 1 ô nhớ. 

#include <iostream>
using namespace std; 
int main(){
   int soTuoi(18); //Bien chua so tuoi cua 1 nguoi
   int& thamChieu(soTuoi); //1 tham chieu den bien day

 /* Tu gio chung ta co the dung ca soTuoi va thamChieu
  de su dung bien nay vi chung cung dan tren 1 o nho */
   cout << "Ban da " << soTuoi << " tuoi. (dung ten bien)" << endl;
   cout << "Ban da " << thamChieu << " tuoi. (dung tham chieu)" << endl;
   return 0; 
}

Và kết quả cho ra là:

Kết quả

Một khi khai báo xong, chúng ta có thể thao tác với tham chiếu như với tên biến, chúng hoàn toàn không khác gì nhau.

!Nhưng mà... việc này thì được tác dụng gì nhỉ?

Câu hỏi rất hay! Với ví dụ chúng ta đưa ra ở trên thì đúng là bạn không thấy được nhiều lợi ích của việc tạo ra thêm 1 tham chiếu. Nhưng thử tưởng tượng khi có 2 lập trình viên cùng làm việc với 1 biến, 1 người phụ trách phần khai báo còn người kia thì sử dụng. Người sau chỉ đơn giản cần 1 cách để tiếp cận biến đó và 1 tham chiếu là nhiều hơn cả đủ.

Vẫn cảm thấy quá trừu tượng và vô dụng? Không sao, đây cũng là 1 trong những khái niệm sẽ được nhắc lại trong phần sau của giáo trình. Bạn chỉ cần làm quen dần dần trước khi dùng nó trong những trường hợp phức tạp hơn sau này.

Tóm tắt bài hoc :
  • 1 biến là 1 thông tin được lưu trong bộ nhớ
  • 1 biến có thể có nhiều kiểu dữ liệu: int, char, bool, vv...
  • 1 biến cần được khai báo trước khi sử dụng. Dùng cú phap của C++, ví dụ int soTuoi(16);
  • Giá trị của 1 biến cũng có thể được in ra dùng cout
  • Tham chiếu là cách thức cho phép đưa ra cho biến 1 biệt danh khác với tên gốc. Ví dụ int& thamChieu(soTuoi);