【C++学习笔记】RAII思想——智能指针

news2024/11/27 16:38:03

智能指针

  • 1 内存泄漏问题
  • 2 RAII(Resource Acquisition Is Initialization)
    • 2.1 使用RAII思想设计的SmartPtr类
    • 2.2 智能指针的原理
    • 2.3 小总结智能指针原理
  • 3 智能指针的拷贝问题
    • 3.1 std::auto_ptr
    • 3.2 std::unique_ptr
    • 3.3 std::shared_ptr
      • 3.3.1 拷贝构造函数
      • 3.3.2 赋值函数
    • 3.4 std::shared_ptr的线程安全问题
      • 3.4 .1智能指针引用计数操作加锁
      • 3.4.2 外界内存资源是非线程安全的
    • 3.5 std::shared_ptr 引发的循环引用问题
  • 4 std::weak_ptr 解决循环引用问题
    • 4.1 weak_ptr解决循环引用的原理
    • 4.2 简易版weak_ptr的实现
  • 5 删除器
  • 6 C++11和boost中智能指针的关系
  • 7 总结

1 内存泄漏问题

在讲智能指针之前,我们先来看下面这段代码有什么问题?

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	// 1、如果p1这里new 抛异常会如何?

	// 2、如果p2这里new 抛异常会如何?

	// 3、如果div调用这里又会抛异常会如何?

	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

这段代码有内存泄漏问题。

1.对p1来说,如果new空间失败了,它会抛出异常直接跳转到main函数进行处理。所以,如果只是p1抛异常不会造成内存泄漏问题。

2.如果p1申请空间成功,对p2来说,如果new空间失败了,它也会抛出异常直接跳转到main函数进行处理,但是p1的内存空间并没有得到释放,会造成内存泄漏问题。
未使用智能指针的解决方式如下:

	int* p1 = new int;
	int* p2 = nullptr;
	try
	{
		p2 = new int;
	}
	catch(...)//表示捕获任意异常
	{
		delete p1;//p2抛异常的话,在这里释放掉p1占的内存
		throw;//重新抛出异常给main函数
	}
	cout << div() << endl;
	delete p1;
	delete p2;

3.假如p1,p2都申请内存成功,并未抛出异常。但是div()函数抛出了异常,直接跳转到main函数,会导致p1和p2的内存空间都没有释放,造成内存泄漏。
未使用智能指针的解决方式如下:

	int* p1 = new int;
	int* p2 = nullptr;
	try
	{
		p2 = new int;
	}
	catch(...)//表示捕获任意异常
	{
		delete p1;
		throw;
	}
	try {
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		delete p2;
		throw;
	}
	delete p1;
	delete p2;
}

以上的的解决方法虽然解决了抛异常可能会造成的内存泄漏问题,但是从代码上来说太过繁琐,不够优雅。
可以用RAII的思想来解决问题。

2 RAII(Resource Acquisition Is Initialization)

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

1.不需要显式地释放资源。

2.采用这种方式,对象所需的资源在其生命期内始终保持有效。

2.1 使用RAII思想设计的SmartPtr类

首先先设计一个SmartPtr类,类中有构造函数和析构函数,如果想申请空间和销毁空间,分别交给构造函数和析构函数即可。

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

完整代码如下:

template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	SmartPtr<int> p1(new int);//申请空间,因为有析构函数出了作用域自动释放
	SmartPtr<int> p2(new int);
	cout << div() << endl;
}

