Effective C++ - Implementations

2023-11-18

前言:实现中需要注意的一些问题。


1 尽可能延后变量的定义

只要你定义了一个变量,而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便要承受构造成本;当这个变量离开作用域时,便要承受析构成本。

例子:

方法A

Widget w;
for (int i = 0; i < n; ++i) {
    w = 取决于i的某个值;
    // ...
}

方法B

for (int i = 0; i < n; ++i) {
    Widget w(取决于i的某个值);
    // ...
}

上面两种方法,哪种好?
方法A:1个构造函数 + 1个析构函数 + n个赋值操作
方法B:n个构造函数 + n个析构函数

因此,除非你知道赋值成本构造+析构成本低,否则,你应该使用方法B。

2 尽量少做转型动作

C++规则的设计目标之一是,保证”类型错误”绝不可能发生。理论上,如果你的程序很”干净地”通过编译,就表示它并不企图在任何对象身上执行任何不安全,无意义,愚蠢荒谬的操作。这是一个极具价值的保证,可别草率地放弃它

不幸的是,转型(cast)破坏了类型系统。那可能导致任何种类的麻烦,有些容易识别,有些非常隐晦。在C++中转型是一个你会想带着极大尊重去亲近的一个特性。(意思是,坑比较多)

转型语法

  1. 旧式转型(C风格)
(T) expression;   // 将expression转型为T
T(expression);    // 同上
  1. C++的风格
// 通常被用来将对象的常量性移除(cast away the constness)
const_cast<T>(expression);

// 主要用来执行"安全向下转型"(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式转型执行的动作,也是唯一可能耗费重大运行成本的转型动作
dynamic_cast<T>(expression);

// 低级转型。实际动作及结果,可能取决于编译器,也就表示它不可移植
reinterpret_cast<T>(expression);

// 用来强迫隐士转换(implicit conversions)。例如,将non-const对象转为cosnt对象,或将int转为double。但是,它无法将const转换为non-const,这个只有const_cast才能办到
static_cast<T>(expression);

旧式转型仍然合法,但新式转型更受欢迎 。原因是:
* 它们很容易在代码中识别出来,不论是人工识别还是使用工具如grep,因此可以简化”找出类型系统在哪个点被破坏的过程”。
* 各转型动作的目标愈窄化。编译器可能诊断出错误的运用。例如,如果你打算将常量性去掉,除非使用新式转型中的const_cast,否则无法编译通过。

注意:许多程序员认为转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型。这是错误的观念。任何一个类型转换(不论是通过转型操作而进行的显示转换,或通过编译器完成的隐式转换),往往真的令编译器编译出运行期间执行的代码。

例子:

class Base { // ... };
class Derived: public Base { // ... };

Derived d;
Base* pb = &d; // 隐式地将Derived* 转换为Base*

这里建立了一个base class指针指向一个derived class对象,但有时候上述的两个指针值并不相同。这种情况下,会有一个偏移量在运行期被施行于Derived*指针身上,用以取得正确的Base*指针值。

上面这个例子表明:单一对象(例如,一个类型为Derived的对象)可能拥有一个以上的地址(例如,以Base*指向它时的地址和以Derived*指向它时的地址)。C,Java,C#都不可能发生这种事,但C++可以。实际上,一旦使用多重继承,这事几乎一直发生着。即使在单一继承中也可能发生。意味着,你通常应该避免做出“对象在C++中如何布局”的假设。当然更不该以此假设为基础执行任何转型动作。例如,将对象地址转型为char*指针然后在它们身上进行指针算术,这几乎总是会导致无定义不明确的行为。

尽量避免使用dynamic_cast

之所以需要dynamic_cast,通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但是你手上却只有一个”指向base”的pointer或reference。你只能靠它们来处理对象。

两个方法可以避免这个问题:

  1. 使用容器并在其中存储直接指向derived class对象的指针(通常是智能指针),如此便消除了“通过base class”接口处理对象的需要。(但是,这种做法使你无法在同一个容器内存储指针,指向所有可能之各种派生类,如果真要处理多种派生类对象,那就需要多个容器)

