c++弱引用指针std::weak_ptr作用详解

文章目录

前言

在C++现代编程中,智能指针已成为管理动态内存的基石。然而,许多开发者在接触std::shared_ptrstd::weak_ptr时,常常对后者的存在意义感到困惑。特别是在阅读大型项目源码时,频繁出现的weak_ptr用法更让人疑惑:既然已经有了强大的shared_ptr,为何还需要这种"弱不禁风"的指针?本文将从设计哲学、实现原理到实际应用场景,全面解析std::weak_ptr的奥秘,并通过丰富的代码示例展示其在实际工程中的价值。

一、为什么需要弱引用指针?

1.1 封于修的智慧:既决高下,也决生死 vs 只旁观,不决生死

正如封于修所言:std::shared_ptr既决高下,也决生死;std::weak_ptr只旁观,不决生死。这句话精辟地概括了两者的本质区别:

  • std::shared_ptr:拥有对象的所有权,通过引用计数决定对象的生死
  • std::weak_ptr:观察对象的状态,但不拥有所有权,不参与生死决策

1.2 核心设计目的

std::weak_ptr的引入主要解决两大问题:

(1)打破循环引用,防止内存泄漏

这是最主要的设计初衷。当两个或多个对象通过shared_ptr相互引用时,会形成循环引用,导致引用计数永远无法归零,内存无法释放。

(2)安全地观察对象,避免悬空指针

在某些场景下,我们需要持有对象的引用,但又不希望因为我们的持有而阻止对象被销毁。weak_ptr提供了一种安全机制来检测对象是否仍然存活。如果对象还活着,就使用它;如果对象已经销毁,就可以感知到它没了,不访问野指针。std::weak_ptr 就是用来安全探测对象是否还存在。

所以std::weak_ptr要搭配std::shared_ptr使用。要理解 std::weak_ptr 的原理,得先看 std::shared_ptr 的内部结构。

内部结构示意图:

二、std::weak_ptr的工作原理

2.1 内部结构解析

要理解std::weak_ptr,必须先了解std::shared_ptr的内部结构。shared_ptr管理的堆内存包含两部分:

复制代码
┌─────────────────────────────────────────┐
│           std::shared_ptr               │
├─────────────────────────────────────────┤
│   ┌─────────────────┐   ┌───────────┐  │
│   │    对象指针     │───│  对象数据  │  │
│   └─────────────────┘   └───────────┘  │
│   ┌─────────────────┐   ┌───────────┐  │
│   │  控制块指针     │───│  控制块    │  │
│   └─────────────────┘   │           │  │
│                         │ use_count │  │
│                         │ weak_count│  │
│                         │   ...     │  │
│                         └───────────┘  │
└─────────────────────────────────────────┘

控制块包含的关键计数器:

  • use_count(强引用计数) :有多少个shared_ptr指向该对象
  • weak_count(弱引用计数) :有多少个weak_ptr指向该对象

2.2 weak_ptr的工作流程

cpp 复制代码
// 创建shared_ptr
auto sp = std::make_shared<MyClass>();  // use_count=1, weak_count=0

// 创建weak_ptr观察该对象
std::weak_ptr<MyClass> wp = sp;  // use_count=1, weak_count=1

// 使用lock()安全访问
if (auto locked_sp = wp.lock()) {  // use_count=2 (临时增加)
    // 对象存活,安全使用
    locked_sp->doSomething();
}  // locked_sp析构,use_count恢复为1

// sp离开作用域
// use_count=0 → 对象被销毁
// weak_count=1 → 控制块保留

// wp离开作用域
// weak_count=0 → 控制块被销毁
  • weak_ptr 的构造会指向跟 shared_ptr 相同的控制块,并把控制块的 weak_count 加 1。注意:不改变 use_count。
  • weak_ptr 不能直接访问对象(没有 * 或 -> 操作符)。必须调用 .lock() 方法。
  • .lock() 会检查控制块的 use_count。如果 use_count > 0(对象还活着):创建一个新的 shared_ptr 返回(此时 use_count +1),保证使用期间对象不会死。如果 use_count == 0(对象已死):返回一个空的 shared_ptr。
  • weak_ptr 析构,把 weak_count 减 1。

