9 Tính Năng Quan Trọng Trong C++11
1. Con trỏ NULL và nullptr
NULL được hiểu đơn giản là hằng số 0 ( #define NULL 0).
Con trỏ nullptr đã được thêm vào C++11 để khắc phục một vài bất cập ở NULL trong phiên bản trước. Kiểu của nullptr là std::nullptr_t.
Để hiểu rõ những bất cập đó là gì, chúng ta cùng xem một số ví dụ sau:
Ví dụ 1:
#include "stdafx.h"
#include
using namespace std;
void f(int)
{
cout << "f(int)" << endl;
}
void f(int* i)
{
cout << "f(int*)" << endl;
}
void main()
{
f(0);
f(nullptr);
}
Với đoạn chương trình trên, khi ta gọi hàm f(0) thì hàm f(int) sẽ được gọi, và khi gọi hàm f(NULL) thì hàm f(int) vẫn được gọi, nhưng khi gọi f(NULL) chúng ta lại mong muốn hàm f(int*) được gọi (vì NULL tượng trưng cho con trỏ NULL), đây là một bất cập của NULL.
Chúng ta xem tiếp ví dụ 2, để thấy rằng con trỏ nullptr đã cập nhật bất cập này của NULL như thế nào.
#include "stdafx.h"
#include
using namespace std;
void f(int)
{
cout << "f(int)" << endl;
}
void f(int* i)
{
cout << "f(int*)" << endl;
}
void main()
{
f(0);
f(nullptr);
}
Với đoạn chương trình trên
-
Khi ta gọi f(0) thì hàm f(int) được gọi.
-
Khi ta gọi f(nullptr) thì hàm f(int*) được gọi.
2. Biến 'auto'
Biến 'auto' đã có từ phiên bản C++ trước. Nhưng biến auto trong phiên bản C++11 có ý nghĩa khác với biến auto ở phiên bản trước. Chúng ta cùng xem 2 ví dụ sau đây:
Ví dụ 1: ý nghĩa của biến auto ở phiên bản C++ trước.
int i = 42; // đây là khai báo rút gọn của khai báo auto int i = 42;
long long l = 42LL; // đây là khai báo rút gọn của: auto long long l = 42LL;
void(*p)(int, float, char) = foo; // định nghĩa của foo: void foo(int, float, char)
vector::iterator it = v.begin() // v có kiểu là vector
Ở ví dụ trên, chúng ta thấy rằng tất cả các biến đều phải được khai báo với kiểu một cách tường minh. Với từ khóa 'auto' trong C++11, chúng ta sẽ viết code ngắn gọn hơn và không phải viết tường minh. Hãy xem ví dụ 2 dưới đây:
auto i = 42; // i có kiểu là int
auto l = 42LL; // l có kiểu long long
auto p = &foo; // p là con trỏ hàm trỏ đến hàm void foo
auto it = v.begin() // v có kiểu là vector
auto var; // ERROR: không thể khai báo biến auto mà không có giá trị mặc định, bởi vì giá trị mặc định là nguồn dữ liệu để xác định kiểu của biến var
Với auto ở phiên bản C++11, chúng ta sẽ code một cách linh động hơn và ít phải sửa lỗi hơn khi có sự thay đổi về kiểu của một biến.
Giả sử chúng ta sửa kiểu của biến v ở đoạn code trên, thì tại dòng 4 chúng ta không phải sửa kiểu của biến it, mà biến it sẽ tự động được cập nhật kiểu theo kiểu đã thay đổi ở v.
3. Từ khóa decltype
Bằng cách sử dụng từ khóa decltype, trình biên dịch(compiler) sẽ tìm ra kiểu dữ liệu của một đoạn mã khai báo.
Ví dụ 1
std::map coll;
decltype(coll) elem;
Với đoạn mã ví dụ trên, ta khai báo biến elem có kiểu của biến coll (std::map).
Ví dụ 2:
template
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
return t1 + t2;
}
Với đoạn mã ví dụ trên, kiểu trả về của hàm add sẽ là kiểu trả về của kết quả (t1 + t2), cụ thể:
-
t1 là int, t2 là int thì kiểu trả về của hàm add là int
-
t1 là int, t2 là float thì kiểu trả về của hàm add là float
-
t1 là float, t2 double là thì kiểu trả về của hàm add là double
-
...
4. Giá trị mặc định khi khởi tạo và các kiểu khởi tạo
Khi khai báo một biến thì biến sẽ có một giá trị khởi tạo được xác định hoặc không xác định tùy thuộc vào cách khai báo. Đối với C++11 chúng ta có thể sử dụng hai dấu nháy nhọn để khởi tạo biến an toàn tránh mất dữ liệu ngoài ý muốn.
Để hiểu rõ thêm, chúng ta cùng xem ví dụ bên dưới:
//Ví dụ 1
int a1[] = { 1, 2, 3.5 }; // OK, nhưng giá trị của a1[2] là 3 (thay vì 3.5) --> mất dữ liệu
int a2[] { 1, 2, 3.5 }; // Error, không thể khai báo số thực ở mảng số nguyên
int a3[] { 1, 2, 3 }; // OK
//Ví dụ 2:
int i; // i có giá trị mặc định là một số không xác định (số rác)
int j{}; // j có giá trị mặc định là 0
//Ví dụ 3
int x1(5.3); // OK, nhưng giá trị của x1 là 5 (mất dữ liệu), đây là cách khởi tạo thông thường của c++ phiên bản cũ
int x2 = 5.3; // OK, nhưng giá trị của x1 là 5 (mất dữ liệu), đây là cách khởi tạo thông thường của c++ phiên bản cũ
int x3{5.0}; // ERROR: không thể ép kiểu float qua int với cách khởi tạo ở C++11
int x4 ={5.3}; // ERROR: không thể ép kiểu float qua int với cách khởi tạo ở C++11
char c1{7}; // OK: mặc dù 7 là số int,nhưng 7 không bị mất dữ liệu (narrowing)
char c2{99999}; // ERROR: vì 99999 nằm ngoài miền giá trị của kiểu char
std::vector v1 { 1, 2, 4, 5}; // OK
std::vector v2 { 1, 2.3, 4, 5.6 }; // ERROR: vì 5.6 là kiểu double không phải kiểu int
5. Khai báo mới cho vòng lặp
C++11 ra mắt một dạng mới của vòng lặp for nhằm thu gọn vòng lặp và dễ dàng sử dụng hơn. Dạng này cũng tương tự như vòng lặp foreach ở các ngôn ngữ lập trình cấp cao như Java, C#,...
Cú pháp của dạng vòng lặp for này như sau:
for (decl : coll) {
Statement
}
Hãy xem những ví dụ dưới đây để hiểu hơn về vòng lặp for mới này:
//Ví dụ 1:
for (int i : {2, 3, 5, 7, 9, 13, 17, 19 }) {
std::cout << i << std::endl;
}
//Ví dụ 2 :
std::vector vec;
for (auto& elem : vec){
elem *= 3;
}
Trong cách duyệt mảng của thư viện std (std:vector, std:list,...) cũng được rút gọn, đơn giản và dễ hiểu hơn:
// Cách 1: sử dụng phương thức begin/end bên trong của mảng:
for (auto _pos = coll.begin(), _end = coll.end(); _pos != _end; ++_pos) {
decl = *_pos;
statement
}
// Cách 2: KHÔNG sử dụng phương thức begin/end bên trong của mảng:
for (auto _pos = begin(coll), _end = end(coll); _pos != _end; ++_pos) {
decl = *_pos;
statement
}
6. Từ khóa 'override' và 'final'
Từ khóa override
Cũng tương tự như phần ở trên, từ khóa override và final được thêm vào trong C++11 để khắc phục những bất cập trong lập trình hướng đối tượng (OOP).
Hãy cùng tham khảo 2 trường hợp bất cập trong C++ phiên bản cũ:
// Trường hợp 1
class B
{
public:
virtual void f(short)
{
std::cout << "B::f" << std::endl;
}
};
class D : public B
{
public:
virtual void f(int)
{
std::cout << "D::f" << std::endl;
}
};
// Trường hợp 2:
class B
{
public:
virtual void f(int) const
{
std::cout << "B::f" << std::endl;
}
};
class D : public B
{
public:
virtual void f(int)
{
std::cout << "D::f" << std::endl;
}
};
Mục đích của đoạn code trên là: viết class D kế thừa từ class B và viết lại hàm f (override f) ở class D, nhưng gặp một số nhược điểm sau:
-
Trường hợp 1: f ở B có tham số kiểu truyền vào là kiểu short, còn hàm f ở D có tham số truyền vào là kiểu int, vì vậy f ở class B không được viết lại ở class D (hay nói cách khác D::f và B::f là hai hàm tách biệt, không có liên quan đến nhau)
-
Trường hợp 2: chúng ta thấy f ở B và f ở D đều có cùng tham số truyền vào là int, nhưng f ở B là một hàm hằng (const function), còn f ở D không phải là hàm hằng, vì vậy f ở class B không được viết lại ở class D mặc dù 2 hàm f đều có chùng tham số
Chính vì những nhược điểm đó mà từ khóa override đã ra đời để giải quyết vấn đề trên. Từ khóa override ở sau một hàm là để chỉ ra rằng hàm này được viết lại từ hàm của lớp cha, nếu ở lớp cha không tồn tại hàm này thì trình biên dịch (compiler) sẽ báo lỗi (compile error).
Vì dụ về cách sử dụng từ khóa override:
class B
{
public:
virtual void f(short)
{
std::cout << "B::f" << std::endl;
}
};
class D : public B
{
public:
virtual void f(short) override
{
std::cout << "D::f" << std::endl;
}
};
Một lợi thế rất quan trọng nữa khi sử dụng từ khóa override là: xét ví dụ ở trên, nếu bạn hoặc một đồng nghiệp khác của bạn vô tình hoặc cố ý sửa tên hàm hoặc tham số đầu vào của hàm f ở class B thì lúc biên dịch sẽ bị lỗi, điều này chỉ ra cho chúng ta thấy nếu muốn sửa một hàm có tên là x ở lớp cha mà lớp con đã viết lại bằng từ khóa override thì phải sửa luôn cả hàm x ở lớp con.
Từ khóa final
Từ khóa final ở sau một hàm muốn chỉ ra rằng: tất cả class con kế thừa từ class này KHÔNG được phép viết lại hàm này. Nếu chúng ta cố tình viết lại hàm có từ khóa final thì lúc biên dịch sẽ bị lỗi.
Ví dụ về cách sử dụng từ khóa final:
class B
{
public:
virtual void f(int) final
{
std::cout << "B::f" << std::endl;
}
};
class D : public B
{
public:
virtual void f(int)
{
std::cout << "D::f" << std::endl;
}
};
Với ví dụ trên, chúng ta thấy rằng hàm f ở class B đã có từ khóa final, nhưng class D vẫn cố tình viết lại hàm f, lúc này khi biên dịch sẽ bị lỗi (compile error).
7. Lambdas expression
Những hàm vô danh được gọi là lambda (còn được gọi là những hàm không có tên).
Một cách cơ bản lambda expession có cú pháp như sau:
[Phạm vi sử dụng biến] (Tham số) -> kiểu-trả-về { Thân hàm }
-
Phạm vi sử dụng biến
-
[=] : cho phép sử dụng tất cả biến trong cùng phạm vi thông qua cách truyền tham số (pass by value)
-
[&]: cho phép sử dụng tất cả biến trong cùng phạm vi thông qua cách truyền tham trị (pass by reference)
-
[this]: cho phép sử dụng tất cả biến trong cùng phạm vi class
-
Bạn cũng có thể cho phép chỉ ra những biến cụ thể được phép sử dụng bằng cách:
[a, &b] (Tham Số) -> kiểu-trả-về { Thân hàm }-
Biến a được truyền vào theo dạng tham số
-
Biến b được truyền vào theo dạng tham trị
-
-
Bạn cũng có thể không cho phép sử dụng bất kì biến bên ngoài bằng cách: [] (Tham Số) -> kiểu-trả-về { Thân hàm }
-
Chúng ta cùng theo dõi một vài ví dụ dưới đây để hiểu thêm về cách sử dụng lambdra expression.
// Ví dụ 1:
[] {
std::cout << "hello lambda" << std::endl;
} (); // xuất ra "hello lambda"
// Ví dụ 2:
auto l = [] {
std::cout << "hello lambda" << std::endl;
};
l(); // xuất ra "hello lambda"
// Ví dụ 3:
auto l = [](const std::string& s) {
std::cout << s << std::endl;
};
l("hello lambda"); // xuất ra "hello lambda"
// Ví dụ 4: chỉ cho phép sử dụng 2 biến x và y (x: truyền tham số, y: truyền tham trị)
int x = 0;
int y = 42;
auto qqq = [x, &y] {
std::cout << "x: " << x << std::endl;
std::cout << "y: " << y << std::endl;
++y;
};
// Ví dụ 5: không sử dụng biến bên ngoài, không có tham số, kiểu trả về là double
[]() -> double {
return 42.0;
}
Con trỏ thông minh (Smart pointers)
Được hỗ trợ bởi thư viện
Bao gồm 3 loại con trỏ:
-
shared_ptr
-
weak_ptr
-
unique_ptr
8. Con trỏ thông minh ra đời để giải quyết một số bất cập trong việc quản lý bộ nhớ ở phiên bản C++ trước.
shared_ptr
Class shared_ptr được sử dụng khi một vùng nhớ được chia sẻ cho nhiều con trỏ (share resource).
Ví dụ:
std:: shared_ptr p1(new int(42));
std:: shared_ptr p2 = p1;
*p1 = 30;
cout << "p1: " << *p1 << endl;
cout << "p2: " << *p2 << endl;
Với ví dụ trên, ta thấy con trỏ p1 và p2 được lưu trữ ở bộ nhớ stack nhưng p1 và p2 cùng trỏ tới 1 vùng nhớ. Như chúng ta đã biết, một biến được lưu ở vùng nhớ stack sẽ tự động được hủy khi chương trình chạy ra khỏi phạm vi (scope) chứa biến đó. Điều đặc biệt đáng nói ở đây là vùng nhớ dùng chung của con trỏ p1 và con trỏ p2 sẽ bị hủy khi biến p1 và p2 bị hủy (vùng nhớ dùng chung của p1 và p2 sẽ bị hủy khi không còn con trỏ nào trỏ tới nó). Đây chính là lợi ích của shared_ptr mang lại.
Ngoài ra chúng ta cũng có thể tự định nghĩa một phương thức hủy cho shared_ptr. Ví dụ ta sẽ xuất ra thông báo "Deleted p" trước khi hủy con trỏ p.
shared_ptr pNico(new string("nico"), [](string* p) {
cout << "Deleted p" << endl;
});
Xử lý mảng và shared_ptr
-
Chú ý rằng: mặc định khi hủy vùng nhớ shared_ptr sẽ gọi delete (không phải delete[])
-
Vì vậy, nếu bạn sử dụng new[] để tạo một mảng đối tượng, bạn phải định nghĩa một cách thức hủy cho riêng bạn. Bạn có thể làm điều này bằng cách định nghĩa một phương thức hủy hoặc một con trỏ hàm hoặc lamdba gọi delete[] để hủy một con trỏ mảng như bình thường.
Ví dụ: định nghĩa một cách thức hủy bằng lamdba:
shared_ptr p(new MyClass[10], [](MyClass* p) {
delete[] p;
});
Chúng ta cũng có thể sử dụng công cụ hỗ trợ chính thức default_delete cho con trỏ unique_ptr (sẽ được giới thiệu ở bên dưới) để gọi delete[] giống như delete.
shared_ptr p(new MyClass[10], default_delete());
shared_ptr không có delete[] mặc định như unique_ptr.
std::unique_ptr p(new int[10]); //OK
std::shared_ptr p(new int[10]); //ERROR: compile lỗi
unique_ptr
Class unique_ptr được sử dụng vùng nhớ được khởi tạo ra và không chia sẻ cho bất kì con trỏ nào khác (có nghĩa là nó không có hàm khởi tạo sao chép - copy constructor) nhưng nó có thể chuyển vùng nhớ cho một con trỏ unique_ptr khác (gọi là khởi tạo di chuyển - move constructor).
Vậy lợi ích của con trỏ unique_ptr là gì, hãy xét ví dụ dưới đây xem có vấn đề gì không nhé:
// Giả sử chúng ta đã có ClassA hoàn chỉnh
void f()
{
ClassA* ptr = new ClassA; //khởi tạo một đối tượng một cách tường minh
// thực thi một vài tương tác lên con trỏ ptr
//....
delete ptr; // hủy vùng nhớ mà con trỏ ptr đang trỏ tới.
}
Với ví dụ trên, chúng ta thấy rằng việc hủy con trỏ ptr ở dòng thứ 6 ẩn nấp rủi ro gây ra lỗi chương trình, trong trường hợp trước khi hủy con trỏ ptr chúng ta đã cho một con trỏ khác ptr2 cùng trỏ tới vùng nhớ mà ptr đang trỏ, và chúng ta đã hủy vùng nhớ này thông qua con trỏ ptr2. Vì vậy khi hủy vùng nhớ dùng chung thông qua con trỏ ptr sẽ gây ra lỗi ngừng chương trình đột ngột (hay nhiều người còn gọi là bug crash). Ví dụ:
void f()
{
ClassA* ptr = new ClassA; //khởi tạo một đối tượng một cách tường minh
ClassA* ptr2 = ptr; // cho con trỏ ptr2 cùng trỏ tới vùng nhớ mà ptr đang trỏ
delete ptr2; // xóa vùng nhớ dùng chung thông qua con trỏ ptr2
delete ptr; // hủy vùng nhớ mà con trỏ ptr đang trỏ tới
}
Trong trường hợp này giải quyết như thế nào?
Giải pháp 1: sử dụng try ... catch
void f()
{
ClassA* ptr = new ClassA; //khởi tạo một đối tượng một cách tường minh
try {
// thực thi một vài tương tác lên con trỏ ptr
//....
delete ptr; // hủy vùng nhớ mà con trỏ ptr đang trỏ tới
}
catch (...) // ... có ý nghĩa cho bất cứ ngoại lệ nào
{
throw;
}
}
Giải pháp 2:
void f()
{
std::unique_ptr ptr (new ClassA); //khởi tạo một đối tượng một cách tường minh sử dụng unique_ptr
// thực thi một vài tương tác lên con trỏ ptr
//....
}
// con tỏ ptr sẽ tự động được hủy khi hàm ptr ra khỏi phạm vi của nó (hàm f kết thúc)
Một số ví dụ về cách sử dụng và khởi tạo con trỏ unique_ptr:
std::unique_ptr up = new int; // Lỗi, không thể khởi tạo con trỏ unique_ptr như một cách thông thường
std::unique_ptr up(new int); // OK
int* sp = up.release(); // sp sẽ trỏ tới vùng nhớ up đang trỏ tới, đồng thời up sẽ không trỏ tới nullptr
string* sp = new string("hello");
unique_ptr up1(sp); //OK
unique_ptr up2(sp); //ERROR: up1 và up2 không thể cùng trỏ tới cùng một vùng nhớ (không thể chia sẻ tài nguyên với nhau)
unique_ptr up3(std::move(up1)); //OK, chuyển vùng nhớ của up1 qua up3, và up1 sẽ trỏ tới nullptr
unique_ptr up4;
up4 = up1; //ERROR: không thể cho 2 con trỏ unique_ptr cùng trỏ tới 1 vùng nhớ.
unique_ptr ptr; //create a unique_ptr
ptr = new ClassA; // ERROR
ptr = unique_ptr(new ClassA); //OK, hủy đối tượng cũ và khởi tạo đối tượng mới
up = nullptr; // hủy đối tượng (hoặc bạn có thể gọi up.reset())
Xử lý unique_ptr với mảng
std::unique_ptr
Mặc định unique_ptr sẽ gọi delete để hủy vùng nhớ nó đang trỏ tới. Vì vậy: std::unique_ptr
Tương tự như shared_ptr bạn có thể tự tạo riêng cho unique_ptr cách thức hủy khi khởi tạo đối tượng như sau:
// Giả sử rằng chúng ta đã có sẵn ClassA
// Cách 1: khởi tạo con trỏ với 1 đối tượng
class ClassADeleter
{
public:
void operator () (ClassA* p) {
std::cout << "call delete for ClassA object" << std::endl;
delete p;
}
};
void main(int argc, _TCHAR* argv[])
{
std::unique_ptr up(new ClassA());
}
// Cách 2: sử dụng lamdba để hủy con trỏ khởi tạo với mảng
auto l = [] (ClassA* p) {
// Thao tác bất kì trước khi hủy con trỏ p
delete[] p;
};
std::unique_ptr> up (new ClassA[10], l);
weak_ptr
Con trỏ shared_ptr sẽ có một vài bất cập nếu chúng ta lạm dụng nó, ví dụ:
class Person
{
public:
string name;
shared_ptr mother;
shared_ptr father;
vector> kids;
Person(const string& n, shared_ptr m = nullptr, shared_ptr f = nullptr)
: name(n), mother(m), father(f)
{
}
~Person()
{
cout << "delete " << endl;
}
};
shared_ptr initFamily mom(new Person(name + "'s mon"));
shared_ptr dad(new Person(name + "'s dad"));
shared_ptr kid(new Person(name, mom, dad));
mom->kids.push_back(kid);
dad->kids.push_back(kid);
return kid;
}
void main()
{
shared_ptr p = initFamily("Nico");
}
Với ví dụ trên chúng ta thấy dad->kids, mom->kids trỏ tới kid và ngược lại kid->father trỏ tới dad và kid->mother trỏ tới mom, chúng ta gọi kiểu trỏ này là sự phụ thuộc vòng tròn (hay gọi là trỏ lặp - dependency cycles). Trong trường hợp này shared_ptr không giải quyết được, chính vì thế weak_ptr đã ra đời.
Con trỏ weak_ptr là con trỏ sẽ trỏ đến một đối tượng shared_ptr, nhưng số lượng tham chiếu đến vùng nhớ được quản lý bởi shared_ptr không tăng lên. weak_ptr được sử dụng để khử những con trỏ lặp như ví dụ trên, cụ thể như sau:
class Person
{
public:
string name;
shared_ptr mother;
shared_ptr father;
vector> kids; // sử dụng weak_ptr để khử con trỏ lặp
Person(const string& n, shared_ptr m = nullptr, shared_ptr f = nullptr)
: name(n), mother(m), father(f)
{
}
~Person()
{
cout << "delete " << endl;
}
};
shared_ptr initFamily mom(new Person(name + "'s mon"));
shared_ptr dad(new Person(name + "'s dad"));
shared_ptr kid(new Person(name, mom, dad));
mom->kids.push_back(kid);
dad->kids.push_back(kid);
return kid;
}
void main()
{
shared_ptr p = initFamily("Nico");
}
9. Move semantics
Move semantics hay còn gọi là move constructor, là tính năng mới vô cùng quan trọng, nó giúp cho tối ưu về tốc độ trong hướng đối tượng. Đây là một tính năng lớn và có rất nhiều điều cần thảo luận, dưới đây tôi chỉ đưa ra cho các bạn về khái niệm và một phần gợi mở để các bạn thấy được vì sao move semantics lại tối ưu được tốc độ.
Chúng ta hãy khảo sát ví dụ sau: giả sử rằng X là một class có một biến con trỏ đang trỏ tới một vùng nhớ, và biến ngày có tên là m_pResource. Theo cách thông thường thì khi chúng ta sẽ viết toán tử = cho class này như sau:
class X
{
public:
X()
{
cout << "Default contructor" << endl;
}
X(const X& lvalue) // copy constructor
{
cout << "Copy contructor" << endl;
// [...]
// Hủy tài nguyên (vùng nhớ) m_pResource đang trỏ tới
// Tạo một vùng nhớ mới có giá trị như rhs.m_pResource đang trỏ tới
// Gán vùng nhớ vừa tạo cho con trỏ m_pResource
// [...]
}
//[...]
};
X foo()
{
X x;
//[...]
return x;
}
void main()
{
X x1 = foo();
}
Giải thích hàm foo()
-
Hàm foo() tạo ra một đối tượng x thuộc class X, sau đó sẽ xử lý đối tượng này theo một mục đích nhất định ([...]), tiếp theo hàm foo trả về đối tượng x
-
Trong hàm main, chúng ta khai báo một biên x1 thuộc class X và được gán bằng kết quả trả về của hàm foo
Câu hỏi đặt ra ở đây là: các bạn có thấy vấn đề gì ở đoạn ví dụ này hay không?
Chúng ta để ý x1 không được gán trực tiếp bởi biến x trong hàm foo mà được gán bởi một đối tượng được sao chép ra từ x, sau đó x sẽ bị hủy khi hàm foo kết thúc.
Việc tạo ra một đối tượng sao chép từ x, rồi gán cho x1, sau đó hủy x là rất tốn chi phí, vậy có cách nào có thể gán trực tiếp từ x trong hàm foo qua x1 được hay không?
Câu trả lời là có, bằng cách sử dụng move semantics (hay còn gọi là move constructor), cụ thể như sau:
class X
{
public:
// default contructor
X()
{
cout << "Default contructor" << endl;
}
// copy constructor
X(const X& lvalue)
{
cout << "Copy contructor" << endl;
// [...]
// Hủy tài nguyên (vùng nhớ) m_pResource đang trỏ tới
// Tạo một vùng nhớ mới có giá trị như rhs.m_pResource đang trỏ tới
// Gán vùng nhớ vừa tạo cho con trỏ m_pResource
// [...]
}
// move constructor
X(X&& rvalue)
{
cout << "Move constructor" << endl;
// [...]
// Hoán đổi vùng nhớ của this->m_pResource và rhs.m_pResource
// [...]
}
};
X foo()
{
X x;
//[...]
return x;
}
void main()
{
X x1 = foo();
}
Hãy chạy cả 2 chương trình trên và tận hưởng thành quả.
Move sematics cũng như C++11 còn rất nhiều cái hay để bàn luận, nhưng ở bài viết này, tôi xin phép được dừng ở đây và sẽ viết thêm nhiều bài về C++11 nhằm chia sẻ với các bạn những cái hay của C++11 để thấy được lý do vì sao mà hầu hết các dự án hiện tại đều sử dụng C++11 thay vì C++98.
Tại sao bạn nên chọn Học LẬP TRÌNH HỆ THỐNG NHÚNG EMBEDDED SYSTEM ngay hôm nay???
✍️ Qua những nội dung dưới đây, bạn sẽ biết tại sao nên theo học & làm lập trình hệ thống nhúng? Những công việc nào trong hệ thống nhúng sẽ được thực hiện? Vậy hãy bắt đầu!!
✍️ Hệ thống nhúng là sự kết hợp của phần cứng và phần mềm. Mục đích của lập trình nhúng là kiểm soát một thiết bị, một quy trình hoặc một system/framework lớn hơn. Chúng hiện diện ở khắp mọi nơi xung quanh chúng ta.
✍️ Một số ví dụ về những thứ bao gồm hệ thống nhúng là những thứ điều khiển các đơn vị cơ bản của một chiếc xe, kiểm soát giao thông, chipset và lập trình trong hộp giải mã cho TV tiên tiến, máy điều hòa nhịp tim, chip trong thiết bị chuyển mạch viễn thông, thiết bị xung quanh và hệ thống điều khiển được nhúng trong lò phản ứng hạt nhân,...
✍️ Có sự phát triển theo cấp số nhân trong lĩnh vực lập trình hệ thống nhúng. Một trong những lý do quan trọng nhất cho điều này là nó là một phần chính của IoT. Giờ đây, các hệ thống ngày càng trở nên thông minh và phân tán, chúng cũng trở nên phức tạp hơn và phụ thuộc lẫn nhau. Điều này dẫn đến sự chuyển đổi trong các hệ thống nhúng từ thông thường sang thông minh. Điều này làm tăng vai trò của các kỹ sư lập trình nhúng (embedded developer).
👉👉 Công việc trong lĩnh vực lập trình hệ thống nhúng là gì?
🍁 Kỹ sư lập trình nhúng, nhưng không tương tự như kỹ sư phần mềm, họ cần hiểu biết sâu sắc về phần cứng mà nó chạy trên đó.
Kỹ sư lập trình nhúng biết sơ đồ của phần cứng và cách các biểu dữ liệu chip liên quan đến mã được viết cho phần cứng.
🍁 Các kỹ sư lập trình nhúng chịu trách nhiệm thiết kế, phát triển, tối ưu hóa và triển khai phần mềm được lập trình vào các thiết bị được xây dựng xung quanh bộ vi xử lý.
👉👉 Cơ hội nghề nghiệp cho các lập trình viên Nhúng?
🍁 Theo nghiên cứu, một trong những kỹ năng hàng đầu trong những năm gần đây là Internet Of Things(IoT), Machine Learning, Artificial Intelligence (AI) và đây là những lĩnh vực cốt lõi trong lập trình nhúng, khiến nó trở thành một trong những công việc được trả lương cao nhất.
🍁 Các kỹ sư lập trình nhúng hiện đang có nhu cầu cao, làm tăng công việc trong các hệ thống nhúng.
Điều đó có nghĩa là bạn có thể mong đợi một mức lương hợp lý hơn. Theo nghiên cứu, mức lương trung bình hàng năm cho một kỹ sư lập trình nhúng ở Hoa Kỳ là khoảng 83.000 USD. Các thuật ngữ được sử dụng phổ biến nhất để mô tả các kỹ sư nhúng:
🏅 Kỹ sư phần mềm (Firmware engineer)
🏅 Kỹ sư người máy (Robotics engineer)
🏅 Kỹ sư phần mềm nhúng (Embedded firmware engineer)
🏅 Kỹ sư hệ thống (Systems engineer)
👉👉 Việc làm tự do (Freelance Jobs)?
🍁 Nghề làm việc tự do đang gia tăng, với sự gia tăng của các sản phẩm như tủ lạnh và hệ thống nhà thông minh và các thiết bị được kết nối sử dụng nhiều phần mềm hơn, nó cũng làm gia tăng nhu cầu công việc về lập trình hệ thống nhúng.
👉👉 Vậy những ai nên tham gia khóa đào tạo này?
1️⃣ - Tất cả những ai đang tìm hiểu về lập trình Nhúng & muốn nắm được nhiều chuyên môn về phát triển các dự án Nhúng để tham gia vào dự án tại Doanh nghiệp.
2️⃣ - Những lập trình viên là newbie hoặc đang tự học nghề lập trình Nhúng (Embedded) nhưng mãi nhưng chưa thành công.
3️⃣ - Các nhà quản lý kinh doanh trong lĩnh vực hệ thống Nhúng (Embedded System) muốn hiểu rõ hơn về qui trình phát triển dự án lập trình hệ thống Nhúng, cách để tạo ra các sản phẩm để hiệu quả hơn trong công tác điều hành quản lý dự án.
4️⃣ - Các kiểm thử viên trong lĩnh vực Nhúng muốn nâng cao hơn sự hiểu biết của mình.
5️⃣ - Hoặc đơn giản nếu bạn chỉ muốn tham gia khám phá nghề "lập trình Nhúng" để từ đó tìm kiếm giải pháp cho ý tưởng của mình.
👉👉 Lời cam kết của khóa đào tạo nhân sự lập trình Nhúng?
1️⃣ - Đây là khóa đào tạo đầy đủ và chi tiết nhất về lập trình Nhúng từ trước đến nay.
2️⃣ - Các bài thực hành trong khóa đào tạo là các "Case Study" rất thực tế mà Chuyên gia IMIC đã dành nhiều tâm huyết biên soạn và đã đưa vào khóa đào tạo cho chính các Học viên của mình.
3️⃣ - Tất cả các phần trong khóa đào tạo được diễn đạt một cách trực quan nhất, dễ hiểu nhất, bạn được tự tay thực hiện các thử nghiệm trên thiết bị để thỏa mãn niềm đam mê của mình với lập trình Nhúng.
4️⃣ - Cam kết hỗ trợ học viên sau khóa đào tạo qua: Group Zalo, Facebook, Website, Email & Hotline.
⚠️ Đặc biệt! Cam kết hỗ trợ giới thiệu nhân sự sau Tốt nghiệp sang một số Doanh nghiệp là đối tác Tuyển dụng nhân sự của IMIC (với điều kiện bạn cần nghiêm túc & nỗ lực học tập để đạt kết quả tốt nhất).
Bạn đang muốn tìm kiếm 1 công việc với mức thu nhập cao.
✅ Hoặc là bạn đang muốn chuyển đổi công việc mà chưa biết theo học ngành nghề gì cho tốt.
✅ Giới thiệu với bạn Chương trình đào tạo nhân sự dài hạn trong 12 tháng với những điều đặc biệt mà chỉ có tại IMIC và đây cũng chính là sự lựa chọn phù hợp nhất dành cho bạn:
👉 Thứ nhất: Học viên được đào tạo bài bản kỹ năng, kiến thức chuyên môn lý thuyết, thực hành, thực chiến nhiều dự án và chia sẻ những kinh nghiệm thực tế từ Chuyên gia có nhiều năm kinh nghiệm dự án cũng như tâm huyết truyền nghề.
👉 Thứ hai: Được ký hợp đồng cam kết chất lượng đào tạo cũng như mức lương sau tốt nghiệp và đi làm tại các đối tác tuyển dụng của IMIC. Trả lại học phí nếu không đúng những gì đã ký kết.
👉 Thứ ba: Cam kết hỗ trợ giới thiệu công việc sang đối tác tuyển dụng trong vòng 10 năm liên tục.
👉 Thứ tư: Được hỗ trợ tài chính với mức lãi suất 0 đồng qua ngân hàng VIB Bank.
👉 Có 4 Chương trình đào tạo nhân sự dài hạn dành cho bạn lựa chọn theo học. Gồm có:
1) Data Scientist full-stack
2) Embedded System & IoT development full-stack
3) Game development full-stack
4) Web development full-stack
✅ Cảm ơn bạn đã dành thời gian lắng nghe những chia sẻ của mình. Và tuyệt vời hơn nữa nếu IMIC được góp phần vào sự thành công của bạn.
✅ Hãy liên hệ ngay với Phòng tư vấn tuyển sinh để được hỗ trợ về thủ tục nhập học.
✅ Chúc bạn luôn có nhiều sức khỏe và thành công!