0%

2020年4月28日 上午11:07

这篇文章涉及到《C++ primer》中分散的多节内容:

  • P32 类型转换
    • 含有无符号数类型的表达式
  • P141 类型转换
    • 算数转换
    • 隐式类型转换
    • 显示转换
  • P263 隐式的类类型转换
  • P514 重载、类型转换与运算符
    • 类型转换运算符
    • 避免有二义性的类型转换

内存层次:

  • 算数类型转换主要涉及到内存,这么做的原因我认为和内存对齐有关,当我们做了统一的整型提升、转换成浮点类型,统一的类型可以更方便的做到内存对齐
  • 这里面最坑的是无符号类型的转换,分两种类型:具体内容在P32 + P141

语法层次:

  • 我认为关于指针的转换都是语法层次的,指针指向是地址,地址就是地址,只不过在语法层次我们有区分了指向不同类型的指针
  • 转换成常量

包装层次:

  • 类类型的转换
    • 构造方法的自动转换,explicit
    • 类型转换运算符的自动转换,explicit

2020年4月28日 下午2:17
不能在析构函数里面抛出异常_C/C++_tianmo2010的专栏-CSDN博客

简短的总结为什么不行:

1. 析构函数从语法角度来说,是可以写抛出异常的代码的,也就是编译器阶段并不会直接限制。
2. 本质就是进行了栈展开,造成了无限递归: [栈展开:C++自带功能](bear://x-callback-url/open-note?id=5D64735D-3D6D-4A86-927F-AA218BA0C8F4-14547-000141D5AE58C07F)
    1. 试想!如果对象出了异常,现在异常处理模块为了维护系统对象数据的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,**那么请问由谁来保证这个对象的资源释放呢?而且这新出现的异常又由谁来处理呢?**不要忘记前面的一个异常目前都还没有处理结束,因此这就陷入了一个矛盾之中,或者说无限的递归嵌套之中。

析构函数中抛出异常时概括性总结

  • C++中析构函数的执行不应该抛出异常;
  • 假如析构函数中抛出了异常,那么你的系统将变得非常危险,也许很长时间什么错误也不会发生;但也许你的系统有时就会莫名奇妙地崩溃而退出了,而且什么迹象也没有,崩得你满地找牙也很难发现问题究竟出现在什么地方;
  • 当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外(这招简直是绝杀!呵呵!);
  • 主人公阿愚吐血地提醒朋友们,一定要切记上面这几条总结,析构函数中抛出异常导致程序不明原因的崩溃是许多系统的致命内伤!

从C++异常处理的角度解释析构函数的执行不应该抛出异常

  • C++异常处理模型是为C++语言量身设计的,更进一步的说,它实际上也是为C++语言中面向对象而服务的,我们在前面的文章中多次不厌其烦的声明到,C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。好的,既然如此!那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源,这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。
  • 不知大家是否明白了这段话所蕴含的真正内在涵义没有,那就是上面的论述C++异常处理模型它其实是有一个前提假设——析构函数中是不应该再有异常抛出的。
  • 试想!如果对象出了异常,现在异常处理模块为了维护系统对象数据的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,那么请问由谁来保证这个对象的资源释放呢?而且这新出现的异常又由谁来处理呢?不要忘记前面的一个异常目前都还没有处理结束,因此这就陷入了一个矛盾之中,或者说无限的递归嵌套之中。
  • 所以C++标准就做出了这种假设,当然这种假设也是完全合理的,在对象的构造过程中,或许由于系统资源有限而致使对象需要的资源无法得到满足,从而导致异常的出现,但析构函数完全是可以做得到避免异常的发生,毕竟你是在释放资源呀!

实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyTest_Base
{
public:
virtual ~ MyTest_Base ()
{
cout << "销毁一个MyTest_Base类型的对象"<< endl;
}
};

void main()
{
try
{
// 构造一个对象,当obj对象离开这个作用域时析构将会被执行
MyTest_Base obj;
}
catch(...)
{
cout << "unknow exception"<< endl;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class MyTest_Base
{
public:
virtual ~ MyTest_Base ()
{
cout << "开始准备销毁一个MyTest_Base类型的对象"<< endl;
// 注意:在析构函数中抛出了异常
throw std::exception("在析构函数中故意抛出一个异常,测试!");
}
void Func() throw()
{
throw std::exception("故意抛出一个异常,测试!");
}
void Other() {}
};

void main()
{
try
{
// 构造一个对象,当obj对象离开这个作用域时析构将会被执行
MyTest_Base obj;

obj.Other();
}
catch(std::exception e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << "unknow exception"<< endl;
}
}
/*
  程序运行的结果是:
  开始准备销毁一个MyTest_Base类型的对象
  在析构函数中故意抛出一个异常,测试!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void main()
{
try
{
// 构造一个对象,当obj对象离开这个作用域时析构将会被执行
MyTest_Base obj;

// 下面这条语句是新添加的
// 调用这个成员函数将抛出一个异常
obj.Func();
obj.Other();
}
catch(std::exception e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << "unknow exception"<< endl;
}
}

/*
程序在控制台上打印一条语句后就崩溃了(如果程序是debug版本,会显示一条程序将被终止的断言;如果是release版本,程序会被执行terminate()函数后退出)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyTest_Base
{
public:
virtual ~ MyTest_Base ()
{
cout << "开始准备销毁一个MyTest_Base类型的对象"<< endl;
// 一点小的改动。把异常完全封装在析构函数内部
try
{
// 注意:在析构函数中抛出了异常
throw std::exception("在析构函数中故意抛出一个异常,测试!");
}
catch(…) {}
}
void Func() throw()
{
throw std::exception("故意抛出一个异常,测试!");
}
void Other() {}
};

/*
  程序运行的结果如下:
  开始准备销毁一个MyTest_Base类型的对象
  故意抛出一个异常,测试!

资源(缓存/IO/变量/代码段)的获取、放入安全

#b计算机基础/c_计算机系统/b_linux系统/补充
2020年4月27日 下午11:35

注:这篇是从安全的角度去考虑的,另外还有从效率的角度去思考
效率这个这个角度我认为是一个tradeoff,计算机软件体系是一个从底层开始层层抽象发展的,这个的目的是为了上下层抽象的过程中简化编程的思维,但是对于数据的处理、拷贝、交互来说,应该是扁平的,这中间就会有矛盾,面对这样的矛盾,我们处理的方法就是让数据的交互来说尽可能的放在这个体系的底层,也就是内核层去完成,尽量不要用到用户态,这个就减少了没有实际效率意义的内核态与用户态之间的数据拷贝,一个专业的名次是0拷贝。

多线程核心知识的两个层面去理解:

  1. 本质讨论的都是资源的获取和放入
  2. 【资源本身】资源的两种保护形式-主动、被动:
  3. 【获取资源方】操作对象面对锁、等待、空之后的做法种类:
  4. 上面三点说明了如何处理,如果不处理会出现什么情况:
    • 为什么我们再多线程下需要使用锁?
      • 脏数据

我们可以根据场景进行自定义设计的地方:

  • 选择合适的锁的类型
    • 锁就是字面意思:锁住大门,不让你进。他有酸甜苦辣味,看你的需要选取。不同的锁的类型,体现你对资源的管理方式。
  • 从线程的角度,你希望线程遇到阻塞之后,如何行事呢?
    • 阻塞待唤醒、忙轮询、一遍轮询、不轮询直接获得操作对象等

2020年4月27日 下午6:45

C++(十三)— map的排序 - 深度机器学习 - 博客园

对有序map中的key排序

  • map这里指定less作为其默认比较函数(对象),就是默认按键值升序排列
  • 可以自定义,按照键值升序排列,注意加载
  • 按照自定义内容进行排序,比如字符串的长度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    #include<iostream>
    #include<algorithm>
    #include<stdio.h>
    #include <vector>
    #include<string>
    #include<map>
    #include <functional> // std::greater
    using namespace std;


    struct CmpByKeyLength {
    bool operator()(const string& k1, const string& k2)const {
    return k1.length() < k2.length();
    }
    };

    int main()
    {
    //1、map这里指定less作为其默认比较函数(对象),就是默认按键值升序排列
    // map<string, int> name_score_map;

    // 2、可以自定义,按照键值升序排列,注意加载
    // #include <functional> // std::greater
    // map<string, int, greater<string>> name_score_map;

    //3、按照自定义内容进行排序,比如字符串的长度
    map<string, int, CmpByKeyLength> name_score_map;

    name_score_map["LiMin"] = 90;
    name_score_map["ZiLinMi"] = 79;
    name_score_map["BoB"] = 92;
    name_score_map.insert(make_pair("Bing", 99));
    name_score_map.insert(make_pair("Albert", 86));

    map<string, int>::iterator iter;
    for ( iter = name_score_map.begin();iter != name_score_map.end();++iter) {
    cout << (*iter).first << endl;
    }

    system("pause");
    return 0;
    }

对有序map中的value排序

  • 把map中的元素放到序列容器(如vector)中,再用sort进行排序。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    #include<iostream>
    #include<algorithm>
    #include<stdio.h>
    #include <vector>
    #include<string>
    #include<map>
    #include <functional> // std::greater
    using namespace std;


    bool cmp(const pair<string, int>& a, const pair<string, int>& b) {
    return a.second < b.second;
    }

    int main()
    {
    //1、map这里指定less作为其默认比较函数(对象),就是默认按键值升序排列
    map<string, int> name_score_map;
    name_score_map["LiMin"] = 90;
    name_score_map["ZiLinMi"] = 79;
    name_score_map["BoB"] = 92;
    name_score_map.insert(make_pair("Bing", 99));
    name_score_map.insert(make_pair("Albert", 86));

    //输出添加的内容
    map<string, int>::iterator iter;
    for (iter = name_score_map.begin(); iter != name_score_map.end(); ++iter) {
    cout << (*iter).first << endl;
    }
    cout << endl;

    // 将map中的内容转存到vector中
    vector<pair<string, int>> vec(name_score_map.begin(), name_score_map.end());
    //对线性的vector进行排序
    sort(vec.begin(), vec.end(), cmp);
    for (int i = 0; i < vec.size(); ++i)
    cout << vec[i].first << endl;

    system("pause");
    return 0;
    }

2020年4月26日 上午9:37

基本关键字:

  • C++这个框架提供给用户的功能接口。也需要并不像我们熟悉的开发框架,是通过给函数传参数进行功能的封装和调用,语言框架他是利用设计的语法 + 编译器的处理,最终达到的功能接口的作用。框架思想是理解C++的关键角度之一。
    附:
  • #define和inline 的区别 - cbwcwy - 博客园

类型转换:

  • 首先,需要说明的是类型和类别的区别
  • 另外,类型转换分为两个大类,编译器自动转换,手动关键字转换。自动类型转换对于不同的语法类型,编译器本身就定死了一些规则,当满足条件触发时,就会按既定的步骤进行类型转换。而手动类型转换,需要我们使用一些运算符,一般来说,如果在开发过程中需要使用手动类型转换才可以完成一些功能时,都是我们不合理的设计代码引起的,这时我们需要去修改我们原有的设计,而不是去使用手动类型转换

运行时类型识别:

  • 只有一种使用场景:我们有基类的指针,需要在继承类中调用基类中没有标志位virtual的成员方法。
    • 编译器也给我们留了对应的功能接口:dynamic_cast运算符。但是一般来说,出现这样的需求时,我们可以通过合理的重新设计代码逻辑,从而不需要这样的接口。
  • 关于typeid运算符
    • 在编译阶段会自动插入一段类型检查代码,这些代码会在运行时进行执行,从而判断对象的类型。这种运算符在python中其实很多很多
    • 从这个例子中我们就可以看出C++虽然是一门静态类型的语言,类型是在编译期进行确定,但是也可以支持动态类型语言在运行期做类型的判定和检查

OOP面向对象编程

  • 这个框架功能的设计就比上面提到的通过关键字、运算符来提供接口实现的功能要复杂。关键字、运算符这些语法点,可以认为在编译器代码中会使用一些映射好的代码进行替换,这些关键字、运算符其实更多的是为了帮助使用者将常用的功能封装好,这样就减少了重复劳动,和使用定义函数、模板、类进行代码复用的思想是一致的,只不过这些关键字、运算符更关注与系统功能的复用,而使用者更关心与逻辑功能的复用。
  • 相反,OOP就不是简简单单的代码替换这么简单,OOP可以把它理解成C++这门框架中的一个子系统
    • 既然提到了系统这个词,我们回想操作系统知识,那么这其中一定包括一定程度的层次抽象,就像linux中包含用户态和内核态一样,当用户态调用了一个系统调用以后,我们需要深入的了解这个系统调用在内核态的工作原理是什么。同样,在C++中我们需要知道OOP这个子系统到底是如何完成功能的
  • 对于编译器来说,资源的单位:变量 ,函数
    • OOP是对变量和函数这两个资源单位的逻辑上的组合,注意这里强调的是逻辑,这是为了更加贴近人类文明中的概念,减少语言框架和使用者之间的逻辑gap,让使用者能够更好的对现实进行抽象。而,对于编译器本身来说,他不需要这些人类社会的抽象,他眼中认为他自己所在的社会就是一个资源管理的系统,资源的单位时变量和函数,而即使是编译器,他也需要通过变量名和函数名和资源的映射表,来找到这些资源。
  • 虽然对于编译器来说,资源的单位是变量和函数,但是由于C++支持了OOP编程,那么资源单元之间就可以定义新的组合关系,并在组合关系之上设计新的功能,比如说:定义继承的访问控制、定义类的作用域、虚函数实现动态绑定机制,抽象基类等等新的功能。这些功能的本质依然是操作资源的基本单位,但是新添加了一些新的设计,花样也更多。
    • 这里需要特别强调关键的一点是:对于C++框架的使用者来说,视角是关注现实事物到类之间关系的转化抽象定义,但是对于C++框架本身的编译器来说,它的视角是更多的是从真实存储(内存的各个段,cpu的各种寄存器)的角度来分析。
    • 在编译器视角中,语法层的继承关系变成了资源单位之间的访问关系,这这一点特别关键。从内存角度来说:派生类对象(所占的内存)中含有与其基类对应的部分,这是继承的关键,其中提到的“含有”就是编译器通过代码实现的逻辑功能,将不同的独立的单元产生逻辑上的语义。

重载运算

  • 重载运算是OOP系统中其中一个支持的功能,为什么重载运算可以单独成为一章节呢?
    • 这里面的原因我认为是计算机的核心功能是完成计算,那么语言框架、编译器处理支持代码(变量、函数)的管理以外,也应该直观的可以进行运算接口。
  • 让自定义的类有运算能力:
    • 在C++已有类型的运算不需要我们程序员关心是因为默认的编译器已经在通识的基础上写好了,并且大家对加法、减法等有统一的认识,因此可以直接写到编译器中,程序员不用管了。那么,OOP中的自定义类,这时我们就需要结合自己对业务功能的理解,定义符合业务特有逻辑的运算操作。
  • 另外,对于重载运算来说更多是编程实战best prictise
    • 哪些运算符不应该被重载
    • 选择作为成员或者非成员

模板泛型

  • 要理解模板的意义,我们需要和define进行对比,我们知道define他只干一件事:在预处理期进行直接、简单的替换操作。怎么评价define呢?往好里说,define功能简单但是也很明确,人们很好理解KISS原则,往不好里说,功能也谈简单了,感觉一点高级功能都没有,连基本的类型、语法检查都没有。
  • 与difine相比,模板看上去好像也是简单的文本替换,但是在编译器中加入了一些高级功能:
    • 函数模板:可以进行自动的类型推断
    • 支持所谓的模板重载:SFINAE机制
    • 支持可变参数:编译器中加入了参数包的处理代码
    • 模板的特例化:对比与OOP中的类继承关系,特例化是另一种意义上的“关系描述方式”

2020年4月26日 下午3:50

复习: 2020年5月16日 下午5:27

  1. C++的学习最关键是是要站在需求的角度,理解C++为什么要设计这个关键字,它是为了遇到了什么样的需求导致了它想设计这样一个语法
  2. 引用就是这样:
    1. 首先记住一句话:引用的本质是为了解决拷贝问题,提高效率和空间
    2. 引用分为左值引用和右值引用,也同样是为了解决多余拷贝的问题
  3. 至于,下面的具体代码分析,我觉得其实一个综合能力的考察,其中涉及到
    1. C++内存模型:不同的对象,对象生命周期,对象名字的作用域是不同的
    2. 对函数返回值的理解
      1. 我自己的一个技巧是:把函数当成一个变量的定义来理解,这样就能把”函数“去掉
      2. eg:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        float &fn2(float r){ //&说明返回的是temp的引用,换句话说就是返回temp本身
        temp=r*r*3.14;
        return temp;
        }

        当出现我们把函数的返回值赋值给其他变量时
        将函数float &fn2(float r){} 抽象为 float & X
        注意:这里的X,当做是一个匿名变量,类比匿名函数lambda就能理解

        # case:1
        float &fn2(float r){
        temp=r*r*3.14;
        return temp;
        }
        float c = fn2(5.0);
        ||
        转换
        \||/
        float& x = temp;// 步骤1:将函数转为为变量理解
        float c = x;// 步骤2:函数返回 fn2(5.0) 转换为 x
        //步骤3:步骤一的变量,是临时变量,不论是不是引用类型,都会消失
        注:这里解释x这个引用变量内存被消失,也不影响,因为没有对象指向x,步骤2中x指向的是temp,而不是临时变量x!


        # case:2
        float fn1(float r){
        temp=r*r*3.14;
        return temp;
        }
        float &b=fn1(5.0)//[Error]
        ||
        转换
        \||/
        float x = temp;
        float &c = x;
        注:case2与case1最大的区别是:c这个引用指向是是临时变量x!而不是temp,所以会报错。

具体分析:

C/C++ 引用作为函数的返回值_C/C++_Jeff_的博客-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//代码来源:RUNOOB
#include<iostream>
using namespace std;
float temp;
float fn1(float r){
temp=r*r*3.14;
return temp;
}
float &fn2(float r){ //&说明返回的是temp的引用,换句话说就是返回temp本身
temp=r*r*3.14;
return temp;
}
int main(){
float a=fn1(5.0); //case 1:返回值
//float &b=fn1(5.0); //case 2:用函数的返回值作为引用的初始化值 [Error] invalid initialization of non-const reference of type 'float&' from an rvalue of type 'float'
//(有些编译器可以成功编译该语句,但会给出一个warning)
float c=fn2(5.0);//case 3:返回引用
float &d=fn2(5.0);//case 4:用函数返回的引用作为新引用的初始化值
cout<<a<<endl;//78.5
//cout<<b<<endl;//78.5
cout<<c<<endl;//78.5
cout<<d<<endl;//78.5
return 0;
}
  1. case 1:用返回值方式调用函数(如下图,图片来源:伯乐在线):
    1. 返回全局变量temp的值时,C++会在内存中创建临时变量并将temp的值拷贝给该临时变量。当返回到主函数main后,赋值语句a=fn1(5.0)会把临时变量的值再拷贝给变量a
  2. case 2:用函数的返回值初始化引用的方式调用函数(如下图,图片来源:伯乐在线)
    1. 这种情况下,函数fn1()是以值方式返回到,返回时,首先拷贝temp的值给临时变量。返回到主函数后,用临时变量来初始化引用变量b,使得b成为该临时变量到的别名。由于临时变量的作用域短暂(在C++标准中,临时变量或对象的生命周期在一个完整的语句表达式结束后便宣告结束,也就是在语句float &b=fn1(5.0);之后) ,所以b面临无效的危险,很有可能以后的值是个无法确定的值。
    2. 如果真的希望用函数的返回值来初始化一个引用,应当先创建一个变量,将函数的返回值赋给这个变量,然后再用该变量来初始化引用:
      1
      2
      int x=fn1(5.0);
      int &b=x;
  3. case 3:用返回引用的方式调用函数(如下图,图片来源:伯乐在线)
    1. 这种情况下,函数fn2()的返回值不产生副本,而是直接将变量temp返回给主函数,即主函数的赋值语句中的左值是直接从变量temp中拷贝而来(也就是说c只是变量temp的一个拷贝而非别名) ,这样就避免了临时变量的产生。尤其当变量temp是一个用户自定义的类的对象时,这样还避免了调用类中的拷贝构造函数在内存中创建临时对象的过程,提高了程序的时间和空间的使用效率。
  4. case 4:用函数返回的引用作为新引用的初始化值的方式来调用函数(如下图,图片来源:伯乐在线)
    1. 这种情况下,函数fn2()的返回值不产生副本,而是直接将变量temp返回给主函数。在主函数中,一个引用声明d用该返回值初始化,也就是说此时d成为变量temp的别名。由于temp是全局变量,所以在d的有效期内temp始终保持有效,故这种做法是安全的。

不要返回局部变量

  1. 不能返回局部变量的引用。如上面的例子,如果temp是局部变量,那么它会在函数返回后被销毁,此时对temp的引用就会成为“无所指”的引用,程序会进入未知状态。
  2. 不能返回函数内部通过new分配的内存的引用。虽然不存在局部变量的被动销毁问题,但如果被返回的函数的引用只是作为一个临时变量出现,而没有将其赋值给一个实际的变量,那么就可能造成这个引用所指向的空间(有new分配)无法释放的情况(由于没有具体的变量名,故无法用delete手动释放该内存),从而造成内存泄漏。因此应当避免这种情况的发生
  3. 当返回类成员的引用时,最好是const引用。这样可以避免在无意的情况下破坏该类的成员。
    1. 类的成员函数返回引用所引发的问题_AderStep-CSDN博客
    2. 如果我们返回类成员的引用,那么相当于给了用户写这个赶回值的接口,通过这个返回的引用我们可以直接修改成员变量的值,即使是private
      1. base.GetX( ) = 10;
    3. 这时候,我们就需要使用const int& GetX( )来定义成员函数,这样只能读,不能写,这样的语句就会报错base.GetX( ) = 10;
  4. 可以用函数返回的引用作为赋值表达式中的左值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include<iostream>
    using namespace std;
    int value[10];
    int error=-1;
    int &func(int n){
    if(n>=0&&n<=9)
    return value[n];//返回的引用所绑定的变量一定是全局变量,不能是函数中定义的局部变量
    else
    return error;
    }

    int main(){
    func(0)=10;
    func(4)=12;
    cout<<value[0]<<endl;
    cout<<value[4]<<endl;
    return 0;
    }

2020年4月20日 下午10:24

关键点:

  1. 定义:我们要找的值在[left,right]闭区间中。在循环的过程中我们要维护这个定义,知道left = right,就可以找到我们target
    1. 注意:这里的[left,right]区间要包括所有的情况,有些题目初始化right = nums.size() 而不是nums.size() - 1就是这个原因
  2. 在循环的过程中需要维护两个条件:
    1. 在[left,right]闭区间的包含target的定义
    2. 并且,每次循环保证[left,right]区间在缩小,否则会造成死循环
      1. 这里就需要考虑上取整 or 下取整
        1. 上取整 mid = right + (left - right) / 2
        2. 下取整 mid = right + (left - right - 1) / 2
          1. mid = (left + right) / 2
      2. 经验结论:
        1. left = mid or right = mid - 1 => 上取整
        2. left = mid + 1 or right = mid => 下取整

例题:

LCP 08. 剧情触发时间
35. 搜索插入位置

2020年4月16日 上午9:43

设计模式六大原则 - 开闭原则 | 李天炜

总结:

  1. 开闭原则是对扩展开放,对修改关闭。
  2. 开闭原则的主旨是为了拥抱变化。
  3. 在六大原则中,开闭原则只是一个思想,没有具体实际操作方法。其他五大原则都是为了实现这个开闭思想的一些方法和工具
  4. 想要遵守开闭原则,就需要一个设计合理的系统。可以说在做系统设计的时候就要考虑到未来的扩展和改变。

依赖倒置原则_百度百科

  1. 一个应用中的重要策略决定及业务模型正是在这些高层的模块中。也正是这些模型包含着应用的特性。但是,当这些模块依赖于低层模块时,低层模块的修改将会直接影响到它们,迫使它们也去改变。这种境况是荒谬的。应该是处于高层的模块去迫使那些低层的模块发生改变。应该是处于高层的模块优先于低层的模块。无论如何高层的模块也不应依赖于低层的模块
  2. 而且,我们想能够复用的是高层的模块。通过子程序库的形式,我们已经可以很好地复用低层的模块了。当高层的模块依赖于低层的模块时,这些高层模块就很难在不同的环境中复用。但是,当那些高层模块独立于低层模块时,它们就能很简单地被复用了。
  3. 这正是位于框架设计的最核心之处的原则
  4. 总结:
    1. 场景:一个应用中的重要策略决定及业务模型正是在这些高层的模块中
    2. 处理方法:无论如何高层的模块也不应依赖于低层的模块,相反高层模块应该独立于低层模块
    3. 效果:框架使用这种设计方式可以在将来很容易的添加子模块,并且对框架的使用者来说,使用方式一样的。

2020年4月15日 下午12:19

注:关于linux中iptable和route这些指令的学习一定要以了解linux系统内核处理过程的设计为前提,否则学习这些命令就是花架子。

  1. linux中关于网络的命令有很多:iptable route rule iostat 等等,我觉得他们很乱,我作为一个使用者我该如何理解,学习并可以做到按照场景需要来控制自己的网络数据包的走向呢?这其中最大的问题就是iptable和route之间的区别是什么?iptable是否和route是取代的关系,还是在一套流程中是合作的关系?
    1. linux - iptables vs route - Super User
    2. route is a command that displays, adds and deletes entries from the kernel’s TCP/IP routing table (aka “Forwarding Information Base”).
    3. iptables is a command that displays, adds, and deletes entries from Netfilter, the Linux kernel’s packet filtering and manipulating subsystem. It handles NAT.
    4. 这篇文章对route和iptable下了一个准确的定义,可以看出route操作的对象是table,而iptable对应的是linux中的一个内核模块Netfilter
  2. 这里首先我们需要了解linux中Netfilter这个内核模块他的逻辑是什么?
    1. iptables详解:图文并茂理解iptables
  3. 这里得再次提醒,一定要站在linux系统内核的角度去分析这个问题,iptable这命令只不过是一个用户接口罢了。从这个图中,我们就可以看到需要做到这几个功能:
    1. Netfilter模块可作为一个网关路由度器,转发其他主机发过来的数据包
    2. 可以接收网络中放给自己的数据包,然后通过内核,也即是Netfilter来传递给上层的协议栈,进而传递给对应的进程端口
    3. 当我们自己发送数据给别人时,也需要通过内核态的Netfilter,然后才可以发送到网络上
  4. 我们做到上面的基础功能是不够的,在这个数据流动的过程中,我们希望可以对信息进行处理:
    1. 对数据包处理的能力:
      1. 过滤功能
      2. 网络地址转换功能
      3. 拆解报文,做出修改,并重新封装的功能
      4. 连接追踪的功能
    2. 定位数据包发送位置的能力:
      1. 判断数据包下一站是去哪里
  5. 上面的对数据包处理的能力1-4功能其实对应了iptable中的四种表 filter表,nat表,mangle表,raw表,定位数据包发送位置的能力就是由route table来完成。也就是说其实iptable是更加偏向于提供对数据包处理的能力,而route则是提供所谓的定位发送的能力,也就是路由本身的意思。这里我们就区分开了iptable和route,他们是在一个大的数据处理流程中有不同的任务,他们之间是合作的关系,共同保证数据的正确传输,也就是Netfilter内核的正确运作。
  6. 这里有个Netfilter简要的处理流程:转发其他主机发过来的数据包
    1. Netfilter 处理网络包的先后顺序:接收网络包,先 DNAT,然后查路由策略,查路由策略指定的路由表做路由,然后 SNAT,再发出网络包。
      1. iptables - SNAT和DNAT的区别 - SegmentFault 思否
      2. SNAT: Source Network Address Translation,是修改网络包源ip地址的。
        1. 修改源ip地址的目的一般都是为了让这个包能再回到自己这里,所以在iptables中,SNAT是在出口,也即POSTROUTING链发挥作用。
      3. DNAT: Destination Network Address Translation,是修改网络包目的ip地址的。
        1. 修改目的ip地址的原因一般就是为了改变包发送的目的地,让包走出去,而不是留下来,所以在iptables中,DNAT是在入口,也即PREROUTING链中发挥作用,以便让包进入FORWARD表
  7. 最后,一个实际的例子来应用route,iptable完成:公司内网要求192.168.0.100 以内的使用 10.0.0.1 网关上网 (电信),其他IP使用 20.0.0.1 (网通)上网。
    1. 参考:
      1. 使用 ip route , ip rule , iptables 配置策略路由_shuti_新浪博客
      2. 理解 OpenStack 高可用(HA)(3):Neutron 分布式虚拟路由(Neutron Distributed Virtual Routing) - SammyLiu - 博客园
    2. 第一步:route
      1. 首先要在网关服务器上添加一个默认路由,当然这个指向是绝大多数的IP的出口网关:ip route add default gw 20.0.0.1
      2. 之后通过 ip route 添加一个路由表:ip route add table 3 via 10.0.0.1 dev ethX (ethx 是 10.0.0.1 所在的网卡, 3 是路由表的编号)
    3. 第二步:rule
      1. 之后添加 ip rule 规则:ip rule add fwmark 3 table 3 (fwmark 3 是标记,table 3 是路由表3 上边。 意思就是凡事标记了 3 的数据使用 table3 路由表)
    4. 第三部:iptables
      1. 之后使用 iptables 给相应的数据打上标记:iptables -A PREROUTING -t mangle -i eth0 -s 192.168.0.1 - 192.168.0.100 -j MARK —set-mark 3
    5. 总结:
      1. 因为 mangle 的处理是优先于 nat 和 fiter 表的,所以在数据包到达之后先打上标记,之后再通过 ip rule 规则,对应的数据包使用相应的路由表进行路由,最后读取路由表信息,将数据包送出网关。