当前位置: 首页 > news >正文

C++要求或禁止在堆中产生对象

有时你想这样管理某些对象,要让某种类型的对象能够自我销毁,也就是能够“delete this”。很明显这种管理方式需要此类型对象被分配在堆中。而其它一些时候你想获得一种保障:“不在堆中分配对象,从而保证某种类型的类不会发生内存泄漏。”如果你在嵌入式系统上工作,就有可能遇到这种情况,发生在嵌入式系统上的内存泄漏是极其严重的,其堆空间是非常珍贵的。有没有可能编写出代码来要求或禁止在堆中产生对象(heap-based object)呢?通常是可以的,不过这种代码也会把“on the heap”的概念搞得比你脑海中所想的要模糊。

  • 要求在堆中建立对象

让我们先从必须在堆中建立对象开始说起。为了执行这种限制,你必须找到一种方法禁止以调用“new”以外的其它手段建立对象。这很容易做到。非堆对象(non-heap object)在定义它的地方被自动构造,在生存时间结束时自动被释放,所以只要禁止使用隐式的构造函数和析构函数,就可以实现这种限制。

把这些调用变得不合法的一种最直接的方法是把构造函数和析构函数声明为private。这样做副作用太大。没有理由让这两个函数都是private。最好让析构函数成为private,让构造函数成为public。处理过程与条款26相似,你可以引进一个专用的伪析构函数,用来访问真正的析构函数。客户端调用伪析构函数释放他们建立的对象。(WQ加注:注意,异常处理体系要求所有在栈中的对象的析构函数必须申明为公有!)

例如,如果我们想仅仅在堆中建立代表unlimited precision numbers(无限精确度数字)的对象,可以这样做:

class UPNumber {

public:

  UPNumber();

  UPNumber(int initValue);

  UPNumber(double initValue);

  UPNumber(const UPNumber& rhs);

  // 伪析构函数 (一个const 成员函数, 因为

  // 即使是const对象也能被释放。)

  void destroy() const { delete this; }

  ...

private:

  ~UPNumber();

};

然后客户端这样进行程序设计:

UPNumber n;                          // 错误! (在这里合法, 但是

                                     // 当它的析构函数被隐式地

                                     // 调用时,就不合法了)

UPNumber *p = new UPNumber;          //正确

...

delete p;                            // 错误! 试图调用

                                     // private 析构函数

p->destroy();                        // 正确

另一种方法是把全部的构造函数都声明为private。这种方法的缺点是一个类经常有许多构造函数,类的作者必须记住把它们都声明为private。否则如果这些函数就会由编译器生成,构造函数包括拷贝构造函数,也包括缺省构造函数;编译器生成的函数总是public(参见Effecitve C++ 条款45)。因此仅仅声明析构函数为private是很简单的,因为每个类只有一个析构函数。

通过限制访问一个类的析构函数或它的构造函数来阻止建立非堆对象,但是在条款26已经说过,这种方法也禁止了继承和包容(containment):

class UPNumber { ... };              // 声明析构函数或构造函数

                                     // 为private

class NonNegativeUPNumber:

  public UPNumber { ... };           // 错误! 析构函数或

                                     //构造函数不能编译

class Asset {

private:

  UPNumber value;

  ...                                // 错误! 析构函数或

                                     //构造函数不能编译

};

这些困难不是不能克服的。通过把UPNumber的析构函数声明为protected(同时它的构造函数还保持public)就可以解决继承的问题,需要包含UPNumber对象的类可以修改为包含指向UPNumber的指针:

class UPNumber { ... };              // 声明析构函数为protected

class NonNegativeUPNumber:

  public UPNumber { ... };           // 现在正确了; 派生类

                                     // 能够访问

                                     // protected 成员

class Asset {

public:

  Asset(int initValue);

  ~Asset();

  ...

private:

  UPNumber *value;

};

Asset::Asset(int initValue)

: value(new UPNumber(initValue))      // 正确

{ ... }

Asset::~Asset()

{ value->destroy(); }                 // 也正确

  • 判断一个对象是否在堆中

如果我们采取这种方法,我们必须重新审视一下“在堆中”这句话的含义。上述粗略的类定义表明一个非堆的NonNegativeUPNumber对象是合法的:

NonNegativeUPNumber n;                // 正确