  2. 在base class内提供virtual函数做你想对各个派生类做的事,即,虚函数的方法。

例如:

class Base {
public: 
    virtual void dosomething() {} // 空实现
};

class Derived : public Base {
public:
    virtual void dosomething() {
        // 真正的实现
    }
};

typedef std::vector<std::tr1::shared_ptr<Base> > base_ptr_t;
base_ptr_t bp;
// ...

for (base_ptr_t::iterator iter = bp.begin(); iter != bp.end(); ++iter) {
    (*iter)->dosomething();  // 注意,这里没有使用dynamic_cast,而使用虚函数的特性
}

请记住
* 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的代替设计
* 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
* 宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。

3 避免返回handles指向对象内部成分

不论handle是个指针,或迭代器,或reference,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里的唯一关键是,有个handle被传出去了,一旦如此你就暴露在handle比其所指对象更长寿的风险下。

例子:

#include <iostream>

class Point {
public:
    Point(int x, int y) {
        m_x = x;
        m_y = y;
    }
    void setX(int newVal) {
        m_x = newVal;
    }
    void setY(int newVal) {
        m_y = newVal;
    }

    void show() const {
        std::cout << m_x << "," << m_y << std::endl;
    }

private:
    int m_x;
    int m_y;
};

class PointMgr {
public:
    PointMgr() : m_point(1, 1) {
    }

    // error: binding of reference to type 'Point' to a value of type 'const Point' drops qualifiers
    //Point& getPoint() const {

#if 0
    // ok, but not suggested
    Point& getPoint() {
        return m_point;
    }
#endif

    // ok, suggested
    const Point& getPoint() const {
        return m_point;
    }


    void showPoint() const {
        m_point.show();
    }

private:
    Point m_point;
};

int main()
{
    PointMgr point_mgr;
    point_mgr.showPoint(); // 1,1

    // error
    //point_mgr.getPoint().setX(2);
    //point_mgr.getPoint().setY(2);

    point_mgr.getPoint().show(); // 1,1

}

例外:
这并不意味你绝对不可以让成员函数返回handle。有时候你必须这么做。例如,operator[]就允许你获取strings和vectors的个别元素,而这些operator[]s就是返回reference指向容器内的数据,那些数据会随着容器被销毁而销毁。尽管如此,这样的函数毕竟是例外,不是常态。

请记住
避免返回handles(包括reference,指针,迭代器)指向对象内部。遵循这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低

4 为”异常安全”而努力是值得的

Strive for exception-safe code.

一个不符合异常安全的代码:

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    lock(&mutex);                 // 取得互斥器
    delete bgImage;               // 摆脱旧的背景图像
    ++imageChanges;               // 修改图像变更次数
    bgImage = new Image(imgSrc);  // 安装新的背景图像
    unlock(&mutex);               // 释放互斥器
}

异常安全有两个条件:当异常被抛出时,带有异常安全性的函数会:
1. 不泄露任何资源。上述代码中,一旦new Image(imgSrc)导致异常,对unlock的调用就绝不会执行,于是互斥器就永远被把持住了。
2. 不允许数据破坏。如果new Image(imgSrc)抛出异常,bgImage就是指向一个已被删除的对象,imageChanges也已被累加,而其实并没有新的图像被成功安装其起来。

异常安全函数(Exception-safe functions提供以下三个保证之一

  • 基本承诺。如果异常被抛出,程序内的任何事物仍然保持在有效状态。没有任何对象或数据结构会因此而败坏。所有对象都处于一种内部前后一致的状态。
  • 强烈保证。如果异常抛出,程序状态不改变。调用这样的函数需要有这样的认知:如果函数成功,就是完全成功;如果函数失败,程序会恢复到“调用函数之前”的状态。
  • 不抛掷(nothrow)保证。承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型身上的所有操作都提供nothrow保证。

异常安全代码,必须提供上述三种保证之一。如果它不这样做,它就不具备异常安全性。

修改后,异常安全地代码(强烈保证):
有个一般化的设计策略,可以很典型地会导致强烈保证,这个策略被称为copy and swap。(原则很简单,为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改,若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap))

pimpl idiom
实现上,通常是将所有”隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象。

struct PMImpl {
    std::tr1::shared_ptr<Image> bgImage; // PMImpl = PrettyMenu Impl
    int imageChanges;
};

class PrettyMenu {
public:
    // ...
private:
    Mutex mutex;
    std::tr1::shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    using std::swap;
    Lock ml(&mutex);

    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // 获取副本
    pNew->bgImage.reset(new Image(imgSrc));                // 修改副本
    ++pNex->imageChanges;

