Standard Template Library Containers (STL Containers)

####Chào các bạn đang theo dõi khóa học lập trình trực tuyến ngôn ngữ C++.

Trong bài học này, mình sẽ hướng dẫn các bạn cách sử dụng một số container class của STL. Trước các bài học trước đây, mình đã từng giới thiệu đến các bạn một class thuộc STL, đó là std::array (trong chương mảng một chiều) hay là std::string trong chương mảng kí tự. std::array thư viện khá đơn giản có thể dùng để thay thế cho mảng một chiều.

Tuy nhiên, nhược điểm của std::array (cũng là nhược điểm của mảng một chiều) là kích thước bộ nhớ được cấp phát là cố định. Do đó, khả năng khai báo thiếu số lượng phần tử hoặc dư thừa quá mức cần thiết thường xuyên xảy ra. Để khắc phục nhược điểm này, STL đã hổ trợ cho chúng ta các container class khác dùng để lưu trữ, xử lý các phần tử với kích thước vùng nhớ tự động thay đổi để phù hợp với yêu cầu người dùng.

Trước khi đi vào tìm hiểu các container class của STL, chúng ta nên phân chúng ra thành 3 loại container khác nhau:

  • Sequence container

    Sequence container là tập hợp các container class nó duy trì trật tự của các phần tử bên trong container. Một đặc tính của Sequence container là các bạn có thể chọn ví trí muốn chèn thêm phần tử vào container. Một ví dụ điển hình là khi các bạn thêm một phần tử vào mảng một chiều, các phần tử còn lại vẫn giữ nguyên thứ tự của chúng như trước khi chèn.

    Ngôn ngữ C++11 cung cấp cho chúng ta 6 sequence containers: std::vector, std::deque, std::array, std::list, std::forward_list, std::basic_string.

  • Associative container

    Associative container là các container được thiết kế kèm theo cơ chế tự động sắp xếp dữ liệu đầu vào khi chúng được đưa vào container. Cơ chế tổ chức dữ liệu của các container này sẽ quyết định vị trí của các phần tử trong container. Thông thường, associative container thực hiện phép so sánh sử dụng toán tử <.

    Một số associative container được sử dụng phổ biến là: std::set, std::map…

  • Container adapter

    Container adapter là các container được sử dụng cho các cấu trúc lưu trữ đặc biệt: std::stack, std::queue, …

#Sequence container

##std::vector

Cũng giống như std::array, vectors lưu trữ các phần tử liên tiếp nhau trên bộ nhớ ảo. Điều này có nghĩa những phần tử trong std::vector có thể truy cập thông qua các con trỏ có cùng kiểu dữ liệu với kiểu của std::vector. Đặc điểm khiến std::vector khác với std::array là nó có thể thay đổi kích thước container tự động khi các phương thức, các toán tử được định nghĩa bên trong class std::vector được sử dụng.

###Khai báo một std::vector

Để sử dụng std::vector, chúng ta cần include thư viện vector vào file chương trình. Dưới đây là cú pháp khai báo cơ bản để tạo ra một đối tượng từ lớp std::vector:

std::vector< <data_type> > <vector_name>;

data_type của vector là một kiểu dữ liệu tổng quát, các bạn có thể chọn tùy ý dựa vào mục đích lưu trữ của chương trình, nó có thể là một kiểu dữ liệu built-in, cũng có thể là kiểu dữ liệu struct do các bạn tự định nghĩa ra. Mình lấy một ví dụ như sau:

#include <vector>

std::vector<__int32> vecInt32;

Như vậy là chúng ta đã có một đối tượng của lớp std::vector có thể lưu trữ các phần tử kiểu __int32 trong container. Lúc này, container của vecInt32 vẫn chưa có phần tử nào cả, chúng ta có thể kiểm chứng bằng cách sử dụng phương thức std::vector::size() để kiểm tra kích thước của container.

std::cout << "Size of vecInt32: " << vecInt32.size() << std::endl;

Kết quả cho ra màn hình là 0 (tương ứng với 0 phần tử hiện tại có trong container của vecInt32).

###Một số phương thức khởi tạo cho đối tượng của lớp std::vector

Sau này khi học đến phần C++ Object oriented programming, các bạn sẽ biết rằng một class có thể có một hoặc nhiều phương thức khởi tạo giá trị cho các thành phần dữ liệu bên trong class đó. Class std::vector có đến hơn 10 phương thức khởi tạo khác nhau:

