[Tự học lập trình C/C++] Bộ nhớ động trong C/C++

Một sự hiểu biết sâu về cách bộ nhớ động thực sự làm việc trong C/C++ là cốt yếu để trở thành một lập trình viên C/C++ giỏi. Bộ nhớ trong chương trình C/C++ của bạn được phân thành hai phần:
  • Stack: Tất cả biến được khai báo bên trong hàm sẽ nhận bộ nhớ từ stack trong C/C++.
  • Heap: Được sử dụng để cấp phát bộ nhớ động khi chương trình chạy.
Nhiều khi, bạn không biết trước bao nhiêu bộ nhớ bạn sẽ cần để lưu thông tin cụ thể trong một biến đã được định nghĩa và kích cỡ bộ nhớ cần thiết có thể được quyết định tại run time.
Bạn có thể cấp phát bộ nhớ tại run time bên trong Heap cho biến đó với một kiểu đã cho bởi sử dụng một toán tử đặc biệt trong C/C++ mà trả về địa chỉ của không gian đã cấp phát. Toán tử này gọi là toán tử new trong C/C++.
Nếu bạn không cần thiết bộ nhớ động đã cấp phát nữa, bạn có thể sử dụng toán tử deletetrong C/C++, sẽ giải phóng bộ nhớ đã được cấp phát trước đó bởi toán tử new.

Toán tử new và delete trong C/C++

Đây là cú pháp chung để sử dụng toán tử new để cấp phát bộ nhớ động cho bất kỳ kiểu dữ liệu nào trong C/C++:
new kieu_du_lieu;
Ở đây, kieu_du_lieu có thể là bất kỳ kiểu dữ liệu có sẵn nào ví dụ như mảng hoặc các kiểu dữ liệu tự định nghĩa như lớp hoặc cấu trúc. Đầu tiên, chúng ta xét các kiểu dữ liệu có sẵn. Ví dụ, chúng ta có thể định nghĩa một con trỏ tới kiểu double và sau đó yêu cầu rằng bộ nhớ được cấp phát tại thời gian thực thi. Chúng ta có thể làm điều này bởi sử dụng toán tử newtrong C/C++ với các lệnh sau:
double* contro  = NULL; // con tro duoc khoi tao voi gia tri null
contro  = new double;   // yeu cau bo nho cho bien
Bộ nhớ có thể chưa được cấp phát thành công, nếu phần bộ nhớ rỗi (free store) đã được sử dụng. Vì thế, đây là bài thực hành tốt cho bạn khi kiểm tra nếu toán tử new trả về con trỏ NULL và thực hiện hành động thích hợp, như sau:
double* contro  = NULL;
if( !(contro  = new double ))
{
   cout << "Error: Het bo nho!" <<endl;
   exit(1);

}
Hàm malloc() từ C, vẫn tồn tại trong C/C++, nhưng tôi đề nghị bạn tránh sử dụng hàm malloc() này. Lợi thế lớn nhất của toán tử new so với hàm malloc() là toán tử new không chỉ cấp phát bộ nhớ, nó còn xây dựng đối tượng theo mục đích chính của C/C++.
Tại bất kỳ thời điểm, khi bạn cảm thấy một biến đã được cấp phát động là không cần thiết nữa, bạn có thể giải phóng bộ nhớ mà nó đã chiếm giữ trong phần bộ nhớ rỗi với toán tửdelete trong C/C++, như sau:
delete contro;        // giai phong bo nho duoc tro boi contro
Bây giờ, dựa vào các khái niệm trên chúng tôi tạo một ví dụ minh họa cách toán tử new và delete trong C/C++ làm việc:
#include <iostream>
using namespace std;

int main ()
{
   double* contro  = NULL; // con tro duoc khoi tao voi gia tri null
   contro  = new double;   // yeu cau bo nho cho bien
 
   *contro = 1234.56;     // luu giu gia tri tai dia chi da duoc cap phat
   cout << "Gia tri cua contro la: " << *contro << endl;

   delete contro;         // giai phong bo nho.

   return 0;
}
Biên dịch và chạy chương trình C/C++ trên sẽ cho kết quả sau:
Gia tri cua contro la: 1234.56

Cấp phát bộ nhớ động cho Mảng trong C/C++

