• [分享交流] C++20协程框架分享
    引入协程背景有大量的异步业务逻辑, 传统的回调代码割裂, 可读性差, 不可避免的回调地狱简化编码复杂度,希望底层能够支持协程, 简化跨线程或者实现rpc的能力项目Github: CrystalNetCrystalNet支持C++20, 包括requires, 协程等特性协程框架是CrystalNet的一个底层支持协程源码路径:kernel/include/kernel/comp/Coroutines测试case: TestCoroutine.h/TestCoroutine.cpp TestPoller.h/TestPoller.cpp细节介绍封装一个通用的协程类template<typename T> class CoTask<T>封装一个阻塞等待类CoWaiter,并提供阻塞等待接口:CoTask<> Waiting(), 用于等待条件满足时唤醒协程设计封装了一个Poller,主要用于处理事件循环, 协程的suspend时候会向Poller抛异步任务调度协程, 直到在CoWaiting时永久阻塞CoTask提供GetParam让用户在协程阻塞时获取到协程句柄,方便用户在条件满足时通过协程句柄唤醒协程Poller提供SendAsync接口实现跨线程的通信(协程方式提供)// 跨线程协程消息(otherPoller也可以是自己) // req暂时只能传指针,而且会在otherChannel(可能不同线程)释放 // req/res 必须实现Release, ToString接口 template<typename ResType, typename ReqType> requires requires(ReqType req, ResType res) { // req/res必须有Release接口 req.Release(); res.Release(); // req/res必须有ToString接口 req.ToString(); res.ToString(); } CoTask<KERNEL_NS::SmartPtr<ResType, AutoDelMethods::Release>> SendToAsync(Poller &otherPoller, ReqType *req) { // 1.ptr用来回传ResType KERNEL_NS::SmartPtr<ResType *, KERNEL_NS::AutoDelMethods::CustomDelete> ptr(KERNEL_NS::KernelCastTo<ResType *>( kernel::KernelAllocMemory<KERNEL_NS::_Build::TL>(sizeof(ResType **)))); ptr.SetClosureDelegate([](void *p) { // 释放packet auto castP = KERNEL_NS::KernelCastTo<ResType*>(p); if(*castP) (*castP)->Release(); KERNEL_NS::KernelFreeMemory<KERNEL_NS::_Build::TL>(castP); }); *ptr = NULL; // 设置stub => ResType的事件回调 UInt64 stub = ++_maxStub; KERNEL_NS::SmartPtr<KERNEL_NS::TaskParamRefWrapper, KERNEL_NS::AutoDelMethods::Release> params = KERNEL_NS::TaskParamRefWrapper::NewThreadLocal_TaskParamRefWrapper(); SubscribeStubEvent(stub, [ptr, params](KERNEL_NS::StubPollerEvent *ev) mutable { KERNEL_NS::ObjectPollerEvent<ResType> *finalEv = KernelCastTo<KERNEL_NS::ObjectPollerEvent<ResType>>(ev); // 将结果带出去 *ptr = finalEv->_obj; finalEv->_obj = NULL; // 唤醒Waiter auto &coParam = params->_params; if(coParam && coParam->_handle) coParam->_handle->ForceAwake(); }); // 发送对象事件 ObjectPollerEvent到 other auto iterChannel = _targetPollerRefChannel.find(&otherPoller); if(LIKELY(iterChannel != _targetPollerRefChannel.end())) { auto objEvent = ObjectPollerEvent<ReqType>::New_ObjectPollerEvent(stub, false, this, iterChannel->second); objEvent->_obj = req; iterChannel->second->Send(objEvent); } else { auto objEvent = ObjectPollerEvent<ReqType>::New_ObjectPollerEvent(stub, false, this, nullptr); objEvent->_obj = req; otherPoller.Push(objEvent); } // 等待 ObjectPollerEvent 的返回消息唤醒 auto poller = this; // 外部如果协程销毁兜底销毁资源 auto releaseFun = [stub, poller]() { poller->UnSubscribeStubEvent(stub); }; auto delg = KERNEL_CREATE_CLOSURE_DELEGATE(releaseFun, void); co_await KERNEL_NS::Waiting().SetDisableSuspend().GetParam(params).SetRelease(delg); if(LIKELY(params->_params)) { auto &pa = params->_params; if(pa->_errCode != Status::Success) { g_Log->Warn(LOGFMT_OBJ_TAG("waiting err:%d, stub:%llu, req:%p") , pa->_errCode, stub, req); UnSubscribeStubEvent(stub); } // 销毁waiting协程 if(pa->_handle) pa->_handle->DestroyHandle(pa->_errCode); } // 3.将消息回调中的ResType引用设置成空 auto res = *ptr; *ptr = NULL; co_return KERNEL_NS::SmartPtr<ResType, KERNEL_NS::AutoDelMethods::Release>(res); } 提供异步化工具函数: PostCaller异步编码举例代码在测试用例:TestPoller, 示例中实现了co_await 请求一个req,并返回一个resclass TestTimeoutStartup : public KERNEL_NS::IThreadStartUp { POOL_CREATE_OBJ_DEFAULT_P1(IThreadStartUp, TestTimeoutStartup); public: TestTimeoutStartup(KERNEL_NS::LibEventLoopThread * target) : _target(target) { } virtual void Run() override { KERNEL_NS::PostCaller([this]() mutable -> KERNEL_NS::CoTask<> { auto targetPoller = co_await _target->GetPoller(); auto req = HelloWorldReq::New_HelloWorldReq(); auto res = co_await targetPoller->template SendAsync<HelloWorldRes, HelloWorldReq>(req).SetTimeout(KERNEL_NS::TimeSlice::FromSeconds(5)); g_Log->Info(LOGFMT_NON_OBJ_TAG(TestTimeoutStartup, "res return")); }); } virtual void Release() override { TestTimeoutStartup::Delete_TestTimeoutStartup(this); } KERNEL_NS::LibEventLoopThread * _target; };
  • [技术干货] C语言入门4-转载
    1. if语句1.1 if表达式成立(真)则语句执行,表达式不成立(假)则语句不执行。在C语言中,0为假,非0为真。if(表达式)    语句AI写代码cpp运行练习:判断输入的数字是否为奇数#include <stdio.h>int main(){    int num=0;    scanf ("%d",&num);    if(num %2==1)    printf("输入的%d是一个奇数\n",num);    return 0;}AI写代码cpp运行1.2 else 在不符合 if 中条件时,有其他执行方案,就用到 if ... else...语句if(判断条件)    语句else    语句AI写代码cpp运行接上面的例子,判断输入的数字是偶数还是奇数并打印#include <stdio.h>int main(){    int num=0;    scanf("%d",&num);    if(num %2==1)    printf("输入的%d是奇数\n",num);    else    printf("输入的%d是偶数\n",num);    return 0;}AI写代码cpp运行练习2,判断输入的年纪是否成年#include <stdio.h>int main(){    int age=0;    scanf("%d",&age);    if(age>=18)    printf("输入的年纪已成年");    else    printf("输入的年纪未成年");    return 0;}AI写代码cpp运行1.3 分支中包含多条语句if 和 else默认只控制一条语句,如果要控制多条语句,则要用 { } 将其括起来。这个块也叫程序块或者复合语句。接上面成年和未成年的例子#include <stdio.h>int main(){    int age=0;    scanf("%d",&age);    if(age>=18)    {        printf("输入的年纪已成年");        printf("开启下一阶段");    }    else    {        printf("输入的年纪未成年");        printf("继续努力吧");    }    return 0;}AI写代码cpp运行1.4嵌套if在 if...else... 语句中,else可以和另一套if 连用,构成多重判断。例如 判断输入的年纪属于哪个层次#include <stdio.h>int main(){    int age=0;    scanf("%d",&age);    if(age<18)    printf("少年");    else if(age<40)    printf("青年");    else if(age<=59)    printf("中年");    else if(age<=89)    printf("老年");    else    printf("寿星");    return 0;}AI写代码cpp运行1.5悬空else如果有多个 if 和 else ,那么 else 和最近的 if 匹配用以下例子分析#include <stdio.h>int main(){    int a=0;    int b=2;    if(a==1)        if(b==2)            printf("Hello\n");        else            printf("World\n");    return 0;}AI写代码cpp运行这段代码的结果是什么都不输出。因为最后一个else 是和第二个 if 匹配的。第一个 if 语句不成立无法执行,那么嵌套的 if 和else 也无法执行,最后什么都不打印。那么如果想 else和第一个 if 匹配,可以添加大括号修改#include <stdio.h>int main(){    int a=0;    int b=2;    if(a==2)    {           if(b==2)        printf("Hello\n");    }    else        printf("World\n");    return 0;}AI写代码cpp运行2.关系操作符关系表达式通常返回 0(表示假) 或 1(所有非0值都表示真)比如 20>12 返回1, 12>20 返回0关系表达式常用于 if 或 while 结构注意:不要混淆 相等运算符 == 和 赋值运算符 =另外,多个关系运算符不宜连用例如i<j<kAI写代码cpp运行以上连续使用两个小于运算符。这是合法表达式,不会报错,但不会得到理想效果。由于关系运算符是从左到右计算,所以实际执行的是下面的表达式(i<j)<kAI写代码cpp运行i < j 返回的是0 或1,最后是 0 或 1 和变量 k 进行比较。如果想要判断 j 是否在 i 和 k 中间,应该做如下修改i<j&&j<kAI写代码cpp运行3.条件操作符也被称为三目运算符,需要接受三个操作数exp1 ? exp2 : exp3AI写代码cpp运行该操作符的运算逻辑:如果 exp1 为真,exp2 计算,计算的结果是整个表达式的结果;如果 exp1 为假,exp3 计算,计算的结果是整个表达式#include <stdio.h>int main(){    int a=0;    int b=0;    scanf("%d\n",&a);    if(a>5)    printf("b=3");    else         printf("b=-3");    return 0;} //改造后#include <stdio.h>int main(){    int a=0;    int b=0;    scanf("%d\n",&a);    b=a>5?3:-3;    printf("%d\n",b);    return 0;}AI写代码cpp运行4.逻辑操作符!:逻辑取反运算符,改变单个表达式的真假&&:逻辑与运算符,两侧的表达式都为真,才为真,否则为假| |:逻辑或运算符,两侧至少有一个表达式为真,才为真,否则为假4.1逻辑取反运算符 !如果现有一变量为假,要用该变量实现某代码,如下有一例子#include <stdio.h>int main(){    int num=0;//C语言中0为假    if(!num)    printf("Hello world\n");    return 0;}AI写代码cpp运行4.2逻辑与运算符需要两边表达式均为真,整个表达式才为真运用举例 现要一变量既要大于等于a,又小于等于b,必须同时满足#include <stdio.h>int main(){    int num=0;    scanf("%d",&num);    if(num>=6 && num<=9)    {        printf("Hello World\n");    }    return 0;}AI写代码cpp运行4.3逻辑或运算符该运算符要求两边都为假时,整个表达式才为假;如果至少有一个为真,那么整个表达式都为真举个例子,若输入的num满足1、3、6的任何一个,均输出Hello World#include <stdio.h>int main(){    int num=0;    scanf("%d",&num);    if(num==1||num==3||num==6)    {        printf("Hello World\n");    }    return 0;}AI写代码cpp运行4.4练习:判断闰年闰年判断规则:能被4整除但不能被100整除的是闰年;能被400整除的是闰年#include <stdio.h>int main(){    int year=0;    scanf("%d",&year);    if(year%400==0)        printf("输入的年份是闰年\n");    else if(year%4==0 && year%100!=0)    printf("输入的年份是闰年\n");    return 0;}//又或者可以用以下代码#include <stdio.h>int main(){    int year=0;    scanf("%d",&year);    if(year%4==0 && year%100!=0 ||year%400==0)        printf("输入的年份是闰年\n");    return 0;}AI写代码cpp运行4.5短路在C语言中逻辑运算符总是先对左侧的表达式求值,再对右边的表达式求值。那么如果左边的表达式满足逻辑运算符的条件,就不再对右边的表达式求值,这种情况被称为短路练习:计算代码结果#include <stdio.h>int main(){    int i=0,a=2,b=4,c=5,d=6;    i=a++ && ++b && d++;    printf("i=%d\n a=%d\n b=%d\n c=%d\n d=%d\n",i,a,b,c,d);    return 0;}AI写代码cpp运行5.switch语句switch 是一种特殊的if else 结构,用于判断条件有多个结果的情况。它将多个 else if 改成可读性更好的形式。switch(expression){    case value1:statement    case value2:statement    default:statement}AI写代码cpp运行上述代码中,根据表达式expression 不同的值,执行相应的case分支。如果找不到对应的值,就执行 default 分支。注意:switch 后的expression 和case后的值都必须是整型表达式5.1 if 语句和 switch语句的对比练习:输入任意一个整数,计算除3之后的余数//用if语句#include <stdio.h>int main(){    int num=0;    scanf("%d",&num);    if(num%3==0)        printf("余数为0\n");    else if(num%3==1)        printf("余数为1\n");         else              printf("余数为2\n");    return 0;}//用switch#include <stdio.h>int main(){    int num=0;    scanf("%d",&num);    switch(num%3)    {    case 0:        printf("余数是0\n");        break;    case 1:        printf("余数是1\n");        break;    case 2:        printf("余数是2\n");        break;    }    return 0;}AI写代码cpp运行在switch语句中,需要注意:case 和后面的数字之间必须有空格;每一个case 语句的代码执行完成之后,需要加上 break ,才能跳出这个switch语句如果删去 case 中的break,代码会继续往下执行,有可能执行其他 case 的代码,直到碰见下一个 break 或者switch语句结束。但 break 也不是每个 case 语句都有,需要根据实际情况来看。练习:输入数字1-7并打印相应的星期数#include <stdio.h>int main(){    int num=0;    scanf("%d",&num);    switch(num)    {    case 1:        printf("星期一");        break;    case 2:        printf("星期二");        break;    case 3:        printf("星期三");        break;    case 4:        printf("星期四");        break;    case 5:        printf("星期五");        break;    case 6:        printf("星期六");        break;    case 7:        printf("星期日");        break;    }    return 0;}AI写代码cpp运行也可以用工作日和休息日的表达方式#include <stdio.h>int main(){    int num=0;    scanf("%d",&num);    switch(num)    {    case 1:    case 2:    case 3:    case 4:    case 5:        printf("工作日");        break;    case 6:    case 7:        printf("休息日");        break;    }    return 0;}AI写代码cpp运行5.2switch语句中的default当 switch 后表达式中无法匹配代码中的 case 语句时,要么不做处理,要么在 switch 语句中加入 default子句。switch(expression){    case value1:statement    case value2:statement    default:statement}AI写代码cpp运行例如上面输入星期数的练习,入如果输入的不是1-7,则要提示输入错误#include <stdio.h>int main(){    int num=0;    scanf("%d",&num);    switch(num)    {    case 1:    case 2:    case 3:    case 4:    case 5:        printf("工作日");        break;    case 6:    case 7:        printf("休息日");        break;    default:        printf("输入错误\n");        break;    }    return 0;}AI写代码cpp运行另外 switch 中 case 和 default 语句顺序没有要求,只是习惯把default 放在最后处理。6.while循环和if 语句相比,两者的语句结构很相似//if语句if(表达式)    语句;//while语句while(表达式)    语句;//循环体如果想包含更多语句,可以加大括号AI写代码cpp运行对于 while 来讲,首先上来是执行表达式,表达式的值为0,循环直接结束;表达式的值不为0,则执行循环语句,语句执行完后再继续判断,是否进行下一次判断举个例子:在屏幕上打印1-10的值#include <stdio.h>int main(){    int i=1;    while(i<=10)    {        printf("%d",i);        i++;    }    return 0;}AI写代码cpp运行练习:逆序打印输入数值的顺序#include <stdio.h>int main(){    int num=0;    scanf("%d",&num);    while(num)        {            printf("%d",num%10);            num/=10;//相当于num=num/10,意义在于不断剥离最后一位数字        }    return 0;}AI写代码cpp运行7.for循环for 循环语法形式如下for(表达式1;表达式2;表达式3)    语句;//同样的,如果想在循环体包含更多语句,可以加上大括号AI写代码cpp运行表达式1:循环变量的初始化表达式2:循环结束条件的判断表达式3:循环变量的调整首先执行表达式1初始化变量,然后执行表达式2的判断部分,表达式2的结果如果等于0,那么循环结束;表达式2的结果如果不等于0,则执行循环语句。当执行语句全部执行后,再去执行表达式3,调整循环变量。然后再去表达式2执行判断在整个循环过程中,表达式1的变量初始化只执行一次,然后在表达式2、循环语句、表达式3中循环。练习:运用for在屏幕上打印1-10 的值#include <stdio.h>int main(){    for(num=1;num<=10;num++)    printf("%d\",num);    return 0;}AI写代码cpp运行将for和while 做对比两者都有初始化,判断,调整三部分。但for 的三部分比较集中,便于代码的保护。在代码较多的情况下,while 循环的三部分显得分散。练习:计算1~100之间3的倍数数字之和#include <stdio.h>int main(){    int num=0,sum=0;    for(num=1;num<=100;num++)    {        if(num%3==0)        sum=sum+num;//相等于sum+=num;    }    printf("%d\n",sum);    return 0;}AI写代码cpp运行8.do-while循环do-while 循环结构如下,该循环体和while 、for 这种需要先判断条件再执行循环的语句不同do-while循环是先执行循环语句,再执行while 后的判断表达式。表达式为真,才会进行下一次;为假则终止。因此在do-while语句中循环体至少是执行一次的do    语句;while(表达式);AI写代码cs运行仍然用在屏幕上打印1-10练习#include <stdio.h>int main(){    int num=1;    do    {        printf("%d\n",num);        num=num+1;    }while(num<=10);    return 0;}AI写代码cpp运行练习2:统计输入数值的位数#include <stdio.h>int main(){    int num=0;    scanf("%d",&num);    int count=0;        do        {            count++;            num=num/10;        }while(num);        printf("%d\n",count);    return 0;}AI写代码cpp运行9. while 和 for  以及 do-while 循环中 break 和 continue9.1 break举例用while 和for 练习在屏幕上打印1-10当打印完1-4之后,num =5 时,循环在break 终止,不再打印和循环break 的作用是永久终止这一层循环。while 循环#include <stdio.h>int main(){    int num=1;    while(num<=10)        {            if(num==5)               break;            printf("%d\n",num);            num++;        }    return 0;}AI写代码cpp运行for 循环//for循环#include <stdio.h>int main(){    int num=1;    for(num=1;num<=10;num++)        {            if(num==5)            break;            printf("%d\n",num);        }    return 0;}AI写代码cpp运行9.2 continue举例while 循环//while循环#include <stdio.h>int main(){    int num=1;    while(num<=10)        {            if(num==5)               continue;//当num=5之后,执行continue:跳过num=5时后面所有代码,直接回到条件判断                        //但这里在数字打印之后才对num自加,故num数值一直是5,陷入死循环            printf("%d\n",num);            num++;        }    return 0;}AI写代码cpp运行continue 作用是跳过后面所有代码,回到条件判断处,继续进行下一次循环的判断。因此如果循环变量的调整是在continue之后的话,可能陷入死循环。 for 循环#include <stdio.h>int main(){    int num=1;    for(num=1;num<=10;num++)    {        if(num==5)         continue;//当num=5之后,执行continue:此次num=5不打印。num继续循环,加1后进入下一次判断         printf("%d\n",num);    }    return 0;}AI写代码cpp运行在 for 循环中 ,continue 的作用是跳过本次循环中 continue 后的代码,直接去到循环的调整部分。未来当某个条件发生的时候,本次循环无需再执行后续某些操作的时候,就可以使用continue 来实现while 和 for 代码对比 同时,do-while循环的 break和 continue 和上述一样,在此不赘述。10.goto语句goto 语句可以实现在同一个函数内跳转到设置好的标号处。在多层循环的代码中方便快速跳出。#include <stdio.h>int main(){    printf("你好世界\n");    goto next;    printf("世界不好\n");    next:        printf("Hello World\n");//跳过第二个printf,只打印第一个和第三个printf    return 0;}AI写代码cpp运行但如果该语句使用不当,会导致在函数内部随意跳转,打乱程序执行顺序,尽量少用。使用场景举例for(...){    for(...)    {        for(...)        {            if(disaster)                goto error;        }    }}AI写代码cpp运行在该循环中想提前退出需要使用break。但一个break 只能跳出一个for 循环。在此处想要完全跳出循环就需要使用3个break。在这种情况下使用goto更方便。11.循环嵌套练习 找出100—200之间的素数并咋屏幕上打印首先用循环找出100—200;再用2~i-1试除#include <stdio.h>int main(){        int i=0;    for(i=100;1<=200;i++)    {        int j=0;        int flag=1;        for(j=2;j<i;j++)        {            if(i%j==0)            {                flag=0;                break;            }        }        if(flag==1)            printf("%d\n",i);    }    return 0;}AI写代码cpp运行————————————————版权声明:本文为CSDN博主「H1B2L3D4」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/H1B2L3D4/article/details/154287668
  • [技术干货] C#使用HttpClient发起HTTP请求的完整指南【转载】
    1、简述在现代应用中,调用 RESTful API 已成为日常开发中不可或缺的一部分。无论你在开发桌面程序、Web 服务还是后台任务,HttpClient 都是 .NET 提供的官方网络请求利器。本文将带你深入了解 HttpClient 的使用方式,并通过多个实践样例帮助你快速掌握它。2、HttpClient 是什么?HttpClient 是 .NET 中用于发送 HTTP 请求和接收响应的核心类,属于命名空间:1using System.Net.Http;它支持:GET / POST / PUT / DELETE 等 HTTP 方法异步请求(基于 async/await)自定义请求头与内容类型连接复用与超时控制JSON 数据序列化与反序列化创建 HttpClient 实例最基础的创建方式如下:1var client = new HttpClient();但是要注意:不要在每次请求时 new HttpClient()!因为它会导致连接未及时释放,引起端口耗尽问题。正确的做法是:在应用生命周期内 重用 HttpClient 实例;或使用 HttpClientFactory(在 ASP.NET Core 中推荐)。3、实践样例下面我们从最常见的 GET 与 POST 请求 开始。示例 1:GET 请求12345678910111213141516171819202122using System;using System.Net.Http;using System.Threading.Tasks; class Program{    static async Task Main()    {        using var client = new HttpClient();         var url = "https://api.github.com/repos/dotnet/runtime";        // 设置 User-Agent,否则 GitHub API 会拒绝访问        client.DefaultRequestHeaders.Add("User-Agent", "CSharpHttpClientDemo");         var response = await client.GetAsync(url);        response.EnsureSuccessStatusCode(); // 确保状态码 200-299         var content = await response.Content.ReadAsStringAsync();        Console.WriteLine("返回内容:");        Console.WriteLine(content);    }}输出为 JSON 格式的仓库信息。示例 2:POST 请求(发送 JSON 数据)123456789101112131415161718192021222324using System;using System.Net.Http;using System.Text;using System.Threading.Tasks;using System.Text.Json; class Program{    static async Task Main()    {        using var client = new HttpClient();         var url = "https://httpbin.org/post";        var data = new { Name = "Alice", Age = 25 };        var json = JsonSerializer.Serialize(data);         var content = new StringContent(json, Encoding.UTF8, "application/json");        var response = await client.PostAsync(url, content);         var result = await response.Content.ReadAsStringAsync();        Console.WriteLine("响应内容:");        Console.WriteLine(result);    }}该示例演示了如何:将 C# 对象序列化为 JSON;使用 StringContent 设置请求体;指定 Content-Type 为 application/json。4、其他常用操作1、设置请求头12client.DefaultRequestHeaders.Add("Authorization", "Bearer your_token_here");client.DefaultRequestHeaders.Add("Accept", "application/json");2、PUT / DELETE 请求123456// PUT 请求var putContent = new StringContent("{\"name\":\"Bob\"}", Encoding.UTF8, "application/json");var putResponse = await client.PutAsync("https://httpbin.org/put", putContent); // DELETE 请求var deleteResponse = await client.DeleteAsync("https://httpbin.org/delete");3、超时与异常处理1234567891011client.Timeout = TimeSpan.FromSeconds(10); try{    var response = await client.GetAsync("https://slowwly.robertomurray.co.uk/delay/5000/url/http://example.com");    Console.WriteLine(await response.Content.ReadAsStringAsync());}catch (TaskCanceledException){    Console.WriteLine("请求超时!");}4、反序列化 JSON 响应12345678910111213using System.Text.Json; var jsonStr = await response.Content.ReadAsStringAsync();var repoInfo = JsonSerializer.Deserialize<Repo>(jsonStr); Console.WriteLine($"项目名称:{repoInfo.name}");Console.WriteLine($"Star 数:{repoInfo.stargazers_count}"); class Repo{    public string name { get; set; }    public int stargazers_count { get; set; }}5、天气查询程序 这是一个实际的 API 调用案例,使用 Open-Meteo API 查询天气:123456789101112131415161718192021222324252627282930313233using System;using System.Net.Http;using System.Text.Json;using System.Threading.Tasks; class Program{    static async Task Main()    {        using var client = new HttpClient();         string url = "https://api.open-meteo.com/v1/forecast?latitude=35&longitude=139&current_weather=true";        var response = await client.GetAsync(url);        response.EnsureSuccessStatusCode();         var json = await response.Content.ReadAsStringAsync();        var weather = JsonSerializer.Deserialize<WeatherResponse>(json);         Console.WriteLine($"当前温度:{weather.current_weather.temperature} °C");        Console.WriteLine($"风速:{weather.current_weather.windspeed} km/h");    }} class WeatherResponse{    public CurrentWeather current_weather { get; set; }} class CurrentWeather{    public double temperature { get; set; }    public double windspeed { get; set; }}运行结果示例:12当前温度:21.3 °C风速:5.2 km/h6、HttpClientFactory(进阶用法)在 ASP.NET Core 中,推荐使用 IHttpClientFactory 管理 HttpClient 实例:123456// Startup.csservices.AddHttpClient("GitHub", client =>{    client.BaseAddress = new Uri("https://api.github.com/");    client.DefaultRequestHeaders.Add("User-Agent", "MyApp");});使用时:123456789101112131415public class GitHubService{    private readonly HttpClient _client;     public GitHubService(IHttpClientFactory factory)    {        _client = factory.CreateClient("GitHub");    }     public async Task<string> GetRepoAsync(string name)    {        var response = await _client.GetAsync($"repos/{name}");        return await response.Content.ReadAsStringAsync();    }}优点:自动管理连接生命周期;支持命名客户端;避免 Socket 耗尽;更易于测试与扩展。功能方法GET 请求GetAsync()POST 请求PostAsync()PUT 请求PutAsync()DELETE 请求DeleteAsync()添加头部DefaultRequestHeaders.Add()设置超时client.Timeout反序列化 JSONJsonSerializer.Deserialize<T>()7、结语通过本文你学到了:如何在 C# 中使用 HttpClient 发起各种 HTTP 请求;如何发送 JSON、处理响应与异常;如何在实际项目中使用 HttpClientFactory 优化性能。建议:在生产环境中,始终重用 HttpClient 或使用 IHttpClientFactory,并注意请求超时与重试机制。
  • [技术干货] 从零构建 gRPC 跨语言通信:C++ 服务端与 C# 客户端完整指南-转载
    一、环境准备必要工具安装1. Protocol Buffers 编译器 (protoc)Protocol Buffers 是一种轻便高效的结构化数据存储格式,gRPC 使用它来定义服务接口和数据结构。你需要下载并安装 Protocol Buffers 编译器 protoc,下载地址为:https://github.com/protocolbuffers/protobuf/releases。安装完成后,确保将 protoc 添加到系统的 PATH 环境变量中,这样在命令行中就可以直接使用它。2. gRPC 相关工具(C++)对于 C++ 开发,你需要安装 gRPC 和 protobuf 库。在 Ubuntu 系统上,可以使用以下命令进行安装:sudo apt install libgrpc++-dev libprotobuf-dev protobuf-compiler grpc-plugins3. .NET 环境C# 开发需要安装 .NET SDK(版本 ≥ 6.0),你可以从 https://dotnet.microsoft.com/download 下载并安装。安装完成后,在 C# 项目中添加必要的 NuGet 包:dotnet add package Grpc.Net.Clientdotnet add package Google.Protobufdotnet add package Grpc.Tools二、定义服务接口创建 proto 文件首先,我们需要创建一个 .proto 文件来定义服务接口和数据结构。创建一个名为 sample_service.proto 的文件,内容如下:syntax = "proto3";option csharp_namespace = "SampleService.Client";package sampleservice;// 请求消息message CalculationRequest {  double a = 1;  double b = 2;}// 响应消息message CalculationResult {  double result = 1;}// 服务定义service Calculator {  // 加法运算  rpc Add (CalculationRequest) returns (CalculationResult);  // 减法运算  rpc Subtract (CalculationRequest) returns (CalculationResult);}在这个 .proto 文件中,我们定义了一个名为 Calculator 的服务,包含两个方法:Add 和 Subtract,分别用于执行加法和减法运算。同时,我们还定义了请求消息 CalculationRequest 和响应消息 CalculationResult。三、C++ 服务端实现1. 生成 gRPC 代码为了根据 .proto 文件生成 C++ 代码,我们需要创建一个脚本 generate.sh:#!/bin/bashprotoc -I=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` sample_service.protoprotoc -I=. --cpp_out=. sample_service.proto运行这个脚本后,会生成以下文件:sample_service.grpc.pb.h:包含服务接口的定义。sample_service.grpc.pb.cc:服务接口的实现代码。sample_service.pb.h:消息类型的定义。sample_service.pb.cc:消息类型的实现代码。2. 实现服务逻辑创建 calculator_service.h 文件,实现服务逻辑的头文件:#include <grpcpp/grpcpp.h>#include "sample_service.grpc.pb.h"using grpc::ServerContext;using grpc::Status;using sampleservice::CalculationRequest;using sampleservice::CalculationResult;using sampleservice::Calculator;class CalculatorServiceImpl final : public Calculator::Service {public:  Status Add(ServerContext* context, const CalculationRequest* request, CalculationResult* response) override;  Status Subtract(ServerContext* context, const CalculationRequest* request, CalculationResult* response) override;};然后,创建 calculator_service.cpp 文件,实现具体的服务逻辑:#include "calculator_service.h"Status CalculatorServiceImpl::Add(ServerContext* context, const CalculationRequest* request, CalculationResult* response) {  double result = request->a() + request->b();  response->set_result(result);  return Status::OK;}Status CalculatorServiceImpl::Subtract(ServerContext* context, const CalculationRequest* request, CalculationResult* response) {  double result = request->a() - request->b();  response->set_result(result);  return Status::OK;}3. 实现服务端主程序创建 server.cpp 文件,实现服务端的主程序:#include <iostream>#include <memory>#include <string>#include <grpcpp/grpcpp.h>#include "calculator_service.h"using grpc::Server;using grpc::ServerBuilder;using grpc::ServerContext;using grpc::Status;void RunServer() {  std::string server_address("0.0.0.0:50051");  CalculatorServiceImpl service;  ServerBuilder builder;  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());  builder.RegisterService(&service);  std::unique_ptr<Server> server(builder.BuildAndStart());  std::cout << "Server listening on " << server_address << std::endl;  server->Wait();}int main() {  RunServer();  return 0;}4. 编译服务端为了编译服务端代码,我们需要创建一个 CMakeLists.txt 文件:cmake_minimum_required(VERSION 3.10)project(grpc_server)set(CMAKE_CXX_STANDARD 17)find_package(Protobuf REQUIRED)find_package(gRPC REQUIRED)add_executable(server server.cpp calculator_service.cpp sample_service.pb.cc sample_service.grpc.pb.cc)target_link_libraries(server PRIVATE gRPC::grpc++ gRPC::grpc protobuf::libprotobuf)然后,按照以下步骤进行编译和运行:mkdir build && cd buildcmake .. && make./server四、C# 客户端实现1. 创建 C# 项目使用以下命令创建一个新的 C# 控制台项目:dotnet new console -n GrpcClientcd GrpcClientdotnet add package Grpc.Net.Clientdotnet add package Google.Protobufdotnet add package Grpc.Tools2. 添加 proto 文件将之前创建的 sample_service.proto 文件复制到 C# 项目目录下,并修改 .csproj 文件,添加以下内容:<ItemGroup>  <Protobuf Include="sample_service.proto" GrpcServices="Client" /></ItemGroup>3. 实现客户端修改 Program.cs 文件,实现客户端代码:using System;using System.Threading.Tasks;using Grpc.Net.Client;using SampleService.Client;class Program {  static async Task Main(string[] args) {    using var channel = GrpcChannel.ForAddress("http://localhost:50051");    var client = new Calculator.CalculatorClient(channel);    try {      // 加法示例      var addRequest = new CalculationRequest { A = 5, B = 3 };      var addResult = await client.AddAsync(addRequest);      Console.WriteLine($"5 + 3 = {addResult.Result}");      // 减法示例      var subRequest = new CalculationRequest { A = 10, B = 4 };      var subResult = await client.SubtractAsync(subRequest);      Console.WriteLine($"10 - 4 = {subResult.Result}");    } catch (Grpc.Core.RpcException ex) {      Console.WriteLine($"RPC failed: {ex.Status.Detail}");    }  }}4. 运行客户端在项目目录下,使用以下命令运行客户端:dotnet run五、测试与验证1. 启动 C++ 服务端在终端中运行编译好的服务端程序:./server服务端启动后,会监听 0.0.0.0:50051 端口。2. 运行 C# 客户端在另一个终端中,进入 C# 客户端项目目录,运行客户端程序:dotnet run如果一切正常,你将看到以下输出:5 + 3 = 810 - 4 = 6AI写代码12这表明客户端成功调用了服务端的方法,并得到了正确的结果。六、常见问题解决1. 连接失败确保服务端地址正确:检查客户端代码中指定的服务端地址和端口是否与服务端实际监听的地址和端口一致。检查防火墙设置:确保防火墙没有阻止客户端与服务端之间的通信。可以临时关闭防火墙进行测试,或者在防火墙中开放相应的端口。服务端和客户端使用相同的协议:确保客户端和服务端使用相同的协议(HTTP/HTTPS)。如果服务端使用的是不安全连接(InsecureServerCredentials),客户端也应该使用不安全连接。2. 序列化错误确保 proto 文件在服务端和客户端完全一致:.proto 文件是服务端和客户端通信的契约,必须保证两端的 .proto 文件内容完全相同。重新生成代码后清理并重新编译:如果修改了 .proto 文件,需要重新生成代码,并清理和重新编译服务端和客户端项目。3. 性能优化对于高频调用,考虑使用连接池:gRPC 本身没有提供连接池的功能,但可以通过第三方库或自定义实现来实现连接池,以减少连接建立和销毁的开销。调整 gRPC 通道参数:可以根据实际情况调整 gRPC 通道的参数,如超时时间、最大消息大小等,以提高性能。七、流程总结以下是按照步骤,用表格形式总结的基于gRPC实现跨语言通信(C++服务端、C#客户端)的流程:gRPC跨语言通信实现流程总结表步骤    任务具体内容    详细描述1. 环境准备    安装必要的工具和库    安装Protocol Buffers编译器(protoc),如在Windows系统中下载安装包并配置环境变量,使其可在命令行中直接调用;安装gRPC相关工具,包括gRPC插件(如grpc_cpp_plugin和grpc_csharp_plugin),用于生成对应语言的源代码;安装.NET环境(如.NET SDK),用于C#客户端开发,可通过官网下载对应版本安装包并安装,安装完成后可通过命令行运行dotnet --version验证安装是否成功;此外,还需安装C++开发环境(如Visual Studio),用于C++服务端的开发,安装时选择对应的开发工作负载,如“桌面开发”等2. 定义服务接口    使用.proto文件定义服务接口和数据结构    创建.proto文件来定义服务接口及数据结构,例如定义一个名为MyService的服务,其中包含一个SayHello方法,该方法接收一个HelloRequest消息类型作为参数,并返回一个HelloReply消息类型,.proto文件中会包含对应的message定义来具体描述HelloRequest和HelloReply的字段,如string name字段等3. 生成代码    根据.proto文件生成C++和C#代码    在命令行中使用protoc编译器,结合相应的gRPC插件来生成代码,对于C++,运行类似protoc --grpc_out=. --plugin=protoc-gen-grpc=grpc_cpp_plugin path/to/your/service.proto的命令,生成C++服务端的桩代码;对于C#,运行类似protoc --grpc_out=. --plugin=protoc-gen-grpc=grpc_csharp_plugin path/to/your/service.proto的命令,生成C#客户端的存根代码4. 实现服务端    在C++中实现服务端逻辑,并编译运行    根据生成的C++桩代码,在C++项目中实现服务端的具体逻辑,如实现MyService服务中的SayHello方法的业务逻辑,编写一个SayHello的重写方法,在该方法中根据输入的HelloRequest来构造HelloReply作为返回值;之后配置C++项目的编译链接环境,添加必要的gRPC、Protocol Buffers库依赖等,编译生成服务端可执行文件,并运行服务端5. 实现客户端    在C#中实现客户端代码,并运行客户端进行测试    根据生成的C#存根代码,在C#项目(如使用.NET SDK创建的控制台应用程序项目)中实现客户端逻辑,创建Channel连接到服务端地址,创建客户端实例,并调用服务端的方法,如调用SayHello方法,构造HelloRequest并发送请求,接收服务端返回的HelloReply;配置C#项目依赖,添加gRPC、Protocol Buffers相关的NuGet包引用,运行客户端进行测试6. 测试与验证    启动服务端和客户端,验证通信是否正常    先启动服务端,确保服务端处于运行状态并监听端口;然后启动客户端,客户端会发送请求到服务端,服务端处理后返回响应给客户端,观察客户端是否能正确接收到预期的响应结果,如在控制台输出“Hello, [name]”之类的响应信息来验证通信是否成功7. 问题解决    遇到问题时,根据具体情况进行排查和解决    如果服务端和客户端无法通信,可以检查网络连接是否正常,如端口是否被正确监听和连接;检查服务端和客户端的.proto文件版本是否一致,以及生成的代码是否正确;检查是否所有必要的依赖库都已正确安装和配置;通过查看日志信息、调试程序等方式来定位问题并解决,根据问题具体表现来逐步排查原因希望这个表格能帮助你清晰地梳理整个gRPC跨语言通信实现的流程。通过以上步骤,我们完成了一个完整的 gRPC 跨语言通信系统的搭建,实现了 C++ 服务端和 C# 客户端之间的通信。这个示例展示了 gRPC 的核心功能,你可以基于此扩展更复杂的业务逻辑。gRPC 的强大之处在于它的跨语言特性和高性能通信能力,非常适合微服务架构。————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/Z_oioihoii/article/details/148904653
  • [技术干货] Flink 窗口(Windows)技术从概念到可落地代码-转载
    1、为什么需要窗口?流是无限的,而许多聚合/统计天然需要有限集合。窗口就是把无限流按时间或会话等规则切成有限桶(bucket),对每个“桶”执行聚合或自定义逻辑,从而在低延迟与有界计算之间取得平衡。 2、Keyed vs Non-Keyed 与通用编程模型Keyed 窗口(并行): stream    .keyBy(...)                // 必须:按 key 切分逻辑流    .window(...)               // 必须:窗口分配器   [.trigger(...)]             // 可选:自定义触发器   [.evictor(...)]             // 可选:逐出器(禁用预聚合)   [.allowedLateness(...)]     // 可选:允许迟到   [.sideOutputLateData(...)]  // 可选:迟到侧输出    .reduce/aggregate/apply()/process()  // 必须:窗口函数   [.getSideOutput(...)]       // 可选:获取侧输出 Non-Keyed 窗口(单并行度): stream    .windowAll(...)   [同上可选项]    .reduce/aggregate/apply()/process(); 经验:优先使用 Keyed 窗口(可扩展、并行),windowAll() 仅用于确有必要的全局聚合。 3、窗口分配器(Window Assigners)3.1 滚动(Tumbling)窗口:固定大小、不重叠input.keyBy(x -> x.userId)     .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5))) // 每5分钟一个窗口     .reduce(...); 支持 Duration 指定大小;支持 offset 调整对齐(如中国时区 Duration.ofHours(-8))。3.2 滑动(Sliding)窗口:固定大小、按步长滑动,可重叠input.keyBy(x -> x.userId)     .window(SlidingEventTimeWindows.of(Duration.ofMinutes(10), Duration.ofMinutes(5))); // 大小10min,步长5min 步长越小,元素被复制到的窗口越多,状态与计算成倍增加。3.3 会话(Session)窗口:按“活跃-空闲-活跃”分段// 静态会话gapinput.keyBy(x -> x.userId)     .window(EventTimeSessionWindows.withGap(Duration.ofMinutes(10))); // 动态会话gap(基于元素自定义)input.keyBy(x -> x.userId)     .window(EventTimeSessionWindows.withDynamicGap(ev -> computeGap(ev)));会话窗口可合并:两个会话之间的间隔小于 gap 会被合并。因此它需要可合并的触发器/窗口函数。3.4 全局(Global)窗口:同一 key 的所有元素都进一个窗口input.keyBy(x -> x.userId)     .window(GlobalWindows.create())     .trigger(/* 必填:自定义触发器,否则永不触发 */);4、窗口函数(Window Functions)4.1 ReduceFunction:同类型增量聚合(高效)input.keyBy(x -> x.userId)     .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5)))     .reduce((a, b) -> Tuple2.of(a.f0, a.f1 + b.f1)); // e.g. 按用户金额滚动累加 4.2 AggregateFunction:IN/ACC/OUT 三段式增量聚合(灵活)static class AvgAgg implements AggregateFunction<Tuple2<String, Long>, Tuple2<Long, Long>, Double> {  public Tuple2<Long, Long> createAccumulator() { return Tuple2.of(0L, 0L); }  public Tuple2<Long, Long> add(Tuple2<String, Long> v, Tuple2<Long, Long> acc){ return Tuple2.of(acc.f0+v.f1, acc.f1+1); }  public Double getResult(Tuple2<Long, Long> acc){ return (double) acc.f0 / acc.f1; }  public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b){ return Tuple2.of(a.f0+b.f0, a.f1+b.f1); }} input.keyBy(t -> t.f0)     .window(SlidingEventTimeWindows.of(Duration.ofMinutes(10), Duration.ofMinutes(5)))     .aggregate(new AvgAgg()); 4.3 ProcessWindowFunction:拿到所有元素 + 窗口上下文(最灵活)static class MyProc extends ProcessWindowFunction<Tuple2<String, Long>, String, String, TimeWindow> {  @Override  public void process(String key, Context ctx, Iterable<Tuple2<String, Long>> in, Collector<String> out) {    long cnt = 0; for (var e : in) cnt++;    out.collect("key=" + key + ", window=[" + ctx.window().getStart() + "," + ctx.window().getEnd() + "), count=" + cnt);  }} 适合需要窗口元信息或自定义逻辑的场景,但会缓存全量元素,内存压力大。可结合 Reduce/Aggregate 实现“增量聚合 + 窗口上下文”的折中:// 组合:先 Reduce(增量最小值),再 Process 输出窗口起始时间input.keyBy(x -> x.key)     .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5)))     .reduce(new MinReduce(), new MinWithWindowStartProc()); 5、触发器(Triggers)作用:决定“何时触发计算、是否清空窗口内容”。TriggerResult: CONTINUE(不动作)FIRE(触发计算)PURGE(清空窗口内容)FIRE_AND_PURGE(触发后再清空)默认触发器: 事件时间窗口 → EventTimeTrigger(watermark 超过窗口结束时触发)全局窗口 → NeverTrigger(永不触发)自定义:基于计数/时间/复合规则都可以;需要自行注册定时器并在回调里返回相应 TriggerResult。 6、逐出器(Evictors)在触发后、窗口函数前/后剔除元素。内置:CountEvictor / DeltaEvictor / TimeEvictor。 注意:使用 Evictor 会禁用所有预聚合(必须传入全量元素),状态与开销显著上升;Python DataStream 暂不支持。 7、允许迟到(Allowed Lateness)与侧输出迟到元素:watermark 已超过窗口结束时间仍到达的元素。 默认:丢弃。设置允许迟到:窗口在 end < wm ≤ end + lateness 区间仍接收数据,并可能再次触发(late firing)。迟到侧输出:把过晚被丢弃的元素放到 side output。final OutputTag<Event> lateTag = new OutputTag<>("late"){}; SingleOutputStreamOperator<Result> out = input    .keyBy(e -> e.key)    .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5)))    .allowedLateness(Duration.ofMinutes(1))      // 允许迟到1分钟    .sideOutputLateData(lateTag)                 // 过晚数据走侧输出    .reduce(new SumReduce()); DataStream<Event> lateStream = out.getSideOutput(lateTag); // 过晚数据单独处理 会话窗口在 late firing 时,还可能引发窗口合并。下游要去重/幂等地消费“更新结果”。 8、连续窗口串联 & 水位线交互窗口输出元素的时间戳被设置为 end-1(结束时间不含)。当 operator 收到 watermark:会触发所有 maxTimestamp < wm 的窗口,并将 watermark 原样下发。因此可以串联相同大小的窗口,让上游 [0,5) 的结果落入下游 [0,5):DataStream<Integer> perKey = input    .keyBy(x -> x.key)    .window(TumblingEventTimeWindows.of(Duration.ofSeconds(5)))    .reduce(new Sum()); DataStream<Integer> global = perKey    .windowAll(TumblingEventTimeWindows.of(Duration.ofSeconds(5)))    .process(new TopKProc()); 9、状态大小与性能考量元素会在所属的每个窗口各存一份。滑动窗口(大size/小step)状态容易爆炸。Reduce / Aggregate 能显著降低状态(急切聚合仅存一个累积值)。使用 Evictor 会禁用预聚合 → 状态显著增加。合理设置 allowedLateness,避免窗口被长时间保留。10、端到端可运行 Demo(事件时间 + 迟到 + 侧输出 + 组合窗口函数)说明:Java 11+,Flink 1.15+,本地可直接跑。示例用内存集合模拟两条流:订单与支付。展示: 事件时间与水位线5 分钟滚动窗口 + 允许迟到 1 分钟侧输出过晚数据AggregateFunction + ProcessWindowFunction 组合(输出窗口元信息)自定义触发器(基于计数的额外早触发)import org.apache.flink.api.common.eventtime.WatermarkStrategy;import org.apache.flink.api.common.functions.AggregateFunction;import org.apache.flink.api.java.tuple.Tuple2;import org.apache.flink.streaming.api.TimeCharacteristic; // 1.12+ 默认事件时间,无需设置import org.apache.flink.streaming.api.datastream.DataStream;import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;import org.apache.flink.streaming.api.windowing.time.Time;import org.apache.flink.streaming.api.windowing.triggers.CountTrigger;import org.apache.flink.streaming.api.windowing.windows.TimeWindow;import org.apache.flink.util.Collector;import org.apache.flink.util.OutputTag; import java.time.Duration;import java.util.Arrays; /** 事件模型:订单(userId, amount, ts) */class Order {  public String userId;  public long amount;  public long ts; // 事件时间戳(毫秒)  public Order() {}  public Order(String userId, long amount, long ts) { this.userId = userId; this.amount = amount; this.ts = ts; }  @Override public String toString() { return "Order{user=" + userId + ", amt=" + amount + ", ts=" + ts + "}"; }} /** 聚合输出:每窗口的总金额与订单数 */class WindowStat {  public String userId;  public long sum;  public long cnt;  public long winStart;  public long winEnd; // 左闭右开  public WindowStat() {}  public WindowStat(String userId, long sum, long cnt, long winStart, long winEnd) {    this.userId = userId; this.sum = sum; this.cnt = cnt; this.winStart = winStart; this.winEnd = winEnd;  }  @Override public String toString() {    return "WindowStat{user=" + userId + ", sum=" + sum + ", cnt=" + cnt +        ", window=[" + winStart + "," + winEnd + ")}";  }} /** 增量聚合(ACC= sum,cnt) */class SumCntAgg implements AggregateFunction<Order, Tuple2<Long, Long>, Tuple2<Long, Long>> {  @Override public Tuple2<Long, Long> createAccumulator() { return Tuple2.of(0L, 0L); }  @Override public Tuple2<Long, Long> add(Order o, Tuple2<Long, Long> acc) { return Tuple2.of(acc.f0 + o.amount, acc.f1 + 1); }  @Override public Tuple2<Long, Long> getResult(Tuple2<Long, Long> acc) { return acc; }  @Override public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) { return Tuple2.of(a.f0 + b.f0, a.f1 + b.f1); }} /** 聚合后再 Process:补充 window 元信息与 key */class WithWindowInfoProc extends ProcessWindowFunction<Tuple2<Long, Long>, WindowStat, String, TimeWindow> {  @Override  public void process(String key, Context ctx, Iterable<Tuple2<Long, Long>> in, Collector<WindowStat> out) {    Tuple2<Long, Long> acc = in.iterator().next();    out.collect(new WindowStat(key, acc.f0, acc.f1, ctx.window().getStart(), ctx.window().getEnd()));  }} public class WindowsEndToEndDemo {  public static void main(String[] args) throws Exception {    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();    env.setParallelism(2);     // 定义事件时间的水位线策略:允许 2 秒乱序;从 Order.ts 提取时间戳    WatermarkStrategy<Order> wm = WatermarkStrategy        .<Order>forBoundedOutOfOrderness(Duration.ofSeconds(2))        .withTimestampAssigner((o, ts) -> o.ts);     // 构造样例数据:注意包含迟到与过晚数据    DataStream<Order> orders = env.fromCollection(Arrays.asList(        new Order("alice", 20, 1_000L),        new Order("alice", 30, 2_000L),        new Order("bob",   10, 2_500L),        new Order("alice", 40, 4_000L),        new Order("bob",   50, 6_100L) // 可能落在下个窗口    )).assignTimestampsAndWatermarks(wm);     // 过晚数据的侧输出标签    OutputTag<Order> lateTag = new OutputTag<>("late"){};     // 5 分钟滚动窗口 + 允许迟到 1 分钟 + 计数触发器(每来 3 条先早触发一次)    SingleOutputStreamOperator<WindowStat> stats = orders        .keyBy(o -> o.userId) // Keyed 窗口可并行        .window(TumblingEventTimeWindows.of(Time.minutes(5)))        .allowedLateness(Time.minutes(1)) // 允许迟到        .sideOutputLateData(lateTag)      // 过晚数据放侧输出        .trigger(CountTrigger.of(3))      // 额外的计数早触发(演示用)        .aggregate(new SumCntAgg(), new WithWindowInfoProc()) // 组合:增量聚合 + 窗口上下文        .name("window-agg");     // 主结果    stats.print().name("main-result");     // 过晚侧输出    DataStream<Order> lateStream = stats.getSideOutput(lateTag);    lateStream.print().name("late-orders");     env.execute("Flink Windows End-to-End Demo");  }}你能在这段 Demo 里看到: 事件时间 + 乱序 2s 的水位线;5 分钟滚动窗口,允许迟到 1 分钟;计数早触发(与默认的时间触发并存);侧输出接收过晚数据;AggregateFunction + ProcessWindowFunction 组合,既高效又能拿到窗口元信息;控制台会看到多次触发(主触发 + 早/迟触发)的输出差异。11、实战建议与常见坑优先事件时间:只要数据里有可靠时间戳,就用事件时间 + 水位线,避免处理时间抖动。滑动窗口慎重:size 大、step 小会造成多倍复制与状态爆炸。尽量增量聚合:优先 Reduce/Aggregate;如需窗口元信息再结合 ProcessWindowFunction。迟到处理要闭环:设置 allowedLateness 与 sideOutputLateData,下游做好更新/去重/幂等。会话窗口可能合并:注意 late firing 会导致 merge,要设计可再处理的下游逻辑。Evictor 慎用:会禁用预聚合,状态显著增加;仅在确需移除窗口内元素时启用。串联窗口:相同大小窗口可自然串联(时间戳为 end-1),上游 [0,5) 可落入下游 [0,5)。监控与排障:给关键窗口算子 name()/setDescription(),结合 Web UI/metrics/日志定位延迟与背压。————————————————                             版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/weixin_43114209/article/details/153249959
  • [技术干货] C++真的比Python更快吗?-转载
    实验测试有人说 C++ 比 Python快几十倍,有人说快上百倍,不如直接来进行实验测试。1. 简单计算场景任务:计算从 1 到 N 的所有整数的平方和N 取值为100万:python代码:import timedef sum_of_squares(n):    total = 0    for i in range(1, n + 1):        total += i * i    return totalif __name__ == "__main__":    N = 1000000    start = time.time()    result = sum_of_squares(N)    end = time.time()    print("Result:", result)    print("Time (Python):", end - start, "seconds")C++代码:#include <iostream>#include <chrono>using namespace std;long long sum_of_squares(int n) {    long long total = 0;    for (int i = 1; i <= n; i++) {        total += 1LL * i * i;    }    return total;}int main() {    int N = 1000000;    auto start = chrono::high_resolution_clock::now();    long long result = sum_of_squares(N);    auto end = chrono::high_resolution_clock::now();    chrono::duration<double> elapsed = end - start;    cout << "Result: " << result << endl;    cout << "Time (C++): " << elapsed.count() << " seconds" << endl;    return 0;}运行时间:Time (Python): 0.06529355049133301 secondsTime (C++): 0.0010278 secondsAI写代码bash12C++ 的速度约是 python 的 65 倍。如果 N 取值为 1000万:输出时间:Time (C++): 0.0167507 secondsTime (Python): 0.6048719882965088 secondsC++ 的速度约是 python 的 36 倍。2. 包含IO场景的测试写一个包含100万条的信息的日志文件。python代码import timedef generate_log_file(filename, n):    with open(filename, "w") as f:        for i in range(n):            f.write(f"[INFO] line {i}: this is a test log message\n")if __name__ == "__main__":    N = 1000000    filename = "log_python.txt"    start = time.perf_counter()    generate_log_file(filename, N)    end = time.perf_counter()    print(f"Generated {N} lines into {filename}")    print("Time (Python):", end - start, "seconds")C++代码#include <iostream>#include <fstream>#include <chrono>using namespace std;void generate_log_file(const string &filename, int n) {    ofstream fout(filename);    for (int i = 0; i < n; i++) {        fout << "[INFO] line " << i << ": this is a test log message\n";    }    fout.close();}int main() {    int N = 1000000;    string filename = "log_cpp.txt";    auto start = chrono::high_resolution_clock::now();    generate_log_file(filename, N);    auto end = chrono::high_resolution_clock::now();    chrono::duration<double> elapsed = end - start;    cout << "Generated " << N << " lines into " << filename << endl;    cout << "Time (C++): " << elapsed.count() << " seconds" << endl;    return 0;}结果Time (Python): 0.6496610003523529 secondsTime (C++): 0.442306 secondsAI写代码12受到磁盘写入速度的限制,在 IO 场景下,C++ 的速度仅为是 python 的 1.46 倍。3. 矩阵密集计算下面再测试一个矩阵相乘的计算场景。python代码,测试普通循环和使用numpy两种方式。import timeimport randomimport numpy as npN = 300  # 矩阵大小# 用 Python 列表生成矩阵A = [[random.random() for _ in range(N)] for _ in range(N)]B = [[random.random() for _ in range(N)] for _ in range(N)]# ---------- Python 三重循环 ----------start = time.perf_counter()C = [[0.0] * N for _ in range(N)]for i in range(N):    for j in range(N):        s = 0.0        for k in range(N):            s += A[i][k] * B[k][j]        C[i][j] = send = time.perf_counter()print(f"Time (Python triple loop): {end - start:.6f} seconds")# ---------- NumPy 实现 ----------A_np = np.array(A)B_np = np.array(B)start = time.perf_counter()C_np = np.dot(A_np, B_np)end = time.perf_counter()print(f"Time (NumPy): {end - start:.6f} seconds")C++代码#include <iostream>#include <vector>#include <cstdlib>#include <ctime>using namespace std;int main() {    int N = 300;  // 矩阵大小    vector<vector<double>> A(N, vector<double>(N));    vector<vector<double>> B(N, vector<double>(N));    vector<vector<double>> C(N, vector<double>(N, 0.0));    srand(time(0));    for (int i = 0; i < N; i++) {        for (int j = 0; j < N; j++) {            A[i][j] = rand() / (double)RAND_MAX;            B[i][j] = rand() / (double)RAND_MAX;        }    }    clock_t start = clock();    for (int i = 0; i < N; i++) {        for (int j = 0; j < N; j++) {            double s = 0.0;            for (int k = 0; k < N; k++) {                s += A[i][k] * B[k][j];            }            C[i][j] = s;        }    }    clock_t end = clock();    cout << "Time (C++): " << (double)(end - start) / CLOCKS_PER_SEC << " seconds\n";    return 0;}结果Time (Python triple loop): 3.641190 secondsTime (NumPy): 0.000746 secondsTime (C++): 0.207 secondsAI写代码C++ 的速度约是 python 相同写法的 17 倍,但使用numpy之后,python的速度约是C++的 277 倍。总结一般情况下,C++ 确实比 Python 会快很多,在普通计算效率上,至少有10倍以上的提升,主要原因是语言本身的差异性:C++ 是编译型语言,源代码会被编译为机器码,直接在 CPU 上运行,几乎没有额外的解释开销。Python 是解释型语言,运行时需要解释器逐行执行代码,每一步操作都要经过额外的对象管理和动态类型检查,计算效率天然落后。但是,当任务涉及文件写入、磁盘读写或网络通信 时,性能瓶颈转移到操作系统和硬件的 IO 延迟上,两者的速度差异会减小。此外,Python在借 助NumPy 等第三库之后,计算效率反而比未经优化的C++速度更快,主要原因是 Numpy 的矩阵乘法核心调用了底层 C/Fortran 的 BLAS/LAPACK 库,这些库经过几十年的优化,包含一系列优化策略:循环展开、SIMD 向量化(利用 SSE/AVX 指令集,一次处理多个数据);Cache 优化(分块算法 block matrix multiplication,减少 cache miss);多线程并行(OpenBLAS/MKL 可以利用多核 CPU 并行计算);硬件加速(在某些场景下甚至能利用 GPU)。因此,直接认为 C++ 比 Python 快是不准确的,C++ 的优化空间会比 Python 更多,但需要的操作会更繁琐。在比较运行效率时,需要考虑测试场景和代码质量。————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/qq1198768105/article/details/151805091
  • [技术干货] C++ TensorRT YOLOv8-SAHI 高性能部署指南
    C++ TensorRT YOLOv8-SAHI 高性能部署指南项目介绍本项目将介绍如何在Jetson等嵌入式设备上实现YOLOv8-SAHI的高性能部署,特别是使用Int8引擎的优化方案。在Jetson Orin Nano (8GB)设备上,图像切片和批量推理的测试时间消耗小于0.05秒,对1080p视频进行切分检测和bytetrack跟踪性能接近15FPS。# 代码仓库: https://github.com/HouYanSong/tensorrtx-yolov8-sahi导出 YOLOv8 Int8 量化模型我们固定输入图像尺寸1440x1080进行切分,其中每张切分子图的大小为640x640重叠度>20%,加上原始图像一次推理对8张图像进行检测,导出Int8量化后BatchSize=8的模型。从yolov8.pt生成yolov8s.wts权重文件pip install ultralytics python gen_wts.py从yolov8s.wts导出yolov8s.engine引擎文件,BatchSize大小为8sudo apt install libeigen3-devrm -fr build cmake -S . -B build cmake --build build cd build ./yolov8_sahi -s ../weights/yolov8s.wts ../weights/yolov8s.engine s模型的参数配置模型的配置文件为include/config.h,这里我们使用yolov8s官方预训练模型,模型的输入大小为640x640总共有80个类别,并且设置模型的kBatchSize = 8,一次最多可8以推理8张图像,指定量化图片的路径导出Int8量化后的模型。#ifndef CONFIG_H #define CONFIG_H // #define USE_FP16 #define USE_INT8 #include <string> #include <vector> const static char *kInputTensorName = "images"; const static char *kOutputTensorName = "output"; const static int kNumClass = 80; const static int kBatchSize = 8; const static int kGpuId = 0; const static int kInputH = 640; const static int kInputW = 640; const static float kNmsThresh = 0.55f; const static float kConfThresh = 0.45f; const static int kMaxInputImageSize = 3000 * 3000; const static int kMaxNumOutputBbox = 1000; const std::string trtFile = "../weights/yolov8s.engine"; const std::string cacheFile = "./int8calib.table"; const std::string calibrationDataPath = "../images/"; // 存放用于 int8 量化校准的图像 const std::vector<std::string> vClassNames { "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush" }; #endif // CONFIG_H YOLOv8-SAHI 切分检测为了验证量化后模型精度以及Batch推理的性能,这里我们使用Int8量化后的模型直接对量化图片进行切分检测,推理命令如下:cd build ./yolov8_sahi -d ../weights/yolov8s.engine ../images/在Jetson Orin Nano (8GB)上使用Int8引擎的YOLOv8-SAHI性能表现如下:sample0102.png YOLOv8-SAHI: 1775ms sample0206.png YOLOv8-SAHI: 46ms sample0121.png YOLOv8-SAHI: 44ms sample0058.png YOLOv8-SAHI: 44ms sample0070.png YOLOv8-SAHI: 44ms sample0324.png YOLOv8-SAHI: 43ms sample0122.png YOLOv8-SAHI: 44ms sample0086.png YOLOv8-SAHI: 45ms sample0124.png YOLOv8-SAHI: 45ms sample0230.png YOLOv8-SAHI: 45ms ...可以看到模型对单张图片的推理时间小于0.5毫秒,可以达到实时检测的要求。YOLOv8-SAHI-ByteTrack 视频跟踪我们可以结合ByteTrack跟踪算法对视频文件进行实时的切分检测和跟踪,在build目录下执行:cd build ./yolov8_sahi_track ../media/c3_1080.mp4 在Jetson Orin Nano (8GB)上YOLOv8-SAHI-ByteTrack性能表现如下:Total frames: 341 Init ByteTrack! Processing frame 20 (8 fps) Processing frame 40 (11 fps) Processing frame 60 (12 fps) Processing frame 80 (12 fps) Processing frame 100 (13 fps) Processing frame 120 (13 fps) Processing frame 140 (13 fps) Processing frame 160 (14 fps) Processing frame 180 (14 fps) Processing frame 200 (14 fps) Processing frame 220 (14 fps) Processing frame 240 (14 fps) Processing frame 260 (14 fps) Processing frame 280 (14 fps) Processing frame 300 (14 fps) Processing frame 320 (14 fps) Processing frame 340 (15 fps) FPS: 15 可以看到模型在1080p的视频上切分检测的帧率接近15FPS,并且ByteTrack的跟踪效果非常优秀。小结通过本项目,开发者可以在资源受限的嵌入式设备上实现高效的YOLOv8切分检测和跟踪,特别适用于需要实时处理的边缘计算场景。
  • [技术干货] C++中的list与forward_list介绍与使用【转载】
    1、list的介绍及使用1template < class T, class Alloc = allocator<T> > class list;list的底层是带头双向链表结构,双向链表中每个元素存储在独立节点中,在节点中通过指针指向其前一个元素和后一个元素。list与forward_list非常相似,最主要的不同在于forward_list是无头单向链表。与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;此外list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)。1.1、构造及赋值重载123456explicit list (const allocator_type& alloc = allocator_type()); // 默认构造explicit list (size_type n, const value_type& val = value_type(), const allocator_type& alloc = allocator_type()); // 构造n个val值template <class InputIterator>  list (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type()); // 迭代器区间构造list (const list& x); // 拷贝构造list& operator= (const list& x); // 赋值重载1.2、迭代器12345678iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;reverse_iterator rbegin();reverse_iterator rend();const_reverse_iterator rbegin() const;const_reverse_iterator rend() const;例如: 123456789101112131415161718192021int main(){    list<int> lt1;    list<int> lt2(10, 2);    list<int> lt3(lt2.begin(), lt2.end());    list<int> lt4(lt3);    lt1 = lt4;    list<int>::iterator it1 = lt1.begin();    while (it1 != lt1.end())    {        cout << *it1 << ' ';        ++it1;    }    cout << endl;    for (auto e : lt4)    {        cout << e << ' ';    }    cout << endl;    return 0;}1.3、空间12bool empty() const; // 判断是否为空size_type size() const; // 元素个数例如: 12345678910int main(){    list<int> lt1;    list<int> lt2(10, 2);    cout << lt1.empty() << endl;    cout << lt2.empty() << endl;    cout << lt1.size() << endl;    cout << lt2.size() << endl;    return 0;}1.4、访问1234reference front(); // 起始元素const_reference front() const;reference back(); // 末尾元素const_reference back() const;例如: 123456789int main(){    list<int> lt1(10, 2);    lt1.front()++;    lt1.back()--;    cout << lt1.front() << endl;    cout << lt1.back() << endl;    return 0;}1.5、修改123456789void push_front (const value_type& val); // 头插void pop_front(); // 头删void push_back (const value_type& val); // 尾插void pop_back(); // 尾删iterator insert (iterator position, const value_type& val); // 插入iterator erase (iterator position); // 删除void swap (list& x); // 交换void resize (size_type n, value_type val = value_type()); // 改变元素的个数void clear(); // 清空例如: 12345678910111213141516171819202122232425262728293031323334353637383940414243int main(){    list<int> lt;    lt.push_back(1);    lt.push_back(2);    lt.push_back(3);    lt.push_back(4);    lt.push_front(5);    lt.push_front(6);    for (auto e : lt)    {        cout << e << ' ';    }    cout << endl;    lt.resize(10, 9);    lt.insert(lt.begin(), 7);    for (auto e : lt)    {        cout << e << ' ';    }    cout << endl;    list<int> lt2(lt);    lt.clear();    for (auto e : lt2)    {        cout << e << ' ';    }    cout << endl;    lt.swap(lt2);    for (auto e : lt)    {        cout << e << ' ';    }    cout << endl;    lt.pop_front();    lt.pop_back();    for (auto e : lt)    {        cout << e << ' ';    }    cout << endl;    return 0;}1.6、操作1234void sort(); // 排序,默认是升序template <class Compare>void sort (Compare comp); // 关于仿函数,后面再说void reverse(); // 逆置例如:123456789101112131415161718192021222324252627282930313233int main(){    list<int> lt;    lt.push_back(1);    lt.push_back(2);    lt.push_back(3);    lt.push_back(4);    lt.push_front(5);    lt.push_front(6);    lt.reverse();    list<int>::iterator it = lt.begin();    while (it != lt.end())    {        cout << *it << ' ';        ++it;    }    cout << endl;    list<int> lt2(lt);    lt.sort();    for (auto e : lt)    {        cout << e << ' ';    }    cout << endl;    greater<int> gt;    lt2.sort(gt);    for (auto e : lt2)    {        cout << e << ' ';    }    cout << endl;    return 0;}2、迭代器失效前面说过,迭代器失效即迭代器所指向的节点的无效。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。例如:12345678910111213int main(){    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };    list<int> lt(array, array + sizeof(array) / sizeof(array[0]));    auto it = lt.begin();    while (it != lt.end())    {        // erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值        lt.erase(it);        ++it;    }    return 0;}当运行到++it时就会报错。 改为如下即可:123456789101112131415161718int main(){    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };    list<int> lt(array, array + sizeof(array) / sizeof(array[0]));    auto it = lt.begin();    while (it != lt.end())    {        lt.erase(it++);         //或者也可以写成:        //it = lt.erase(it);    }    for (auto e : lt)    {        cout << e << ' ';    }    cout << endl;    return 0;}3、list的模拟实现123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203template<class T>struct list_node{    list_node<T>* _prev;    list_node<T>* _next;    T _data;    list_node(const T& x = T())        :_prev(nullptr)        ,_next(nullptr)        ,_data(x)    {}};// ///////////////////////////////////////////////////////////template<class T,class Ref,class Ptr>struct __list_iterator{    typedef list_node<T> Node;    typedef __list_iterator<T,Ref,Ptr> self;//这里的拷贝构造函数和析构函数都没有写,默认的就够用的了。    Node* _node;    __list_iterator(Node* node)        :_node(node)    {}    self& operator++()    {        _node = _node->_next;        return *this;    }    self& operator--()    {        _node = _node->_prev;        return *this;    }    self& operator++(int)    {        self tmp(*this);        _node = _node->_next;        return tmp;    }    self& operator--(int)    {        self tmp(*this);        _node = _node->_prev;        return tmp;    }    Ref operator*()                   {        return _node->_data;    }    Ptr operator->()                     {        return &_node->_data;    }    bool operator!=(const self& s)    {        return _node != s._node;    }    bool operator==(const self& s)    {        return _node == s._node;    }};// /////////////////////////////////////////template<class T>class List{public:    typedef list_node<T> Node;    // 正向迭代器    typedef __list_iterator<T,T&,T*> iterator;    typedef __list_iterator<T,const T&,const T*> const_iterator;    // //////////////////////////////////////////////////////    iterator begin()    {        return _head->_next;//这里也可以写为:iterator(_head->_next);    }    iterator end()    {        return _head;//这里也可以写为:iterator(_head);    }    const_iterator begin() const    {        return _head->_next;    }    const_iterator end() const    {        return _head;    }    // ///////////////////////////////////////////////////////////    void empty_init()    {        _head = new Node;        _head->_next = _head;        _head->_prev = _head;        _size = 0;    }    List()    {        empty_init();    }    List(const List<T>& lt)    {        empty_init();        const_iterator it = lt.begin();        while (it != lt.end())        {            push_back(*it);            ++it;        }        //像下面这样写也是可以的        /*for (auto e : lt)        {            push_back(e);        }*/    }    /*List<T>& operator=(const List<T>& lt) // 传统写法    {        if (this != &lt)        {            clear();            for (auto e : lt)            {                push_back(e);            }        }        return *this;    }*/    void swap(List<T>& lt)    {        std::swap(_head, lt._head);        std::swap(_size, lt._size);    }    List<T>& operator=(List<T> lt) // 现代写法    {        swap(lt);        return *this;    }    iterator insert(iterator pos, const T& x)    {        Node* newnode = new Node(x);        Node* cur = pos._node;        Node* prev = cur->_prev;        prev->_next = newnode;        newnode->_prev = prev;        newnode->_next = cur;        cur->_prev = newnode;        ++_size;        return iterator(newnode);    }    void push_front(const T& x)    {        insert(begin(), x);    }    void pop_front()    {        erase(begin());    }    void pop_back()    {        erase(--end());    }    ~List()    {        clear();        delete _head;        _head = nullptr;    }    void clear()    {        iterator it = begin();        while (it != end())        {            it = erase(it);        }    }    iterator erase(iterator pos)    {        Node* cur = pos._node;        Node* prev = cur->_prev;        Node* next = cur->_next;        delete cur;        prev->_next = next;        next->_prev = prev;        --_size;        return iterator(next);    }    void push_back(const T& x)    {        Node* newnode = new Node(x);        Node* end = _head->_prev;        end->_next = newnode;        newnode->_prev = end;        newnode->_next = _head;        _head->_prev = newnode;        _size++;    }    size_t size()    {        return _size;    }private:    Node* _head;    size_t _size;};4、forward_list介绍与使用1template < class T, class Alloc = allocator<T> > class forward_list;forward_list的底层结构是无头单向链表。4.1、构造及赋值重载12345678910111213//默认构造explicit forward_list (const allocator_type& alloc = allocator_type());//构造n个valexplicit forward_list (size_type n, const value_type& val,                        const allocator_type& alloc = allocator_type());//用迭代区间构造template <class InputIterator>forward_list (InputIterator first, InputIterator last,                const allocator_type& alloc = allocator_type());//拷贝构造forward_list (const forward_list& fwdlst);//赋值重载forward_list& operator= (const forward_list& fwdlst);4.2、迭代器123456iterator before_begin() noexcept; // 返回容器第一个元素之前的位置const_iterator before_begin() const noexcept;iterator begin() noexcept; // 返回第一个元素的位置const_iterator begin() const noexcept;iterator end() noexcept; // 返回最后一个元素之后的位置const_iterator end() const noexcept;例如: 123456789101112131415161718192021int main(){       forward_list<int> f1;    forward_list<int> f2(5, 3);    forward_list<int> f3(f2.begin(), f2.end());    forward_list<int> f4(f3);    f1 = f4;    forward_list<int>::iterator it1 = f2.begin();    while (it1 != f2.end())    {        cout << *it1 << ' ';        ++it1;    }    cout << endl;    for (auto& e : f3)    {        cout << ++e << ' ';    }    cout << endl;    return 0;}4.3、容量1bool empty() const noexcept; // 判断是否为空4.4、访问12reference front(); // 返回第一个元素const_reference front() const;4.5、修改12345678void push_front (const value_type& val); //头插void pop_front(); // 头删iterator insert_after ( const_iterator position, const value_type& val ); // 之后插入iterator erase_after (const_iterator position); // 之后删除void swap (forward_list& fwdlst); // 交换void resize (size_type n); // 增大元素个数void resize (size_type n, const value_type& val);void clear() noexcept; // 清空例如: 1234567891011121314151617181920212223242526272829int main(){       forward_list<int> f1;    f1.push_front(1);    f1.push_front(2);    f1.push_front(3);    f1.push_front(4);    f1.push_front(5);    cout << f1.empty() << endl;    cout << f1.front() << endl;    f1.insert_after(f1.before_begin(), 6);    forward_list<int>::iterator it1 = f1.begin();    while (it1 != f1.end())    {        cout << *it1 << ' ';        ++it1;    }    cout << endl;    forward_list<int> f2;    f2.resize(10, 5);    f1.swap(f2);    f2.clear();    for (auto e : f1)    {        cout << e << ' ';    }    cout << endl;    return 0;}4.6、操作1234void sort(); // 排序,默认为升序template <class Compare>void sort (Compare comp);void reverse() noexcept; // 逆置例如:12345678910111213141516171819202122int main(){       forward_list<int> f1;    f1.push_front(5);    f1.push_front(4);    f1.push_front(3);    f1.push_front(5);    f1.push_front(2);    f1.sort();    for (auto e : f1)    {        cout << e << ' ';    }    cout << endl;    f1.reverse();    for (auto e : f1)    {        cout << e << ' ';    }    cout << endl;    return 0;}5、迭代器的分类5.1、按功能分类迭代器按功能分类可以分为正向迭代器和反向迭代器。关于反向迭代器,会在模板进阶部分进行模拟实现。5.2、按性质分类迭代器按性质(由容器的底层实现决定的)分类可以分为单向迭代器、双向迭代器以及随机迭代器。单向迭代器:只支持++,不支持--,例如:forward_list(单链表)。双向迭代器:支持++,也支持--,例如:list(双向链表)随机迭代器:支持++,也支持--,还支持+以及-,例如:vector(顺序表)、string(顺序表)以及deque(后面讲)。例如:算法库中有一个sort模板函数,用来进行排序1234template <class RandomAccessIterator>void sort (RandomAccessIterator first, RandomAccessIterator last);  template <class RandomAccessIterator, class Compare>void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);但是该模板函数不能用来对list以及forward_list进行排序,因为该模板函数要求的是传随机迭代器,这也就是为什么,明明算法库中有sort模板函数,但是forward_list以及list中也实现了sort函数的原因。6、list与vector的对比 vectorlist底层结构动态顺序表,一段连续空间带头结点的双向循环链表随机访问支持随机访问,访问某个元素效率O(1)不支持随机访问,访问某个元素效率为O(N)插入和删除任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容会开辟新空间、拷贝元素以及释放旧空间,导致效率更低任意位置插入和删除效率高,不需要搬移元素,时间复杂度为 O(1)空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高。底层节点动态开辟,节点容易造成内存碎片,空间利用率低, 缓存利用率低。迭代器原生态指针对原生态指针进行封装迭代器失效在插入元素后,要给迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效。删除后,迭代器需要重新赋值否则会失效。插入元素不会导致迭代器失效, 删除元素时,只会导致当前迭代器失效,其他迭代器不受影响。使用场景需要高效存储,随机访问,不关心插入删除效率。大量插入和删除操作,不关心随机访问。
  • [问题求助] 27期文档提供不全
    在27期文档1.1节末尾,有如下的描述:We provided a sample submission archive task1/sampleSubmission.tar.gz for your reference.但我没有在27期赛题界面没有发现提供这个文件。请问这个文件应该在哪里下载?
  • [问题求助] 28期文档不全
    在28期的任务描述中,提到2.4节Section 2.4 explains how to measure the prediction accuracy and the average computational cost of a solution,但文档没有2.4节。
  • [技术干货] C++ 底层实现细节隐藏全攻略:从简单到复杂的五种模式 -转载
    1 引言:为什么要“隐藏实现”就像尼采在《快乐的科学》中提醒我们的——“当你长久凝视深渊,深渊也在凝视你”——在大型 C++ 工程里,若让头文件暴露过多细节,你迟早会被那些细节反噬。1.1 头文件暴露带来的三大痛点编译依赖雪崩每次改动都会触发包含链再编译;模板、<vector>、第三方库头动辄解析数千行。ABI (Application Binary Interface)易碎类内成员一增删,布局立即改变;旧版动态库替换新库时直接 crash。实现束缚与保密难题头文件挂上 <zstd.h>、<grpc.h>,下游就必须安装对应 SDK;闭源算法也随之“开源”。弗洛伊德说:“痛苦源于不被满足的欲望。”在工程实践中,这种痛苦往往来自我们既想频繁迭代实现,又不想惊扰所有调用者的矛盾。1.2 ABI 稳定 vs API 兼容:先分清概念维度    ABI 稳定    API 兼容关注点    二进制级对象布局、符号名、调用约定    头文件中的函数/类签名调用方    旧版可执行文件 / 脚本绑定    源码级重新编译的客户端破坏方式    改成员顺序、类型大小、虚表    改函数参数、返回值、删接口典型需求    动态库热更新、插件系统、灰度发布    开源库版本升级、内部全量编译要诀ABI 不变:客户端 不需 重新编译,只换 .so/.dll 即可。API 不变:客户端 需要 重新编译,但头文件保持旧版即可通过。隐藏实现(无论静态对象、接口抽象、还是 pimpl)瞄准的正是“让 ABI 保持常绿;让 API 改动最小”。1.3 选型三问法——评估你到底要不要隐藏问 题    如果回答 是→ 倾向“隐藏实现”1?成员数据未来会膨胀吗?    ? 担心 ABI 破碎,就用能固定布局的手段(指针、pimpl……)2?你的库会做动态分发 / 热更新吗?    ? 需要在线换 .so 时头文件不动,必须确保 ABI 稳定3?编译依赖或商业保密是痛点吗?    ? 把重型或闭源依赖锁进 .cpp,减速编译风暴若三问皆否——项目小、永远静态链接、内部随时重编——直接按值成员最快捷;否则就要在后续章节中挑选合适的“隐藏模式”,让深渊无从反噬你。2 模式一:直接按值成员 —— “裸奔”也能跑当奥卡姆挥下他的剃刀,留下的往往是最简单也是最快的方案;在 C++ 对象设计里,按值持有成员正是这把“剃刀”下的自然产物。2.1 典型写法与最小示例// logger.h#include <vector>#include <string>class Logger {public:    void push(std::string msg) {                  // 接口直接用到 STL        buffer_.emplace_back(std::move(msg));     // buffer_ 按值持有    }    void flush();private:    std::vector<std::string> buffer_;             // ← 直接按值成员};// logger.cpp#include "logger.h"#include <fstream>void Logger::flush() {    std::ofstream ofs("out.log", std::ios::app);    for (auto& m : buffer_) ofs << m << '\n';    buffer_.clear();}编译器在 包含 logger.h 的每个翻译单元 都要解析 <vector>、<string> 及其依赖树;而 sizeof(Logger) = sizeof(std::vector<std::string>),对象布局随 std::vector ABI 漂移而改变。2.2 何时按值最合适:小项目、性能至上零指针跳转、零堆分配数据跟随对象一起分配在堆或栈上,CPU 缓存局部性最佳。头文件依赖可接受项目规模 ≤ 数万行,模板元编译时长不是瓶颈。完全静态、无热更新需求二进制一次性部署,后续升级肯定重编所有依赖。数据成员基本不会变化类设计稳定,字段数与顺序已定型。对外开源、无需隐藏实现直接暴露实现只是透明而非缺陷。正如行为心理学的“行动—奖励”模型告诉我们的:当即时性能收益足够大时,开发者往往愿意接受未来的维护成本 —— 直接按值成员正是这种即时奖励的体现。2.3 风险清单:ABI 飘动、编译依赖膨胀风险维度    产生原因    典型后果    缓解手段ABI 漂移    成员数据随业务新增/删减    动态库与旧客户端不兼容,替换即崩    用 -fvisibility=hidden + 静态链接;或改用指针/pimpl编译时间爆炸    每次改头文件,所有依赖单元重新编译    大型仓库 CI 级联重编,排队久    预编译头 (PCH)、模块化、Include-What-You-Use依赖绑死    头文件 #include 重型/商业库    下游必须装同版本依赖,交付麻烦    接口抽象或拆分头,把实现换成前置声明源码泄露    头文件暴露算法 & 第三方接口    难以闭源;安全审计复杂    版本拆层,核心算法转移 .cpp侧写原则:只要你能接受表中风险 且 避免频繁成员变动,模式一就足够;否则请准备在下一章进入“对象隔离”的世界。3 模式二:对象放到实现文件 —— 静态 / 单例隔离“真正的自由并非无所事事,而是能对纷繁事物说:我不要。”——罗素在谈论简约时如是说;把大对象搬进 .cpp 文件,正是对头文件冗余的果断拒绝。3.1 设计要点与示例// telemetry.h  —— 头文件零依赖#pragma onceclass Telemetry {public:    static void push(int value);   // 仅暴露静态接口    static void flush();};// telemetry.cpp —— 真正的实现细节#include "telemetry.h"#include <vector>#include <mutex>namespace {    // 全局静态对象,外部不可见    struct Impl {        std::vector<int>   buffer;        std::mutex         mtx;    };    Impl g_impl;  // <—— 状态全局仅一份}void Telemetry::push(int v) {    std::lock_guard lg(g_impl.mtx);    g_impl.buffer.emplace_back(v);}void Telemetry::flush() {    std::lock_guard lg(g_impl.mtx);    /* …写磁盘 / 网络… */    g_impl.buffer.clear();}头文件:只有前向声明,无任何 STL 或第三方 include。状态:g_impl 私有且驻留在实现文件,调用者看不到、对象布局不受影响。3.2 适用场景场景    说明状态全局唯一    统计、配置、日志级别等「进程级」资源无需 per-instance 拷贝    调用者只关心“用它”,不关心“持有它”编译依赖需隔离,但性能要求极高    零堆分配、零指针跳转,直接访问静态对象3.3 底层原理:静态对象生命周期 & 初始化顺序C++ 静态存储期对象遵循 “按翻译单元内部声明顺序初始化,按逆序析构”。但跨文件的初始化先后是不定序,可能引发 static initialization order fiasco。技术点    主要问题    解决方案跨 .cpp 文件依赖    A 的静态对象在 B 使用前还未初始化    用 Meyers 单例 (static 局部变量) 或 std::call_once多线程首次访问    g_impl 在两个线程同时第一次使用    C++11 之后函数内 static 初始化是 线程安全;或显式 std::once_flag析构次序敏感    g_impl 已析构但 still referenced in atexit code    把资源留到 std::atexit 注册或 不析构(进程结束 OS 回收)3.4 与其他模式对比维度    模式一:按值成员    模式二:静态/单例    模式三+四:指针或 pimpl对象布局是否受私有成员影响    ? 受影响    ? 固定    ? 固定每实例独立状态    ?    ?(共享)    ?运行期开销    最优    最优    指针 + 堆分配线程安全复杂度    低(随对象)    高(需全局锁 / onceFlag)    低或中(随实例)热更新可行性    差    一般(需确保符号不变)    最好3.5 风险与注意事项全局单例导致测试耦合建议在测试中暴露 reset/replace 钩子或用接口注入替身。并发性能瓶颈高并发下 std::mutex 可改环形缓冲 + 无锁队列,再异步落盘。析构顺序陷阱若对象析构必须释放外部资源(文件句柄、socket),可使用 std::unique_ptr + 自定义 deleter 注册到 std::atexit。荣格说过:“潜意识的东西不被意识到时,会主宰你的命运,并被你称作命运。”若你忽视静态对象的生命周期与线程安全,它迟早会在深夜的 core dump 里提醒你——命运已经到来。3.6 何时升级到下一模式?需要多实例独立状态(日志模块 per-module、IPC per-connection);计划 在线热替换、插件化;成员字段将来肯定膨胀、ABI 必须固定。当出现上述任一需求,就进入下一章,看看“抽象接口 + 智能指针”如何在不牺牲性能的前提下,实现更灵活的隔离。4 模式三:抽象接口 + 智能指针 —— 策略模式轻隔离维特根斯坦在《逻辑哲学论》中说,“世界是由事实,而非事物构成的”;用抽象接口把“事实”(行为)独立出来,你的类型就不再被具体“事物”(实现细节)绑死。4.1 设计动机与最小示例// transport.h —— 头文件只暴露抽象协议#pragma once#include <memory>#include <string_view>class ITransport {                         // 纯虚接口public:    virtual ~ITransport() = default;    virtual void send(std::string_view) = 0;    virtual void flush() = 0;};class Client {public:    explicit Client(std::unique_ptr<ITransport> t) : t_(std::move(t)) {}    void post(std::string_view msg) {        t_->send(msg);                     // ↖ 行为由策略决定    }    void sync() { t_->flush(); }private:    std::unique_ptr<ITransport> t_;        // 轻隔离:API 不暴露实现头};// tcp_transport.cpp —— 一种策略#include "transport.h"#include <asio.hpp>  // 只在实现文件依赖class TcpTransport : public ITransport {    /* …socket、buffer 等成员… */    void send(std::string_view m) override { /* … */ }    void flush() override { /* … */ }};AI写代码cpp头文件只出现 ITransport 前向声明;asio.hpp 完全隔离。切换到 ShmTransport、WebSocketTransport 只需换构造注入,不动 Client 头文件与 ABI。4.2 运行时多态的成本剖析组成    发生位置    运行期成本    关键细节vtable    程序启动时由编译器/链接器生成    常驻内存一张表    每个多态类 1 张,子类共享父类条目虚调用    每次 t_->send()    1 次间接跳转    对于 I/O-bound 场景可忽略;CPU-bound 热循环要留意unique_ptr    堆分配一块实现对象    1 次 new + 指针间接    可用自定 allocator 或 “placement new” 池化降低开销心理学的“认知负荷理论”指出:当处理器负担被转嫁到长期记忆(vtable 静态区)时,工作记忆(每次调用代价)就得到释放——这正是虚表设计思路的隐喻。4.3 生命周期与异常安全Client make_tcp_client() {    return Client(std::make_unique<TcpTransport>());}AI写代码cpp资源掌控:unique_ptr 保证 RAII;Client 移动构造/赋值 = 默认即可。异常传播:若 new TcpTransport 抛异常,对象构造失败,调用方拿不到 Client,无资源泄漏。销毁顺序:Client 析构顺序 = Client → TcpTransport → socket;避免静态对象“先析构先死”问题。4.4 模板与虚函数:编译期 vs 运行期的权衡维度    模板策略 template<class TTransport>    虚函数策略 ITransport编译期开销    每用一种 TTransport 生成一套代码    代码一次生成,所有策略共享运行期开销    0 间接跳转,内联优化极佳    1 次虚表间接;不易被内联二进制大小    随策略数量线性增长    固定ABI 稳定性    每次换模板参数需重新编译客户端    只要接口不变可热替换 .so隐藏依赖    需要在头文件 #include 具体实现    头文件只需前向声明实战指北CPU-bound、策略极少 → 模板更快;I/O-bound、策略易扩展 → 虚函数 + 智能指针最灵活。4.5 典型适用场景典型库    抽象点    额外收益日志库(文件 / UDP / ringbuffer)    ILogSink    动态切换后端,单测可注入 MockSink序列化库(JSON / Protobuf / FlatBuffers)    ISerializer    线上灰度迁移格式,无痛替换网络传输层(TCP / QUIC / TLS)    ITransport    改协议不动业务代码4.6 何时升级到“轻量 pimpl”或“完整 pimpl”?升级信号    原因同一策略内部私有成员会不断膨胀    虚函数接口虽稳,但对象大小仍随成员变;用 pimpl 固定布局热更新要求极高,甚至连 vtable 位置都要稳    pimpl 把虚函数也包进 Impl,客户端看到的只是一个指针要隐藏第三方闭源库符号    把实现挪到 Impl,对外不暴露任何符号下一章将展示如何通过“轻量 pimpl”一步把对象布局 彻底 固定为一个指针——既保留多态灵活性,又让 ABI 坚不可摧。5 模式四:轻量 pimpl —— 指针成员直接持有内部类“形式即自由的容器。”黑格尔的这句话在软件架构里尤显贴切:只要把内部形态塞进一个不变的容器(指针),外部世界就再也不会被它束缚。5.1 基本形态与精简示例// widget.h —— 头文件极简#pragma once#include <memory>class Widget {public:    Widget();                               // 构造    ~Widget();                              // 析构(在 .cpp = default)    void draw();                            // 对外接口    void resize(int w, int h);private:    class Impl;                             // 前向声明    std::unique_ptr<Impl> p_;               // ← 轻量 pimpl:仅一指针}AI写代码cpp// widget.cpp —— 所有依赖锁在实现文件#include "widget.h"#include <SDL.h>#include <vector>class Widget::Impl {public:    SDL_Surface* surface = nullptr;    std::vector<unsigned char> framebuffer;    int width{0}, height{0};    void draw_core();    void resize_core(int w, int h);};Widget::Widget() : p_(std::make_unique<Impl>()) {}Widget::~Widget() = default;void Widget::draw()              { p_->draw_core();   }void Widget::resize(int w, int h){ p_->resize_core(w, h); }头文件暴露量:<memory> + 指针大小;SDL.h 和 std::vector 完全隐藏。ABI:sizeof(Widget) 永远等于一个指针的大小,无论 Impl 如何膨胀。5.2 轻量 pimpl 相比上一模式的进阶点维度    策略接口 + 智能指针(上一章)    轻量 pimpl对象布局固定    ?    ?多后端切换    通过不同派生类    需在 Impl 内切换隐藏第三方头    部分(接口本身仍暴露纯虚类)    完全隐藏指针/间接层级    2(外层+虚表)    1(外层指针)CPU 内联机会    受虚调用限制    非虚 → 编译器可内联5.3 底层原理:不完整类型与“单一不变量”不完整类型规则头文件只出现 class Impl;,编译器在看到完整定义前不允许:sizeof(Impl)成员访问 p_->x因此所有实现细节都被推迟到 .cpp,让头文件彻底免疫变化。单一不变量对象布局 = 指针大小(32 位系统 4 B;64 位 8 B)。社会心理学中的“锚定效应”暗示:一旦外部依赖对某个数值建立了预期,它就会固化为评估基准。在 ABI 里,这个“锚”就是那枚指针——永远不变。异常安全构造:std::make_unique<Impl>() 要么成功、要么抛;Widget 保证不留悬空指针。析构:在 .cpp 里 = default,此时 Impl 已完整,编译通过。5.4 性能影响与优化路径成本来源    开销级别    可选优化    适用场景一次堆分配    中(16 B 对大对象可忽略)    Small-Buffer Optimization (SBO):在 Widget 内嵌 std::aligned_storage<MAX>,小于阈值时不分配    高频创建小对象一次指针间接    较低(一次 L1 命中≈1 ns)    不需要    I/O-bound 或重计算任务代码大小    Widget 方法难以内联(需查看完整 Impl)    将短函数声明为 inline 并放 .cpp 尾部(仍不暴露头文件)    对微基准极端敏感的库5.5 常见陷阱与安全守则陷阱    根因    应对策略在头文件写 ~Widget() = default;    此时 Impl 不完整 → 链接失败    把析构放到 .cpp 并 = default拷贝构造遗漏深拷贝    unique_ptr 禁用拷贝,编译报错    明确 Widget(const Widget&) = delete; 或自写深拷贝不必要的虚函数    Impl 已经隐藏,可直接用非虚成员    仅当需要派生多种 Impl 时再用虚表循环依赖    .cpp 中 #include 互相引用    前向声明 + 头文件剥离,或拆分文件5.6 适用场景与决策指北触发条件    轻量 pimpl 是否合适私有成员将频繁扩展    ?库需做动态链接,对外闭源    ?对象创建次数极多,且对象极小    需评估 → SBO 或考虑仍按值成员需切换多策略后端    虚接口 + 指针可能更弹性若把工程维护比作登山:抽象接口 给了你灵活路线,轻量 pimpl 则帮你把帐篷和粮食都缩进一个背包——再崎岖的后续迭代,也无需重新规划补给点。在下一章,我们将走向“完全 pimpl”——当库成为横跨多进程、多架构的大型二进制 SDK 时,如何构筑一座真正“随时可换核心、外壳不动摇”的护城河。6 模式五:完整 pimpl 框架 —— 大型二进制 SDK 的护城河“建筑的第一要义是经得起时间。”——勒·柯布西耶提醒建筑师如此,在软件世界里,完整 pimpl 正是一种让 C++ 库在多年跨版本演进中仍能屹立的钢筋混凝土结构。6.1 为什么“轻量”已不够多语言绑定:Python 或 Rust 插件只能看到稳定的 C 符号;即使虚表布局改变也可能崩溃。跨架构发行:同一 .so 运行在 x86_64 与 ARM64,内部成员对齐差异要求对象头绝对恒定。大规模灰度 OTA:数万设备在线替换 .so;任何字段漂移都会造成批量 SIGSEGV。完整 pimpl 通过 “双层外壳” 把一切可变因素(字段、虚表、依赖库、内联函数)统统移走,只留下接口符号与一个固定宽度的指针。6.2 架构拆分与构建细节层级    文件    可见性    说明API 层    widget.h    __attribute__((visibility("default")))或 __declspec(dllexport)    只导出构造 / 析构 / 功能函数桥接层    widget.cpp    默认隐藏    每个接口函数内部仅做 p_->func() 转发实现层    widget_impl.*    全隐藏    含全部成员、第三方头、虚函数表ELF 系统可在 linker 脚本写:# widget.map{  global:    _ZN6Widget*;      # 仅导出 Widget::* 符号  local:    *;                # 其余一律隐藏};6.3 符号与可见性:不同平台实战指北平台    推荐手段    备注Linux / Android    -fvisibility=hidden + version script    GCC & Clang 通用Windows    .def 文件或 __declspec(dllexport/import)    链接器自动生成 .lib 导入表macOS    -fvisibility=hidden + -exported_symbols_list    支持弱符号混链认知心理学告诉我们:隐藏无关信息能显著降低理解负荷。同理,隐藏符号让调试堆栈与 nm 输出更聚焦。6.4 ABI 版本管理三板斧技术    作用    使用要点Inline Namespace + SONAME    把所有导出符号包裹在 inline namespace v1;升级破坏性接口时切 v2    旧程序链接旧 SONAME,避免符号撕裂Symbol Versioning (GNU)    同名函数多版本共存    __asm__(".symver newfun,oldfun@VER_2");Opaque Handle + C API    最保险:向外只暴露 C 函数 + void* 句柄    多语言绑定/插件首选6.5 性能与内存:完整 pimpl 还能再榨吗?优化项    技法    典型收益    复杂度SBO (Small Buffer Opt)    std::aligned_storage<64> 作为内嵌缓存;大于阈值再 new    省一次堆分配(99% 小对象)    中Arena 分配    自定义 operator new 批量分配 Impl    降低 malloc 碎片    中指针标记/索引表    8 字节指针换 4 字节索引 + 段地址    省内存、提升缓存命中    高Link-time ODR folding    -flto + -fmerge-all-constants    减少重复模板实例    低6.6 完整 pimpl 升级演示:三步热替换不崩溃旧版 v1class Impl { int a; };AI写代码cpp1客户端运行:Widget w;新增字段class Impl { int a; double b; std::vector<int> cache; };AI写代码cpp1只改 widget_impl.cpp,接口头不变 → 编译生成 libwidget.so v1.1。热替换mv libwidget.so.1.1 /usr/lib/旧客户端不停机 dlopen 新的 SONAME → 正常调用,因 sizeof(Widget) 依旧是 1 指针。6.7 从旧类迁移到完整 pimpl 的 checklist步骤    关键点1 抽取内部数据    把所有私有成员移动到 Impl2 转移 include    仅 widget_impl.cpp 保留重型头;头文件只留前向声明3 导出宏    写 WIDGET_API 宏统一 __declspec(dllexport) / __attribute__4 更新构造/析构    在 .cpp 里 Widget::~Widget() = default;5 审查拷贝/移动    明确 delete 或实现深拷贝6 增加 CI ABI 检查    用 abi-compliance-checker 生成报告,防止误改接口收束当你的库需要跨平台、跨语言、跨年份地“活”下去时,完整 pimpl 就是那道护城河,它让实现可以日新月异,而外部世界永远只看到同一块坚固的城墙。7 结语:如何快速选择最适合你的模式“选择本身即是一种设计。” —— 赫伯特·西蒙强调决策是有限理性下的优化。在 C++ 隐藏实现策略中,没有绝对完美的答案,只有最适配的局部最优。7.1 五秒决策表问题    是 → 采取的模式    否 → 下一个问题1. 成员字段未来肯定扩张?    模式四或五(pimpl)    → 22. 库需热更新 / 多语言绑定?    模式四或五    → 33. 每实例都要独立状态?    模式二(静态对象 ?)模式三(接口+指针)或四    → 44. 性能极端敏感且对象小、频建?    模式一或二 + Small Buffer    → 55. 项目规模 ≤?数万行,能全量重编?    模式一(按值成员)    ? 结束认知心理学的“时间压制决策”显示:在时间有限时,人们更倾向使用启发式——这张五秒表就是供你“快速启发”的工具。7.2 组合拳:多模式混用的工程实践实际工程往往 “一库多模式”:核心算法模块 —— 模式五(完整 pimpl)需要长寿命、闭源、跨语言;依赖大量第三方库(AI、压缩、加密)。业务胶水层 —— 模式三(接口 + 智能指针)快速换 mock / stub 进行单测;领域逻辑多变、策略众多。轻量工具类 —— 模式一(按值成员)必须零堆分配、零跳转;只依赖 STL,变化频率低。这种“分区施策”既保证了 性能关键路径 的极致效率,也让 易变模块 拥有最大演进空间。7.3 深入阅读 & 实战工具资源类别    推荐    摘要书籍    《Large Scale C++ Volume I》    专章详细讨论 ABI、组件边界与隐藏技术工具    abi-compliance-checker    自动 diff 两版库的符号与布局差异文章    “Non-virtual Interface + pimpl” (Herb Sutter)    解析 NVI 与 pimpl 结合的可测试性CI 插件    GitHub Action cpp-pimpl-guard    提交时检测头文件对外可见性的变化7.4 尾声:让“可变”与“稳定”和谐共生“恒常才是变化的另一种形态。”——叔本华提醒我们,世界的本质是在变化中寻找不变。在 C++ 工程里,那份“不变”正是:外部契约稳定,内部随需而动。无论你最终选用哪种隐藏模式,只要记住——先明白边界,再谈实现——就能在未来的版本洪流里稳坐中流砥柱。结语在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/qq_21438461/article/details/148204325
  • [技术干货] C++指针全面指南
    指针是C++中最强大但也最复杂的特性之一。它直接操作内存地址,为程序员提供了极大的灵活性和控制力。以下是C++指针的详细使用方法。一、指针基础1. 指针声明与初始化int var = 10; // 普通整型变量int *ptr = &var; // 指针变量,存储var的地址cout << "变量值: " << var << endl; // 输出: 10cout << "指针地址: " << ptr << endl; // 输出: 0x7ffd2a3a4b24 (示例地址)cout << "指针解引用: " << *ptr << endl; // 输出: 102. 指针的基本操作int x = 5, y = 10;int *p1 = &x;int *p2 = &y;*p1 = 20; // 通过指针修改x的值cout << x << endl; // 输出: 20p1 = p2; // 指针赋值,现在p1也指向ycout << *p1 << endl; // 输出: 10二、指针与数组1. 数组名的指针特性int arr[5] = {1, 2, 3, 4, 5};int *p = arr; // 数组名就是首元素地址cout << *p << endl; // 输出: 1cout << *(p + 2) << endl; // 输出: 3 (指针算术)cout << p[3] << endl; // 输出: 4 (指针下标访问)2. 指针遍历数组for (int *ptr = arr; ptr < arr + 5; ptr++) { cout << *ptr << " ";}// 输出: 1 2 3 4 5三、指针与函数1. 指针作为函数参数void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp;}int main() { int x = 5, y = 10; swap(&x, &y); cout << x << " " << y << endl; // 输出: 10 5 return 0;}2. 返回指针的函数int* findMax(int *arr, int size) { if (size <= 0) return nullptr; int *max = arr; for (int i = 1; i < size; i++) { if (arr[i] > *max) { max = &arr[i]; } } return max;}四、动态内存管理1. new和delete操作符// 分配单个intint *p = new int(42); cout << *p << endl; // 输出: 42delete p;// 分配数组int *arr = new int[5]{1, 2, 3, 4, 5};delete[] arr; // 注意使用delete[]释放数组2. 智能指针(C++11起)#include <memory>// unique_ptr (独占所有权)std::unique_ptr<int> uptr(new int(10));cout << *uptr << endl;// shared_ptr (共享所有权)std::shared_ptr<int> sptr1 = std::make_shared<int>(20);std::shared_ptr<int> sptr2 = sptr1; // 共享所有权五、多级指针int var = 100;int *ptr = &var;int **pptr = &ptr; // 指向指针的指针cout << **pptr << endl; // 输出: 100六、指针与const1. 指向常量的指针const int value = 5;const int *ptr = &value; // 不能通过ptr修改value// *ptr = 10; // 错误!2. 常量指针int x = 10;int *const ptr = &x; // ptr的指向不能改变*ptr = 20; // 可以修改指向的值// ptr = &y; // 错误!3. 指向常量的常量指针const int y = 30;const int *const ptr = &y; // 既不能改指向也不能改值七、函数指针1. 基本函数指针#include <iostream>using namespace std;int add(int a, int b) { return a + b; }int subtract(int a, int b) { return a - b; }int main() { // 声明函数指针 int (*operation)(int, int); operation = add; cout << operation(5, 3) << endl; // 输出: 8 operation = subtract; cout << operation(5, 3) << endl; // 输出: 2 return 0;}2. 函数指针作为参数void calculate(int x, int y, int (*op)(int, int)) { cout << op(x, y) << endl;}int main() { calculate(10, 5, add); // 输出: 15 calculate(10, 5, subtract); // 输出: 5 return 0;}八、void指针void print(void *ptr, char type) { switch(type) { case 'i': cout << *(int*)ptr << endl; break; case 'f': cout << *(float*)ptr << endl; break; case 'c': cout << *(char*)ptr << endl; break; }}int main() { int a = 10; float b = 3.14f; char c = 'X'; print(&a, 'i'); print(&b, 'f'); print(&c, 'c'); return 0;}九、指针安全注意事项空指针检查:解引用前检查指针是否为nullptrif (ptr != nullptr) { *ptr = 10;}野指针问题:指针释放后应立即置空delete ptr;ptr = nullptr;内存泄漏:确保每个new都有对应的delete数组越界:指针算术不要超出分配的内存范围类型安全:避免不安全的类型转换
  • [区域复赛赛题问题] 复赛正式赛较复赛练习赛是否存在更改点
    听说别的赛区群里说了复赛现场赛会有新的变更点,但是本赛区群没收到通知,想确认一下这个信息。
  • [区域复赛赛题问题] 系统报错信息解释较粗略
    如图,在官方帖子找到了该解释,但是这个解释“对象块未加载的“具体含义是什么呢?
  • [区域复赛赛题问题] 采样比例问题
    根据参数注意到,线下对线上数据的采样率接近2/3。请问复赛正式赛的采样率是否等于练习赛?这个规模的采样率,与更低规模的采样率,使用的策略可能大为不同。是否下发数据的采样率偏高?