DesignPattern 2 工厂模式

工厂顾名思义就是创建产品,根据产品是具体产品还是具体工厂可分为简单工厂模式和工厂方法模式,根据工厂的抽象程度可分为工厂方法模式和抽象工厂模式。该模式用于封装和管理对象的创建,属于创建型模式,其中简单工厂模式不属于GOF23设计模式中。

二、Factory 工厂模式

  • 定义:

    工厂模式(Factory Pattern):在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

  • 场景:

  1. 不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选;
  2. 将new操作简单封装
  3. 需要依赖具体环境创建不同实例,这些实例都有相同的行为
  4. 抽象工厂模式一般用于严格要求以面向对象思想进行开发的超大型项目中

1. 简单工厂模式

简单工厂模式并没有列入到GOF23设计模式中,原因为太简单了,而且违反了开闭原则。

  • 特点

    定义一个创建对象的接口,由工厂类进行实例化,统一管理。

  • 应用场景

    工厂类负责创建的对象比较少;客户端(应用层)只需要知道传入工厂类的参数,对于如何创建对象(逻辑)不关心。

  • 优点

    统一管理, 简单易用

  • 缺点

    新增类的情况只能去修改代码,违反开闭原则,增加系统复杂度

  • 实例

    小米、华为、苹果三个品牌的手机都是在同一个工厂里生产的,且都是同一时间进行生产,那么工厂就可以决定生产哪个品牌的手机了
  • UML 类图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 手机类
class MobilePhone {
public:
virtual void description() = 0; // 纯虚函数
};

// 小米手机类
class Miui : public MobilePhone {
public:
void description() { // 描述信息
cout << "小米手机" << endl;
}
};

// 华为手机类
class Huawei : public MobilePhone {
public:
void description() {
cout << "华为手机" << endl;
}
};

