You are on page 1of 8

Abstract Base Class và Pure Virtual Functions:

Một lớp cơ sở trừu tượng chỉ dùng để làm cơ sở cho một lớp khác và chúng không có thể
tạo ra đối tượng. vì mục đích của nó chỉ dung để định nghĩa các khái niệm tổng quát. Đi
kèm với Lớp này chúng ta có một hình thức là Pure Virtual Function (PVF). Đây là một
lớp ảo được coi là không có gì bởi vì chúng ta có thể nhận biết nó qua từ khóa virtual và
theo sau hàm này được gán là 0 (=0).
Cú pháp :
virtual void f() =0;
PVF rất hữu dụng vì chúng làm rõ rang tính chất trừu tượng của class, cũng như o6ng báo
cho người viết và trình biên dịch mục đích sẽ làm gì.
Ví dụ :
#include <iostream>
using namespace std;

class SVien {
public:
virtual void speak() const = 0; // hai PVF
virtual void eat() const = 0;
int sleep(); // hàm bình thường
};

Khi một lớp ảo được thừa kế, thì tất cả các hàm PVF sẽ được triển khai hoặc các lớp thừa
kế đó sẻ trỏ thành các lớp ảo.
Tuy không tạo được các đối tượng nhưng các pointer cũng như các biến tham chiếu đến
các thành phần của lớp trừu tượng vẫn được coi là hợp lệ.
Các lớp thừa hưởng từ lớp ảo luôn luôn phải khai báo lại các PVF mà nó thừa hưởng và
có thể định nghĩa rõ rang để tạo ra các đối tượng
Ví dụ:
class HSinh: public SVien {
….
void eat ()
{
…………
}
}
Định Nghĩa các PVF:
Chúng ta có thể định nghĩa các PVF bên ngoài lớp ảo để từ đó các lớp dẫn xuất sẻ xử
dụng chung một code, chúng ta không thể định nghĩa các PVF bên trong một lớp :
Ví dụ :
#include <iostream>
using namespace std;

class SVien {
public:
virtual void speak() const = 0;
virtual void eat() const = 0;

//! virtual void study() const = 0 {}


};
void SVien::eat() const {
cout << "SVien::eat()" << endl;
}

void SVien::speak() const {


cout << "SVien::speak()" << endl;
}

class HSinh : public SVien {


public:
// Dùng chung code voi SVien:
void speak() const { SVien::speak(); }
void eat() const { SVien::eat(); }
};

int main() {
HSinh NVA; // Học sinh NVA
NVA.speak();
NVA.eat();
}
Các thứ tự gọi các Constructor và Destructor trong Inheritance :
Trong Inheritance khi chúng ta khởi tạo một đối tượng thì hàm xây dựng của lớp cha sẽ
được gọi trước và sau đó mới là hàm xây dựng của lớp con. Cũng như quá trình hủy-
destructor thì quá trình này ngược lại là hàm Hủy của Lớp Con sẽ được gọi trước, sau đó
mới đến hàm hủy của lớp cha.

Constructor Destructor

Ví dụ :
class Con: public Cha{
private:
char *name;
public:
Con (const int max):Cha(max) //gọi hàm Constructor của Cha
{
name=0;
}
Con(const Con& sd): Cha(sd) { name =0;}
~Con(){ delete name; } // xoá vùng nhớ của con trỏ thành viên của lớp con
// khi đã cấp vùng nhớ trong hàm Constructor
}
Copy Constructor và operator= của lớp cha và lớp con:
Khi chúng ta không tự định nghĩa các copy constructor hay toán tử gán thì trình biên dịch
sẻ tự động tạo ra các constructor hay các toán tử gán mặc định, nhưng đôi khi xảy ra
trường hợp sai khi chúng ta có nhưng con trỏ thành viên bên trong các class.
Copy Constructor :
ClassA::ClassA(const ClassA&)
Operator=:
ClassA& ClassA:: operator=(const &ClassA)
Trong constructor và toán tử gán của lớp con chúng ta phải gọi copy constructor hay toán
tử gán của lớp cha
Ví du:
ClassA::ClassA(const ClassA& Con): ClassB(Con) {…..} //gọi Constructor
của // Cha
ClassA& ClassA:: operator=(const ClassA& Con ){
….
ClassB::operator=(Con);// gọi operator= của Cha
}

