《C++程序设计原理与实践》笔记 第14章 设计图形类

news2024/11/13 19:18:30

本章借助图形接口类介绍接口设计的思想和继承的概念。为此,本章将介绍与面向对象程序设计直接相关的语言特性:类派生、虚函数和访问控制。

14.1 设计原则

我们的图形接口类的设计原则是什么?

14.1.1 类型

我们的程序设计理念是在代码中直接表示应用领域的概念。例如,Window表示窗口,Line表示一条线,Point表示一个坐标点,Color表示颜色,Shape表示所有形状的统称。最后一个例子Shape与其他例子的区别在于它是一个一般化的、抽象的概念。我们永远无法在屏幕上看到一个“一般形状”,只能看到线、六边形等具体形状。这一点已经反应在我们的类型定义中:创建一个Shape变量将导致编译错误。

我们的图形接口类构成了一个库。这些类旨在作为你定义其他图形类时的参考示例,以及作为复杂图形类的基本组件。考虑到我们的库的规模以及图形应用领域的庞大,我们不能指望完整性。相反,我们的目标是简洁性和可扩展性

一个关键设计决策是提供很多具有较少操作的“小”类,而不是一个带有很多参数和操作的类。我们认为这样能够更加直接、有效地建模我们的图形领域。

14.1.2 操作

我们的理念是用最小的接口来实现我们想做的事情。更加便捷的操作可以通过非成员函数或新的类来实现。

我们希望类的接口具有一致的风格。例如,在不同的类中所有执行相似操作的函数有相同的名字,接受相同类型的参数,还可能要求参数的顺序也相同。

  • 构造函数:如果形状需要一个位置,则接受一个Point作为第一个参数。
  • 所有处理点的函数都使用Point,而不是一对int
  • 如果函数需要宽度和高度,参数总是按这个顺序出现。在这种微小的细节上保持一致会极大地方便使用,并减少运行时错误。
  • 逻辑上等价的操作有相同的名字。例如,向任何形状添加点的函数都叫add(),任何画线的函数都叫draw_lines()。这种一致性能帮助我们记忆(需要记住的细节更少)以及设计新类(“就跟往常一样”)。有时,这种一致性甚至允许我们编写能用于很多不同类型的代码,称为泛型,见19~21章。

14.1.3 命名

逻辑上不同的操作应该有不同的名字。但是,为什么是将Shape “attach” 到Window,而将Point “add” 到Shape?这两种情况都是“将一个东西放到另一个东西中”,但这种相似性背后隐藏了一个根本的不同点:Shape::add()的参数是传值,而Window::attach()的参数是传引用。例如,对于

opl.add(Point(100, 100));

opl会保存这个点的副本,而实际参数Point(100, 100)这个临时对象在add()调用之后就消失了。另一方面,对于

win.attach(opl);

win并不会创建opl的副本,它只是保存opl的一个引用。因此,我们必须保证在win使用opl时不能离开opl的作用域。这意味着我们不能创建一个对象,将它附加到窗口之后就立即销毁,例如:

win.attach(Rectangle(Point(100, 200), 50, 30));

在这种情况下,当add()调用完成后,win所引用的对象已经不存在了(变成了“野指针”)。这就是13.10节的例子中必须使用Vector_ref管理未命名对象的原因。

14.1.4 可变性

当我们设计一个类时,“谁可以修改其数据(表示)?”以及“如何修改?”是必须回答的关键问题。我们试图保证只有类自身能够修改其对象的状态(即private成员),从而有机会检查“愚蠢的”值,例如半径为负数的Circle

14.2 Shape类

Shape类是一个一般概念,表示可以显示在屏幕上的对象:

  • 将图形对象与Window关联起来,从而提供了与物理屏幕的联系。
  • 处理画线所使用的颜色和线型,因此它包含一个Line_style、一个画线的Color和一个填充的Color
  • 包含了一系列Point以及画线的默认方法(依次连接)。

下面首先给出完整的类,之后讨论其实现细节。

class Shape  {        // deals with color and style, and holds sequence of lines 
public:
    void draw() const;                 // deal with color and draw lines
    virtual void move(int dx, int dy); // move the shape +=dx and +=dy