// 苹果手机类
class Iphone : public MobilePhone {
public:
void description() {
cout << "苹果手机" << endl;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 手机生产工厂
class Factory {
public:
// 根据传入来的类型,进行对应的分配对象返回
MobilePhone *manufacturePhone(PHONE_TYPE type) {
switch (type) {
case MIUI:
return (new Miui());
break;
case HUAWEI:
return (new Huawei());
break;
case IPHONE:
return (new Iphone());
break;
default:
return NULL;
break;
}
}
};

void test()
{
// 建立工厂对象
unique_ptr<Factory> factory(new Factory);

// 工厂对象调用manufacturePhone方法给父类指针对象分配对象
unique_ptr<MobilePhone> xiaoMi(factory->manufacturePhone(MIUI));
xiaoMi->description();
unique_ptr<MobilePhone> huaWei(factory->manufacturePhone(HUAWEI));
unique_ptr<MobilePhone> iphone(factory->manufacturePhone(IPHONE));
}

2. 工厂模式

  • 特点

    定义一个创建对象的接口,由工厂类的派生类进行实例化。

  • 场景

    创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口;客户不关心创建产品的细节,只关心产品的品牌。

  • 优点

    解决了简单工厂的缺点

  • 缺点

    类的个数容易过多;每新增一个新产品时就需要增加两个类,导致系统的复杂度增加,不好维护;

  • 实例

    不再只是由一个手机生产的工厂生产手机了,而是每种品牌都有自己的生产工厂,每个品牌的手机生产不会影响到其他手机生产线;增删手机品牌不需要去修改其他手机厂商的代码,符合开闭原则。

  • UML 类图

基于简单工厂实例代码,调整工厂方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 手机生产工厂
class Factory {
public:
virtual MobilePhone *manufacturePhone() = 0; // 纯虚函数
};

// 小米手机生产工厂
class MiuiFactory : public Factory {
public:
MobilePhone *manufacturePhone() {
return (new Miui());
}
};

// 华为手机生产工厂
class HuaweiFactory : public Factory {
public:
MobilePhone* manufacturePhone() {
return (new Huawei());
}
};

// 苹果手机生产工厂
class IphoneFactory : public Factory {
public:
MobilePhone* manufacturePhone() {
return (new Iphone());
}
};

void test()
{
// 定义工厂指针对象
unique_ptr<Factory> factory(new MiuiFactory);
// 工厂对象调用manufacturePhone方法给父类指针对象分配对象
unique_ptr<MobilePhone> xiaoMi(factory->manufacturePhone());
xiaoMi->description();

// 从新给工厂对象分配对象
unique_ptr<Factory> factory2(new HuaweiFactory());
unique_ptr<MobilePhone> huaWei(factory->manufacturePhone());
huaWei->description();
}

三、抽象工厂

抽象工厂模式是工厂方法模式的升级版,不再是局限于生产一种类型的生产工厂。

  • 特点

    为创建一组(有多类)相关或依赖的对象提供创建接口

  • 场景

  1. 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
  2. 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
  • 优点

    解决了工厂模式的缺点,将一个系列的产品族统一到一起创建。

  • 缺点

  1. 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口;
  2. 增加了系统的抽象性和理解难度。
  3. 当系统中只存在一个等级结构的产品时,抽象工厂模式将退化到工厂方法模式。
  4. 当增加一个新的产品族时只需增加一个新的具体工厂,不需要修改原代码,满足开闭原则;当产品族中需要增加一个新种类的产品时,则所有的工厂类都需要进行修改,不满足开闭原则。
  • 实例

    以上面的手机工厂为基础,在拓展出一个升级版的手机生产工厂,手机二代。

  • UML 类图

基于工厂模式代码,新增升级版手机类,调整如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 升级版手机类
class NewMobilePhone {
public:
virtual void newDescription() = 0;
};

// 升级版小米手机类
class NewMiui : public NewMobilePhone {
public:
void newDescription() {
cout << "小米手机二代" << endl;
}
};

// 升级版华为手机类
class NewHuawei : public NewMobilePhone {
public:
void newDescription() {
cout << "华为手机二代" << endl;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 手机生产工厂
class Factory {
public:
virtual MobilePhone *manufacturePhone() = 0; // 纯虚函数

virtual NewMobilePhone *newManufacturePhone() = 0; // 纯虚函数
};

// 小米手机生产工厂
class MiuiFactory : public Factory {
public:
// 生产小米手机(生成小米手机对象返回)
MobilePhone *manufacturePhone() {
return (new Miui());
}

// 生产升级版小米手机(生成小米手机二代对象返回)
NewMobilePhone *newManufacturePhone() {
return (new NewMiui());
}
};

// 华为手机生产工厂
class HuaweiFactory : public Factory {
public:
// 生产华为手机(生成华为手机对象返回)
MobilePhone *manufacturePhone() {
return (new Huawei());
}

// 生产升级版华为手机(生成华为手机二代对象返回)
NewMobilePhone *newManufacturePhone() {
return (new NewHuawei());
}
};

void test()
{
// 定义工厂指针对象
unique_ptr<Factory> factory(new MiuiFactory);
// 生产小米手机
unique_ptr<MobilePhone> xiaoMi(factory->manufacturePhone());
xiaoMi->description();
// 生产小米手机二代
unique_ptr<NewMobilePhone> xiaoMi(factory->newManufacturePhone());
newXiaoMi->newDescription();
}

DesignPattern 1 单例模式

一、Singleton 单例模式

  • 定义:

    单例模式(Singleton Pattern,也称为单件模式),使用最广泛的设计模式之一。单例模式是指在整个系统生命周期内,保证一个类只能产生一个实例,确保该类的唯一性。

  • 使用场景:

    在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动
    程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用
    的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。

  • 特点:

  1. 单例类只有一个实例对象;
  2. 构造函数和析构函数为私有类型,即该单例对象必须由单例类自行创建;
  3. 拷贝构造函数和赋值构造函数是私有类型,保证实例的唯一性;
  4. 单例类对外提供一个访问该单例的全局访问点,一个静态方法。
  • 优点:
  1. 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  2. 保证程序的线程安全,可以避免对资源的多重占用。
  3. 单例模式设置全局访问点,可以优化和共享资源的访问。
  • 缺点
  1. 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背
    开闭原则。
  2. 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  3. 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

1. 懒汉式-线程不安全

线程安全问题仅出现在第一次初始化(new)过程中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Singleton
{
private:
static Singleton* instance;

Singleton() {}; //构造和析构 私有化
~Singleton() {};
Singleton(const Singleton&); //拷贝构造和赋值构造 私有化
const Singleton& operator=(const Singleton&);

public:
static Singleton* getInstance()
{
if (instance == NULL)
instance = new Singleton();

return instance;
}
};

Singleton* Singleton::instance = nullptr;
Singleton* instance_1 = Singleton::getInstance();
Singleton* instamce_2 = Singleton::getInstance();
  • 获取了两次实例,但是只有一个构造函数被调用,即只生成了唯一实例。
  • 可能存在的问题:线程安全:实例化竞争;内存泄漏:只有构造没有析构

2. 加锁的懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SingleInstance_1
{
public:
static SingleInstance_1*& getInstance();
static void deleteInstance();

private:
SingleInstance_1();
~SingleInstance_1();

SingleInstance_1(const SingleInstance_1& signal) = delete;
const SingleInstance_1& operator=(const SingleInstance_1& signal) = delete;

static SingleInstance_1* m_Instance_1;
static std::mutex mutex;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
SingleInstance_1* SingleInstance_1::m_Instance_1 = nullptr;
std::mutex SingleInstance_1::mutex;

SingleInstance_1*& SingleInstance_1::getInstance()
{
if (SingleInstance_1::m_Instance_1 == nullptr)
{
std::unique_lock<std::mutex> lock(SingleInstance_1::mutex);
if (SingleInstance_1::m_Instance_1 == nullptr)
{
SingleInstance_1::m_Instance_1 = new (std::nothrow) SingleInstance_1();
}
}

return m_Instance_1;
}

void SingleInstance_1::deleteInstance()
{
std::unique_lock<std::mutex> lock(SingleInstance_1::mutex);
if (SingleInstance_1::m_Instance_1)
{
delete m_Instance_1;
m_Instance_1 = nullptr;
}
}

SingleInstance_1::SingleInstance_1() { std::cout << "create\n "; };
SingleInstance_1::~SingleInstance_1() { std::cout << "delete\n"; };

3. 加锁的懒汉式 - 智能指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class SingleInstance_2
{
public:
static std::shared_ptr<SingleInstance_2> getInstance();
~SingleInstance_2()
{
std::cout << this << std::endl;
}

private:
SingleInstance_2()
{
std::cout << this << std::endl;
}

SingleInstance_2(const SingleInstance_2& single) = delete;
const SingleInstance_2& operator=(const SingleInstance_2& single) = delete;
};

static std::shared_ptr<SingleInstance_2> m_Instance = nullptr;
static std::mutex mutex;

std::shared_ptr<SingleInstance_2> SingleInstance_2::getInstance()
{
if (m_Instance == nullptr)
{
std::unique_lock<std::mutex> lock(mutex);
if (m_Instance == nullptr)
{
auto temp = std::shared_ptr<SingleInstance_2>(new SingleInstance_2());
m_Instance = temp;
}
}
return m_Instance;
}

4. 推荐的懒汉式 - 静态局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SingleInstance_3
{
public:
~SingleInstance_3() { std::cout << this << std::endl; };
SingleInstance_3(const SingleInstance_3&) = delete;
const SingleInstance_3& operator=(const SingleInstance_3&) = delete;

static SingleInstance_3& getInstance()
{
static SingleInstance_3 m_Instance;
return m_Instance;
}
private:
SingleInstance_3() { std::cout << this << std::endl; };
};

//SingleInstance_3& instance = SingleInstance_3::getInstance()
  • 通过局部静态变量的特性保证了线程安全
  • 不需要使用共享指针,代码简洁
  • 如果采用返回指针实现而不是返回引用实现,无法避免用户使用delete instance导致对象被提前销毁

5. 饿汉式

相比于懒汉式而言,代码一运行就初始化创建实例 ,本身就线程安全

1
2
3
// 对象在程序执行时优先创建
// 源码首行加上
Singeleton* Singeleton::m_Instance = new (std::nothrow) Singleton();

std::nothrow

普通new一个异常的类型std::bad_alloc,机在内存分配失败的时候直接报异常。在内存不足时,new (std::nothrow) 并不抛出异常,而是将指针置NULL,因此 std::nothrow 可以实现对非零指针的检查。

DesignPattern 介绍

一、说明

  • 设计模式(Design pattern): 是软件开发经验的总结,是软件设计中常见问题的典型解决方案。每个模式都像一个蓝图,是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
  • 1995 年,GoF(Gang of Four,四人组)合作出版了《Design Patterns: Elements of Reusable Object-Oriented Software》一书,共收录了 23 种设计模式,从此树立了软件设计模式领域的里程碑,人称【GoF设计模式】。

二、原则

  • 设计模式有7大原则,目的只有一个:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。
设计原则 一句话归纳 目的
开闭原则 对扩展开放,对修改关闭 降低维护带来的新风险
依赖倒置原则 高层不应该依赖低层,要面向接口编程 更利于代码结构的升级扩展
单一职责原则 一个类只干一件事,实现类要单一 便于理解,提高代码的可读性
接口隔离原则 一个接口只干一件事,接口要精简单一 功能解耦,高聚合、低耦合
迪米特法则 不该知道的不要知道,一个类应该保持对其它对象最少的了解,降低耦合度 只和朋友交流,不和陌生人说话,减少代码臃肿
里氏替换原则 不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义 防止继承泛滥
合成复用原则 尽量使用组合或者聚合关系实现代码复用,少使用继承 降低代码耦合

1. 开闭原则

开闭原则(Open Closed Principle,OCP):软件实体应当对扩展开放,对修改关闭

2. 里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP):继承必须确保超类所拥有的性质在子类中仍然成立。

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
  2. 子类中可以增加自己特有的方法;
  3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
  4. 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等

3. 依赖倒置原则

依赖倒置原则(Dependence Inversion Principle,DIP):高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

  1. 每个类尽量提供接口或抽象类,或者两者都具备
  2. 变量的声明类型尽量是接口或者是抽象类
  3. 任何类都不应该从具体类派生
  4. 使用继承时尽量遵循里氏替换原则

4. 单一职责原则

单一职责原则(Single Responsibility Principle,SRP)单一功能原则:规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分

5. 接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP)要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。

  1. 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑
  2. 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法
  3. 了解环境,拒绝盲从。根据具体的环境因素和业务逻辑拆分接口
  4. 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情

6. 迪米特法则

迪米特法则(Law of Demeter,LoD)又叫作最少知识原则:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。

  1. 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
  2. 在类的结构设计上,尽量降低类成员的访问权限。
  3. 在类的设计上,优先考虑将一个类设置成不变类。
  4. 在对其他类的引用上,将引用其他对象的次数降到最低。
  5. 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
  6. 谨慎使用序列化(Serializable)功能。

7. 合成复用原则

合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则:在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循里氏替换原则。

三、思维导图

设计模式根据其意图或目,分为三大类:创建型模式【5个】、结构型模式【7个】、行为型模式【11个】

1. 创建型模式

创建型模式的关注点是“怎样创建对象”,特点是“将对象的创建与使用分离”,使用者不需要关注对象的创建细节,对象的创建由相关的工厂来完成。

创建型模式分为以下5种。

  • 单例(Singleton)模式:类只能产生一个实例,保证全局使用的是同一对象。
  • 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
  • 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
  • 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
  • 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。

其他

参考链接:https://blog.csdn.net/penriver/article/details/118571991

Qt_QFtp

QT - QFtp模块环境部署

由于 QT 官方自 Qt5.0 开始移除了 QFtp 模块,作为一个单独的模块,因此使用时需要下载该模块源码自行编译部署环境。

1. Qt5.12 + LiNUX GCC

1.1 下载源码

地址:https://github.com/qt/qtftp.git
源码目录结构大致如下:

1
2
3
4
5
6
7
└─ qtftp
├─ examples
│ └─ qftp
├─ modules
├─ src
│ └─qftp
└─ tests

1.2 编译源码

  1. 修改 src/qftp/qftp.pro 文件如下:
1
2
3
4
5
6
7
8
...
TARGET = QtFtp
# CONFIG += static
CONFIG += staticlib #生成静态库
# CONFIG -= shared
CONFIG += shared
QT = core network #生成动态库
...
  1. 修改 src/qftp/Headers/qftp.h 文件如下:
1
2
3
4
...
//#include <QtFtp/qurlinfo.h>
#include <qurlinfo.h>
...
  1. 右键 src,点击“构建src”,成功后构建目录文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
├── bin
├── include
│ └── QtFtp
│ ├── headers.pri
│ ├── QFtp
│ ├── qftp.h
│ ├── QtFtp
│ ├── QtFtpDepends
│ ├── QtFtpVersion
│ ├── qtftpversion.h
│ ├── QUrlInfo
│ └── qurlinfo.h
├── lib
│ ├── cmake
│ │ └── Qt5Ftp
│ ├── libQt5Ftp.a
│ ├── libQt5Ftp.la
│ ├── libQt5Ftp.prl
│ ├── libQt5Ftp.so -> libQt5Ftp.so.5.0.0
│ ├── libQt5Ftp.so.5 -> libQt5Ftp.so.5.0.0
│ ├── libQt5Ftp.so.5.0 -> libQt5Ftp.so.5.0.0
│ ├── libQt5Ftp.so.5.0.0
│ ├── libQt5Ftp.so.5.0.0.debug
│ └── pkgconfig
│ └── Qt5Ftp.pc
├── mkspecs
│ ├── modules
│ │ └── qt_lib_ftp.pri
│ └── modules-inst
│ ├── qt_lib_ftp.pri
│ └── qt_lib_ftp_private.pri
└── src

1.3 拷贝文件

  1. 构建目录/lib 下的 libQt5Ftp.a,l ibQt5Ftp.la, libQt5Ftp.prl, libQt5Ftp.so, libQt5Ftp.so.5, libQt5Ftp.so.5.0, libQt5Ftp.so.5.0.0 拷贝至 qt安装目录/gcc_64/lib 目录下;
1
2
3
4
5
6
7
cp lib/libQt5Ftp.a ~/Qt5.12.9/5.12.9/gcc_64/lib
cp lib/libQt5Ftp.la ~/Qt5.12.9/5.12.9/gcc_64/lib
cp lib/libQt5Ftp.prl ~/Qt5.12.9/5.12.9/gcc_64/lib
cp lib/libQt5Ftp.so ~/Qt5.12.9/5.12.9/gcc_64/lib
cp lib/libQt5Ftp.so.5 ~/Qt5.12.9/5.12.9/gcc_64/lib
cp lib/libQt5Ftp.so.5.0 ~/Qt5.12.9/5.12.9/gcc_64/lib
cp lib/libQt5Ftp.so.5.0.0 ~/Qt5.12.9/5.12.9/gcc_64/lib
  1. 构建目录/mkspecs/modules-inst 下的 两个文件拷贝至 qt安装目录/gcc_64/mkspecs/modules/ 目录下;
1
2
cp mkspecs/modules-inst/qt_lib_ftp.pri ~/Qt5.12.9/5.12.9/gcc_64/mkspecs/modules/
cp mkspecs/modules-inst/qt_lib_ftp_private.pri ~/Qt5.12.9/5.12.9/gcc_64/mkspecs/modules/
  1. 构建目录/include 下的 QtFtp 文件夹拷贝至 qt安装目录/gcc_64/include 目录下
1
cp -r include/QtFtp/ ~/Qt5.12.9/5.12.9/gcc_64/include/
  1. 源码/src 目录下的 qftp.h和qurlinfo.h 两个文件覆盖拷贝至 qt安装目录/gcc_64/include/QtFtp 目录下
1
2
cp -f src/qftp/q ~/Qt5.12.9/5.12.9/gcc_64/include/QtFtp/
cp -f src/qftp/qurlinfo.h ~/Qt5.12.9/5.12.9/gcc_64/include/QtFtp/

1.4 项目使用

  1. 在qt项目的 .pro文件添加模块:
1
QT += ftp
  1. 代码里添加对应的头文件:
1
#include <QFtp>

2. QT5.15 + MSVC2019

该编译部署流程与上述基本一致,细节稍有不同。

2.1 下载源码和环境准备

  1. 下载源码:https://github.com/qt/qtftp.git
  2. perl环境安装:https://www.perl.org/

2.2 编译源码

  1. 修改 src/qftp/qftp.pro 文件如下:
1
2
3
4
5
6
7
...
TARGET = QtFtp
CONFIG += static
# CONFIG -= shared
CONFIG += shared
QT = core network #生成动态库
...
  1. 修改 src/qftp/Headers/qftp.h 文件如下:
1
2
3
4
...
//#include <QtFtp/qurlinfo.h>
#include <qurlinfo.h>
...
  1. 右键 src,点击“构建src”,对于 examples 和 tests 工程报错可忽略;

2.3 拷贝文件

  1. 构建目录\lib 下的 Qt5Ftp.dll、Qt5Ftp.lib、Qt5Ftp.prl、Qt5Ftpd.dll、Qt5Ftpd.lib、Qt5Ftpd.prl 拷贝至 qt安装目录\5.15.2\msvc2019_64\lib 目录下;

  2. 构建目录\mkspecs\modules-inst 下的 qt_lib_ftp.pri 和 qt_lib_ftp_private.pri 两个文件拷贝至 qt安装目录\5.15.2\msvc2019_64\mkspecs\modules 目录下;

  3. 构建目录\include 下的 QtFtp 文件夹拷贝至 qt安装目录\5.15.2\msvc2019_64\include 目录下

  4. 源码\src 目录下的 qftp.h和qurlinfo.h 两个文件覆盖拷贝至 qt安装目录\5.15.2\msvc2019_64\include\QtFtp 目录下

2.4 项目使用

同上

《Effective C++》笔记(四)

取自《Effective C++:改善程序与设计的55个具体做法》中文版第三版

四、设计与声明

13:让接口容易被正确使用,不易被误用

欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能会做出什么样的错误。

《Effective C++》笔记(三)

取自《Effective C++:改善程序与设计的55个具体做法》中文版第三版

三、资源管理

13:以对象管理资源

假设一个基类:

1
class Investment { ... };

该类通过一个工厂函数供应特定的对象:

1
Investment* createInvestment(); //返回指针,指向动态分配内存

在调用端使用了该函数返回的对象后,如果忽略了 delete 语句,则会造成内存泄漏。

为确保 createInvestment 返回的资源总是被释放,可将资源放至对象内,以期通过“析构函数自动调用机制”来确保资源释放。对此标准库提供 auto_ptr ,即“类指针对象”,也叫“智能指针”,使用如下:

1
2
3
4
5
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
//调用工厂函数,经由auto_ptr的析构函数自动删除pInv
}

这是实例用来示范“以对象管理资源”的两个关键想法:

  1. 获得资源后立即放进管理对象内。
  2. 管理对象运用析构函数确保资源被释放。

注意,auto_ptr 为防止多个 auto_ptr 指向同一个对象,有一个不寻常的性质:若复制它们,它们会变成 null,复制所得的指针取得资源的唯一拥有权。auto_ptr 的替代方案是“引用技术型智慧指针”(RCSP),RCSP 也是智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源,但是无法打破环状引用。

TRI的 tr1::shared_ptr 就是个 RCSP,使用如下:

1
2
3
4
{
std::tr1::shared_pt<Investment> pInv(createInvestment());
//和auto_ptr使用相似
}

auto_ptr 和 tr1::shared_ptr 两者在析构函数内做的是 delete 而不是 delete[],因此不适合用在动态分配的数组上。

为防止资源泄露,请使用 RAII 对象,它们在构造函数中获得资源并在析构函数中释放资源。
两个常被使用的 RAII classes 分别是 auto_ptr 和 tr1::shared_ptr,后者通常是较佳选择。若选择 auto_ptr,复制动作会使它指向 null。

14:在资源管理类中小心copying行为

假设需要建立一个类用来管理机锁,基本结构由 RAII 守则支配(资源在构造期间获得,在析构期间释放):

1
2
3
4
5
6
7
8
9
10
11
class Lock
{
public:
explicit Lock(Mutex* pm): mutexPtr(pm)
{ lock(mutexPtr); } //获得资源

~Lock()
{ unlock(mutexPtr); } //释放资源
private:
Mutex *mutexPtr;
};

用户的用法符合 RAII 方式:

1
2
3
4
5
Mutex m;
...
{
Lock m1(&m);
}

如果 Lock 对象被复制,如何处理?

  1. 禁止复制。参照条款6,将 copying 操作申明为 private:
1
2
3
4
class Lock: private Uncopyable {
public:
...
};
  1. 对底层资源使用“引用计数法”。利用 tr1::shared_ptr 允许指定所谓的“删除器”实现:
1
2
3
4
5
6
7
8
9
class Lock
{
public:
explicit Lock(Mutex* pm): mutexPtr(pm, unlock)
{ lock(mutexPtr).get; } //以unlock为删除器,不再需要申明析构函数

private:
std::tr1::shared_ptr<Mutex> *mutexPtr;
};
  1. 复制底部资源。进行深度拷贝。
  2. 转移底部资源的拥有权。例如 auto_ptr 将资源的拥有权从被复制物转移到目标物。

复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 的 copying 行为。
常见的 RAII class copying 行为是:抑制 copying、施行引用计数。

15:在资源管理类中提供对原始资源的访问

对于条款13的例子,假设添加一个函数用来处理 Investment 对象,如果传的是 tr1::shared_ptr 对象则会出错,有两种做法可以实现该目标:显式转换和隐式转换。

  1. 显式转换:使用 tr1::shared_ptr 和 auto_ptr 的 get 成员函数来执行显示转换,即返回智能指针内部的原始指针。
  2. 隐式转换:使用其重载的 (operator->和operator*)操作符。

对于需要获取 RAII 对象内的原始资源,可通过添加转换函数的方式实现,见实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
FontHandle getFont();   //C API
void releaseFont(FontHandle fh);

class Font {
public:
explicit Font(FontHandle fh): f(fh) {}
~Font() { releaseFont(f); }

FontHandle get() const { return f; } //显式转换函数
operator FontHandle() condt { return f; } //隐式转换函数
private:
FontHandle f;
}

使用如下:

1
2
3
4
5
6
void changeFontSize(FontHandle f, int newSize); //C API
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize); //使用显式转换函数
changeFontSize(f, newFontSize); //使用显式转换函数

注意,使用隐式转换会增加错误发生机会。

APIs 往往要求原始资源,所以每一个 RAII class 应该提供一个“取得其所管理之资源”的方法;
对原始资源的访问可以经由显式转换或隐式转换。一般而言,显式转换安全些,但隐式转换使用比较方便。

16:成对使用 new 和 delete 时要采用相同形式

1
2
3
4
5
std::string* stringPtr1 = new std::string; 
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1;
delete [] stringPtr2;

对于上述例子,如果对 stringPtr1 采用 “delete []” 形式,可能会多次调用析构函数,释放多余的内存;如果对 stringPtr2 采用 “delete”,可能导致内存释放不完全。

因此调用 new 时使用 [],那么对应的调用 delete 时使用 [];如果调用 new 时没有使用 [],那么对应的调用 delete 时也不该使用 []。对于创建 typedef 类型对象则尤为注意。

如果在 new 表达式中使用 [],必须在相应的 delete 表达式中也使用 [];如果在 new 表达式中不使用 [],不要再相应的 delete 表达式中使用 []。

17:以独立语句将 newed 对象置入智能指针

用一个实例来描述,现假设有以下函数:

1
2
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

此时调用 processWidget(new Widget, priority()); 函数是无法通过编译的,因为构造函数是个 explicit 构造哈桑农户,无法隐式转换,现改写如下:

1
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

此时,编译器在编译 processWidget 前创建代码,完成下述三件事:

  • 调用 priority
  • 执行 new Widget
  • 调用 tr1::shared_ptr 构造函数

然而,C++编译器的工作次序弹性很大,若以下列次序依次完成:

  1. 执行 new Widget
  2. 调用 priority
  3. 调用 tr1::shared_ptr 构造函数

则会出现一种情况带来的资源泄露:编译器在调用 priority 阶段出现异常,从而导致 “new Widget” 返回的指针遗失。该类问题的规避很简单,使用分离语句即可:

1
2
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

以独立语句将 newed 对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。

《Effective C++》笔记(二)

取自《Effective C++:改善程序与设计的55个具体做法》中文版第三版

二、构造/析构/赋值运算

05:了解C++默默编写并调用哪些函数

对于创建一个空类,当C++处理过后,如果没有声明,则编译器会为它声明一个 copy 构造函数、一个 copy assignment 操作符和一个析构函数,此外还有 default 构造函数。因此,对于 class Empty { }; ,等同于如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Empty { 
public:
Empty() { ... } //default 构造函数
Empty(const Empty& rhs) { ... } //copy 构造函数
~Empty() { ... } //析构函数

Empty& operator=(const Empty& rhs) { ... } //copy assignment 操作符
};

//唯有这些函数被需要时才会被编译器创建出来
Empty e1; //default 构造函数
//析构函数
Empty e2(e1); //copy 构造函数
e2 = e1; //copy assignment 操作符

如果类中声明了一个构造函数,则编译器不再为它创建 default 构造函数。

编译器产生的 copy assignment 操作符,一般而言只有当生出的代码合法且有适当机会证明其意义,才会去创建它,否则会拒绝创建。见下实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T>
class NameObject {
public:
NameObject(std::string& name, const T& value);
...
private:
std::string& nameValue; //这是一个reference
const T objectValue; //这是一个const
};

std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p (newDog, 2);
NamedObject<int> s (oldDog, 2);
p = s; //此时p的成员变量发生了什么?

对于 p = s; 的赋值操作,C++的响应是拒绝编译,其一是C++并不允许 “让 reference 改指向不同对象”;再者就是更改 const 成员是不合法的,编译器不知道如何处理;因此,这就需要自己去定义 copy assignment 操作符了。此外,如果一个基类的 copy assignment 操作符声明为 private,编译器就会拒绝生成子类的 copy assignment 操作符,原因则是默认生成的函数无权调用二点权限问题。

对于 “C++并不允许 ‘让 reference 改指向不同对象’ ” 的解释,我所理解的就是,假设程序可以运行的情况下, ‘p = s’ 希望得到的是 p.nameValue 去引用 oldDog,而因为这一条机制的原因,实际得到的为:p.nameValue 引用的对象并没有变更,变更的是 newDog = “Satch”,尽管输出看起来一致,但实现机制不一样。

编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。

06:若不想使用编译器自动生成的函数,就该明确拒绝

对于一个 class,如果不希望其支持某一个功能,只要不声明对应函数即可,但对于系统会自动生成的 copy 构造函数、copy assignment 操作符,则可以采用将其声明为 private 的方式去阻止外部调用,而对于内部调用的情况,可以通过不去定义函数来避免调用。

1
2
3
4
5
6
7
8
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};