那么现在NonNegativeUPNumber对象n中的UPNumber部分也不在堆中,这样说对么?答案要依据类的设计和实现的细节而定,但是让我们假设这样说是不对的,所有UPNumber对象 —即使是做为其它派生类的基类—也必须在堆中。我们如何能强制执行这种约束呢?

没有简单的办法。UPNumber的构造函数不可能判断出它是否做为堆对象的基类而被调用。也就是说对于UPNumber的构造函数来说没有办法侦测到下面两种环境的区别:

NonNegativeUPNumber *n1 =

  new NonNegativeUPNumber;            // 在堆中

NonNegativeUPNumber n2;               //不再堆中

不过你可能不相信我。也许你想你能够在new操作符、operator new和new 操作符调用的构造函数的相互作用中玩些小把戏(参见条款M8)。可能你认为你比他们都聪明,可以这样修改UPNumber,如下所示:

class UPNumber {

public:

  // 如果建立一个非堆对象,抛出一个异常

  class HeapConstraintViolation {};

  static void * operator new(size_t size);

  UPNumber();

  ...

private:

  static bool onTheHeap;                 //在构造函数内,指示

                                         // 对象是否被构造在

  ...                                    // 堆上

};

// obligatory definition of class static

bool UPNumber::onTheHeap = false;

void *UPNumber::operator new(size_t size)

{

  onTheHeap = true;

  return ::operator new(size);

}

UPNumber::UPNumber()

{

  if (!onTheHeap) {

    throw HeapConstraintViolation();

  }

  proceed with normal construction here;

  onTheHeap = false;                    // 为下一个对象清除标记

}

如果不再深入研究下去,就不会发现什么错误。这种方法利用了这样一个事实:“当在堆上分配对象时,会调用operator new来分配raw memory”,operator new设置onTheHeap为true,每个构造函数都会检测onTheHeap,看对象的raw memory是否被operator new所分配。如果没有,一个类型为HeapConstraintViolation的异常将被抛出。否则构造函数如通常那样继续运行,当构造函数结束时,onTheHeap被设置为false,然后为构造下一个对象而重置到缺省值。

这是一个非常好的方法,但是不能运行。请考虑一下这种可能的客户端代码:

UPNumber *numberArray = new UPNumber[100];

第一个问题是为数组分配内存的是operator new[],而不是operator new,不过(倘若你的编译器支持它)你能象编写operator new一样容易地编写operator new[]函数。更大的问题是numberArray有100个元素,所以会调用100次构造函数。但是只有一次分配内存的调用,所以100个构造函数中只有第一次调用构造函数前把onTheHeap设置为true。当调用第二个构造函数时,会抛出一个异常,你真倒霉。

即使不用数组,bit-setting操作也会失败。考虑这条语句:

UPNumber *pn = new UPNumber(*new UPNumber);

这里我们在堆中建立两个UPNumber,让pn指向其中一个对象;这个对象用另一个对象的值进行初始化。这个代码有一个内存泄漏,我们先忽略这个泄漏,这有利于下面对这条表达式的测试,执行它时会发生什么事情:

new UPNumber(*new UPNumber)

它包含new 操作符的两次调用,因此要调用两次operator new和调用两次UPNumber构造函数(参见条款8)。程序员一般期望这些函数以如下顺序执行:

调用第一个对象的operator new

调用第一个对象的构造函数

调用第二个对象的operator new

调用第二个对象的构造函数

但是C++语言没有保证这就是它调用的顺序。一些编译器以如下这种顺序生成函数调用:

调用第一个对象的operator new

调用第二个对象的operator new

调用第一个对象的构造函数

调用第二个对象的构造函数

编译器生成这种代码丝毫没有错,但是在operator new中set-a-bit的技巧无法与这种编译器一起使用。因为在第一步和第二步设置的bit,第三步中被清除,那么在第四步调用对象的构造函数时,就会认为对象不再堆中,即使它确实在。

这些困难没有否定让每个构造函数检测*this指针是否在堆中这个方法的核心思想,它们只是表明检测在operator new(或operator new[])里的bit set不是一个可靠的判断方法。我们需要更好的方法进行判断。

如果你陷入了极度绝望当中,你可能会沦落进不可移植的领域里。例如你决定利用一个在很多系统上存在的事实,程序的地址空间被做为线性地址管理,程序的栈从地址空间的顶部向下扩展,堆则从底部向上扩展:

在以这种方法管理程序内存的系统里(很多系统都是,但是也有很多不是这样),你可能会想能够使用下面这个函数来判断某个特定的地址是否在堆中:

// 不正确的尝试,来判断一个地址是否在堆中

bool onHeap(const void *address)

{

  char onTheStack;                   // 局部栈变量

  return address < &onTheStack;

}

这个函数背后的思想很有趣。在onHeap函数中onTheSatck是一个局部变量。因此它在堆栈上。当调用onHeap时,它的栈框架(stack frame)(也就是它的activation record)被放在程序栈的顶端,因为栈在结构上是向下扩展的(趋向低地址),onTheStack的地址肯定比任何栈中的变量或对象的地址小。如果参数address的地址小于onTheStack的地址,它就不会在栈上,而是肯定在堆上。

到目前为止,这种逻辑很正确,但是不够深入。最根本的问题是对象可以被分配在三个地方,而不是两个。是的,栈和堆能够容纳对象,但是我们忘了静态对象。静态对象是那些在程序运行时仅能初始化一次的对象。静态对象不仅仅包括显示地声明为static的对象,也包括在全局和命名空间里的对象(参见条款47)。这些对象肯定位于某些地方,而这些地方既不是栈也不是堆。

它们的位置是依据系统而定的,但是在很多栈和堆相向扩展的系统里,它们位于堆的底端。先前内存管理的图片到讲述的是事实,而且是很多系统都具有的事实,但是没有告诉我们这些系统全部的事实,加上静态变量后,这幅图片如下所示:

onHeap不能工作的原因立刻变得很清楚了,不能辨别堆对象与静态对象的区别:

void allocateSomeObjects()

{

  char *pc = new char;               // 堆对象: onHeap(pc)

                                     // 将返回true

  char c;                            // 栈对象: onHeap(&c)

                                     // 将返回false

  static char sc;                    // 静态对象: onHeap(&sc)

                                     // 将返回true

  ...

}

现在你可能不顾一切地寻找区分堆对象与栈对象的方法,在走头无路时你想在可移植性上打主意,但是你会这么孤注一掷地进行一个不能获得正确结果的交易么?绝对不会。我知道你会拒绝使用这种虽然诱人但是不可靠的“地址比对”技巧。

令人伤心的是不仅没有一种可移植的方法来判断对象是否在堆上,而且连能在多数时间正常工作的“准可移植”的方法也没有。如果你实在非得必须判断一个地址是否在堆上,你必须使用完全不可移植的方法,其实现依赖于系统调用,只能这样做了。因此你最好重新设计你的软件,以便你可以不需要判断对象是否在堆中。

如果你发现自己实在为对象是否在堆中这个问题所困扰,一个可能的原因是你想知道对象是否能在其上安全调用delete。这种删除经常采用“delete this”这种声明狼籍的形式。不过知道“是否能安全删除一个指针”与“只简单地知道一个指针是否指向堆中的事物”不一样,因为不是所有在堆中的事物都能被安全地delete。再考虑包含UPNumber对象的Asset对象:

class Asset {

private:

  UPNumber value;

  ...

};

Asset *pa = new Asset;

很明显*pa(包括它的成员value)在堆上。同样很明显在指向pa->value上调用delete是不安全的,因为该指针不是被new返回的。

幸运的是“判断是否能够删除一个指针”比“判断一个指针指向的事物是否在堆上”要容易。因为对于前者我们只需要一个operator new返回的地址集合。因为我们能自己编写operator new函数(参见Effective C++条款8—条款10),所以构建这样一个集合很容易。如下所示,我们这样解决这个问题:

void *operator new(size_t size)

{

  void *p = getMemory(size);         //调用一些函数来分配内存,

                                     //处理内存不够的情况

  把 p加入到一个被分配地址的集合;

  return p;

}

void operator delete(void *ptr)

{

  releaseMemory(ptr);                // return memory to

                                     // free store

  从被分配地址的集合中移去ptr;

}

bool isSafeToDelete(const void *address)

{

  返回address是否在被分配地址的集合中;

}

这很简单,operator new在地址分配集合里加入一个元素,operator delete从集合中移去项目,isSafeToDelete在集合中查找并确定某个地址是否在集合中。如果operator new 和 operator delete函数在全局作用域中,它就能适用于所有的类型,甚至是内建类型。