    void set_color(Color col);
    Color color() const;
    void set_style(Line_style sty);
    Line_style style() const;
    void set_fill_color(Color col);
    Color fill_color() const;

    Point point(int i) const;       // read only access to points
    int number_of_points() const;

    Shape(const Shape&) = delete;   // prevent copying
    Shape& operator=(const Shape&) = delete;

    virtual ~Shape() {}
protected:
    Shape();
    Shape(initializer_list<Point> points);  // add() the Points to this Shape

    virtual void draw_lines() const;   // draw the appropriate lines
    void add(Point p);                 // add p to points
    void set_point(int i,Point p);     // points[i]=p;
private:
    vector<Point> points;              // not used by all shapes
    Color lcolor;                      // color for lines and characters
    Line_style ls; 
    Color fcolor;                      // fill color
};

14.2.1 一个抽象类

Shape的构造函数是protected,这意味着:

  • 只有Shape的派生类可以直接使用它(使用:Shape语法)。
  • Shape只能用作其他类(例如LineOpen_polyline)的基类。
  • 不能直接创建Shape对象,这反映了“我们无法看见一个一般形状”的思想(如14.1.1节所述)。

如果一个类只能被用作基类,则它是抽象类(abstract class)。另一种更常用的定义抽象类的方法是纯虚函数,见14.3.5节。与抽象类相对的是具体类(concrete class),即可以创建对象的类。

声明virtual ~Shape() {}定义了一个虚析构函数,将在17.5.2节中解释。

14.2.2 访问控制

Shape类将所有数据成员均声明为private,因此需要提供访问函数。这里选择了一种较为简单、方便、易读的风格:如果有一个表示属性X的成员,则提供一对函数X()set_X()分别用于该成员的读写(这种成员函数分别叫做取值函数(getter)和设值函数(setter))。例如:

void Shape::set_color(Color col) {
    lcolor = col;
}

Color Shape::color() const {
    return lcolor;
}

这种风格最主要的不便之处在于成员变量和取值函数不能用相同的名字。我们给取值函数选择最方便的名字,因为它们是公共接口的一部分,而私有变量的命名不那么重要(注:另一种常用的方法是让所有成员变量以_结尾)。注意,我们用const指出取值函数不修改对象。

Shape保存了一个Point的向量,叫做points。由于points是私有的,因此提供了一组访问函数:

// protected
void Shape::add(Point p) {
    points.push_back(p);
}

// protected
void Shape::set_point(int i, Point p) {
    points[i] = p;
}

Point Shape::point(int i) const {
    return points[i];
}

int Shape::number_of_points() const {
    return points.size();
}

只有Shape的派生类(例如CirclePolygon)才知道这些点的含义,而Shape只是存储它们。因此,派生类需要控制如何添加点:

  • CircleRectangle不允许用户添加点,因为没有意义。
  • Lines只允许添加成对的点。
  • Open_polylineMarks允许添加任意多个点。
  • Polygon需要对添加的点进行相交性检查。

函数add()protected(即只能被类自身和派生类访问),从而保证由派生类控制如何添加点,而如果add()publicprivate都无法实现这一保证。

类似地,set_point()也是protected,即只有派生类知道是否可以在不违反不变式的前提下修改点。

在派生类的成员函数中可以使用这些函数,例如Lines::draw_lines()(见13.3节)。

这些访问函数不会使程序变慢,因为它们会被编译器优化掉(内联)。调用number_of_points()和直接调用points.size()使用一样多的内存,执行一样多的指令。

14.2.3 绘制形状

Shape类最基本的功能是绘制形状。它借助FLTK和操作系统的机制来完成这一工作,但是从用户的视角,它只是提供了两个函数:

  • draw():设置线型和颜色,然后调用draw_lines()
  • draw_lines():在屏幕上绘制像素。

draw()函数只是简单地调用FLTK函数来设置颜色和线型,接着调用draw_lines()在屏幕上进行实际的绘制,最后将颜色和线型恢复到调用之前:

void Shape::draw() const {
    Fl_Color oldc = fl_color();
    // there is no good portable way of retrieving the current style
    fl_color(lcolor.as_int());            // set color
    fl_line_style(ls.style(),ls.width()); // set style
    draw_lines();
    fl_color(oldc);      // reset color (to previous)
    fl_line_style(0);    // reset line style to default
}

(既然每次绘制之前都要设置颜色和线型,那么恢复的意义是什么?)

注意,draw()不处理填充颜色或者线的可见性,这些都由draw_lines()处理。

现在考虑如何实现draw_lines()。让一个Shape类的函数来完成每一种形状的绘制是非常困难的,而TextRectangleCircle等大多数派生类都有更好的绘制方式。因此Shape类为每个派生类提供了自己定义绘制方式的机会,这就是将Shape::draw_lines()声明为virtual的意义所在:

class Shape {
    // ...
    // let each derived class define its own draw_lines() if it so chooses
    virtual void draw_lines() const;
    // ...
};

struct Circle : Shape {
    // ...
    void draw_lines() const override;  // override Shape::draw_lines()
    // ...
};

因此,如果Shape是一个Circle,那么Shapedraw_lines()必须以某种方式调用Circledraw_lines();如果Shape是一个Rectangle,则调用Rectangledraw_lines()。这正是关键字virtual所保证的:如果派生类定义了与基类的虚函数名字和类型相同的函数,那么(通过基类指针或引用)调用该函数时,调用的是派生类的函数而不是基类的函数,这种技术称为覆盖(overriding)。

注意,尽管在Shape中处于核心地位,draw_lines()还是被定义成protected。它不是给“一般用户”调用的(这是draw()的目的),而只是作为一个“实现细节”被draw()Shape的派生类使用。

这样就完成了12.2节中的显示模型:驱动屏幕的系统知道WindowWindow知道Shape,并可以调用其draw()函数;最后,draw()调用特定形状类的draw_lines()函数。

显示模型

gui_main()简单地调用Fl::run(),我们使用Simple_window::wait_for_button()来代替了。

Shapemove()函数简单地将保存的每个点相对于当前位置移动一个偏移量:

// move the shape +=dx and +=dy
void Shape::move(int dx, int dy) {
    for (int i = 0; i<points.size(); ++i) {
        points[i].x+=dx;
        points[i].y+=dy;
    }
}

move()也是虚函数,因为派生类可能有需要移动的数据,而Shape并不知道,例如Axis

14.2.4 拷贝和可变性

Shape类将拷贝构造函数和拷贝赋值运算符声明为delete

Shape(const Shape&) = delete;   // prevent copying
Shape& operator=(const Shape&) = delete;

这样做的效果是禁用默认的拷贝操作

但是拷贝在很多地方都有用。如果没有拷贝,甚至难以使用vectorpush_back()将参数的拷贝放在向量中)。所以为什么要禁止拷贝?如果一个类型的默认拷贝操作可能引起麻烦,就应该禁止拷贝。

标准库中禁止拷贝的例子:

  • istreamostream:输入/输出流有复杂的内部状态,允许拷贝意味着两个流共享输入/输出设备,这可能引起麻烦。
  • unique_ptr:本身的含义就是“拥有对象唯一所有权的指针”,允许拷贝违反了这一不变式。

注:

  • = delete是C++11引入的新语法,在此之前通过将拷贝构造函数和拷贝赋值运算符声明为private来禁止拷贝。
  • 书中给出的禁止拷贝的第一个原因是“截断”,例如将一个Circle赋值给一个Shape,或者添加到vector<Shape>。然而这是C++语言本身的特性决定的,不应该成为禁止拷贝的原因。
  • 第二个原因是传引用参数,例如Window::attach()的参数必须是传引用,Window不能保存Shape的拷贝,因为对原Shape对象的修改不会影响到副本。但这是由Window本身决定的,与Shape是否可拷贝无关。

当把一个派生类对象(通过值拷贝)赋给基类对象时,如果基类没有禁止拷贝,并且派生类有额外的成员,则会发生截断(slicing)。例如,CircleShape多一个半径r,如果将一个Circle赋值给Shape,则这个Circle对象会被截断,成员r并不会被拷贝。

截断