int main()
{
	try

	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

2.2 智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		delete _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

使用案例:

class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		delete _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
struct Date //为了方便演示定义一个日期类
{
	int year;
	int month;
	int day;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	SmartPtr<int> p1(new int);
	*p1 = 10;
	cout << *p1 << endl;
	SmartPtr<Date> p2(new Date);
	//这里本来应该是p2.operator->()->year=2023;
	//语法上可以省略括号:p2->->year=2023;
	//为了语法可读性,可以写成p2->year=2023;
	p2->year = 2023;
	p2->month = 7;
	p2->day = 4;
	cout << p2->year << " " << p2->month << " " << p2->day << " " << endl;
	cout << div() << endl;
}

int main()
{
	try

	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

2.3 小总结智能指针原理

  1. RAII特性

  2. 重载operator*和opertaor->,具有像指针一样的行为。

3 智能指针的拷贝问题

在智能指针的基础上,来看一下下面这段代码:

class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		delete _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
int main()
{
	SmartPtr<int> sp1(new int(1));
	SmartPtr<int> sp2(sp1);
	return 0;
}

运行该代码,程序会直接崩溃,原因是同一块空间,析构了两次。
原因是这里的默认的拷贝构造函数是浅拷贝,但是这里不能用深拷贝来解决问题,因为C++原本的指针赋值就是浅拷贝。

C++提供以下这些库来解决浅拷贝问题

3.1 std::auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。

auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理

std::auto_ptr官方文档

// C++98 管理权转移 auto_ptr


	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			// 管理权转移
			sp._ptr = nullptr;//把被拷贝的指针置空了
		}
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 检测是否为自己给自己赋值
			if (this != &ap)
			{
				// 释放当前对象中资源
				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}
		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 像指针一样使用

		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

// 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
int main()
{
 std::auto_ptr<int> sp1(new int);
 std::auto_ptr<int> sp2(sp1); // 管理权转移
//  sp1被置空
 *sp2 = 10;
 cout << *sp2 << endl;
 cout << *sp1 << endl;
 return 0;
//}

主要的思想就是为了防止浅拷贝带来的重复析构同一块空间问题,auto_ptr这个库在处理的时候,拷贝完成后,就把被拷贝的指针给置空了,这样就不会出现重复析构了问题了。

但是这种方式显然是不合理的。
接下来重点介绍以下三个库

3.2 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr
std::unique_ptr官方文档

C++11库才更新智能指针实现

C++11出来之前,boost搞出了更好用scoped_ptr/shared_ptr/weak_ptr
C++11将boost库中智能指针精华部分吸收了过来

C++11->unique_ptr/shared_ptr/weak_ptr

unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理

template<class T>

class unique_ptr

{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}
	~unique_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
	// 像指针一样使用

	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	//因为类里面会默认写出拷贝构造函数和赋值函数,
	//所以这里要手动声明一下
	//后面加个 =delete 就是防止被拷贝的意思
	unique_ptr(const unique_ptr<T>&sp) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
private:
	T* _ptr;
};

这里直接用库里的测试一下
确实是不让拷贝
在这里插入图片描述

3.3 std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
std::shared_ptr官方文档

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。

  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。

  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;

  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

由于要使用到引用计数,如果有多个对象指向同一块空间,这里我们叫做空间a,需要统计有几个对象。
这里采用的方案是再开辟一块空间给引用计数,这块空间我们叫做空间b,当空间a被一个对象指向时,空间b就计数++,当一个对象不再指向该空间时,空间b就减1。当减到0时,说明这是最后一个对象指向空间a了,可以析构了。这样就避免了重复析构的问题。

如下图演示:
当有新增对象指向_ptr时,_count++;
在这里插入图片描述
在这里插入图片描述
当sp2不再指向_ptr时,_count-=1;

在这里插入图片描述
当sp1不再指向_ptr时,_count-=1,此时_count==0,可以析构了。
在这里插入图片描述

3.3.1 拷贝构造函数

这里拷贝构造函数代码分析如上图所示

template<class T>
	class my_shared_ptr
	{
	public:
		my_shared_ptr(T* ptr)
			:_ptr(ptr),_count(new int(1))
		{}
		my_shared_ptr(const my_shared_ptr<T>& sp)
			:_ptr(sp._ptr),_count(sp._count)
		{
			add();//(*_count)++
		}
		void release()//--(*_count)并判断是否等于0
		{
			if (--(*_count) == 0)
			{
				delete _ptr;
				delete _count;
			}
		}
		void add()
		{
			(*_count)++;
		}
		~my_shared_ptr()
		{
			release();//--(*_count)==0,才能析构
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _count;//开辟一块引用计数的空间
	};
}

3.3.2 赋值函数

假如现在sp1,sp2,sp3指向了同一块空间,sp4指向另一空间。
现在要求赋值
sp1=sp4或者sp4=sp1
在这里插入图片描述
分析如下:

sp1=sp4

在这里插入图片描述
就是说sp1指向了sp4的空间。那么原来sp1指向的_count就要减去1
然后把sp1指向sp4即可,同时右边的_count要++在这里插入图片描述

sp4=sp1
在这里插入图片描述就是sp4指向的空间改为sp1指向的空间,然后sp4的_count-=1,由于_count==0,所以要析构掉图片右边的两个空间。左边_count++
在这里插入图片描述

代码如下:

		my_shared_ptr<T>& operator=(my_shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				release();
				_ptr = sp._ptr;
				_count = sp._count;
				add();
			}
			return *this;
		}
		void release()
		{
			if (--(*_count) == 0)
			{
				delete _ptr;
				delete _count;
			}
		}
		void add()
		{
			(*_count)++;
		}

