首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >女朋友:有点感慨,这些年她在背后默默为你做了那么多事......

女朋友:有点感慨,这些年她在背后默默为你做了那么多事......

作者头像
范蠡
发布2022-08-26 12:52:02
3700
发布2022-08-26 12:52:02
举报

我之前在 《女朋友问我:什么时候用 C 而不用 C++?》这篇文章中说,C++ 与 C 语言的一个很大区别是,C++ 编译器在我们的代码背后偷偷加了许多代码,尤其是 C++11 有了明确的右值引用,引出移动构造和右值赋值(operator =(T&&))之后,这一点更加明显。

最近把 C++11 右值相关的内容重新复习了一下之后,加上使用右值技术改造了一些项目之后,甚是感慨,所以有了这篇文章。

先来看一段代码吧:

/**
 * 版本1
 */

#include <iostream>
#include <functional>
#include <map>
#include <string>
#include <memory>
#include <cstdint>

enum class Method {
    Get,
    Post
};

struct Request {

};

struct Response {
    int32_t     data1;
    std::string data2;
};

using Handler = std::function<bool(const Request& req, Response& resp)>;

struct HandlerItem {
    Handler     handler;
    int32_t     timeout;
};

class HttpSessionManger {
public:
    bool registerRouter(const std::string& path, Method method, Handler handler, int32_t timeout = 5000) {
        auto iter = m_routeHandlers.find(method);
        if (iter == m_routeHandlers.end()) {
            m_routeHandlers[method] = std::map<std::string, HandlerItem>();
        }

        HandlerItem handlerItem;
        handlerItem.handler = handler;
        handlerItem.timeout = timeout;

        auto iter2 = m_routeHandlers[method].find(path);
        if (iter2 != m_routeHandlers[method].end()) {
            // 路由存在
            return false;
        }

        m_routeHandlers[method][path] = handlerItem;

        return true;
    }

    bool onRoute(const std::string& path, Method method, void* param) {
        auto iter = m_routeHandlers.find(method);
        if (iter == m_routeHandlers.end()) {
            return false;
        }

        auto iter2 = iter->second.find(path);
        if (iter2 == iter->second.end()) {
            return false;
        }
        

        HandlerItem handlerItem = iter2->second;
        
        Request req;
        Response resp;
        return handlerItem.handler(req, resp);
    }

private:
    //内层map的key为path,value为HandlerItem
    std::map<Method, std::map<std::string, HandlerItem>> m_routeHandlers;
};


class HttpServer {
public:
    void init(const std::string& ip, short port) {
        m_spSessionManager = std::make_shared<HttpSessionManger>();
    }

    bool registerRouter(const std::string& path, Method method, Handler handler, int32_t timeout = 5000) {
        return m_spSessionManager->registerRouter(path, method, handler, timeout);
    }

    bool run(const std::string& path, Method method, void* param) {
        return m_spSessionManager->onRoute(path, method, param);
    }

private:
    std::shared_ptr<HttpSessionManger>  m_spSessionManager;
};

int main()
{
    HttpServer httpServer;
    httpServer.init("0.0.0.0", 8888);

    int32_t param1 = 123;
    std::string param2 = "helloworld";

    httpServer.registerRouter("/test", Method::Get, [&param1, &param2](const Request& req, Response& resp) -> bool {
        resp.data1 = param1;
        resp.data2 = param2;

        return true;
    });

    httpServer.run("/test", Method::Get, nullptr);

    
    return 0;
}

这段代码模拟了一个 HttpServer 框架,对外暴露 HttpServer 类,HttpServer 类内部实际管理路由的类是 HttpSessionManager,路由注册和调度都是对 HttpSessionManager 中的路由进行管理。

我们用 cppinsightshttps://cppinsights.io/)看下实际编译器为我们生成了什么样的代码。

#include <iostream>
#include <functional>
#include <map>
#include <string>
#include <memory>
#include <cstdint>

enum class Method : int
{
    Get,
    Post
};

struct Request
{
    // inline constexpr Request() noexcept = default;
};

struct Response
{
    int32_t data1;
    std::basic_string<char> data2;
    // inline ~Response() noexcept = default;
    // inline Response() noexcept = default;
};


using Handler = std::function<bool(const Request&, Response&)>;

struct HandlerItem
{
    std::function<bool(const Request&, Response&)> handler;
    int32_t timeout;
    // inline HandlerItem(const HandlerItem &) noexcept(false) = default;
    // inline HandlerItem & operator=(const HandlerItem &) noexcept(false) = default;
    // inline ~HandlerItem() noexcept = default;
    // inline HandlerItem() noexcept = default;
};