注:书中说“使用拷贝后的Shape可能会引起崩溃,因为没有拷贝成员r”是不正确的。因为只有在值拷贝时才会发生截断,而此时变量类型是Shape(而不是Shape*Shape&),通过该变量调用draw_lines()函数时调用的是Shape::draw_lines()而不是Circle::draw_lines(),根本不可能访问到成员r

下面是通过禁止拷贝来避免截断的例子:

void my_fct(Open_polyline& op, const Circle& c) {
    Open_polyline op2 = op;  // error: Shape's copy constructor is deleted
    vector<Shape> v;
    v.push_back(c);          // error: Shape's copy constructor is deleted
    // ...
    op = op2;                // error: Shape's assignment is deleted
}

Marked_polyline mp("x");
Circle c(p,10);
my_fct(mp,c);  // the Open_polyline argument refers to a Marked_polyline
  • v.push_back(c)将一个const Circle&传递给const Shape&参数,这一步没有问题。但push_back()内部将参数拷贝到向量中时会发生截断,因为向量元素类型是Shape,而参数实际引用了一个Circle对象。
  • 函数my_fct()的参数op实际引用了一个Marked_polyline对象,如果将其拷贝到op2则会发生截断。

如果想要拷贝一个默认拷贝操作已经被禁用的类型的对象,可以显式地写一个函数来完成这一工作。这种拷贝函数通常叫做clone()。显然,只有当读取成员的函数足够表达构造副本所需的内容时才能编写出clone(),所有的Shape类都已满足这一条件。

14.3 基类和派生类

下面从一个更加技术性的视角来讨论基类和派生类。当设计图形接口库时,我们依赖三个关键的语言机制:

  • 派生(derivation):从一个类构造另一个类,使得新类可以代替原来的类。其中新类叫做派生类(derived class)或子类(subclass),原来的类叫做基类(base class)或父类/超类(superclass)。这通常称为继承(inheritance),因为派生类除了自己的成员外,还获得(“继承”)了基类的所有成员。例如,Circle派生(继承)自Shape,换句话说,“Circle是一种Shape”或者“ShapeCircle的基类”。
  • 虚函数(virtual function):在基类中定义一个函数、在派生类中有一个名称和类型相同的函数,当用户(通过基类指针或引用)调用基类函数时,调用的实际上是派生类的函数。这通常称为运行时多态(run-time polymorphism)、动态分派(dynamic dispatch)或覆盖(overriding),因为调用哪个函数是在运行时根据实际使用的对象类型来确定的(即 “virtual” 意味着“可以被覆盖”(can be overriden))。例如,当Window(通过Shape*Shape&)对一个实际是CircleShape调用draw_lines()函数时,实际调用的是Circledraw_lines(),而不是Shape本身的draw_lines()
  • 私有和保护成员(private and protected members):保持类的实现细节(数据成员)为私有的,保护它们不被直接使用而使得维护复杂化。这通常称为封装(encapsulation)。

继承、多态和封装的使用是面向对象程序设计(object-oriented programming)最常见的定义。因此,除了其他的程序设计风格之外,C++直接支持面向对象程序设计。

12.4节给出了图形接口类的继承关系图,箭头从派生类指向基类。

注:派生类的指针或引用可以直接赋给基类的指针或引用;反之则必须使用dynamic_cast,如果转换失败则返回空指针或抛出std::bad_cast。例如:

Circle c(Point(100, 100), 50);
Shape* ps = &c;  // OK
Circle* pc = dynamic_cast<Circle*>(ps);  // OK
Rectangle* pr = dynamic_cast<Rectangle*>(ps);  // nullptr

Shape& rs = c;  // OK
Circle& rc = dynamic_cast<Circle&>(rs);  // OK
Rectangle& rr = dynamic_cast<Rectangle&>(rs);  // std::bad_cast

14.3.1 对象布局

如9.4.1节所述,一个类的成员定义了对象在内存中的布局:数据成员在内存中一个接一个地存储。 当使用继承时,派生类的成员被添加在基类成员之后。例如,ShapeCircle的对象布局如14.2.4节所示。

