智能指针及强相关知识经验总结 --- 移动语义、引用计数、循环引用、move()、自定义删除器等

目录

前言

一、shared_ptr

1. 基本用法和构造方法

2. 引用计数机制

3. weak_ptr 解决循环引用

二、unique_ptr

1. 基本用法和构造方法

2. 独占性

3. 所有权转移

1)unique_ptr :: release()

2)移动语义 和 move()

三、 对比 shared_ptr 和 unique_ptr

unique_ptr:

shared_ptr:

自定义删除器

默认删除器

总结 


前言

        智能指针的主要目的是自动管理内存分配和释放,以减少程序员错误和减轻程序员的负担。它是通过一个特殊的类,封装了一个指针,并添加一些额外的语义来实现的。使用智能指针的好处在于,它可以帮助程序员避免常见的内存错误,如内存泄漏和使用已被释放的内存。智能指针还可以使代码更加清晰和易于理解,因为它们使得内存分配和所有权转移变得显式和清晰。现我们将从概念和实际场景对共享指针独占指针进行理解和使用,掌握如何高效的使用智能指针来便捷整个项目的编码操作。

一、shared_ptr

1. 基本用法和构造方法

对其而言,有两种常用的构造方法,具体使用方法和普通指针类似,下面给出示例:

构造方法:

// 1.创建一个 shared_ptr,指向 int 类型的对象
std::shared_ptr<int> sharedInt = std::make_shared<int>(42);// 2.创建一个 shared_ptr,指向一个动态分配的对象
std::shared_ptr<double> sharedDouble(new double(3.14));

访问指针(使用):

// 1.视作普通指针进行访问
*sharedInt = 10;
std::cout << "Value of sharedInt: " << *sharedInt << std::endl;
// 2.调用get()方法取到普通指针再访问
*sharedInt.get() = 20;
std::cout << "Value of sharedInt: " << *sharedInt.get() << std::endl;

通过上面的代码示例,我们了解到 shared_ptr 的常用构造方法有两种:

(1)使用 make_shared():是推荐的创建 shared_ptr 的方法,它在单次分配中同时创建对象和控制块,效率更高

(2)直接使用 “new” 出的对象初始化:可以直接使用 new 关键字创建动态分配的对象并将其传递给 shared_ptr 构造函数。但这样做可能导致性能损失,因为需要额外的内存用于控制块。

对指针的访问方法明明可以直接对智能指针变量进行访问,那 get() 方法存在的必要性是什么?

当我们需要将指针传给接受普通指针的函数或功能模块时,智能指针是不能被函数接收后自动隐式类型转换为所需类型的,所以 get() 方法很好地解决了这个问题,下面给出示例代码:

print_int(sharedInt);       // 编译报错
print_int(sharedInt.get());

2. 引用计数机制

shared_ptr 特性:

  • 提供共享所有权的智能指针。
  • 使用引用计数来追踪资源的所有者数量。
  • 当最后一个 shared_ptr 指向资源销毁时,资源被释放。

至于共享所有权为了便于对比理解,本文后面再阐述,这里先来理解引用计数的概念:

引用计数:

share_ptr 使用一个控制块(control block)来管理引用计数和其他信息。控制块是在内存中动态分配的,包含引用计数和指向实际对象的指针。当创建新的 shared_ptr 时,会为对象分配一个新的控制块,并将引用计数初始化为 1当共享指针被复制或赋值时,引用计数递增当共享指针被销毁(超出作用域)时,引用计数递减。当引用计数变为零时,说明没有任何指针指向该对象,因此对象和控制块的内存都会被释放。

shared_ptr :: use_count()

shared_ptr :: reset()

为了便于理解,下面利用 shared_ptr 内置的 use_count() 方法,使用代码和运行结果帮助感受引用计数变化的过程: 

