LCR训练计划2(剑指 Offer 22. 链表中倒数第k个节点)-140
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* trainingPlan(ListNode* head, int cnt) {
ListNode* cur = NULL;
ListNode* pre = head;
while(pre!=NULL)
{
ListNode* t = pre->next;
pre->next = cur;
cur = pre;
pre = t;
}
/*以上是反转链表操作
定义ListNode指针类型的变量temp记录反转后cur的位置,因为后续操作cur的位置会发生变化,此时的cur为反转后链表的头节点,
因为后续找到倒数第cnt个节点后要再进行一次反转,因为题目要找的是正常顺序链表的倒数第cnt个节点
此时的反转链表只是为了方便找到倒数第cnt的节点位置*/
ListNode* temp = cur;
//while循环用来找到正数第cnt节点的位置,因为我们已经将链表反转了,从题意倒数变成了正数
while(cnt>1)
{
cur=cur->next;
cnt--;
}
//循环过后,cur已经找到了倒数第cur节点的位置,定义ListNode指针类型的变量temphead记录倒数第cur节点的位置
ListNode* temphead = cur;
/*以下是将反转后的链表再一次反转的操作,再一次反转过后链表变为正常顺序的链表,
最后根据题意返回temphead及往后的所有节点*/
ListNode* cur1 = NULL;
ListNode* pre1 = temp;
while(pre1!=NULL)
{
ListNode* t1 = pre1->next;
pre1->next = cur1;
cur1 = pre1;
pre1 = t1;
}
return temphead;
}
};
每日问题
C++循环引用指的是什么,在使用过程当中需要注意什么问题
C++中的循环引用
循环引用(Cyclic Reference)通常指的是两个或多个对象相互引用,形成一个闭环,使得这些对象之间无法被销毁或释放,导致内存泄漏或资源未被及时回收。这种情况最常见于使用指针或智能指针的场景中,特别是在涉及到对象之间相互持有指针或引用时。
循环引用的例子:
考虑一个简单的 C++ 例子,假设我们有两个类 A 和 B,它们通过指针互相引用:
cpp
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptr
};
class B {
public:
std::shared_ptr<A> a_ptr; // B 持有 A 的 shared_ptr
};
在这个例子中:
类 A 有一个 shared_ptr 指向类 B 的实例。
类 B 也有一个 shared_ptr 指向类 A 的实例。
如果在程序中创建了 A 和 B 的实例,并且它们通过 shared_ptr 互相引用,当这两个对象超出作用域时,由于 shared_ptr 的引用计数机制,它们互相持有对方,导致它们的引用计数永远不为零。因此,这些对象的析构函数永远不会被调用,从而导致内存泄漏。
循环引用的具体问题
1.内存泄漏:
循环引用的最大问题就是内存泄漏。在智能指针(如 std::shared_ptr)的情况下,引用计数机制会不断增加,导致内存无法被释放。
2.资源无法释放:
除了内存泄漏,循环引用还可能导致其他资源(如文件句柄、数据库连接等)无法及时释放,影响程序性能和稳定性。
3.性能问题:
循环引用会影响垃圾回收或引用计数机制的正常工作,使得它们无法有效地管理资源,可能导致程序资源消耗过多或响应变慢。
避免循环引用的策略
1.弱指针(std::weak_ptr):
如果我们希望避免循环引用,可以使用 std::weak_ptr 替代 std::shared_ptr。std::weak_ptr 不会增加引用计数,也不会阻止对象被销毁。它可以用来观察一个对象,而不持有它。
例如,可以将其中一个指针改为 weak_ptr 来打破循环引用:
cpp
class A;
class B {
public:
std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免循环引用
};
class A {
public:
std::shared_ptr<B> b_ptr;
};
在这种情况下,
B 类持有 A 类的弱引用,这样当 A 被销毁时,不会阻止 B 的析构,同时 A 依然可以安全地持有 B 的 shared_ptr。
2.手动管理资源:
如果循环引用不能避免(例如某些设计模式中确实需要双向引用),可以选择手动管理资源释放,确保在适当的时候解除相互引用。比如在对象销毁前,将智能指针置空或重新设置。
3.避免不必要的双向引用:
在设计时,尽量避免不必要的双向引用。例如,可以考虑将其中一个对象的引用设计为单向引用,而不是双向引用。这样就可以避免引起复杂的引用关系。
4.设计模式:观察者模式:
如果存在双向引用的需求,考虑使用观察者模式来解耦对象间的关系。在这种模式中,一个对象(通常是"主题")可以有多个观察者(其他对象),而不需要在两个对象之间建立直接的循环引用关系。
5.手动释放资源:
如果确实存在循环引用,且无法使用 weak_ptr 等方法来解决,可以通过手动解除循环引用(例如,在合适的时机将指针重置为 nullptr)来避免资源泄漏。
总结
循环引用 发生在对象通过智能指针(如 shared_ptr)或裸指针互相引用,导致它们无法被销毁,造成内存泄漏。
解决方法 主要是使用 std::weak_ptr 来打破循环引用,或者通过手动解除循环引用来管理资源。
在 C++ 中,避免循环引用是一个重要的设计考虑,特别是在复杂的对象关系和资源管理场景中。
new或malloc在申请空间时,超出可申请的大小就会分配异常,可申请的大小是多少
在 C++ 中,new 或 C 中的 malloc 等内存分配函数,如果申请的内存超过系统能分配的最大内存大小,通常会出现异常或错误。这个最大可分配的内存大小取决于多个因素:
1.操作系统和架构的限制:
在 32 位系统上,通常最多能分配的内存是 4GB(不过,由于操作系统通常会把一些内存空间保留给内核,用户空间可能会少于 4GB),每个进程的最大虚拟内存空间通常在 2GB 到 3GB 之间。
在 64 位系统上,理论上支持的内存大小可以达到几 EB(Exabyte,亿亿字节),但实际上,系统能分配的内存通常受硬件(物理内存)、操作系统和其他限制的影响,通常是几 TB 或者更多。
2.可用物理内存和虚拟内存:
malloc 或 new 在分配内存时,操作系统会尝试为进程分配一个足够大的虚拟内存块。如果虚拟内存不足,或者物理内存不足以满足请求,分配会失败。
3.片化:
内存碎片化也会影响内存分配的成功与否。即使总的可用虚拟内存很大,如果内存已经被分割成许多小块,可能无法分配一个大的连续内存块。
错误和异常处理:
C++:
如果 new 无法分配内存,会抛出 std::bad_alloc 异常。你可以使用 try-catch 块捕获这个异常。
cpp
try {
int* arr = new int[10000000000]; // 试图申请一个非常大的数组
} catch (const std::bad_alloc& e) {
std::cerr << "内存分配失败: " << e.what() << std::endl;
}
C:
如果 malloc 或 calloc 无法分配内存,它们会返回 NULL,并且通常会设置 errno 为 ENOMEM。
实际可申请的大小:
这个大小通常依赖于:
系统的总物理内存和虚拟内存
系统的内存分配策略
操作系统的配置(例如,最大进程内存限制)
程序本身的内存使用情况
在大多数现代操作系统中,单个进程的虚拟地址空间在 64 位系统上非常大,但要注意物理内存的限制和操作系统的配置可能会导致超出物理内存时出现分配失败或严重的性能下降。
总结:
对于 new 和 malloc,具体能分配多少内存受到操作系统、硬件架构、物理内存、虚拟内存空间以及内存碎片等多方面因素的限制。
在 64 位操作系统上,通常可分配的内存大小非常大,然而,最终是否能成功分配内存也取决于具体的系统和当前的内存使用状况。