在实际当中,有三种因素制约着对这种设计方式的使用。第一是我们极不愿意在全局域定义任何东西,特别是那些已经具有某种含义的函数,象operator new和operator delete。正如我们所知,只有一个全局域,只有一种具有正常特征形式(也就是参数类型)的operator new和operator delete(条款9)。这样做会使得我们的软件与其它也实现全局版本的operator new 和operator delete的软件(例如许多面向对象数据库系统)不兼容。

我们考虑的第二个因素是效率:如果我们不需要这些,为什么还要为跟踪返回的地址而负担额外的开销呢?

最后一点可能有些平常,但是很重要。实现isSafeToDelete让它总能够正常工作是不可能的。难点是多继承下来的类或继承自虚基类的类有多个地址,所以无法保证传给isSafeToDelete的地址与operator new 返回的地址相同,即使对象在堆中建立。有关细节参见条款M24和条款M31。

我们希望这些函数提供这些功能时能够不污染全局命名空间,没有额外的开销,没有正确性问题。幸运的是C++使用一种抽象mixin基类满足了我们的需要。

抽象基类是不能被实例化的基类,也就是至少具有一个纯虚函数的基类。mixin(“mix in”)类提供某一特定的功能,并可以与其继承类提供的其它功能相兼容(参见Effective C++条款7)。这种类几乎都是抽象类。因此我们能够使用抽象混合(mixin)基类给派生类提供判断指针指向的内存是否由operator new分配的能力。该类如下所示:

class HeapTracked {                  // 混合类; 跟踪

public:                              // 从operator new返回的ptr

  class MissingAddress{};            // 异常类,见下面代码

  virtual ~HeapTracked() = 0;

  static void *operator new(size_t size);

  static void operator delete(void *ptr);

  bool isOnHeap() const;

private:

  typedef const void* RawAddress;

  static list<RawAddress> addresses;

};

这个类使用了list(链表)数据结构跟踪从operator new返回的所有指针,list是标准C++库的一部分(参见Effective C++条款49和本书条款35)。operator new函数分配内存并把地址加入到list中;operator delete用来释放内存并从list中移去地址元素。isOnHeap判断一个对象的地址是否在list中。

HeapTracked类的实现很简单,调用全局的operator new和operator delete函数来完成内存的分配与释放,list类里的函数进行插入操作和删除操作,并进行单语句的查找操作。以下是HeapTracked的全部实现:

// mandatory definition of static class member

list<RawAddress> HeapTracked::addresses;

// HeapTracked的析构函数是纯虚函数,使得该类变为抽象类。

// (参见Effective C++条款14). 然而析构函数必须被定义,

//所以我们做了一个空定义。.

HeapTracked::~HeapTracked() {}

void * HeapTracked::operator new(size_t size)

{

  void *memPtr = ::operator new(size);  // 获得内存

  addresses.push_front(memPtr);         // 把地址放到list的前端

  return memPtr;

}

void HeapTracked::operator delete(void *ptr)

{

  //得到一个 "iterator",用来识别list元素包含的ptr;

  //有关细节参见条款35

  list<RawAddress>::iterator it =

    find(addresses.begin(), addresses.end(), ptr);

  if (it != addresses.end()) {       // 如果发现一个元素

    addresses.erase(it);             //则删除该元素

    ::operator delete(ptr);          // 释放内存

  } else {                           // 否则

    throw MissingAddress();          // ptr就不是用operator new

  }                                  // 分配的,所以抛出一个异常

}

bool HeapTracked::isOnHeap() const

{

  // 得到一个指针,指向*this占据的内存空间的起始处,

  // 有关细节参见下面的讨论

  const void *rawAddress = dynamic_cast<const void*>(this);

  // 在operator new返回的地址list中查到指针

  list<RawAddress>::iterator it =

    find(addresses.begin(), addresses.end(), rawAddress);

  return it != addresses.end();      // 返回it是否被找到

}

尽管你可能对list类和标准C++库的其它部分不很熟悉,代码还是很一目了然。条款M35将解释这里的每件东西,不过代码里的注释已经能够解释这个例子是如何运行的。

只有一个地方可能让你感到困惑,就是这个语句(在isOnHeap函数中)

const void *rawAddress = dynamic_cast<const void*>(this);

