【C++训练营】现代C++编程(隐藏)

一、面向对象的特性

1.1 编码规范

1.1.1 效率

  • 时间/空间:计算相关逻辑的时间复杂度和空间复杂度
  • 内存:考虑内存占用和cache命中率
  • 堆/栈:从生存周期、内存管理复杂性、对象大小等角度来考虑堆栈应用
  • 存储:考虑存储护具方式和读取方式,程序本身的结构来考虑

1.1.2 可读性

1.1.3 中文规范

  • 允许代码中写,不允许代码中写中文字符串,若是需要,统一使用ts工具转换为中文

1.1.4 健壮性

  • 错误检查、边界检查、资源管理、异常处理、测试和质量控制

我们来看一个错误:迭代器失效:

迭代器的概念:

       迭代器是一个可以对其执行类似于指针的操作(比如:解除引用(operator*()))和递增(operato++())的对象,我们可以将其理解为指针。但是他又不是我们所谓的普通的指针,我们可以称其为广义指针,我们可以通过sizeof来查看其所占的内存并不是4个字节,而是12个字节。

序列式容器迭代器失效:

       对于序列式容器,例如:vector,deque;由于序列式容器是组合式容器,当前元素的iterator被删除后,其后的所有元素的迭代器都会失效,这是因为vector、deque都是连续存储的一段空间,所以当对其进行erase操作时,其后的每一个元素都会向前移动一个位置。

for (it = vec.begin(); it != vec.end(); it++)
{
    if (*it>2)
        vec.erase(it);//此处会发生迭代器失效
}

       已经失效的迭代器不能进行 ++ 操作,所以程序中断了。不过vector的erase操作可以返回下一个有效的迭代器,所以只要我们每次执行删除操作的时候,将下一个有效迭代器返回就可以顺利执行后续操作了。

for (it = vec.begin(); it != vec.end(); )
{
   if (*it==3)
   {
       it = vec.erase(it);//更新迭代器it
   }
   it++;
}

       这样删除it指向的元素后,返回的是下一个元素的迭代器,这个迭代器是vector内存调整过后新的有效的迭代器。此时就可以进行正常的删除与访问操作了。

vector 迭代器失效问题总结:
  • 当执行erase方法时,指向删除节点的迭代器全部失效,指向删除节点之后的全部迭代器也失效了
  • 当进行push_back()方法后,end操作返回的迭代器肯定失效
  • 当插入(push_back)一个元素之后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效
  • 当插入(push_back)一个元素之后,如果空间未重新分配,指向插入位置之前的元素的迭代器仍然有效,但是指向插入位置之后元素的迭代器全部失效
deque 迭代器失效总结:
  • 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用全部失效,但是如果在首尾位置添加元素,迭代器会失效,但是指针和引用不会失效
  • 如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器全部失效
  • 在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效