class HttpSessionManger
{

public:
    inline bool registerRouter(const std::basic_string<char>& path, Method method, std::function<bool(const Request&, Response&)> handler, int32_t timeout)
    {
        std::_Rb_tree_iterator<std::pair<const Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > > > > iter = std::_Rb_tree_iterator<std::pair<const Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > > > >(this->m_routeHandlers.find(method));
        if (operator==(iter, this->m_routeHandlers.end())) {
            this->m_routeHandlers.operator[](method).operator=(std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > >());
        }

        HandlerItem handlerItem = HandlerItem();
        handlerItem.handler.operator=(handler);
        handlerItem.timeout = timeout;
        std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, HandlerItem> > iter2 = std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, HandlerItem> >(this->m_routeHandlers.operator[](method).find(path));
        if (operator!=(iter2, this->m_routeHandlers.operator[](method).end())) {
            return false;
        }

        this->m_routeHandlers.operator[](method).operator[](path).operator=(handlerItem);
        return true;
    }

    inline bool onRoute(const std::basic_string<char>& path, Method method, void* param)
    {
        std::_Rb_tree_iterator<std::pair<const Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > > > > iter = std::_Rb_tree_iterator<std::pair<const Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > > > >(this->m_routeHandlers.find(method));
        if (operator==(iter, this->m_routeHandlers.end())) {
            return false;
        }

        std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, HandlerItem> > iter2 = std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, HandlerItem> >(iter.operator->()->second.find(path));
        if (operator==(iter2, iter.operator->()->second.end())) {
            return false;
        }

        HandlerItem handlerItem = HandlerItem(iter2.operator->()->second);
        Request req = Request();
        Response resp = Response();
        return handlerItem.handler.operator()(req, resp);
    }

private:
    std::map<Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > >, std::less<Method>, std::allocator<std::pair<const Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > > > > > m_routeHandlers;
public:
    // inline ~HttpSessionManger() noexcept = default;
    // inline HttpSessionManger() noexcept = default;
};

class HttpServer
{

public:
    inline void init(const std::basic_string<char>& ip, short port)
    {
        this->m_spSessionManager.operator=(std::make_shared<HttpSessionManger>());
    }

    inline bool registerRouter(const std::basic_string<char>& path, Method method, std::function<bool(const Request&, Response&)> handler, int32_t timeout)
    {
        return static_cast<const std::__shared_ptr_access<HttpSessionManger, 2, 0, 0>&>(this->m_spSessionManager).operator->()->registerRouter(path, method, std::function<bool(const Request&, Response&)>(handler), timeout);
    }

    inline bool run(const std::basic_string<char>& path, Method method, void* param)
    {
        return static_cast<const std::__shared_ptr_access<HttpSessionManger, 2, 0, 0>&>(this->m_spSessionManager).operator->()->onRoute(path, method, param);
    }


private:
    std::shared_ptr<HttpSessionManger> m_spSessionManager;
public:
    // inline constexpr HttpServer() noexcept = default;
    // inline ~HttpServer() noexcept = default;
};

int main()
{
    HttpServer httpServer = HttpServer();
    httpServer.init(std::basic_string<char>("0.0.0.0", std::allocator<char>()), 8888);
    int32_t param1 = 123;
    std::basic_string<char> param2 = std::basic_string<char>(std::basic_string<char>("helloworld", std::allocator<char>()));

    class __lambda_106_53
    {
    public:
        inline bool operator()(const Request& req, Response& resp) const
        {
            resp.data1 = param1;
            resp.data2.operator=(param2);
            return true;
        }

    private:
        int32_t& param1;
        std::basic_string<char>& param2;
    public:
        // inline /*constexpr */ __lambda_106_53(const __lambda_106_53 &) noexcept = default;
        // inline /*constexpr */ __lambda_106_53(__lambda_106_53 &&) noexcept = default;
        __lambda_106_53(int& _param1, std::basic_string<char>& _param2)
            : param1{ _param1 }
            , param2{ _param2 }
        {}

    };

    httpServer.registerRouter(std::basic_string<char>("/test", std::allocator<char>()), Method::Get, std::function<bool(const Request&, Response&)>(std::function<bool(const Request&, Response&)>(__lambda_106_53(__lambda_106_53{ param1, param2 }))), 5000);
    httpServer.run(std::basic_string<char>("/test", std::allocator<char>()), Method::Get, nullptr);
    return 0;
}