Tuy nhiên, mình chỉ hướng dẫn các bạn sử dụng một số phương thức khởi tạo đơn giản. Ví dụ:

std::vector<__int32> vec1(); //gọi phương thức khởi tạo mặc định, khởi tạo kích thước container là 0.

std::vector<__int32> vec2(5); //gọi phương thức khởi tạo std::vector(size_t _Count) với giá trị truyền vào là 5, khởi tạo kích thước container là 5 phần tử kiểu __int32.

std::vector<__int32> vec3(vec2); //gọi phương thức khởi tạo std::vector(const std::vector &other), khởi tạo một container giống với container của vector other.

Những phương thức khởi tạo còn lại thường ít được sử dụng.

###Sử dụng các phương thức trong class std::vector

Sau khi các bạn khai báo (có thể có khởi tạo hoặc không) một đối tượng của lớp std::vector, lúc này chúng ta có thể sử dụng các phương thức (các hàm bên trong lớp) trong lớp std::vector để thao tác với container của đối tượng đó.

####Modifiers

#####std::vector::push_back(const vector_type &value)

Khi mình viết std::vector::push_back, các bạn có thể hiểu phương thức push_back được định nghĩa bên trong khối lệnh có tên vector, và vector được định nghĩa bên trong khối lệnh của std.

Phương thức này cho phép thêm một phần tử vào sau phần tử có vị trí cuối cùng trong container, phần tử mới được thêm vào sẽ trở thành phần tử có chỉ số cao nhất trong container. Ví dụ:

std::vector<__int32> vecInt32;
for (int i = 1; i <= 10; i++)
{
    vecInt32.push_back(i);
}

Mỗi lần sử dụng phương thức push_back, kích thước của container sẽ tăng lên 1. Nếu số phần tử vượt quá kích thước của container, vector sẽ tự động cấp phát lại vùng nhớ đủ để chứa tất cả các phần tử.

#####std::vector::pop_back()

Gọi phương thức này sẽ xóa đi một phần tử tại chỉ số cuối cùng trong container. Ví dụ:

std::vector<__int32> vecInt32;

for (int i = 1; i <= 10; i++)
{
	vecInt32.push_back(i);
}
//container = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }

vecInt32.pop_back();
//container = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }

Nếu các bạn sử dụng phương thức này khi container của vector rỗng, Visual studio (trong chế độ Debug) sẽ thông báo chương trình vi phạm điều kiện của assertion trong phương thức pop_back.

#####std::vector::insert

Để sử dụng phương này, chúng ta cần sử dụng thêm STL Iterator, đến bài học về STL Iterator mình sẽ trình bày rõ hơn về phương thức này. Các bạn có thể hiểu đơn giản là một iterator cũng là một con trỏ, nó trỏ vào container của std::vector giúp phương thức insert xác định được vị trí cần chèn phần tử.

std::vector<__int32> vecInt32;
std::vector<__int32>::iterator iter;

//iter point to the begin of vecInt32
iter = vecInt32.begin();

//insert new element into vecInt32's container
vecInt32.insert(iter, 10);

//container = { 10 }

iter = vecInt32.end();
vecInt32.insert(iter, 20);

//container = { 10, 20 }

iter = vecInt32.begin() + 1;
vecInt32.insert(iter, 15);

//container = { 10, 15, 20 }

Kích thước của container sẽ tự động thay đổi đủ để chứa tất cả các phần tử sau khi insert.

#####std::vector::erase(const_iterator position)

Phương thức này cho phép xóa một phần tử tại vị trí được trỏ đến bởi iterator position. Ví dụ:

std::vector<int> myvector;

// set some values (from 1 to 10)
for (int i=1; i<=10; i++) 
	myvector.push_back(i);

// erase the 6th element
myvector.erase (myvector.begin()+5);

//container = { 1, 2, 3, 4, 5, 7, 8, 9, 10 }

####Element access

#####operator[size_type index]

Sử dụng toán tử [] cho vector sẽ trả về giá trị tại chỉ số index được truyền vào (Tương tự mảng một chiều).

std::vector<int> myvector(10); //10 zero-initialized elements

for (int i = 0; i < myvector.size(); i++)
	myvector[i] = i;
	
std::cout << myvector[4] << std::endl;