关联式容器迭代器失效:

       对于关联式容器(比如map,set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前的iterator即可。这是因为map之类的容器,使用了红黑树来实现:插入、删除一个节点不会对其他节点造成影响。erase迭代器只是被删除元素的迭代器失效,但是返回值为void,所以要采用erase(iterator++) 的方式删除迭代器。

void mapTest()
{
    map<int, int>m;
    for (int i = 0; i < 10; i++)
    {
        m.insert(make_pair(i, i + 1));
    }
    map<int, int>::iterator it;
    for (it = m.begin(); it != m.end(); it++)
    {
        if ((it->first)>5)
            m.erase(it);
    }

}

我们只需稍稍修改即可:

void mapTest()
{
    map<int, int>m;
    for (int i = 0; i < 10; i++)
    {
        m.insert(make_pair(i, i + 1));
    }
    map<int, int>::iterator it;

    for (it = m.begin(); it != m.end(); )
    {
        if (it->first==5)
            m.erase(it++);
        it++;
    }
    for (it = m.begin(); it != m.end();it++)
    {
        cout << (*it).first << " ";
    }
    cout << endl;
}

此时迭代器也没有任何问题了

       这里主要来解释一下 erase(iterator++) 的执行过程:这句话分为三步:先将iterator传到erase里面,然后iterator自增,然后执行erase,所以iterator在失效前已经进行自增了。

       map是关联容器,以红黑树或者平衡二叉树组织数据,虽然删除了一个元素,整棵树也会调整,以符合红黑树或者二叉树的规范,但是单个节点在内存中的地址没有变化,变化的是各个节点之间的指向关系。

1.1.5 可扩展

  • 模块化、松耦合、可拔插架构

1.2 封装  继承  多态

1.2.1 封装和继承

封装:抽象模型,区分内外

继承:继承的语义是让一个子类型 is a 父类型,在C++当中主要是为了让派生类吸收基类的一般成员。一般来说,我们只需要使用 public 来进行继承,不能使用 private 继承,他违反了吸收成员这一语义。

重要的知识点:

  1. 继承不是没有代价,要永远提醒自己组合优于继承
  2. 请使用 public 继承,如果你写出 private 继承,请思考能否把被继承类替换为成员

1.3 多态

含义:同一接口,不同表现

       具体到 C++ 中有两种多态,通过函数重载或者函数模版实现的静态多态和通过虚函数实现的动态多态。

       其中静态多态我们不看,因为函数重载的实现不过是编译器给代码中看起来相同的函数内部取了一个新的名字,函数模版的实现也是编译器给每一种类型的函数都生成了一个特定版本。

1.3.1 静态多态

函数重载。需要在Linux和windows下的汇编代码中进行查看。

1.3.2 动态多态

我们先来看一看需求:

       我们想要实现的多态是这样的,加入我们的程序是一个想要获取文件的类型。所有的文件都有同一个函数 getSuffix(),当我们单独对图片文件执行该函数的时候会得到“.jpg”,对视频文件执行该函数的时候会得到“.mp4”。这个时候我们想用统一的类型文件,我们想要实现的是对于所有类型的文件,执行getSuffix函数他们都可以找到正确的类型后缀。这种行为就是我们讨论的多态。

       根据上述描述,我们可以发现继承和多态不能分别来看,将二者的功能结合我们就能得到面向对象最核心的能力:

  1. 继承基类以获取其内容,来解决代码重用的问题
  2. 声明某一个子类完全兼容于某一个基类
  3. 基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同

接下来,我们来看一看设计:

我们来看一段代码,我们来进行分析一下代码会输出什么?

class KFile
{
public: KFile(size_t  filesize ):m_filesize(filesize) {};
      //得到后缀
      std::string getSuffix() const{ 
          return "";
      }

private:
    size_t m_filesize;
};

class KImage : public KFile
{
public: KImage(size_t  filesize) :KFile(filesize) {};
    std::string getSuffix() const{
        return ".jpg";
    }
};

class KVideo : public KFile
{
public: KVideo(size_t  filesize) :KFile(filesize) {};
    std::string getSuffix() const{
        return ".mp4";
    }
};

int main()
{
    KImage* image = new KImage(10);
    KVideo* video = new KVideo(10);

    KFile* filePtr = nullptr;

    filePtr = image;
    auto suffix = filePtr->getSuffix();
    filePtr = video;
    suffix = filePtr->getSuffix();

    return 0;
}

       当时这里,我想到的是《Effective C++》中的避免遮掩继承而来的名称,但是我记错了,书上写的也是继承的基类中的成员函数是虚函数。之后,我又想了想,父类的对象指针指向的地址和子类的对象指针指向的地址是一样的,也就是说可以使用父类指针指向子类,但是不能使用子类指针指向父类,所以代码中的 “filePtr = image” 具有迷惑性,因为是同一个地址,所以指向的函数还是基类的成员函数,因为指针类型是父类。

如何进行解决呢??

解决思路一:

       我们先来一个简单的,因为在前面已经分析出,父类和子类指针指向的地址是一样的,所以我们可以进行强制类型转换,将父类转换为子类。实现代码如下:

auto suffix = dynamic_cast<Derived*>(filePrt)->getSuffix();
// 请注意,只能用于具有继承关系的类之间,且至少有一个虚函数

我们来复习一下四种强制类型转换

static_cast:
  • 用于基本的非多态类型转换,比如将int转换为float,或者将派生类指针转换为基类指针
  • 他不会进行运行时类型检查,因此不能用于涉及多态的情况
int i = 10;
float f = static_cast<float>(i);
dynamic_cast:
  • 用于处理多态性,特别是将基类指针或者引用转换为派生类指针或者引用
  • 他在运行时检查转换的有效性,如果转换失败,会返回nullptr(对于指针)或者抛出异常(对于引用)
  • 只能用于具有继承关系的类之间,且至少有一个虚函数
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b);
if(d) {
    // 成功转换
}
const_cast:
  • 用于移除或者添加const或volatile属性
  • 他不会改变表达式的值类型