为了处理虚函数调用,我们必须在对象中存储更多信息,用于区分调用虚函数时实际调用的是哪个函数。常用方法是增加一个函数表的地址,这个表通常称为vtbl(“virtual table” 或 “virtual function table”,虚函数表),它的地址通常称为vptr(“virtual pointer”,虚指针)。将vptrvtbl加入布局图中得到下图:

vtbl

基本上,虚函数调用生成的代码简单地寻找vptr,通过它找到vtbl,然后调用其中正确的函数。 其代价大约是两次内存访问加上一次普通函数调用,既简单又快速。

每个具有虚函数的类只有一个vtbl,而不是每个对象都有一个,因此vtbl并不会显著增加程序目标代码的大小。

注意,上图中没有画出任何非虚函数,因为这种函数的调用方式没有任何特殊之处,它们不会增加对象的大小。

定义一个和基类中虚函数的名字和类型都相同的函数,使得派生类的函数代替基类的版本被放入vtbl的技术称为覆盖(overriding)。

14.3.2 派生类和定义虚函数

我们通过在类名后给出一个基类来指定一个类是派生类。例如:

class D : public B {
    // ...
};

struct D : B {
    // ...
};

其中基类前的public关键字表示公有继承,详见14.3.4节。

虚函数必须在类内被声明为virtual。但是如果把函数定义放在类外,则函数定义中不必也不能使用关键字virtual。例如:

class B {
public:
    virtual void f();
};

void B::f() {
    // ...
}  

14.3.3 覆盖

覆盖虚函数时,必须使用与基类中完全相同的名字和类型(参数表、是否const)。例如:

class B {
public:
    virtual int f(int x);
};

class D : public B {
public:
    int f(int x) override;
};

注:

  • 如果派生类的函数覆盖了基类的虚函数,则派生类的函数也是虚函数。
  • 从C++11开始,可以使用override来声明虚函数覆盖。如果一个函数声明了override,但并未覆盖基类的虚函数,或者基类对应的函数不是virtual,将产生编译错误,从而可以让编译器来保证覆盖了正确的虚函数。这对于大而复杂的类层次结构是非常有帮助的。
  • 覆盖虚函数时,派生类中的函数可以声明或不声明为virtual,也可以声明或不声明为override。最好的做法是:只将基类中的虚函数声明为virtual,派生类中覆盖的函数声明为override(这已经意味着该函数也是虚函数),如上面的例子所示。
  • 如果派生类定义了一个与基类中的名字和类型完全相同的函数,但基类中的函数不是virtual,则称为隐藏(hide)。隐藏与覆盖的区别是:当通过基类指针或引用调用函数时,覆盖调用的是派生类的函数,而隐藏调用的是基类的函数。见下面的例子。

★下面通过一个纯技术性的例子来解释覆盖:

覆盖的例子

程序的输出如下:

B::f()
B::g()
D::f()
B::g()
D::f()
B::g()
B::f()
B::g()
D::f()
D::g()
DD::f()
DD:g()

这里的几个关键点:

  • call()的参数类型是const B&,不知道参数的实际类型,只知道是BB的派生类,因此通过运行时多态来确定实际调用的函数。另外,只能调用const成员函数。
  • 第3~4行由call(d)输出,参数的实际类型是D。由于D::f()覆盖了B::f(),因此b.f()调用的是D::f();由于D::g()不是const,因此b.g()调用的是的B::g()
  • 第5~6行由call(dd)输出,参数的实际类型是DD。由于DD::f()不是const,且DD未覆盖D::f(),因此b.f()调用的是D::f();由于DD::g()隐藏(而不是覆盖)了B::g(),因此b.g()调用的是B::g()
  • 对于最后6行,当通过变量调用函数时,由于已知变量的实际类型,且变量不是const,因此会优先调用变量的类型自己定义的函数。

当你理解了为什么是这样的结果,你就会明白继承和虚函数机制了。

14.3.4 访问

C++为类成员访问提供了一个简单的模型。类成员可以是:

  • 共有的(public):如果一个成员是public,则可以被所有函数访问。
  • 受保护的(protected):如果一个成员是protected,则只能被类自身和派生类访问。
  • 私有的(private):如果一个成员是private,则只能被类自身访问。