对于上述定义,如果尝试拷贝 HomeForSale 对象,则编译器就会提示错误信息。 因为这个机制,可以单独设计一个 base class,然后去继承即可,如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
class Uncopyable {
protected:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale: private Uncopyable {
... //class 不再声明 copy 构造函数和 copy assignment 操作符
};

为驳回编译器自动提供的机能,可将相应的成员函数声明为 private 并且不予实现。使用像 Uncopyable 这样的 base class 也是一种做法。

07:为多态基类声明 virtual 析构函数

1
2
3
4
5
6
7
8
9
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};

class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };

对于上诉计时代码,若设计工厂函数,返回指针指向一个计时对象,如下:

1
2
3
TimeKeeper* ptk = getTimeKeeper();  //从继承体系获取一个动态分配对象
...
delete ptk; //释放资源

然而,getTimeKeeper() 返回的是一个派生类对象(如 AtomicClock),而对象经由基类释放,因C++规则:当派生类对象经由基类指针删除,而基类带有非虚析构函数,其结果未有定义–实际执行导致该对象的派生类成分可能未被销毁,即派生类的析构函数未被执行,从而导致资源泄露等问题。解决方式:给基类一个虚析构函数:

1
2
3
4
5
6
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};

对于不被用于继承的类,则不需要设计虚析构函数。虚函数在运行期间决定被调用是由一个 vptr 指针指出,因此如果一个类含有虚函数,其对象的占用空间也相应的会增加大小,可能会带来不必要的麻烦。

由于很多基类的设计目的并不是用于多态用途,如标准 string 和 STL容器等,不具有虚析构函数,因此应尽量避免用于继承。

如果在一个需要设计抽象类的场合却没有纯虚函数的情况下,可以通过声明纯虚析构函数实现,相应的也需要为这个纯虚析构函数添加定义,如下示例:

1
2
3
4
5
6
class AWOV {
public:
virtual ~AWOV() {} = 0; //声明纯虚析构函数
}

AWOV::~AWOV() {} //定义

polymorphic(带多态性质的)基类应该声明一个虚析构函数;如果类带有任何虚函数,也应该声明一个虚析构函数。
如果一个类的设计不用于基类使用,或者不具备多态性质,则不该声明虚析构函数。

08:别让异常逃离析构函数

C++ 并不禁止析构函数吐出异常,但也不鼓励这样做。比如,对于一个容器 vector 含有多个 Wigdets 对象元素,析构第一个元素期间,有个异常被抛出,此时其他元素仍会被销毁,若第二个析构又抛出异常,则会存在两个异常,对于C++而言,程序若不是结束执行就是导致不明确行为。

如果析构函数必须执行一个动作,而这个动作可能会在失败时抛出异常,怎么办呢?通常有两种方法:

  1. 如果抛出异常就结束程序,通常通过 abort 完成
1
2
3
4
5
6
7
8
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
制作运转记录,记下对 close 的调用失败;
std::abort();
}
}
  1. 吞下因调用 close 而发生的异常
1
2
3
4
5
6
7
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
制作运转记录,记下对 close 的调用失败;
}
}

一个更好的策略是提供一个 close 函数,将调用 close 的责任从 DBConn 析构函数转移到客户手上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DBConn {
public:
...
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if (~closed) {
try {
db.close();
}
catch (...) {
制作运转记录,记下对 close 的调用失败;
... //或吞下异常
}
}
}
}

析构函数绝对不要吐出异常。如果一个析构函数调用的函数可能吐出异常,应捕捉异常后吞下或者结束程序。
如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么类应提供一个普通函数执行该操作。

09:绝不在构造和析构过程中调用 virtual 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Transaction {
public:
Transaction();
virtual void logTransaction() const = 0;
...
}

Transaction::Transaction()
{
...
logTransaction();
}

class BuyTransaction: public Transaction {
public:
virtual void logTransaction() const;
...
};

对于以上示例,如果此时调用 BuyTransaction b;,首先 Transaction 构造函数会被最先调用,而此时 BuyTransaction 的构造函数还未被调用,这时候调用的 logTransaction 函数是 Transaction 内部的版本,而 Transaction 内部版本为纯虚函数,因此程序无法正常执行。唯一避免该问题的做法是:确定构造函数和析构函数都没有调用 virtual 函数。

如何确保每一次 Transaction 继承体系的对象被创建时都会调用 logTransaction 函数呢,一种做法是将该函数改为非虚函数。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const;
...
}

Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo);
}

class BuyTransaction: public Transaction {
public:
BuyTransaction(parameters): Transaction(createLogString(parameters))
{...}
...
private:
static std::string createLogString(parameters);
};

此种方式将 “无法使用virtual函数从基类向下调用” 替换为 “令子类将必要的构造信息向上传递至基类”。

在构造和析构期间不要调用虚函数,因为这类调用不会下降至子类。

10:令 operator= 返回一个 reference to *this

1
2
int x, y, z;
x = y = z = 15; //连锁赋值,被解析为 x = (y = (z = 15));

为了实现类的连锁复制,赋值操作符必须返回一个 reference 指向操作符的左侧实参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Widget {
public:
...
Widget& operator=(const Widget& ths) //返回值是一个引用
{
...
return *this; //返回左侧对象
}

Widget& operator=(int ths) //此函数也适用
{
...
return *this;
}
}

令赋值操作符返回一个 reference to *this。

11:在 operator= 中处理 “自我赋值”

1
2
3
w = w;          //自我赋值
a[i] = a[j]; //潜在的自我赋值
*px = *py; //潜在的自我赋值