// 首次创建一个 shared_ptr,引用计数初始为 1
std::shared_ptr<int> sp1 = std::make_shared<int>(42);
// 打印引用计数
cout << sp1.use_count() << endl;// 创建另一个 shared_ptr,引用计数增加为 2
std::shared_ptr<int> sp2 = sp1;
cout << sp2.use_count() << endl;// 创建另一个 shared_ptr,引用计数增加为 3
std::shared_ptr<int> sp3 = sp2;
cout << sp3.use_count() << endl;// 销毁一个 shared_ptr 对象,引用计数减 1,变为 2
sp2.reset();
cout << sp3.use_count() << endl;

运行结果:

要注意的是,使用引用来引用智能指针变量并不会引起变量引用计数的变化

// 为了避免代码冗杂,下面代码直接续在前面代码后,不再重复给出
auto& ref_sp = sp1;
cout << ref_sp.use_count() << endl;

 运行结果:

ref_sp 仅仅是智能指针 sp1 的一个引用,它们共享相同的引用计数,因此不会增加引用计数。这只是一个别名,没有创建新的智能指针对象。

3. weak_ptr 解决循环引用

weak_ptr 主要用于解决 shared_ptr 的循环引用问题。循环引用可能导致对象无法正常释放,因为 shared_ptr 的引用计数永远不会变为零。通过使用 weak_ptr,可以打破循环引用,允许对象在不再被引用时正常释放。

具体什么是循环引用,下面给出简单示例:

class ObjectB; // 提前声明class ObjectA {
public:std::shared_ptr<ObjectB> objectB;       // 注意这里是 shared_ptrObjectA() {std::cout << "ObjectA constructed" << std::endl;}~ObjectA() {std::cout << "ObjectA destructed" << std::endl;}
};class ObjectB {
public:std::weak_ptr<ObjectA> objectA;        // 注意这里是 weak_ptrObjectB() {std::cout << "ObjectB constructed" << std::endl;}~ObjectB() {std::cout << "ObjectB destructed" << std::endl;}
};

考虑两个对象相互引用的情况,其中 ObjectA 持有 shared_ptr<ObjectB>,而 ObjectB 持有 shared_ptr<ObjectA>。这样的循环引用会导致对象永远无法释放。在此案例中,只需要将两者其中之一改为 weak_ptr 即可解决他俩相互推脱都不释放的循环引用造成的问题。

测试用例:

// 创建 shared_ptr 和 weak_ptr
std::shared_ptr<ObjectA> sharedA = std::make_shared<ObjectA>();
std::shared_ptr<ObjectB> sharedB = std::make_shared<ObjectB>();// 建立关联
sharedA->objectB = sharedB;
sharedB->objectA = sharedA;

运行结果: 

当我们正常创建并初始化上面两类对象时,通过观察控制台窗口打印的构造和析构函数内容,发现 weak_ptr 成功使得两对象被系统释放并调用各自的析构函数。

weak_ptr :: lock()

由于 lock() 函数具有返回其保留的对象的能力,所以我们可以利用其特性,在以 weak_ptr 为类成员的类中自建一个功能,用于判断该 weak_ptr 包含的对象即 shared_ptr 是否存在,有没有被释放掉,不妨我们将其命名为 is_exist() ,将其作为类成员函数,方便我们利用类对象调用从而了解以 weak_ptr 为类型的成员变量包含的对象是否被释放。

class ObjectB {
public:std::weak_ptr<ObjectA> objectA;// 构造函数和析构函数bool is_exist()    // 提供访问方法{// 使用 lock() 获取 shared_ptrif (auto sharedPtr = objectA.lock()) {// 对象存在,可以安全地使用 sharedPtrstd::cout << "Object exist!" << std::endl;return true;}else {// 对象已经被销毁std::cout << "Object has been released" << std::endl;return false;}}
};

调用测试 is_exist() 功能:

void test3()
{// 创建 shared_ptr 和 weak_ptrstd::shared_ptr<ObjectA> sharedA = std::make_shared<ObjectA>();std::shared_ptr<ObjectB> sharedB = std::make_shared<ObjectB>();// 建立关联sharedA->objectB = sharedB;sharedB->objectA = sharedA;sharedB->is_exist();    // 测试B类中的 weak_ptr// 1.reset作用于sharedBsharedB.reset();         // 销毁sharedB 使其恢复刚构造完成的状态// 在使用 sharedB 之前检查是否为空if (sharedB) {sharedB->is_exist();}else {std::cout << "sharedB is null." << std::endl;}
}

运行结果:

接着我们尝试销毁ObjectB类型对象内部的 weak_ptr 指针包含的对象:

void test3()
{// 创建 shared_ptr 和 weak_ptrstd::shared_ptr<ObjectA> sharedA = std::make_shared<ObjectA>();std::shared_ptr<ObjectB> sharedB = std::make_shared<ObjectB>();// 建立关联sharedA->objectB = sharedB;sharedB->objectA = sharedA;sharedB->is_exist();      // reset()销毁前作对比用// 2.reset作用于ObjectB类型对象内部的weak_ptr指针包含的对象sharedB->objectA.reset();sharedB->is_exist();
}

运行结果:

我们发现实现的 is_exist() 功能成功反映了 weak_ptr 对象是否被销毁的状态,以便我们进一步安全操作,由于 reset() 后指针变为悬空指针,通过检测避免出现访问悬空指针的情况。

二、unique_ptr

1. 基本用法和构造方法

std::unique_ptr 是 C++11 引入的智能指针,用于管理动态分配的对象,它独占(unique ownership)所指向的对象。与 shared_ptr 不同,std::unique_ptr 不使用引用计数,因此每个 std::unique_ptr 拥有对对象的唯一所有权。这意味着当 std::unique_ptr 被销毁或通过 std::move 转移所有权时,它所管理的对象会被销毁

构造方法:

比如需要创建一个已有类的智能指针,下面给出示例类的声明:

class MyClass {
public:MyClass() {std::cout << "MyClass constructed" << std::endl;}~MyClass() {std::cout << "MyClass destructed" << std::endl;}void DoSomething() {std::cout << "Doing something..." << std::endl;}
};

 具体构造语句:

// 创建 unique_ptr
std::unique_ptr<MyClass> uniquePtr1 = std::make_unique<MyClass>();
std::unique_ptr<MyClass> uniquePtr2(new MyClass);

使用方法:

// 使用 unique_ptr
uniquePtr1->DoSomething();
uniquePtr1.get()->DoSomething();
uniquePtr2->DoSomething();
uniquePtr2.get()->DoSomething();

运行结果: 

对于 unique_ptr 而言,与 shared_ptr 一致的是存在 get() 方法可以取到普通指针。不同的是没有引用计数,独占所拥有的对象,那独占性又是如何体现呢?

2. 独占性

std::unique_ptr 是一种独占所有权的智能指针,它确保一个对象只能由一个 std::unique_ptr 拥有。这意味着任何时候,只有一个 std::unique_ptr 指向一个特定的动态分配对象。这是通过禁止复制构造函数和赋值运算符来实现的,因为这些操作会导致多个 std::unique_ptr 指向同一个对象,破坏了独占性。

注意:这里的独占性是仅仅允许一对一的情况存在,不可多对一和一对多!

不妨我们下面采用各种方式来验证其独占性的规则:

// 创建 unique_ptr
std::unique_ptr<MyClass> uniquePtr1 = std::make_unique<MyClass>();// 1. 尝试复制构造
// std::unique_ptr<MyClass> uniquePtr2 = uniquePtr1; // 编译错误// 2. 尝试赋值运算符
// std::unique_ptr<MyClass> uniquePtr3;
// uniquePtr3 = uniquePtr1; // 编译错误

具体编译报错形式(注意编译器检查标红):

另外,我们还有通过其他方法验证独占性,由于部分概念还未阐述,具体操作会在后面给出。