继承也分为共有继承、受护继承和私有继承:

  • 共有继承(public inheritance):基类的public成员对派生类是publicprotected成员对派生类是protectedprivate成员对派生类不可见。
  • 受保护继承(protected inheritance):基类的publicprotected成员对派生类是protectedprivate成员对派生类不可见。
  • 私有继承(private inheritance):基类的publicprotected成员对派生类是privateprivate成员对派生类不可见。
继承方式\基类成员publicprotectedprivate
publicpublicprotected不可见
protectedprotectedprotected不可见
privateprivateprivate不可见

注:

  • 这些定义忽略了友元(friend)的概念和一些次要的细节,这不在本书的范围之内。
  • 类默认私有继承,结构体默认公有继承。

14.3.5 纯虚函数

抽象类(abstract class)是只能作为基类的类。我们使用抽象类来表示抽象的概念。抽象概念的思想是极其有用的,例如形状/矩形,动物/狗,水果/苹果。在程序中,抽象类通常定义了一组相关的类(类层次结构(class hierarchy))的接口。

14.2.1节展示了如何通过将构造函数声明为protected来定义抽象类。另一种更常用的方法是声明一个或多个纯虚函数(pure virtual function),即必须被派生类覆盖的虚函数。纯虚函数通过语法= 0来声明,例如:

class B {
public:
    virtual void f() = 0;  // pure virtual function
    virtual void g() = 0;
};

B b;  // error: B is abstract

因为B有纯虚函数,我们不能创建B类的对象。覆盖所有的纯虚函数可以解决这一“问题”:

class D1 : public B {
public:
    void f() override;
    void g() override;
};

D1 d1; // OK

注意,除非所有的纯虚函数都被覆盖了,否则派生类仍然是抽象的

class D2 : public B {
public:
    void f() override;
    // no g()
};

D2 d2;  // error: D2 is (still) abstract

class D3 : public D2 {
public:
    void g() override;
};

D3 d3;  // OK

带有纯虚函数的类通常作为纯粹的接口,即它们通常没有数据成员(数据成员将在派生类中定义),因此也没有构造函数。

14.4 面向对象程序设计的好处

通过继承,我们可以获得(其中之一或两者都有):

  • 接口继承(interface inheritance):需要基类(指针或引用)参数的函数可以接受一个派生类对象(并且可以通过基类提供的接口使用派生类对象)。例如,Window::attach(Shape&)可以接受Shape的任何派生类对象。
  • 实现继承(implementation inheritance):当定义派生类及其成员函数时,我们可以使用基类提供的功能(例如数据成员和成员函数)。例如,Line直接复用了Shape::draw_lines()Closed_polyline::draw_lines()调用Open_polyline::draw_lines()来完成大部分的绘制。

一个不能提供接口继承的设计(即派生类对象不能被当作其公有基类的对象使用)是一个拙劣且容易出错的设计。

接口继承之所以得名,是因为其优点:使用基类提供的接口的代码无需知道具体的派生类。实现继承之所以得名,是因为其优点:基类提供的功能简化了派生类的实现。

注意,我们的图形库设计严重依赖于接口继承:“图形引擎”调用Shape::draw(),进而调用虚函数Shape::draw_lines()完成实际的绘制工作。无论是“图形引擎”还是Shape类都不知道有哪些具体形状。特别是,“图形引擎”(FLTK加上操作系统的图形功能)是在我们的图形类之前若干年就编写、编译好的!我们只是定义了特定的形状,并将其作为Shape附加到Window中。而且,由于Shape类不知道你的图形类,当你每次定义新的图形类时,不需要重新编译Shape类。

换句话说,我们可以向程序中添加新形状,而不用修改已有的代码。这是一个软件设计/开发/维护的圣杯:扩展一个系统而不用修改它。哪些改进不必修改已有的类还是有一定限制的(例如Shape提供了非常有限的服务),同时这种技术也不是对所有的程序设计问题都能很好地应用(例如第17~19章定义的vector,继承机制对其没什么用处)。然而,接口继承仍然是设计和实现对于改进需求的鲁棒性(健壮性)很强的系统的最有力的技术之一。

