每天积累一个C++小技巧

每天积累一个C++小技巧

1 static 小技巧

  1. 在全局变量前,加上关键字static,该变量就被定义成为一个静态全局变量
  • 该变量在全局数据区分配内存

  • 未经初始化的静态全局变量会被程序自动初始化为0 (自动变量的值是随机的, 除非它被显式初始化)

  • 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的

  1. 在局部变量前,加上关键字static,该变量就被定义成为一个静态局部变量
  • 该变量在全局数据区分配内存
  • 静态局部变量在程序执行到该对象的声明处时被首次初始化,会被程序自动初始化为0;
  • 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域也随之结束。
// 静态局部变量
#include <iostream>
using namespace std;
void fn();
int main()
{
        fn();
        fn();
        fn();
        return 0;
}
void fn()
{
        static int n=10;
        cout<<n<<endl;
        n++;
}

// 如果把 n 声明为 static, 输出就是 10,11,12
// 如果不是 static, 输出就是 10,10,10
  1. 静态函数

在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其他文件使用。

优点:

  • 静态函数不能被其他文件所用
  • 其他文件中可以定义相同名字的函数,不会发生冲突
  1. 静态数据成员

在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。先举一个静态数据成员的例子。

#include <iostream.h>
class Myclass
{
public:
  Myclass(int a,int b,int c);
  void GetSum();
private:
  int a,b,c;
  static int Sum; 				  //声明静态数据成员
};
int Myclass::Sum=0;					//定义并初始化静态数据成员
Myclass::Myclass(int a,int b,int c)
{
  this->a=a;
  this->b=b;
  this->c=c;
  Sum+=a+b+c;
}
void Myclass::GetSum()
{
  cout<<"Sum="<<Sum<<endl;
}
void main()
{
  Myclass M(1,2,3);
  M.GetSum();
   Myclass N(4,5,6);
  N.GetSum();
  M.GetSum();
}

对于非静态成员来说,每个类对象都有自己的拷贝。而静态数据成员被当作是类得成员。无论这个类的对象被定义了多少个,静态数据成员子啊程序中也只有一份拷贝,由该类型的所有对象共享空间。也就是说,静态数据成员是该类的所有对象共有

  • 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义
  • 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它

静态数据成员有两个优势:

  • 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性
  • 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;
  1. 静态成员函数

​ 与静态数据成员一样,我们也可以创建一个静态成员函数,他为类的全部服务而不是为某一个类的具体对象服务。静态成员函数和静态数据成员一样,都是类的内部实现,属于类定义的一部分。

​ 静态成员函数无法访问类对象的非静态数据成员,也无法访问非静态成员函数,他只能调用其余的静态成员函数。

#include <iostream.h>
class Myclass
{
public:
  Myclass(int a,int b,int c);
  static void GetSum();/声明静态成员函数
private:
  int a,b,c;
   static int Sum;//声明静态数据成员
};
int Myclass::Sum=0;//定义并初始化静态数据成员
Myclass::Myclass(int a,int b,int c)
{
  this->a=a;
  this->b=b;
  this->c=c;
  Sum+=a+b+c; //非静态成员函数可以访问静态数据成员
}
void Myclass::GetSum() //静态成员函数的实现
{
  // cout<<a<<endl; //错误代码,a是非静态数据成员
  cout<<"Sum="<<Sum<<endl;
}
void main()
{
  Myclass M(1,2,3);
  M.GetSum();
  Myclass N(4,5,6);
  N.GetSum();
  Myclass::GetSum();
}

关于静态成员函数,可以总结为以下几点:
• 出现在类体外的函数定义不能指定关键字static;
• 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数
非静态成员函数可以任意地访问静态成员函数和静态数据成员
• 静态成员函数不能访问非静态成员函数和非静态数据成员;
• 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长
• 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:
<类名>::<静态成员函数名>(<参数表>)调用类的静态成员函数。

2 const 和 constexpr 的区别

// example 

#include <iostream>
#include <array>
using namespace std;

void dis_1(const int x){
    //错误,x是只读的变量
    array <int,x> myarr{1,2,3,4,5};
    cout << myarr[1] << endl;
}

void dis_2(){
    const int x = 5;
    array <int,x> myarr{1,2,3,4,5};
    cout << myarr[1] << endl;
}

int main()
{
   dis_1(5);
   dis_2();
}

// 可以看到 dis_1() 和 函数 dis_2() 函数中都包含一个 const int x,但dis_1() 函数中x无法完成初始化array容器的任务,而dis_2()函数中的x 却可以。

