C++ đã trở thành lựa chọn hàng đầu cho nhiều lĩnh vực khác nhau, từ phát triển phần mềm hệ thống cho đến trò chơi điện tử và ứng dụng máy tính. Trong bài viết này, chúng ta sẽ tìm hiểu sâu hơn về ngôn ngữ lập trình C++, từ cú pháp cơ bản đến các tính năng nâng cao, và tìm hiểu tại sao nó vẫn giữ được sức hút mãnh liệt sau hơn 40 năm ra đời.
1. C++ là gì?
Ngôn ngữ lập trình C++ là một ngôn ngữ lập trình máy tính được phát triển từ ngôn ngữ C ban đầu vào những năm 1980 bởi Bjarne Stroustrup tại Bell Labs. C++ là một ngôn ngữ lập trình đa năng, hỗ trợ cả lập trình hướng đối tượng (OOP) và lập trình hướng thủ tục. Nó được sử dụng rộng rãi trong phát triển ứng dụng máy tính, game, hệ điều hành, và nhiều lĩnh vực công nghiệp khác.
C++ được kết hợp nhiều tính năng từ ngôn ngữ C, nhưng cũng bổ sung thêm các tính năng mới như lớp (class) và đa kế thừa (inheritance) để hỗ trợ lập trình hướng đối tượng. Nó cung cấp cả các thư viện tiêu chuẩn mạnh mẽ cho việc xử lý chuỗi, tệp tin, đồ họa, và nhiều tác vụ khác.
C++ là một trong những ngôn ngữ lập trình phổ biến và mạnh mẽ, và nó thường được sử dụng trong phát triển phần mềm yêu cầu hiệu suất cao và kiểm soát gần gũi với tài nguyên hệ thống.
Để hiểu được C++ một cách toàn diện nhất, bạn nên nắm được kiến thức về ngôn ngữ C cơ bản trước khi tìm hiểu về C++. Ở 200Lab đã có bài viết giới thiệu về C, các bạn có thể tham khảo thêm nhé.
2. Ứng dụng của ngôn ngữ lập trình C++
Ngôn ngữ lập trình C++ có rất nhiều ứng dụng và được sử dụng rộng rãi trong nhiều lĩnh vực khác nhau. Dưới đây là một số ví dụ về ứng dụng của C++:
- Phát triển phần mềm hệ thống: C++ thường được sử dụng để phát triển các ứng dụng hệ thống, chẳng hạn như hệ điều hành (Windows, macOS, và Linux), trình điều khiển thiết bị như trình điều khiển đồ họa (NVIDIA và AMD) và trình điều khiển mạng (card mạng Ethernet hoặc các thiết bị Wi-Fi).
- Phát triển ứng dụng máy tính: C++ được sử dụng để xây dựng các ứng dụng máy tính chuyên nghiệp như các phần mềm chỉnh sửa hình ảnh (ví dụ: Adobe Photoshop), phần mềm 3D (ví dụ: Autodesk Maya), và các ứng dụng về đồ họa và video.
- Phát triển game: C++ là một trong những ngôn ngữ phổ biến trong việc phát triển trò chơi máy tính. Nhiều game nổi tiếng như League of Legends, Minecraft, Counter-Strike đều được viết bằng C++ để đảm bảo hiệu suất cao và kiểm soát tài nguyên trên máy tính tốt hơn.
- Ứng dụng tài chính: C++ được sử dụng trong việc phát triển các ứng dụng tài chính như hệ thống giao dịch tài chính (thị trường chứng khoán, thị trường ngoại hối (forex), và thị trường tương lai (futures)) và phân tích dữ liệu tài chính. Với sự hỗ trợ mạnh mẽ của các thư viện như QuantLib, Boost C++ Libraries,... Việc phát triển các ứng dụng tài chính sử dụng C++ đang là sự lựa chọn của rất nhiều doanh nghiệp.
- Ứng dụng thời gian thực: C++ thích hợp cho phát triển ứng dụng thời gian thực như hệ thống điều khiển và robot tự động hóa. Một số hệ thống điều khiển được phát triển bằng C++ có thể kể đến như hệ thống ô tô tự lái (Tesla), dây chuyền sản xuất công nghiệp ở các nhà máy, bộ định tuyến mạng. Robot tự động hoá sử dụng C++ để phát triển được sử dụng trong các nhà máy công nghiệp như Robot Palletizing dùng để bốc xếp hàng hoá, Robot Pick and Place dùng để gắp và sắp xếp sản phẩm như bánh mì, gia vị,...
- Phát triển phần mềm nhúng: C++ cũng được sử dụng trong việc phát triển phần mềm nhúng cho các thiết bị như smartphone (iPhone, Samsung), điều hoà (Daikin, Panasonic,...), máy giặt (Electrolux, Hitachi) và các thiết bị IoT như August Smart Lock (khoá thông minh chuông cửa), Camera theo dõi chuyển động trong nhà, bộ điều khiển giọng nói Google Home,...
- Phát triển ứng dụng đám mây: C++ được sử dụng trong việc phát triển các phần mềm đám mây và dịch vụ web, đặc biệt trong các tình huống đòi hỏi hiệu suất cao và bảo mật.
3. Tại sao nên học ngôn ngữ C++?
Học ngôn ngữ lập trình C++ có nhiều lợi ích và lý do, bao gồm:
- Hiệu suất cao: C++ là một trong những ngôn ngữ lập trình nhanh nhất và hiệu quả về mặt tài nguyên. Điều này rất quan trọng khi bạn cần xây dựng các ứng dụng yêu cầu hiệu suất cao hoặc phải xử lý dữ liệu lớn.
- Kiểm soát tài nguyên: C++ cho phép bạn kiểm soát trực tiếp tài nguyên hệ thống như bộ nhớ và các tài nguyên khác. Điều này quan trọng trong các ứng dụng đòi hỏi quản lý tài nguyên chặt chẽ như hệ thống nhúng và trình điều khiển thiết bị.
- Lập trình hướng đối tượng: C++ hỗ trợ lập trình hướng đối tượng, cách lập trình mạnh mẽ và phổ biến. Điều này giúp bạn tổ chức mã nguồn một cách rõ ràng và dễ bảo trì.
- Khả năng tái sử dụng mã nguồn: C++ cho phép bạn viết mã có thể tái sử dụng dễ dàng, giúp tiết kiệm thời gian và công sức khi phát triển các ứng dụng phức tạp.
- Được sử dụng trong nhiều lĩnh vực: C++ được sử dụng rộng rãi trong nhiều lĩnh vực, bao gồm phát triển game, phần mềm hệ thống, phát triển ứng dụng máy tính, tài chính, và nhiều lĩnh vực công nghiệp khác.
- Kết hợp với các ngôn ngữ khác: C++ có khả năng kết hợp với các ngôn ngữ khác như C, Python, và Java, cho phép bạn tận dụng các tính năng của C++ trong các dự án đa ngôn ngữ.
- Khả năng học các ngôn ngữ khác dễ dàng: Học C++ giúp bạn hiểu các khái niệm cơ bản trong lập trình, điều này làm cho việc học các ngôn ngữ khác dễ dàng hơn.
- Mở rộng cơ hội nghề nghiệp: Có kiến thức về C++ sẽ giúp bạn có thêm nhiều cơ hội trên thị trường việc làm. Hiện tại ở Việt Nam có rất nhiều công ty đang tuyển dụng các vị trí như Embedded Software Engineer yêu cầu cần phải nắm chắc ngôn ngữ C++.
4. Ngôn ngữ C++ hoạt động như thế nào?
Cách hoạt động của C++ gần như tương đồng với ngôn ngữ C. Tuy nhiên, C++ có một số đặc điểm riêng và mở rộng hơn so với C, quá trình biên dịch trong C++ cần trải qua 3 bước: tiền xử lý (pre-processing), biên dịch (compiling) và liên kết (linking), sau đó chúng ta chạy file thực thi thu được để xuất kết quả ra màn hình.
4.1. Pre-processing (Tiền xử lý)
Quá trình tiền xử lý trong C++ là giai đoạn ban đầu của quá trình biên dịch, trong đó mã nguồn C++ được xử lý bởi bộ tiền xử lý. Bộ tiền xử lý là một công cụ xử lý các chỉ thị đánh dấu bằng dấu thăng (#), được gọi là chỉ thị tiền xử lý.
Nhiệm vụ chính của bộ tiền xử lý là chuẩn bị mã nguồn cho giai đoạn biên dịch chính, bao gồm nhiệm vụ như bao gồm tệp tiêu đề, định nghĩa giá trị macro, biên dịch có điều kiện và loại bỏ bình luận. Đầu ra của quá trình tiền xử lý là một tệp mã nguồn đã sửa đổi, sau đó được truyền đến trình biên dịch để xử lý tiếp. Một số chỉ thị tiền xử lý thường xuất hiện:
#include
: Thêm thư viện vào mã nguồn.
#include <iostream>
#define
: Định nghĩa giá trị macro.
#define PI 3.14159265
#include
: Thêm nội dung tệp mã nguồn khác vào tệp hiện tại.
#include "myheader.h"
Tiền xử lý xử lý các chỉ thị này và tạo ra một tệp mã nguồn đã được sửa đổi. Để xem mã đã qua tiền xử lý trông như thế nào, bạn có thể chạy lệnh sau ở terminal:
g++ -E your_program.cpp
4.2. Compiling (Biên dịch)
Sau khi đã trải qua giai đoạn tiền xử lý, trình biên dịch mới đưa tệp mã nguồn đã được sửa đổi vào giai đoạn biên dịch. Ở giai đoạn này, trình biên dịch (compiler) sẽ chuyển mã nguồn C++ thành mã đối tượng (object code), mã đối tượng này chính là ngôn ngữ máy mà máy tính có thể hiểu.
Quá trình biên dịch được diễn ra như sau:
- Phân tích cú pháp (Syntax Analysis): Trước hết, trình biên dịch sẽ phân tích cú pháp của mã nguồn C++. Điều này bao gồm việc kiểm tra xem mã nguồn có tuân theo cú pháp ngôn ngữ C++ hay không. Nếu có lỗi cú pháp, trình biên dịch sẽ tạo ra thông báo lỗi.
- Tạo bản sao nội dung mã đối tượng (Intermediate Representation): Trình biên dịch thường tạo ra một bản sao trung gian của mã đối tượng, được biểu diễn dưới dạng cây cú pháp trừu tượng (abstract syntax tree) hoặc mã trung gian (intermediate representation). Mã trung gian này được sử dụng để dễ dàng tạo mã đối tượng cho các nền tảng mục tiêu khác nhau.
- Tạo mã đối tượng (Code Generation): Sau khi mã trung gian đã được tạo, trình biên dịch tiếp tục tạo mã đối tượng. Điều này bao gồm việc chuyển đổi mã trung gian thành mã máy (machine code) hoặc mã hợp ngữ (assembly code) cho CPU cụ thể của máy tính bạn đang sử dụng.
- Tạo tệp mã đối tượng (Object File): Kết quả của quá trình biên dịch là một hoặc nhiều tệp mã đối tượng (object files). Mỗi tệp mã đối tượng thường tương ứng với một tệp mã nguồn và chứa mã máy hoặc mã hợp ngữ đã được tạo từ mã nguồn tương ứng.
4.3. Linking (Liên kết)
Sau khi đã trải qua bước biên dịch, nếu trình biên dịch tạo ra nhiều hơn một object file (file có đuôi .o
hoặc .obj
trên Windows), một trình liên kết đang đứng chờ sẵn sẽ liên kết các tệp mã đối tượng với nhau cùng với các thư viện bên ngoài để tạo ra một chương trình hoàn chỉnh.
Tổng quan về qúa trình biên dịch trong C++ diễn ra như sau:
Giả sử bạn có hai tệp mã nguồn C++: "main.cpp" và "functions.cpp" "main.cpp" chứa hàm main()
và "functions.cpp" chứa một số hàm được gọi trong "main.cpp" Sau khi bạn biên dịch cả hai tệp này, trình biên dịch sẽ tạo ra hai tệp mã đối tượng: "main.o" và "functions.o".
Cuối cùng, trình liên kết (linking) sẽ kết hợp các tệp mã đối tượng này và liên kết với các thư viện cần thiết (như thư viện chuẩn C++) để tạo ra một chương trình thực thi hoàn chỉnh. Chương trình này có thể chạy trên máy tính của bạn và thực hiện các chức năng đã được viết trong mã nguồn C++.
5. Các khái niệm quan trọng trong ngôn ngữ C++
5.1. Hướng đối tượng trong C++
Hướng đối tượng (Object-Oriented Programming, OOP) là một phương pháp lập trình mà trong đó chương trình được thiết kế bằng cách xem xét và mô phỏng thế giới thực thông qua các đối tượng. Ở 200Lab đã có bài viết giải thích rất chi tiết về lập trình hướng đối tượng, các bạn có thể đọc để hiểu sâu hơn nhé.
C++ là một ngôn ngữ lập trình hướng đối tượng mạnh mẽ, và nó hỗ trợ các khái niệm và cơ chế cốt lõi của OOP. Dưới đây là một số khái niệm quan trọng trong OOP sử dụng trong C++:
5.1.1. Lớp (Class)
Lớp là một mô tả của một loại đối tượng. Nó xác định các thuộc tính (biến thành viên) và phương thức (hàm thành viên) mà các đối tượng của lớp này có thể sử dụng.
Khai báo lớp trong C++
Để khai báo một lớp trong C++, bạn sử dụng từ khóa class
và sau đó đặt tên cho lớp. Bên trong lớp, bạn có thể định nghĩa các thuộc tính và phương thức của lớp. Dưới đây là cú pháp cơ bản để khai báo lớp trong C++:
class Person {
public:
std::string name;
int age;
void displayInfo() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
Các phần tử của class được quản lý bởi ba thuộc tính truy cập: private, protected hoặc public (thuộc tính mặc định khi truy cập vào một phần tử trong class là private). Các phần tử private không thể được truy cập bên ngoài class mà chỉ có thể được truy cập thông qua các phương thức của class chứa chúng. Ngược lại, các phần tử public có thể được truy cập ở bất kỳ class nào.
class Person {
public:
// Các thuộc tính public
std::string name;
int age;
// Phương thức public
void displayInfo() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
private:
// Các thuộc tính private
int privateData;
// Phương thức private
void privateMethod() {
// Nội dung phương thức
}
};
Truy cập các thành viên dữ liệu trong C++
Sau khi bạn đã khai báo lớp, bạn có thể tạo đối tượng từ lớp đó và sử dụng các thành phần public của đối tượng. Ví dụ:
Person person1;
person1.name = "Alice";
person1.age = 30;
person1.displayInfo();
5.1.2. Đối tượng (Object)
Trong C++, một đối tượng (Object) là một thể hiện cụ thể của một lớp (Class). Nó được tạo ra bằng cách sử dụng lớp để định nghĩa cấu trúc và hành vi của đối tượng. Một đối tượng có thể chứa dữ liệu (thuộc tính) và có thể thực hiện các hành động (phương thức) liên quan đến dữ liệu đó. Dưới đây là cách tạo và sử dụng đối tượng trong C++:
Khai báo lớp: Trước hết, bạn cần định nghĩa một lớp (Class) để mô tả cấu trúc của đối tượng.
class Person {
public:
std::string name;
int age;
void displayInfo() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
Tạo đối tượng: Để tạo một đối tượng từ lớp, bạn sử dụng cú pháp sau:
Lop lop_concrete; // Tạo đối tượng từ lớp
Ví dụ:
Person person1; // Tạo đối tượng person1 từ lớp Person
Sử dụng đối tượng: Sau khi tạo đối tượng, bạn có thể sử dụng các thuộc tính và phương thức của đối tượng đó. Ví dụ:
person1.name = "Alice"; // Gán giá trị cho thuộc tính name của person1
person1.age = 30; // Gán giá trị cho thuộc tính age của person1
person1.displayInfo(); // Gọi phương thức displayInfo() của person1
Tạo nhiều đối tượng: Bạn có thể tạo nhiều đối tượng từ cùng một lớp. Mỗi đối tượng có thể chứa các giá trị khác nhau cho các thuộc tính của nó.
Person person2; // Tạo đối tượng person2
person2.name = "Bob";
person2.age = 25;
person2.displayInfo();
Person person3; // Tạo đối tượng person3
person3.name = "Charlie";
person3.age = 35;
person3.displayInfo();
Mỗi đối tượng là một thể hiện riêng biệt của lớp và có thể có các giá trị khác nhau cho các thuộc tính của nó. Điều này cho phép bạn mô phỏng và làm việc với nhiều đối tượng khác nhau trong chương trình của bạn.
5.1.3. Kế thừa (Inheritance)
Kế thừa (Inheritance) là một trong những khái niệm quan trọng trong hướng đối tượng (OOP) trong C++. Nó cho phép bạn tạo lớp con dựa trên một lớp cha đã tồn tại, kế thừa các thuộc tính và phương thức của lớp cha và mở rộng hoặc sửa đổi chúng. Kế thừa giúp tái sử dụng mã nguồn và xây dựng các mối quan hệ "is-a" (là một) giữa các lớp.
5.1.3.1. Cách sử dụng kế thừa
Cú pháp cơ bản của kế thừa:
class LopCha {
// Các thành phần của lớp cha
};
class LopCon : public LopCha {
// Các thành phần của lớp con
};
Giả sử chúng ta có lớp cha Animal
và muốn tạo lớp con Dog
kế thừa từ Animal
. Lớp Dog
sẽ kế thừa các thuộc tính và phương thức của Animal
:
#include <iostream>
// Lớp cha
class Animal {
public:
std::string name;
Animal(std::string n) : name(n) {}
void speak() {
std::cout << "Animal speaks" << std::endl;
}
};
// Lớp con
class Dog : public Animal {
public:
Dog(std::string n) : Animal(n) {}
void speak() {
std::cout << "Dog barks" << std::endl;
}
};
int main() {
Animal animal("Generic Animal");
Dog dog("Buddy");
// Gọi phương thức từ lớp cha
animal.speak(); // "Animal speaks"
// Gọi phương thức từ lớp con
dog.speak(); // "Dog barks"
// Truy cập thuộc tính kế thừa từ lớp cha
std::cout << "Dog's name: " << dog.name << std::endl;
return 0;
}
Trong ví dụ trên:
- Chúng ta đã định nghĩa lớp cha
Animal
với thuộc tínhname
và phương thứcspeak
. - Lớp con
Dog
kế thừa từ lớp chaAnimal
. Nó có thể truy cập thuộc tínhname
và phương thứcspeak
từ lớp cha, nhưng nó cũng có thể định nghĩa lại phương thứcspeak
để có hành vi khác biệt. - Trong hàm
main()
, chúng ta tạo một đối tượnganimal
từ lớpAnimal
và một đối tượngdog
từ lớpDog
. Chúng ta thấy rằng lớp conDog
có thể sử dụng cả thuộc tính và phương thức từ lớp chaAnimal
.
5.1.3.2. Mối quan hệ giữa các đối tượng
Có hai mối quan hệ giữa các đối tượng trong C++, quan hệ “Has-A” và quan hệ “Is-A”.
Quan hệ Has-A: Trong hướng đối tượng, mối quan hệ "Has-A" thường liên quan đến việc một lớp chứa một đối tượng của một lớp khác. Mối quan hệ này thể hiện một lớp "có" hoặc "chứa" một đối tượng của lớp khác, và được sử dụng để tạo mối quan hệ giữa các lớp để tái sử dụng mã nguồn và tạo cấu trúc phức tạp hơn. Mối quan hệ "Has-A" thường được gọi là quan hệ hợp thành (composition) hoặc quan hệ thành phần (aggregation).
class Engine {
public:
void start() {
std::cout << "Engine started" << std::endl;
}
};
class Car {
private:
Engine carEngine; // Mối quan hệ "Has-A" - Lớp Car chứa một đối tượng Engine
public:
void startCar() {
carEngine.start();
std::cout << "Car started" << std::endl;
}
};
int main() {
Car myCar;
myCar.startCar();
return 0;
}
Trong ví dụ này:
- Chúng ta có hai lớp:
Engine
vàCar
. - Lớp
Engine
đại diện cho động cơ của xe hơi và có một phương thứcstart()
để khởi động động cơ. - Lớp
Car
chứa một đối tượng của lớpEngine
, thể hiện mối quan hệ "Has-A." Khi bạn gọi phương thứcstartCar()
của lớpCar
, nó sẽ gọi phương thứcstart()
của đối tượngcarEngine
(động cơ) để khởi động động cơ và sau đó thông báo rằng xe hơi đã khởi động.
Mối quan hệ "Has-A" cho phép bạn xây dựng các lớp phức tạp bằng cách kết hợp các đối tượng khác, tận dụng tính tái sử dụng của mã nguồn và làm cho mã của bạn dễ quản lý hơn.
Quan hệ Is-A: Quan hệ "Is-A" thể hiện mối quan hệ giữa một lớp con và lớp cha trong đó lớp con được coi là một loại của lớp cha. Một cách đơn giản, nó thể hiện rằng lớp con "là một" lớp cha. Quan hệ "Is-A" thường được sử dụng để xác định mối quan hệ kế thừa trong OOP.
class Animal {
public:
void eat() {
std::cout << "Animal is eating" << std::endl;
}
};
class Dog : public Animal {
public:
void bark() {
std::cout << "Dog is barking" << std::endl;
}
};
int main() {
Dog myDog;
myDog.eat(); // Đúng, vì mối quan hệ "Is-A" cho phép Dog "là một" Animal
myDog.bark();
return 0;
}
Trong ví dụ này:
- Lớp
Animal
đại diện cho một động vật và có một phương thứceat
. - Lớp
Dog
kế thừa từ lớpAnimal
, thể hiện mối quan hệ "Is-A" giữa chúng. LớpDog
có thể sử dụng phương thứceat
của lớpAnimal
và cũng có phương thức riêng của nóbark
.
Mối quan hệ "Is-A" cho phép bạn sử dụng một đối tượng của lớp con trong một ngữ cảnh đòi hỏi một đối tượng của lớp cha, vì lớp con được xem là một biến thể hoặc loại cụ thể của lớp cha.
5.1.3.3. Các loại kế thừa trong C++
Kế thừa đơn: Kế thừa đơn (Single Inheritance) trong C++ là một loại kế thừa mà một lớp con chỉ kế thừa từ một lớp cha duy nhất. Trong kế thừa đơn, lớp con chưa một lớp cha và có thể thừa kế các thuộc tính và phương thức của lớp cha đó.
#include <iostream>
// Lớp cha
class Animal {
public:
void eat() {
std::cout << "Animal is eating" << std::endl;
}
};
// Lớp con
class Dog : public Animal {
public:
void bark() {
std::cout << "Dog is barking" << std::endl;
}
};
int main() {
Dog myDog;
myDog.eat(); // Gọi phương thức từ lớp cha
myDog.bark(); // Gọi phương thức từ lớp con
return 0;
}
Kế thừa đa cấp: Kế thừa đa cấp (Multilevel Inheritance) trong C++ là một loại kế thừa trong đó một lớp con kế thừa từ một lớp cha, sau đó một lớp khác kế thừa từ lớp con. Kết quả là có một chuỗi kế thừa đa cấp, trong đó các lớp con nằm lớp trên cùng kế thừa từ lớp cha ở dưới cùng.
#include <iostream>
// Lớp cha
class Grandparent {
public:
void speak() {
std::cout << "Grandparent speaks" << std::endl;
}
};
// Lớp con 1
class Parent : public Grandparent {
public:
void talk() {
std::cout << "Parent talks" << std::endl;
}
};
// Lớp con 2
class Child : public Parent {
public:
void babble() {
std::cout << "Child babbles" << std::endl;
}
};
int main() {
Child myChild;
myChild.speak(); // Gọi phương thức từ lớp cha (Grandparent)
myChild.talk(); // Gọi phương thức từ lớp con 1 (Parent)
myChild.babble(); // Gọi phương thức từ lớp con 2 (Child)
return 0;
}
Trong ví dụ trên, lớp Child
kế thừa từ lớp Parent
, và lớp Parent
kế thừa từ lớp Grandparent
, tạo thành một chuỗi kế thừa đa cấp. Khi bạn tạo một đối tượng của lớp Child
, nó có thể truy cập các phương thức từ tất cả các lớp cha trong chuỗi kế thừa.
Kế thừa đa cấp cho phép bạn tạo cấu trúc phân cấp phức tạp và mô hình thế giới thực trong lập trình hướng đối tượng. Tuy nhiên, nên sử dụng nó cẩn thận để tránh làm cho mã nguồn trở nên quá phức tạp và khó bảo trì.
Kế thừa phân cấp: Kế thừa phân cấp (Hierarchical Inheritance) trong C++ là một loại kế thừa trong đó nhiều lớp con kế thừa từ một lớp cha duy nhất. Điều này tạo ra một cấu trúc phân cấp, trong đó các lớp con chia sẻ một lớp cha chung và có thể có các thuộc tính và phương thức riêng.
#include <iostream>
// Lớp cha
class Shape {
public:
void draw() {
std::cout << "Drawing a shape" << std::endl;
}
};
// Lớp con 1
class Circle : public Shape {
public:
void draw() {
std::cout << "Drawing a circle" << std::endl;
}
};
// Lớp con 2
class Rectangle : public Shape {
public:
void draw() {
std::cout << "Drawing a rectangle" << std::endl;
}
};
int main() {
Circle circle;
circle.draw(); // Gọi phương thức từ lớp con 1 (Circle)
Rectangle rectangle;
rectangle.draw(); // Gọi phương thức từ lớp con 2 (Rectangle)
return 0;
}
Trong ví dụ trên, lớp Circle
và lớp Rectangle
kế thừa từ lớp cha Shape
, tạo thành một cấu trúc phân cấp. Mặc dù tất cả các lớp chia sẻ phương thức draw
từ lớp cha, mỗi lớp con có thể định nghĩa lại (override) phương thức draw
để có hành vi riêng của nó.
Kế thừa phân cấp cho phép bạn xây dựng các mô hình phân cấp và tạo các biến thể của lớp cha trong lập trình hướng đối tượng.
5.1.3.4. Phạm vi truy cập (Access Visibility)
Phạm vi truy cập (access visibility) trong kế thừa trong C++ quy định cách mà thành viên (thuộc tính và phương thức) của lớp cha có thể được truy cập từ lớp con. C++ hỗ trợ ba loại phạm vi truy cập trong kế thừa:
Public Inheritance (Kế thừa công khai): Trong kế thừa công khai, toàn bộ phạm vi truy cập của các thành viên public và protected của lớp cha được duy trì trong lớp con. Tất cả thành viên public của lớp cha trở thành public trong lớp con, và thành viên protected của lớp cha trở thành protected trong lớp con.
class LopCha {
public:
int x; // public thành viên
protected:
int y; // protected thành viên
};
class LopCon : public LopCha {
// x và y đều có thể truy cập từ LopCon
};
Protected Inheritance (Kế thừa bảo vệ): Trong kế thừa bảo vệ, toàn bộ phạm vi truy cập của các thành viên public và protected của lớp cha trở thành protected trong lớp con. Thành viên public của lớp cha trở thành protected và thành viên protected của lớp cha vẫn là protected trong lớp con.
class LopCha {
public:
int x; // public thành viên
protected:
int y; // protected thành viên
};
class LopCon : protected LopCha {
// x và y đều trở thành protected trong LopCon
};
Private Inheritance (Kế thừa riêng tư): Trong kế thừa riêng tư, toàn bộ phạm vi truy cập của các thành viên public và protected của lớp cha trở thành private trong lớp con. Cả thành viên public và protected của lớp cha trở thành private trong lớp con.
class LopCha {
public:
int x; // public thành viên
protected:
int y; // protected thành viên
};
class LopCon : private LopCha {
// x và y đều trở thành private trong LopCon
};
5.1.4. Đa hình (Polymorphism)
Đa hình (Polymorphism) là một trong những khái niệm quan trọng trong hướng đối tượng (OOP) trong C++ và các ngôn ngữ lập trình khác. Đa hình cho phép bạn gọi cùng một phương thức với cùng một tên từ các đối tượng thuộc các lớp con khác nhau và có hành vi khác nhau.
Ví dụ trong thực tế cả chó và mèo đều có thể phát ra âm thanh khi kêu, tuy nhiên chó thì sủa "gâu gâu" còn mèo thì kêu "meo meo", tuy đều là âm thanh nhưng được thực hiện bởi hai đối tượng khác nhau
5.1.4.1. Các loại đa hình trong C++
Có hai loại đa hình: đa hình biên dịch (compile-time polymorphism) và đa hình chạy thời gian (runtime polymorphism).
Đa hình tĩnh (Compile-time Polymorphism): Đa hình tĩnh xảy ra khi trình biên dịch quyết định xem phải gọi phương thức nào dựa trên kiểu dữ liệu tại thời điểm biên dịch. Đa hình tĩnh thường được thực hiện thông qua quá tải phương thức (method overloading).
#include <iostream>
class Calculator {
public:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
};
int main() {
Calculator calc;
int result1 = calc.add(5, 3); // Gọi phiên bản int
double result2 = calc.add(3.5, 2.5); // Gọi phiên bản double
std::cout << "Result 1: " << result1 << std::endl;
std::cout << "Result 2: " << result2 << std::endl;
return 0;
}
Trong ví dụ này, lớp Calculator
có hai phương thức add
với các kiểu tham số khác nhau. Trình biên dịch sẽ quyết định xem phải gọi phương thức nào dựa trên kiểu dữ liệu của các đối số khi bạn gọi nó.
Đa hình động (Runtime Polymorphism): Đa hình động xảy ra khi quyết định phải gọi phương thức nào dựa trên kiểu thực tế của đối tượng tại thời điểm chạy. Đa hình động thường được thực hiện thông qua kế thừa và ghi đè phương thức (method overriding).
#include <iostream>
// Lớp cha
class Animal {
public:
virtual void speak() {
std::cout << "Animal speaks" << std::endl;
}
};
// Lớp con
class Dog : public Animal {
public:
void speak() override {
std::cout << "Dog barks" << std::endl;
}
};
// Lớp con khác
class Cat : public Animal {
public:
void speak() override {
std::cout << "Cat meows" << std::endl;
}
};
int main() {
Animal* myAnimal;
Dog myDog;
Cat myCat;
myAnimal = &myDog;
myAnimal->speak(); // Gọi phương thức của lớp Dog
myAnimal = &myCat;
myAnimal->speak(); // Gọi phương thức của lớp Cat
return 0;
}
Trong ví dụ này, lớp Animal
có một phương thức speak
được đánh dấu bằng từ khóa virtual
, và các lớp con Dog
và Cat
ghi đè phương thức speak
bằng từ khóa override
. Khi gọi phương thức speak
từ một con trỏ đối tượng myAnimal
, chương trình sẽ xác định phương thức cụ thể nào được gọi tại thời điểm chạy dựa trên kiểu thực sự của đối tượng mà con trỏ đang trỏ đến.
5.1.5. Trừu tượng (Abstraction)
Tính trừu tượng trong C++ là một trong 4 tính chất quan trọng của lập trình hướng đối tượng, mục tiêu chính của nó là làm giảm sự phức tạp bằng cách ẩn các chi tiết không liên quan trực tiếp tới người dùng.
Nó cho phép bạn che dấu các chi tiết phức tạp bên trong một đối tượng hoặc hàm, và chỉ tập trung vào các khía cạnh quan trọng và cần thiết cho việc sử dụng. Trừu tượng giúp bạn tạo ra các giao diện đơn giản để tương tác với các thành phần phức tạp.
Một ví dụ trong thực tế dễ thấy nhất về tính trừu tượng đấy là việc chúng ta đi ăn ở một nhà hàng, khi đến nhà hàng việc chúng ta cần làm chỉ là gọi món và sau đó ăn, chứ không quan tâm món ăn đó được làm ra như thế nào. Việc này giúp chúng ta tập trung hơn vào những việc khác như cảm nhận mùi vị món ăn, trò chuyện với bạn bè,... thay vì để ý xem món ăn được làm ra như thế nào.
Lớp trừu tượng (Abstract Class): Trong C++, bạn có thể định nghĩa các lớp trừu tượng. Đây là các lớp không thể tạo ra đối tượng trực tiếp, nhưng chúng định nghĩa các giao diện cho các lớp con. Bạn chỉ cần triển khai (implement) các phương thức ảo trong các lớp con. Ví dụ:
class Shape {
public:
virtual void draw() = 0; // Phương thức ảo thuần túy (pure virtual method)
};
class Circle : public Shape {
public:
void draw() override {
// Triển khai cách vẽ hình tròn
}
};
class Rectangle : public Shape {
public:
void draw() override {
// Triển khai cách vẽ hình chữ nhật
}
};
Hàm trừu tượng (Abstract Function): Trong các lớp trừu tượng, bạn có thể định nghĩa các hàm trừu tượng, hoặc còn gọi là phương thức ảo thuần túy. Điều này buộc các lớp con phải triển khai chúng. Ví dụ:
class Animal {
public:
virtual void speak() = 0;
};
class Dog : public Animal {
public:
void speak() override {
// Triển khai cách con chó kêu
}
};
class Cat : public Animal {
public:
void speak() override {
// Triển khai cách con mèo kêu
}
};
Trừu tượng qua giao diện (Abstraction via Interfaces): Trong C++, bạn có thể sử dụng các giao diện (interfaces) để định nghĩa trừu tượng. Một giao diện là một tập hợp các phương thức trừu tượng mà các lớp khác phải triển khai. Ví dụ:
class Drawable {
public:
virtual void draw() = 0;
};
class Button : public Drawable {
public:
void draw() override {
// Triển khai cách vẽ nút
}
};
class Text : public Drawable {
public:
void draw() override {
// Triển khai cách vẽ văn bản
}
};
Nhìn chung, tính trừu tượng giúp bạn tạo ra các lớp và giao diện có thể tái sử dụng và giúp che dấu các chi tiết bên trong một đối tượng, cho phép bạn tập trung vào việc sử dụng nó mà không cần quan tâm đến cách nó hoạt động bên trong.
5.2. Dấu :: (Scope Resolution Operator)
Dấu "Scope Resolution Operator" (::) trong C++ được sử dụng để tham chiếu đến phạm vi (scope) của các biến, hàm và lớp trong chương trình. Nó cho phép bạn xác định rõ ràng đối tượng hoặc phạm vi mà bạn muốn truy cập. Dấu "Scope Resolution Operator" có một số cách sử dụng:
Truy cập phạm vi của biến toàn cục hoặc tĩnh: Để truy cập biến toàn cục hoặc tĩnh từ bên trong một hàm hoặc lớp, bạn sử dụng "::" để xác định rõ ràng phạm vi của biến.
int x = 10; // Biến toàn cục
void myFunction() {
int x = 5; // Biến cục bộ
std::cout << x << std::endl; // In biến cục bộ
std::cout << ::x << std::endl; // In biến toàn cục
}
Truy cập phạm vi của lớp cơ sở: Khi bạn có kế thừa từ một lớp cơ sở và muốn truy cập thành viên của lớp cơ sở, bạn sử dụng "::" để chỉ ra phạm vi của lớp cơ sở.
class BaseClass {
public:
void someFunction() {
// ...
}
};
class DerivedClass : public BaseClass {
public:
void anotherFunction() {
BaseClass::someFunction(); // Gọi phương thức của lớp cơ sở
}
};
Truy cập phạm vi của namespace: Khi bạn muốn truy cập các thành phần (biến, hàm, lớp) trong một không gian tên (namespace) trong C++, bạn sử dụng "::" để chỉ định không gian tên.
namespace MyNamespace {
int x = 42;
void myFunction() {
// ...
}
}
int main() {
int x = 10;
std::cout << x << std::endl; // In biến x cục bộ
std::cout << MyNamespace::x << std::endl; // In biến x từ không gian tên
return 0;
}
Dấu "::" giúp bạn quản lý phạm vi và tránh xung đột giữa các biến và thành viên của các phạm vi khác nhau trong chương trình C++.
5.3. STL (Standard Template Library)
STL (Standard Template Library) trong C++ là một tập hợp các mẫu (templates) và cấu trúc dữ liệu hữu ích được cung cấp sẵn trong thư viện tiêu chuẩn của ngôn ngữ C++. STL cung cấp một bộ công cụ mạnh mẽ để thực hiện các cấu trúc dữ liệu và thuật toán phổ biến, giúp bạn viết mã ngắn gọn, hiệu quả và dễ bảo trì. Dưới đây là một ví dụ cụ thể về cách sử dụng một số STL phổ biến như vector
, algorithm
, và map
trong C++:
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
int main() {
// Sử dụng vector để lưu trữ danh sách các số nguyên
std::vector<int> numbers = {5, 2, 9, 1, 5, 6};
// Sắp xếp danh sách các số theo thứ tự tăng dần
std::sort(numbers.begin(), numbers.end());
// In danh sách các số đã sắp xếp
std::cout << "Danh sách các số sau khi sắp xếp: ";
for (const int& num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
// Sử dụng map để lưu trữ dữ liệu key-value
std::map<std::string, int> studentScores;
studentScores["Alice"] = 95;
studentScores["Bob"] = 89;
studentScores["Charlie"] = 78;
// In điểm số của mỗi học sinh
std::cout << "Điểm số của học sinh:" << std::endl;
for (const auto& pair : studentScores) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
Trong ví dụ này, chúng ta sử dụng các thành phần STL như sau:
- Sử dụng
std::vector
để lưu trữ danh sách các số nguyên. - Sử dụng
std::sort
từ<algorithm>
để sắp xếp danh sách các số. - Sử dụng
std::map
để lưu trữ các dữ liệu key-value, trong đó các học sinh có tên và điểm số tương ứng.
Kết quả của chương trình sẽ là danh sách các số đã sắp xếp theo thứ tự tăng dần và danh sách điểm số của các học sinh. STL giúp chúng ta thực hiện các nhiệm vụ này một cách dễ dàng và hiệu quả.
5.4. Ngoại lệ (Exception) trong C++
Ngoại lệ (Exception) trong C++ là một cơ chế cho phép xử lý các tình huống bất thường và lỗi trong chương trình một cách điều khiển và thông báo lỗi cho các phần khác của chương trình. Sử dụng ngoại lệ giúp bạn tách các phần của mã xử lý lỗi ra khỏi mã chính, cải thiện tính bảo mật và bảo dự phòng lỗi.
5.4.1. Các cách sử dụng ngoại lệ
Trả ngoại lệ (Throwing an Exception): Để trả ra một ngoại lệ, bạn sử dụng từ khóa throw
theo sau là một giá trị hoặc một đối tượng kiểu ngoại lệ (thường là một đối tượng của một lớp con kế thừa từ std::exception
).
throw std::runtime_error("An error occurred");
Bắt ngoại lệ (Catching an Exception): Để bắt ngoại lệ, bạn sử dụng khối try...catch
. Trong khối try
, bạn đặt mã mà bạn muốn kiểm tra xem có ngoại lệ xảy ra hay không. Nếu ngoại lệ xảy ra, mã bên trong khối catch
sẽ được thực thi.
try {
// Mã có thể gây ra ngoại lệ
}
catch (const std::exception& e) {
// Xử lý ngoại lệ ở đây
}
Xây dựng kiểu ngoại lệ tùy chỉnh (Custom Exception): Bạn có thể xây dựng kiểu ngoại lệ tùy chỉnh bằng cách kế thừa từ std::exception
hoặc các lớp con của nó.
class MyException : public std::exception {
public:
const char* what() const noexcept {
return "My custom exception occurred";
}
};
5.4.2. Ví dụ sử dụng ngoại lệ
Dưới đây là một ví dụ đơn giản về cách sử dụng ngoại lệ:
#include <iostream>
#include <stdexcept>
int divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero is not allowed");
}
return a / b;
}
int main() {
try {
int result = divide(10, 0);
std::cout << "Result: " << result << std::endl;
}
catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
Trong ví dụ này, hàm divide
ném một ngoại lệ khi bị chia cho 0. Trong main
, chúng ta sử dụng một khối try...catch
để xử lý ngoại lệ, và chương trình in thông báo lỗi ra màn hình.
5.5. Hàm Lambda (Lambda Function)
Hàm Lambda (Lambda Function) trong C++ là một cách để tạo và sử dụng các hàm vô danh (anonymous functions) một cách ngắn gọn và tiện lợi. Hàm Lambda thường được sử dụng để truyền các hàm nhỏ, đơn giản, hoặc một đoạn mã ngắn làm tham số cho các hàm hoặc thuật toán khác. Chúng cung cấp một cách định nghĩa hàm trong nơi chúng được sử dụng mà không cần phải đặt tên cho hàm đó. Cú pháp của hàm Lambda trong C++ như sau:
[ capture_clause ] ( parameter_list ) -> return_type {
// Mã thân của hàm Lambda
}
capture_clause
là danh sách các biến ngoại (có thể không có) mà hàm Lambda sẽ truy cập. Biến ngoại này có thể được truyền vào hàm Lambda và được sử dụng bên trong mã thân của nó.parameter_list
là danh sách các tham số của hàm Lambda.return_type
là kiểu dữ liệu trả về của hàm Lambda.
Dưới đây là một ví dụ cụ thể về việc sử dụng hàm Lambda để sắp xếp một vector:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 2, 9, 1, 5, 6};
// Sử dụng Lambda để sắp xếp vector theo thứ tự tăng dần
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a < b;
});
// In danh sách các số đã sắp xếp
std::cout << "Danh sách các số sau khi sắp xếp: ";
for (const int& num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
Trong ví dụ này, hàm Lambda được sử dụng để truyền vào std::sort
để sắp xếp vector numbers
theo thứ tự tăng dần. Hàm Lambda định nghĩa kiểu bool
và trả về true
nếu a
nhỏ hơn b
, và false
nếu ngược lại. Hàm Lambda cung cấp tính linh hoạt và cho phép bạn định nghĩa các hàm nhỏ, ngắn gọn mà không cần phải tạo một hàm riêng biệt với tên.
6. Tài liệu học ngôn ngữ C++
C++ documentation (cppreference.com): Cung cấp thông tin chi tiết về ngôn ngữ C++, thư viện chuẩn và cú pháp.
C++ for Dummies by Stephen R. Davis: Sách này dành cho người mới học C++ và giải thích ngôn ngữ một cách dễ hiểu.
GeeksforGeeks - C++ Programming Language: GeeksforGeeks cung cấp hướng dẫn và ví dụ về C++.
Tài liệu tham khảo
7. Kết luận về C++
C++, với sự linh hoạt, hiệu suất cao và khả năng hỗ trợ đa mô hình lập trình, đã khẳng định vị trí của mình trong thế giới lập trình. Từ hệ thống, ứng dụng máy tính để bàn, đến phát triển game và thực tế ảo, C++ đã và đang đóng góp một phần quan trọng trong việc tạo ra những sản phẩm công nghệ tiên tiến.
Việc nắm vững C++ không chỉ mở ra cơ hội nghề nghiệp rộng lớn, mà còn giúp bạn hiểu sâu hơn về cách thức hoạt động của máy tính. Hãy tiếp tục khám phá và tận dụng sức mạnh của C++ để tạo ra những sản phẩm phần mềm chất lượng cao nhé!
Bạn hãy thường xuyên theo dõi các bài viết hay về Lập Trình & Dữ Liệu trên 200Lab Blog nhé. Cũng đừng bỏ qua những khoá học Lập Trình tuyệt vời trên 200Lab nè.
Một vài bài viết mới bạn sẽ thích:
CSS cơ bản: Sự khác nhau giữa Flexbox và Grid
Git cơ bản: Định nghĩa các lệnh Git thường gặp
Lập trình C cơ bản - Giới thiệu ngôn ngữ C
Clean code là gì ? Các nguyên tắc để viết clean code
Bài toán tháp Hà Nội và cách giải sử dụng đệ quy
Bài viết liên quan
Hướng dẫn Khởi tạo dự án Typescript với VSCode
Sep 25, 2024 • 7 min read
Lập trình C cơ bản - Ứng dụng của ngôn ngữ C
May 02, 2024 • 17 min read
Java Core là gì? So sánh Java Core và Java
Oct 02, 2023 • 11 min read
Clean code là gì ? Nguyên tắc viết clean code trong Lập Trình
Sep 28, 2023 • 27 min read
Bài toán tháp Hà Nội và cách giải sử dụng Đệ Quy
Sep 28, 2023 • 13 min read
Prompt Engineering là gì? Tìm hiểu về Prompt Engineer
Sep 25, 2023 • 22 min read