3.4 std::shared_ptr的线程安全问题

接下来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时
    ++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错
    乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁
    的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

3.4 .1智能指针引用计数操作加锁

namespace ffg
{
	template<class T>
	class my_shared_ptr
	{
	public:
		my_shared_ptr(T* ptr)
			:_ptr(ptr),_count(new int(1)),_mtx(new mutex)
		{}
		my_shared_ptr(const my_shared_ptr<T>& sp)
			:_ptr(sp._ptr),_count(sp._count),_mtx(sp._mtx)
		{
			add();
		}
		my_shared_ptr<T>& operator=(my_shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				release();
				_ptr = sp._ptr;
				_count = sp._count;
				_mtx = sp._mtx;
				add();
			}
			return *this;
		}
		void release()//凡是涉及到++,--的操作都需要加锁处理。注意这里锁的内存空间释放问题
		{//不能提前delete _mtx,因为_count可能还大于0

			_mtx->lock();
			bool flag = false;
			if (--(*_count) == 0)
			{
				delete _ptr;
				delete _count;
				flag = true;
			}
			_mtx->unlock();
			if (flag)
			{
				delete _mtx;
			}
		}
		void add()//加锁
		{
			_mtx->lock();
			(*_count)++;
			_mtx->unlock();
		}
		~my_shared_ptr()
		{
			release();
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T* get() const
		{
			return _ptr;
		}
		int use_count()
		{
			return *_count;
		}
	private:
		T* _ptr;
		int* _count;
		mutex* _mtx;
	};
}

测试代码:

3.4.2 外界内存资源是非线程安全的

外界资源未加锁前:

struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
void SharePtrFunc(ffg::my_shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
	cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,加锁了,这里是线程安全的。
		//但是管理的资源并不是线程安全的,外界需要自己加锁处理,所以不是线程安全的。
		ffg::my_shared_ptr<Date> copy(sp);
		//mtx.lock();
		sp->_year++;
		sp->_month++;
		sp->_day++;
		//mtx.unlock();
	}
}
int main()
{
	ffg::my_shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	const size_t n = 10000;
	mutex mtx;
	thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	t1.join();
	t2.join();
	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
	cout << p.use_count() << endl;
	return 0;
}

这里的输出应该p->_year,p->_month,p->_day都应该是20000,但是由于线程安全问题,可能会导致结果出现错误。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
所以,需要程序员在外界加上锁。

void SharePtrFunc(ffg::my_shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
	cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,加锁了,这里是线程安全的。
		//但是管理的资源并不是线程安全的,外界需要自己加锁处理,所以不是线程安全的。
		ffg::my_shared_ptr<Date> copy(sp);
		mtx.lock();
		sp->_year++;
		sp->_month++;
		sp->_day++;
		mtx.unlock();
	}
}
int main()
{
	ffg::my_shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	const size_t n = 10000;
	mutex mtx;
	thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	t1.join();
	t2.join();
	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
	cout << p.use_count() << endl;
	return 0;
}

多次执行:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
直接使用C++库的shared_ptr也同理。

3.5 std::shared_ptr 引发的循环引用问题

先来看以下这两段代码:
建立了两个链表节点,n1,n2相互连接

struct ListNode
{
	ffg::my_shared_ptr<ListNode> _pre;
	ffg::my_shared_ptr<ListNode> _next;

	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};