Ép kiểu lên ( Up casting) và ép kiểu xuống (Down casting):


Khi chúng ta thực hiện phép ép kiểu từ lớp con thành một lớp cha, ta được phép
upcasting. Khi thực hiện việc ép kiểu sẻ xuất hiện trường hợp đối tượng bị slicing nếu
chúng ta không ép bằng các phép tham chiếu hoặc con trỏ.
Ví dụ :
#include <iostream>
using namespace std;
enum note { A, B, C };

class Cha {
public:
void play(note) const {
cout << "Cha::play" << endl;
}
};
class Con : public Cha {
public:

void play(note) const {


cout << "Con::play" << endl;
}
};

void Epkieu(Cha& i) {
i.play(A);
}

int main() {
Con obj;
Epkieu(obj); // Upcasting
}
Trong ví dụ trên hàm Epkieu nhận đối số là một đối tượng được tham chiếu đến lớp Cha,
trong hàm main() hàm ép kiểu nhận đối số là một đối tượng thuộc lớp con thừa hưởng từ
lớp cha. Kết quả của lời gọi hàm sẻ thực hiện hàm play() của lớp cha,thay vì lớp con.
Downcasting :
Ngược lại khi ta ép kiểu lớp cha thành lớp con thì ta có downcasting, ví dụ ta có Circle là
con của Shape:
Khi ta ép từ Circle Shape : Upcast
ShapeCircle : Downcast.
Khi thực hiện phép downcast chỉ thực hiện trên các phép tham chiếu hay con trỏ và thông
qua các toán tử ép kiểu  dynamic_cast.
Ví dụ :
#include <iostream>
using namespace std;

class Pet { public: virtual ~Pet(){}};


class Dog : public Pet {};
class Cat : public Pet {};

int main() {
Pet* b = new Cat; // Upcast
// cast  Dog*: không rõ vì tham chiếu đến Cat, sẽ trả về giá trị 0
//Pet* c= new Dog;
// Dog* d1=dynamic_cast<Dog*>(c);
Dog* d1 = dynamic_cast<Dog*>(b);
// cast  Cat*: trả về giá trị rõ rang(
Cat* d2 = dynamic_cast<Cat*>(b);
cout << "d1 = " << (long)d1 << endl;
cout << "d2 = " << (long)d2 << endl;
}
Tính đa xạ : polymorphism.
Polymorphism được triển khai trong C++ đi kèm với các virtual function có nghĩa là
nhiều hình thức, ví dụ như chúng ta có cùng một giao diện nhưng hình thức sử dụng giao
diện này khác nhau tùy thuộc vào các kiểu khác nhau của các virtual functions.
Trong OOP polymorphism và templates, exception hadling là những đặc tính mạnh mẽ
của kiểu lập trình này.
Kế thừa từ nhiều lớp :
Một lớp con có thể kế thừa từ nhiều lớp và cũng tương tự như kế thừa đơn giản, lớp con
này kế thừa các thành phần của các lớp Cha và Mẹ và kiểu kế thừa cũng thông qua các
thuộc tính truy xuất như public, protected và private. Khi khởi tạo thì các constructor của
lớp cha được gọi theo thứ tự trong danh sách như hình thức của lớp bao.

Sự mơ hồ trong đa kế thừa :
Ví dụ trong class Cha và Me có một hàm trùng tên là :
class Cha{

int eat();
}
class Me{

int eat();
}
Và trong hàm main khi chúng ta tạo một đối tượng của lớp con :
Con c;
Và lời gọi c.eat(); được coi là mơ hồ vì sẻ không rõ lời gọi tới hàm nào thuộc lớp Cha và
Mẹ, để khắc phục tình trạng này ta sử dụng toán tử phạm vi :
c.Cha::eat();
c.Me::eat();
Hoặc chúng ta cũng có thể sử dụng phép tham chiếu hay con trỏ để upcast một đối tượng
lớp con thành Cha hay Me và thực hiện lời gọi hàm.
Lớp cơ sở ảo:
Chúng ta không thể khai báo hai lần cùng một lớp trong danh sách của các lớp cha cho
một lớp con. Ví dụ: ta có 4 lớp |A,B,C,D trong đó :
B và C là lớp con của A và D là lớp con của B và C hay nói các khác D kế thừa gián tiếp
A thong qua B và C. khi đối tượng thuộc lớp D truy xuất thành phần của lớp A, sẽ có sự
mơ hồ trong cách truy xuất này vì sẽ không biết truy xuất thông qua B hay C.