3. 所有权转移

        出于编程过程中实际需要,我们定义的 unique_ptr 不得不在其他地方被延续使用下去,但是出于一对一的独占性要求,unique_ptr 是不可复制的(包括上面验证的复制构造函数、赋值运算符)。此时应运而生的就是 “所有权转移” ,既然不能复制那就将自身拥有的对象转移给别的对象进行管理。

下面给出案例,便于理解 “所有权转移” 的使用方法和存在意义:

1)unique_ptr :: release()

现在假定我们需要使用该指针作为函数参数实现功能,而接受的参数类型为C语音风格的普通指针,这时候就有两种选择:

(1)利用unique_ptr :: get()

(2)利用unique_ptr :: release()

        通过上图我们了解到 release() 的本质是转移其拥有指针的所有权,以返回值传递权限,通过接受该函数返回值实现所有权接收,要注意的是 release() 后,unique_ptr 会自动置空。这里需要区分一个概念, release() 仅仅是将 unique_ptr 对象的指针置为 nullptr,并不会影响已经转移的资源(或对象)

这意味着上面两方法的本质区别就在于 release() 该行以后的代码都应避免访问和使用 release() 后的 unique_ptr 指针,因为 release() 后的指针此时变为悬空指针,再次访问会报错或产生未定义行为。

为了便于反映问题,我们对 MyClass 类稍作修改,使其具有一个成员变量 n ,类声明如下:

class MyClass {
public:MyClass() {std::cout << "MyClass constructed" << std::endl;}~MyClass() {std::cout << "MyClass destructed" << std::endl;}void DoSomething() {std::cout << "Doing something..." << std::endl;}int n{};     // 自主初始化空
};

我们将以普通指针作函数参数的功能命名为:use_uniquePtr_1(MyClass* mc),实际实现如下:

void use_uniquePtr_1(MyClass* mc)
{cout << "use_uniquePtr_1(MyClass* mc)" << endl;mc->DoSomething();
}

 来看如下代码示例调用上方函数:

// ************* 普通指针 <--> unique_ptr.release() **************
std::unique_ptr<MyClass> up = std::make_unique<MyClass>();use_uniquePtr_1(up.get());
if (!up) { cout << "(1) up is nullptr" << endl; }use_uniquePtr_1(up.release());
if (!up) { cout << "(2) up is nullptr" << endl; }

运行结果:

我们看到运行结果最后并没有打印执行析构函数,正是因为 release() 将 up 的所有权转交给use_uniquePtr_1(up.release()); 函数,然而在该函数内并未在结束时对 up 存储的对象进行释放处理,导致此函数后面执行部分也不能释放这块内存,也就自然造成了内存泄漏,为程序后续更大的灾难性错误奠定了基础。

如果我再给代码后面加一句访问 up 的操作,会报错吗?

std::unique_ptr<MyClass> up = std::make_unique<MyClass>();
use_uniquePtr_1(up.get());
if (!up) { cout << "(1) up is nullptr" << endl; }use_uniquePtr_1(up.release());
if (!up) { cout << "(2) up is nullptr" << endl; }up->DoSomething();     // ### 注意此行 ###
if (!up) { cout << "(3) up is nullptr" << endl; }

运行结果:

我们看到程序依旧没有报错,成功结束整个调试过程,但是我们明明在 release() 后 up 变为了空指针,为什么在调用类内成员函数 up->DoSomething(); 时没有报错呢?结束了,但是不代表没有进行非法操作,将同样的代码在部分编译器和平台就会运行报错!为什么这里程序正常结束没有报错访问空指针?

在 C++ 中,调用空指针的成员函数并不一定会导致运行时错误。在这种情况下,DoSomething()  函数可能是一个非虚函数,而非虚函数在调用时并不会引发空指针解引用导致的崩溃。当然,也有前提就是不通过this指针访问成员变量,比如我下面给出论证:

std::unique_ptr<MyClass> up = std::make_unique<MyClass>();
use_uniquePtr_1(up.get());
if (!up) { cout << "(1) up is nullptr" << endl; }
cout << up->n << endl;			// 注意此行use_uniquePtr_1(up.release());
if (!up) { cout << "(2) up is nullptr" << endl; }
cout << up->n << endl;			// 注意此行up->DoSomething();
if (!up) { cout << "(3) up is nullptr" << endl; }

运行上面代码:

嘿嘿,报错了吧,不急不急,访问this指针才报错是吧,如果将 release() 后的cout代码行换为访问类内静态成员的值,还会报错吗?

代码如下:

std::unique_ptr<MyClass> up = std::make_unique<MyClass>();
use_uniquePtr_1(up.get());
if (!up) { cout << "(1) up is nullptr" << endl; }
//cout << up->n << endl;
cout << up->s_n << endl;			// 注意此行use_uniquePtr_1(up.release());
if (!up) { cout << "(2) up is nullptr" << endl; }
//cout << up->n << endl;
cout << up->s_n << endl;			// 注意此行

运行结果:

具体该部分访问空指针的问题可以参考本人另外一篇文章:悬空指针 ---- 未定义行为

2)移动语义和 move()

        我们了解利用move()实现功能的本质还是实行对象所有权的转移,那么我们不妨以此方法来解决和上面 unique_ptr :: release() 情境下的问题,但是我们注意到 move() 返回值类型与参数类型一致,所以仅需改变函数功能要求,将参数从C语言普通指针转换为智能指针类型,即可实现 move() 测试将外部智能指针通过参数传递方式实现所有权转移。

为了便于理解 move() 的功能,下面给出简单示例:

void uniquePointer_move()
{std::unique_ptr<MyClass> up1 = std::make_unique<MyClass>();std::unique_ptr<MyClass> up2 = move(up1);     // 通过move()使得up2接管了up1拥有的对象// move()后只会释放 up2 管理的对象,而不会释放 up1 的对象// ### up1 在构造时调用构造函数, up2 在析构时调用析构函数 ###
}

运行结果: 

可以看到正如代码注释部分预测的情况一致,并且没有发生内存泄漏的情况。

为了与上面 release() 情形对应,将函数功能名称定义为use_uniquePtr_2(unique_ptr<MyClass>& mc),这里需要对函数参数为智能指针引用类型并非值类型的原因作出解释:unique_ptr 是独占所有权的智能指针,它的移动构造函数将转移所有权的同时使原指针为空。因此,在 use_uniquePtr_2 调用后,实参将不再拥有对象的所有权,实参的值将变成 nullptr。这就导致在 use_uniquePtr_2 函数内部,mc(形参) 是一个空的 nullptr,而在尝试使用 mc->DoSomething(); 时,会导致空指针解引用,从而编译器报错。

具体测试实现如下:

void uniquePointer_move()
{std::unique_ptr<MyClass> up1 = std::make_unique<MyClass>();std::unique_ptr<MyClass> up2 = move(up1);if (up1)      // 试图对 move() 转移后丢失对象所有权的智能指针访问{cout << "up1 is not nullptr" << endl;use_uniquePtr_2(up1);}use_uniquePtr_2(up2);
}

运行结果:

通过结果发现 use_uniquePtr_2(up1); 并未被调用,说明经 move() 函数转移对象所有权后,智能指针up1被置空,还原为初始默认状态,为空指针。如果后面代码在未对 up1 判空的情况下使用,就会造成悬空指针的风险,发生程序未定义行为或程序崩溃

三、 对比 shared_ptr 和 unique_ptr

unique_ptr:

  1. 独占所有权:

    unique_ptr 独占其所指向的对象的所有权。一个特定的 unique_ptr 是唯一能够拥有和管理其指向对象的智能指针。
  2. 轻量级:

    由于独占所有权,unique_ptr 通常比 shared_ptr 更轻量级,因为它不需要维护引用计数。
  3. 移动语义:

    支持移动语义,可以通过移动转移所有权,避免复制开销
  4. 适用场景:

    当你有一个明确的所有权关系,且对象不需要被多个智能指针共享时,使用unique_ptr