const int* ci = new int(10);
int* modifiable = const_cast<int*>(ci);
reinterpret_cast:
  • 用于低级别的重新解释转换,比如将指针转换为足够大的整数类型,或者反过来
  • 他不进行任何类型的检查,因此使用时需要非常小心
int i = 65;
char* c = reinterpret_cast<char*>(&i);

注意:

       在使用这些转换的时候,应该尽可能使用static_cast和dynamic_cast,因为他们提供了类型安全。const_cast和reinterpret_cast应该谨慎使用,因为他们可能会绕过类型系统提供的保护。

解决思路二:

通过虚函数来实现,实现的代码如下:

class KFile
{
public: KFile(size_t  filesize ):m_filesize(filesize) {};
      //得到后缀
      virtual std::string getSuffix() const{  // 需要在父类成员函数变成虚函数
          return ""; 
      }

private:
    size_t m_filesize;
};

C++虚函数实现原理:

       C++中的虚函数是通过虚函数表和许指针共同实现的,其中虚函数表在编译时就已经确定了,通常被存放在静态数据段,而虚指针则是属于类中的一个成员,在类的内存布局的最前面。当调用虚函数的时候,会先访问虚指针拿到虚表地址,然后从虚表找到对应的虚函数并执行。

虚函数使用须知:

       从原理上看,虚函数只是多了一次寻址,但是实际应用中使用虚函数会比普通函数开销大很多,原因主要是:

  • 运行时,虚函数的使用场景往往是运行时,即编译时无法确定,这意味着调用函数的路径里会有很多编译器无法优化的跳转和分支。
  • 虚表多了一层寻址,这种间接寻址更容易导致缓存不命中,需要频繁访问内存,大大降低了访问效率
  • 虚函数往往无法进行内联优化,而普通成员函数可以以内联的方法提供程序运行效率。

注意:

       根据上述描述,非必要的情况我们不使用虚函数;但是反过来,只要是有用到虚函数的场景,比如运行时确定,必要的多态,那就毫不犹豫地使用虚函数,这套机制远比大多数自己实现的运行时多态要好的多。

解决思路三:

       通过函数指针来传递函数的地址,就是在子类初始化函数的时候,将子类中的成员函数地址传递给父类进行赋值。实现的代码如下:

class KFile
{
public:
    // 定义函数指针
    using GetSuffixFunc = std::string(*)();
      KFile(size_t  filesize, GetSuffixFunc getSuffixFunc) 
        : m_filesize(filesize)
        , m_getSuufixFunc(getSuffixFunc)
      {};
      //得到后缀
      std::string getSuffix() const{ 
          return "";
      }

private:
    size_t m_filesize;
    GetSuffixFunc m_getSuffixFunc;
};

class KImage : public KFile
{
public: KImage(size_t  filesize) :KFile(filesize, &getSuffix) {};
    std::string getSuffix() const{
        return ".jpg";
    }
};

class KVideo : public KFile
{
public: KVideo(size_t  filesize) :KFile(filesize, &getSuffix) {};
    std::string getSuffix() const{
        return ".mp4";
    }
};

二、现代C++编程

2.1 C++11新增用法

2.1.1 列表初始化