class A
{
public :
int a;
};
class B:public A{
public: int b;
};
class C:public A{
public : int c;
};
class D:public B,public C{
public: int d;
}
int main(){
D objD;
objD.a=5;
//objD.B::a=5;
objD.b=1;
objD.c=1;
objD.d=1;
}
Để khắc phục tình trang này, chúng ta thêm từ khóa virtual vào cho lớp
cha A khi định nghĩa các lớp B và C :
class B :virtual A và class C: virtual A.
khi định nghĩa như trên D bây giờ được xem là kế thừa trực tiếp từ A và
có thể truy xuất trực tiếp mà không gây ra sự mơ hồ.

kế đến khi ta lấy địa chỉ của objD tức là trả về một địa chỉ của objD
hay là thành phần A trong B hoặc C, ở đây lại xuất hiện sư mơ hồ khi
truy xuất theo tham chiếu hay con trỏ.

Vì B và C thừa hưởng từ A,nên khi trỏ tới, trình biên dịch sẻ không
hiểu trỏ tới từ vùng B-> A hay từ C->A, nên (A*)&objD sẽ dẫn đến sai.
Để khắc phục chúng ta phải chỉ dẩn một cách tường minh và B, C xem như
là hai lớp trung gian:

(A*)(B*)&objD hay (A*)(C*)&objD


Multiple access:
Khi đối tượng lớp D truy xuất vào lớp cơ sở ảo A, thì tùy thuộc vào thuộc tính thừa
hưởng như public, protected và private mà trình biên dịch sẻ chọn con đường có khả năng
truy xuất hiệu quả nhất.
Khai báo using (using declaration) và using chỉ hướng (using directive):
Đối với using directive sẻ đưa ra tất cả các thành phần trong một namespace và using
declaration chỉ đưa ra các thành phần trong một thời điểm.
Khi sử dụng với từ khoá namespace thì chúng ta đã sử dụng using directive
Ví dụ :
#ifndef NAMESPACEINT_H
#define NAMESPACEINT_H
namespace Int {
enum sign { positive, negative };
class Integer {
int i;
sign s;
public:
Integer(int ii = 0)
: i(ii),
s(i >= 0 ? positive : negative)
{}
sign getSign() const { return s; }
void setSign(sign sgn) { s = sgn; }
// ...
};
}
Khi chúng ta sử dụng using directive thì tức là đưa tất cả các thành phần của Int ra sử
dụng hay vào một namespace khác.

#ifndef NAMESPACEMATH_H
#define NAMESPACEMATH_H
#include "NamespaceInt.h"
namespace Math {
using namespace Int;
Integer a, b;
Integer divide(Integer, Integer);
// ...
}
#endif

Còn đối với khai báo using- using declaration chúng ta sẽ làm việc với các phạm vi hiện
hành ví dụ :
#ifndef USINGDECLARATION_H
#define USINGDECLARATION_H
namespace U {
inline void f() {}
inline void g() {}
}
namespace V {
inline void f() {}
inline void g() {}
}
#include "UsingDeclaration.h"
void h() {
using namespace U; // Using directive
using V::f; // Using declaration
f(); // Gọi V::f();
U::f(); // gọi f() của U
}
int main() {}
nếu ta có thêm một namespace khác như sau :
#include "UsingDeclaration.h"
namespace Q {
using U::f;
using V::g;
// ...
}
void m() {
using namespace Q;
f(); // gọi U::f();
g(); // goi V::g();
}
int main() {}

You might also like