Giả sử bạn muốn cấp phát bộ nhớ cho một mảng ký tự, ví dụ một chuỗi 20 ký tự. Bạn sử dụng cùng cú pháp đã sử dụng ở trên, như sau:
char* contro  = NULL;   // con tro duoc khoi tao voi gia tri null
contro  = new char[20]; // yeu cau bo nho cho bien
Để xóa mảng mà chúng ta vừa tạo, cú pháp trong C/C++ là:
delete [] contro;        // xoa mang da duoc tro boi contro
Đây là cú pháp chung của toán tử new, bạn có thể sử dụng để cấp phát bộ nhớ cho một mảng đa chiều trong C/C++:
double** contro  = NULL;     // con tro duoc khoi tao voi gia tri null
contro  = new double [3][4]; // cap phat bo nho cho mang 3x4
Tuy nhiên, cú pháp để giải phóng bộ nhớ cho mảng đa chiều vẫn giống ở trên:
delete [] contro;        // xoa mang da duoc tro boi contro

Cấp phát bộ nhớ động cho Đối tượng trong C/C++

Đối tượng không khác các kiểu dữ liệu đơn giản. Ví dụ: bạn xem xét code sau, trong đó chúng ta đang sử dụng một mảng đối tượng để làm rõ khái niệm này:
#include <iostream>
using namespace std;

class NhanVien
{
   public:
      NhanVien() { 
         cout << "Constructor duoc goi!" <<endl; 
         
      }
      ~NhanVien() { 
         cout << "Destructor duoc goi!" <<endl; 
         
      }
};

int main( )
{
   NhanVien* mangNhanVien = new NhanVien[5];

   delete [] mangNhanVien; // xoa mang

   return 0;
}
Nếu bạn chuẩn bị cấp phát bộ nhớ cho một mảng 5 đối tượng NhanVien, constructor đơn giản trên sẽ được gọi 4 lần và tương tự trong khi xóa các đối tượng, destructor cũng sẽ được gọi với số lần tương tự.

[Tự học lập trình c/c++] Vấn đề xử lý ngoại lệ

Một Exception (ngoại lệ) là một vấn đề xuất hiện trong khi thực thi một chương trình. Một Exception trong C++ là một phản hồi về một tình huống ngoại lệ mà xuất hiện trong khi một chương trình đang chạy, ví dụ như chia cho số 0.
Exception cung cấp một cách để truyền điều khiển từ một phần của một chương trình tới phần khác. Exception Handling (Xử lý ngoại lệ) trong C++ được xây dựng dựa trên 3 từ khóa là: try, catch, và throw.
  • throw: Một chương trình ném một Exception khi một vấn đề xuất hiện. Việc này được thực hiện bởi sử dụng từ khóa throw trong C++.
  • catch: Một chương trình bắt một Exception với một Exception Handler tại vị trí trong một chương trình nơi bạn muốn xử lý vấn đề đó. Từ khóa catch trong C++ chỉ dẫn việc bắt một exception.
  • try: Một khối try có thể được bắt bởi một số lượng cụ thể exception. Nó được theo sau bởi một hoặc nhiều khối catch.
Giả sử một khối sẽ tạo một Exeption, một phương thức bắt một exception bởi sử dụng kết hợp các từ khóa try và catch. Một khối try/catch được đặt xung quanh code mà có thể tạo một exception. Code bên trong một khối try/catch được xem như là code được bảo vệ, và cú pháp để sử dụng try/catch trong C++ như sau:
try
{
   // phan code duoc bao ve
}catch( ten_Exception e1 )
{
   // day la khoi catch
}catch( ten_Exception e2 )
{
   // day la khoi catch
}catch( ten_Exception eN )
{
   // day la khoi catch
}
Bạn có thể liệt kê nhiều lệnh catch để bắt các kiểu exception khác nhau trong trường hợp khối try của bạn xuất hiện nhiều hơn một exception trong các tình huống khác nhau.

Ném Exception trong C++