C++11为了统一进行赋值,所以创建出了列表初始化。我们可以来看一看下面的代码:

KTryClass kt1(520)
KTryClass kt2{520}
KTryClass kt3 = {520}

创建出匿名对象:

//创建一个匿名对象。
KPerson createObj()
{
    return { 1,"jack" };
}

2.1.2 using关键字

在老版的C++中using用于声明域名空间,在C++11中,using也提供了重新定义类型别名的方式。

C++98:
typedef  旧的类型名 新的类型;
//使用例子:
typedef  unsigned int uint_t;
typedef  int (*funp)(int ,int);

C++11
using 新的类型 = 旧的类型
//例子:
using  uint_t = int;
using funp = int (*)(int,int);

using 的优点:

  • using 代码结更加清晰,可读性较好
  • using 可以给模版类重定义,但是 typedef 无法使用此功能

2.1.3 nullptr

       nullptr是为了替换NULL而发明出来的,nullptr的类型为std::nullptr_t,但是NULL的类型为int,可以防止一些类型转换导致的问题。

我们可以来看一看下面的代码:

#include <iostream>

using namespace std;

void funTest(char* p)
{
    cout << "void fun(char *p)" << endl;
}

void funTest(int p)
{
    cout << "void fun(int p)" << endl;
}

int main()
{
                   
    funTest(NULL); 
    //funTest(nullptr);
    //funTest(200);  //想要调用void fun(int p)
    return 0;
}

       结果输出为:“void fun(int p)”,因为NULL在底层中的定义为整数0。下面让我们来看一看NULL的底层实现。

//NULL底层实现
#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

之后,让我们来模拟一下nullptr的底层实现(非官方)

const /*常量*/
class nullptr_t
{
public:
    template<class T>
    operator T*() const // 向任意类的非成员指针转换
    {
        return 0;
    }
    
    template<class C, class T>
    operator T C::*() const  // 向任意类型的成员指针转换
    {
        return 0;
    }
private:
    void operator&() const 
    {}  // 不可以进行取地址操作
}nullptr = {};

2.1.4 override(重要且常见)

       用于明确指示派生类中的成员函数是对基类中的虚函数的重写。如果没有对虚函数进行重写,会提示错误。一般子类写了override,就不用加virtual了。

class KTryBaseClass
{
public:
    virtual void displayData() 
    {
        cout << "Base Class displayData" << endl;
    }
};

class Child : public KTryBaseClass
{
public:
    //添加override后,必须重写父类的虚函数。【防止函数名错误】
    void displayData() override
    {
        cout << "child Class displayData" << endl;
    }
};

2.1.5 explicit(重要且常见)

发生了隐式类型转换的有风险的代码,在Qt中使用的多。

explicit 关键字通常使用于类的构造函数,以防止隐式类型转换。下面是explicit关键字的说明:

  1. 防止隐式转换:当你在类的构造函数前加上 explicit 关键字时,你阻止了编译器自动将一个类型转换为该类的实例。这种转换通常发生在函数调用或赋值操作中。

  2. 提高代码安全性:使用 explicit 可以提高代码的安全性,因为它防止了意外地将一个值赋给一个对象,这可能会导致错误或不可预测的行为。

  3. 需要显式构造:如果一个构造函数被声明为 explicit,那么你必须使用显式类型转换或直接初始化来创建该类的对象。

  4. 单参数构造函数explicit 关键字通常与只有一个参数的构造函数一起使用,因为这种情况下编译器可能会尝试进行隐式转换。

  5. 模板类:在模板类中,explicit 关键字也可以用于模板构造函数。

  6. 继承和 explicit:如果基类的构造函数被声明为 explicit,那么派生类不能有与基类构造函数参数相同的隐式转换。

  7. C++11 以后:在 C++11 及以后的版本中,explicit 也可以用于转换函数。

2.1.6 delete(重要且不常见)

       用于阻止特殊成员函数的生成或者禁用某些成员函数,可以通过delete来删除或者禁用特殊成员函数,例如禁止赋值构造函数或者移动赋值运算符的使用。单例模式中经常使用。