同时,实现继承也能带来很多好处,但它不是灵丹妙药。通过将有用的服务放在Shape中,我们避免了在派生类中一遍又一遍地重复工作。这对于现实世界中的程序设计尤为重要。然而,它的代价是任何对于Shape接口或数据成员布局的修改都必须重新编译所有的派生类及其用户代码。对于一个广泛使用的库来说,这种重新编译是绝对行不通的。

简单练习

习题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/393094.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人工智能( AI )将如何颠覆项目管理?看看这六大关键领域

Gartner 研究预测&#xff0c;到 2030 年&#xff0c;80% 的项目管理任务将由 AI 运行&#xff0c;由大数据、机器学习和自然语言处理提供支持。 这些即将到来的技术发展视为前所未有的机遇。为这一颠覆时刻做好充分准备的企业和项目负责人将收获最大的回报。项目管理的每个方…

Linux操作系统学习(互斥)

文章目录线程安全互斥量互斥锁的原理线程安全补充可重入函数死锁线程安全 ​ 由于多个线程是共享同一个地址空间的&#xff0c;也就是很多资源都是共享的&#xff0c;那么线程通信就会很方便&#xff0c;但是方便的同时缺乏访问控制&#xff0c;可能会由于一个线程的操作问题&…

元数据管理、治理、系统、建设方案、范例等

【数据治理工具】–元数据系统 1.元数据系统 1.1 概述 如果想建设好元数据系统&#xff0c;需要理解元数据系统的相关概念&#xff0c;如数据、数据模型、元数据、元模型、ETL、数据血缘等等。 首先&#xff0c;要清楚数据的定义、数据模型的定义。数据一般是对客观事物描述…

全国程序员薪酬大曝光!看完我酸了····

2023年&#xff0c;随着互联网产业的蓬勃发展&#xff0c;程序员作为一个自带“高薪多金”标签的热门群体&#xff0c;被越来越多的人所关注。在过去充满未知的一年中&#xff0c;他们的职场现状发生了一定的改变。那么&#xff0c;程序员岗位的整体薪资水平、婚恋现状、职业方…

Halo开源建站工具

目录 特性 代码开源 易于部署 插件机制 附件管理 搜索引擎 快速开始 最新主题 下载安装主题 开发者指南 我的本地站点 docker管理 本地站点 gaghttps://halo.run/ 支持h2文件系统存储数据&#xff0c;支持docker部署。 特性 我们会一直探索&#xff0c;追求更好…

【JavaSE】方法的使用初学者易懂

前言 大家好&#xff0c;我是程序猿爱打拳。今天讲解的是Java中方法的使用。Java中的方法类似于C语言里面的函数其中都有实参与形参。但Java中的方法又比C语言中的函数更为强大&#xff0c;为何呢&#xff1f;请看下文。 目录 1.为什么要有方法&#xff1f; 2.方法的概念及使…

Centos 虚拟机安装

文章目录Centos 虚拟机安装一、模版虚拟机环境准备安装VMvare&#xff0c;安装CentosCentos 虚拟机安装 一、模版虚拟机环境准备 安装VMvare&#xff0c;安装Centos 创建虚拟机&#xff0c;然后选择自定义安装 然后是默认的&#xff0c;点一下步 这一步选择稍后安装操作系…

Java下浅谈String.valueOf()

今日遇到遇见无语的事情&#xff0c;mybatis查询数据库结果 List<Map<String, String>> 需要转换为字符串&#xff0c;但是在debug时&#xff0c;在idea小窗口单独执行代码&#xff0c;是可以正常编译的&#xff0c;离开idea小窗口执行就报错&#xff1a; 类型转换…

Anaconda安装Pytorch(win系统)

前面有一篇博客专门讲了安装CPU版本的Pytorch&#xff0c;因为当时没有GPU&#xff0c;现在有了3090&#xff0c;专门记录一下安装GPU版的过程。一、添加清华源可参考官方anaconda | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror创建虚拟环境若没有…

一文解决Vue所有报错【持续更】