int main()
{
	ffg::my_shared_ptr<ListNode> n1(new ListNode);
	ffg::my_shared_ptr<ListNode> n2(new ListNode);

	n1->_next = n2;//这里注释掉任意一行都能成功析构
	//n2->_pre = n1;
	return 0;
}

上面这段代码,在main函数里的
当有n1->_next=n2,或者n2->_pre=n1,不同时存在的时候n1,n2就能成功析构。
在这里插入图片描述
但是当这两行代码同时存在的时候,就不能成功析构。
在这里插入图片描述
先来分析以下第一种情况
这里假设是

n1->_next = n2;

如下图,当n2和左边的_next 指向右边的ListNode空间时,引用计数为2
在这里插入图片描述

然后n2析构了以后,右边的引用计数为1。
blog.csdnimg.cn/d7700d19400649b2a79f977aaed83bf1.png)

接下来到n1析构,引用计数为0,左边的_next随着ListNode的析构也被释放掉了,所以右边的引用计数也变为0,右边的ListNode也被析构了,_prve也不存在了。
在这里插入图片描述

接下来分析第二种情况,也就是循环引用的问题

在这里插入图片描述

n1->_next = n2;
n2->_pre = n1;

首先是n2先析构,右边的引用计数变为1
在这里插入图片描述
接下来是n1析构,左边的引用计数变为1
在这里插入图片描述
此时循环引用问题就来了。

n2和n1析构,引用计数都减到1,但是_next还是指向下一个节点,_prve还是指向上一个节点。
也就是说_next析构了,右边的ListNode就能释放,右边的_prev也能释放。
_prev析构了,左边的ListNode就能释放,左边的_next也能释放掉。
所以这两个ListNode互相牵制了,谁也不会释放。
左边的释放条件是右边释放,右边释放条件是左边释放。相互矛盾了。
这就是循环引用问题。

4 std::weak_ptr 解决循环引用问题

注意:
1.weak_ptr是专门设计出来,解决shard_ptr的循环引用问题。
2.它不是常规的智能指针,不支持RAII,不支持资源管理
3.支持像指针一样的功能

这里先使用库里面的weak_ptr看看效果
在这里插入图片描述
成功解决了循环引用问题。

4.1 weak_ptr解决循环引用的原理

原理很简单,就是:n1->_next = n2;和n2->_prev = n1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。

4.2 简易版weak_ptr的实现

	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const my_shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

测试:

struct ListNode
{
	ffg::weak_ptr<ListNode> _pre;
	ffg::weak_ptr<ListNode> _next;

	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};

int main()
{
	ffg::my_shared_ptr<ListNode> n1(new ListNode);
	ffg::my_shared_ptr<ListNode> n2(new ListNode);
	cout << n1.use_count() << endl;//1
	cout << n2.use_count() << endl;//1
	n1->_next = n2;
	n2->_pre = n1;
	return 0;
}

在这里插入图片描述
从图中可以看出引用计数变为1了,可以说明,_next和_prev不会在增加引用计数,这样就解决了循环引用问题。

5 删除器

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题。

库里面的删除器测试:
在这里插入图片描述

6 C++11和boost中智能指针的关系

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost 的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

7 总结

智能指针一开始是为了解决某些情况下因抛异常而导致内存泄漏问题。
由RAII思想设计的智能指针,会管理资源的内存和释放。它支持像指针一样的功能
但是从而由智能指针引发了诸如拷贝问题,线程安全问题,循环引用问题。
智能指针是线程安全的,因为库里设计的时候加锁处理了,但是外界的资源不是线程安全的,需要程序员处理。
一开始的C++98库中的auto_ptr解决拷贝问题是不合理的,于是有了unique_ptr(强行让智能指针不能做拷贝),shared_ptr(引用计数解决拷贝问题中的重复析构),weak_ptr是配合shared_ptr使用了,解决了循环引用问题。

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

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

相关文章

等保二级配置(四)口令复杂度策略和禁止root登录