这样的代码我第一次见:

bool isLucky(int number);
if (isLucky('a')) ;
if (isLucky(true)) ;
if (isLucky(3.5));
//修改
bool isLucky(int number);       //原始版本
bool isLucky(char) = delete;    //拒绝char
bool isLucky(bool) = delete;    //拒绝bool
bool isLucky(double) = delete;  //拒绝float和double

2.1.7 default(不重要且不常见)

default用于生成默认的特殊函数实现:

class KTryClass
{
public:
    KTryClass() = default; //自动生成默认构造函数
    ~KTryClass() = default;

    KTryClass(int id) = delete;//禁止类型转换
    KTryClass(const KTryClass &obj) = delete; //禁止拷贝构造
private
    int m_id;
};

KTryClass obj1(10); //error
KTryClass obj2 = obj1;//error

2.1.8 final(不重要且不常见)

用于标记虚函数,表示该函数不能在派生类中再次被重写:

class KTryBaseClass
{
public:
    virtual void displayData() final
    {
        cout << "Base Class displayData" << endl;
    }
};

class Child : public KTryBaseClass
{
public:
    //添加final后,改虚函数不允许被重写,否则编译错误
    virtual void displayData() 
    {
        cout << "child Class displayData" << endl;
    }
};

2.1.9 constexpr(重要并且需要学会使用)

       在C++中,constexpr关键字用于声明一个值或者函数是编译时常量。这意味着他们的值必须在编译时已知,并且可以在编译时进行评估。constexpr 可以用于变量、函数、类构造函数等。

  1. 编译时常量constexpr 变量必须在编译时已知,并且它们的值不能在运行时改变。

  2. 常量表达式constexpr 变量的值必须是常量表达式,这意味着它们的值可以在编译时计算。

  3. 常量函数constexpr 函数必须返回一个常量表达式,并且其所有操作也必须是常量表达式。

  4. 模板参数constexpr 可以用于模板参数,允许在编译时确定模板参数的值。

  5. 类成员constexpr 可以用于类的构造函数,允许在编译时初始化对象。

  6. 内联变量:在C++17中,inlineconstexpr 可以一起使用,以创建可以在编译时评估的内联变量。

  7. 数组大小constexpr 可以用于数组的大小,允许在编译时确定数组的大小。

  8. 性能优化:使用 constexpr 可以提高程序的性能,因为编译器可以在编译时计算这些值,而不是在运行时。

  9. 类型限制constexpr 变量的类型必须是字面类型(如 intfloatdouble 等),或者是其他 constexpr 类型。

  10. 编译器支持constexpr 是C++11中引入的特性,因此需要编译器支持C++11或更高版本。

constexpr int max(int a, int b) {
    return a > b ? a : b;
}

int main() {
    constexpr int result = max(5, 10); // 在编译时计算
    return 0;
}

我们来看一看constexpr的作用:

常量函数计算

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
}

int main() {
    constexpr int result = factorial(5); 

    return 0;
}

构建静态数据表

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
}

int main() {
    constexpr int result = factorial(5); 

    return 0;
}

安全类型的枚举 

enum class DayOfWeek : int {
    Monday = 1,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
};

constexpr DayOfWeek today = DayOfWeek::Wednesday;

2.1.10 std::tuple

  std::tuple 是 C++ 标准库中的一个类模板,用于将多个值打包成一个单一的对象。它可以存储不同类型的值,并且可以通过索引或类型来访问这些值。std::tuple 提供了一种便捷的方式来组织和访问多个值,特别适用于函数返回多个值或者需要将多个值打包传递给函数的情况

#include <iostream>
#include <tuple>

int main() {
    // 创建一个std::tuple,存储一个整数、一个字符串和一个浮点数
    std::tuple<int, std::string, double> myTuple(42, "Hello", 3.14159);

    // 访问元素
    int intValue = std::get<0>(myTuple);
    std::string strValue = std::get<1>(myTuple);
    double doubleValue = std::get<2>(myTuple);

    return 0;
}