如果会运用对象来管理资源,且确定“资源管理对象”在复制时正确,则“自我赋值为安全的”;但是如果自行管理资源,可能会出现“复制前释放资源”的问题,举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Bitmap { ... };
class Widget {
...
private:
Bitmap* pb;
};

Widget& Widget::operator=(const Widget& rhs) //不安全的版本
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}

这里存在的安全性问题为若 *this 和 rhs 指的是同一个对象,如在出现一个指针指向一个被销毁的对象的情况。传统的一个解决做法为在最前面添加一个“证同测试”检验:

1
if (this == &rhs) return *this; //证同测试

但是该方法仍存在一个隐患:假设 new Bitmap 异常,那么始终会有一个指针指向一个被删除的对象。那么从“异常安全性”的方面考虑,可得到方案如下–复制后删除:

1
2
3
4
5
6
7
Widget& Widget::operator=(const Widget& rhs) 
{
Bitmap* pOrig = pb; //记住原pb
pb = new Bitmap(*rhs.pb);
delete pOrig; //删除原pb
return *this;
}

该方案即使 new Bitmap 异常,pb 也会保持原样。另外一种方案是 copy and swap 技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget {
...
void swap (Widget& rhs);
...
};

Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); //制作数据副本
swap(temp); //和副本交换数据
return *this;
}

确保当对象自我赋值时 operator= 有良好行为。
确定任何函数如果操作一个以上的对象,其中多个对象指向同一个对象时,其行为仍正确。

12:复制对象时勿忘其每一个成分

如果设计类没有使用系统自动生成的拷贝函数,而是设计自己的拷贝函数,那么出错时编译器有可能不会提示。如果拷贝函数中忘记写全成员变量,那么其会保持不变。

拷贝函数应该确保复制“对象类的所有成员变量”及“所有基类成分”。
不要尝试某个拷贝函数实现另一个拷贝函数,应该将共同代码放入第三个函数中再调用。

Linux termios 分析

termios是在Posix规范中定义的标准接口,表示终端设备(包括虚拟终端、串口等)。因为串口是一种终端设备,所以通过终端编程接口对其进行配置和控制。

A、termios结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
struct termios
{
tcflag_t c_iflag; /* input mode flags *//* 输入模式标志*/
tcflag_t c_oflag; /* output mode flags *//* 输出模式标志*/
tcflag_t c_cflag; /* control mode flags */ /* 控制模式标志*/
tcflag_t c_lflag; /* local mode flags */ /*区域模式标志或本地模式标志或局部模式*/
cc_t c_line; /* line discipline */ /*行控制line discipline */
cc_t c_cc[NCCS]; /* control characters *//* 控制字符特性*/
speed_t c_ispeed; /* input speed *//* 输入速度 */
speed_t c_ospeed; /* output speed *//* 输出速度 */
#define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
#define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
};

1. c_iflag 标志常量:Input mode (输入模式)

input mode可以在输入值传给程序之前控制其处理的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* c_iflag bits */
#define IGNBRK 0000001 //忽略输入中的 BREAK 状态(忽略命令行中的中断)
#define BRKINT 0000002 //当发生中断时发送SIGINT信号;如果设置了GNBRK,将忽略 BREAK
#define IGNPAR 0000004 //忽略桢错误和奇偶校验错
#define PARMRK 0000010 //标记奇偶校验错误
#define INPCK 0000020 //启用输入奇偶检测
#define ISTRIP 0000040 //去掉输入字符的第8位
#define INLCR 0000100 //将输入的NL转换为CR(将收到的换行符号转换为回车)
#define IGNCR 0000200 //忽略CR。(忽略输入中的回车)
#define ICRNL 0000400 //将输入的CR映射到NL(当输入信号有 CR 时不会终止输入)
#define IUCLC 0001000 //将输入的大写字符转换为小写字符
#define IXON 0002000 //启用输出的 XON/XOFF 流控制。( 启动输出软件流控)
#define IXANY 0004000 //允许字符重新启动输出
#define IXOFF 0010000 //启用输入的 XON/XOFF 流控制。
#define IMAXBEL 0020000 //当输入队列满时响零;Linux 没有实现这一位,总是将它视为已设置。
#define IUTF8 0040000

2. c_oflag 标志常量:Output mode (输出模式)

Output mode主要负责控制输出字元的处理方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* c_oflag bits */
#define OPOST 0000001 //启用具体实现自行定义的输出处理
#define OLCUC 0000002 //将输出中的小写字母转化为大写字母
#define ONLCR 0000004 //将输出中的NL转换为CR
#define OCRNL 0000010
#define ONOCR 0000020 //不在第 0 列输出回车
#define ONLRET 0000040 //不输出回车
#define OFILL 0000100 //发送填充字符作为延时,而不是使用定时来延时
#define OFDEL 0000200 //填充字符是 ASCII DEL (0177)。如果不设置,填充字符则是 ASCII NUL
#if defined __USE_MISC || defined __USE_XOPEN
# define NLDLY 0000400 //新行延时掩码。取值为 NL0 和 NL1
# define NL0 0000000
# define NL1 0000400
# define CRDLY 0003000 //回车延时掩码。取值为 CR0, CR1, CR2, 或 CR3
# define CR0 0000000
# define CR1 0001000
# define CR2 0002000
# define CR3 0003000
# define TABDLY 0014000 //水平跳格延时掩码。取值为 TAB0, TAB1, TAB2, TAB3(或 XTABS)
# define TAB0 0000000
# define TAB1 0004000
# define TAB2 0010000
# define TAB3 0014000
# define BSDLY 0020000 //回退延时掩码。取值为 BS0 或 BS1。(从来没有被实现过)
# define BS0 0000000
# define BS1 0020000
# define FFDLY 0100000 //进表延时掩码。取值为 FF0 或 FF1
# define FF0 0000000
# define FF1 0100000
#endif


#define VTDLY 0040000 //竖直跳格延时掩码。取值为 VT0 或 VT1
#define VT0 0000000
#define VT1 0040000


#ifdef __USE_MISC
# define XTABS 0014000
#endif

3. c_cflag 标志常量:Control mode ( 控制模式)

Control mode主要用于控制终端设备的硬件设置。利用termios结构的c_cflag的标志来加以控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* c_cflag bit meaning */
#ifdef __USE_MISC
# define CBAUD 0010017 //波特率的位掩码 (4+1 位)
#endif
#define B0 0000000 /* hang up *///0波特率(放弃DTR)
#define B50 0000001
#define B75 0000002
#define B110 0000003
#define B134 0000004
#define B150 0000005
#define B200 0000006
#define B300 0000007
#define B600 0000010
#define B1200 0000011
#define B1800 0000012
#define B2400 0000013
#define B4800 0000014
#define B9600 0000015
#define B19200 0000016
#define B38400 0000017
#ifdef __USE_MISC
# define EXTA B19200
# define EXTB B38400
#endif
#define CSIZE 0000060 //数据位的位掩码(字符长度掩码)
#define CS5 0000000 //5个数据位
#define CS6 0000020
#define CS7 0000040
#define CS8 0000060 //8个数据位
#define CSTOPB 0000100 //设置两位停止位,否则为一位
#define CREAD 0000200 //接收使能
#define PARENB 0000400 //校验位使能
#define PARODD 0001000 //使用奇校验而不使用偶校验
#define HUPCL 0002000 //关闭时挂断(放弃DTR)
#define CLOCAL 0004000 //本地链接(不改变端口所有者)
#ifdef __USE_MISC
# define CBAUDEX 0010000
#endif
#define B57600 0010001
#define B115200 0010002
#define B230400 0010003
#define B460800 0010004
#define B500000 0010005
#define B576000 0010006
#define B921600 0010007
#define B1000000 0010010
#define B1152000 0010011
#define B1500000 0010012
#define B2000000 0010013
#define B2500000 0010014
#define B3000000 0010015
#define B3500000 0010016
#define B4000000 0010017
#define __MAX_BAUD B4000000
#ifdef __USE_MISC
# define CIBAUD 002003600000 /* input baud rate (not used) *///输入速度的掩码
# define CMSPAR 010000000000 /* mark or space (stick) parity */
# define CRTSCTS 020000000000 /* flow control *///使能硬件流控制
#endif

4. c_lflag 标志常量:Local mode (局部模式)