我前面说过带有多继承或虚基类的对象会有几个地址,这导致编写全局函数isSafeToDelete会很复杂。这个问题在isOnHeap中仍然会遇到,但是因为isOnHeap仅仅用于HeapTracked对象中,我们能使用dynamic_cast操作符(参见条款M2)的一种特殊的特性来消除这个问题。只需简单地放入dynamic_cast,把一个指针dynamic_cast成void*类型(或const void*或volatile void* 。。。。。),生成的指针将指向“原指针指向对象内存”的开始处。但是dynamic_cast只能用于“指向至少具有一个虚拟函数的对象”的指针上。我们该死的isSafeToDelete函数可以用于指向任何类型的指针,所以dynamic_cast也不能帮助它。isOnHeap更具有选择性(它只能测试指向HeapTracked对象的指针),所以能把this指针dynamic_cast成const void*,变成一个指向当前对象起始地址的指针。如果HeapTracked::operator new为当前对象分配内存,这个指针就是HeapTracked::operator new返回的指针。如果你的编译器支持dynamic_cast 操作符,这个技巧是完全可移植的。

使用这个类,即使是最初级的程序员也可以在类中加入跟踪堆中指针的功能。他们所需要做的就是让他们的类从HeapTracked继承下来。例如我们想判断Assert对象指针指向的是否是堆对象:

class Asset: public HeapTracked {

private:

  UPNumber value;

  ...

};

我们能够这样查询Assert*指针,如下所示:

void inventoryAsset(const Asset *ap)

{

  if (ap->isOnHeap()) {

    ap is a heap-based asset inventory it as such;

  }

  else {

    ap is a non-heap-based asset record it that way;

  }

}

象HeapTracked这样的混合类有一个缺点,它不能用于内建类型,因为象int和char这样的类型不能继承自其它类型。不过使用象HeapTracked的原因一般都是要判断是否可以调用“delete this”,你不可能在内建类型上调用它,因为内建类型没有this指针。

  • 禁止堆对象

判断对象是否在堆中的测试到现在就结束了。与此相反的领域是“禁止在堆中建立对象”。通常对象的建立这样三种情况:对象被直接实例化;对象做为派生类的基类被实例化;对象被嵌入到其它对象内。我们将按顺序地讨论它们。

禁止用户直接实例化对象很简单,因为总是调用new来建立这种对象,你能够禁止用户调用new。你不能影响new操作符的可用性(这是内嵌于语言的),但是你能够利用new操作符总是调用operator new函数这点(参见条款M8),来达到目的。你可以自己声明这个函数,而且你可以把它声明为private。例如,如果你想不想让用户在堆中建立UPNumber对象,你可以这样编写:

class UPNumber {

private:

  static void *operator new(size_t size);

  static void operator delete(void *ptr);

  ...

};

现在用户仅仅可以做允许它们做的事情:

UPNumber n1;                         // okay

static UPNumber n2;                  // also okay

UPNumber *p = new UPNumber;          // error! attempt to call

                                     // private operator new

把operator new声明为private就足够了,但是把operator new声明为private,而把iperator delete声明为public,这样做有些怪异,所以除非有绝对需要的原因,否则不要把它们分开声明,最好在类的一个部分里声明它们。如果你也想禁止UPNumber堆对象数组,可以把operator new[]和operator delete[](参见条款M8)也声明为private。(operator new和operator delete之间的联系比大多数人所想象的要强得多。有关它们之间关系的鲜为人知的一面,可以参见我的文章counting objects里的sidebar部分。)

有趣的是,把operator new声明为private经常会阻碍UPNumber对象做为一个位于堆中的派生类对象的基类被实例化。因为operator new和operator delete是自动继承的,如果operator new和operator delete没有在派生类中被声明为public(进行改写,overwrite),它们就会继承基类中private的版本,如下所示:

class UPNumber { ... };             // 同上

class NonNegativeUPNumber:          //假设这个类

  public UPNumber {                 //没有声明operator new

  ...

};

NonNegativeUPNumber n1;             // 正确

static NonNegativeUPNumber n2;      // 也正确

NonNegativeUPNumber *p =            // 错误! 试图调用

  new NonNegativeUPNumber;          // private operator new

如果派生类声明它自己的operator new,当在堆中分配派生对象时,就会调用这个函数,于是得另找一种不同的方法来防止UPNumber基类的分配问题。UPNumber的operator new是private这一点,不会对包含UPNumber成员对象的对象的分配产生任何影响:

class Asset {

public:

  Asset(int initValue);

  ...

private:

  UPNumber value;

};

Asset *pa = new Asset(100);          // 正确, 调用

                                     // Asset::operator new 或

                                     // ::operator new, 不是

                                     // UPNumber::operator new

实际上,我们又回到了这个问题上来,即“如果UPNumber对象没有被构造在堆中,我们想抛出一个异常”。当然这次的问题是“如果对象在堆中,我们想抛出异常”。正像没有可移植的方法来判断地址是否在堆中一样,也没有可移植的方法判断地址是否不在堆中,所以我们很不走运,不过这也丝毫不奇怪,毕竟如果我们能辨别出某个地址在堆上,我们也就能辨别出某个地址不在堆上。但是我们什么都不能辨别出来。

相关文章:

C++要求或禁止在堆中产生对象

有时你想这样管理某些对象&#xff0c;要让某种类型的对象能够自我销毁&#xff0c;也就是能够“delete this”。很明显这种管理方式需要此类型对象被分配在堆中。而其它一些时候你想获得一种保障&#xff1a;“不在堆中分配对象&#xff0c;从而保证某种类型的类不会发生内存泄…...

为什么阿里开发手册推荐用静态工厂方法代替构造器?

&#x1f345; 作者简介&#xff1a;哪吒&#xff0c;CSDN2021博客之星亚军&#x1f3c6;、新星计划导师✌、博客专家&#x1f4aa; &#x1f345; 哪吒多年工作总结&#xff1a;Java学习路线总结&#xff0c;搬砖工逆袭Java架构师 &#x1f345; 技术交流&#xff1a;定期更新…...

前端写法建议【让项目更加易于维护】

背景 标题前提条件&#xff1a; 没有字典接口、或其他原因&#xff0c;需要前端手动维护的情况 示例环境&#xff1a;vue2&#xff0c;其他项目同理 示例 如果项目有某种类别&#xff0c;前端和后端约定好了&#xff0c;某些情况下&#xff0c;需要前端写死时。 比如有字段…...

EasyExcel 自定义转换器、自定义导出字典映射替换、满足条件内容增加样式,完整代码+详细注释说明

虽然最之前是在其他地方看到的&#xff0c;但最终因缘巧合下找到了原文&#xff0c;还是尊重一下原作者。 参考引用了这位佬的博客&#xff0c;确实方便使用。 https://blog.csdn.net/qq_45914616/article/details/137200688?spm1001.2014.3001.5502 这是一个基于Easyexcel通过…...

C语言学习笔记 Day10(指针--中)

Day10 内容梳理&#xff1a; 目录 Chapter 7 指针 7.4 指针 & 数组 &#xff08;1&#xff09;指针操作数组元素 &#xff08;2&#xff09;指针加减运算 1&#xff09;加法 2&#xff09;减法 &#xff08;3&#xff09;指针数组 7.5 多级指针 Chapter 7 指针 …...

网页显示打印 pdf

文件服务使用 minio&#xff0c;使用 nginx 反向代理。 将文件存放在 minio 上&#xff0c;如果是公开的文件&#xff0c;则统一放到一个桶&#xff0c;设置为公开只读。 如果是私有文件&#xff0c;则使用临时链接&#xff0c;给有权限的用户查看和打印。 要实现在 html 页…...

1948-2024.5金融许可信息明细数据

1948-2024.5金融许可信息明细数据 1、时间&#xff1a;1948-2024.5 2、指标&#xff1a;来源表、机构编码、机构名称、所属银行、机构类型、业务范围、机构住所、地理坐标、行政区划代码、所属区县、所属城市、所属省份、邮政编码、发证日期、批准日期、发证机关、流水号、是…...

【笔记】从零开始做一个精灵龙女-画贴图阶段(终)

这篇主要是细节&#xff0c;包括花纹和其它一些细化 皮肤 脖子 脖子一定要压暗&#xff0c;不然前后关系体现不出来 脸 1. 忘了有uv缝了&#xff0c;记得打开投影模式画 顺着头发轨迹长的方向画出发际线 背包手镯 1.先画出暗色花纹&#xff1a; 2.再加亮色&#xff0c;亮…...

从MySQL到Elasticsearch:创建酒店索引案例