这里面有一个非常精妙的 对象块和控制块分离 的设计细节:

  • 对象释放: 所有 shared_ptr 离开作用域,use_count 归零,对象被 delete。
  • 内存完全释放: 如果还有 weak_ptr 活着,控制块必须保留(因为 weak_ptr 还要去查计数器)。只有所有 weak_ptr 也离开作用域,weak_count 归零,控制块才会被 delete。

三、核心应用场景与代码示例

3.1 场景一:解决循环引用问题

cpp 复制代码
#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
public:
    std::shared_ptr<Child> child;
    ~Parent() { std::cout << "Parent destroyed" << std::endl; }
    
    void showChild() {
        if (child) {
            std::cout << "Parent has a child" << std::endl;
        }
    }
};

class Child {
public:
    // 关键:使用weak_ptr而不是shared_ptr打破循环
    std::weak_ptr<Parent> parent;
    ~Child() { std::cout << "Child destroyed" << std::endl; }
    
    void showParent() {
        // 安全地访问父对象
        if (auto parent_sp = parent.lock()) {
            std::cout << "Child has a parent" << std::endl;
            parent_sp->showChild();
        } else {
            std::cout << "Parent no longer exists" << std::endl;
        }
    }
};

int main() {
    std::cout << "=== 循环引用示例 ===" << std::endl;
    
    auto parent = std::make_shared<Parent>();
    auto child = std::make_shared<Child>();
    
    // 建立双向关系
    parent->child = child;      // Parent拥有Child
    child->parent = parent;     // Child弱引用Parent
    
    // 演示安全访问
    child->showParent();
    
    std::cout << "\n引用计数信息:" << std::endl;
    std::cout << "Parent use_count: " << parent.use_count() << std::endl;
    std::cout << "Child use_count: " << child.use_count() << std::endl;
    
    // 离开作用域时,所有对象都能正常销毁
    // 如果没有使用weak_ptr,这里会发生内存泄漏
    return 0;
}

3.2 场景二:缓存与资源管理

cpp 复制代码
#include <iostream>
#include <memory>
#include <unordered_map>
#include <string>

class Resource {
public:
    std::string name;
    Resource(const std::string& n) : name(n) {
        std::cout << "Resource '" << name << "' created" << std::endl;
    }
    ~Resource() {
        std::cout << "Resource '" << name << "' destroyed" << std::endl;
    }
    
    void use() {
        std::cout << "Using resource: " << name << std::endl;
    }
};

class ResourceCache {
private:
    // 缓存使用weak_ptr,不阻止资源销毁
    std::unordered_map<std::string, std::weak_ptr<Resource>> cache;
    
public:
    std::shared_ptr<Resource> getResource(const std::string& name) {
        auto it = cache.find(name);
        
        // 检查资源是否仍在缓存中且存活
        if (it != cache.end()) {
            if (auto resource = it->second.lock()) {
                std::cout << "Cache hit: " << name << std::endl;
                return resource;  // 资源仍存活,直接返回
            } else {
                // 资源已被销毁,从缓存中移除
                cache.erase(it);
            }
        }
        
        // 缓存未命中,创建新资源
        std::cout << "Cache miss, creating: " << name << std::endl;
        auto newResource = std::make_shared<Resource>(name);
        cache[name] = newResource;  // 存储weak_ptr引用
        
        return newResource;
    }
    
    void cleanup() {
        // 清理已失效的缓存项
        for (auto it = cache.begin(); it != cache.end(); ) {
            if (it->second.expired()) {
                std::cout << "Cleaning up expired: " << it->first << std::endl;
                it = cache.erase(it);
            } else {
                ++it;
            }
        }
    }
    
    size_t getCacheSize() const {
        return cache.size();
    }
};

int main() {
    std::cout << "=== 缓存管理示例 ===" << std::endl;
    
    ResourceCache cache;
    
    // 第一次获取资源
    auto res1 = cache.getResource("texture1");
    res1->use();
    
    {
        // 第二次获取相同资源(应命中缓存)
        auto res2 = cache.getResource("texture1");
        res2->use();
        
        std::cout << "\n当前缓存大小: " << cache.getCacheSize() << std::endl;
        
        // res2离开作用域,但res1仍持有资源
    }
    
    std::cout << "\n释放res1..." << std::endl;
    res1.reset();  // 释放资源
    
    // 资源已被销毁,但缓存中仍有weak_ptr
    std::cout << "清理前缓存大小: " << cache.getCacheSize() << std::endl;
    
    cache.cleanup();  // 清理过期缓存
    std::cout << "清理后缓存大小: " << cache.getCacheSize() << std::endl;
    
    return 0;
}