Các bạn lưu ý chỉ số bắt đầu của container cũng là 0 giống với mảng một chiều.

#####std::vector::at(size_type index)

Tương tự operator[].

std::vector<int> myvector(10); //10 zero-initialized elements

for (int i = 0; i < myvector.size(); i++)
	myvector[i] = i;
	
std::cout << myvector.at(4) << std::endl;

####Capacity

#####std::vector::size()

Phương thức size trả về số lượng phần tử chứa trong container của vector.

std::vector<int> myints;
std::cout << "0. size: " << myints.size() << std::endl;

for (int i=0; i<10; i++) 
	myints.push_back(i);
	
std::cout << "1. size: " << myints.size() << std::endl;

#####std::vector::max_size()

Trả về số lượng phần tử tối đa mà vector có thể chứa, đồng nghĩa với kích thước vùng nhớ tối đa có thể cấp phát cho container của vector.

std::vector<int> myvector;

// set some content in the vector
for (int i = 0; i<100; i++) 
	myvector.push_back(i);

std::cout << "size: " << myvector.size() << std::endl;
std::cout << "max_size: " << myvector.max_size() << std::endl;

#####std::vector::capacity()

Trả về kích thước bộ nhớ đã cấp phát cho container của vector.

std::vector<int> myvector;

// set some content in the vector
for (int i = 0; i<100; i++) 
	myvector.push_back(i);

std::cout << "size: " << myvector.size() << std::endl;
std::cout << "capacity: " << (int)myvector.capacity() << std::endl;

Kích thước vùng nhớ được cấp phát cho container thường sẽ lớn hơn số lượng phần tử mà container đang chứa. Điều này đảm bảo khi người dùng thêm phần tử vào container, vector sẽ chưa cần tốn chi phí cấp phát lại vùng nhớ mới.

Mình vừa trình bày một số phương thức đơn giản thường được sử dụng của class std::vector. Các bạn có thể tự mình tìm hiểu các phương thức khác của class std::vector qua đường dẫn sau:

http://www.cplusplus.com/reference/vector/vector/

##std::list

std::list containers mô phỏng lại cấu trúc dữ liệu doubly-linked lists; Doubly-linked list có thể lưu trữ các phần tử không liên tiếp nhau trên vùng nhớ nhưng vẫn đảm bảo được thứ tự của các phần tử khi truy xuất. Trật tự của các phần tử được giữ bởi những sự kết nối giữa các cặp phần tử.

Cấu trúc dữ liệu doubly-linked list cho phép người dùng thêm và xóa phần tử tại vị trí bất kỳ trong container, nhưng không hổ trợ truy cập ngẫu nhiên.

###Khai báo một std::list

Để sử dụng std::list, chúng ta cần include thư viện list vào file chương trình. Dưới đây là cú pháp khai báo một đối tượng của lớp std::list:

std::list< <data_type> > list_name;

Trong ví dụ dưới đây mình tạo ra một std::list dùng để lưu trữ các phần tử số nguyên:

#include <list>

std::list<__int32> my_list;

###Khởi tạo đối tượng của lớp std::list

Cũng tương tự như class std::vector, std::list cũng có một số phương thức khởi tạo riêng. Một số phương thức khởi tạo thường dùng như:

std::list<__int32> list1(); //gọi default constructor

std::list<__int32> list2(5); //gọi phương thức khởi tạo list(size_t _Count), khởi tạo danh sách có _Count phần tử

std::list<__int32> list3(list2); //gọi phương thức khởi tạo list(const list_type &other), khởi tạo danh sách có container giống với other

###Sử dụng các phương thức của lớp std::list

####Capacity

(Tương tự std::vector)

####Element access

Đối với cấu trúc dữ liệu doubly-linked list, chúng ta chỉ có thể truy xuất giá trị ở vị trí đầu tiên và cuối cùng trong container, std::list hỗ trợ cho chúng ta 2 phương thức truy xuất.

#####std::list::front()

Phương thức trả về giá trị của phần tử đầu tiên trong list container.

std::list<__int32> my_list(5);
std::cout << my_list.front() << std::endl;

#####std::list::back()

Ngược lại với phương thức ở trên, phương thức back trả về giá trị của phần tử cuối cùng trong list container.

std::list<__int32> my_list(5);
std::cout << my_list.back() << std::endl;

####Modifiers

#####std::list::push_back(const list_type &value)