口令复杂度策略 序号检查项名称检查指南符合性说明整改方法1口令复杂度策略1.确认账号现用密码&#xff1b; 2.查看配置文件/etc/pam.d/system-auth中相关参数。1.密码长度不小于8位&#xff0c;至少包括数字、大写字母、小写字母、特殊字符中的三种字符&#xff0c;非常见弱密…

23款奔驰GLS450时尚型更换原厂几何多光束大灯,让智能照亮您前行的路

奔驰几何多光束大灯核心特点就是通过内部的84颗可独立控制的LED光源&#xff0c;行车远光灯会甄别对向驶来的车辆或者行人&#xff0c;并且动态的跟随目标&#xff0c;之后阴影话该区域&#xff0c;避免晃到车辆和行人。

简单分享下小程序商城开发要多少钱

小程序商城的开发费用因多种因素而异&#xff0c;包括以下几点&#xff1a; 一、功能需求&#xff1a;商城的功能要求不同&#xff0c;开发所需的工作量也会有所不同。例如&#xff0c;基本的商品展示和购买功能相对较简单&#xff0c;而复杂的订单管理、支付、物流等功能可能需…

linux中安装Nginx的具体步骤

1.首先介绍一下Ngnix nginx是一款使用c语言编写的高性能的HTTP和反向代理服务器&#xff0c;特点是占有内存少&#xff0c;并发能力强&#xff0c;事实上Nginx的并发能力确实在同类型的网页服务器中表现得最好&#xff0c;中国大陆使用Nginx网站用户有&#xff1a;百度、京东、…

【IMX6ULL - LOGO替换】linux内核启动打印logo替换详细教程

Starting kernel ...[ 0.000000] Booting Linux on physical CPU 0x0 [ 0.000000] Linux version 4.1.15 (root@ubuntu) (gcc version 5.3.0 (GCC) ) #1 SMP PREEMPT Thu Jul 6 16:06:11 CST 2023这里我们针对性的修改 “root@ubuntu”

java的集合框架ListSetMap

什么是集合&#xff1f; 存储多个对象的单一对象&#xff08;java容器&#xff09;。 为什么不用数组&#xff1f; 集合与数组都是对数据进行存储操作的结构&#xff0c;简称java容器。 此时的存储主要是内存方面的&#xff0c;不涉及到持久话存储(.txt&#xff0c;.jpg&#x…

XXX汽车SAP ERP系统预月结模式助力成本高效结算(投稿数字化月报二)

XXX汽车业务复杂&#xff0c;零部件数据繁多&#xff0c;SAP ERP系统实施时&#xff0c;引进了行业的领先模式&#xff0c;所以系统挑战相对大&#xff0c;尤其是在月底进行账务结算时&#xff0c;出现过结算异常的情况&#xff0c;而公司对月结有固定的完成时间&#xff0c;因…

Seafile搭建个人云盘 - 内网穿透实现在外随时随地访问

文章目录 1. 前言2. SeaFile云盘设置2.1 Owncould的安装环境设置2.2 SeaFile下载安装2.3 SeaFile的配置 3. cpolar内网穿透3.1 Cpolar下载安装3.2 Cpolar的注册3.3 Cpolar云端设置3.4 Cpolar本地设置 4. 公网访问测试5. 结语 转载自cpolar极点云文章&#xff1a;使用SeaFile搭建…

Java 设计模式——代理模式

目录 1.概述2.结构3.静态代理3.1.实现3.2.优缺点 4.动态代理4.1.JDK 动态代理4.1.1.实现4.1.2.思考4.1.2.1.ProxyFactory 是代理类吗&#xff1f;4.1.2.2.动态代理的执行流程是什么样&#xff1f;4.1.2.3.为什么 JDK 动态代理只能代理有接口的类&#xff1f; 4.2.CGLIB 动态代理…

初识mysql数据库之数据类型

目录 一、数据类型 1. 数据类型分类 2. 数值类型 2.1 整数类型 2.2 位字段类型 2.3 小数类型 3. 字符串类型 3.1 char 3.2 varchar 4. 日期和时间类型 5. enum和set 5.1 enum介绍 5.2 set介绍 5.3 enum测试 5.4 set测试 5.5 enum的查找 5.6 set的查找 一、数据…