3.3 场景三:观察者模式与Chrome中的PostTask

cpp 复制代码
#include <iostream>
#include <memory>
#include <functional>
#include <vector>
#include <thread>
#include <chrono>

// 模拟Chrome中的异步任务场景
class TaskObserver {
public:
    virtual ~TaskObserver() = default;
    virtual void onTaskCompleted(int taskId) = 0;
};

class Worker {
private:
    std::vector<std::weak_ptr<TaskObserver>> observers;
    
public:
    // 注册观察者(使用weak_ptr避免阻止观察者销毁)
    void addObserver(std::weak_ptr<TaskObserver> observer) {
        observers.push_back(observer);
    }
    
    // 模拟异步任务执行
    void performTask(int taskId) {
        std::cout << "Worker: Starting task " << taskId << std::endl;
        
        // 模拟耗时操作
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        
        std::cout << "Worker: Task " << taskId << " completed" << std::endl;
        
        // 通知所有观察者
        notifyObservers(taskId);
    }
    
private:
    void notifyObservers(int taskId) {
        // 安全地通知所有观察者
        for (auto it = observers.begin(); it != observers.end(); ) {
            if (auto observer = it->lock()) {
                // 观察者仍存活,调用回调
                observer->onTaskCompleted(taskId);
                ++it;
            } else {
                // 观察者已被销毁,从列表中移除
                std::cout << "Removing expired observer" << std::endl;
                it = observers.erase(it);
            }
        }
    }
};

class MyObserver : public TaskObserver {
private:
    std::string name;
    
public:
    MyObserver(const std::string& n) : name(n) {}
    
    void onTaskCompleted(int taskId) override {
        std::cout << name << ": Received completion of task " << taskId << std::endl;
    }
};

int main() {
    std::cout << "=== 异步任务观察者示例 ===" << std::endl;
    
    Worker worker;
    
    {
        // 创建观察者
        auto observer1 = std::make_shared<MyObserver>("Observer1");
        auto observer2 = std::make_shared<MyObserver>("Observer2");
        
        // 注册观察者
        worker.addObserver(observer1);
        worker.addObserver(observer2);
        
        // 执行任务
        worker.performTask(1);
        
        // observer2离开作用域被销毁
        std::cout << "\nDestroying observer2..." << std::endl;
    }
    
    // 再次执行任务,只有observer1会收到通知
    std::cout << "\nPerforming another task..." << std::endl;
    worker.performTask(2);
    
    return 0;
}

3.4 场景四:Chrome中PostTask的典型用法

cpp 复制代码
// 模拟Chrome中基于weak_ptr的异步任务提交
#include <iostream>
#include <memory>
#include <functional>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>

class Task {
public:
    virtual ~Task() = default;
    virtual void execute() = 0;
};

class MyObject : public std::enable_shared_from_this<MyObject> {
private:
    std::string name;
    bool active = true;
    
public:
    MyObject(const std::string& n) : name(n) {}
    
    ~MyObject() {
        std::cout << name << ": Object destroyed" << std::endl;
    }
    
    void deactivate() {
        active = false;
        std::cout << name << ": Object deactivated" << std::endl;
    }
    
    // 安全提交任务的方法
    void postTask(const std::string& taskName) {
        // 获取weak_ptr用于任务提交
        std::weak_ptr<MyObject> weak_this = shared_from_this();
        
        // 模拟Chrome的PostTask
        auto task = [weak_this, taskName]() {
            // 尝试提升为shared_ptr
            if (auto shared_this = weak_this.lock()) {
                // 对象仍存活,执行任务
                if (shared_this->active) {
                    std::cout << shared_this->name 
                              << ": Executing task '" << taskName << "'" 
                              << std::endl;
                    shared_this->processTask(taskName);
                } else {
                    std::cout << "Task '" << taskName 
                              << "' skipped - object inactive" << std::endl;
                }
            } else {
                // 对象已被销毁,安全地跳过任务
                std::cout << "Task '" << taskName 
                          << "' skipped - object destroyed" << std::endl;
            }
        };
        
        // 在实际的Chrome中,这里会将task提交到任务队列
        std::cout << name << ": Task '" << taskName << "' posted" << std::endl;
        
        // 模拟异步执行
        std::thread([task]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
            task();
        }).detach();
    }
    