Local mode主要用来控制终端设备不同的特色。利用termios结构里的c_lflag的标志来设定局部模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* c_lflag bits */
#define ISIG 0000001 //若收到信号字符(INTR、QUIT等),则会产生相应的信号
#define ICANON 0000002 //终端规范模式,默认为规范模式,否则为非规范模式
#if defined __USE_MISC || (defined __USE_XOPEN && !defined __USE_XOPEN2K)
# define XCASE 0000004 //(Linux 下不被支持)如果同时设置了ICANON,终端只有大写
#endif
#define ECHO 0000010 //启动输入字符的本地本地回显功能
#define ECHOE 0000020 //若设置ICANNO,则允许退格操作
#define ECHOK 0000040 //若设置ICANNO,则KILL字符会删除当前行
#define ECHONL 0000100 //若设置ICANNO, 则允许回显换行符
#define NOFLSH 0000200 //禁止在产生 SIGINT, SIGQUIT 和 SIGSUSP 信号时刷新输入和输出队列
#define TOSTOP 0000400 //向试图写控制终端的后台进程组发送 SIGTTOU 信号
#ifdef __USE_MISC
# define ECHOCTL 0001000 //若设置ECHO,则控制字符(制表符、换行符)会显示成"^X",其中X的ASCII码
等于给相应控制字符的ASCII码加上0x40
# define ECHOPRT 0002000 //若设置ICANNO,字符在删除的同时被打印
# define ECHOKE 0004000 //若设置ICANNO,回显删除一行中的每个字符
# define FLUSHO 0010000 //(Linux 下不被支持)输出被刷新
# define PENDIN 0040000 //(Linux 下不被支持)在读入下一个字符时,输入队列中所有字符被重新输出
#endif
#define IEXTEN 0100000 //启用自定义的输入处理,这个标志必须与 ICANON同时使用
#ifdef __USE_MISC
# define EXTPROC 0200000
#endif

5. c_cc 数组:特殊控制字元可提供使用者设定一些特殊的功能

特殊控制字元主要是利用termios结构里c_cc的阵列成员 来做设定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* c_cc characters */
#define VINTR 0 //中断字符。发出 SIGINT 信号。
#define VQUIT 1 //退出字符。发出 SIGQUIT 信号。
#define VERASE 2 //删除字符。删除上一个还没有删掉的字符,但不删除上一个EOF 或行首。
#define VKILL 3 //终止字符。删除自上一个 EOF 或行首以来的输入。
#define VEOF 4 //文件尾字符。这个字符使得 tty 缓冲中的内容被送到等待输入的用户程序中,而不必等到 EOL。
#define VTIME 5 //非 canonical 模式读时的延时,以十分之一秒为单位
#define VMIN 6 //VMIN :非 canonical 模式读的最小字符数
#define VSWTC 7 //(Linux 下不被支持)开关字符。
#define VSTART 8 //开始字符。重新开始被 Stop 字符中止的输出。
#define VSTOP 9 //停止字符。
#define VSUSP 10 //挂起字符。
#define VEOL 11 //附加的行尾字符。
#define VREPRINT 12 //重新输出未读的字符。
#define VDISCARD 13 //(Linux 下不被支持)开关:开始/结束丢弃未完成的输出。
#define VWERASE 14 //删除词。
#define VLNEXT 15 //字面上的下一个。引用下一个输入字符,取消它的任何特殊含义。
#define VEOL2 16 //另一个行尾字符。

6. 其他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* tcflow() and TCXONC use these */
#define TCOOFF 0
#define TCOON 1
#define TCIOFF 2
#define TCION 3


/* tcflush() and TCFLSH use these */
#define TCIFLUSH 0
#define TCOFLUSH 1
#define TCIOFLUSH 2


/* tcsetattr uses these */
#define TCSANOW 0
#define TCSADRAIN 1
#define TCSAFLUSH 2

B. 相关函数

1. tcgetattr

1
2
/* Put the state of FD into *TERMIOS_P.  */
extern int tcgetattr (int __fd, struct termios *__termios_p) __THROW;

取得终端介质(fd)初始值,并把其值 赋给temios_p;函数可以从后台进程中调用;但是,终端属性可能被后来的前 台进程所改变。

2. tcsetattr

1
2
3
4
/* Set the state of FD to *TERMIOS_P.
Values for OPTIONAL_ACTIONS (TCSA*) are in <bits/termios.h>. */
extern int tcsetattr (int __fd, int __optional_actions,
const struct termios *__termios_p) __THROW;

设置与终端相关的参数 (除非需要底层支持却无法满足),使用termios_p 引用的 termios 结构。optional_actions (tcsetattr函数的第二个参数)指定了什么时候改变会起作用:

  • TCSANOW:改变立即发生
  • TCSADRAIN:改变在所有写入 fd 的输出都被传输后生效。
  • TCSAFLUSH :改变在所有写入 fd 引用的对象的输出都被传输后生效,所有已接受但未读入的输入都在改变发生前丢弃

3. tcsendbreak

1
2
/* Send zero bits on FD.  */
extern int tcsendbreak (int __fd, int __duration) __THROW;

传送连续的 0 值比特流,持续一段时间,如果终端使用异步串行数据传输的话。如果 duration 是 0,它至少传输 0.25 秒,不会超过 0.5 秒。如果duration 非零,它发送的时间长度由实现定义。如果终端并非使用异步串行数据传输,tcsendbreak() 什么都不做。

4. tcdrain

1
2
3
4
5
6
/* Wait for pending output to be written on FD.


This function is a cancellation point and therefore not marked with
__THROW. */
extern int tcdrain (int __fd);

等待直到所有写入 fd 引用的对象的输出都被传输。

5. tcflush

1
2
3
/* Flush pending data on FD.
Values for QUEUE_SELECTOR (TC{I,O,IO}FLUSH) are in <bits/termios.h>. */
extern int tcflush (int __fd, int __queue_selector) __THROW;

丢弃要写入引用的对象,但是尚未传输的数据,或者收到但是尚未读取的数据,取决于 queue_selector 的值:

  • TCIFLUSH:刷新收到的数据但是不读
  • TCOFLUSH:刷新写入的数据但是不传送
  • TCIOFLUSH:同时刷新收到的数据但是不读,并且刷新写入的数据但是不传送

6. tcflow

1
2
3
/* Suspend or restart transmission on FD.
Values for ACTION (TC[IO]{OFF,ON}) are in <bits/termios.h>. */
extern int tcflow (int __fd, int __action) __THROW;

挂起 fd 引用的对象上的数据传输或接收,取决于 action 的值:

  • TCOOFF :挂起输出
  • TCOON :重新开始被挂起的输出
  • TCIOFF :发送一个 STOP 字符,停止终端设备向系统传送数据
  • TCION :发送一个 START 字符,使终端设备向系统传输数据打开一个终端设备时的默认设置是输入和输出都没有挂起。

7. 波特率函数

被用来获取和设置 termios 结构中,输入和输出波特率的值。新值不会马上生效,直到成功调用了 tcsetattr() 函数。设置速度为 B0 使得 modem “挂机”。与 B38400 相应的实际比特率可以用setserial(8) 调整。 输入和输出波特率被保存于 termios 结构中。

1
2
3
4
5
6
7
8
9
10
11
/* Return the output baud rate stored in *TERMIOS_P.  */
extern speed_t cfgetospeed (const struct termios *__termios_p) __THROW;

/* Return the input baud rate stored in *TERMIOS_P. */
extern speed_t cfgetispeed (const struct termios *__termios_p) __THROW;

/* Set the output baud rate stored in *TERMIOS_P to SPEED. */
extern int cfsetospeed (struct termios *__termios_p, speed_t __speed) __THROW;

/* Set the input baud rate stored in *TERMIOS_P to SPEED. */
extern int cfsetispeed (struct termios *__termios_p, speed_t __speed) __THROW;
  • cfgetospeed() 返回 termios_p 指向的 termios 结构中存储的输出波特率
  • cfgetispeed() 返回 termios结构中存储的输入波特率
  • cfsetospeed() 设置 termios_p 指向的 termios 结构中存储的输出波特率为speed
  • cfsetispeed() 设置 termios 结构中存储的输入波特率为 speed

8. 返回值

cfgetospeed()、cfgetispeed() 返回 termios 结构中存储的输入波特率。
其他函数返回 0-成功;-1-失败,并且为 errno 置值来指示错误。

C. 实例:串口

串口是计算机上一种非常通用设备通信的协议,常用PC机上包含的是RS232规格的串口,具有连接线少,通讯简单,得到广泛的使用。

Linux对所有设备的访问是通过设备文件来进行的,串口也是这样,为了访问串口,只需打开其设备文件即可操作串口设备。在linux系统下面,每一个串口设备都有设备文件与其关联,设备文件位于系统的/dev目录下面。如linux下的/ttyS0,/ttyS1分别表示的是串口1和串口2。

在串口编程中,比较重要的是串口的设置,需要设置的部分包括:波特率,数据位,停止位,奇偶校验位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* stty.h 头文件*/
#ifndef __STTY_H__
#define __STTY_H__

//包含头文件

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#include <pthread.h>

// 串口设备信息结构
typedef struct tty_info_t
{
int fd; // 串口设备ID
pthread_mutex_t mt; // 线程同步互斥对象
char name[24]; // 串口设备名称,例:"/dev/ttyS0"
struct termios ntm; // 新的串口设备选项
struct termios otm; // 旧的串口设备选项
} TTY_INFO;
//