前言 Vue是一个流行的前端框架&#xff0c;许多web开发人员使用Vue来构建他们的应用程序。然而&#xff0c;正如任何其他框架一样&#xff0c;Vue也可能会发生错误。在这篇技术文章中&#xff0c;我们将探讨Vue常见的报错以及如何解决它们。 常见错误 1. Vue Template Error …

【目标检测】61、Dynamic Head Unifying Object Detection Heads with Attentions

文章目录一、背景二、方法2.1 scale-aware attention2.2 spatial-aware attention2.3 task-aware attention2.4 总体过程2.5 和现有的检测器适配2.6 和其他注意力机制的关联三、效果四、代码论文链接&#xff1a; https://arxiv.org/pdf/2106.08322.pdf代码链接&#xff1a;htt…

一文带你了解阿里的开源Java诊断工具 :Arthas

Arthas 是阿里开源的 Java 诊断工具&#xff0c;相比 JDK 内置的诊断工具&#xff0c;要更人性化&#xff0c;并且功能强大&#xff0c;可以实现许多问题的一键定位&#xff0c;是我用到的最方便的诊断工具。 下载和安装见官网 https://arthas.aliyun.com/doc/profiler.html 下…

Gem5模拟器,如何在linux系统中查看内存、CPU、硬盘、进程、网络等信息(十二)

虽然说&#xff0c;这个记录的是与Linux相关的操作&#xff0c;每次查每次忘&#xff0c;必须写一个来归总一下&#xff0c;以免我漫山遍野找命令。但是不想新开一一个主题&#xff0c;再加上确实是在运行模拟器时会关注这方面的信息&#xff0c;就把这一节搁这儿啦。 常见的查…

MedCalc v20.217 医学ROC曲线统计分析参考软件

MedCalc是一款医学 ROC 曲线统计软件,用于ROC曲线分析的参考软件,医学工作者设计的医学计算器,功能齐全。它可以帮助医生快速作出普通的医学计算,从而对症下药。提供超过76种常用的规则和方法,包括:病人数据、单位参数、费用计算等等。甚至可以将图形另存为BMP,PNG,GIF…

ATL中__if_exists的替代方案

__if_exists 和 __if_not_exists 是什么? __if_exists 和 __if_not_exists 是微软 ATL (Active Template Library&#xff0c;活动模板库) 中的关键字&#xff0c;可以用来在编译期间测试一个标识符是否存在。如果该标识符存在&#xff0c;则其关联的语句将会被执行。 __if_e…

2023年3月软考中级(系统集成项目管理工程师)报名走起!!!

系统集成项目管理工程师是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目之一&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职…

【Vue】Vue常见的6种指令

Vue的6种指令-前言指令&#xff08;Directives&#xff09;是vue 为开发者提供的模板语法&#xff0c;用于辅助开发者渲染页面的基本结构。vue 中的指令按照不同的用途可以分为如下6 大类① 内容渲染指令 ② 属性绑定指令 ③ 事件绑定指令 ④ 双向绑定指令 ⑤ 条件渲染指令 ⑥ …

Fortinet设备审计

作为网络安全领域的领导者&#xff0c;Fortinet提供了多种网络安全解决方案&#xff0c;包括下一代防火墙&#xff0c;即FortiGate。通过EventLog Analyzer的FortiGate预定义报表以及其他Fortinet应用程序的详尽列表&#xff0c;充分发挥Fortinet设备的最大作用。FortiGate您的…

粒子群算法

粒子群算法1 粒子群算法介绍2 基本思想3 算法流程4 代码实现1 粒子群算法介绍 粒子群优化算法(PSO&#xff1a;Particle swarm optimization) 是一种进化计算技术&#xff08;evolutionary computation&#xff09;。源于对鸟群捕食的行为研究。粒子群优化算法的基本思想是通过…

我建议,专家不要再建议了

作者| Mr.K 编辑| Emma来源| 技术领导力(ID&#xff1a;jishulingdaoli)关于买房&#xff0c;专家建议&#xff1a;不建议掏空六个钱包凑首付。&#xff08;网友&#xff1a;丈母娘等不到我自己挣够&#xff09;关于农村剩男多&#xff0c;城市剩女多&#xff0c;专家建议&am…