private:
    void processTask(const std::string& taskName) {
        std::cout << name << ": Processing " << taskName << std::endl;
    }
};

int main() {
    std::cout << "=== Chrome风格PostTask示例 ===" << std::endl;
    
    {
        auto obj = std::make_shared<MyObject>("MyObject");
        
        // 提交多个异步任务
        obj->postTask("Task1");
        obj->postTask("Task2");
        
        // 在任务执行前销毁对象
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        std::cout << "\nDestroying object before tasks complete..." << std::endl;
    }
    
    // 给异步任务时间完成
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    
    std::cout << "\n=== 对象失效但未销毁的场景 ===" << std::endl;
    
    auto obj2 = std::make_shared<MyObject>("Object2");
    obj2->postTask("Task3");
    
    // 使对象失效但不立即销毁
    obj2->deactivate();
    obj2->postTask("Task4");
    
    // 给异步任务时间完成
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    
    return 0;
}

四、std::weak_ptr的进阶用法

4.1 使用expired()快速检查

cpp 复制代码
#include <iostream>
#include <memory>
#include <vector>

class Resource {
public:
    int id;
    Resource(int i) : id(i) {
        std::cout << "Resource " << id << " created" << std::endl;
    }
    ~Resource() {
        std::cout << "Resource " << id << " destroyed" << std::endl;
    }
};

int main() {
    std::cout << "=== expired()方法示例 ===" << std::endl;
    
    std::weak_ptr<Resource> wp;
    
    // 检查未初始化的weak_ptr
    std::cout << "Initial state - expired: " << (wp.expired() ? "true" : "false") << std::endl;
    
    {
        auto sp = std::make_shared<Resource>(1);
        wp = sp;
        
        std::cout << "After assignment - expired: " << (wp.expired() ? "true" : "false") << std::endl;
        
        // 使用lock()获取shared_ptr
        if (auto locked = wp.lock()) {
            std::cout << "Resource " << locked->id << " is alive" << std::endl;
        }
    }
    
    // 对象已销毁
    std::cout << "After scope - expired: " << (wp.expired() ? "true" : "false") << std::endl;
    
    // 注意:expired()和lock()的竞态条件
    std::cout << "\n=== 竞态条件演示 ===" << std::endl;
    
    auto sp2 = std::make_shared<Resource>(2);
    std::weak_ptr<Resource> wp2 = sp2;
    
    // 在另一个线程中释放资源
    std::thread([&sp2]() {
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        sp2.reset();
    }).detach();
    
    // 主线程检查
    std::this_thread::sleep_for(std::chrono::milliseconds(5));
    
    // 这里存在竞态条件:expired()检查后,对象可能被销毁
    if (!wp2.expired()) {
        // 不安全:对象可能在这之后被销毁
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        
        // 正确做法:直接使用lock()
        if (auto safe_sp = wp2.lock()) {
            std::cout << "Safe access: Resource " << safe_sp->id << std::endl;
        } else {
            std::cout << "Resource was destroyed after check" << std::endl;
        }
    }
    
    std::this_thread::sleep_for(std::chrono::milliseconds(20));
    return 0;
}

4.2 weak_ptr的自定义删除器

cpp 复制代码
#include <iostream>
#include <memory>

class Resource {
public:
    int* data;
    
    Resource() {
        data = new int[100];
        std::cout << "Resource allocated with 100 integers" << std::endl;
    }
    
    ~Resource() {
        delete[] data;
        std::cout << "Resource deallocated" << std::endl;
    }
};