Exception có thể bị ném ở bất cứ đâu bên trong một khối code bởi sử dụng các lệnh throwtrong C++. Toán hạng của lệnh throw quyết định kiểu cho exception và có thể là bất kỳ biểu thức nào và kiểu kết quả của biểu thức quyết định kiểu của exception bị ném.
Ví dụ sau minh họa việc ném một exception khi chia cho số 0 trong C++:
double phepchia(int a, int b)
{
   if( b == 0 )
   {
      throw "Chu y: Ban dang chi cho so 0!!!";
   }
   return (a/b);
}

Bắt Exception trong C++

Khối catch theo sau khối try trong C++ sẽ bắt bất kỳ exception nào. Bạn có thể xác định kiểu của exception bạn muốn bắt và điều này được xác định bởi khai báo exception mà xuất hiện trong các dấu ngoặc đơn theo sau từ khóa catch trong C++.
try
{
   // phan code duoc bao ve
}catch( ten_Exception e )
{
  // phan code de xu ly ngoai le co ten la ten_Exception
}
Code trên sẽ bắt một exception có kiểu là ten_Exception. Nếu bạn muốn xác định rằng một khối catch nến xử lý bất kỳ kiểu exception nào bị ném trong một khối try, bạn phải đặt một dấu ba chấm (…) trong các dấu ngoặc đơn theo sau từ khóa catch, như sau:
try
{
   // phan code duoc bao ve
}catch(...)
{
  // phan code de xu ly bat ky kieu ngoai le nao
}
Ví dụ sau ném một exception khi chia cho số 0 và chúng ta bắt nó trong khối catch.
#include <iostream>
using namespace std;

double phepchia(int a, int b)
{
   if( b == 0 )
   {
      throw "Chu y: Ban dang chi cho so 0!!!";
   }
   return (a/b);
}

int main ()
{
   int x = 15;
   int y = 0;
   double z = 0;
 
   try {
     z = phepchia(x, y);
     cout << z << endl;
   }catch (const char* msg) {
     cerr << msg << endl;
   }

   return 0;
}
Bởi vì chúng ta đang tạo một exception có kiểu const char*, vì thế trong khi bắt exception này, chúng ta phải sử dụng const char* trong khối catch. Biên dịch và chạy chương trình C++ trên sẽ cho kết quả sau:
Chu y: Ban dang chi cho so 0!!!

Standard Exception trong C++

C++ cung cấp một danh sách các Standard Exception được định nghĩa trong <exception>mà chúng ta có thể sử dụng trong các chương trình. Những exception này được sắp xếp theo cấu trúc thứ tự cha-con như sau:
Cấu trúc Exceptions trong C++
Bảng dưới là miêu tả ngắn gọn về mỗi exception được đề cập trong sơ đồ trên:
ExceptionMiêu tả
std::exceptionMột exception và lớp cha của tất cả Standard Exception trong C++
std::bad_allocCó thể được ném bởi new
std::bad_castCó thể được ném bởi dynamic_cast
std::bad_exceptionĐây là thiết bị hữu ích để xử lý Unexpected Exception trong một chương trình C++
std::bad_typeidCó thể được ném bởi typeid
std::logic_errorMột exception mà theo lý thuyết có thể được phát hiện bởi việc đọc code
std::domain_errorĐây là một exception được ném khi một miền toán học không hợp lệ được sử dụng
std::invalid_argumentĐược ném do các tham số không hợp lệ
std::length_errorĐược ném khi một std::string quá lớn được tạo ra
std::out_of_rangeCó thể được ném bởi một phương thức, ví dụ std::vector và std::bitset<>::operator[]().
std::runtime_errorMột exception mà theo lý thuyết không thể được phát hiện bởi việc đọc code
std::overflow_errorĐược ném nếu một sự tràn luồng toán học (mathematical overflow) xuất hiện
std::range_errorXuất hiện khi bạn cố gắng lưu giữ một giá trị bên ngoài dãy giá trị
std::underflow_errorĐược ném nếu một mathematical underflow (sự tràn dưới) xuất hiện

Định nghĩa Exception mới trong C++

Bạn có thể định nghĩa các exception cho riêng bạn bằng việc kế thừa và ghi đè tính năng lớp exception trong C++. Ví dụ sau minh họa cách bạn có thể sử dụng lớp std::exception để triển khai exception của riêng bạn theo một cách chuẩn trong C++:
#include <iostream>
#include <exception>
using namespace std;

