文章目录
- 类图
- 泛化
- 实现
- 关联
- 聚合
- 组合
- 依赖
- 总结
- 类内部的三种权限(公有、保护、私有)
- 类的三种继承方式
- 描述与图
- 总结
- 面向对象七大原则
- 单一职责原则(Single Responsibility Principle)
- 里氏替换原则(Liskov Substitution Principle)
- 依赖倒置原则(Dependence Inversion Principle)
- 接口隔离原则(Interface Segregation Principle)
- 迪米特法则(Law Of Demeter)
- 开闭原则(Open Close Principle)
- 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
- 关于类的静态成员
- 类的静态成员函数
- 关于C++构造函数的继承问题
- 创建型模式
- Factory模式(工厂模式)
- AbstractFactory模式(抽象工厂模式)
类图
泛化
- 【泛化】是一种继承关系,表示一般与特殊的关系,它指定了子类如何继承父类的所有特征和行为。例如:老虎是动物的一种,即有老虎的特性也有动物的共性。
- 【代码体现】:继承。
- 特征:空白三角形+实线 指向父类
实现
-
【实现关系】是一种类与接口的关系,表示类是接口所有特征和行为的实现。
-
特征:空白三角形箭头+虚线 箭头指向接口
-
【代码体现】:纯虚函数
-
对于C++,其接口类一般具有以下特征:
- 最好不要有成员变量,但可以有静态常量(static const或enum)
- 如果成员变量,尤其是可变的成员变量,定义在接口中,等于是把实现细节暴露出来了,不符合接口定义的要求,所以一般不在接口中定义可变的成员变量。
而常量可以定义在接口中,因为有时接口需要返回状态,而这些状态可以定义成常量放在接口中。
- 如果成员变量,尤其是可变的成员变量,定义在接口中,等于是把实现细节暴露出来了,不符合接口定义的要求,所以一般不在接口中定义可变的成员变量。
- 要有纯虚接口方法
- 由于不能让接口类自身能够实例化,并且需要子类必须实现接口暴露的方法,所以接口方法都要声明成纯虚函数。
声明成纯虚函数意味着接口类自身不需要提供方法的定义,方法的定义需要由接口类的子类提供,并且接口类自身也因此变成了抽象类而不能被实例化。
- 由于不能让接口类自身能够实例化,并且需要子类必须实现接口暴露的方法,所以接口方法都要声明成纯虚函数。
- 要有虚析构函数,并提供默认实现
- 在使用接口类的指针访问接口类的子类的实例时,当对接口类的指针做delete时,如果接口类的析构函数不是虚析构函数的话,将只会调用接口类(父类)的析构函数,接口类的子类的析构函数将不会被调用,内存泄露将会产生,所以接口类的析构函数必须定义成虚析构函数。
- 如果接口类的析构函数不提供默认实现,即如果接口类的析构函数是纯虚析构函数的话,接口类的子类将被迫必须提供析构函数的实现,这样对接口类的子类不友好。
- 不要声明构造函数
- 不要显式定义任何的构造函数,但也不要在接口中加入如下代码来禁止生成构造函数:
Testable() = delete; Testable(const Testable&) = delete;
- 因为C++的调用机制要求子类的构造函数调用时一定会先调用父类的构造函数,如果禁止生成父类构造函数,代码编译时会报错。如果程序员不显式的提供构造函数,编译器也会隐式的加上构造函数的,虽然这些构造函数对于接口类来说实际没有什么意义。
- 例子:
- 最好不要有成员变量,但可以有静态常量(static const或enum)
class Testable
{
public:
static const int START = 1; // #1
static const int STOP = 2;
virtual void test() = 0; // #2: 接口方法
virtual ~Testable() {}; // #3: 从C++11开始可以: virtual ~Testable() = default;
}
关联
- 【关联关系】是一种拥有的关系,它使一个类知道另一个类的属性和方法;如:老师与学生,丈夫与妻子关联可以是双向的,也可以是单向的。双向的关联可以有两个箭头或者没有箭头,单向的关联有一个箭头。
- 【代码体现】:成员变量
- 特征:实线 n n或 实线 1 n 箭头指向被拥有者
- 上图中,老师与学生是双向关联,老师有多名学生,学生也可能有多名老师。但学生与某课程间的关系为单向关联,一名学生可能要上多门课程,课程是个抽象的东西他不拥有学生。
聚合
- 【聚合关系】:是整体与部分的关系,且部分可以离开整体而单独存在。如车和轮胎是整体和部分的关系,轮胎离开车仍然可以存在。
- 【聚合关系】是关联关系的一种,是强的关联关系;关联和聚合在语法上无法区分,必须考察具体的逻辑关系。
- 特征:菱形+实线+箭头
组合
-
【组合关系】:是整体与部分的关系,但部分不能离开整体而单独存在。如公司和部门是整体和部分的关系,没有公司就不存在部门。
-
组合关系是关联关系的一种,是比聚合关系还要强的关系,它要求普通的聚合关系中代表整体的对象负责代表部分的对象的生命周期。
依赖
- 【依赖关系】:是一种使用的关系,即一个类的实现需要另一个类的协助,所以要尽量不使用双向的互相依赖.
- 【代码表现】:局部变量、方法的参数、对静态方法的调用、函数返回值
- 【箭头及指向】:带箭头的虚线,指向被使用者。
总结
各种关系的强弱顺序:
泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖
下面这张UML图,比较形象地展示了各种类图关系:
类内部的三种权限(公有、保护、私有)
权限按照以下两点递减:
- 是否能被外部访问
- 是否能被继承的子类访问
类的三种继承方式
描述与图
-
public继承方式
基类中所有 public 成员在派生类中为 public 属性;
基类中所有 protected 成员在派生类中为 protected 属性;
基类中所有 private 成员在派生类中不能使用。 -
protected继承方式
基类中的所有 public 成员在派生类中为 protected 属性;
基类中的所有 protected 成员在派生类中为 protected 属性;
基类中的所有 private 成员在派生类中不能使用。 -
private继承方式
基类中的所有 public 成员在派生类中均为 private 属性;
基类中的所有 protected 成员在派生类中均为 private 属性;
基类中的所有 private 成员在派生类中不能使用。
继承方式/基类成员 | public成员 | protected成员 | private成员 |
---|---|---|---|
public继承 | public | protected | 不可见 |
protected继承 | protected | protected | 不可见 |
private继承 | private | private | 不可见 |
总结
- 父类的private成员在子类不能使用、不可见
- 父类的成员在子类中的权限,最高为其子类的继承方式。
面向对象七大原则
单一职责原则(Single Responsibility Principle)
- 每一个类应该专注于做一件事情。
- 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;提高类的可读性,提高系统的可维护性;变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
里氏替换原则(Liskov Substitution Principle)
- 子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
- 使用里氏替换原则时需要注意,子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。
依赖倒置原则(Dependence Inversion Principle)
- 程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
- 面向抽象编程,也就是面向抽象类或接口编程。
接口隔离原则(Interface Segregation Principle)
应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。
迪米特法则(Law Of Demeter)
又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。
开闭原则(Open Close Principle)
面向扩展开放,面向修改关闭。
组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
尽量使用合成/聚合达到复用,尽量少用继承。原则: 一个类中有另一个类的对象。
关于类的静态成员
-
类的静态成员与类本身直接相关,而不是与类的各个对象保持关联。
-
我们不能在类的内部初始化静态数据成员,必须在类的外部初始化。
-
字面值常量类型constexpr除外
类的静态成员函数
/*类静态成员、静态成员函数例子如下*/
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
class Guest{
private:
static int m_num;
static int m_count;
static double m_totalFee;
string name;
int num;
double fee;
public:
Guest(string name, double fee):name(name),fee(fee)
{
m_num++;
num = m_num;
m_count++;
m_totalFee+=fee;
}
~Guest()
{
m_count--;
}
void show() const{
printf("num:%d,name:%s,fee:%.2lf\n",num,name.c_str(),fee);
}
static int GetCount()
{
return m_count;
}
static double GetTotalIncome(){
return m_totalFee;
}
};
int Guest::m_num = 0;
int Guest::m_count = 0;
double Guest::m_totalFee = 0;
int main(int argc, char *argv[])
{
Guest g1("lihua",11.99), g2("zhangsan",12.99), g3("xiaomei",13.99);
g1.show();
g2.show();
g3.show();
printf("count:%d,totalFee:%.2f\n",Guest::GetCount(),Guest::GetTotalIncome());
return 0;
}
关于C++构造函数的继承问题
- 构造方法用来初始化类的对象,与父类的其它成员不同,它不能被子类继承(子类可以继承父类所有的成员变量和成员方法,但不继承父类的构造方法)。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,子类需要调用其父类的构造方法。
- 如果没有显式的构造函数,编译器会给一个默认的构造函数,并且该默认的构造函数仅仅在没有显式地声明构造函数情况下创建。
- 如果子类调用父类带参数的构造方法,需要用初始化父类成员对象的方式。
#include <iostream.h>
class animal
{
public:
animal(int height, int weight) //有且仅有 有参参数,必须显性调用
{
cout<<"animal construct"<<endl;
}
…
};
class fish:public animal
{
public:
fish():animal(400,300)
{
cout<<"fish construct"<<endl;
}
…
};
void main()
{
fish fh;
}
- 在fish类的构造函数后,加一个冒号(:),然后加上父类的带参数的构造函数。这样,在子类的构造函数被调用时,子类就会去调用父类的带参数的构造函数去构造对象。
创建型模式
Factory模式(工厂模式)
//Product.h
#ifndef _PRODUCT_H_
#define _PRODUCT_H_
class Product
{
public:
virtual ~Product() = 0; //多态时,调用子类的析构函数
protected:
Product(); //不可被外部调用,抽象基类不可以被实例化
private:
};
class ConcreteProduct:public Product
{
public:
~ConcreteProduct();
ConcreteProduct();
protected:
private:
};
#endif
//Product.cpp
#include "Product.h"
#include <iostream>
Product::Product()
{
std::cout<<"Product()...."<<std::endl;
}
Product::~Product()
{
std::cout<<"~Product()...."<<std::endl;
}
ConcreteProduct::ConcreteProduct()
{
std::cout<<"ConcreteProduct()...."<<std::endl;
}
ConcreteProduct::~ConcreteProduct()
{
std::cout<<"~ConcreteProduct()...."<<std::endl;
}
//Factory.h
#ifndef _FACTORY_H_
#define _FACTORY_H_
class Product;
//factory抽象基类
class Factory
{
public:
virtual ~Factory() = 0; //析构函數
virtual Product* CreateProduct() = 0;
protected:
Factory(); //构造函数,外部不可以调用,子类可以调用
private:
};
//factory实例
class ConcreteFactory:public Factory
{
public:
~ConcreteFactory();
ConcreteFactory();
Product* CreateProduct();
protected:
private:
};
#endif
//Factory.cpp
#include "Product.h"
#include "Factory.h"
#include <iostream>
Factory::Factory()
{
std::cout<<"Factory()...."<<std::endl;
}
Factory::~Factory()
{
std::cout<<"~Factory()...."<<std::endl;
}
ConcreteFactory::ConcreteFactory()
{
std::cout<<"ConcreteFactory()......"<<std::endl;
}
ConcreteFactory::~ConcreteFactory()
{
std::cout<<"~ConcreteFactory()......"<<std::endl;
}
Product* ConcreteFactory::CreateProduct()
{
return new ConcreteProduct();
}
//main.cpp
#include "Factory.h"
#include "Product.h"
#include <iostream>
#include <cstdio>
int main(int argc, char* argv[])
{
Factory *fac = new ConcreteFactory();
Product* p = fac->CreateProduct();
delete p;
delete fac;
return 0;
}
#makefile
CC = g++ -g
BaseIncludePath = ../include/
SRCS = main.cpp Product.cpp Factory.cpp
OBJS = $(addsuffix .o,$(basename ${SRCS}))
main.exe: $(OBJS)
$(CC) $(OBJS) -o main.exe
%.o: %.cpp
$(CC) -c $< -o $@ -I$(BaseIncludePath)
clean:
del $(OBJS) main.exe
AbstractFactory模式(抽象工厂模式)
-
抽象工厂模式和工厂方法模式一样,都符合开闭原则。但是不同的是,在抽象工厂模式中,增加一个产品族很容易,而增加一个产品等级结构却很难,工厂模式则反之。
-
也就是说,在抽象工厂模式中,增加一个具体的工厂很容易,但是你想在工厂中多生产一种产品,就需要修改很多个类,会违背开闭原则,这种情况下应该使用工厂模式。
-
简单来说:工厂模式新增产品类型容易,抽象工厂模式新增工厂类型容易
表头 | 表头 |
---|---|
单元格 | 单元格 |
单元格 | 单元格 |