int main() {
    std::cout << "=== 自定义删除器示例 ===" << std::endl;
    
    // 创建带有自定义删除器的shared_ptr
    auto deleter = [](Resource* res) {
        std::cout << "Custom deleter called" << std::endl;
        delete res;
    };
    
    std::shared_ptr<Resource> sp(new Resource(), deleter);
    std::weak_ptr<Resource> wp = sp;
    
    std::cout << "use_count: " << sp.use_count() << std::endl;
    std::cout << "weak_count: " << sp.use_count() << std::endl;
    
    // 释放shared_ptr
    sp.reset();
    
    // weak_ptr仍然可以检测对象状态
    std::cout << "After reset - expired: " << wp.expired() << std::endl;
    
    return 0;
}

五、性能考虑与最佳实践

5.1 性能特点

  1. 内存开销weak_ptr本身很小(通常两个指针大小),但会增加控制块的weak_count
  2. 操作成本lock()操作需要原子操作检查use_count,有一定开销
  3. 适用场景:适合生命周期不确定或需要避免循环引用的场景

5.2 最佳实践

cpp 复制代码
// 良好实践示例
class GoodPractice {
private:
    std::weak_ptr<Dependency> dependency_;
    
public:
    void setDependency(std::shared_ptr<Dependency> dep) {
        dependency_ = dep;  // 存储weak_ptr而不是shared_ptr
    }
    
    void useDependency() {
        // 正确:使用lock()获取临时shared_ptr
        if (auto dep = dependency_.lock()) {
            dep->use();
        }
        // lock()返回的shared_ptr离开作用域,不延长生命周期
    }
};

// 避免的实践
class BadPractice {
private:
    std::shared_ptr<Dependency> dependency_;  // 错误:不必要的所有权
    
public:
    void useDependency() {
        if (dependency_) {
            dependency_->use();
        }
        // 即使不再需要,dependency_仍保持对象存活
    }
};

六 能否都用weak_ptr 不用shared_ptr?

核心问题:谁来决定对象的生命周期?

关键点:weak_ptr本身不拥有对象的所有权,它只是一个"观察者"。如果所有指针都是weak_ptr,就没有任何指针负责保持对象存活!

代码示例说明

cpp 复制代码
#include <iostream>
#include <memory>

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

int main() {
    std::cout << "=== 如果全部使用weak_ptr会发生什么 ===" << std::endl;
    
    // 尝试只用weak_ptr,不用shared_ptr
    std::weak_ptr<MyClass> wp;
    
    {
        // 必须先用shared_ptr创建对象
        auto sp = std::make_shared<MyClass>();  // 对象诞生
        wp = sp;  // weak_ptr开始观察
        
        // 此时对象是存活的
        if (auto temp = wp.lock()) {
            temp->doSomething();  // 正常工作
        }
        
        // sp离开作用域,对象立即被销毁!
        // 因为没有任何shared_ptr拥有它了
    }
    
    // 现在wp观察的对象已经不存在了
    if (auto temp = wp.lock()) {
        // 这里永远不会执行
        temp->doSomething();
    } else {
        std::cout << "对象已经被销毁了!" << std::endl;
    }
    
    return 0;
}

为什么必须有shared_ptr?

1. 生命周期管理需要"所有者"
  • shared_ptr所有者 - 决定对象何时被销毁
  • weak_ptr观察者 - 只能观察,不能决定生死
cpp 复制代码
// 错误示例:试图只用weak_ptr
std::weak_ptr<MyClass> createObject() {
    // 无法只用weak_ptr创建对象!
    // 必须先用shared_ptr创建
    auto sp = std::make_shared<MyClass>();
    return sp;  // 转换为weak_ptr返回
}

void problematicExample() {
    auto wp = createObject();
    // 对象在createObject()返回时立即被销毁!
    // 因为sp离开作用域,引用计数归零
    if (auto obj = wp.lock()) {
        obj->doSomething();  // 永远不会执行!
    }
}
2. 引用计数机制要求
cpp 复制代码
// 控制块的生命周期
// use_count = 0 → 对象被销毁
// weak_count = 0 → 控制块被销毁

// 如果只有weak_ptr:
// use_count永远为0 → 对象立即被销毁
// weak_ptr无法将use_count从0变为1

正确的使用模式

模式1:主从关系
cpp 复制代码
class Manager {
private:
    std::shared_ptr<Resource> resource_;  // 管理者拥有资源
    
public:
    std::weak_ptr<Resource> getResource() {
        return resource_;  // 返回weak_ptr给使用者
    }
};