代码有点乱,主要是掺杂着许多模板的代码,我用红色字体标出哪些是编译器自动插入的代码。分析之后,我们可以得出以下结论:

  • 在 C++ 中 classstruct 已经没本质区别了,struct 不再是 C 语言中那样的 plain object,即使不显式定义结构体的构造函数、析构函数等,编译器也会为我们自动生成,因为一旦定义了一个结构体变量,就会调用其构造和析构函数。
  • lamda 表达式本质上是一个(匿名)对象,其之所以能捕获外部变量是因为这些被捕获的变量会以某种方式(值记录或者引用记录)记录在这个匿名对象中,本质上和 std::bind 一样,例如上述代码中注册路由的 lamda 表达最后就变成了类__lambda_106_53

更精彩的事情还在后面,我们将版本 1 的代码修改一下,来看下版本 2:

/**
 * 版本2
 */

#include <iostream>
#include <functional>
#include <map>
#include <string>
#include <memory>
#include <cstdint>

enum class Method {
    Get,
    Post
};

struct Request {
public:
    Request() = default;
};

struct Response {
public:
    Response() = default;

    int32_t     data1;
    std::string data2;
};

using Handler = std::function<bool(const Request& req, Response& resp)>;

struct HandlerItem {
    Handler     handler;
    int32_t     timeout;
};

class HttpSessionManger {
public:
    bool registerRouter(const std::string& path, Method method, Handler&& handler, int32_t timeout = 5000) {
        auto iter = m_routeHandlers.find(method);
        if (iter == m_routeHandlers.end()) {
            m_routeHandlers.emplace(std::pair<Method, std::map<std::string, HandlerItem>>(method, std::move(std::map<std::string, HandlerItem>())));
        }

        HandlerItem handlerItem;
        handlerItem.handler = std::move(handler);
        handlerItem.timeout = timeout;

        auto iter2 = m_routeHandlers[method].find(path);
        if (iter2 != m_routeHandlers[method].end()) {
            // 路由存在
            return false;
        }

        m_routeHandlers[method].emplace(std::pair<std::string, HandlerItem>(path, std::move(handlerItem)));

        return true;
    }

    bool onRoute(const std::string& path, Method method, void* param) {
        auto iter = m_routeHandlers.find(method);
        if (iter == m_routeHandlers.end()) {
            return false;
        }

        auto iter2 = iter->second.find(path);
        if (iter2 == iter->second.end()) {
            return false;
        }


        HandlerItem& handlerItem = iter2->second;

        Request req;
        Response resp;
        return handlerItem.handler(req, resp);
    }

private:
    //内层map的key为path,value为HandlerItem
    std::map<Method, std::map<std::string, HandlerItem>> m_routeHandlers;
};


class HttpServer {
public:
    void init(const std::string& ip, short port) {
        m_spSessionManager = std::make_shared<HttpSessionManger>();
    }

    bool registerRouter(const std::string& path, Method method, Handler&& handler, int32_t timeout = 5000) {
        return m_spSessionManager->registerRouter(path, method, std::move(handler), timeout);
    }

    bool run(const std::string& path, Method method, void* param) {
        return m_spSessionManager->onRoute(path, method, param);
    }

private:
    std::shared_ptr<HttpSessionManger>  m_spSessionManager;
};

int main()
{
    HttpServer httpServer;
    httpServer.init("0.0.0.0", 8888);

    int32_t param1 = 123;
    std::string param2 = "helloworld";

    httpServer.registerRouter("/test", Method::Get, [&param1, &param2](const Request& req, Response& resp) -> bool {
        resp.data1 = param1;
        resp.data2 = param2;

        return true;
    });

    httpServer.run("/test", Method::Get, nullptr);


    return 0;
}

我用红色字体标记出与版本 1 相比的改动之处。

我们再用 cppinsights 来看下编译器实际会生成什么代码:

#include <iostream>
#include <functional>
#include <map>
#include <string>
#include <memory>
#include <cstdint>

enum class Method : int
{
    Get,
    Post
};

struct Request
{

public:
    inline constexpr Request() noexcept = default;
};


struct Response
{

public:
    inline Response() noexcept = default;
    int32_t data1;
    std::basic_string<char> data2;
    // inline ~Response() noexcept = default;
};


using Handler = std::function<bool(const Request&, Response&)>;


struct HandlerItem
{
    std::function<bool(const Request&, Response&)> handler;
    int32_t timeout;
    // inline HandlerItem(HandlerItem &&) noexcept = default;
    // inline ~HandlerItem() noexcept = default;
    // inline HandlerItem() noexcept = default;
};