在现代的数据管理中&#xff0c;Elasticsearch&#xff08;简称ES&#xff09;因其强大的搜索功能和灵活的索引结构而受到广泛欢迎。本篇博客将介绍如何根据MySQL数据库中的酒店表定义&#xff0c;创建一个相应的Elasticsearch索引。 MySQL与Elasticsearch的对比 在开始之前&…...

Webkit与Web Push API:提升用户体验的推送技术

Web Push API是一种允许网站向用户发送通知的Web技术&#xff0c;即使用户没有打开网站也能接收到信息。这项技术可以显著提升用户的参与度和满意度。Webkit&#xff0c;作为Safari和其他浏览器的内核&#xff0c;对Web Push API的支持情况如何&#xff1f;本文将深入探讨Web P…...

Java线程池的拒绝策略

在 Java 线程池中&#xff0c;常见的拒绝策略&#xff1a; AbortPolicy&#xff08;中止策略&#xff09; 特点&#xff1a;直接抛出 RejectedExecutionException 异常来拒绝新任务的提交。应用场景&#xff1a;适用于对系统的稳定性要求较高&#xff0c;不希望丢失任务&#…...

【C++进阶】继承

【C进阶】继承 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;C&#x1f96d; &#x1f33c;文章目录&#x1f33c; 1. 继承的概念及定义 1.1 继承的概念 1.2 继承定义 1.2.1 定义格式 1.2.2 继承父类成员访问方式的变化 1.3 继承类模…...

立体相机镜面重建(一)镜面标定

无论是单目、双目或者是多屏幕镜面重建&#xff0c;都需要事先对屏幕和相机的相对位置进行标定&#xff0c;求得相机到屏幕之间的相对变换关系。如果求得屏幕和相机之间的变换关系呢&#xff1f;接下来是标定流程。 &#xff08;一&#xff09;准备&#xff1a; 1&#xff09…...

【如何有效解决前端Vue中的常见难题】

&#x1f41f;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢编程&#x1fab4; &#x1f419;个人主页&#x1f947;&#xff1a;Aic山鱼&#x1f420;WeChat&#xff1a;z7010cyy &#x1f988;系列专栏&#xff1a;&#x1f3de;️ 前端-JS基础专栏✨前端-Vue框架专栏…...

CLAMP-1靶机渗透测试

一、靶机下载地址 https://www.vulnhub.com/entry/clamp-101,320/ 二、信息收集 1、主机发现 # 使用命令 nmap 192.168.145.0/24 -sn | grep -B 2 "00:0C:29:88:B4:BF" 2、端口扫描 # 使用命令 nmap 192.168.145.0/24 -p- -sV 3、指纹识别 # 使用命令 whatweb …...

JavaScript中的Truthy Falsy值以及等号判断

1.Falsy & Truthy Falsy的值false&#xff0c;0&#xff0c;-0&#xff0c; “”&#xff0c; null&#xff0c; undefined&#xff0c;NaNTruthy的值除了以上之外的其他值 2.等号判断 console.log(10 10); console.log(0 ); console.log(0 false); console.log( fa…...

uniapp——展开和收起

案例展示 代码 后台返回的数据格式如下&#xff1a; {1: "大富科技速度快放假手动阀",2: "第三方斯蒂芬斯蒂芬是的开发时间",4: "45345345",5: "电饭锅电饭锅地方" }<view class"tipTitle">温馨提示</view> &l…...

WebGL2学习(2): GLSL ES 3.0

更多精彩内容尽在 dt.sim3d.cn &#xff0c;关注公众号【sky的数孪技术】&#xff0c;技术交流、源码下载请添加VX&#xff1a;digital_twin123 WebGL 2.0 给 GLSL 带来了重大变化。WebGL 1.0 中使用的 GLSL 版本是 GLSL ES 1.0。 WebGL 2.0 中仍然可用。但是&#xff0c;通过编…...

[大模型实战] DAMODEL云算力平台部署LLama3.1大语言模型

[大模型实战] DAMODEL云算力平台部署LLama3.1大语言模型 目录 一、LLama3.1二、DAMODEL云算力平台2.1 提供的服务2.1.1 AI训练2.1.2 AI推理2.1.3 高性能计算2.1.4 图像&#xff0f;视频渲染2.1.5 定制化部署 2.2 支持的GPU 三、在DAMODEL部署LLama3.13.1 在DAMODEL创建实例&…...

驱动开发系列09 - Linux设备模型之设备,驱动和总线