Phương thức này cho phép thêm một phần tử vào sau phần tử có vị trí cuối cùng trong container, phần tử mới được thêm vào sẽ trở thành phần tử có chỉ số cao nhất trong container. Ví dụ:

std::list<int> mylist;
int myint;

std::cout << "Please enter some integers (enter 0 to end):" << std::endl;

do 
{
	std::cin >> myint;
	mylist.push_back (myint);
} while (myint);

std::cout << "mylist stores " << mylist.size() << " numbers." << std::endl;

#####std::list::pop_back()

Xóa một phần tử cuối cùng trong list container. Ví dụ:

std::list<int> mylist;
int sum (0);
mylist.push_back (100);
mylist.push_back (200);
mylist.push_back (300);

while (!mylist.empty())
{
	sum+=mylist.back();
	mylist.pop_back();
}

std::cout << "The elements of mylist summed " << sum << std::endl;

Phương thức std::list::empty() dùng để kiểm tra container của list có rỗng hay không, nếu số phần tử của list khác 0 thì trả về false, ngược lại trả về true. (Các class containers của STL đều có phương thức này).

#####std::list::push_front(const list_type &value)

Ngược lại với phương thức std::list::push_back, phương thức này dùng để thêm một phần tử có cùng kiểu với list vào vị trí đầu tiên trong container.

std::list<int> mylist;
mylist.push_front (200);
mylist.push_front (300);

//container = { 300, 200 }

#####std::list::pop_front()

Xóa một phần tử trong list container tại vị trí đầu tiên.

std::list<int> mylist;
mylist.push_back (100);
mylist.push_back (200);
mylist.push_back (300);

std::cout << "Popping out the elements in mylist: ";
while (!mylist.empty())
{
std::cout << ' ' << mylist.front();
mylist.pop_front();
}

std::cout << std::endl;
std::cout << "Final size of mylist is " << mylist.size() << std::endl;

####Operations

#####std::list::sort()

Phương thức này sẽ sắp xếp lại dữ liệu bên trong container theo thứ tự tăng dần (mặc định là vậy).

std::list<__int32> my_list;
std::list<__int32>::iterator iter;
my_list.push_back(4);
my_list.push_back(6);
my_list.push_back(-2);
my_list.push_back(-1);
my_list.push_back(7);

//container = { 4, 6, -2, -1, 7 }

my_list.sort();

//container = { -2, -1, 4, 6, 7 }

Các bạn cũng có thể thay đổi điều kiện so sánh của phương thức sort bằng cách tự tạo một hàm so sánh 2 phần tử cùng kiểu dữ liệu với list. Ví dụ:

bool my_comparison(__int32 n1, __int32 n2)
{
	return n2 < n1;
}

int main()
{
	std::list<__int32> my_list;
	std::list<__int32>::iterator iter;
	my_list.push_back(4);
	my_list.push_back(6);
	my_list.push_back(-2);
	my_list.push_back(-1);
	my_list.push_back(7);
	
	//container = { 4, 6, -2, -1, 7 }

	my_list.sort(my_comparison);

	//container = { 7, 6, 4, -1, -2 }
	
	return 0;
}

Mình vừa liệt kê ra một vài phương thức thường dùng khi sử dụng std::list. Các bạn có thể tìm hiểu thêm một số phương thức khác của std::list tại đường dẫn

http://www.cplusplus.com/reference/list/list/

Vậy là mình đã cùng các bạn tìm hiểu 2 sequence container trong bộ thư viện STL của ngôn ngữ C++. Do giới hạn thời gian và để đảm bảo hiệu suất của khóa học, các container tiếp theo mình sẽ đưa đường dẫn để các bạn tự tìm hiểu các phương thức trong từng class container. Cách tự tìm hiểu cũng giống như mình làm cùng các bạn ở trên.

#Associative container

##std::set

std::set là class định nghĩa của một dạng container chỉ cho phép lưu trữ các phần tử có giá trị là duy nhất, phần tử có giá trị trùng lặp là không được cho phép. Những phần tử được thêm vào container sẽ được tự động sắp xếp dựa trên giá trị của chúng.

Dưới đây là một đoạn chương trình mẫu sử dụng std::set

#include <iostream>
#include <set>