class HttpSessionManger
{
public:
    inline bool registerRouter(const std::basic_string<char>& path, Method method, std::function<bool(const Request&, Response&)>&& handler, int32_t timeout)
    {
        std::_Rb_tree_iterator<std::pair<const Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > > > > iter = this->m_routeHandlers.find(method);
        if (operator==(iter, this->m_routeHandlers.end())) {
            this->m_routeHandlers.emplace<std::pair<Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > > > >(std::pair<Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > > >(method, std::move(std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > >())));
        }

        HandlerItem handlerItem = HandlerItem();
        handlerItem.handler.operator=(std::move(handler));
        handlerItem.timeout = timeout;
        std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, HandlerItem> > iter2 = this->m_routeHandlers.operator[](method).find(path);
        if (operator!=(iter2, this->m_routeHandlers.operator[](method).end())) {
            return false;
        }

        this->m_routeHandlers.operator[](method).emplace<std::pair<std::basic_string<char>, HandlerItem> >(std::pair<std::basic_string<char>, HandlerItem>(path, std::move(handlerItem)));
        return true;
    }

    inline bool onRoute(const std::basic_string<char>& path, Method method, void* param)
    {
        std::_Rb_tree_iterator<std::pair<const Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > > > > iter = this->m_routeHandlers.find(method);
        if (operator==(iter, this->m_routeHandlers.end())) {
            return false;
        }

        std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, HandlerItem> > iter2 = iter.operator->()->second.find(path);
        if (operator==(iter2, iter.operator->()->second.end())) {
            return false;
        }

        HandlerItem& handlerItem = iter2.operator->()->second;
        Request req = Request();
        Response resp = Response();
        return handlerItem.handler.operator()(req, resp);
    }


private:
    std::map<Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > >, std::less<Method>, std::allocator<std::pair<const Method, std::map<std::basic_string<char>, HandlerItem, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, HandlerItem> > > > > > m_routeHandlers;
public:
    // inline ~HttpSessionManger() noexcept = default;
    // inline HttpSessionManger() noexcept = default;
};

class HttpServer
{
public:
    inline void init(const std::basic_string<char>& ip, short port)
    {
        this->m_spSessionManager.operator=(std::make_shared<HttpSessionManger>());
    }

    inline bool registerRouter(const std::basic_string<char>& path, Method method, std::function<bool(const Request&, Response&)>&& handler, int32_t timeout)
    {
        return static_cast<const std::__shared_ptr_access<HttpSessionManger, 2, false, false>&>(this->m_spSessionManager).operator->()->registerRouter(path, method, std::move(handler), timeout);
    }

    inline bool run(const std::basic_string<char>& path, Method method, void* param)
    {
        return static_cast<const std::__shared_ptr_access<HttpSessionManger, 2, false, false>&>(this->m_spSessionManager).operator->()->onRoute(path, method, param);
    }


private:
    std::shared_ptr<HttpSessionManger> m_spSessionManager;
public:
    // inline constexpr HttpServer() noexcept = default;
    // inline ~HttpServer() noexcept = default;
};

int main()
{
    HttpServer httpServer = HttpServer();
    httpServer.init(std::basic_string<char>("0.0.0.0", std::allocator<char>()), 8888);
    int32_t param1 = 123;
    std::basic_string<char> param2 = std::basic_string<char>("helloworld", std::allocator<char>());

    class __lambda_110_53
    {
    public:
        inline /*constexpr */ bool operator()(const Request& req, Response& resp) const
        {
            resp.data1 = param1;
            resp.data2.operator=(param2);
            return true;
        }

    private:
        int32_t& param1;
        std::basic_string<char>& param2;
    public:
        // inline /*constexpr */ __lambda_110_53(const __lambda_110_53 &) noexcept = default;
        // inline /*constexpr */ __lambda_110_53(__lambda_110_53 &&) noexcept = default;
        __lambda_110_53(int& _param1, std::basic_string<char>& _param2)
            : param1{ _param1 }
            , param2{ _param2 }
        {}

    };

    httpServer.registerRouter(std::basic_string<char>("/test", std::allocator<char>()), Method::Get, std::function<bool(const Request&, Response&)>(__lambda_110_53{ param1, param2 }), 5000);
    httpServer.run(std::basic_string<char>("/test", std::allocator<char>()), Method::Get, nullptr);
    return 0;
}

我也用红色字体标记出来,与上一个版本的不同,不过有一些不同的地方,在上述代码中看不出来,如对 std::map::emplace() 方法的调用,不知道读者注意到没有,在这个版本中,我能用引用的地方就用引用,能用右值的地方都使用 std::move 转换成了右值 ,可以 emplace 绝不 insert。因此,编译器也为我们的 HandlerItem 结构体生成了移动构造函数(注意与拷贝构造函数区分),虽然这里的例子,对于这个结构体拷贝构造和移动构造性能上没啥差别。