shared_ptr:

  1. 共享所有权:

    shared_ptr允许多个智能指针共享对同一对象的所有权,通过引用计数来追踪对象的引用次数。
  2. 相对重量级:

    由于需要维护引用计数,shared_ptr相对于unique_ptr来说可能更重量级。
  3. 循环引用问题:

    当存在循环引用时,shared_ptr需要谨慎使用,因为它可能导致内存泄漏
  4. 适用场景:

    当需要多个智能指针共享同一个对象,并且对象的生命周期不容易预测时,使用shared_ptr

自定义删除器

        众所周知C++中类内的析构函数可以自定义实现释放资源时要进行的额外特定操作,虽然智能指针也是类,但是我们无法修改其析构函数以帮助我们实现释放资源时进行的相关操作,于是我们可以通过构建自定义删除器来指定创建的智能指针对象在析构时要额外执行的指令,通俗讲自定义删除器本质上也是一种仿函数。现在利用仿函数自定义 free() 对C语音风格函数返回的指针处理,并将该规则传给智能指针,删除器简单实现如下:

class FreeDeleter       // 自定义删除器
{
public:void operator()(void* p){free(p);}
};

假定返回一个堆区指针,其解引用所得值为100,函数定义如下:

int* some_c_function()
{int* p = (int*)malloc(sizeof(int));*p = 100;return p;
}

在该函数外利用智能指针接受返回值,实现自动管理返回指针指向的内存空间,请注意如下两种写法的异同:

void do_work_1()        // unique_ptr 接收
{auto p = unique_ptr<int, FreeDeleter>(some_c_function());printf("%d\n", *p.get());
}
void do_work_2()        // shared_ptr 接收
{auto p = shared_ptr<int>(some_c_function(), FreeDeleter());printf("%d\n", *p.get());
}

注意:在 shared_ptr 的构造函数中提供的删除器是用于释放资源的,而在 unique_ptr 模板参数中提供的删除器是用于释放资源的,因此使用时需要小心确保删除器的正确性。

那 shared_ptr 和 unique_ptr 两者对象绑定自定义删除器的方式可以互换吗?

这是运行前的编译器报错显示,所以答案显然是不可以的。

默认删除器

其实如果想要利用智能指针来接管已有指针的指向的内存或包含的对象,更容易想到的是利用默认删除器来实现:

void do_work_3()
{auto p = unique_ptr<int>(some_c_function());printf("%d\n", *p.get());
}

评价:

  1. 简洁性:更为简洁,省略了提供自定义删除器的步骤,适用于一般情况。

  2. 默认删除器:使用默认的删除器,这是 unique_ptr 的默认行为,会调用 delete 进行内存释放。(这就意味着无论处理的是 malloc 还是 new 产生的堆区指针,都用 delete 释放)

        总体来说,选择取决于你对内存释放的需求和是否有额外的清理操作。在一般情况下,使用默认删除器的 do_work_3 更为简洁,而提供自定义删除器的 do_work_1/2 更适用于需要特定清理操作的情况


总结 

易错点和注意事项:

  • std::shared_ptr 需要小心循环引用。
  • 避免裸指针操作,尽可能使用智能指针的接口。
  • 避免混合使用 new/deletestd::make_shared/std::make_unique
  • 不要将 std::shared_ptr 转为 std::unique_ptr,除非确保没有其他 shared_ptr 指向同一资源
  • 注意对智能指针部分操作可能带来的悬空指针问题

        在选择智能指针时,根据对象的所有权需求和额外的清理操作来决定使用 shared_ptr 还是 unique_ptr。shared_ptr 适用于多个指针共享对象的情况,而 unique_ptr 更适合独占所有权的场景。通过灵活运用它们的特性,可以提高代码的安全性和效率。希望本文对正处于学习智能指针阶段的探索者能有一定的指导作用,为日常实际项目的运用中提供有用的建议。

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

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

相关文章

Linux中项目部署步骤