int main ()
{
	std::set<int> myset;
	std::set<int>::iterator it;

	// set some initial values:
	for (int i=1; i<=5; i++) 
		myset.insert(i*10);    // set: 10 20 30 40 50

	it = myset.find(20);
	myset.erase (it);
	myset.erase (myset.find(40));

	std::cout << "myset contains:";
	for (it=myset.begin(); it!=myset.end(); ++it)
		std::cout << ' ' << *it;
	std::cout << std::endl;

	return 0;
}

##std::map

std::map là class định nghĩa một loại container dùng để lưu trữ các phần tử theo cấu trúc kết hợp (key_value, mapped_value). Dữ liệu trong container được sắp xếp dựa trên key_value, do đó key_value không được trùng lặp. Với mỗi key_value sẽ ánh xạ đến một mapped_value duy nhất.

http://www.cplusplus.com/reference/map/map/

Các bạn lưu ý khi sử dụng std::map, mỗi khi thêm phần tử vào container thì phải thêm đủ một cặp giá trị <key, value>. Ngôn ngữ C++ cung cấp cho chúng ta class std::pair giúp chúng ta nhóm 2 đối tượng có cùng (hoặc khác) kiểu dữ liệu thành một cặp tương ứng với cặp <key, value> của std::map.

std::pair< data_type, data_type > pair_name;

Một ví dụ về sử dung std::map để quản lý thông tin nhân viên:

#include <iostream>
#include <string>
#include <map>

struct Employee
{
	std::string name;
	__int32 year_of_experience;
};

int main()
{
	std::map<__int32, Employee> listEmployee;

	listEmployee.insert(std::pair<__int32, Employee>(1, { "Le Tran Dat", 5 }));
	listEmployee.insert(std::pair<__int32, Employee>(2, { "Someone", 0 }));

	listEmployee[4] = { "new employee", 1 }; //operator[<key>] = <value>

	std::map<__int32, Employee>::iterator iter = listEmployee.find(3);
	if (iter != listEmployee.end())
	{
		std::cout << "Employee ID:        " << iter->first << std::endl;
		std::cout << "Employee name:      " << (iter->second).name << std::endl;
		std::cout << "Year of experience: " << (iter->second).year_of_experience << std::endl;
	}
	else
	{
		std::cout << "ID 3 is not exist" << std::endl;
	}

	return 0;
}

#Container adapter

##std::stack

Stack (hay còn gọi là ngăn xếp) là một dạng container có cơ chế tổ chức lưu trữ dữ liệu đặc biệt: LIFO (Last In First Out).

Phần tử được thêm vào sau cùng sẽ được lấy ra đầu tiên.

http://www.cplusplus.com/reference/stack/stack/

Dưới đây là một ví dụ ứng dụng cách tổ chức lưu trữ của cấu trúc dữ liệu Stack để chuyển đổi một số từ hệ thập phân sang hệ nhị phân:

#include <iostream>
#include <stack>

int main()
{
	//input
	__int32 decNumber;
	std::cin >> decNumber;

	//converting
	std::stack<bool> binary;
	while (decNumber)
	{
		binary.push((decNumber % 2 == 1));
		decNumber /= 2;
	}

	//output
	while (!binary.empty())
	{
		std::cout << binary.top();
		binary.pop(); //remove an element of stack
	}
	std::cout << std::endl;

	return 0;
}

Cấu trúc dữ liệu Stack còn có nhiều ứng dụng thực tiễn, ví dụ tạo ra chuỗi trạng thái cho phép người dùng trở lại trạng thái trước đó (undo).

##std::queue

Queue (hay còn gọi là hàng đợi) là một cấu trúc dữ liệu hoạt động ngược lại so với cấu trúc dữ liệu Stack: FIFO (First In First Out). Chúng ta gặp cấu trúc dữ liệu này khá nhiều trong thực tế, ví dụ xếp hàng chờ mua hàng, người nào đến trước sẽ được mua hàng trước và ra khỏi hàng chờ sớm nhất.

http://www.cplusplus.com/reference/queue/queue/

Dưới đây là một ví dụ sử dụng std::queue:

#include <iostream>      
#include <queue>         

int main()
{
	std::queue<int> myqueue;
	int myint;

	std::cout << "Please enter some integers (enter 0 to end):" << std::endl;

	do {
		std::cin >> myint;
		myqueue.push(myint);
	} while (myint);

	std::cout << "myqueue contains: ";
	while (!myqueue.empty())
	{
		std::cout << ' ' << myqueue.front();
		myqueue.pop();
	}
	std::cout << std::endl;

	return 0;
}