DAY40:贪心算法(九)单调递增的数字(贪心的思路)

文章目录 738.单调递增的数字&#xff08;暴力解也需要看一下&#xff09;暴力解写法注意&#xff1a;必须引入isIncreasing变量的原因 贪心思路遍历顺序 最开始的写法debug测试&#xff1a;逻辑错误 修改版debug测试int转化为字符串的原因to_string和stoi的用法 总结 738.单调…

CPCI-QU-216A正交解码计数器卡

CPCI-QU-216A是用于高速正交解码计数器CPCI板卡&#xff0c;包括6个立计数器&#xff0c;支持索引锁存&#xff0c;支持步长比较输出&#xff0c; 6个高速隔离输入&#xff0c;6个高速隔离输出。 l 32位 cPCI接口 l 6通道隔离输入 l 6通道隔离输出&#xff08;默认用于比较值输…

0125 计算机系统概述

目录 1.计算机系统概述 1.1计算机发展历程 1.2计算机系统层次结构 计算机硬件 计算机软件 计算机系统层次结构 计算机系统工作原理 指令执行过程 1.2部分习题 1.3计算机性能指标 计算机主要性能指标 几个专业术语 1.3部分习题 1.计算机系统概述 1.1计算机发展历程…

三星面板产能紧缺?超半数三星电视机采用其他厂商的面板

今年有超过50%的三星电视未使用自家屏幕&#xff0c;而是采用了来自中国厂商的大部分零件。 据韩国媒体《The Elec》报道&#xff0c;超过一半的三星电视机使用了其他厂商的面板。根据三星电子面板库存明细&#xff0c;大部分的面板来自中国的华星光电、惠科、京东方和咸阳彩虹…

小红书数据分析!Citywalk声量大涨,年轻人为何迷恋它?

近来&#xff0c;一种新型旅游形式——citywalk火了。惬意的城市漫游&#xff0c;成为时下年轻人最潮的逛GAI方式。小红书上更是掀起了citywalk游记潮流&#xff0c;人们纷纷在平台分享记录自己的出游感受。citywalk具体怎么玩&#xff1f;哪些人爱玩&#xff1f;通过分析小红书…

我们来谈谈tcp

"让新离开地表&#xff0c;才能找到盘旋爬升的动力。" 一、认识Tcp报头 (1) 协议报头格式 我们先来认识认识tcp协议报头字段。 跟tcp协议字段报头比起来&#xff0c;udp可真是太轻松了。 协议字段作用源/目的端口号从哪里来&#xff0c;到哪里去32位序号/32位确认…

前端vue入门(纯代码)21_vuex

努力不一定成功&#xff0c;但是&#xff0c;不努力一定很轻松&#xff01;&#xff01;&#xff01; 【23.Vuex】 [可以去官网看看Vuex3文档](Vuex 是什么&#xff1f; | Vuex (vuejs.org)) 问题1&#xff1a;Vuex是什么&#xff1f; 【官方理解1】&#xff1a;Vuex 是一个专…

vue中的.env全局配置

关于文件名&#xff1a; .env 全局默认配置文件&#xff0c;不论什么环境都会加载合并 .env.development 开发环境下的配置文件 .env.production 生产环境下的配置文件 .env文件配置&#xff1a; 我这里要讲的env配置是用启动命令启动项目来配置不同的全局变量 我配置了两…

十七、Jenkins(centos7系统)运行python3代码

十七、Jenkins(centos7系统)运行python3代码 source /usr/python/envs/everyday/bin/activate #激活python3 虚拟环境 创建虚拟环境&#xff1a;https://blog.csdn.net/qq_42846555/article/details/131579627 source /usr/python/envs/everyday/bin/activate #激活python3 虚…

自我介绍,千万别来虚的!

大家好&#xff0c;我是鲏。 已经帮小伙伴改了 500 多份简历了&#xff0c;也发现了一些大家写简历时的共性问题。其中让我印象比较深刻的一个点就是 自我介绍 &#xff0c;基本上所有同学的自我介绍都是这么写的&#xff1a; 读这篇文章的朋友们&#xff0c;你是不是也是这么…