2.1.11 std::quoted

功能:用于给字符串加双引号

2.2 C++ 自动类型推导

2.2.1 auto

2.2.1.1 auto简介 和 基础用法

       auto 关键字,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型。auto 仅仅是一个占位符,在编译器期间,他会被真正的类型所替代。或者说,C++中的比那辆必须是有明确类型的,只是这个类型是由编译器自己推导出来的。使用auto关键字推导的变量必须马上初始化。

auto n = 10;
auto f  = 12.8;
auto p = &n;
auto url = “http://www.wps.cn”;
auto  *t = &n, m = 99;
auto val = static_cast<int>(2.0f);
vector<int> v;
auto iter = v.begin();  
auto ptr = make_unique<uintptr_t>();

第1行,10是一个整数,默认是 int 类型,所以推导出变量 n 的类型是 int。
第 2 行中,12.8 是一个小数,默认是 double 类型,所以推导出变量 f 的类型是 double。
第 3 行中,&n 的结果是一个 int* 类型的指针,所以推导出变量 p 的类型是 int*。
第 4 行中,由双引号""包围起来的字符串是 const char* 类型,所以推导出变量 url 的类型是
                 const char*,也即一个常量指针。
第 5行中,auto定义了多个连续的变量。 编译器会根据 auto *t 推导出 auto 为 int
2.2.1.2 auto 使用限制
  1. auto 不能在函数的参数中使用
  2. 作用于类的成员变量,auto不能作用于类的非静态成员变量(也就是没有static 关键字修饰的成员变量)中。静态中指的是用于 const 相关的操作。
  3. auto 关键字不能定义数组
  4. auto 不能用于模版

std::vector<bool> 的特殊性

位压缩:std::vector<bool> 是一个特例,他使用位(bit)而不是字节(byte)来存储布尔值。这是为了节省内存,但是他也意味着对 std::vector<bool> 的访问与常规向量有所不同。

代理对象:访问 std::vector<bool> 中的元素(例如 vec[3])返回一个临时的代理对象,而不是普通的 bool 值。这是一个特殊的代理类型,用于提供对底层位的访问。他实际上并不是 bool 的引用。代理对象允许你读取和写入单个位,而不需要复制整个 bool 值。

代理对象的特殊之处在于:

  1. 不是 bool 的引用:代理对象不是对 bool 值的引用,而是一个可以独立存在的对象。这意味着你可以将代理对象作为函数的参数传递,或者在多个位置存储对同一 bool 值的引用。

  2. 临时对象:当你访问 std::vector<bool> 的元素时,返回的是一个临时的代理对象,而不是一个持久的对象。这意味着你不能直接修改这个临时对象,但你可以通过代理对象来修改 std::vector<bool> 中对应的位。

  3. 位操作:代理对象允许你直接对位进行操作。例如,你可以使用 ===!= 等操作符来设置或检查 bool 值。

  4. 存储效率:由于 std::vector<bool> 存储的是位而不是完整的 bool 值,因此它在存储上更加高效。这对于大量 bool 值的场景非常有用。

2.2.2 decltype

decltype 是 C++11 新增的一个关键字,他和auto 的功能是一样的,都是用来在编译时期进行自动类型推导的。他是decltype type的缩写,主要功能是用来声明类型。

auto并不适合于所有自动推导的类型,并且auto的使用必须要初始化变量。而特殊的情况可能无法满足。declart type就可以解决这个问题。它可以在编译的时候推导出一个表达式

2.3 C++编程的三法则和五法则(重要)

2.3.1 三法则

三法则规定,如果一个类需要显式定义以下其中一项时,那么他必须显式定义这全部的三项:

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 析构函数

2.3.2 五法则

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 析构函数
  • 移动构造函数
  • 移动赋值运算符

       除了三法则中的三项外,我们还建议实现移动语义。与拷贝操作相比,移动操作更加高效,因为它们利用已分配的内存并避免不必要的拷贝操作。

       不实现移动语义通常不被视为错误。如果缺少移动语义,编译器通常会尽可能使用效率较低的复制操作。如果一个类不需要移动操作,我们可以轻松跳过这些操作。但是,使用它们会提高效率。

       因为用户显式定义三法则中的任意一项时,会阻止编译器隐式定义移动语义,导致失去优化的机会。