###Tổng kết

Trong bài học này, mình đã hướng dẫn các bạn sử dụng một số class container thuộc Standard Template Library (STL) và cách để tự tìm hiểu cách sử dụng chúng. Về mặt cơ bản, các class container chỉ là một tập các thư viện thực thi các cấu trúc dữ liệu thường sử dụng trong lập trình. Cấu trúc dữ liệu là cách thức tổ chức, định dạng dữ liệu trong bộ nhớ máy tính. Chọn đúng cấu trúc dữ liệu cho một bài toán nào đó có thể giúp lập trình viên giải quyết vấn đề dễ dàng hơn.


Hẹn gặp lại các bạn trong bài học tiếp theo trong khóa học lập trình C++ hướng thực hành.

Mọi ý kiến đóng góp hoặc thắc mắc có thể đặt câu hỏi trực tiếp tại diễn đàn

www.daynhauhoc.com

2 Likes
std::vector<__int32> vecInt32;
for (int i = 1; i <= 10; i++)
{
    vecInt32.push_back(i);
}

Tai sao neu khai bao vector std::vector<__int32> vecInt32() thi ko duoc a?

Vì cú pháp của C++ nó thế.

1 Like
  • Ở đoạn code trên, mình chạy đúng rồi từ kích thước 10 giảm xuống còn 9.
std::vector<__int32> vecInt32;

for (int i = 1; i <= 10; i++)
{
	vecInt32.push_back(i);
}
//container = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }

vecInt32.pop_back();
//container = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }
  • Tuy nhiên, mình nhập giá trị của vector vào, sau đó xóa giá trị cuối đi, in lại kiểm tra thì vẫn y nguyên. Bạn nào biết xin chỉ giúp. Thanks.
#include<iostream>
#include<vector>

using namespace std;

int main()
{
	vector<__int32> VecInt32;
	cout<<"Size of the vector: "<<VecInt32.size()<<endl;
	
	//increase the vector size
	for (int i = 0; i<10; i++)
	    VecInt32.push_back(i);
	    
	cout<<"Size of Vector: "<<VecInt32.size()<<endl;
	
	//Enter the value of vector
	for (int i = 0; i<10; i++)
	   VecInt32[i]=i;
	//Printf the value of the vector
	for (int i = 0; i<10; i++)
	   cout<<"The vector value: "<<VecInt32[i]<<endl;
	
	//Delete the last member of vector
	VecInt32.pop_back();
	
	
	cout<<"Size of Vector: "<<VecInt32.size()<<endl;
	
	for (int i = 0; i<10; i++)
	   cout<<"The vector value: "<<VecInt32[i]<<endl;
	
	return 0;
}

Tại sao lại for đến < 10 mà không phải là < VecInt32.size()? Nếu 10 > size của vector thì rất dễ gặp lỗi.

Ở link này có nói

The destructor of the stored object is called, and length of the vector is removed by 1. If the container’s capacity is not reduced, then you can still access the previous memory location but in this case, there is no use of accessing an already popped element, as it will result in an undefined behavior.

Đọc thêm

  • Trong ví dụ của bài học sử dụng tăng kích thước của vector với giá trị 10. Mình thử in xem giá trị của ô cuối cùng đã xóa thì có giá trị nữa hay không. Nếu xóa thành công thì ô số 9 có giá trị rác đúng ko?
  • Để kiểm tra mình dùng #include<windows.h>Sleep(1000). Tuy nhiên, kết quả vẫn vậy.
  • Nguyên nhân code có sai gì ko?

Bạn cố in ra phần tử [9] thì đều là undefined behavior thôi. Kết quả của việc in ra là gì mà chả được.

Không liên quan đến vector.

Size = 9 mà truy cập vào phần tử [9].

  • Trong một số bài trước, tác giả nói là khi ô nhớ bị xóa nếu máy tính chưa sử dụng thì giá trị vẫn lưu nếu sử dụng thì lưu giá trị khác. Nếu sau thời gian delay thì sẽ lưu giá trị khác. Nếu truy xuất thì có thể có giá trị rác. Nên mình dùng Sleep .
  • Mục đích mình kiểm tra xem có đúng ko? Chứ ko phải cái bạn nói truy xuất sai.
  • Cũng xác định xem lệnh pop_back xóa ô cuối cùng của véc tơ đi hoạt động đúng ko? Đó là cái mình muốn hỏi. Vì đang học nên nhiều cái ko biết thì hỏi cụ thể.