安装jdk&#xff0c;tomcat 安装步骤 1&#xff0c;将压缩包&#xff0c;拷贝到虚拟机中。 通过工具&#xff0c;将文件直接拖到虚拟机的/home下 2&#xff0c;回到虚拟机中&#xff0c;查看/home下&#xff0c;有两个压缩文件 3&#xff0c;给压缩文件做解压缩操作 tar -z…

Hive数据倾斜之:数据类型不一致导致的笛卡尔积

Hive数据倾斜之&#xff1a;数据类型不一致导致的笛卡尔积 目录 Hive数据倾斜之&#xff1a;数据类型不一致导致的笛卡尔积一、问题描述二、原因分析三、精度损失四、问题解决 一、问题描述 如果两张表的jion&#xff0c;关联键分布较均匀&#xff0c;没有明显的热点问题&…

计算机基础知识64

ForeignKey属性 to&#xff1a;设置要关联的表 related_name&#xff1a; 反向操作时&#xff0c;使用的字段名&#xff0c;用于代替原反向查询时的’表名_set’ related_query_name:反向查询操作时&#xff0c;使用的连接前缀&#xff0c;用于替换表名 to_field:设置要关联的表…

最长连续序列(leetcode 128)

文章目录 1.问题描述2.难度等级3.热门指数4.解题思路方法一&#xff1a;排序方法二&#xff1a;哈希表 5.实现示例参考文献 1.问题描述 给定一个未排序的整数数组 nums &#xff0c;找出数字连续的最长序列&#xff08;不要求序列元素在原数组中连续&#xff09;的长度。 请你…

【华为OD题库-049】评论转换输出-java

题目 在一个博客网站上&#xff0c;每篇博客都有评论。每一条评论都是一个非空英文字母字符串。评论具有树状结构&#xff0c;除了根评论外&#xff0c;每个评论都有一个父评论。 当评论保存时&#xff0c;使用以下格式: 首先是评论的内容; 然后是回复当前评论的数量。 最后是当…

如何保持操纵机构丝杆的精度?

滚珠丝杆是操纵机构中的重要组成部分&#xff0c;可以传递较高的扭矩&#xff0c;并且具有低摩擦、高效率和快速响应的特性&#xff0c;这使得操纵机构能够实现高速、高精度的运动控制&#xff0c;这对于整个系统的性能和精度具有决定性的影响&#xff0c;保持操纵机构丝杆的精…

互联网Java工程师面试题·Spring Boot篇·第二弹

目录 8、什么是 YAML&#xff1f; 9、如何实现 Spring Boot 应用程序的安全性&#xff1f; 10、如何集成 Spring Boot 和 ActiveMQ&#xff1f; 11、如何使用 Spring Boot 实现分页和排序&#xff1f; 12、什么是 Swagger&#xff1f;你用 Spring Boot 实现了它吗&#xff1f; …

YoloV5改进策略:Swift Parameter-free Attention,无参注意力机制,超分模型的完美迁移

摘要 https://arxiv.org/pdf/2311.12770.pdf https://github.com/hongyuanyu/SPAN SPAN是一种超分网络模型。SPAN模型通过使用参数自由的注意力机制来提高SISR的性能。这种注意力机制能够增强重要信息并减少冗余,从而在图像超分辨率过程中提高图像质量。 具体来说,SPAN模…

【wvp】测试记录

ffmpeg 这是个莫名其妙的报错&#xff0c;通过排查&#xff0c;应该是zlm哪个进程引起的 会议室的性能 网络IO也就20M

全志T527设置gpio口输出高电平实际输出低电平

前言 在调试T527的时候&#xff0c;主板另外添加了gpio口去控制usb口的电源开关&#xff0c;软件上面需要在内核运行的时候将gpio口设置输出高电平&#xff0c;usb口才可以正常使用。改好系统固件后&#xff0c;升级发现&#xff0c;机器开机动画过程中可以控制gpio口去打开us…

ArkUI组件--Button组件

