用Qt实现一个桌面弹幕程序(二)--实现一个弹幕②



  • 现在就一起来愉快地编写一个弹幕类吧

    杰洛君 以单身多年的手速很快地 右键 点击项目 新建 了一个Danmu.h 和 Danmu.cpp 文件<( ̄︶ ̄)>

    熟悉C++的你,一定知道接下来就是编写弹幕类的时候了!

    这个弹幕类是重载QLabel的,所以它的类至少应该是这个样子:

    #ifndef DANMU_H
    #define DANMU_H
    
    #include <QLabel>
    
    class Danmu : public QLabel{
    
        Q_OBJECT
    
      public:
    
      Danmu(QWidget * parent);       //构造函数
    
      ~Danmu();     //析构函数
    
      public slots:
    
      private:
    };
    
    #endif // DANMU_H
    

    按照上一篇文章中我们提到了弹幕的好几种属性:

    • 自己的文字内容
    • 自己的字体
    • 自己的颜色
    • 自己的大小
    • 自己的飞行快慢
    • 自己的透明度

    于是在这个类中应该有对应的私有属性:

    • QString 类型的 DText属性
    • QFont 类型的danmuFont属性
    • QColor 类型的qcolor属性
    • int 类型的宽度和高度
    • int 类型的 runTime 持续时间属性
    • double 类型的Transparency属性

    当然,杰洛君后来又想到了一些属性,于是也添加进来:

    • int 类型的 type属性-- --用于区分横飞的弹幕与置顶以及位于底部的弹幕
    • int 类型的PosX与PosY属性-- --用于记录弹幕的初始位置~

    于是乎你的弹幕类中的私有属性至少应该是这样子的:

          int PosX;
          int PosY;
          QString DText;
          QString color;  //这个color属性保存英文颜色字符串
          QColor qcolor;
          int type;
          QFont danmuFont;
          int DHeight;
          double Transparency;
          int runTime;
    

    由于写过一段时间的JavaEE,杰洛君本能地为这些属性写了响应的Get和Set方法<( ̄ˇ ̄)/

    什么是Get和Set方法?
    就是为每个私有成员设置公有的设置和获取方法
    例如:
    void setColor(QString color);
    QString getColor();

    当然你也写成用c++风格的get方法 QString Color();

    小A : 类里的方法都是可以直接访问私有成员的呀,这有什么必要吗?

    杰洛君: 额额,确实可以直接访问,写Get和Set方法是个人编程习惯啦= ̄ω ̄=

    有了这么多私有变量,自然构造函数就要更加复杂啦,但是看到PosX 和 PosY 属性的时候,杰洛君呆住了。。。{{{(>_<)}}} 弹幕的起点怎么确定呀?

    如何确定弹幕的起始位置?

    弹幕是从哪里开始飞的呢?

    小A:简单,从屏幕的最右边开始呗~

    确实,弹幕从屏幕的外部飞入,那么这个具体位置该怎么设置呢,设置一个定值?比如分辨率为800720 那就设置 900吧,这样看起来就是从屏幕外侧飞进来了,那如果是1366768呢?900不就不够了吧。。。

    于是乎获取屏幕的分辨率成为我们要解决的一个问题。

    幸好Qt已经为我们做好了这些工作:

    在Danmu包含的头文件中加入 QRect 与 QDesktopWidget吧~

    QDesktopWidget* desktopWidget;  //获取桌面设备  
    QRect screenRect;
    desktopWidget = QApplication::desktop(); //获取桌面Widget
    screenRect = desktopWidget->screenGeometry(); //获取桌面大小的矩形
    

    这样我们就获得了桌面大小的这样的一个矩形,可以获得它的长和宽,利用这点就可以确定出起始位置的设置方法,保证不同电脑上弹幕的起始位置都在屏幕外。

    于是,杰洛君在Danmu的私有成员中加入了一个QRect 类型的 screenRect属性,这下你知道这个矩形是做什么用的了吧~

    如何确定弹幕的高度和宽度?

    弹幕只有一行内容所以它的高度是单个字的高度,这个几乎可以说是固定的,但是它的长度就不是固定的了,随用户输入字数多少而变化。

    这时如果能够根据内容的多少知道我们的字体会有多长该有多好呀~

    好在Qt 也为我们做好这个工作

    利用QFontMetrics类中的 int QFontMetrics::width(const QString & text, int len = -1) const方法 可以获得一段字符串的像素长度。

    具体用法:

    QFontMetrics metrics(this->getQFont()); //传入一个QFont字体
    metrics.height(); //得到字体高度
    metrics.width(DText); //获得DText字符串的像素宽度
    this->setFixedHeight(metrics.height()+5); //弹幕设置固定的高度
    this->setFixedWidth(metrics.width(DText)+4); //弹幕设置固定的宽度

    如果你觉得不保险,可以在这基础上再加上一些整数,因为背景是透明的,所以只要字体显示完整就可以了

    如何绘制弹幕内容?

    上篇文章中我们用到了 QPalette 改变弹幕的颜色,通过改变字体去改变弹幕的大小。

    但是这个效果其实并不好,没有描边,看起来就是不像弹幕。

    于是,杰洛君决定重载 弹幕的 绘制函数 重绘出我们需要的效果来。

    首先在 Danmu类中添加 :

    protected:
          void paintEvent(QPaintEvent *);       //弹幕的绘制函数
    

    这个是重载了QLabel的绘制函数,在接收到绘制时间信号时会调用这个方法。

    下面是它的实现:

    void Danmu::paintEvent(QPaintEvent *){  //弹幕绘制函数
            QPainter painter(this);     //以弹幕窗口为画布
            painter.save();
            QFontMetrics metrics(this->getQFont());     //获取弹幕字体
            QPainterPath path;      //描绘路径用
            QPen pen(QColor(0, 0, 0, 230));       //自定义画笔的样式,让文字周围有边框
            painter.setRenderHint(QPainter::Antialiasing);  //反走样
            int penwidth = 4;  //设置描边宽度
            pen.setWidth(penwidth);
            int len = metrics.width(DText);
            int w = this->width();
            int px = (len - w) / 2;
            if(px < 0)
            {
                px = -px;
            }
            int py = (height() - metrics.height()) / 2 + metrics.ascent();
            if(py < 0)
            {
                py = -py;
            }
            path.addText(px+2,py+2,this->getQFont(),DText);     //画字体轮廓
            painter.strokePath(path, pen);  //描边
            painter.drawPath(path);  //画路径
            painter.fillPath(path, QBrush(this->getQColor()));      //用画刷填充
            painter.restore();
    }
    

    这里用到了QPainter类,这个类很重要,建议多看看文档,熟悉熟悉它的用法。有机会杰洛君会找几个例子好好描述它。

    这里比较难理解的是py的计算,里面用到了 metrics.ascent(),这个是什么呢?杰洛君通过F1的帮忙知道了这个是什么。

    int QFontMetrics::ascent() const
    The ascent of a font is the distance from the baseline to the highest position characters extend to.

    ascent 就是字体的最高位置到字体基线这么一段距离。

    所以py的计算就是 弹幕窗体的高度 - 字体的高度 得到留白的大小,这个留白大小平均分之后 再加上 基线以上的字体高度,得到真正绘制字体的位置。

    小C:你这么说,我怎么懂呀。。。

    确实用杰洛君拙劣的文字描述很难懂,怎么办呢?下面就看看文档的图示吧~

    0_1456660335692_pic13.png

    从这张图中我们可以清晰地看到基线并不是文字的最下方。这么看是不是好懂些了。

    如何让弹幕飞行?

    好了,杰洛君知道屏幕左上角为(0,0)位置,利用上述的屏幕矩形,把弹幕的起始横向位置设置为 矩形宽度加上 200 或者 随机某个整数,这样弹幕的横向位置就看起来比较随机了。

    而纵向设置为 屏幕宽度 以内的随机整数,这样竖直方向的弹幕看起来也就比较随机了。

    (p.s.其实范围应该是屏幕高度减去字体高度,小伙伴们可以想一想这是为什么?)

    如何生成随机数?
    就像c++中有srand() 和 rand()函数一样,Qt 有 qsrand() 和 qrand() 函数
    具体用法:
    头文件加上QTime
    qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
    int y = qrand()%rect.height(); //得到矩形高度范围内的一个随机数

    问题来了,弹幕如何才能做到飞行?

    方案 1:

    使用Qt 的 QTimer定时器,每隔20ms 调用一个槽函数
    这个槽函数做弹幕X方向坐标递减,然后弹幕 move到相应的位置
    重绘
    判断是否飞离屏幕,若飞离则析构弹幕。

    对于这个方案,确实可行,效果也不错,但是有一个问题就是,弹幕一旦多起来就会看起来很卡。

    (不要问杰洛君是怎么知道的ヽ(≧□≦)ノ)

    方案 2:

    弹幕移动就是一个动画,那就用Qt 的QPropertyAnimation吧~
    确定起始位置,终点位置和持续时间
    启动动画
    动画结束时就析构弹幕

    恩恩,方案二看起来更好呀,那就用它吧~(^__^)

    QPropertyAnimation * anim2=new QPropertyAnimation(this, "pos");
    anim2->setStartValue(QPoint(this->getPosX(),this->getPosY()));
    anim2->setEndValue(QPoint(-(this->width()), this->getPosY()));
    anim2->setDuration(this->getRunTime());
    anim2->setEasingCurve(QEasingCurve::Linear);
    this->setWindowOpacity(this->getTransparency());
    this->show();
    anim2->start();
           
    //利用Qt的信号和槽函数响应机制,让动画销毁时 弹幕 析构       
    
    connect(anim2,SIGNAL(finished()),this,SLOT(deleteLater()));
    

    上面这段代码应该放在哪里呢?杰洛君把它放在了Danmu的构造函数中。

    或许你会觉得anim2这个指针只有new 没有 delete 存在内存泄漏。

    不过Qt 拥有一个机制,那就是如果该对象为QObject的派生类,销毁父对象时会销毁它的子对象,而我们的动画对象是有声明父对象的,所以析构弹幕时动画也会销毁。

    千辛万苦终于走到这步

    好了,细节设计好了,就设计一下构造函数吧~

    Danmu(
    QWidget * parent,
    QString text,
    QString color,
    int type,
    QRect rect,
    QFont danmuFont = QFont("SimHei",20,100),
    double Transparency = 1.00,
    int runTime=15000
    );       //构造函数,常用
    

    在构造函数中,带有默认参数,这样可以减轻很多工作量。

    ###具体Danmu.h头文件实现:

    #ifndef DANMU_H
    #define DANMU_H
    
    #include <QLabel>
    #include <QRect>
    #include <QColor>
    #include <QDebug>
    #include <QTextCharFormat>
    #include <QPainter>
    #include <iostream>
    #include <QTime>
    #include <QPropertyAnimation>
    #include <QParallelAnimationGroup>
    class Danmu : public QLabel{
    
        Q_OBJECT
    
      public:
    
          Danmu(QWidget * parent,QString text,QString color,int type,QRect rect,QFont danmuFont = QFont("SimHei",20,100),double Transparency = 1.00,int runTime=15000);       //构造函数,常用
    
          ~Danmu();     //析构函数
    
          //一些成员变量的Get方法与Set方法
          //全部放上来会显得累赘所以略去
    
      protected:
          void paintEvent(QPaintEvent *);       //重点,弹幕的绘制函数
    
      private:
          int PosX;
          int PosY;
          QString DText;
          QString color;
          QColor qcolor;
          int type;
          QFont danmuFont;
          int DHeight;
          double Transparency;
          QRect screenrect;
          QPropertyAnimation *anim2;
          int runTime;
    };
    
    #endif // DANMU_H
    

    ###具体Danmu.cpp文件实现:

    #include "Danmu.h"
    
    Danmu::Danmu(QWidget * parent,QString text,QString color,int type,QRect rect,QFont danmuFont,double Transparency,int runTime):QLabel(parent){
        DText = text;
        //this->setText(text);        //设置内容
        this->setColor(color);      //设置内容
        this->setType(type);        //设置类型
        this->setQFont(danmuFont);      //弹幕字体
        this->setTransparency(Transparency);        //弹幕透明度
        this->setRunTime(runTime);
        this->setScreenRect(rect);
        QFontMetrics metrics(this->getQFont());
        QPalette palll=QPalette();
        QString DColor = this->getColor();
        anim2 = NULL;
        //颜色字符串转化为特定的颜色
        if(DColor == "White"){
            palll.setColor(QPalette::WindowText,QColor(255,255,246,255));
            this->setQColor(QColor(255,255,246,255));
        }else if(DColor =="Red"){
            palll.setColor(QPalette::WindowText,QColor(231,0,18,255));
            this->setQColor(QColor(231,0,18,255));
        }else if(DColor =="Yellow"){
            palll.setColor(QPalette::WindowText,QColor(254,241,2,255));
            this->setQColor(QColor(254,241,2,255));
        }else if(DColor =="Brown"){
            palll.setColor(QPalette::WindowText,QColor(149,119,57,255));
            this->setQColor(QColor(149,119,57,255));
        }else{
            palll.setColor(QPalette::WindowText,QColor(255,255,246,255));
            this->setQColor(QColor(255,255,246,255));
        }
        this->setPalette(palll);        //设置调色盘
        this->setFixedHeight(metrics.height()+5);
        this->setFixedWidth(metrics.width(DText)+4);
        int yy = qrand()%rect.height(); //临时的位置,还要防止高度在屏幕下导致显示不完整
        int y = yy<(rect.height()-metrics.height()-5)?(yy):(rect.height()-metrics.height()-5);
        int xx = rect.width()+qrand()%500;
        this->move(xx,y);
        this->setPosX(xx);//设置弹幕水平的位置
        this->setPosY(y);       //设置弹幕垂直位置
    
        this->setWindowFlags(Qt::FramelessWindowHint|Qt::Tool|Qt::WindowStaysOnTopHint);    //设置弹幕为无窗口无工具栏且呆在窗口顶端
    
        this->setAttribute(Qt::WA_TranslucentBackground, true);
        this->setFocusPolicy(Qt::NoFocus);
        this->hide();
        anim2=new QPropertyAnimation(this, "pos");
        anim2->setDuration(this->getRunTime());
        anim2->setStartValue(QPoint(this->getPosX(),this->getPosY()));
        anim2->setEndValue(QPoint(-(this->width()), this->getPosY()));
        anim2->setEasingCurve(QEasingCurve::Linear);
        this->setWindowOpacity(this->getTransparency());
        this->show();
        this->repaint();
        anim2->start();
            //connect(anim2,SIGNAL(finished()),this,SLOT(deleteLater()));
    }
    
    void Danmu::paintEvent(QPaintEvent *){  //弹幕绘制函数
            QPainter painter(this);     //以弹幕窗口为画布
            painter.save();
            QFontMetrics metrics(this->getQFont());     //获取弹幕字体
            QPainterPath path;      //描绘路径用
            QPen pen(QColor(0, 0, 0, 230));       //自定义画笔的样式,让文字周围有边框
            painter.setRenderHint(QPainter::Antialiasing);
            int penwidth = 4;
            pen.setWidth(penwidth);
            int len = metrics.width(DText);
            int w = this->width();
            int px = (len - w) / 2;
            if(px < 0)
            {
                px = -px;
            }
            int py = (height() - metrics.height()) / 2 + metrics.ascent();
            if(py < 0)
            {
                py = -py;
            }
            path.addText(px+2,py+2,this->getQFont(),DText);     //画字体轮廓
            painter.strokePath(path, pen);
            painter.drawPath(path);
            painter.fillPath(path, QBrush(this->getQColor()));      //用画刷填充
            painter.restore();
    }
    
    
    
    Danmu::~Danmu(){
        qDebug()<<"弹幕被析构"<<endl;
    }
    
    //下面是get和set方法的实现
    //同样全部放上无法凸显重点故略去
    
    
    

    上面就是具体代码了,这段代码有很多可以优化的地方。

    其中那一大段if else 想必会被很多人唾弃吧。

    不过还请体谅体谅杰洛君,这个可怜的娃为了实现这么小一个功能已经耗尽脑筋,放弃思考了〒▽〒。

    有时间会采用enum与switch-case做替换的。

    至于颜色的取值,大家可以找一些简单的取色软件获取颜色的16位值或者RGB值

    关于最终程序中用到的颜色,这里给个参考:

    • "White" QColor(255,255,246,255));
    • "Red" QColor(231,0,18,255));
    • "Yellow" QColor(254,241,2,255));
    • "Green" QColor(0,152,67,255));
    • "Blue" QColor(0,160,234,255));
    • "Pink" QColor(226,2,127,255));
    • "Grass" QColor(144,195,32,255));
    • "DBlue" QColor(0,46,114,255));
    • "DYellow" QColor(240,171,42,255));
    • "DPurple" QColor(104,58,123,255));
    • "LBlue" QColor(129,193,205,255));
    • "Brown" QColor(149,119,57,255));

    这些颜色的值都取自B站手机客户端上的可选颜色~

    在main函数中试一试吧

    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        MainWindow w;
        qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
        QDesktopWidget* desktopWidget;                                    //获取桌面设备
        QRect screenRect;
        desktopWidget = QApplication::desktop();                   //获取桌面设备
        screenRect = desktopWidget->screenGeometry();              //获取桌面大小的矩形
        Danmu danmu(NULL,"Hello World","Red",1,screenRect);
        w.show();
    
        return a.exec();
    }
    

    不出意外你就会看到一个弹幕飞过了,啊啊啊,终于成功了!!!ค(TㅅT)

    杰洛君已经哭成狗狗了。。。

    你会看到那个动画那段connect代码被注释了。

    这是因为启用这段代码,动画结束后就会析构弹幕,但是我们的弹幕不是在堆上新建的对象,所以delete一个栈上的对象 退出程序时,这个弹幕的析构函数将被再次调用,一个对象调用了两次析构函数,程序当然就瞬间爆炸了。

    所以注释了这段代码,这个不是大问题,只用new的方式建立Danmu对象就可以启用这段代码了。

    后续

    实现了一个弹幕了,但是后续问题接踵而至。

    弹幕失去控制了!!!

    我想隐藏它该怎么办?

    弹幕好污好羞耻又该如何监控。

    放心,后续都会讲到的,今天就到这里吧~O(∩_∩)O哈哈~

    或许你会觉得杰洛君废话很多,不过这些废话姑且算是自己的思考过程进去,希望你们会喜欢~。

    题外话
    本来博文早上应该就可以发的了,不过我认识的一位会画画的女神大大上午指导我画画去了~

    0_1456661850900_draw.jpg

    我只能表示:

    手残党上女神的课一上午,胜过画室苦画三年呀!



  • @杰洛飞 说:

    弹幕是从哪里开始飞的呢?

    小A:简单,从屏幕的最左边开始呗~

    不对啊。应该从屏幕的最右边飞啊。



  • 最后这个线稿画得很棒!



  • @jiangcaiyang 对对对,我改改



  • @jiangcaiyang 谢谢☺ 会继续把它练好~



  • 啪啪啪,写的不错。弹幕解析那一块,能不能直接解析b站的弹幕文件?最后线稿不错。



  • @qyvlik 这个没有做哦,不过可以作为这个程序的一个拓展方向,有机会会试试,嘿嘿😝



  • @杰洛飞 好好好。



  • 最后的手绘是本文一大亮点 -。-



  • 不是很明白,什么叫做“弹幕类”?它的目的是什么?



  • @stlcours 弹幕就是 类似 文字跑马灯效果,也就是通常情况下让文字从右往左进行移动。这个效果呢,实现起来很简单,也可以用Qt Quick以及Qt Widgets实现。



  • 用Label性能会比较悲催
    建议从qtquick的Text做起可能比较好



  • @MidoriYakumo

    嘿嘿嘿, DarkFlameMaster 这个是用 QML 实现的弹幕播放器,不过在弹幕轨迹分配和弹幕播放器中的时间轴算法找不到其他可以参考的。有什么好的相关文章推荐吗?



  • 没研究过诶,你那个有什么问题么?