本文到这里基本上就要结束了,有读者会问,你贴了这么多代码到底想说啥?我想说:

  • 在 C++11 标准之前,基本上所有的拷贝动作都是编译器通过拷贝构造函数(也包括赋值拷贝)悄悄完成的,但是在 C++11 及以后,有了明确的右值和右值引用的概念之后,复用右值成了可能,所以对于编译器来说,拷贝对象总是尽量先使用移动拷贝(包括移动构造和赋值移动),所以你应该尽量利用编译器这种行为,尽量复用右值和移动构造,显式地提供移动构造函数和移动赋值函数,把不用的对象重复利用起来,以提高代码效率。举个简单的例子: #include <string> int main() {     //在C++11之前,如果str1单纯只是为了用来构造str2,没法废物利用     std::string str1("hello");     std::string str2(str1);     //C++11标准之后,如果str3单纯只是为了用来构造str4,我们可以将     //str3转换为右值,让str4复用str3的资源,一旦复用之后,str3就被     //“榨干”了,不信你输出str3复用前后的值试试。     std::string str3("world");     std::string str4(std::move(str3));     return 0; }
  • 由于 C++11 标准及之后,编译器在对象的构造上偷偷做了更多的事情,你应该搞清楚这些背后的动作,用以优化你的代码,减少不必要的操作;
  • 编译器提供了更多的语法糖,例如上文的 lamda 表达式、for-each 语法、C++17 的结构化绑定等等,导致编译器在你的程序背后偷偷做了更多的事情,所以作为一名好的 C++ 程序员,你应该搞清楚这些语法糖背后到底对应什么样的代码,这样在排查内存问题、优化代码执行效率方面才能更加有的放矢。
  • C++11 基于右值的技术体系,你一定要深刻理解,并多加运用,它是 C++11 标准提供给程序员的福利,而不是负债!

有读者在前面一篇文章《女朋友:一个 bug 查了两天,再解决不了,和你的代码过去吧!》中,有这样的代码:

void onAccept(int fd) {
    auto pConnection = std::make_unique<HttpConnection>(fd);
    //使用std::move将左值pConnection变成右值
    auto pSession = std::make_shared<HttpSession>(std::move(pConnection), this);
    auto clientID = pSession->getClientID();
    {
        std::lock_guard<std::mutex> scopedLock(m_sessionMutex);
        m_mapSessions.emplace(clientID, pSession);
    }
}

HttpSession 的构造函数是这样的:

class HttpSession {
public:
    HttpSession(std::unique_ptr<HttpConnection>&& pConnection, HttpSessionManager* pSessionManager) : m_spConnection (std::move(pConnection)) {
        m_clientID = pConnection->getIP() + ":" + pConnection->getPort() + ":" + generateUniqueID();
    }

private:
    std::unique_ptr<HttpConnection>     m_spConnection;
};

有读者留言:传进来的参数 pConnection 已经在 onAccept 函数中使用 std::move 变成右值了,为啥在 HttpSession 的构造函数中又要使用 std::move 再转一次呢?

答案是,pConnection 传到 HttpSession 构造函数中时又变成了左值,我们创建 m_spConnection 对象时想使用移动构造函数(为了复用),所以需要再一次把 pConnection 使用 std::move 转换成右值。注意,右值引用不是右值,是左值。函数的实参是右值,函数实际执行时的形参不是右值,而是左值,尽管形参的函数参数的签名看起来像右值:

class HttpSession {
public:
    //pConnection的类型看起来像右值
    HttpSession(std::unique_ptr<HttpConnection>&& pConnection, ...) : m_spConnection (std::move(pConnection)) {
        ...
    }
};

用一张图来梳理下这里的关系:

好啦,本文到此为止,感谢阅读,如果有不明白的地方,欢迎在文章下面留言。

本文是《女朋友要去 XXX 系列》第六篇,本系列:

篇一《女朋友要去面试 C++,我建议她这么做

篇二 《女朋友问我:什么时候用 C 而不用 C++?

篇三 《女朋友:一个 bug 查了两天,再解决不了,和你的代码过去吧!

篇四《女朋友:七夕,你的 HttpServer 又崩了......

篇五 《女朋友问:这些年你看过哪些网络编程书籍?

推荐阅读

关注我,更多有趣实用的编程知识~

有帮助的话,点个赞呗

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-08-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能服务器开发 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档