struct MyException : public exception
{
  const char * what () const throw ()
  {
    return "Exception trong C++";
  }
};
 
int main()
{
  try
  {
    throw MyException();
  }
  catch(MyException& e)
  {
    std::cout << "MyException da duoc bat!" << std::endl;
    std::cout << e.what() << std::endl;
  }
  catch(std::exception& e)
  {
    // phan nay danh cho cac error khac
  }
}
Nó sẽ cho kết quả sau:
MyException da duoc bat!
Exception trong C++
Ở đây, what() là một phương thức public được cung cấp bởi lớp exception trong C++ và nó đã được ghi đè bởi tất cả các lớp exception con. Ví dụ này trả về nguyên nhân của một exception trong C++.

[Tự học lập trình C/C++] Truy xuất dữ liệu kiểu tệp - file

Tới bây giờ, chúng ta đã sử dụng thư viện chuẩn iostream, cung cấp các phương thức cin và cout để đọc từ Standard Input và ghi tới Standard Output tương ứng.
Chương này sẽ hướng dẫn bạn cách đọc và ghi một file. Điều này cần một Thư viện chuẩn C++ khác là fstream, mà định nghĩa 3 kiểu dữ liệu mới:
Kiểu dữ liệuMiêu tả
ofstreamKiểu dữ liệu này biểu diễn Output File Stream và được sử dụng để tạo các file và để ghi thông tin tới các file đó
ifstreamKiểu dữ liệu này biểu diễn Input File Stream và được sử dụng để đọc thông tin từ các file
fstreamKiểu dữ liệu này nói chung biểu diễn File Stream, và có các khả năng của cả ofstream và ifstream, nghĩa là nó có thể tạo file, ghi thông tin tới file và đọc thông tin từ file
Để thực hiện tiến trình xử lý file trong C++, bạn bao các header file là <iostream> và <fstream> trong source file của chương trình C++ của bạn.

Mở một File trong C++

Một file phải được mở trước khi bạn có thể đọc thông tin từ nó hoặc ghi thông tin tới nó. Hoặc đối tượng ofstream hoặc đối tượng fstream có thể được sử dụng để mở một file với mục đích viết hoặc đối tượng ifstream được sử dụng để mở file chỉ với mục đích đọc.
Dưới đây là cú pháp chuẩn cho hàm open(), là một thành viên của các đối tượng fstream, ifstream và ofstream trong C++:
void open(const char *ten_file, ios::che_do);
Tại đây, tham số đầu tiên xác định tên và vị trí của file để được mở và tham số thứ hai của hàm thành viên open() định nghĩa chế độ mà file nên được mở.
Chế độMiêu tả
ios::appChế độ Append. Tất cả output tới file đó được phụ thêm vào cuối file đó
ios::ateMở một file cho outpur và di chuyển điều khiển read/write tới cuối của file
ios::inMở một file để đọc
ios::outMở một file để ghi
ios::truncNếu file này đã tồn tại, nội dung của nó sẽ được cắt (truncate) trước khi mở file
Bạn có thể kết hợp hai hoặc nhiều giá trị này bằng việc hoặc chúng cùng với nhau (sử dụng (|). Ví dụ, nếu bạn muốn mở một file trong chế độ ghi và muốn cắt (truncate) nó trong trường hợp nó đã tồn tại, bạn theo cú pháp sau:
ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc );
Theo cách tương tự, bạn có thể mở một file với mục đích đọc và ghi như sau:
fstream  vietjack;
vietjack.open("file.dat", ios::out | ios::in );

Đóng một File trong C++

Khi một chương trình C++ kết thúc, nó tự động đóng tất cả Stream, giải phóng tất cả bộ nhớ đã cấp phát và đóng tất cả file đã mở. Nhưng nó là bài thực hành tốt cho một lập trình viên nên đóng tất cả file đã mở trước khi kết thúc chương trình.
Dưới đây là cú pháp chung cho hàm close() trong C++, là một thành viên của các đối tượng fstream, ifstream và ofstream trong C++:
void close();

Ghi File trong C++