class User {
private:
    std::weak_ptr<Resource> resource_;  // 使用者只观察资源
    
public:
    void useResource() {
        if (auto res = resource_.lock()) {
            res->use();  // 安全使用
        }
    }
};
模式2:工厂模式
cpp 复制代码
class ObjectFactory {
private:
    std::unordered_map<int, std::weak_ptr<MyObject>> cache_;
    
public:
    std::shared_ptr<MyObject> createOrGet(int id) {
        // 先检查缓存
        if (auto it = cache_.find(id); it != cache_.end()) {
            if (auto obj = it->second.lock()) {
                return obj;  // 返回存活的共享对象
            } else {
                cache_.erase(it);  // 清理过期条目
            }
        }
        
        // 创建新对象(必须有shared_ptr!)
        auto newObj = std::make_shared<MyObject>(id);
        cache_[id] = newObj;  // 缓存weak_ptr引用
        
        return newObj;
    }
};

类比理解

可以把shared_ptrweak_ptr的关系类比为:

  • shared_ptr 就像房子的房主 - 决定房子何时拆除
  • weak_ptr 就像房子的访客 - 可以来访,但不能决定房子命运

如果所有人都只是访客(weak_ptr),没有人当房主(shared_ptr),那么房子从一开始就不会存在,或者会立即被拆除!

不能全部使用weak_ptr的原因:

  1. 生命周期管理 :必须有至少一个shared_ptr来"拥有"对象,决定其生命周期
  2. 对象创建weak_ptr不能单独创建对象,必须依赖shared_ptr
  3. 引用计数use_count必须大于0对象才能存活,而weak_ptr不增加use_count

正确的设计原则:

  • 需要拥有 对象时用shared_ptr
  • 只需要观察 对象时用weak_ptr
  • 两者配合使用,各司其职

这就是为什么在Chrome的PostTask中你会看到两者配合使用:对象本身由shared_ptr管理生命周期,而异步任务通过weak_ptr来安全地访问对象。

总结

std::weak_ptr是C++智能指针体系中不可或缺的一环,它体现了"观察而非拥有"的设计哲学。通过本文的深入分析,我们可以总结出以下几点:

  1. 核心价值weak_ptr主要解决循环引用和悬空指针两大问题,在复杂对象关系中起到关键作用。

  2. 工作原理 :通过共享shared_ptr的控制块,维护独立的弱引用计数,实现对象生命周期的安全观察。

  3. Chrome中的应用 :在异步编程模型中,weak_ptr确保了任务执行时对象的安全访问,避免了因任务队列延迟导致的野指针访问。

  4. 使用原则

    • 当需要观察对象但不拥有所有权时使用weak_ptr
    • 总是通过lock()方法安全地访问对象
    • 在缓存、观察者模式、打破循环引用等场景中优先考虑
  5. 设计启示weak_ptr的成功在于它平衡了灵活性与安全性,为C++大型项目提供了可靠的生命周期管理方案。

理解并熟练运用weak_ptr,是成为高级C++开发者的重要标志。它不仅是解决特定问题的工具,更是一种设计思想的体现------在软件架构中,明确的所有权关系和安全的生命周期管理是构建稳定、可维护系统的基石。

相关推荐
小菱形_2 小时前
【C#】IEnumerable
开发语言·c#
爱敲点代码的小哥2 小时前
Directoy文件夹操作对象 、StreamReader和StreamWriter 和BufferedStream
开发语言·c#
这是程序猿2 小时前
基于java的ssm框架经典电影推荐网站
java·开发语言·spring boot·spring·经典电影推荐网站
Nan_Shu_6142 小时前
学习:Java (1)
java·开发语言·学习
李慕婉学姐2 小时前
【开题答辩过程】以《基于PHP的饮食健康管理系统设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
开发语言·php
李慕婉学姐2 小时前
【开题答辩过程】以《基于PHP的养老中心管理系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
开发语言·php
曹牧2 小时前
Java:String.startsWith 方法
java·开发语言
秃然想通2 小时前
Java多态完全指南:深入理解“一个接口,多种实现”
java·开发语言
fengyue01102 小时前
C++使用epoll实现高并发tcp服务
linux·服务器·网络·c++