C++11 là một phiên bản cải tiến và nâng cấp từ C++98 (hay các bạn vẫn gọi là C++), với những tính năng mới tối ưu hơn, dễ sử dụng hơn, dễ quản lý bộ nhớ hơn và khắc phục được các nhược điểm của phiên bản C++98. Những cải tiến quan trọng đó bao gồm các tính năng thú vị sau đây.
1. Khai báo biến với từ khoá auto
Bình thường, trong C++98, khi khai báo biến buộc phải chỉ rõ kiểu dữ liệu của biến, chẳng hạn:
1 2 3 |
int i; long long l; foo* p; |
Trong C++11, ta có thể yêu cầu chương trình dịch tự xác định kiểu dữ liệu của biến thông qua giá trị ban đầu của biến bằng cách khai báo biến với từ khoá auto, đoạn mã trên có thể viết lại như sau:
1 2 3 |
auto i = 42; // i là biến kiểu int auto l = 42LL; // l là biến thuộc kiểu long long auto p = new foo(); // p là biến con trỏ foo* |
Khai báo biến với từ khoá auto giúp tiết kiệm được kha khá mã nguồn nhưng vẫn dễ đọc:
1 2 3 4 |
std::map<std::string, std::vector<int>> map; for(auto it = begin(map); it != end(map); ++it) { //it là biến thuộc kiểu std::map<std::string, std::vector<int>>::interator } |
Lưu ý là C++11 hiểu được cả >>
Khi sử dụng auto cho kết quả trả lại của hàm, phải chỉ rõ kiểu kết quả trả lại của hàm:
1 2 3 4 5 6 7 |
template <typename T1, typename T2> auto compose(T1 t1, T2 t2) -> decltype(t1 + t2) { //decltype(t1 + t2) = kiểu dữ liệu của biểu thức t1 + t2 return t1+t2; } // Sử dụng hàm compose: auto v = compose(2, 3.14); // Kiểu của biến v là double auto u = compose(2, 3); // Kiểu của biến u là int |
2. Con trỏ std::nullptr
Trong C++98 giá trị vô nghĩa của biến con trỏ là NULL, NULL là số nguyên: 0. Sự đồng nhất NULL và 0 có điểm bất tiện. Trong ví dụ dưới đây, khi gọi foo(NULL) thì hàm func nào sẽ được gọi?
1 2 3 4 5 6 |
//Định nghĩa chồng tên hai hàm foo void foo(int* x) {} void foo(int x) {} //Sử dụng: foo(NULL); |
Câu trả lời là foo(int) chứ không phải là foo(int*) vì NULL là một số nguyên.
C++11 định nghĩa rõ ràng giá trị vô nghĩa của con trỏ là nullptr. Trong ví dụ trên, nếu gọi foo(nullptr) thì foo(int*) sẽ được gọi.
nullptr được sử dụng tương tự như NULL, ngoại trừ một điểm: nullptr thuộc kiểu con trỏ. Giá trị NULL trong C++11 vẫn được sử dụng để tương thích ngược với C++98 các phiên bản trước. Ví dụ sau minh hoạ cách sử dụng nullptr:
1 2 3 4 5 6 |
int* p1 = NULL; int* p2 = nullptr; if(p1 == p2) { // Phép so sánh NULL == nullptr có giá trị true } bool f = nullptr; // nullptr có thể tự động chuyển kiểu bool, f = false int i = nullptr; // Lỗi: biến int i không thể nhận giá trị con trỏ |
3. Cấu trúc lặp for duyệt qua các phần tử của một tập hợp (Ranged-base for loop)
C++11 bổ sung cấu trúc lặp for dùng để duyệt qua các phần tử của một tập hợp, tương tự như thuật toán for_each trong thư viện algorithms của C++98 nhưng thuận tiện hơn nhiều:
1 2 3 4 |
int a[] = {1,2,3,4,5}; for (auto& x: a) x--; // Duyệt qua các phần tử của a, mỗi phần tử giảm đi 1 đơn vị for (auto x: a) cout<< x << ' '; // duyệt qua các phần tử của a, ghi giá trị ra stdout |
Tương tự:
1 2 3 4 5 6 7 8 9 10 11 |
std::map<std::string, std::vector<int>> map; std::vector<int> v = {1, 2, 3}; map["one"] = v; for(const auto& kvp : map) { std::cout << kvp.first << std::endl; for(auto v : kvp.second) { std::cout << v << std::endl; } } |
4. Sử dụng chỉ định override và final cho các hàm ảo
Trong chương trình sau, ở định nghĩa lớp B, người lập trình muốn định nghĩa lại (override) hàm ảo Show() của lớp cha: A:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> using namespace std; class A { public: virtual void Show() { } }; class B: public A { public: virtual void Show() const { } }; int main() { B b; A *p = &b; p->Show(); return 0; } |
Nếu chương trình đúng thì p->Show() phải gọi đến B::Show(), nhưng chương trình trên gọi đến A::Show(). Nguyên nhân do hàm Show trong lớp B khai báo khác với nguyên mẫu hàm Show trong lớp A (có/không có const). Theo đó, Show trong lớp B chỉ được coi là định nghĩa chồng tên (overload) và là hàm khác với Show của lớp A. Đây là một lỗi trong lập trình. Để đạt mục đích định nghĩa lại, phải sửa: bỏ const đi.
Nhằm tránh những lỗi như trên và để rõ ràng hơn trong mã nguồn, C++11 bổ sung chỉ định override để chỉ rõ hàm ảo là hàm định nghĩa lại.
Trong ví dụ sau, nếu có chỉ định override cho hàm Show, chương trình dịch có thể bắt lỗi được ngay:
1 2 3 4 5 6 7 8 9 10 11 |
class A { public: virtual void Show() { } }; class B: public A { public: void Show() const override { // Lỗi: Không có hàm Show (có const) nào để định nghĩa lại } }; |
C++11 cũng bổ sung thêm chỉ định final cho hàm ảo, khi đó hàm ảo này ở các lớp con sẽ không được phép định nghĩa lại. Ví dụ:
1 2 3 4 5 6 7 8 9 10 11 |
class A { public: virtual void Show() final { } }; class B: public A { public: void Show() { //Lỗi: Show không được phép định nghĩa lại từ lớp A, do Show trong lớp A có chỉ thị final } }; |
5. Kiểu dữ liệu liệt kê enum class
Trong C++98, cùng một phạm vi mã nguồn (scope), các giá trị trong 2 kiểu dữ liệu liệt kê (enum) phải khác nhau, chẳng hạn:
1 2 |
enum E1 {a, b, c}; enum E2 {c, d, f}; // Lỗi: Trùng giá trị c với E1. |
C++11 khắc phục hạn chế nói trên bằng kiểu liệt kê enum class. Enum class hỗ trợ định kiểu mạnh: chỉ rõ giá trị liệt kê thuộc kiểu liệt kê nào, do đó tránh được xung đột tên:
1 2 3 4 5 |
enum class E1 {a, b, c}; enum class E2 {c, d, f}; //Sử dụng: E1 e1 = E1::c; // Giá trị c của E1 E2 e2 = E2::c; // Giá trị c của E2 |
6. Con trỏ thông minh (smart pointers)
C++11 định nghĩa một số kiểu dữ liệu “con trỏ thông minh” trong thư viện memory nhằm quản lý bộ nhớ cấp phát động tốt hơn so với việc sử dụng kiểu dữ liệu con trỏ truyền thống.
Có ba loại con trỏ thông minh:
– shared_ptr: Vùng nhớ do một con trỏ shared_ptr trỏ đến có thể cùng được trỏ bởi nhiều con trỏ shared_ptr khác. Vùng nhớ do con trỏ shared_ptr trỏ đến được quản lý theo cơ chế đếm tham chiếu (đếm số con trỏ trỏ tới). Khi số tham chiếu bằng 0, vùng nhớ được giải phóng tự động.
– weak_ptr: Con trỏ weak_ptr trỏ tới vùng nhớ trỏ bởi shared_ptr nhưng không làm tăng số tham chiếu tới vùng nhớ, do đó không ảnh hưởng đến vòng đời của vùng nhớ.
– unique_ptr: Vùng nhớ do con trỏ unique_ptr trỏ tới không được đồng thời trỏ bởi các con trỏ khác. Vùng nhớ trỏ bởi con trỏ unique_ptr cũng được tự động giải phóng khi không còn con trỏ nào trỏ tới.
Sử dụng shared_ptr và weak_ptr:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
shared_ptr<int> p (new int(10)); // Con trỏ shared_ptr trỏ đến vùng nhớ kích thước int, giá trị 10 cout <<p.use_count() <<endl; // Số tham chiếu p.use_count() = 1 cout <<*p <<endl; // Giá trị int tại vùng nhớ = 10, nếu *p là một đối tượng (object), có thể sử dụng toán tử -> auto q = p; cout <<p.use_count() <<endl; // Số tham chiếu là 2 cout <<*q <<endl; // Cùng giá trị với *p weak_ptr<int> w = p; // Con trỏ weak_ptr trỏ đến vùng nhớ trỏ bởi p cout <<p.use_count() <<endl; // Số tham chiếu là 2, không thay đổi cout <<*w.lock() <<endl; // w.lock() = p q = nullptr; cout <<p.use_count() <<endl; // Số tham chiếu là 1 if (p) cout <<*p <<endl; // (bool)p = true p = nullptr; // Số tham chiếu là 0, vùng nhớ được giải phóng tự động. if (w.expired()) cout <<"Expired" <<endl; // w.expired() khi đó có giá trị true |
Sử dụng hàm make_shared thay cho new cấp phát bộ nhớ cho con trỏ shared_ptr:
1 2 3 4 5 6 7 8 9 10 |
class A { public: A(int x){} }; void foo(shared_ptr<A> p, int x) {} int bar() {} // Sử dụng: auto p( make_shared<A>(10) ); // Thay thế cho shared_ptr<A> p (new A(10) ); foo( make_shared<A>(10), bar() ); // Thay thế cho foo( new A(10), bar() ); |
Trong ví dụ trên make_shared hiệu quả hơn do cấp phát một vùng nhớ cho cả p và đối tượng thuộc lớp A, trong khi new phải dùng đến 2 lần cấp phát. Ở lệnh gọi foo, khi bar() gây ra lỗi make_shared(10) vẫn đảm bảo giải phóng được vùng nhớ không gây rò rỉ bộ nhớ như khi sử dụng new A(10).
Sử dụng weak_ptr giải quyết vấn đề tham chiếu vòng của shared_ptr:
1 2 3 4 5 6 7 8 9 10 |
class Node { public: shared_ptr<Node> parent, child; }; void foo() { auto root( make_shared<Node>() ); root->child = make_shared<Node>(); root->child->parent = root; // Tham chiếu vòng, root.use_count() = 2 } |
Khi thoát khỏi hàm foo, biến cục bộ root bị huỷ nhưng số tham chiếu đến vùng nhớ do root trỏ đến vẫn còn lại 1, vì vậy vùng nhớ không được giải phóng.
Để khắc phục, có thể “bẻ gẫy” tham chiếu vòng bằng weak_ptr như sau:
1 2 3 4 5 6 7 8 9 10 11 |
class Node { public: weak_ptr<Node> parent; shared_ptr<Node> child; }; void foo() { auto root( make_shared<Node>() ); root->child = make_shared<Node>(); root->child->parent = root; // root.use_count() = 1 } |
Sử dụng con trỏ unique_ptr:
1 2 3 4 |
unique_ptr<int> u ( new int(10) ); // Con trỏ u trỏ đến vùng nhớ int, giá trị int = 10 (có thể thay bằng make_unique) auto v = move(u); // Chuyển vùng nhớ trỏ bởi v cho u (xem thêm phần 10). if (u) cout <<*u; // u không còn trỏ đến vùng nhớ trước đó if (v) cout <<*v; // vùng nhớ đã được chuyển cho v |
7. Hàm lambda
Ở ví dụ sau, cần phải định nghĩa 2 hàm cmp và print để phục vụ cho thao tác sort và for_each:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> #include <vector> #include <algorithm> using namespace std; bool cmp(int a, int b) { return a > b; } void print(int x) { cout <<x <<' '; } int main() { vector<int> v = {3, 2, 5, 6, 9, 1}; sort(v.begin(), v.end(), cmp); for_each(v.begin(), v.end(), print); return 0; } |
C++11 cung cấp cú pháp viết các hàm lamda, trong ví dụ trên, thay vì phải định nghĩa cmp và print chỉ cần sử dụng hàm lamda như sau:
1 2 |
sort( v.begin(), v.end(), [](int a, int b){return a > b;} ); for_each( v.begin(), v.end(), [](int x){cout <<x <<' ';} ); |
Sử dụng các biến bên ngoài trong hàm lambda:
1 2 3 4 |
vector<int> v = {3, 2, 5, 6, 9, 1}; int c1 = 0, c2 = 0, k = 5; for_each( v.begin(), v.end(), [&c1, &c2, k](int x){if (x > k) c1++; else if (x < k) c2++; } ); cout <<c1 <<' ' <<c2; // c1 = 2, c2 = 3 |
Hàm lambda không có tên, trong trường hợp muốn tái sử dụng một hàm lamda đã viết, cần lưu trữ hàm trong biến thuộc kiểu std::function
1 2 3 4 5 6 7 8 9 |
function<bool(int)> c = [](int x) {return x >= 0;}; // hoặc ngắn gọn hơn: auto c = ... function<int(int)> f = [&f](int n) {return n < 2 ? 1 : n*f(n-1);}; // Hàm lamda đệ quy, trường hợp này kiểu trả lại của hàm không xác định nên không sử dụng auto được. // Sử dụng: int x = 10; if (c(x)) cout <<"factorial of " <<x <<" is " << f(x); else cout << "not available"; |
8. Các hàm std::begin và std::end
C++11 cung cấp 2 hàm std::begin và std::end tự do (không phải hàm thành phần, member function, của một đối lớp nào). begin(T) và end(T) trả lại giá trị tương ứng của T.begin() và T.end() hoặc con trỏ tới phần tử đầu và sau phần tử cuối đối với một mảng tĩnh (array[]):
1 2 3 4 5 6 7 8 |
int a[] = {1, 2, 3, 4, 5}; vector<int> v = {1, 2, 3, 4, 5}; for (auto i = begin(a); i != end(a); i++) cout <<*i <<' '; cout <<endl; for (auto i = begin(v); i != end(v); i++) cout <<*i <<' '; cout <<endl; |
Sử dụng begin, end làm tăng tính tổng quát (generic) của chương trình
1 2 3 4 5 6 7 8 9 10 11 |
template <class T> void print(const T& a) { for (auto i = begin(a); i != end(a); i++) cout <<*i <<' '; cout <<endl; } // Sử dụng: int a[] = {1, 2, 3, 4, 5}; vector<int> v = {1, 2, 3, 4, 5}; print(a); print(v); |
9. static_assert và thư viện type_traits
Sử dụng static_assert để kiểm tra điều kiện tham số của một định nghĩa template trong thời gian dịch (compile-time):
1 2 3 4 5 6 7 8 9 10 11 |
template<class T, int n> class A { static_assert(n > 0, "n must be greater than 0"); T a[n]; }; int main() { A<int, 10> a; A<int, 0> b; // Lỗi: "n must be greater than 0" return 0; } |
Kết hợp static_assert và thư viện type_traits trong kiểm tra kiểu dữ liệu trong thời gian dịch:
1 2 3 4 5 6 7 8 9 10 11 |
template <typename T1, typename T2> auto add(T1 t1, T2 t2) -> decltype(t1 + t2) { static_assert(std::is_integral<T1>::value, "Type T1 must be integral"); static_assert(std::is_integral<T2>::value, "Type T2 must be integral"); return t1 + t2; } // Sử dụng: std::cout << add(1, 3.14) << std::endl; // Lỗi std::cout << add("one", 2) << std::endl; // Lỗi |
Hy vọng bài chia sẻ này sẽ giúp ích cho các bạn!