这是因为 dis_1() 函数中的 const int x 知识想强调x是一个只读的变量,其本质任然是变量,无法用来初始化array容器;而 dis_2() 函数中的 const int x,表明x是一个只读变量的同时,x还是一个值为5的常量,所以可以用来初始化array容器。

C++ 11 标准中,为了解决const 关键字的双重语义问题,保留了const表示只读的语义,而将”常量“的语义划分给了新添加的constexpr关键字。建议将const和constexpr的功能分开,即凡是表达 ”只读“语义的场景都使用了 const, 表达常量语义的场景都使用 constexpr。

在上面的实例程序中,dis_2() 函数中使用 const int x 是不规范的,应使用 constexpr 关键字。

#include <iostream>
#include <array>
using namespace std;
constexpr int sqr1(int arg){
    return arg*arg;
}
const int sqr2(int arg){
    return arg*arg;
}
int main()
{
    array<int,sqr1(10)> mylist1;//可以,因为sqr1是constexpr函数
    array<int,sqr2(10)> mylist1;//不可以,因为sqr2不是constexpr函数
    return 0;
}

3 virtual 函数和纯 virtual函数

首先强调一个概念:

  • 定义一个函数为 virtual ,不代表函数为不被实现的函数。
  • 定义为虚函数是为了 允许用基类的指针来调用子类的这个函数
  • 定义一个函数为纯virtual 函数,才代表函数没有被实现
  • 定义 纯 virtual 函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数
// example

class A
{
public:
    virtual void foo()
    {
        cout<<"A::foo() is called"<<endl;
    }
};
class B:public A
{
public:
    void foo()
    {
        cout<<"B::foo() is called"<<endl;
    }
};
int main(void)
{
    A *a = new B();
    a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
    return 0;
}

// 这个例子是虚函数的一个典型应用,通过这个例子,也许就对虚函数有了一些概念。虚就虚在所谓的“动态联编”上面,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被称为 “虚函数”

虚函数:只能借助于指针或者引用来达到多态的效果。虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。

在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。

纯虚函数:纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法

virtual void funtion1()=0

引入的原因:

  1. 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数
  2. 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理的

解决方法:

​ 为了解决上述的问题,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。

纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

4 强制类型转换的使用和区别

  1. static_cast:

    在C++语言中static_cast用于数据类型的强制转换,强制将一种数据类型转换为另一种数据类型。例如将整型数据转换为浮点型数据。

  2. const_cast:

    const 限定符通常被用来限定变量,用于表示该变量的值不能被修改。

    而const_cast 则正是用于强制去掉这种不能被修改的常数特性。但是需要特别注意的是const_cast不是用于去除变量的常量性,其去除常量性的对象必须为指针或引用

  3. reinterpret_cast:

    reinterpret_cast 主要有三种强制转换用途:改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转换为指针或引用类型

  4. dynamic_cast:

    • 其他三种都是编译时完成的,dynamic_cast 是运行时处理的,运行时要进行类型检查

    • 不能用于内置的基本数据类型的强制转换

    • dynamic_cast 转换如果成果的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。

    • 使用dynamic_cast 进行转换的,基类中一定要有虚函数,否则编译不通过

      ​ B中需要检查有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。

      ​ 这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中,只有定义了虚函数的类才有虚函数表。

    • 在类的转换时,在类层次间及逆行上行转换时,dynamic_cast 和 static_cast 的效果是一样的。在进行下行转换时,dynamic_cast 具有类型检查的功能,比static_cast 更安全

      • 向上转换,即子类指针指向父类指针;向下转换,即将父类指针转化子类指针。
      • 向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。

5 智能指针的使用和区别

智能指针:

为什么产生智能指针:

​ 在平时写代码的时候,时常会忘记释放自己动态开辟出来的资源,因此我们在处理相关逻辑的似乎后就会变得异常的谨慎,但是即使这样,有一些隐形的问题,还是会导致资源被泄漏了,让人防不胜防。有这样的困惑,就有大佬来解决这个问题。

啥是智能指针:

智能指针顾名思义就是自动化、智能的管理指针所指向的动态开辟资源的释放。

智能指针具备三要素:

RAII

像指针一样

拷贝和赋值

RAII:资源分配即初始化,定义一个类来封装资源的分配和释放,在构造函数完成分配和初始化,在析构函数完成资源的释放。

所谓的像指针一样,也就是智能指针可以进行指针的诸多操作。

拷贝和赋值时智能指针需要具备的功能

智能指针的原理:

管理类中的指针成员一般有两种方式:

  1. 采用值型的方式管理,每个类对象都保留一份指针指向的对象的拷贝

  2. 使用智能指针,从而实现指针指向的对象共享

    我们所使用的智能指针通常实现的技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指针指向同一对象。每次创建类的新对象时,初始化指针就将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数减至0,则删除基础对象)。

  3. 智能指针的进化史,auto_ptr(unique_ptr 具有移动拷贝构造函数), scoped_ptr, shared_ptr, weak_ptr

    scoped_ptr:

​ scoped_ptr 问世后,虽然可以防止内存泄漏。但是他不能拷贝赋值这就很难受,大佬们又想出了一个牛掰的智能指针 shared_ptr,它就很好的解决了scoped_ptr 不能实现的拷贝赋值。

​ shared_ptr 相对于其他智能指针的区别在于它维护了一个引用计数,用于检测当前对象多管理的指针是否还被其他的指针使用,构造函数,引用计数+1,析构函数,引用计数-1,判断是否为0,为0就释放这个指针和这个引用计数空间。

​ weak_ptr是为了解决shared_ptr 的引用计数问题,因此两个设为友元类。

在这里插入图片描述

6 CUDA之Stream的使用

​ 这篇短文主要是介绍cuda里面stream的概念。用到CUDA的程序一般需要处理海量的数据,内存带宽经常会成为主要的瓶颈。在stream的帮助下,CUDA程序可以有效地将内存读取和数值运算并行,从而提升数据的吞吐量。

​ 本文使用了一个非常native的例子:像素色彩空间转换,将一张7680*4320的8 bit RGB图像转成同样尺寸的8 bit YUV。计算非常简单,就是数据量非常大。转换公式直接照抄维基百科:

在这里插入图片描述

​ 由于GPU到CPU不能直接读取对方的内存,CUDA程序一般会有以下三个步骤:1)将数据从CPU内存转移到GPU内存,2)GPU进行运算并将结果保存在GPU内存,3)将结果从GPU内存拷贝到CPU内存。

​ 如果不做特别的处理,那么CUDA会默认只使用一个stream(default stream)。在这种情况下,刚刚提到的三个步骤就如菊花链般蛋痛的串联,必须等一步完成了才能进行下一步。是不是很别扭?

在这里插入图片描述

NVIDIA家的GPU有以下很不错的技能(不知道是不是独有):

  1. 数据拷贝和数值计算可以同时进行
  2. 两个方向的拷贝可以同时进行(GPU到CPU,和CPU到GPU),数据如同行使在双向快车道

但同时,这数据和计算并行也有一点合乎逻辑的限制:进行数值计算的kernel不能读写正在被拷贝的数据

Stream正是帮助我们实现以上两个并行的重要工具。基本的概念是:

  1. 将数据拆分成许多块,每一块交给一个stream来处理

  2. 每一个stream包含了三个步骤:

    • 将属于该stream的数据从CPU内存转移到GPU内存
    • GPU进行运算并将结果保存到GPU内存
    • 将该stream的结果从GPU内存拷贝到CPU内存
  3. 所有的stream被同时启动,由GPU的scheduler决定如何并行 同时启动

    在这样的骚操作下,假设我们把数据分成A,B两块,各由一个stream来处理。A的数值计算可以和B的数据传输同时进行,而A与B的数据传输也可以同时进行。由于第一个stream只用到了数据A,而第二个stream只用到了数据B,“进行数值计算的kernel不能读写正在拷贝的数据”这一限制并没有被违反。效果如下:

    在这里插入图片描述

实际上在Nsight 里面看上去是这样(这里用了8个stream):

在这里插入图片描述

stream 同步:

​ cuda包括两种类型的 host-device同步:显示同步和隐式同步;

​ 前面文章中介绍过的很多函数都是隐式同步的,比如 cudaMemcpy 函数,它会使得host应用程序在数据传输完成之前都会被阻塞。许多与内存相关的操作都带用隐式同步行为,比如:

  • host 上的固定内存分配,比如 cudaMallocHost
  • device上的内存分配,比如 cudaMalloc
  • device 上的内存初始化
  • 同一device 上两个地址之间的内存拷贝
  • 一级缓存、共享内存配置的修改

CUDA提供了几种显示同步的方法:

  • 使用cudaDeviceSynchronize 函数同步device

  • 使用cudaStreamSynchronize函数同步stream

  • 使用cudaEventSynchronize函数同步stream中的event

除此之外,CUDA还提供了下面的函数使用event及进行跨流同步:
cudaError_t cudaStreamWaitEvent(cudaStream_t stream, cudaEvent_t event);