nếu phần tử là int thì có thể nó ko bị “xóa” đúng nghĩa. Nếu là object có cấp phát động như string thì mới bị xóa đúng nghĩa.

vector nó là 1 cái mảng động thôi chả có gì phức tạp hết. Mảng động data kèm theo kích thước mảng (capacity) và số phần tử đang xài trong mảng đó (size). Khi push_back 1 phần tử, nếu size < capacity thì nó gán data[size] = newElement, rồi size sẽ tăng lên 1. Nếu size == capacity thì nó sẽ tạo mảng newData mới có kích thước (capacity) gấp đôi, copy mảng hiện tại sang mảng mới, rồi deallocate (gọi delete[]) mảng hiện tại và gán con trỏ mảng mới vào con trỏ mảng cũ. Khi pop_back thì nó sẽ gọi hàm hủy (destructor) của phần tử cuối cùngnếu size > 0, rồi giảm size xuống 1 đơn vị. Với mảng int thì hàm hủy của int chả làm gì cả, nên nó vẫn nằm ở đó ko mất đi đâu. Nếu hàm hủy có làm gì đó như của string chẳng hạn thì mới thấy mất. Hoặc nếu bạn thay đổi kích cỡ của mảng hiện tại (capacity) bằng cách gọi .reserve(100) thì mới thấy nó bị mất, hoặc gọi .shrink_to_fit() thì tương đương với tạo 1 mảng mới với size == capacity, lúc này object kia mới mất đi hoàn toàn, ko phải còn lại cái xác nữa.

// ban đầu
T* data = nullptr;
int capacity = 0;
int size = 0;

//thêm phần tử `v` thứ nhất
data = new T[capacity = 1];
data[size++] = v;

//thêm phần tử `v` sau đó
if (size == capacity)
{
  T* newData = new T[capacity * 2];
  //move các phần tử từ mảng cũ sang mảng mới
  for (int i = 0; i < size; ++i) newData[i] = std::move(data[i]);
  delete [] data; // xóa mảng hiện tại
  data = newData; // trỏ vào mảng mới
}
data[size++] = v;

//xóa phần tử
//...gọi dtor của data[size]...
size--;

ở đây dtor của T = int ko có nên pop_back() chỉ thực hiện có size–, nên giá trị int bị “xóa” vẫn còn y nguyên ở chỗ cũ

nếu T = string thì khác: con trỏ tới chuỗi thật sự sẽ được giải phóng khi gọi hàm hủy ~string() nên sẽ ko xài đc nữa

ví dụ:

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

struct mystring {
    size_t len = 0;
    char* data = nullptr;
    mystring(const char* s) : len{strlen(s)}, data{new char[len + 1]}
    { strcpy(data, s); }
    mystring(const mystring& s) : len{s.len}, data{new char[len + 1]}
    { strcpy(data, s.data); }
    ~mystring()
    { delete [] data; }
};

int main()
{
    {
        vector<mystring> p;
        p.push_back("hello world");
        p.push_back("hello john");
        p.push_back("hello michael");
        p.push_back("hello bob");
        p.push_back("hello alice");
        p.push_back("hello eve");
        p.pop_back();
        cout << p[5].data << endl;
    }
    {
        vector<string> p;
        p.push_back("hello world");
        p.push_back("hello john");
        p.push_back("hello michael");
        p.push_back("hello bob");
        p.push_back("hello 123 alice");
        p.push_back("hello 123456 eve");
        p.pop_back();
        p.pop_back();
        cout << p[4] << endl; //small string optimization (SSO) ON
        cout << p[5] << endl; //SSO OFF
    }
}

1 trong các output:

x+�
hello 123 alice
@.�3456 eve

với mystring ko có optimization gì hết, hàm hủy được gọi, p[5] sẽ in ra giá trị rác.
với string, tùy vào chuỗi ban đầu có SSO hay ko (<16 ký tự) thì mảng cấp phát động bị hủy dẫn tới in ra giá trị rác, hoặc mảng tĩnh chứa small string < 16 ký tự vẫn còn, vẫn in ra được giá trị cũ vì hàm hủy ko hủy mảng tĩnh này.

2 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?