1.声明Button组件 Button(label?:ResourceStr) #label是按钮上显示的文本 ①label是文字类型 所写文字会在按钮上显示 ②不输入label内容&#xff0c;需要额外定义一些描述。例如插入图片&#xff08;需要定义图片属性&#xff09; Button(){Image($r(app.media.xxx)).wi…

【题目】栈和队列专题

文章目录 专题一&#xff1a;栈系列1. 中缀表达式转后缀表达式&#xff08;逆波兰式&#xff09;2. 有效的括号3. 用栈实现队列4. 最小栈 专题一&#xff1a;栈系列 1. 中缀表达式转后缀表达式&#xff08;逆波兰式&#xff09; 算法原理 2. 有效的括号 题目链接 算法原理 代…

SpringBoot-监听Nacos动态修改日志级别

目录 一、pom文件 二、项目配置文件 三、日志配置文件 四、日志监听类 五、日志动态修改服务类 线上系统的日志级别一般都是 INFO 级别&#xff0c;有时候需要查看 WARN 级别的日志&#xff0c;所以需要动态修改日志级别。微服务项目中使用 Nacos 作为注册中心&#xff0c…

C++面试宝典第2题:逆序输出整数

题目 写一个方法&#xff0c;将一个整数逆序打印输出到控制台。注意&#xff1a;当输入的数字含有结尾的0时&#xff0c;输出不应带有前导的0。比如&#xff1a;123的逆序输出为321&#xff0c;8600的逆序输出为68&#xff0c;-609的逆序输出为-906。 解析 这道题本身并没有什么…

Java架构师技术架构路线

目录 1 概论2 如何规划短中长期的技术架构路线图3 如何规划面向未来的架构4 如何修订路线图执行过程中的偏差5 如何落地路线图-阿里系糙快猛之下的敏捷模式想学习架构师构建流程请跳转:Java架构师系统架构设计 1 概论 首先,规划一个短中长期的技术路线图是非常重要的。短中…

java SSM毕业生信息管理myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

前言 学校的规模不断扩大&#xff0c;学生数量急剧增加&#xff0c;有关学生的各种信息量也成倍增长。面对庞大的信息量需要有学生信息管理系统来提高学生管理工作的效率。通过这样的系统可以做到信息的规范管理、科学统计和快速查询、修改、增加、删除等&#xff0c;从而减少管…

lv11 嵌入式开发 RTC 17

目录 1 RTC简介 ​编辑2 Exynos4412下的RTC控制器 2.1 概述 2.2 特征 2.3 功能框图 3 寄存器介绍 3.1 概述 3.2 BCD格式的年月日寄存器 3.3 INTP中断挂起寄存器 3.4 RTCCON控制寄存器 3.5 CURTICCNT 作为嘀嗒定时器使用的寄存器 4 RTC编程 5 练习 1 RTC简介 RTC(…

关于优雅的使用SQL多行转多列的记录(doris)

文章目录 应用需求场景记录过程1. 准备数据2. 给数据根据姓名分组&#xff0c;加上序号.3. 根据name分组成map结构4. 拆分map 应用需求场景 准备的数据是这样的&#xff1a; 需要将每个人的成绩显示在一行上&#xff0c;需要的结果如下&#xff0c;但是我的情况是课程有非常…

联想LJ2655DN激光打印机清零方法

随着打印机的使用越来越频繁&#xff0c;需要更换耗材的时候也越来越多&#xff1b;但是更换上新的耗材后&#xff0c;很多用户都会遇到一个问题&#xff0c;就是更换完新的耗材后打印机仍然提示寿命将近&#xff0c;或者无墨粉盒灯情况&#xff0c;这个时候就需要我们对打印机…

qt 5.15.2 主窗体事件及绘制功能

qt 5.15.2 主窗体事件及绘制功能 显示主窗体效果图如下所示&#xff1a; main.cpp #include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[]) {QApplication a(argc, argv);MainWindow w;w.setFixedWidth(600);w.setFixedHeight(6…