三、现代C++闭包方法

3.1 简介

在老版C++程序中,回调一般都是用函数制作的方式来做的(虚函数其实也是函数指针),因为函数无法携带状态,这个其实是非常不方便的,所以这种情况下,闭包出现了。

在现在C++编程中,闭包是指能够捕获其作用域内的变量,并在其定义的作用域之外使用这些变量的函数对象。

闭包函数实现的方式常用的有两种:仿函数和lambda表达式。

仿函数就是将类函数化,即实现类的()运算符,()运算符可以带参数,也可以不带参数。

3.2 仿函数

       仿函数就是将类函数化,即实现类的()运算符,()运算符可以带参数,也可以不带参数,感觉就是将()进行运算符重载。本质上就是将一个对象当做函数来实现,类中对()的重载,类中实现 operator()。

class KFunctorClass
{
public:
    void  operator() ()
    {
        std::cout << "测试无参数仿函数用法" << std::endl;
        return;
    }

    int  operator()(int a, int b)
    {
        return a > b ? 1 : 0;
    }
private:

};

3.3 lambda表达式

       lambda 表达式是C++11最重要也是最常用的特性之一,他是现代编程语言的一个特点,lambda表达式有如下的一些特点:

声明式的编程风格:就地匿名定义目标函数或者函数对象,不需要额外写一个命名函数或者函数对象。

简洁:避免了代码膨胀和功能分散,让开发更加高效

在需要的时间和地点实现功能闭包,使程序更加灵活

capture(捕获列表) : 捕获一定范围内的变量

  • [] - 不捕捉任何变量
  • [&] - 捕获外部作用域中所有变量,并作为引用在函数体内使用 (按引用捕获)
  • [=] - 捕获外部作用域中所有变量,并作为副本在函数体内使用 (按值捕获)

注:拷贝的副本在匿名函数体内部是只读的.类中的变量可以改,局部变量

无法修改。

  • [=, &foo] - 按值捕获外部作用域中所有变量,并按照引用捕获外部变量 foo
  • [bar] - 按值捕获 bar 变量,同时不捕获其他变量
  • [&bar] - 按引用捕获 bar 变量,同时不捕获其他变量
  • [this] - 捕获当前类中的 this 指针
    • 让 lambda 表达式拥有和当前类成员函数同样的访问权限
    • 如果已经使用了 & 或者 =, 默认添加此选项

params(参数列表) :和普通函数的参数列表一样,如果没有参数,参数列表选项可以省略。s

opt(选项) : 可省略

    • mutable: 可以修改按值传递进来的拷贝(注意只能修改拷贝,而不是值本身)
    • exception:指定函数抛出异常,如抛出整数类型的异常,可以使用throw();

ret(参返回值类型) : 在C++11中,lambda表达式的是通过返回值后置语法来定义的。

body : 函数体

四、git基础用法

4.1 git配置命令

// 在使用Git之前需要先配置用户名,Email
git config --global user.name "xxx"
git config --global user.email "xxx"
// 查看配置消息
git config --list
// 拷贝远程远端仓库到本地
git clone 地址

4.2 保存修改指令

// 查看工作区的状态
git status
// 添加文件到缓冲区中
git add <修改的文件>
// 提交修改
git commmit -m "文件描述"
// 工作区和暂存区中的代码一致

4.3 比较差异

// 查看提交历史
git log
git log --oneline

4.4 远程代码库的操作

// 查看远程代码库
git remote -v
// 添加删除远程代码库
git remote add/remove 网址
本地分支  --->  远程分支
git push
// 远程分支   ---->    本地分支
git pull 
// 向服务器询问远程仓库信息
git remote show origin

上一篇:C#对FTP服务器操作类


下一篇:nrm 使用详解