// 串口操作函数
TTY_INFO *readyTTY(int id);
int setTTYSpeed(TTY_INFO *ptty, int speed);
int setTTYParity(TTY_INFO *ptty, int databits, int parity, int stopbits);
int cleanTTY(TTY_INFO *ptty);
int sendnTTY(TTY_INFO *ptty, char *pbuf,int size);
int recvnTTY(TTY_INFO *ptty, char *pbuf,int size);
int lockTTY(TTY_INFO *ptty);
int unlockTTY(TTY_INFO *ptty);

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
/* stty.c 源码*/
/*从头文件中的函数定义不难看出,函数的功能,使用过程如下:
(1) 打开串口设备,调用函数setTTYSpeed();
(2) 设置串口读写的波特率,调用函数setTTYSpeed();
(3) 设置串口的属性,包括停止位、校验位、数据位等,调用函数setTTYParity();
(4) 向串口写入数据,调用函数sendnTTY();
(5) 从串口读出数据,调用函数recvnTTY();
(6) 操作完成后,需要调用函数cleanTTY()来释放申请的串口信息接口;
其中,lockTTY()和unlockTTY()是为了能够在多线程中使用。
在读写操作的前后,需要锁定和释放串口资源。 */

#include <stdio.h>
#include <sys/ioctl.h>
#include "stty.h"

// 初始化串口设备并进行原有设置的保存
TTY_INFO *readyTTY(int id)
{
TTY_INFO *ptty;

ptty = (TTY_INFO *)malloc(sizeof(TTY_INFO));
if(ptty == NULL)
return NULL;
memset(ptty,0,sizeof(TTY_INFO));
pthread_mutex_init(&ptty->mt,NULL);
sprintf(ptty->name,"/dev/ttyS%d",id);

// 打开并且设置串口
// O_RDWR 读、写打开
// O_NOCTTY 该参数不会使打开的文件成为该进程的控制终端
// O_NDELAY 这个程序不关心DCD信号线所处的状态,端口的另一端是否激活或者停止
ptty->fd = open(ptty->name, O_RDWR | O_NOCTTY |O_NDELAY);
if (ptty->fd <0)
{
free(ptty);
return NULL;
}

// 取得并且保存原来的设置
tcgetattr(ptty->fd,&ptty->otm);
return ptty;
}


// 清理串口设备资源
int cleanTTY(TTY_INFO *ptty)
{
// 关闭打开的串口设备
if(ptty->fd>0)
{
tcsetattr(ptty->fd,TCSANOW,&ptty->otm);
close(ptty->fd);
ptty->fd = -1;
free(ptty);
ptty = NULL;
}

return 0;
}


// 设置串口通信速率
// ptty 参数类型(TTY_INFO *),已经初始化的串口设备信息结构指针
// speed 参数类型(int),用来设置串口的波特率
// return 返回值类型(int),函数执行成功返回零值,否则返回大于零的值
int setTTYSpeed(TTY_INFO *ptty, int speed)
{
int i;

// 进行新的串口设置,数据位为8位
bzero(&ptty->ntm, sizeof(ptty->ntm));
tcgetattr(ptty->fd,&ptty->ntm);
ptty->ntm.c_cflag = /*CS8 |*/ CLOCAL | CREAD;

switch(speed)
{
case 300:
ptty->ntm.c_cflag |= B300;
break;
case 1200:
ptty->ntm.c_cflag |= B1200;
break;
case 2400:
ptty->ntm.c_cflag |= B2400;
break;
case 4800:
ptty->ntm.c_cflag |= B4800;
break;
case 9600:
ptty->ntm.c_cflag |= B9600;
break;
case 19200:
ptty->ntm.c_cflag |= B19200;
break;
case 38400:
ptty->ntm.c_cflag |= B38400;
break;
case 115200:
ptty->ntm.c_cflag |= B115200;
break;
}
ptty->ntm.c_iflag = IGNPAR;
ptty->ntm.c_oflag = 0;

tcflush(ptty->fd, TCIFLUSH);
tcsetattr(ptty->fd,TCSANOW,&ptty->ntm);

return 0;
}


// 设置串口数据位,停止位和效验位
// ptty 参数类型(TTY_INFO *),已经初始化的串口设备信息结构指针
// databits 参数类型(int), 数据位,取值为7或者8
// stopbits 参数类型(int),停止位,取值为1或者2
// parity 参数类型(int),效验类型 取值为N,E,O,,S
// return 返回值类型(int),函数执行成功返回零值,否则返回大于零的值
int setTTYParity(TTY_INFO *ptty,int databits,int parity,int stopbits)
{
// 取得串口设置
if( tcgetattr(ptty->fd,&ptty->ntm) != 0)
{
printf("SetupSerial [%s]\n",ptty->name);
return 1;
}

bzero(&ptty->ntm, sizeof(ptty->ntm));
ptty->ntm.c_cflag = CS8 | CLOCAL | CREAD;
ptty->ntm.c_iflag = IGNPAR;
ptty->ntm.c_oflag = 0;

// 设置串口的各种参数
ptty->ntm.c_cflag &= ~CSIZE;

//设置数据位数
switch (databits)
{
case 7:
ptty->ntm.c_cflag |= CS7;
break;
case 8:
ptty->ntm.c_cflag |= CS8;
break;
default:
printf("Unsupported data size\n");
return 5;
}

// 设置奇偶校验位数
switch (parity)
{
case n:
case N:
ptty->ntm.c_cflag &= ~PARENB; /* Clear parity enable */
ptty->ntm.c_iflag &= ~INPCK; /* Enable parity checking */
break;
case o:
case O:
ptty->ntm.c_cflag |= (PARODD|PARENB); /* 设置为奇效验*/
ptty->ntm.c_iflag |= INPCK; /* Disnable parity checking */
break;
case e:
case E:
ptty->ntm.c_cflag |= PARENB; /* Enable parity */
ptty->ntm.c_cflag &= ~PARODD; /* 转换为偶效验*/
ptty->ntm.c_iflag |= INPCK; /* Disnable parity checking */
break;
case S:
case s: /*as no parity*/
ptty->ntm.c_cflag &= ~PARENB;
ptty->ntm.c_cflag &= ~CSTOPB;
break;
default:
printf("Unsupported parity\n");
return 2;
}

// 设置停止位
switch (stopbits)
{
case 1:
ptty->ntm.c_cflag &= ~CSTOPB;
break;
case 2:
ptty->ntm.c_cflag |= CSTOPB;
break;
default:
printf("Unsupported stop bits\n");
return 3;
}

ptty->ntm.c_lflag = 0;
ptty->ntm.c_cc[VTIME] = 0; // inter-character timer unused

ptty->ntm.c_cc[VMIN] = 1; // blocking read until 1 chars received

tcflush(ptty->fd, TCIFLUSH);
if (tcsetattr(ptty->fd,TCSANOW,&ptty->ntm) != 0)
{
printf("SetupSerial \n");
return 4;
}

return 0;
}


int recvnTTY(TTY_INFO *ptty,char *pbuf,int size)
{
int ret,left,bytes;
left = size;

while(left>0)
{
ret = 0;
bytes = 0;

pthread_mutex_lock(&ptty->mt);

// 得到缓冲区里有多少字节要被读取,然后将字节数放入bytes里面
ioctl(ptty->fd, FIONREAD, &bytes);
if(bytes>0)
{
ret = read(ptty->fd,pbuf,left);
}
pthread_mutex_unlock(&ptty->mt);
if(ret >0)
{
left -= ret;
pbuf += ret;
}
usleep(100);
}

return size - left;
}


int sendnTTY(TTY_INFO *ptty,char *pbuf,int size)
{
int ret,nleft;
char *ptmp;

ret = 0;
nleft = size;
ptmp = pbuf;

while(nleft>0)
{
pthread_mutex_lock(&ptty->mt);
ret = write(ptty->fd,ptmp,nleft);
pthread_mutex_unlock(&ptty->mt);

if(ret >0)
{
nleft -= ret;
ptmp += ret;
}
//usleep(100);
}

return size - nleft;
}


int lockTTY(TTY_INFO *ptty)
{
if(ptty->fd < 0)
{
return 1;
}

return flock(ptty->fd, LOCK_EX);
}


int unlockTTY(TTY_INFO *ptty)
{
if(ptty->fd < 0)
{
return 1;
}

return flock(ptty->fd, LOCK_UN);
}


#ifdef LEAF_TTY_TEST
///////////////////////////////////////////////////////////////////////////////

// 接口测试

int main(int argc,char **argv)
{
TTY_INFO *ptty;
int nbyte,idx;
unsigned char cc[16];

ptty = readyTTY(0);
if(ptty == NULL)
{
printf("readyTTY(0) error\n");
return 1;
}

lockTTY(ptty);
if(setTTYSpeed(ptty,9600)>0)
{
printf("setTTYSpeed() error\n");
return -1;
}

if(setTTYParity(ptty,8,N,1)>0)
{
printf("setTTYParity() error\n");
return -1;
}

idx = 0;
while(1)
{
cc[0] = 0xFA;
sendnTTY(ptty,&cc[0],1);
nbyte = recvnTTY(ptty,cc,1);
printf("%d:%02X\n",idx++,cc[0]);
}

cleanTTY(ptty);
}
#endif

D. 实例:非堵塞按键监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//用非阻塞io
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

int kbhit(void)
{
struct termios oldt, newt; //termios结构体
int ch;
int oldf;

tcgetattr(STDIN_FILENO, &oldt); //获取标准输入设备(一般是键盘)的文件描述符
newt = oldt;
newt.c_lflag &= ~(ICANON | ECHO); //终端非规范模式,屏蔽整行缓存;输入不回显
tcsetattr(STDIN_FILENO, TCSANOW, &newt); //设置改变立即发生,不需要回车

//设置为非堵塞
oldf = fcntl(STDIN_FILENO, F_GETFL, 0); //获取文件状态标志
fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); //设置文件状态标志为非阻塞I/O

