![7bad0f0db457ae1ff41df1e2c37a0415.png](https://i-blog.csdnimg.cn/blog_migrate/f3bd8266ffbd754abc70ad7f0898909b.jpeg)
一、面向对象编程(Object-Oriented Programming)
1、对象分解 (Object Structure) : 将复杂系统分解为若干个子对象,每一个子对象又可以继续分解, 直到不需要再分解为止。每一个对象都有其自己的状态(数据)与行为(函数),上下级对象是包含关系,同一级的对象之间可以互相传递消息,即通过行为改变其他对象的状态。如计算机系统可以分解为CPU, 存储系统和 I/O 系统,CPU 又可以分解为 ALU, 寄存器,控制器,时钟等。
2、类的继承 (Class Structure): 对象的状态和行为通过类来定义,类的定义可以继承自更高级(抽象)的类。如,SRAM 继承自 RAM, RAM 继承自存储设备。
二、类的实现
1、从 UML 到类的定义(程序员角度)
![6755aa934a0f7dc4221aecef7d979160.png](https://i-blog.csdnimg.cn/blog_migrate/f22639f0e06bf38272bda4c3081188e1.jpeg)
class Product {
public:
Product() = default; // default constructor
Product(const Product&); // copy constructor
Product(Product&&); // move constructor
Product& operator=(const Product&) = default;
Product& operator=(Product&&) = default;
// destructor is not declared, should be generated by the compiler
public:
void set_name(const std::string&);
std::string name() const;
void set_availability(bool);
bool available() const;
// code omitted for brevity
private:
std::string name_;
double price_;
int rating_;
bool available_;
};
2、从类的定义到结构体(编译器视角)
struct Product {
std::string name_;
bool available_;
double price_;
int rating_;
};
// we forced the compiler to generate the default constructor
void Product_constructor(Product&);
void Product_copy_constructor(Product& this, const Product&);
void Product_move_constructor(Product& this, Product&&);
// default implementation
Product& operator=(Product& this, const Product&);
// default implementation
Product& operator=(Product& this, Product&&);
void Product_set_name(const std::string&);
// takes const because the method was declared as const
std::string Product_name(const Product& this);
void Product_set_availability(Product& this, bool b);
bool Product_availability(const Product& this);
类的静态成员变量保存在程序的静态区,普通成员变量保存在栈区,成员函数保存在代码区。当调用某一对象的成员函数时,编译器会将该对象的指针或引用传递给相应的成员函数,从而允许成员函数找到该对象的数据。
3、对象的初始化与析构
对象的创建过程包括成员变量(非静态)的内存分配及其初始化,初始化由类的构造函数完成。当离开对象作用域时,会调用析构函数并释放内存空间。如果类自定义了非空的默认构造函数或有参数的构造函数,则编译器不会创造默认构造函数。如果类自定义了非空的析构函数,则编译器不会创造默认析构函数。可以通过 default/ delete 强制编译器创建或不创建默认构造函数。
class Product {
public:
Product() = default;//创建默认构造函数
Product() = delete;//不创建默认构造函数
// ...
};
4、拷贝构造函数
C++ 允许创建已有对象的拷贝对象。
Product p1;
p1.set_price(4.2);
Product p2 = p1; // p2 has the same price
默认的拷贝构造函数是浅拷贝,即拷贝已有对象的成员变量的值到新的对象。如果对象包含指针类型的成员变量,则拷贝对象和原对象将同时操作同一个变量地址,从而引起错误。这时候就需要改写默认拷贝构造函数实现深拷贝,即创建同类型指针,并将原对象指针所引用的变量值拷贝到新对象指针所指的位置。
5、移动构造函数
如果想要创建一个 Warehouse 对象 large,它的值是 small + mid 的结果,则利用拷贝构造函数:
Warehouse small;
Warehouse mid;
// ... some data inserted into the small and mid objects
Warehouse large{small + mid}; // operator+(small, mid)
// considering declared as friend in the Warehouse class
Warehouse operator+(const Warehouse& a, const Warehouse& b) {
Warehouse sum; // temporary
sum.size_ = a.size_ + b.size_;
sum.capacity_ = a.capacity_ + b.capacity_;
sum.products_ = new Product[sum.capacity_];
for (int ix = 0; ix < a.size_; ++ix) { sum.products_[ix] =
a.products_[ix]; }
for (int ix = 0; ix < b.size_; ++ix) { sum.products_[a.size_ + ix] =
b.products_[ix]; }
return sum;
}
编译器首先会创建临时对象 Warehouse sum, 然后将它作为参数传递给新对象的拷贝构造函数,最后 sum 的内存释放。由于临时变量会被销毁,所以要求拷贝构造函数为深拷贝。利用移动构造函数可以区别对待临时变量,实现对临时变量的浅拷贝,且数据不会因临时变量的销毁而丢失,从而提高了程序的效率。移动构造函数定义如下:
class Warehouse {
public:
// constructors omitted for brevity
Warehouse(Warehouse&& src)
: size_{src.size_},
capacity_{src.capacity_},
products_{src.products_}
{
src.size_ = 0;
src.capacity_ = 0;
src.products_ = nullptr;
}
};
注意,此时移动构造函数参数为右值引用的形式(Warehouse&&),当拷贝对象是右值(不能放在等号左侧的临时变量,字面值等)时会调用移动构造函数。在移动构造函数内部,首先对临时变量 src 进行浅拷贝,然后将src 内部的指针变量改为 nullptr。这样,接下来 src 在调用析构函数时就不会将新对象指针所指的内容销毁。利用移动构造函数后的新对象创建过程如下:
![5e38db24af91e89c2a4be4dc0d9af6e5.png](https://i-blog.csdnimg.cn/blog_migrate/2451eaa9f56d494a930afa29c50eb473.png)
6、算符重载
class Money
{
public:
Money() {}
explicit Money(double v) : value_{v} {}
// construction/destruction functions omitted for brevity
public:
friend constexpr bool operator<(const Money&, const Money&);
private:
double value_;
};
constexpr bool operator<(const Money& a, const Money& b){
return a.value_ == b.value_;
}
算符重载函数是类的友元函数。
三、类的相互联系
1、包含关系
即类的定义包含了其他类的对象,如汽车包含了人和发动机。区别在于,汽车类“必须包含”发动机,而“可以包含”人。
![bc4cbd90f590c7ef0b78a88b748a96c0.png](https://i-blog.csdnimg.cn/blog_migrate/6035280fbd7704152d7dc9e4377f9cb0.png)
2、继承关系
2.1 public 继承关系:
class Rectangle {
public:
// argument checks omitted for brevity
void set_width(int w) { width_ = w; }
void set_height(int h) { height_ = h; }
int area() const { return width_ * height_; }
private:
int width_;
int height_;
};
class Square : public Rectangle {
public:
void set_side(int side) {
set_width(side);
set_height(side);
}
int area() {
area_ = Rectangle::area();
return area_;
}
private:
int area_;
};
public 继承允许子类继承基类的公有和保护成员,且不改变它们的可见性,允许将子类对象转化为基类对象使用。在检查语法规则通过后,编译器会将基类的结构体包含在子类结构体内。当需要将子类对象转化成父类对象时,只需要读取子类对象中父类长度的数据即可(如从 Square 对象中读 sizeof(Rectangle) 长度的数据)。
struct Square {
Rectangle _parent_subobject_;
int area_;
};
void Square_set_side(Square& this, int side) {
// Rectangle_set_width(static_cast<Rectangle&>(this), side);
Rectangle_set_width(this._parent_subobject_, side);
// Rectangle_set_height(static_cast<Rectangle&>(this), side);
Rectangle_set_height(this._parent_subobject_, side);
}
int Square_area(Square& this) {
// this.area_ = Rectangle_area(static_cast<Rectangle&>(this));
this.area_ = Rectangle_area(this._parent_subobject_);
return this.area_;
}
2.2 private 继承(类的默认继承方式)
private 继承允许子类继承基类的公有和保护成员,且将它们作为子类的私有成员,不允许将子类对象转化为基类对象使用。这等价于将父类对象作为子类对象的私有成员变量。
class Square {
public:
void set_side(int side) {
rectangle_.set_width(side);
rectangle_.set_height(side);
}
int area() {
area_ = rectangle_.area();
return area_;
}
private:
Rectangle rectangle_;
int area_;
};
2.3 protected 继承
protected 继承允许子类继承基类的公有和保护成员,且将它们作为子类的保护对象,只允许子类的友元和派生类访问它们。不允许将 protected 继承的子类转化为基类。
2.4 总结
i 当允许用户代码通过子类对象调用基类的公有成员时,用 public 继承。由于这种继承方式 不改变基类成员的可见性,所以允许以基类的方式调用子类对象。
ii 当不允许用户代码和子类的派生类通过子类对象调用基类的公有成员时(例如,我们不希望用 Rectangle类 的行为来改变 Square类 的对象状态),用 private 继承。这种继承方式将基类中公有和保护成员转化为子类的私有成员,所以不允许将子类对象转化为父类对象。否则,对基类公有成员的隐藏就很容易失效了。
iii 当不允许用户代码通过子类对象调用基类公有成员,但允许子类的派生类调用基类的公有和保护成员时,用 protected 继承。这种继承方式将基类中公有和保护成员转化为子类的保护成员。由于这种继承方式保护了基类公有成员不被用户代码可见,所以同样不允许将子类对象转化为父类对象。
3. 动态多态与虚函数
3.1 程序员角度
有时会希望父类和子类共用一个同名成员函数,而各自有不同的实现。这时,就可以在父类中定义虚函数而在子类中重写该函数。
class Musician {
public:
virtual void play() { std::cout << "Play an instrument"; }
};
class Guitarist : public Musician {
public:
void play() override { std::cout << "Play a guitar"; }
};
如果父类函数不需要实现虚函数内容,也可以将其声明为纯虚函数。此时父类就变成了一个抽象类,不能直接定义父类对象。子类必须重写该函数,否则就会出现未定义的错误。
class Musician {
public:
virtual void play() = 0;
};
在子类重写函数时可以使用 override 关键字来强制编译器检查该函数是否为父类的虚函数。
3.2 编译器角度
对于父类 Account:
class Account
{
public:
virtual void cash_out() {
// the default implementation for cashing out
}
virtual ~Account() {}
private:
double balance_;
};
它定义了两个虚函数,cash_out() 和 ~Account()。编译器会将父类转化成如下形式:
struct Account
{
VTable* __vptr;
double balance_;
};
void Account_constructor(Account* this) {
this->__vptr = &Account_VTable;
}
VTable Account_VTable[] = {
&Account_cash_out,
&Account_destructor
};
void Account_cash_out(Account* this) {
// the default implementation for cashing out
}
void Account_destructor(Account* this) {}
可见,父类结构体中添加了 VTable* 类型的变量,它指向一个数组 Account_Vtable[]。该数组的成员为虚函数的函数指针。
对于子类 SavingsAccount:
class SavingsAccount : public Account
{
public:
void cash_out() override {
// an implementation specific to SavingsAccount
}
};
它重写了 cash_out() 虚函数。编译器会将它转化成如下形式:
struct SavingsAccount
{
Account _parent_subobject_;
VTable* __vptr;
};
VTable* SavingsAccount_VTable[] = {
&SavingsAccount_cash_out,
&Account_destructor,
};
void SavingsAccount_constructor(SavingsAccount* this) {
this->__vptr = &SavingsAccount_VTable;
}
void SavingsAccount_cash_out(SavingsAccount* this) {
// an implementation specific to SavingsAccount
}
可见,由于重写了 cashout() 函数,所以子类的虚函数表 VTable 用 SavingsAccount_cashout 函数指针代替了Accountcashout 函数指针,而 ~Account()函数并没有被重写,所以继续使用父类的Accountdestructor 函数指针。重写函数的调用是通过_vptr 调用函数指针实现的。
当创建一个子类对象时,其_vptr变量指向自己的虚函数表,从而会调用重写的子类函数而不是父类函数,实现了动态多态。