Trong khi lập trình C++, bạn ghi thông tin tới một file từ chương trình của bạn bởi sử dụng toán tử chèn luồng là (<<), giống như khi bạn sử dụng toán tử đó để tạo thông tin đầu ra tới màn hình. Chỉ có một điểm khác nhau là bạn sử dụng một đối tượng ofstream hoặcfstream trong C++ thay cho đối tượng cout trong C++.

Đọc một File trong C++

Bạn đọc thông tin từ một file trong chương trình C++ của bạn bởi sử dụng toán tử trích luồng là (>>), giống như bạn sử dụng toán tử đó để nhập thông tin đầu vào từ bàn phím. Điểm khác nhau là bạn sử dụng một đối tượng ifstream hoặc fstream thay vì sử dụng đối tượng cin trong C++.

Ví dụ về Đọc và Ghi file trong C++

Chương trình C++ sau mở một file trong chế độ đọc và ghi. Sau khi ghi thông tin được nhập vào bởi người sử dụng tới một file là vietjack.dat, chương trình đọc thông tin từ file đó và tạo kết quả đầu ra trên màn hình:
#include <fstream>
#include <iostream>
using namespace std;
 
int main ()
{
    
   char data[100];

   // mo mot file trong che do write.
   ofstream outfile;
   outfile.open("tuhoctincoban.dat");

   cout << "Ghi du lieu toi file!" << endl;
   cout << "Nhap ten cua ban: "; 
   cin.getline(data, 100);

   // ghi du lieu da nhap vao trong file.
   outfile << data << endl;

   cout << "Nhap tuoi cua ban: "; 
   cin >> data;
   cin.ignore();
   
   // ghi du lieu da nhap vao trong file.
   outfile << data << endl;

   // dong file da mo.
   outfile.close();

   // mo mot file trong che do read.
   ifstream infile; 
   infile.open("vietjack.dat"); 
 
   cout << "\n===========================\n" ;
   cout << "Doc du lieu co trong file!" << endl; 
   infile >> data; 

   // ghi du lieu tren man hinh.
   cout << data << endl;
   
   // tiep tuc doc va hien thi du lieu.
   infile >> data; 
   cout << data << endl; 

   // dong file da mo.
   infile.close();

   return 0;
}
Ví dụ trên sử dụng các hàm bổ sung từ đối tượng cin, như hàm getline() để đọc dòng từ bên ngoài và hàm ignore() để bỏ qua các ký tự phụ bên trái lệnh read trước đó.

Con trỏ vị trí File trong C++

Cả hai đối tượng istream và ostream đều cung cấp các hàm thành viên để xác định lại vị trí của con trỏ vị trí file (file-position pointer). Các hàm thành viên này là seekg (viết tắt của seek get) cho istream và seekp (viết tắt của seek put) cho ostream trong C++.
Tham số cho seekg và seekp thương là một long int. Tham số thứ hai có thể được xác định để chỉ dẫn hướng tìm kiếm. Hướng tìm kiếm có thể là ios::beg (mặc định) để xác định vị trí liên quan tới phần bắt đầu của một Stream, là ios::cur để xác định vị trí liên quan tới vị trí hiện tại trong một Stream hoặc là ios::end để xác định vị trí liên quan tới phần kết thúc của một Stream trong C++.
Con trỏ vị trí file là một giá trị integer mà xác định vị trí trong file, tính toán là số byte từ vị trí bắt đầu của file đó. Dưới đây là một số ví dụ để xác định vị trí của con trỏ vị trí file trong C++:
// xac dinh vi tri byte thu n cua doi tuong file
doi_tuong_file.seekg( n );

// xac dinh vi tri n byte ve sau cua doi tuong file
doi_tuong_file.seekg( n, ios::cur );

// xac dinh vi tri n byte bat dau tu cuoi cua doi tuong file
doi_tuong_file.seekg( n, ios::end );

// xac dinh vi tri tai cuoi doi tuong file
doi_tuong_file.seekg( 0, ios::end );

Bài đăng phổ biến

Bài viết mới nhất

Tin học cơ bản - Nền tảng của mọi kỹ năng

Mọi thông tin trên blog đều được giữ bản quyền bởi Tin học cơ bản. Các bạn nếu muốn lấy thông tin từ blog vui lòng ghi rõ nguồn Tinhoccoban.net

TIN HỌC CƠ BẢN