该函数可以使指定的流等待指定的事件,该事件可能与同一个stream相关,也可能与不同的stream相关,如果是不同的stream那么这个函数就是执行跨stream同步功能:

在这里插入图片描述

7 CUDA之event的使用

一个cuda时间是cuda stream中的一个标记点,它可以用来检查正在执行的stream操作是否已经到达了该点。使用实践可以用来执行以下两个基本任务:

  • 同步stream的执行操作
  • 监控device的进展

CUDA提供了在stream中的任意点插入并查询事件完成情况的函数,只有当stream中先前的所有操作都执行结束后,记录在该流中的事件才会起作用。

声明和创建一个事件的方式如下:

cudaEvent_t enent;
cudaError_t cudaEventCreate(cudaEvent_t* event);

// 调用下面的函数可以销毁一个事件
cudaError_t cudaEventDestroy(cudaEvent_t event);

// 一个event可以使用如下函数进入cuda stream的操作队列中
cudaError_t cudaEventRecord(cudaEvent_t event, cudaStream_t stream = 0);

// 下面的函数会在host中阻塞式地等待一个事件完成
cudaError_t cudaEventSynchronize(cudaEvent_t event);

// 与stream类似的,也可以非阻塞式地去查询event的完成情况
cudaError_t cudaEventQuery(cudaEvent_t event);

// 如果想知道两个事件之间的操作所耗费的时间,可以调用
cudaError_t cudaEventElapsedTime(float* ms, cudaEvent_t start, cudaEvent_t stop);
// 这个函数以ms为单位返回开始和停止两个事件之间的运行时间,启动和停止事件不必在同一个cuda stream中

8 CUDA编程中阻塞和同步异步

阻塞和非阻塞:

从简单的开始,我们读取文件的模型举例。

在发起读取文件的请求时,应用层会调用系统内核的IO接口。

如果应用层调用的是阻塞型IO,那么在调用之后,应用层即刻被挂起,一直出于等待数据返回的状态,指导系统内核从磁盘读取完数据并返回给应用层,应用层才用获得的数据进行接下来的操作。

如果应用层调用的是非阻塞IO,那么调用之后,系统内核会立即返回(虽然还没有文件内容数据),应用层并不会被挂起,它可以做其他任意它想做的操作。(至于文件内容数据如何返回给应用层,这已经超过了阻塞和非阻塞的辨别范畴。)

这便是(脱离同步和异步来说之后)阻塞和非阻塞的区别。总结来说,是否是阻塞还是非阻塞,关注的是接口调用(发出请求)后等待数据返回时的状态。被挂起无法执行其他操作的则是阻塞型的,可以被立即【抽离】去完成其他【任务】的则是非阻塞型的

在这里插入图片描述

同步和异步:

​ 阻塞和非阻塞解决了应用层等待数据返回时的状态问题,那系统内核获取到的数据到底如何返回给应用层呢?这里不同类型的操作便体现的是同步和异步的区别。

​ 对于同步型的调用,应用层需要自己去向系统内核询问,如果数据还未读取完毕,那么此时读取文件的任务还未完成,应用层根据其阻塞和非阻塞的划分,或挂起或去做其他事情(所以同步和异步并不决定其等待数据返回时的状态);如果数据已经读取完毕,那此时系统内核将数据返回给应用层,应用层既可以用取得的数据做其他相关的事情。

​ 而对于异步型的调用,应用层无需主动向系统内核问询,在系统内核读取完文件数据之后,会主动通知应用层数据已经读取完毕,此时应用层可以接受系统内核返回过来的数据,再做其他事情。

​ 这便是(脱离阻塞和非阻塞来说之后)同步和异步的区别。也就是说,是否同步还是异步,关注的是任务完成时消息通知的方式。由调用方盲目主动问询的方式是同步调用。由调用方主动通知调用方任务已完成的方式是异步调用。

在这里插入图片描述

同步阻塞的例子:

​ 你上QQ问书店老板有没有《分布式系统》这本书,如果是同步阻塞,书店老板会说,你稍等,“我查一下”,然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。(你一直等的,等老板给你回复)

同步非阻塞的例子:

​ 而如果是同步非阻塞,不管老板有没有告诉你,你自己一边玩去了,当然你也会偶尔过几分钟问一下老板,书找到了吗?

异步机制的例子:

​ 书店老板会直接告诉你,我查一下啊,查好了,我直接回复你,然后直接那人就下线了(不返回结果)。然后查好了,他会主动联系你。

  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值