road to master

零、学习计划

  • 数据库相关
    • 索引
      • 我以为我对数据库索引很了解,直到我遇到了阿里面试官 - 知乎 (zhihu.com)
      • 给我一分钟,让你彻底明白MySQL聚簇索引和非聚簇索引 - 知乎 (zhihu.com)
      • 聚集索引(聚类索引)与非聚集索引(非聚类索引) - 知乎 (zhihu.com)
      • (2 封私信 / 6 条消息) 索引是什么? - 知乎 (zhihu.com)
    • 存储引擎
      • 一张图让你看懂InnoDB (360doc.com)
    • 事务
    • 解析、优化、执行等过程
    • mysql脏页是什么 - 知乎 (zhihu.com)
  • 数据结构
    • 数据库相关结构体:B树、B+树、红黑树…
    • 如何系统地学习数据结构与算法? - 知乎 (zhihu.com)
    • 红黑树,超强动静图详解,简单易懂_吴师兄学算法 (cxyxiaowu.com)
    • 完全二叉树的节点数,你真的会算吗? (qq.com)
  • 算法
    • leecode564. 寻找最近的回文数-困难题目 - 知乎 (zhihu.com)
  • 网络通信
    • 15w+字的计算机网络知识核心总结!再也不怕面试官问我网络知识了,飘了!_小林coding的技术博客_51CTO博客
    • 突击大厂面试,图解网络开放下载! (qq.com)
    • 答应我,这次搞懂 I/O 多路复用!_小林coding的博客-CSDN博客
    • 无锁编程基础与代码实现 - 知乎 (zhihu.com)
    • SQL Server里的闩锁介绍 - Woodytu - 博客园 (cnblogs.com)
  • 性能相关
    • 面试官:如何写出让 CPU 跑得更快的代码_51CTO博客_写代码吃cpu吗
    • TLB miss与cache miss
      • TLB缓存是个神马鬼,如何查看TLB miss? - 知乎 (zhihu.com)
      • Cache原理简介及cache miss_cache miss rate_hithj_cainiao的博客-CSDN博客
  • 计算机相关
    • 浮点数的二进制表示 - 阮一峰的网络日志 (ruanyifeng.com)
    • 内存管理
      • Linux内存管理:linux内存管理—虚拟地址、逻辑地址、线性地址、物理地址的区别(一)_鱼思故渊的博客-CSDN博客
      • Linux内存管理 – /proc/{pid}/smaps讲解 - 简书 (jianshu.com)
      • Linux 内存管理 - 简书 (jianshu.com)
  • 优秀博主
    • 小林coding的博客_CSDN博客-图解计算机网络,图解操作系统,图解MySQL领域博主

一、计算机相关

1. cache miss

2. TLB miss

二、Linux

1. 基础命令