    swap(pImpl, pNew);  // 置换数据,释放mutex
}

请记住
1. 异常安全函数(Exception-safe functions)即使发生异常,也不会泄露资源,或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常性
2. “强烈保证”往往能够以copy-and-swap实现出来,但”强烈保证”并非对所有函数都可实现,或具备现实意义(时间和空间成本)。
3. 函数提供的”异常安全保证”,通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者

5 透彻了解inlining的里里外外

Inline函数,可以调用它们又不需蒙受函数调用所招致的额外开销。

没有白吃的午餐

inline函数,背后的整体观念是,将“对此函数的每一个调用”都以函数体替换之。但这样做可能增加你的目标代码大小。在一台内存有限的机器上,过度热衷inlining会造成程序体积太大,即使拥有虚内存,inline造成的代码膨胀亦会导致额外的换页行为,降低指令高速缓存装置的命中率(instruction cache hit rate),以及伴随而来的效率损失。

记住
inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出

  • 隐喻方式是将函数定义于class定义式内。

例如:

class Person {
public:
    int age() const { return theAge }; // 一个隐喻的inline申请,age被定义于class定义式内
private:
    int theAge;
};
  • 明确声明inline函数的做法则是在其定义式前加上关键字inline。

例如:标准的max template(来自<algorithm>

template<typename T>
inline const T& std::max(const T& a, const T& b)
{
    return a < b ? b : a;
}

总结:

  1. 一个表面上看似inline的函数是否真是inline?取决于你的建置环境,主要取决于编译器。
  2. 编译器通常不对”通过函数指针而进行的调用”实施inlining(编译器没有能力提出一个指针指向并不存在的函数)。
  3. 构造函数和析构函数,是否选择inline化,并非是个轻松的决定。因为空的构造函数里,编译器可能会做很多事情。
  4. 影响升级。inline函数无法随着程序库的升级而升级,也就是,如果f是程序库内的一个inline函数,客户将f函数本体编进其程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译。这往往是大家不愿意见到的。然而,如果f是non-inline函数,一旦它有任何修改,客户端只需要重新连接就好,远比重新编译的负担少的多。如果程序采取动态链接,升级版函数甚至可以不知不觉地被应用程序吸纳。
  5. 可能影响调试。大部分调试器面对inline函数都束手无策。毕竟你如何在一个并不存在的函数内设立断点呢?虽然某些建置环境勉力支持对inlined函数的调试,其他许多建置环境仅仅只能“在调试版程序中禁止发生inlining”。
  6. 80-20经验法则。平均而言,一个程序往往将80%的执行时间花费在20%的代码上头。这是一个重要的法则,因为它提醒你,作为一个软件开发者,你的目标是找出这可以有效增进程序整体效率的20%代码,然后将它inline或者竭尽所能地将它瘦身。但除非你选对目标,否则一切都是虚功。

请记住
1. 将大多数inlining限制在小型,被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
2. 不要只因为function templates出现在头文件,就将它们声明为inline。

6 将文件间的编译依存关系降至最低

int main()
{
    int x;              // 定义一个int
    Person p(params);   // 定义一个Person
}

当编译器看到x的定义式,它知道必须分配多少内存(通常位于stack内)才能够持有一个int。(每个编译器都知道int有多大)

当编译器看到p的定义式,它也知道必须分配足够空间以放置一个Person,但是,它如何知道一个Person对象有多大呢?编译器获得这项信息的唯一办法就是询问class定义式。然而,如果class定义式可以合法地不列出实现细目,编译器该如何知道分配多少空间呢?

对于C++代码,你可以:将对象实现细目隐藏于一个指针背后。

针对Person我们可以这样做:把Person分隔为两个classes,一个只提供接口,另一个负责实现该接口

例如:

class PersonImpl;   // Person实现类的前置声明
class Date;
class Address;

class Person {
public:
    Person(const std::string& name, const Date& birthday, const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;

private:
    std::tr1::shared_ptr<PersonImpl> pImpl; // 指针,指向实现物,隐藏实现细节
};

Personclass只内含一个指针成员,指向其实现类PersonImpl。这种设计被称为:pimpl idion (pimpl是 Pointer to implementation的缩写)。这样的设计下,Person的客户端就完全与Date,Addresses以及Persons的实现细节分离了。这些class的任何实现修改都不需要Person客户端重新编译。同时,由于客户无法看到Person的实现细节,也就不会写出什么:取决于内部细节的代码。这真正是“接口与实现分离”

分离的关键在于:以“声明的依赖性”代替“定义的依赖性”。现实中,让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。

其他每一件事,都源于这个简单的设计策略:
1,如果使用object referencesobject pointers可以完成任务,就不要使用object。(你可以只靠一个类型声明式,就定义出指向该类型的referencespointer;但如果定义某类型的objects,就需要用到该类型的定义式)。
2,如果能够,尽量以class声明式替换class定义式。(注意,当你声明一个函数,而它用到某个class时,你并不需要该class的定义,即使函数以by value的方式传递该类型参数或返回值)。

例如:定义func函数,但不需要Person的定义。但是,在调用func函数时,就需要知道Person的定义。也就是,比如一个函数库有非常多的函数,但是我们可能只用到了其中很少的函数,对我们用到的函数,在客户端通过前置声明的方式(而不是包含所有定义的方式),可以减少对不必要类型定义的依赖。

#include <stdio.h>

class Person;
void func(Person &p)
{
    printf("func\n");
}

int main()
{
    printf("main\n");
    return 0;
}
$g++ -o declare_var declare_var.cpp 
$./declare_var 
main

3,为声明式和定义式提供不同的头文件。为了促进严守上述准则,需要两个头文件,一个用于声明式,一个用于定义式。当然,这些文件必须保持一致性,如果有一个声明式被改变了,两个文件都得改变。因此,程序库客户应该总是#include一个声明文件而非前置声明若干函数,程序库作者也应该提供这两个头文件。

例如:
C++标准程序库头文件<iosfwd>内含iostream各组件的声明式,其对应定义则分布在若干不同的头文件内,包括<sstream><streambuf><fstream><iostream>

Handle classes

Person这样使用pimpl idiom的classes,往往被称为Handle classes。意思是,对于Person这样的class,如果要做点实事:

一种办法是,将它们的所有函数转交给相应的实现类,并由后者完成实际工作。

例如:下面是Person两个成员函数的实现。

#include "Person.h"
#include "PersonImpl.h"

Person::Person(const std::string&name, const Date& birthday, const Address& addr) : pImpl(new PersonImpl(name, birthday, addr))
{}

std::string Person::name() const
{
    return pImpl->name();   // 相同的名字
}

Interface classes

另一种办法是,令Person成为一种特殊的abstract base class(抽象基类),称为”Interface class”。这种class的目的是详细一一描述derived classes的接口,因此它通常不带成员变量,也没有构造函数,只有一个virtual析构函数,以及一组pure virtual函数,用来叙述整个接口。

例如:

class Person {
public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
    // ...
};

class RealPerson: public Person {
public:
    RealPerson(const std::string& name, const Date& birthday, const Address& addr) : theName(name), theBirthDate(birthday), theAddress(addr)
    {}

    virtual ~RealPerson() {}
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;

private:
    std::string theName;
    Date theBirthDate;
    Address theAddress;
};

Handle classes和Interface classes的利弊


Handle classes和Interface classes**解除了接口和实现之间的耦合关系,从而降低文件间的编译依赖。**


但是,这种设计使你在运行期丧失了若干速度,同时,又让你为每个对象超额付出若干内存。

在Handle classes身上,成员函数必须通过implementation pointer取得对象数据,那会为每一次访问增加一层间接性。而每一个对象消耗的内存数量必须增加implementation pointer的大小。

在Interface classes身上,由于每个函数都是virtual,所以你必须为每次函数调用付出一个间接跳跃成本。此外,Interface class派生的对象必须内含一个vptr,这个指针可能会增加存放对象所需的内存数量。

Handle classes和Interface classes,由于设计上用来隐藏实现细节,因此无法实现inline优化。

那是否应该使用Handle classes和Interface classes呢?你应该考虑以渐进的方式使用这些技术。在程序发展过程中使用,以求实现代码有变化时,对客户端带来最小的冲击。而当它们导致速度或大小差异成为主要矛盾时,就用具象类(concrete classes)替换Handle classes和Interface classes。

请记住
1. 支持”编译依赖最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。
2. 程序库头文件应该以”完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Effective C++ - Implementations 的相关文章

随机推荐

  • 区块链三加一:深度论区块链的联盟链意义与作用

    随着区块链技术的发展 越来越受个人及企业的关注 而和区块链联系最为紧密的 恐怕就是金融行业了 然而虽然区块链大受热捧 但毕竟区块链是属于公有区块链 公有区块链有着其不可编辑 不可篡改的特点 这就使得公有链并不适合企业使用了 毕竟如果某金融企
  • UE4右键菜单(上篇ContentBrowser的补充)

    在上篇中我们讲到拓展我们的ContentBrowser的文件夹右键菜单 看回上篇代码可以发现其实真正起实现拓展 看到效果 的代码就只有一行 在代码中我们可以看到 MenuBuilder 这个东西是可以为我们创建一个菜单的 今天我们继续来改造
  • VOC格式数据集转适用YOLOv8格式

    直接设置VOC数据集的文件夹目录 生成的v8格式数据集存放目录以及标签的名字就行了 直接执行后就会是下面这图这样重新创建一个文件夹存放符合yolov8格式的数据集 yolov8直接加载那个yaml文件就行 我下边只有一类 注意最后的yaml
  • 计算机网络物联网论文,物联网对计算机网络技术发展分析

    摘要 随着科学技术和计算机网络技术的快速发展 物联网行业呈现出了巨大的发展潜力并且不断完善 这几年在物联网概念的强力刺激下 很多的公共服务和商业服务也加入到这一技术的应用当中 计算机网络是构成物联网的基础 所以计算机网络技术的发展和物联网是
  • 前端学习之常见标签的使用(2)

    目录 h标签 p标签 br标签 字符实体 img标签 a标签 mailto链接 base标签 锚点 div span video H5新增 audio H5新增 h标签 h标签 标题标签 在HTML中 一共有六级标题标签 h1 h6 在显示
  • 类的分文件编写

    类的分文件编写 point h 1 在开头加上以下代码 pragma once 防止头文件重复包含 include
  • 【Cadence】virtuoso Layout复制版图链接到新的原理图

    稍微记录一下备忘 需求描述 1 假设cell1中含有schematic1和layout1 2 现复制cell1到cell2 3 修改cell2中的schematic2后 需要相应地修改layout2 4 如果打开layout2 会发现链接到
  • 【操作系统】王道考研 p59-60 减少延迟时间的方法、磁盘的管理

    减少延迟时间的方法 磁盘的管理 以下是减少延迟时间的方法的内容 前情回顾 简而言之 如果要读写逻辑上相邻的盘片会延长时间 因为读写完一个盘片后要处理一段时间 处理完已经转过想要读的下一个盘片了 磁盘地址结构的设计 减少寻道时间的方法 物理地
  • mac上使用IntelliJ IDEA运行项目

    一 Mac OSX系统下安装Tomcat 1 下载Tomcat官方下载地址 2 解压Tomcat到目录 Library 中 并把文件夹名由 apache tomcat 6 0 16 改为 Tomcat 3 打开 应用程序 Applicati
  • vue3 中的 toRefs 是什么? 什么时候用?

    定义 toRefs可以将对象 只能接收rective对象 中的属性变成响应式 正常reactive对象数据也是响应式的 如果用toRefs解构出去会更加方便 什么时候用 数据量如果很多 我们一般会用解构来简化代码 那么在vue3 中如果使用
  • 数字电路的时钟(1)-- 时钟抖动和分类

    时钟抖动通常分为时间间隔误差 Time Interval Error 简称TIE 又叫相位抖动 周期抖动 Period Jitter 和相邻周期抖动 cycle to cycle jitter 三种抖动 TIE又称为phase jitter
  • 华为OD机试 - 生日礼物(Java)

    题目描述 小牛的孩子生日快要到了 他打算给孩子买蛋糕和小礼物 蛋糕和小礼物各买一个 他的预算不超过x元 蛋糕cake和小礼物gift都有多种价位的可供选择 请返回小牛共有多少种购买方案 输入描述 第一行表示cake的单价 以逗号分隔 第二行
  • IDEA类和方法的模板注释

    2 1 修改类注释模板 在File gt Settings gt Editor gt File and Code Templates下分别修改Class Interface Enum等注释模板 Class模板部分修改如下 其余的举一反三进行
  • Java字符串左移右移动

    public class MoveTheKnumber public static void main String args String startStr ABCDEF System out println getKnumber lef
  • python plt 绘图详解(plt.版本)

    文章目录 一 plt介绍 二 安装与导入 三 使用说明 1 使用plt plot与plt show绘制并显示图像 1 曲线颜色 color 简写为 c 2 点型 标记marker 3 线型 linestyle 简写为 ls 4 剩余参数说明
  • 基于matlab轴的优化设计,基于MATLAB的转轴可靠性优化设计

    科 技 天 地 56 INTELLIGENCE 基于 MATLAB 的转轴可靠性优化设计 上海理工大学管理学院 郑 红 摘 要 基于 MATLAB 的可靠性优化设计是应用 MATLAB 软件 在优化设计中将设计参数作为随机变量 以产品的可靠
  • 整理了60个 Python 实战例子,拿来即用!

    人生苦短 我用 Python 大家好 最近有一些朋友问我有没有一些 Python 实战小案例 今天我整理排版了一遍 给大家分享一下 喜欢记得点赞 收藏 关注 整理了60个Python小例子 拿来即用 整理了60个 Python 实战例子 拿
  • Ubuntu系统下安装微信

    安装微信实现截图发送图片功能 1 打开终端输入命令 更新软件源 sudo apt get update 2 输入以下命令 一定注意 O 中间是大写字母O 不是0也不是小写o wget O https deepin wine i m dev
  • verilog之状态机详细解释(二)

    一 有限状态机设计的一般步骤 1 逻辑抽象 得出状态转换图 就是把给出的一个实际逻辑关系表示为时序逻辑函数 可以用状态转换表来描述 也可以用状态转换图来描述 这就需要 分析给定的逻辑问题 确定输入变量 输出变量以及电路的状态数 通常是取原因
  • Effective C++ - Implementations

    前言 实现中需要注意的一些问题 尽可能延后变量的定义 尽量少做转型动作 转型语法 尽量避免使用dynamic cast 避免返回handles指向对象内部成分 为异常安全而努力是值得的 透彻了解inlining的里里外外 将文件间的编译依存