Log in to reply
 

走马观花

最近的回复

  • boost.asio是一个很棒的网络库,这回儿我也开始系统地学习起来了。想想当年接触boost,也有八年多了。这次开始接触boost,觉得既熟悉又陌生。熟悉的是小写字母+下划线的命名方式、晦涩的模板、很慢的编译速度以及较大的程序体积,陌生的是asio的各种概念:io服务、接收器、套接字等等:我之前对网络编程不是非常了解。

    于是根据我的理解,参考《Boost.Asio C++网络编程》实现了这样一个简单的客户端和服务端通信的例子,例子非常简单,还不完善,但是幸运的是,可以在本机上互通了。
    下面是客户端的代码:

    #include <iostream> #include <boost/asio.hpp> #include <boost/proto/detail/ignore_unused.hpp> using namespace std; using namespace boost::asio; using namespace boost::system; using namespace boost::proto::detail;// 提供ignore_unused方法 void writeHandler( const boost::system::error_code& ec, size_t bytesTransferred ) { if ( ec ) { cout << "Write data error, code: " << ec.value( ) << "transferred: " << bytesTransferred << endl; } else { cout << "OK! " << bytesTransferred << "bytes written. " << endl; } } int main(int argc, char *argv[]) { ignore_unused( argc ); ignore_unused( argv ); io_service service; ip::tcp::socket sock( service ); ip::tcp::endpoint ep( ip::address::from_string( "127.0.0.1" ), 6545 ); boost::system::error_code ec; sock.connect( ep, ec ); if ( ec ) { cout << "Connect error, code: " << ec.value( ) << ", We will exit." << endl; return ec.value( ); } else { char buf[1024] = "Hello world!"; sock.async_write_some( buffer( buf ), writeHandler ); sock.close( ); } return service.run( ); }

    下面是服务端的代码:

    #include <iostream> #include <boost/asio.hpp> #include <boost/proto/detail/ignore_unused.hpp> using namespace std; using namespace boost::asio; using namespace boost::system; using namespace boost::proto::detail;// 提供ignore_unused方法 void acceptHandle( const boost::system::error_code& code ) { cout << "Accepted." << endl; } int main(int argc, char *argv[]) { ignore_unused( argc ); ignore_unused( argv ); io_service service; ip::tcp::endpoint ep( ip::address::from_string( "127.0.0.1" ), 6545 ); boost::system::error_code ec; ip::tcp::socket sock( service ); ip::tcp::acceptor acceptor( service, ep ); acceptor.async_accept( sock, acceptHandle ); if ( ec ) { cout << "There is an error in server. code: " << ec.value( ) << endl; } return service.run( );// 阻塞运行 }

    运行结果是这样的:
    78448d7b-b3ae-42fc-9e2e-4dd2fbdac2c2-image.png

    我对boost.asio中几个概念的理解:

    io_service,这就是一个类似事件循环的东西,它为io设备提供服务,故名。不管是套接字、文件还是串口设备,都要使用它的服务。它的run()函数相当于启动了一个事件循环。一旦有消息了,即进行响应。这也是实现异步编程的重要基础。 socket,这个类则是套接字,可以处理TCP或者是UDP请求。有同步以及异步的处理方式,也有带异常以及不带异常的处理方式。 acceptor,接收器,仅仅是服务端使用。相当于其余框架中的listener,作接收用的。

    比较浅显,如果有不当之处,敬请指正。

    read more
  • 843143141.jpg
    闲下来了,我又开始大规模地学习了。
    最近开始学习内存模型和无锁结构。因为这个是和操作系统密切相关的,懂得这些对于编写C++服务端应用程序
    有着非常好的帮助。之前我对内存模型以及无锁结构几乎没有什么了解,我就询问群里的大佬看看有没有可以参考的资料。
    大佬很高兴,并且推荐了我一本名为《Memory Model》的电子书。这本电子书虽然页数不多,但是从起源到发展,
    从源码到汇编,都给我们详细地介绍了。看了一遍,不是非常理解,但是依然尝试将自己的理解写下来,以便日后翻阅。
    首先因为多核处理器成为主流,多线程的程序已经非常常见,因此我们不可避免地要处理多线程程序的同步问题。
    然后,因为编译器默认都对源码进行了优化,在单核处理器中这通常不是什么问题,但是在多核处理器中,就会因为编译器
    对其进行了乱序处理而导致程序出现问题。由此深入地探讨内存模型。
    内存模型主要分为:
    载-载 顺序(load-load order)
    载-存 顺序(load-store order)
    存-载 顺序(store-load order)
    存-存 顺序(store-store order)
    依赖载入顺序(dependent loads order)

    通过内存栅栏(memory barrier)能够避免编译器对指令的乱序。Linux中有

    READ_ONCE( x, value ) WRITE_ONCE( x )

    避免这些读写被编译器乱序或者是优化掉。

    这里谈到volatile关键字。在另外一篇博客上说,volatile具有“易变性、不可优化性、顺序性”。简单说,由于
    被volatile声明的变量,指令须从内存读取,并且不能被编译器乱序以及优化。在Java(语言扩展)和MSVC(系统兼容)上,
    还附带了Accquire()和Release()语义,因此可部分用于多线程环境。但多数情况下,还是慎用volatile,
    因为不同架构的处理器,它的内存模型是千变万化的,不能一而概之。

    至于C++11,它提供了std::atomic<T>这个模板类,相当于提供了很多方式来实现不同内存模型的原子操作。
    它的load()和store()方法,第二个参数有以下几个选项:

    std::memory_order_relaxed std::memory_order_seq_cst std::memory_order_acq_rel std::memory_order_acquire std::memory_order_release std::memory_order_consume

    我们最常用来实现RCpc(Release Consistency、Processor Consistency)是使用

    std::memory_order_acquire std::memory_order_release

    这两对。

    作为例子,在实现自旋锁时使用std::atomic<T>是这样的:

    struct SpinLock2 { void lock( ) { for ( ; ; ) { while ( lock_.load( std::memory_order_relaxed ) ); if ( !lock_.exchange( true, std::memory_order_acquire ) ) break; } } void unlock( ) { lock_.store( false, std::memory_order_release ); } std::atomic<bool> lock_ = { false }; };

    read more
  • 113.jpg
    1、什么是lambda表达式,什么是闭包?
    lambda表达式即lambda函数,也就是匿名函数。

    lambda表达式在C++中包含了
    []表示捕获
    ()是函数的参数,需要指定类型
    ->type是返回的类型,可以省略,如果编译器无法推出类型的话可以强制编写
    {}是函数体。

    lambda可以被声明为mutable的,作用是将捕获的内容进行改变。
    闭包是函数的定义以及定义函数时提供的环境,总称为闭包。lambda函数也是一种闭包。
    lambda本身是匿名函数,而捕获语句则是提供了定义函数时提供的环境。

    2、什么是右值引用?
    右值引用相对与左值引用而言的。左值即=运算符左边的变量,右值是=运算符右边的常量或变量。由此可以看出,
    右值引用指的是对常量或变量的引用。它的用途包含了移动语义和完美转发。
    移动语义就是弥补了C++历史在处理变量传递时丢失的一种语义。它和值传递、引用传递一样,是变量传递的方式之一。
    如果没有移动语义,为了将一个类的实例传递给另外一个实例,就需要额外地进行构造、赋值、销毁的操作。
    对于一些比较复杂的变量,的确是非常耗时并且消耗大的操作。(浪费指令时间、浪费内存)

    对于这样的函数返回:
    vector<string> str_split(const string& s) {
    vector<string> v;
    // ...
    return v; // v是左值,但优先移动,不支持移动时仍可复制。
    }

    标准要求先调用移动构造函数,如果不符合那么再调用拷贝构造函数。所以可以轻松地写出这种写法而不必担心效率问题。
    同时,现代编译器都会对返回值进行优化,成为RVO以及NRVO。所以不用太担心会多调用构造析构函数。

    对于完美转发,C++对于引用的转发有规则。传统的C++是无法对引用进行再引用的。但是现代的C++放宽了它的使用范围。
    只有右引用右值的时候,才会产生右引用。这也称为引用折叠。

    3、auto关键字的作用是什么?
    auto关键字为的是能够让编译器自动推导类型。自C++98之后,编译器对类型的推导变得越来越智能了。
    而我们在编写复杂代码的时候,冗长的类型不仅容易出错,有时也不容易人工推导出类型。
    因此auto可以简化我们的任务量,让类型的推导交给编译器完成。
    除了auto外,我们还可以使用decltype()来让编译器推导类型。

    read more
  • 我感觉比起《Physically Based Rendering Technique》,还是《Ray Tracing in a Weekend》更容易上手,因为慢慢地能够做出一个渲染效果,这个是有成就感的。🎓

    read more

关注我们

微博
QQ群