1.1 so相关
# 查看.so导出函数
nm -D **.so(可加grep)
objdump -tT **.so
# 查看哪些so导出了指定函数
find / -name "*.so" | xargs nm -AD | grep "T usleep"
1.2 文件文本相关
# 搜索多个文件里面的字符串
grep -niR check_argument build/*.sh
find -type f -name "*.sh" |xargs grep "check_arguments"
# 查找匹配行,及显示它上下n行
cat xxx.txt |grep str -C 10
1.3 vi命令
# 字符串替换
:%s/gmdb-kvlite/GMDB-Lite/g

:%s/GmcYangTreeNodeT/GmcYangNodeT/g
1.4 压缩、解压相关
# tar.gz 压缩
tar -zcvf xxx.tar.gz xxx/
# rpm 解压
rpm2cpio xxx.rpm | cpio -di
1.5 查看进程、线程
# 查看线程
ps -T -p `pidof fwmd`
1.6 远程scp
scp local_file remote_username@remote_ip:remote_file

2. C编程

2.1 占位符的使用
数据类型 格式化符号 说明
uint64_t %llu/%lu 在x86下,使用%llu,在x86_64下,对应%lu,不同场景,尽量使用一致的打印
uint32_t %u 为了保持写法一致,推荐使用PRI*系列
int64_t %lld/%ld 同上
int32_t %d 同上
size_t %zu sizeof、strlen返回都是size_t,需使用%zu,否则在不同平台上容易编译报错
枚举变量 对枚举增加强转,再使用匹配的类型打印 在C标准对枚举的长度没有准确定义,甚至是有符号还是无符号可能都有不一致。复杂,打印它的格式化转换,需要注意
枚举常量 根据枚举值选择,如有符号用%d,无符号用%u 常量编译后,直接在代码段,不用强转
long 编译跨平台的软件时尽量不要使用long类型,或者需要对long类型做特殊处理

3. GLIBC版本

查看GLIBC版本号

# libc.so打印信息中包含有版本号
/lib64/libc.so.6
# lib.so通常支持多个版本,即向前兼容,查看该文件中包含的字符串可以看到其支持的版本,通常是连续的
strings /lib64/libc.so.6 |grep GLIBC
# ldd是glibc提供的命令
ldd --version
# getconf GNU_LIBC_VERSION
getconf GNU_LIBC_VERSION

编码中的预编译宏,判断版本号

#if __GLIBC__ >= 2 && __GLIBC_MINOR__ >= 30

4. 共享内存

# 清理共享内存
ipcrm -a
# 查看共享内存
ipcs

5. 进程状态

状态 描述
O 进程正在处理器运行(这个状态很少见)
S 休眠状态(sleep)
R 进程处于运行或就绪状态
I 空闲状态(idle)
Z 僵尸状态(zombie)
T 跟踪状态(traced)
B 进程正在等待更多的内存页
D 不可中断的深度睡眠

三、shell

$用法

# 命令替换(括号或者反引号)
$(pwd)
$`pwd`

# 变量替换($var和${var}本质上相同,但后者会精确变量名的范围)
A=hello
A_1=$AB        # A_1值为空,因为没有名字为AB的变量
A_2=${A}B    # A_2值为helloB

# 其他用法
$0    # shell本身文件名
$?    # 最后运行命令的返回值
$!    # shell最后运行的后台process的PID
$$    # shell本身的pid
$#    # shell的参数个数
$@    # 所有参数列表, 以"$1 $2 … $n"的形式输出所有参数,此选项参数可超过9个
$*    # 所有参数列表, 以"$1" "$2" … "$n" 的形式输出所有参数
$1    # 表示第一个参数,$2表示第二个参数,$n表示第n个参数
  • $@$* 的区别

    当他们不被 “” 包含时,他们之间没有任何区别,都是将所有参数当成一份数据,彼此之间用空格分隔;

    当它们都被 “” 包含时,区别如下:

    • "$*"会将所有的参数从整体上看做一份数据,而不是把每个参数都看做一份数据
    • "$@"仍然将每个参数都看作一份数据,彼此之间是独立的

set命令

Linux set命令用于设置shell的执行方式,可根据不同需要来做设置

以下只列举常用的几个,具体百度:

set -e    # 若指令执行结果不为0,立即结束shell
set -f    # 取消使用通配符
set -t    # 执行完随后的指令,立即退出shell
set -v    # 显示shell所读取的输入值

变量(字符串)

# 变量声明
  # 直接赋值
  NAME=proj-name
  # 通过命令替换赋值
  CUR_DIR=$(cd "$(dirname $0)"; pwd)    # $()用来命令替换,$0表示当前shell脚本的文件名
  # 变量使用,通过变量给变量赋值
  OUTPUT_DIR="$CUR_DIR"/output
  
# 变量替换($var和${var}本质上相同,但后者会精确变量名的范围)
A=hello
A_1=$AB        # A_1值为空,因为没有名字为AB的变量
A_2=${A}B    # A_2值为helloB

# 获取字符串长度
echo ${#A}

# 提取子字符串
echo ${A:0:2}    # 输出 he

运算符

# 关系运算符 -eq(相等) -ne(不等) -gt(大于) -lt(小于) -ge(大于等于) -le(小于等于)
if [ $a -eq $b ]; then
    echo $a
fi
  
# 逻辑运算符 ! && ||
if [ !$a && $b ]

# 字符串运算符
  # = 检测两个字符串是否相等
  if [ $a = $b ]
  # != 检测两个字符串是否不相等
  if [ $a != $b ]
  # -z 检测字符串长度是否为0
  if [ -z $a ]
  # -n 检测字符串长度是否不为0
  if [ -n $a ]
  # $ 检测字符串长度是否为空
  if [ $a ]
  
# 文件测试运算符
file="/var/test.sh"
if [ -r $file ]
  # -b 检测是否为块设备文件
  # -c 检测是否为字符设备文件
  # -d 检测是否为目录
  # -f 检测是否为普通文件
  # -g 检测是否设置了SGID位
  # -k 检测是否设置了粘着位(Sticky Bit)
  # -p 检测是否为有名管道
  # -u 检测是否设置了SUID位
  # -r 检测是否可读
  # -w 检测是否可写
  # -x 检测是否可执行
  # -s 检测是否为空
  # -e 检测是否存在
  # -S 检测是否为socket

流程控制

# 退出shell
exit 0

文件包含

shell脚本可以包含外部的脚本,包含后可以使用被包含脚本的函数和变量

# 用 . 的方式包含外部脚本
. ./test.sh
# 用source的方式包含外部脚本
source ./test.sh

参数解析

# 调用脚本传入参数
./test.sh -a aarch64 -b dev -t release -v 2.0.1
# test.sh文件中的参数解析
arch=''; branch=''; type=''; version=''; date=''; command='';
while getopts ":a:b:c:d:t:v:" opt
do
    case "$opt" in
        a)
        arch="$OPTARG"
        ;;
        b)
        branch="$OPTARG"
        ;;
        c)
        command="$OPTARG"
        ;;
        d)
        date="$OPTARG"
        ;;
        t)
        type="$OPTARG"
        ;;
        v)
        version="$OPTARG"
        ;;
        ?)
        echo "Unknown Parameters."; exit 1;
        ;;
    esac
done

四、UML类图

类图、接口图

类图

类图通常分为三层。

第一层:表示类的名称,如果是抽象类,用斜体表示

第二层:表示类的特性,通常就是字段和属性

第三层:表示类的操作,通常就是方法或行为

前面符号的含义

‘+’ :public

‘-’ :private

‘#’ :protected

接口图

接口图和类图的区别是顶端有<>显示。

接口图通常分为两层。

第一层:表示接口名称

第二层:表示接口方法

此外,接口还有另外一种表示方法,俗称棒棒糖表示法。

UML六种关系

UML分别有以下六种关系:实现、泛化、组合、聚合、关联、依赖。

关系紧密程度:组合 > 聚合 > 关联 > 依赖 > 泛化 = 实现

实现

表示类与接口之间的关系,指类实现了该接口。

虚线 + 空心三角形 表示。

泛化

表示类与类之间的关系,指类之间的父子关系,一个类继承了另一个类。

实线 + 空心三角形 表示。

组合 & 聚合

组合与聚合都是表示部分与整体的关系,它们的区别如下:

聚合

表示一种弱的拥有关系,即A对象可以包含B,但B不是A的一部分,可以脱离A而存在。

例子:雁群(A)与大雁(B),雁群可以包含大雁,但大雁不是雁群的一部分,它脱离雁群也能存在。

表示方法:空心菱形 + 实线箭头

组合

表示一种强的拥有关系,即A对象的组成结构中包含B,A和B的生命周期一样。

例子:大雁(A)与翅膀(B)就是组合关系。

表示方法:实心菱形 + 实线箭头

关联

表示类与类之间的关系,指A类中拥有B类。

  • 例子:企鹅(A类)需要知道天气(B类)情况,所以A类中包含B类。
  • 表示方法:用 实线箭头 来表示。
依赖

表示类与类之间的关系,指A类的方法依赖B类。

  • 例子:动物(A类)在活动(方法)时需要氧气和水(B类),这样就形成了依赖关系。
  • 表示方法:用 虚线箭头 来表示。

五、设计模式

六大设计原则

单一原则(SRP)

一个类只专注于做一件事,应该有且仅有一个原因引起类的变更。即高内聚,低耦合。

里氏替换原则(LSP)

所有引用父类的地方必须能透明地使用其子类的对象。属于代码规范

子类必须完全实现父类的抽象方法,但不得重写父类的非抽象方法。

子类中可以增加自己的特有方法。

子类可以重载父类方法,但不能覆盖。

子类实现抽象方法时,返回值可以是父类返回值的子类。

依赖倒置原则(DIP)

定义

高层模块不应该依赖底层模块,两者都应该依赖其抽象;

抽象不应该依赖细节;

细节应该依赖抽象。

具体实现

模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,依赖关系是通过接口或抽象列产生;

接口或抽象类不依赖于实现类;

实现类依赖接口或抽象类。

接口隔离原则(ISP)

只给客户端提供其需要的接口,把不需要的剔除掉。

迪米特原则

一个对象应该对其他对象有最少的了解。

你的内部是如何实现和我没关系,我只需要知道你提供的public内容,其他的一概不关心。

开闭原则(OCP)

对拓展开放,对修改关闭。

重要性

开闭原则是最基础的一个原则,前面介绍的所有原则都是开闭原则的具体形态,而开闭原则才是其底层逻辑。

创建型模式

结构型模式

装饰模式

场景

当系统需要新功能的时候,是向旧的类中添加新的代码。这些新加的代码通常装饰了原有类的核心职责或主要行为,从而增加了主类的复杂度,装饰模式提供了一个非常好的解决方案。它把每个要装饰的功能放在单独的类中,并让这个类包装它所要装饰的对象。

概念

动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。

模式解析

  1. 装饰模式利用SetComponent来对对象进行包装,这样每个装饰对象的实现就和如何使用这个对象分离开了,每个装饰对象只关心自己的功能,不需要关心如何被添加到对象链当中。

UML类图解析

Component

定义一个对象接口,可以给这些对象动态地添加职责。

ConcreteComponent

定义了一个具体的对象,也可以给这个对象添加一些职责。

Decorator(装饰抽象类)

继承了Component,从外类来拓展Component类的功能。但对于Component来说,是无需知道Decorator的存在的。

ConcreteDecorator(装饰对象类)

定义了具体的装饰类,起到给Component添加职责的功能。

优点:

  • 把类中的装饰功能从类中搬移去除,简化了原有的类
  • 有效地把类的核心职责和装饰功能区分开了,而且可以去除相关类中重复的逻辑

行为模式

策略模式

场景

在可能遇到多种不同算法或者策略的情形,根据实际业务逻辑来变化使用的算法,这时可以根据上下文来决定使用哪种算法。

概念

策略模式就是一种定义了一系列的算法,将每个算法封装起来,并可以相互替换。策略模式让算法独立于它的客户而变化,策略模式是一种对象行为型模式

模式解析

  1. 策略模式是对算法的封装,把算法的责任和算法本身分开,委派给不同的对象管理。
  2. 策略模式中,由客户端决定在什么情况下使用什么策略。
  3. 策略模式仅仅封装算法,提供新算法到系统中,以及老算法从系统中“退休”。

UML类图解析

Context(环境类)

环境类是使用算法的角色。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。

Strategy(抽象策略类)

为所有支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。

StrategyA(具体策略类)

实现了抽象策略中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类的对象。

优点:

  • 完美支持“开闭原则”,用户可以在不修改原有系统的基础上选择算法或策略, 也可以灵活地增加或删除。
  • 提供了管理算法族的办法,易于管理,且可避免重复代码。
  • 使用策略者模式可以避免多重条件选择语句,多重条件选择语句是硬编码,不易维护。

缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。所以策略模式只适用于客户端知道所有的算法或行为的情况,同时也加重了客户端的使用难度。
  • 策略模式将造成系统产生很多具体策略类,任何消息的变化都会导致系统增加一个新的具体策略类。
  • 无法同时在客户端使用多个具体策略类

模式对比

策略模式和状态模式的类图相似。

六、锁

基础锁 – 互斥锁、自旋锁

互斥锁和自旋锁是最底层的两种锁,很多高级的锁都是基于他们实现的,可以认为是各种锁的地基。

比如:读写锁既可以选择用互斥锁实现,也可以用自旋锁实现。

区别
  • 互斥锁:加锁失败后,线程会释放CPU,给其他线程(发生线程切换)
  • 自旋锁:加锁失败后,线程会忙等,直到它拿到锁(忙等)
互斥锁

互斥锁是一种独占锁,比如当A线程加锁成功后,只要线程A没有释放锁,线程B加锁就会失败,于是会释放CPU给其他线程,最终线程B由于一直加锁失败,释放CPU,最终被阻塞。

对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为睡眠状态,等锁被释放后,内核会在合适的时间唤醒线程,当这个线程成功获取到锁后,继续执行。

所以,互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮我们切换线程,虽然简化了锁的难度,但是存在一定的性能开销成本(在加锁失败和锁被释放时,分别会有一次上下文切换,如果代码执行的时间很短,可能上下文切换的时间都比锁住的代码执行时间还长)。

所以,如果能确定被锁住的代码执行时间很短,就不应该用互斥锁,而选择自旋锁

自旋锁

自旋锁是通过CPU提供的CAS函数(Compare And Swap),在用户态完成加锁和解锁的操作,不会主动产生上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。

加锁过程:

  • 查看锁状态,如果锁是空闲的,执行第二步
  • 将锁设置为当前线程持有

CAS函数把这两个步骤合并成一条硬件级指令,形成原子指令,这样保证了这两个步骤的原子性。

自旋锁是比较简单的一种锁,一直自旋,利用CPU周期,直到锁可用。需要注意:在单核CPU上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程),否则自旋锁在单核CPU上无法使用,因为自旋的线程永远不会释放CPU。

读写锁

读写锁由读锁和写锁构成,只读取共享资源用读锁(共享锁),修改共享资源用写锁(独占锁)。

使用场景:能明确区分读操作和写操作的场景。

读优先锁&写优先锁

介绍

读优先锁:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取读锁。

写优先锁:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。

缺点

不管是优先读锁还是优先写锁,都有可能出现把非优先的一方饿死的情况,为了解决这个问题,可以搞个公平读写锁,不偏袒任何一方。

公平读写锁

介绍

用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现饿死的现象。

乐观锁&悲观锁

前面提到的互斥锁、自旋锁、读写锁,都属于悲观锁。

悲观锁

悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,很容易出现冲突,所以访问共享资源前,先要上锁。(相反,如果多线程同时修改共享资源的概率比较低,可以采用乐观锁)

乐观锁

乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间有没有发生冲突,如果没有其他线程在修改资源,那么操作完成;如果发现有其他线程已经修改过资源,就放弃本次操作。

其实,乐观锁全程并没有加锁,所以它也叫无锁编程

在线文档编辑、git、svn等都使用了乐观锁。

乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景下,才考虑使用乐观锁

七、代码坏味道

坏味道概述

直观

一眼过去就可以看到的问题,比如:

  • 魔鬼数字
  • 函数/类过长
  • 圈复杂度高
  • 函数/变量命名不规范等

建议:规范性的问题,尽量通过工具去扫描

微观

需要仔细检查才能发现的问题,比如:

  • 类字段定义不合理
  • 函数功能不单一
  • 变量作用域过长等

建议:代码局部层面问题,重点排查,优先关注

宏观

代码架构上的整体的问题,比如:

  • 类职责不单一
  • 上帝类
  • 分层不清楚
  • 上下文混乱等

建议:需求本身设计,类定义,类结构关键设计问题,这些问题需要结合业务和架构发展

设计原则 - 简单设计四个基本原则

  • 通过所有测试:软件系统对外部需求被正确的完成,包括功能性需求和非功能性需求,并通过客户验收的标准
  • 尽可能消除重复:让软件走向高内聚,低耦合,达到良好正交性的过程(并不是所有的重复都可以消除,这条原则被描述为最小化重复,而不是消除重复)
  • 尽可能清晰表达:漂亮的代码如同优美的散文,从不隐藏设计者的意图,恰如其分的抽象,直截了当的控制代码被阅读的次数远远大于其修改的次数
  • 更少的代码元素:尽可能降低设计的复杂度,保持简单

坏味道

重复代码

现象

在多个地点上看到同样的程序结构

解决方法

  • 提炼函数
  • 函数上移:两个互为兄弟的子类中有重复代码,提取公共函数,到父类中
  • 塑造模板函数:部分代码相似,细节不同。先提取函数,再制造模板方法模式,把差异部分交给子类实现
  • 提炼类:两个毫不相关的类出现重复,提取到一个类中
过长函数

现象

函数体过长,复杂度高,难理解,后续修改容易引入问题

解决办法

  • 提炼函数:将重复代码放到一个函数中,并让函数名清晰的解释函数的用途
  • 以查询取代临时变量:程序中将表达式结果放到临时变量中,可以将这个表达式提炼到一个独立函数中,调用这个新函数去替换这个临时表达式,这个新函数就可以被其他函数调用
  • 引入参数对象:将参数封装到一个对象中,以一个对象取代这些参数
  • 分解条件表达式:将if else等语句的条件表达式提炼出来,放到独立的函数中去
  • 保持对象完整:从某个对象里取出若干值,将其作为某次函数调用时的参数,由原来传递参数改为传递整个对象
  • 以函数对象取代函数:大型函数中有许多参数和临时变量,将函数放到一个单独对象中,局部变量和参数就变成了对象内的字段,然后可以在同一个对象中将这个大型函数分解为许多小函数
过多的注释

现象

一段代码有长长的注释,然后发现,这些注释之所以存在是因为代码很糟糕

解决方法

注释应该是解释why,而不是how和what,代码告诉你how,而注释告诉你why

你感觉需要撰写注释,请先尝试重构,试着让所有注释都变得多余

  • 提炼函数
  • 重命名函数
夸夸其谈未来性

现象

过度关注未来可能的变化,增加了不必要的东西:

  • 在理解需求时主观的认为需求变动非常大,在设计过程中出现过度的设计
  • 追求设计模式的使用,经常对程序不必要的地方进行设计模式的使用,导致代码不易理解
  • 程序的设计过程中封装变化混乱,没有将封装变化进行到底。最后,过滤考虑了程序的未来性,但这些未来性并不明确

解决办法

  • 如果某个抽象类其实没有太大的作用,请运用折叠继承关系
  • 非不要的委派(delegation)可与用inline class除掉
  • 如果函数的某些参数未被用上,可删除
  • 如果函数名称带有多余的抽象意味,可修改函数名,让它现实一些
过大的类

现象

一个类过于臃肿,出现太多实例变量。容易产生重复代码,维护困难,代码复用性变差

解决办法

  • 提炼类:建立一个新类,将相关的函数和字段从旧类搬移到新类
  • 提炼子类:一个类中的某些特性只能被一部分实例使用到,可以新建一个子类,将只能由一部分实例用到的特性转移到子类中
  • 提炼接口:两个类中的接口有相同的部分,此时可以将相同的子集提炼到一个独立接口中
  • 复制被监视数据
冗余类

现象

某个类原本对得起自身的价值,但重构使它身形缩水,不再做那么多工作;或开发者事前规划了某些变化,并添加一个类来应付这些变化,但变化实际没有发生

解决办法

  • 折叠继承关系:某个子类并未带来该有的价值,因此需要把类折叠起来
  • 将类内联化:如果一个类不再承担足够责任、不再有单独存在的理由,就用内联将其塞进去
过长参数列表

现象

函数有太多的参数(常常同时存在过长函数、数据泥团、基本类型偏执等其他坏味道)

解决办法

  • 一个参数可通过另一个参数查到时,可使用“以查询取代参数”
  • 多个参数属于同一个数据结构时,可传入数据结构的对象,以保持对象完整
  • 多个参数有关联,总是同时使用,可以引入参数对象
  • 某个参数是空标记用于区分函数行为,可移除标记参数
  • 多个函数有相同的参数,实际上是围绕这些参数工作,可以将多个函数组合成类
发散式变化

现象

某个class经常因为不同的原因在不同的方向上变化

解决办法

  • 提炼类:建立一个新类,将相关的函数和字段从旧类搬移到新类
霰弹式修改

现象

如果每遇到某种变化,都必须在许多不同的classes内做小修改。需要修改的代码散布四处,不但很难找到它们,也很容易忘记某个重要的修改

解决办法

主要思路是将功能集中到一起,常用到以下手段:

  • 搬移函数、搬移字段、搬移语句到函数等搬移特性的重构手法
  • 如果本身架构层次上不应该分开,可使用内联函数、内联类、移除子类等手法将模块合并到一起
依恋情节

现象

函数对某个class的兴趣高过对自己所处host class的兴趣。

解决办法

  • 搬移函数:在该函数最长引用的class中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将就函数完全移除。
  • 提炼函数
数据泥团

现象

两个类中的相同值域、或许多函数签名中的相同参数。这些"总绑在一起出现的数据"真应该放进属于他们自己的对象中。

解决办法

  • 提炼类:建立一个新类,将相关的函数和字段从旧类搬移到新类
  • 引入参数对象:将参数封装到一个对象中,以一个对象取代这些参数
  • 保持对象完整:从某个对象取出若干值,将其作为某次函数调用是的参数,由原本传递参数改为传递整个对象
基本类型偏执

现象

使用大量基本数据类型,而不是将数据定义成数据结构

解决办法

  • 以对象取代数据值:将数据值变成对象
  • 以类取代型别码:如果带着一个有意义的符号名,type code的可读性还是不错的。问题在于,符号名终究只是个别名,编译器看见的、进行型别校验的,还是背后那个数值
  • 以子类取代型别码:它把“对不同行为的了解”从class用户那儿转移到了class自身。如果需要再加入新的行为变化,我这是需要添加subclass就行了。如果没有多态机制,就必须找到所有条件式,并逐一修改它们
switch惊悚现身

现象

switch语句是根据类型码进行的多分支选择语句,写出短小的switch语句很难,即便只有两种条件的switch语句也比我们想要的单个代码块或函数大得多。

解决办法

如果是面向对象语言,大多数情况可以用多态替换:

  • 提取函数:将switch语句提取到一个函数中
  • 搬移函数:将它移到需要多态性的类中
  • 通过 子类/状态/策略 中的一种取代类型码
  • 最后以多态取代条件表达式

如果是面向过程语言:

  • 如果case分支超过5个,为了使代码看起来更简洁,考虑用表驱动的方式
  • 如果可以预见类型码会不断新增,考虑采用策略模式,将控制和处理分离,可以提高扩展性,符合开闭原则
平行继承关系

现象

当为某个class增加一个subclass,必须也为另一个class增加一个subclass。如果发现某个继承体系的名称前缀和另一个继承体系的名称前缀完全相同,那么就是平行继承关系

解决办法

让一个继承体系的实例引用另一个继承体系的实例。如果再接再厉运用搬移函数和搬移字段,就可以将引用端的继承体系消弭于无形

令人迷惑的临时字段

现象

有时你会看到这样的对象:其内某个变量仅为某种特定场景而设,或者只在该对象某一段声明周期内生效

解决办法

提炼类:把这些变量和其相关函数提炼到一个独立class中,提炼后的新对象将是一个method object

过度耦合的消息链

现象

如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后在请求另一个对象…这就是消息链

解决办法

  • 隐藏委托关系
  • 提炼函数:观察消息链最终得到的对象是用来干什么的,可以的话将使用该对象的代码提炼到一个独立函数中
中间人

现象

某个类接口有一半的函数都委托给其他类,这样就是过度运用。

解决办法

  • 移除中间人:直接跟真正负责的对象打交道
  • 将函数内联化:如果这样的函数只有少数几个,可以用内联把他们放进调用端
  • 以继承取代委托:如果这些中间人还有其他行为,可以用继承取代委托,把它变成实责对象的子类
狎昵关系

现象

有时你会看到两个类过于亲密,花费太多时间去探究彼此的私有数据

解决办法

  • 搬移函数、搬移字段:帮他们划清界限,减少狎昵行径
  • 将双向关联改为单向
  • 提炼类:把两者共同点提炼到一个新的类
异曲同工的类

现象

两个函数做同一件事,却有着不同的签名

解决办法

  • 重命名函数:根据他们的用途重新命名
  • 搬移函数:将某些行为植入类,直到两者的协议一致为止
不完美的库类

现象

库构造得不够好,不能让我们修改其中的类使它完成我们希望完成的工作

解决办法

纯稚的数据类

现象

Data Class是指,它们拥有一些字段,以及用于访问这些字段的函数,除此之外一无长处

解决办法

  • 封装字段:类的数据如果有public字段,将其封装
  • 封装集合:类中如果有容器类的字段,将其封装
  • 移除setting函数:对于不该被其他类修改的字段,移除设置函数
被拒绝的遗赠

现象

继承某个类的子类,并不需要父类的某些函数、数据或不需要实现父类实现的接口

解决办法

  • 下移方法、下移字段:让子类拥有只有子类需要的方法或字段
  • 以委托代替继承:如果子类复用了父类的方法,却不愿意支持父类的接口,考虑用此方法

八、编译相关

先由cmake工具,输出标准的构建文件:makefile或project文件,然后再由make构建出最终的执行文件。

CMAKE

简介

CMake是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的makefile或者project文件,能测试编译器所支持的C++特性,类似UNIX下的automake。

CMake 的组态文件取名为 CMakeLists.txt,所有语句都写在这里面,当CMakeLists.txt文件确定后,直接使用cmake命令运行,就能得到输出文件。

Cmake 并不直接建构出最终的软件,而是产生标准的建构文件(如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然后再按照一般的建构方式使用。

执行流程
# 1. 用于配置编译选项,一般不需要配置,但如果第2步出现错误,就需要执行此步
$ ccmake dirName
# 2. 根据CMakeLists.txt生成Makefile,一般跳过第1步直接执行此步
$ cmake dirName
# 3. 执行Makefile文件,编译程序,生成可执行文件
$ make
CMakeLists.txt

MAKE

编译so

#  将test.c编译成libtest.so
gcc -fPIC -shared test.c -o libtest.so

九、Git相关

基本概念

  • workspace:工作区(就是电脑能看到的目录)
  • stage area:暂存区/缓存区(.git目录下的index文件中)
  • local repository:版本库/本地仓库(.git目录)
  • remote repository:远程仓库
  • HEAD:一个指向当前工作中的本地分支的指针,可以当做当前分支的别名

仓库操作

# 初始化仓库
git init

# 克隆仓库到本地
  # 通用
  git clone ssh://[email protected]:2222/Gauss/gmdb-kvlite.git
  # 克隆指定分支
  git clone -b branchName url

提交、修改

# 添加文件到缓存区
  # 通用
  git add *
  # 把所有文件加入缓存区,根据.gitignore过滤(-u表示只添加有更新的文件)
  git add . -u

# 提交改动到本地仓
  # 通用
  git commit
  # 继续上一次的提交
  git commit --amend    # 如果上一次已经push过,那么这次push要用 git push -f

查看状态

# 查看改动
git diff

# 查看当前分支状态
  # 通用
  git status
  # 查看当前分支状态(只看被追踪的文件)
  git status -uno

撤销操作

# 放弃文件更改(工作区),撤销修改、撤销文件删除
git checkout -- xxx.cpp

# 回退代码到指定版本
git reset xxxx

# 清除所有未跟踪文件
git clean -dfx .

git reset

用于回退版本,可以指定退回某一次提交的版本

不止可以回退到之前的版本,还可以回到之后的版本(比如有A->B->C三个版本,当前从C回退到A版本,可以直接从A回到C版本)

参数:

  • –mixed:只保留工作目录的改动,将缓存区和本地仓库回退,默认
  • –soft:保留工作目录和缓存区的改动,将本地仓库回退
  • –hard:没有保留,将工作区、缓存区和本地仓库都回退
  • 注意:慎用–hard参数,它会删除回退点之前的所有信息
# 回退到上一个版本(mixed)
git reset HEAD
# 回退所有内容到上一版本
git reset HEAD^
# 回退指定文件的版本到上个版本
git reset HEAD^ filename
# 回退到指定版本
git reset --soft 052e
# 回退到上上上个版本
git reset --hard HEAD~3
# 将本地的状态回退到和远程一样
git reset --hard origin/master
git rm
git mv
git fetch

用于拉代码,fetch + merge = pull

# 拉取远程仓origin的master分支
git fetch origin master
git remote

用于操作远程仓库

# 显示所有远程仓库(-v显示出详细信息)
git remote -v
# 显示指定远程仓库的信息
git remote show url
# 添加远程仓库(reponame是本地的仓库)
git remote add reponame url
# 删除远程仓库
git remote rm reponame
# 修改仓库名
git remote rename old_name new_name
git rebase
# cherry-pick两个commit点
git cherry-pick xxx1
git cherry-pick xxx2
# rebase到cherry-pick之前的commit点
git rebase -i xxx0
# 之后进入到第一个修改"git文件"页面,将不需要的commit点标题前面的"pick"改为"s"
# 然后进入到第二个修改"git文件"页面,将不需要的commit点的标题删掉,保留一个即可
# 推到远程仓
git push -f nixing dev_V5R3_RD:dev_V5R3_RD

日志操作

git log
git reflog

设置命令

# 设置大小写敏感
git config --global core.ignorecase false

分支操作

# 查看所有分支
git branch -a
# 查看当前分支
git branch
# 切换分支
git checkout branchName
# 删除分支
git branch -d branchName
# 强制删除分支
git branch -D branchName

标签说明

标签名 说明
feature 新功能(feature)开发
fix bug修复
refactor 重构代码、优化配置&参数、优化逻辑及功能
test 添加单元测试用例相关
docs 添加文档/注释等
style 不修改业务逻辑下,仅做代码规范的格式化
chore 构建脚本变动
release 发布版本,用于开发分支到release分支的同步

git mm

基本工作流程(需安装git mm)

# 1. 初始化git仓
git mm init -u ssh://[email protected]:2222/DCP_Industry_ServiceRouter/manifest.git -b br_V8R21C00_main_c30001215_B271_gmdb
# 2. 同步代码
git mm sync build fullcode/cmake fullcode/manifest fullcode/router_base
# 3. start
git mm start br_V8R21C00_main_c30001215_B271_gmdb
# 4. 修改文件后,先 git add 和 git commit,然后 git mm upload

十、单位转换

时间转换

秒(s) 毫秒(ms) 微秒(us) 纳秒(ns)
1 1000 1000000 1000000000
1 1000 1000000
1 1000

存储转换

MB KB B bit
1 1024 1024 * 1024 1024 * 1024 * 8

你可能感兴趣的:(学习)