ch = getchar();
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
fcntl(STDIN_FILENO, F_SETFL, oldf);

if(ch != EOF)
{
ungetc(ch, stdin);
return 1;
}
return 0;
}

int main(void)
{
while(!kbhit())
puts("Press a key!");
printf("You pressed '%c'!/n", getchar());
return 0;
}

《Effective C++》笔记(一)

取自《Effective C++:改善程序与设计的55个具体做法》中文版第三版

一、让自己习惯C++

01:视C++为一个语言联邦

将C++视为一个由相关语言组成的联邦,其主要的次语言有四个:

  1. C:以C为基础。
  2. Object-Oriented C++:面向对象设计。
  3. Template C++:泛型编程。
  4. STL:template程序库。
  • C++高效编程守则视状况而变化,取决于使用C++的哪一部分。

02:尽量以const,enum,inline替换#define

  1. const
1
2
#define ASPECT_RATIO 1.653      //宏
const double AspectRatio 1.653 //常量

该宏定义,记号名称 ASPECT_PATIO 有可能没有进入记号表,运用该常量出现错误时可能不会直接指示该名称;此外,预编译器替换名称可能导致目标码出现多个1.653;采用常量定义没有这两个问题;

对于class专属常量,确保此常量只有一份实体,需要成为static成员;#define 无法创建class专属常量,实例如下:

1
2
3
4
5
6
7
class GamePlayer {
private:
static const int NumTurns = 5; //声明常量
int score[NumTurns]; //使用该常量
...
}
const int GamePlayer::NumTurns; //定义
  1. enum

有的旧编译器不支持声明时赋值,然而上述的 GamePlayer::score数组声明需要一个常量值,这时可采用enum替换:

1
2
3
4
5
6
class GamePlayer {   
private:
enum { NumTurns = 5 }
int score[NumTurns]; //使用该常量
...
}

注意,取一个 const 的地址合法,但是取一个 enum 或 #define 的地址则不合法。

  1. inline
1
2
3
4
5
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

int a = 5, b = 0;
CALL_WITH_MAX(++a, b); //a累加二次
CALL_WITH_MAX(++a, b+10); //a累加一次

对于该示例,宏不是函数,不会有函数调用带来的额外开销;宏中所有的实参需要加上小括号;对于使用宏带来的问题,可采用 template inline实现,如下:

1
2
3
4
5
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
  • 对于单纯常量,最好以 const 对象或 enunms 替换 #define。
  • 对于形似函数的宏,最好改用 inline 函数替换 #define。

03:尽可能使用const

  1. const 指针
1
2
3
4
char greeting[] = "Hello";
const char* p = greeting; //指针指向的值是常量
char* const p = greeting; //指针是常量
const char* const p = greeting;

const 允许指定一个语义约束,而编译器会强制实施该约束,使某值保持不变;

STL迭代器的作用类似 T* 指针,如果声明为const表示该指针的常量,如果使得指向的值为常量,需使用const_iterator,如下:

1
2
3
4
5
6
std::vector<int> vec;
...
const std::vector>int::iterator iter = vec.begin();
++iterz; //错误,iter是常量指针
std::vector>int::const_iterator cIter = vec.begin();
*vIter = 10; //错误,cIter指向的值是常量
  1. const 成员函数

将const作用于成员函数,可使class接口易于理,且使“操作const对象”成为可能;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CTextBlock {
public:
...
std::size_t length() const;
private:
char* pText;
std::size_t textLength; //最近一次计算的文本区块长度
bool lengthIsValid; //目前的长度是否有效
};

std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); //错误,const成员函数内
lengthIsValid = true; //不能赋值给 textLength、lengthIsValid
}
return textLength;
}

上诉问题的解决办法可添加 mutable,如下:

1
2
3
4
5
6
7
8
9
class CTextBlock {
public:
...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength; //成员变量可能总被变更
mutable bool lengthIsValid;
};

对于是否带有 const 可实现重载,对于出现的代码重复,可通过转型避免。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TextBlock {
punlic:
...
const char& operator[] (std::size_t position) const
{
...
return text[Position];
}
char& operator[] (std::size_t position)
{
return const_cast<char&>( //移除const
static_cast<const TextBlock&>(*this) //加上const
[position]
);
}
...
};

该代码有两个转型动作,第一个,将原始类型 TextBlock& 转换为 const TextBlock&,从而得以调用 const operator[];第二次则从返回值中移除const。对于反向做法:令 const 版本调用 non-const 版本,则是一种错误行为,存在安全性风险。

  • 将某些东西声明为 const 可帮助编译器侦测出错误用法。
  • 编译器强制实施 bitwise constness,但编写程序应使用“概念上的常量性”。(实在无法理解原文中这些概念的描述)
  • 当 const 和 non-const 成员函数等价时,令 non-const 调用 const 版本可避免代码重复。

04:确定对象被使用前已被初始化

1
2
3
4
5
inx x = 0;                              //int手工初始话
const char* text = "A C-style string"; //指针手工初始化

double d;
std::cin>>d; //以读取 input stream 的方式完成初始化

读取未初始化的值会导致不明确的行为,通常 C part of C++ 不保证发生初始化;

对于内置类型以外的东西,初始化通过构造函数实现,但需要分清赋值和初始化,如下实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class PhoneNumber { ... };
class ABEntry {
public:
ABEntry(const std::string& name, const std::list<PhoneNumber>& Phones);
private:
std::string theNumber;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};

//此种方式为赋值操作
ABEntry::ABEntry(const std::string& name, const std::list<PhoneNumber>& Phones)
{
theNumber = name;
thePhones = Phones;
numTimesConsulted = 0;
}

//此种方式为初始化操作(使用成员初值列)
ABEntry::ABEntry(const std::string& name, const std::list<PhoneNumber>& Phones)
:theNumber(name),
thePhones(Phones),
numTimesConsulted(0)
{}

赋值的构造函数实际为先调用默认构造函数,再进行赋值操作,因此使用成员初值列来初始化的方式要高效的多;对于内置对象如 numTimesConsulted,则没有区别。
C++的成员初始化顺序往往是固定的,以其声明的顺序被初始化,与初值列的顺序无关;此外,对于如果一个成员变量的初始化需要依赖另外一个成员变量,如数组的长度,则那个成员变量需要先有初值。

函数内的 static 对象被称为 local static 对象,其他的 static 对象被称为 non-local static 对象,程序结束时 static 对象会自动销毁。编译单元是指产出单一目标文件的那些源码。
问题:某一个编译单元某个 non-local static 对象的初始化用到了另一个编译单元的 non-local static 对象,它所用到的这个对象可能尚未被初始化。
这里采用的设计是将每个 non-local static 对象搬到自己的专属函数内,该对象被声明为 static,函数返回一个指向该对象的指针,然后用户调用这些函数,而不是这些对象,即 non-local static 对象 被 local static 对象替换了。这类函数称为 reference-returning 函数。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//第一个编译单元
class FileSystem { ... };
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}

//第二个编译单元
class Directory { ... };
Directory::Directory(param)
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir()
{
static Directory td;
return td;
}

然而,这种写法使得在多线程中带有不确定性,一种处理方法是在单线程启动阶段手工调用所有 reference-returning 函数;另外需要的则是加强设计以避免这类情况。

为内置对象进行手工初始化,因为 C++ 不保证初始化它们。
构造函数最好使用成员初值列,而不是在构造函数内使用赋值操作。
为避免“跨编译单元之初始化次序”问题,请以 local static 对象代替 non-local static 对象。

Aseprite 编译安装

A. Windows编译 环境准备

1. 安装Visual studio

  • 下载地址:https://visualstudio.microsoft.com/zh-hans/downloads/
  • Community版本即可,组件需要 [使用C++的桌面开发]
  • 需要能找到 Common7\Tools\VsDevCmd.bat 文件,默认安装路径 C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat

    注:将安装语言改为english,否则编译完成后容易出现无法安装

2. 安装Cmake

3. 下载Ninja

4. 下载Skia

5. 下载 Aseprite 源码

B. Windows编译 编译安装

打开cmd输入指令工作

  1. 在 Aseprite 源码目录下创建 build 文件夹
    1
    2
    3
    cd Aseprite
    mkdir build
    cd build
  2. 编译指令,注意替换三处路径
    1
    2
    3
    call "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat" -arch=x64

    cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DLAF_BACKEND=skia -DSKIA_DIR="D:\Skia" -DSKIA_LIBRARY_DIR="D:\Skia\out\Release-x64" -DSKIA_LIBRARY="D\Skia\out\Release-x64\skia.lib" -G Ninja ..
  3. 安装
    1
    ninja aseprite
    成功后可以在 Aseprite\build\bin 目录下找到该exe文件,可发送快捷键至桌面
    重新编译的话删掉改 build 目录即可

C. Linux编译

D. 其他

  1. 中文包:https://github.com/J-11/Aseprite-Simplified-Chinese/releases
  2. 官方安装指南:https://github.com/aseprite/aseprite/blob/main/INSTALL.md