一:概述 Linux 设备模型(LDM)是 Linux 内核中引入的一个概念。用于管理内核对象(那些需要引用计数的对象、例如文件、设备、总线甚至驱动程序),以及描述它们之间的层次结构,以及这些内核对象之间绑定关系。Linux 设备模型引入了对象生命周期管理、引用计数、以及面向对象…...

HTML实现弹出层

leopard/ˈlepərd/ 豹子&#xff0c;豹纹 弹出层指的是鼠标悬停于某个元素之上时显示的一个界面组件。 关注和理解特性&#xff1a;z-index属性和动态生成HTML元素。 HTML5新增&#xff1a; figure:媒体内容(图像&#xff0c;音频&#xff0c;视频)&#xff0c;用于包含一…...

Android控件详解

在Android应用程序中&#xff0c;界面由布局和组件组成。布局相当于框架&#xff0c;而控件则是框架里面的内容。了解过Android布局后&#xff0c;如果要设计ui界面&#xff0c;还需要了解和掌握各个控件的应用。 一个界面的设计&#xff0c;先从创建容器开始&#xff0c;再向…...

记忆化搜索专题篇

目录 斐波那契数 不同路径 最长递增子序列 猜数字大小II 矩阵中的最长递增路径 声明&#xff1a;下面将主要使用递归记忆化搜索来解决问题&#xff01;&#xff01;&#xff01; 斐波那契数 题目 思路 斐波那契数的特点就是除了第一个数是0&#xff0c;第二个数是1&…...

入网测评检查项大全(安全资料)

1. Linux操作系统 2. Windows操作系统 3. Tomcat中间件 4. Nginx中间件 5. Mysql数据库 6. Weblogic中间件 7. Oracle数据库 8. Redis数据库 9. 达梦数据库 10. 应用系统 11. 渗透测试 13 .AIX操作系统 14 .中创中间件 15 .IIS中间件 16 .Apache中间件 17 .Mari…...

uni-app 开发App时调用uni-push 实现在线系统消息推送通知 保姆教程

一、引言 在开发App时避免不了需要推送系统通知&#xff0c;以提高用户的使用体验。在自己的一个工具型的小app上全流程接入了uni-push2.0的推送能力&#xff0c;做个记录&#xff0c;以防后期需要用到。在阅读本教程前最好先看看官方文档&#xff0c;结合官方文档使用&#xf…...

13.StringRedisTemplete使用

上一篇说到改变了RedisTemplate的默认序列化器后&#xff0c;在redis中存入Java对象后&#xff0c;在redis中的呈现是&#xff1a;会记录类的字节码 这也是代码中可以强制装换为对应的java对象的原因&#xff1a; Test void testStudent() {redisTemplate.opsForValue().set(&q…...

[工具]-gitee+pycharm-配置

安装git ​ 查看git是否安装设置成功&#xff1a; ​ git config user.name ​ git config user.email ​ 码云账号设置邮箱 pycharm设置gitee 打开 PyCharm&#xff0c;在 Settings - Plugins 里面&#xff0c;搜索 Gitee 插件&#xff0c;安装后重启 PyCharm。 pychar…...

中间件是一种在客户端和服务器之间进行通信和处理的软件组件或服务

中间件是一种在客户端和服务器之间进行通信和处理的软件组件或服务。中间件位于应用程序和操作系统之间&#xff0c;可以提供一些功能&#xff0c;如请求转发、数据转换、安全性和身份验证、日志记录等。 中间件的主要作用是将应用程序与底层基础设施解耦&#xff0c;提供了一…...

RCE-eval长度限制突破技巧

目录 一、长度17的限制绕过 1、最简单的绕过 &#xff08;一&#xff09;绕过 &#xff08;二&#xff09;编写一句话木马 2、文件包含的利用 &#xff08;一&#xff09;远程文件包含的利用 &#xff08;二&#xff09;本地文件包含的利用 3、usort绕过 &#xff08…...

【黑马】MyBatis

目录 MyBatis简介JDBC缺点&#xff1a;MyBatis针对于JDBC进行简化&#xff0c;简化思路&#xff1a; MyBatis快速入门具体构建步骤解决SQL映射文件的警告提示 Mapper代理开发案例&#xff1a;使用Mapper代理方式完成案例具体步骤详解&#xff1a;Mapper代理方式 Mapper核心配置…...